mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Imagemap sanitization
This commit is contained in:
+4
-2
@@ -89,13 +89,14 @@ dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.lru)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.paralithic)
|
||||
|
||||
// Dynamically Loaded
|
||||
slim(libs.paralithic)
|
||||
slim(libs.paperlib)
|
||||
slim(libs.adventure.api)
|
||||
slim(libs.adventure.minimessage)
|
||||
slim(libs.adventure.platform)
|
||||
slim(libs.adventure.serializer.legacy)
|
||||
slim(libs.adventure.serializer.plain)
|
||||
slim(libs.bstats)
|
||||
slim(libs.sentry)
|
||||
|
||||
@@ -310,6 +311,7 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
|
||||
// surfaces are only referenced from :adapters:bukkit:plugin, which is merged into the artifact
|
||||
// after this task runs. Both would be stripped as unreachable. Excluding them from minimization
|
||||
// leaves it reaching ~25KB of Gson internals, which is not worth a mode that fails silently.
|
||||
relocate('com.dfsek.paralithic', "${lib}.paralithic")
|
||||
relocate('io.github.slimjar', "${lib}.slimjar")
|
||||
exclude('modules/loader-agent.isolated-jar')
|
||||
exclude(supersededVolmLibPackages)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOrigin;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapRotation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisImageMapUnknownColor;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
|
||||
import java.awt.geom.Line2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class ImageMapStudioModel {
|
||||
private static final Pattern COLOR = Pattern.compile("#[0-9a-fA-F]{6}");
|
||||
private static final Pattern KEY_CHARACTER = Pattern.compile("[^a-z0-9/._-]");
|
||||
|
||||
private ImageMapStudioModel() {
|
||||
}
|
||||
|
||||
public static SourceMetadata inspect(Path path, BufferedImage image, String format) {
|
||||
return inspect(path, image, format, "not inspected", 1D, 1D);
|
||||
}
|
||||
|
||||
public static SourceMetadata inspect(
|
||||
Path path,
|
||||
BufferedImage image,
|
||||
String format,
|
||||
String colorProfile,
|
||||
double minimumAlpha,
|
||||
double maximumAlpha
|
||||
) {
|
||||
IrisImage irisImage = new IrisImage(image, format);
|
||||
return new SourceMetadata(
|
||||
path.toAbsolutePath().normalize(),
|
||||
irisImage.getFormat(),
|
||||
irisImage.getWidth(),
|
||||
irisImage.getHeight(),
|
||||
(long) irisImage.getWidth() * irisImage.getHeight(),
|
||||
irisImage.getColorMode().name(),
|
||||
irisImage.getColorComponentCount(),
|
||||
irisImage.getChannelCount(),
|
||||
irisImage.getBitDepth(),
|
||||
irisImage.hasAlpha(),
|
||||
minimumAlpha,
|
||||
maximumAlpha,
|
||||
colorProfile
|
||||
);
|
||||
}
|
||||
|
||||
public static KMap<String, String> legend(List<LegendRow> rows) {
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
for (LegendRow row : rows) {
|
||||
String color = row.color() == null ? "" : row.color().trim().toUpperCase(Locale.ROOT);
|
||||
String target = row.target() == null ? "" : row.target().trim();
|
||||
if (color.isEmpty() && target.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!COLOR.matcher(color).matches()) {
|
||||
throw new IllegalArgumentException("Legend color must use #RRGGBB syntax: " + color);
|
||||
}
|
||||
if (target.isEmpty()) {
|
||||
throw new IllegalArgumentException("Legend target must not be blank for " + color);
|
||||
}
|
||||
String previous = colors.put(color, target);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException("Legend contains duplicate color " + color);
|
||||
}
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
public static List<LegendRow> legendRows(IrisImageMap definition) {
|
||||
List<LegendRow> rows = new ArrayList<>();
|
||||
if (definition.getColors() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : definition.getColors().entrySet()) {
|
||||
rows.add(new LegendRow(entry.getKey().toUpperCase(Locale.ROOT), entry.getValue()));
|
||||
}
|
||||
rows.sort(Comparator.comparing(LegendRow::color));
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static IrisImageMap normalizeTypeSettings(IrisImageMap definition) {
|
||||
IrisImageMapType type = definition.getType();
|
||||
if (type == null) {
|
||||
return definition;
|
||||
}
|
||||
IrisImageMap defaults = new IrisImageMap();
|
||||
switch (type) {
|
||||
case GRAYSCALE_HEIGHT, RGB_HEIGHT -> definition
|
||||
.setThreshold(defaults.getThreshold())
|
||||
.setFalloff(defaults.getFalloff())
|
||||
.setColorTolerance(defaults.getColorTolerance())
|
||||
.setUnknownColor(IrisImageMapUnknownColor.ERROR)
|
||||
.setColors(new KMap<>());
|
||||
case COLOR_MAP -> definition
|
||||
.setMinimumHeight(defaults.getMinimumHeight())
|
||||
.setMaximumHeight(defaults.getMaximumHeight())
|
||||
.setVerticalOffset(defaults.getVerticalOffset())
|
||||
.setClamp(defaults.isClamp())
|
||||
.setInverted(defaults.isInverted())
|
||||
.setCurveExponent(defaults.getCurveExponent())
|
||||
.setSmoothingRadius(defaults.getSmoothingRadius())
|
||||
.setThreshold(defaults.getThreshold())
|
||||
.setFalloff(defaults.getFalloff());
|
||||
case BINARY_MASK, GRAYSCALE_MASK, ALPHA_MASK -> {
|
||||
definition
|
||||
.setMinimumHeight(defaults.getMinimumHeight())
|
||||
.setMaximumHeight(defaults.getMaximumHeight())
|
||||
.setVerticalOffset(defaults.getVerticalOffset())
|
||||
.setClamp(defaults.isClamp())
|
||||
.setColorTolerance(defaults.getColorTolerance())
|
||||
.setUnknownColor(IrisImageMapUnknownColor.ERROR)
|
||||
.setColors(new KMap<>());
|
||||
if (type != IrisImageMapType.BINARY_MASK) {
|
||||
definition
|
||||
.setThreshold(defaults.getThreshold())
|
||||
.setFalloff(defaults.getFalloff());
|
||||
}
|
||||
}
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
public static KList<IrisImageMapMask> masks(List<MaskRow> rows) {
|
||||
KList<IrisImageMapMask> masks = new KList<>();
|
||||
for (MaskRow row : rows) {
|
||||
masks.add(new IrisImageMapMask()
|
||||
.setMap(row.map())
|
||||
.setOperation(row.operation())
|
||||
.setInverted(row.inverted())
|
||||
.setThreshold(row.threshold())
|
||||
.setFalloff(row.falloff()));
|
||||
}
|
||||
return masks;
|
||||
}
|
||||
|
||||
public static MaskRow maskRow(
|
||||
String mapValue,
|
||||
Object operationValue,
|
||||
boolean inverted,
|
||||
Object thresholdValue,
|
||||
Object falloffValue
|
||||
) {
|
||||
String map = mapValue == null ? "" : mapValue.trim();
|
||||
if (map.isBlank()) {
|
||||
throw new IllegalArgumentException("Composed mask binding key must not be blank");
|
||||
}
|
||||
IrisImageMapMaskOperation operation;
|
||||
try {
|
||||
operation = IrisImageMapMaskOperation.valueOf(
|
||||
String.valueOf(operationValue).trim().toUpperCase(Locale.ROOT)
|
||||
);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IllegalArgumentException("Unknown mask operation for '" + map + "'", exception);
|
||||
}
|
||||
double threshold = unitValue(thresholdValue, "Mask threshold for '" + map + "'");
|
||||
double falloff = unitValue(falloffValue, "Mask falloff for '" + map + "'");
|
||||
return new MaskRow(map, operation, inverted, threshold, falloff);
|
||||
}
|
||||
|
||||
public static List<MaskRow> maskRows(List<IrisImageMapMask> masks) {
|
||||
List<MaskRow> rows = new ArrayList<>();
|
||||
if (masks == null) {
|
||||
return rows;
|
||||
}
|
||||
for (IrisImageMapMask mask : masks) {
|
||||
if (mask != null) {
|
||||
rows.add(new MaskRow(
|
||||
mask.getMap(),
|
||||
mask.getOperation(),
|
||||
mask.isInverted(),
|
||||
mask.getThreshold(),
|
||||
mask.getFalloff()
|
||||
));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static String safeKey(String fileName) {
|
||||
String normalized = fileName == null ? "image-map" : fileName.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalized.endsWith(".png")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 4);
|
||||
}
|
||||
normalized = KEY_CHARACTER.matcher(normalized).replaceAll("-");
|
||||
while (normalized.contains("--")) {
|
||||
normalized = normalized.replace("--", "-");
|
||||
}
|
||||
normalized = normalized.replaceAll("^[./_-]+|[./_-]+$", "");
|
||||
return normalized.isBlank() ? "image-map" : normalized;
|
||||
}
|
||||
|
||||
public static List<String> warnings(
|
||||
IrisImageMap definition,
|
||||
int sourceWidth,
|
||||
int sourceHeight,
|
||||
IrisWorldBoundary boundary
|
||||
) {
|
||||
List<String> warnings = new ArrayList<>();
|
||||
double blocksPerPixel = definition.getBlocksPerPixel();
|
||||
if (Double.isFinite(blocksPerPixel)) {
|
||||
if (blocksPerPixel < 1D) {
|
||||
warnings.add("Sub-block source scale will discard detail during block generation.");
|
||||
} else if (blocksPerPixel > 512D) {
|
||||
warnings.add("Each source pixel spans more than one Minecraft region (512 blocks).");
|
||||
}
|
||||
}
|
||||
long worldWidth = Math.round(sourceWidth * blocksPerPixel);
|
||||
long worldHeight = Math.round(sourceHeight * blocksPerPixel);
|
||||
if (worldWidth < 16L || worldHeight < 16L) {
|
||||
warnings.add("The transformed source covers less than one complete chunk on at least one axis.");
|
||||
}
|
||||
if (boundary != null) {
|
||||
Point2D.Double[] polygon = sourceWorldCorners(definition, sourceWidth, sourceHeight);
|
||||
Point2D.Double[] borderCorners = new Point2D.Double[]{
|
||||
new Point2D.Double(boundary.minimumX(), boundary.minimumZ()),
|
||||
new Point2D.Double(boundary.maximumX(), boundary.minimumZ()),
|
||||
new Point2D.Double(boundary.maximumX(), boundary.maximumZ()),
|
||||
new Point2D.Double(boundary.minimumX(), boundary.maximumZ())
|
||||
};
|
||||
for (Point2D.Double corner : borderCorners) {
|
||||
if (!contains(polygon, corner)) {
|
||||
warnings.add("The configured world boundary is not fully covered by the source image.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
double minimumX = Double.POSITIVE_INFINITY;
|
||||
double maximumX = Double.NEGATIVE_INFINITY;
|
||||
double minimumZ = Double.POSITIVE_INFINITY;
|
||||
double maximumZ = Double.NEGATIVE_INFINITY;
|
||||
boolean outsideBoundary = false;
|
||||
for (Point2D.Double corner : polygon) {
|
||||
minimumX = Math.min(minimumX, corner.x);
|
||||
maximumX = Math.max(maximumX, corner.x);
|
||||
minimumZ = Math.min(minimumZ, corner.y);
|
||||
maximumZ = Math.max(maximumZ, corner.y);
|
||||
if (corner.x < boundary.minimumX() || corner.x > boundary.maximumX()
|
||||
|| corner.y < boundary.minimumZ() || corner.y > boundary.maximumZ()) {
|
||||
outsideBoundary = true;
|
||||
}
|
||||
}
|
||||
if (outsideBoundary) {
|
||||
warnings.add("The source image extends outside the world boundary; mapped content there is not playable.");
|
||||
}
|
||||
double footprintWidth = maximumX - minimumX;
|
||||
double footprintHeight = maximumZ - minimumZ;
|
||||
if (footprintWidth > boundary.getSize() * 2D || footprintHeight > boundary.getSize() * 2D) {
|
||||
warnings.add("The transformed source is substantially larger than the playable boundary.");
|
||||
} else if (footprintWidth < boundary.getSize() * 0.5D
|
||||
|| footprintHeight < boundary.getSize() * 0.5D) {
|
||||
warnings.add("The transformed source is substantially smaller than the playable boundary.");
|
||||
}
|
||||
}
|
||||
return List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public static Point2D.Double[] sourceWorldCorners(IrisImageMap definition, int width, int height) {
|
||||
return new Point2D.Double[]{
|
||||
sourceToWorld(definition, 0D, 0D),
|
||||
sourceToWorld(definition, width, 0D),
|
||||
sourceToWorld(definition, width, height),
|
||||
sourceToWorld(definition, 0D, height)
|
||||
};
|
||||
}
|
||||
|
||||
public static Point2D.Double sourceToWorld(IrisImageMap definition, double sourceX, double sourceZ) {
|
||||
IrisImageMapOrigin origin = definition.getOrigin();
|
||||
IrisImageMapOrigin sourceOrigin = definition.getSourceOrigin();
|
||||
double x = sourceX - sourceOrigin.getX();
|
||||
double z = sourceZ - sourceOrigin.getZ();
|
||||
if (definition.isMirrorX()) {
|
||||
x = -x;
|
||||
}
|
||||
if (definition.isMirrorZ()) {
|
||||
z = -z;
|
||||
}
|
||||
Point2D.Double rotated = rotateForward(x, z, definition.getRotation());
|
||||
return new Point2D.Double(
|
||||
origin.getX() + (rotated.x * definition.getBlocksPerPixel()),
|
||||
origin.getZ() + (rotated.y * definition.getBlocksPerPixel())
|
||||
);
|
||||
}
|
||||
|
||||
public static int targetColor(String target) {
|
||||
if (target == null) {
|
||||
return 0xFF252936;
|
||||
}
|
||||
int hash = target.hashCode();
|
||||
int red = 72 + Math.floorMod(hash, 152);
|
||||
int green = 72 + Math.floorMod(hash >>> 8, 152);
|
||||
int blue = 72 + Math.floorMod(hash >>> 16, 152);
|
||||
return 0xFF000000 | (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
public static int heightColor(double height, double minimum, double maximum) {
|
||||
double range = maximum - minimum;
|
||||
double normalized = range == 0D ? 0.5D : Math.max(0D, Math.min(1D, (height - minimum) / range));
|
||||
double red;
|
||||
double green;
|
||||
double blue;
|
||||
if (normalized < 0.5D) {
|
||||
double factor = normalized * 2D;
|
||||
red = 38D + (45D * factor);
|
||||
green = 77D + (111D * factor);
|
||||
blue = 132D - (42D * factor);
|
||||
} else {
|
||||
double factor = (normalized - 0.5D) * 2D;
|
||||
red = 83D + (169D * factor);
|
||||
green = 188D + (58D * factor);
|
||||
blue = 90D + (150D * factor);
|
||||
}
|
||||
return 0xFF000000 | ((int) red << 16) | ((int) green << 8) | (int) blue;
|
||||
}
|
||||
|
||||
private static Point2D.Double rotateForward(double x, double z, IrisImageMapRotation rotation) {
|
||||
return switch (rotation) {
|
||||
case DEG_0 -> new Point2D.Double(x, z);
|
||||
case DEG_90 -> new Point2D.Double(-z, x);
|
||||
case DEG_180 -> new Point2D.Double(-x, -z);
|
||||
case DEG_270 -> new Point2D.Double(z, -x);
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean contains(Point2D.Double[] polygon, Point2D.Double point) {
|
||||
boolean inside = false;
|
||||
int previous = polygon.length - 1;
|
||||
for (int index = 0; index < polygon.length; index++) {
|
||||
Point2D.Double current = polygon[index];
|
||||
Point2D.Double before = polygon[previous];
|
||||
if (Line2D.ptSegDist(before.x, before.y, current.x, current.y, point.x, point.y) <= 0.0000001D) {
|
||||
return true;
|
||||
}
|
||||
boolean crosses = (current.y > point.y) != (before.y > point.y);
|
||||
if (crosses) {
|
||||
double intersection = ((before.x - current.x) * (point.y - current.y)
|
||||
/ (before.y - current.y)) + current.x;
|
||||
if (point.x < intersection) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
previous = index;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
private static double unitValue(Object value, String name) {
|
||||
double number;
|
||||
try {
|
||||
number = Double.parseDouble(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new IllegalArgumentException(name + " must be a number", exception);
|
||||
}
|
||||
if (!Double.isFinite(number) || number < 0D || number > 1D) {
|
||||
throw new IllegalArgumentException(name + " must be finite and within 0..1");
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
public record SourceMetadata(
|
||||
Path path,
|
||||
String format,
|
||||
int width,
|
||||
int height,
|
||||
long pixels,
|
||||
String colorMode,
|
||||
int colorComponents,
|
||||
int channels,
|
||||
int bitDepth,
|
||||
boolean alpha,
|
||||
double minimumAlpha,
|
||||
double maximumAlpha,
|
||||
String colorProfile
|
||||
) {
|
||||
public String summary() {
|
||||
String alphaSummary = alpha
|
||||
? String.format(Locale.ROOT, "yes (%.1f%%..%.1f%%)", minimumAlpha * 100D, maximumAlpha * 100D)
|
||||
: "no";
|
||||
return format.toUpperCase(Locale.ROOT) + " | " + width + " × " + height
|
||||
+ " | " + pixels + " pixels | " + colorMode
|
||||
+ " | " + bitDepth + "-bit | " + channels + " channel(s) | alpha "
|
||||
+ alphaSummary + " | profile " + colorProfile;
|
||||
}
|
||||
}
|
||||
|
||||
public record LegendRow(String color, String target) {
|
||||
}
|
||||
|
||||
public record MaskRow(
|
||||
String map,
|
||||
IrisImageMapMaskOperation operation,
|
||||
boolean inverted,
|
||||
double threshold,
|
||||
double falloff
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.core.localization.DesktopUiMessages;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.engine.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.image.IrisImageMapMaskSampler;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.SwingWorker;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
final class ImageMapStudioPreviewPanel extends JPanel implements AutoCloseable {
|
||||
private static final Color BACKGROUND = new Color(12, 15, 22);
|
||||
private static final Color PANEL_BACKGROUND = new Color(20, 24, 33);
|
||||
private static final Color BORDER = new Color(48, 55, 72);
|
||||
private static final Color TEXT = new Color(230, 234, 242);
|
||||
private static final Color SECONDARY = new Color(151, 160, 178);
|
||||
|
||||
private final SourceCanvas sourceCanvas = new SourceCanvas();
|
||||
private final WorldCanvas worldCanvas = new WorldCanvas();
|
||||
private final JLabel statusLabel = new JLabel(" ");
|
||||
|
||||
ImageMapStudioPreviewPanel() {
|
||||
super(new BorderLayout());
|
||||
setBackground(BACKGROUND);
|
||||
JPanel source = wrap(IrisLanguage.plain(DesktopUiMessages.IMAGEMAP_SOURCE), sourceCanvas);
|
||||
JPanel interpreted = wrap(IrisLanguage.plain(DesktopUiMessages.IMAGEMAP_INTERPRETED), worldCanvas);
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, source, interpreted);
|
||||
split.setBorder(BorderFactory.createEmptyBorder());
|
||||
split.setDividerSize(5);
|
||||
split.setResizeWeight(0.42D);
|
||||
split.setBackground(BACKGROUND);
|
||||
add(split, BorderLayout.CENTER);
|
||||
statusLabel.setForeground(SECONDARY);
|
||||
statusLabel.setBackground(PANEL_BACKGROUND);
|
||||
statusLabel.setOpaque(true);
|
||||
statusLabel.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(1, 0, 0, 0, BORDER),
|
||||
BorderFactory.createEmptyBorder(5, 9, 5, 9)
|
||||
));
|
||||
add(statusLabel, BorderLayout.SOUTH);
|
||||
worldCanvas.setHoverConsumer(statusLabel::setText);
|
||||
}
|
||||
|
||||
void setPreview(
|
||||
BufferedImage source,
|
||||
CompiledIrisImageMap compiled,
|
||||
IrisImageMapMaskSampler maskSampler,
|
||||
IrisWorldBoundary boundary,
|
||||
IrisImageMapApplication application,
|
||||
int minimumWorldHeight,
|
||||
DoubleBinaryOperator proceduralHeightSampler
|
||||
) {
|
||||
sourceCanvas.setSource(source);
|
||||
worldCanvas.setPreview(
|
||||
compiled, maskSampler, boundary, application, minimumWorldHeight, proceduralHeightSampler
|
||||
);
|
||||
}
|
||||
|
||||
void setSource(BufferedImage source) {
|
||||
sourceCanvas.setSource(source);
|
||||
worldCanvas.clear();
|
||||
}
|
||||
|
||||
void setDiagnosticConsumer(Consumer<String> diagnosticConsumer) {
|
||||
worldCanvas.setDiagnosticConsumer(diagnosticConsumer);
|
||||
}
|
||||
|
||||
void setOverlays(boolean chunks, boolean regions, boolean boundary, boolean coverage) {
|
||||
worldCanvas.setOverlays(chunks, regions, boundary, coverage);
|
||||
}
|
||||
|
||||
BufferedImage renderInterpretedSnapshot(int width, int height) {
|
||||
return worldCanvas.renderSnapshot(width, height).image();
|
||||
}
|
||||
|
||||
BufferedImage renderSourceSnapshot(int width, int height) {
|
||||
sourceCanvas.setSize(width, height);
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try {
|
||||
sourceCanvas.paint(graphics);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
worldCanvas.close();
|
||||
}
|
||||
|
||||
private static JPanel wrap(String title, JPanel canvas) {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setBackground(PANEL_BACKGROUND);
|
||||
panel.setBorder(BorderFactory.createLineBorder(BORDER));
|
||||
JLabel label = new JLabel(title);
|
||||
label.setForeground(TEXT);
|
||||
label.setBackground(PANEL_BACKGROUND);
|
||||
label.setOpaque(true);
|
||||
label.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, BORDER),
|
||||
BorderFactory.createEmptyBorder(6, 9, 6, 9)
|
||||
));
|
||||
panel.add(label, BorderLayout.NORTH);
|
||||
panel.add(canvas, BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static final class SourceCanvas extends JPanel {
|
||||
private BufferedImage source;
|
||||
|
||||
private SourceCanvas() {
|
||||
setBackground(BACKGROUND);
|
||||
setPreferredSize(new Dimension(420, 520));
|
||||
}
|
||||
|
||||
private void setSource(BufferedImage source) {
|
||||
this.source = source;
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
super.paintComponent(graphics);
|
||||
if (source == null) {
|
||||
drawEmpty(graphics, IrisLanguage.plain(DesktopUiMessages.IMAGEMAP_NO_SOURCE));
|
||||
return;
|
||||
}
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
||||
int padding = 16;
|
||||
double scale = Math.min(
|
||||
(getWidth() - (padding * 2D)) / source.getWidth(),
|
||||
(getHeight() - (padding * 2D)) / source.getHeight()
|
||||
);
|
||||
scale = Math.max(0.0001D, scale);
|
||||
int width = Math.max(1, (int) Math.round(source.getWidth() * scale));
|
||||
int height = Math.max(1, (int) Math.round(source.getHeight() * scale));
|
||||
int x = (getWidth() - width) / 2;
|
||||
int y = (getHeight() - height) / 2;
|
||||
canvas.drawImage(source, x, y, width, height, null);
|
||||
canvas.setColor(new Color(255, 255, 255, 64));
|
||||
canvas.drawRect(x, y, width - 1, height - 1);
|
||||
if (scale >= 8D && source.getWidth() <= 256 && source.getHeight() <= 256) {
|
||||
canvas.setColor(new Color(255, 255, 255, 25));
|
||||
for (int sourceX = 1; sourceX < source.getWidth(); sourceX++) {
|
||||
int lineX = x + (int) Math.round(sourceX * scale);
|
||||
canvas.drawLine(lineX, y, lineX, y + height);
|
||||
}
|
||||
for (int sourceZ = 1; sourceZ < source.getHeight(); sourceZ++) {
|
||||
int lineY = y + (int) Math.round(sourceZ * scale);
|
||||
canvas.drawLine(x, lineY, x + width, lineY);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WorldCanvas extends JPanel implements AutoCloseable {
|
||||
private final AtomicLong revision = new AtomicLong();
|
||||
private CompiledIrisImageMap compiled;
|
||||
private IrisImageMapMaskSampler maskSampler = IrisImageMapMaskSampler.empty();
|
||||
private IrisWorldBoundary boundary;
|
||||
private IrisImageMapApplication application = IrisImageMapApplication.CUSTOM;
|
||||
private int minimumWorldHeight;
|
||||
private DoubleBinaryOperator proceduralHeightSampler;
|
||||
private BufferedImage rendered;
|
||||
private SwingWorker<RenderResult, Void> worker;
|
||||
private Consumer<String> diagnosticConsumer = ignored -> {
|
||||
};
|
||||
private Consumer<String> hoverConsumer = ignored -> {
|
||||
};
|
||||
private double centerX;
|
||||
private double centerZ;
|
||||
private double blocksPerScreenPixel = 1D;
|
||||
private boolean showChunks = true;
|
||||
private boolean showRegions = true;
|
||||
private boolean showBoundary = true;
|
||||
private boolean showCoverage = true;
|
||||
private Point dragOrigin;
|
||||
private double dragCenterX;
|
||||
private double dragCenterZ;
|
||||
|
||||
private WorldCanvas() {
|
||||
setBackground(BACKGROUND);
|
||||
setPreferredSize(new Dimension(620, 520));
|
||||
MouseAdapter mouse = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent event) {
|
||||
dragOrigin = event.getPoint();
|
||||
dragCenterX = centerX;
|
||||
dragCenterZ = centerZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent event) {
|
||||
if (dragOrigin == null) {
|
||||
return;
|
||||
}
|
||||
centerX = dragCenterX - ((event.getX() - dragOrigin.x) * blocksPerScreenPixel);
|
||||
centerZ = dragCenterZ - ((event.getY() - dragOrigin.y) * blocksPerScreenPixel);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent event) {
|
||||
dragOrigin = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent event) {
|
||||
publishHover(event.getX(), event.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseWheelMoved(MouseWheelEvent event) {
|
||||
double beforeX = screenToWorldX(event.getX());
|
||||
double beforeZ = screenToWorldZ(event.getY());
|
||||
double factor = Math.pow(1.12D, event.getPreciseWheelRotation());
|
||||
blocksPerScreenPixel = Math.max(0.01D, Math.min(1_000_000D, blocksPerScreenPixel * factor));
|
||||
centerX += beforeX - screenToWorldX(event.getX());
|
||||
centerZ += beforeZ - screenToWorldZ(event.getY());
|
||||
requestRender();
|
||||
}
|
||||
};
|
||||
addMouseListener(mouse);
|
||||
addMouseMotionListener(mouse);
|
||||
addMouseWheelListener(mouse);
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent event) {
|
||||
requestRender();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setPreview(
|
||||
CompiledIrisImageMap compiled,
|
||||
IrisImageMapMaskSampler maskSampler,
|
||||
IrisWorldBoundary boundary,
|
||||
IrisImageMapApplication application,
|
||||
int minimumWorldHeight,
|
||||
DoubleBinaryOperator proceduralHeightSampler
|
||||
) {
|
||||
this.compiled = compiled;
|
||||
this.maskSampler = maskSampler == null ? IrisImageMapMaskSampler.empty() : maskSampler;
|
||||
this.boundary = boundary;
|
||||
this.application = application == null ? IrisImageMapApplication.CUSTOM : application;
|
||||
this.minimumWorldHeight = minimumWorldHeight;
|
||||
this.proceduralHeightSampler = proceduralHeightSampler;
|
||||
IrisImageMap definition = compiled.getDefinition();
|
||||
centerX = definition.getOrigin().getX();
|
||||
centerZ = definition.getOrigin().getZ();
|
||||
fitSource();
|
||||
requestRender();
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
revision.incrementAndGet();
|
||||
if (worker != null) {
|
||||
worker.cancel(true);
|
||||
worker = null;
|
||||
}
|
||||
compiled = null;
|
||||
rendered = null;
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void setDiagnosticConsumer(Consumer<String> diagnosticConsumer) {
|
||||
this.diagnosticConsumer = diagnosticConsumer == null ? ignored -> {
|
||||
} : diagnosticConsumer;
|
||||
}
|
||||
|
||||
private void setHoverConsumer(Consumer<String> hoverConsumer) {
|
||||
this.hoverConsumer = hoverConsumer == null ? ignored -> {
|
||||
} : hoverConsumer;
|
||||
}
|
||||
|
||||
private void setOverlays(boolean chunks, boolean regions, boolean boundary, boolean coverage) {
|
||||
showChunks = chunks;
|
||||
showRegions = regions;
|
||||
showBoundary = boundary;
|
||||
showCoverage = coverage;
|
||||
requestRender();
|
||||
}
|
||||
|
||||
private void fitSource() {
|
||||
if (compiled == null) {
|
||||
return;
|
||||
}
|
||||
Point2D.Double[] corners = ImageMapStudioModel.sourceWorldCorners(
|
||||
compiled.getDefinition(), compiled.getSourceWidth(), compiled.getSourceHeight()
|
||||
);
|
||||
double minimumX = Double.POSITIVE_INFINITY;
|
||||
double maximumX = Double.NEGATIVE_INFINITY;
|
||||
double minimumZ = Double.POSITIVE_INFINITY;
|
||||
double maximumZ = Double.NEGATIVE_INFINITY;
|
||||
for (Point2D.Double corner : corners) {
|
||||
minimumX = Math.min(minimumX, corner.x);
|
||||
maximumX = Math.max(maximumX, corner.x);
|
||||
minimumZ = Math.min(minimumZ, corner.y);
|
||||
maximumZ = Math.max(maximumZ, corner.y);
|
||||
}
|
||||
centerX = (minimumX + maximumX) / 2D;
|
||||
centerZ = (minimumZ + maximumZ) / 2D;
|
||||
int width = Math.max(320, getWidth());
|
||||
int height = Math.max(320, getHeight());
|
||||
blocksPerScreenPixel = Math.max(
|
||||
(maximumX - minimumX) / (width * 0.82D),
|
||||
(maximumZ - minimumZ) / (height * 0.82D)
|
||||
);
|
||||
blocksPerScreenPixel = Math.max(0.01D, blocksPerScreenPixel);
|
||||
}
|
||||
|
||||
private void requestRender() {
|
||||
if (compiled == null || getWidth() <= 0 || getHeight() <= 0) {
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
long currentRevision = revision.incrementAndGet();
|
||||
if (worker != null) {
|
||||
worker.cancel(true);
|
||||
}
|
||||
int width = Math.max(1, getWidth());
|
||||
int height = Math.max(1, getHeight());
|
||||
worker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected RenderResult doInBackground() {
|
||||
return renderSnapshot(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
if (isCancelled() || currentRevision != revision.get()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
RenderResult result = get();
|
||||
rendered = result.image();
|
||||
if (result.errors() > 0) {
|
||||
diagnosticConsumer.accept(result.errors() + " preview sample(s) failed; invalid pixels are magenta.");
|
||||
}
|
||||
repaint();
|
||||
} catch (CancellationException ignored) {
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (ExecutionException exception) {
|
||||
IrisLogging.reportError(exception.getCause());
|
||||
diagnosticConsumer.accept("Preview render failed: " + exception.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private RenderResult renderSnapshot(int width, int height) {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
if (compiled == null) {
|
||||
return new RenderResult(image, 0);
|
||||
}
|
||||
int errors = 0;
|
||||
IrisImageMapType type = compiled.getType();
|
||||
IrisImageMap definition = compiled.getDefinition();
|
||||
for (int screenZ = 0; screenZ < height; screenZ++) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
break;
|
||||
}
|
||||
double worldZ = centerZ + ((screenZ - (height / 2D)) * blocksPerScreenPixel);
|
||||
for (int screenX = 0; screenX < width; screenX++) {
|
||||
double worldX = centerX + ((screenX - (width / 2D)) * blocksPerScreenPixel);
|
||||
int color;
|
||||
try {
|
||||
double maskWeight = maskSampler.sample(worldX, worldZ);
|
||||
color = switch (type) {
|
||||
case GRAYSCALE_HEIGHT, RGB_HEIGHT -> ImageMapStudioModel.heightColor(
|
||||
interpretedHeight(worldX, worldZ, maskWeight),
|
||||
definition.getMinimumHeight(),
|
||||
definition.getMaximumHeight()
|
||||
);
|
||||
case COLOR_MAP -> categoricalApplication()
|
||||
&& !IrisImageMapRuntime.selectCategorical(maskWeight)
|
||||
? BACKGROUND.getRGB()
|
||||
: ImageMapStudioModel.targetColor(compiled.sampleTarget(worldX, worldZ));
|
||||
case BINARY_MASK, GRAYSCALE_MASK, ALPHA_MASK -> grayscale(
|
||||
compiled.sampleNormalized(worldX, worldZ)
|
||||
);
|
||||
};
|
||||
} catch (RuntimeException exception) {
|
||||
color = 0xFFFF3DA8;
|
||||
errors++;
|
||||
}
|
||||
if (showCoverage && !compiled.containsWorld(worldX, worldZ)
|
||||
&& ((screenX + screenZ) & 7) < 3) {
|
||||
color = blend(color, 0xFF090B10, 0.48D);
|
||||
}
|
||||
image.setRGB(screenX, screenZ, color);
|
||||
}
|
||||
}
|
||||
return new RenderResult(image, errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics graphics) {
|
||||
super.paintComponent(graphics);
|
||||
if (compiled == null) {
|
||||
drawEmpty(graphics, IrisLanguage.plain(DesktopUiMessages.IMAGEMAP_NO_PREVIEW));
|
||||
return;
|
||||
}
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
if (rendered != null) {
|
||||
canvas.drawImage(rendered, 0, 0, getWidth(), getHeight(), null);
|
||||
}
|
||||
drawWorldGrid(canvas, 16D, new Color(255, 255, 255, 28), showChunks);
|
||||
drawWorldGrid(canvas, 512D, new Color(99, 161, 255, 96), showRegions);
|
||||
if (showCoverage) {
|
||||
drawCoverage(canvas);
|
||||
}
|
||||
if (showBoundary && boundary != null) {
|
||||
drawBoundary(canvas);
|
||||
}
|
||||
drawOrigin(canvas);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawWorldGrid(Graphics2D canvas, double spacing, Color color, boolean enabled) {
|
||||
if (!enabled || spacing / blocksPerScreenPixel < 7D) {
|
||||
return;
|
||||
}
|
||||
canvas.setColor(color);
|
||||
double minimumX = screenToWorldX(0);
|
||||
double maximumX = screenToWorldX(getWidth());
|
||||
double minimumZ = screenToWorldZ(0);
|
||||
double maximumZ = screenToWorldZ(getHeight());
|
||||
double firstX = Math.floor(minimumX / spacing) * spacing;
|
||||
double firstZ = Math.floor(minimumZ / spacing) * spacing;
|
||||
for (double worldX = firstX; worldX <= maximumX; worldX += spacing) {
|
||||
int screenX = worldToScreenX(worldX);
|
||||
canvas.drawLine(screenX, 0, screenX, getHeight());
|
||||
}
|
||||
for (double worldZ = firstZ; worldZ <= maximumZ; worldZ += spacing) {
|
||||
int screenZ = worldToScreenZ(worldZ);
|
||||
canvas.drawLine(0, screenZ, getWidth(), screenZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCoverage(Graphics2D canvas) {
|
||||
Point2D.Double[] corners = ImageMapStudioModel.sourceWorldCorners(
|
||||
compiled.getDefinition(), compiled.getSourceWidth(), compiled.getSourceHeight()
|
||||
);
|
||||
Polygon polygon = new Polygon();
|
||||
for (Point2D.Double corner : corners) {
|
||||
polygon.addPoint(worldToScreenX(corner.x), worldToScreenZ(corner.y));
|
||||
}
|
||||
canvas.setStroke(new BasicStroke(2F));
|
||||
canvas.setColor(new Color(99, 161, 255, 205));
|
||||
canvas.drawPolygon(polygon);
|
||||
}
|
||||
|
||||
private void drawBoundary(Graphics2D canvas) {
|
||||
int minimumX = worldToScreenX(boundary.minimumX());
|
||||
int maximumX = worldToScreenX(boundary.maximumX());
|
||||
int minimumZ = worldToScreenZ(boundary.minimumZ());
|
||||
int maximumZ = worldToScreenZ(boundary.maximumZ());
|
||||
int x = Math.min(minimumX, maximumX);
|
||||
int z = Math.min(minimumZ, maximumZ);
|
||||
int width = Math.abs(maximumX - minimumX);
|
||||
int height = Math.abs(maximumZ - minimumZ);
|
||||
canvas.setColor(new Color(255, 188, 82, 220));
|
||||
canvas.setStroke(new BasicStroke(2F));
|
||||
canvas.drawRect(x, z, width, height);
|
||||
}
|
||||
|
||||
private void drawOrigin(Graphics2D canvas) {
|
||||
IrisImageMap definition = compiled.getDefinition();
|
||||
int x = worldToScreenX(definition.getOrigin().getX());
|
||||
int z = worldToScreenZ(definition.getOrigin().getZ());
|
||||
canvas.setColor(new Color(255, 255, 255, 190));
|
||||
canvas.drawLine(x - 7, z, x + 7, z);
|
||||
canvas.drawLine(x, z - 7, x, z + 7);
|
||||
}
|
||||
|
||||
private void publishHover(int screenX, int screenZ) {
|
||||
if (compiled == null) {
|
||||
return;
|
||||
}
|
||||
double worldX = screenToWorldX(screenX);
|
||||
double worldZ = screenToWorldZ(screenZ);
|
||||
String value;
|
||||
try {
|
||||
double maskWeight = maskSampler.sample(worldX, worldZ);
|
||||
value = switch (compiled.getType()) {
|
||||
case GRAYSCALE_HEIGHT, RGB_HEIGHT -> String.format(
|
||||
"Y %.3f", interpretedHeight(worldX, worldZ, maskWeight)
|
||||
);
|
||||
case COLOR_MAP -> categoricalApplication()
|
||||
&& !IrisImageMapRuntime.selectCategorical(maskWeight)
|
||||
? "procedural"
|
||||
: String.valueOf(compiled.sampleTarget(worldX, worldZ));
|
||||
case BINARY_MASK, GRAYSCALE_MASK, ALPHA_MASK -> String.format(
|
||||
"%.4f", compiled.sampleNormalized(worldX, worldZ)
|
||||
);
|
||||
};
|
||||
if (!maskSampler.isEmpty()) {
|
||||
value += String.format(" | mask %.4f", maskWeight);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
value = exception.getMessage();
|
||||
}
|
||||
hoverConsumer.accept(IrisLanguage.plain(
|
||||
DesktopUiMessages.IMAGEMAP_PREVIEW_STATUS,
|
||||
MessageArgument.untrusted("x", String.format("%.2f", worldX)),
|
||||
MessageArgument.untrusted("z", String.format("%.2f", worldZ)),
|
||||
MessageArgument.untrusted("value", value),
|
||||
MessageArgument.untrusted("scale", String.format("%.3f", blocksPerScreenPixel))
|
||||
));
|
||||
}
|
||||
|
||||
private double interpretedHeight(double worldX, double worldZ, double maskWeight) {
|
||||
double mappedHeight = compiled.sampleHeight(worldX, worldZ);
|
||||
if (application != IrisImageMapApplication.TERRAIN_HEIGHT || maskSampler.isEmpty()) {
|
||||
return mappedHeight;
|
||||
}
|
||||
IrisImageMap definition = compiled.getDefinition();
|
||||
double proceduralLocalHeight = proceduralHeightSampler == null
|
||||
? definition.getMinimumHeight() - minimumWorldHeight
|
||||
: proceduralHeightSampler.applyAsDouble(worldX, worldZ);
|
||||
double mappedLocalHeight = mappedHeight - minimumWorldHeight;
|
||||
return minimumWorldHeight + IrisImageMapRuntime.blendTerrainHeight(
|
||||
mappedLocalHeight, proceduralLocalHeight, maskWeight
|
||||
);
|
||||
}
|
||||
|
||||
private boolean categoricalApplication() {
|
||||
return application == IrisImageMapApplication.BIOME
|
||||
|| application == IrisImageMapApplication.REGION
|
||||
|| application == IrisImageMapApplication.SURFACE_BLOCK;
|
||||
}
|
||||
|
||||
private int worldToScreenX(double worldX) {
|
||||
return (int) Math.round((getWidth() / 2D) + ((worldX - centerX) / blocksPerScreenPixel));
|
||||
}
|
||||
|
||||
private int worldToScreenZ(double worldZ) {
|
||||
return (int) Math.round((getHeight() / 2D) + ((worldZ - centerZ) / blocksPerScreenPixel));
|
||||
}
|
||||
|
||||
private double screenToWorldX(double screenX) {
|
||||
return centerX + ((screenX - (getWidth() / 2D)) * blocksPerScreenPixel);
|
||||
}
|
||||
|
||||
private double screenToWorldZ(double screenZ) {
|
||||
return centerZ + ((screenZ - (getHeight() / 2D)) * blocksPerScreenPixel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
revision.incrementAndGet();
|
||||
if (worker != null) {
|
||||
worker.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawEmpty(Graphics graphics, String text) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setColor(SECONDARY);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
int width = canvas.getFontMetrics().stringWidth(text);
|
||||
int x = Math.max(8, (graphics.getClipBounds() == null ? 0 : graphics.getClipBounds().width - width) / 2);
|
||||
int y = Math.max(20, graphics.getClipBounds() == null ? 20 : graphics.getClipBounds().height / 2);
|
||||
canvas.drawString(text, x, y);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static int grayscale(double value) {
|
||||
int level = (int) Math.round(Math.max(0D, Math.min(1D, value)) * 255D);
|
||||
return 0xFF000000 | (level << 16) | (level << 8) | level;
|
||||
}
|
||||
|
||||
private static int blend(int first, int second, double secondWeight) {
|
||||
double firstWeight = 1D - secondWeight;
|
||||
int red = (int) ((((first >>> 16) & 0xFF) * firstWeight) + (((second >>> 16) & 0xFF) * secondWeight));
|
||||
int green = (int) ((((first >>> 8) & 0xFF) * firstWeight) + (((second >>> 8) & 0xFF) * secondWeight));
|
||||
int blue = (int) (((first & 0xFF) * firstWeight) + ((second & 0xFF) * secondWeight));
|
||||
return 0xFF000000 | (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private record RenderResult(BufferedImage image, int errors) {
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.CommandSender;
|
||||
@@ -472,7 +473,7 @@ public final class MultiverseGuardListener implements Listener {
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
for (String line : lines) {
|
||||
sender.sendMessage(line);
|
||||
ComponentMessenger.sendSection(sender, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.image.IrisImageMapCompiler;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
@@ -27,16 +28,23 @@ import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ImageResourceLoader extends ResourceLoader<IrisImage> {
|
||||
public ImageResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) {
|
||||
super(root, idm, folderName, resourceTypeName, IrisImage.class, options);
|
||||
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize());
|
||||
int cacheSize = options.registerPreservation()
|
||||
? IrisSettings.get().getPerformance().getObjectLoaderCacheSize()
|
||||
: options.cacheSize();
|
||||
loadCache = new KCache<>(this::loadRaw, cacheSize);
|
||||
}
|
||||
|
||||
public boolean supportsSchemas() {
|
||||
@@ -54,20 +62,51 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
|
||||
protected IrisImage loadFile(File j, String name) {
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
BufferedImage bu = ImageIO.read(j);
|
||||
|
||||
if (bu == null) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (unsupported or corrupt image)");
|
||||
return null;
|
||||
try (ImageInputStream input = ImageIO.createImageInputStream(j)) {
|
||||
if (input == null) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath()
|
||||
+ " (unsupported or corrupt image)");
|
||||
return null;
|
||||
}
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
if (!readers.hasNext()) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath()
|
||||
+ " (unsupported or corrupt image)");
|
||||
return null;
|
||||
}
|
||||
ImageReader reader = readers.next();
|
||||
try {
|
||||
reader.setInput(input, true, true);
|
||||
String format = reader.getFormatName().toLowerCase(Locale.ROOT);
|
||||
if (!"png".equals(format)) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath()
|
||||
+ " (expected PNG, got " + format + ")");
|
||||
return null;
|
||||
}
|
||||
int width = reader.getWidth(0);
|
||||
int height = reader.getHeight(0);
|
||||
if (!supportedDimensions(width, height)) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath()
|
||||
+ " (dimensions " + width + "x" + height + " exceed the supported image-map limits)");
|
||||
return null;
|
||||
}
|
||||
BufferedImage image = reader.read(0);
|
||||
if (image == null) {
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath()
|
||||
+ " (unsupported or corrupt image)");
|
||||
return null;
|
||||
}
|
||||
IrisImage loaded = new IrisImage(image, format);
|
||||
loaded.setLoadFile(j);
|
||||
loaded.setLoader(manager);
|
||||
loaded.setLoadKey(name);
|
||||
logLoad(j, loaded);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return loaded;
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
IrisImage img = new IrisImage(bu);
|
||||
img.setLoadFile(j);
|
||||
img.setLoader(manager);
|
||||
img.setLoadKey(name);
|
||||
logLoad(j, img);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return img;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage());
|
||||
@@ -75,6 +114,16 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean supportedDimensions(int width, int height) {
|
||||
if (width < IrisImageMapCompiler.MINIMUM_DIMENSION
|
||||
|| width > IrisImageMapCompiler.MAXIMUM_DIMENSION
|
||||
|| height < IrisImageMapCompiler.MINIMUM_DIMENSION
|
||||
|| height > IrisImageMapCompiler.MAXIMUM_DIMENSION) {
|
||||
return false;
|
||||
}
|
||||
return (long) width * height <= IrisImageMapCompiler.MAXIMUM_PIXELS;
|
||||
}
|
||||
|
||||
void getPNGFiles(File directory, String prefix, Set<String> m, HashSet<String> visitedDirectories) {
|
||||
if (directory == null || !directory.exists()) {
|
||||
return;
|
||||
|
||||
@@ -46,6 +46,7 @@ import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisObjectScale;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPool;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
@@ -119,6 +120,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private ResourceLoader<IrisObject> objectLoader;
|
||||
private ResourceLoader<IrisMatterObject> matterLoader;
|
||||
private ResourceLoader<IrisImage> imageLoader;
|
||||
private ResourceLoader<IrisImageMap> imageMapLoader;
|
||||
private ResourceLoader<IrisStructure> structureLoader;
|
||||
private ResourceLoader<IrisJigsawPool> jigsawPoolLoader;
|
||||
private ResourceLoader<IrisJigsawPiece> jigsawPieceLoader;
|
||||
@@ -241,6 +243,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return loadAny(IrisImage.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisImageMap loadAnyImageMap(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisImageMap.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisDimension loadAnyDimension(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisDimension.class, key, nearest);
|
||||
}
|
||||
@@ -484,6 +490,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
this.expressionLoader = registerLoader(IrisExpression.class, replacement);
|
||||
this.objectLoader = registerLoader(IrisObject.class, replacement);
|
||||
this.imageLoader = registerLoader(IrisImage.class, replacement);
|
||||
this.imageMapLoader = registerLoader(IrisImageMap.class, replacement);
|
||||
this.matterLoader = registerLoader(IrisMatterObject.class, replacement);
|
||||
this.structureLoader = registerLoader(IrisStructure.class, replacement);
|
||||
this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class, replacement);
|
||||
@@ -510,13 +517,19 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
.setPrettyPrinting();
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> replacement = new KMap<>();
|
||||
dataFolder.mkdirs();
|
||||
recoverStructureTransactions();
|
||||
biomeLoader = registerLoader(IrisBiome.class, replacement);
|
||||
regionLoader = registerLoader(IrisRegion.class, replacement);
|
||||
dimensionLoader = registerLoader(IrisDimension.class, replacement);
|
||||
generatorLoader = registerLoader(IrisGenerator.class, replacement);
|
||||
expressionLoader = registerLoader(IrisExpression.class, replacement);
|
||||
imageLoader = registerLoader(IrisImage.class, replacement);
|
||||
imageMapLoader = registerLoader(IrisImageMap.class, replacement);
|
||||
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
|
||||
gson = builder.create();
|
||||
loaders = replacement;
|
||||
if (biomeLoader == null || dimensionLoader == null) {
|
||||
if (biomeLoader == null || regionLoader == null || dimensionLoader == null
|
||||
|| generatorLoader == null || expressionLoader == null
|
||||
|| imageLoader == null || imageMapLoader == null) {
|
||||
throw new IllegalStateException("Unable to initialize Iris datapack compiler loaders for " + dataFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,6 +720,12 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
folderCache.reset();
|
||||
}
|
||||
|
||||
public void unload(String name) {
|
||||
if (name != null && !name.isBlank()) {
|
||||
loadCache.invalidate(name);
|
||||
}
|
||||
}
|
||||
|
||||
public File fileFor(T b) {
|
||||
return resolveFile(b.getLoadKey(), ".json", getFolders());
|
||||
}
|
||||
|
||||
+5
@@ -612,6 +612,10 @@ public final class BukkitCommandMessagesExtended {
|
||||
"iris.bukkit.commandstudio.opening_noise_explorer",
|
||||
C.GREEN + "Opening Noise Explorer!"
|
||||
);
|
||||
public static final TextKey COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO = TextKey.of(
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio",
|
||||
C.GREEN + "Opening the Image Map Studio."
|
||||
);
|
||||
public static final TextKey COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE = TextKey.of(
|
||||
"iris.bukkit.commandstudio.cannot_add_items_virtual_inventory_because",
|
||||
C.RED + "Cannot add items to virtual inventory because of: " + "{value}"
|
||||
@@ -952,6 +956,7 @@ public final class BukkitCommandMessagesExtended {
|
||||
COMMAND_STUDIO_OPENING_VSCODE_PACK,
|
||||
COMMAND_STUDIO_PACK_HAS_VERSION,
|
||||
COMMAND_STUDIO_OPENING_NOISE_EXPLORER,
|
||||
COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO,
|
||||
COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE,
|
||||
COMMAND_STUDIO_OPENING_INVENTORY_NOW,
|
||||
COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD,
|
||||
|
||||
@@ -90,6 +90,72 @@ public final class DesktopUiMessages {
|
||||
public static final TextKey PREGEN_TIME = TextKey.of("iris.desktop.pregen.time", "{remaining} remaining ({elapsed} elapsed)");
|
||||
public static final TextKey PREGEN_METHOD = TextKey.of("iris.desktop.pregen.method", "Generation method: {method}");
|
||||
public static final TextKey PREGEN_MEMORY = TextKey.of("iris.desktop.pregen.memory", "Memory: {used} ({usage}) Pressure: {pressure}/s");
|
||||
public static final TextKey IMAGEMAP_TITLE = TextKey.of("iris.desktop.imagemap.title", "Image Map Studio");
|
||||
public static final TextKey IMAGEMAP_PRESET = TextKey.of("iris.desktop.imagemap.preset", "Preset:");
|
||||
public static final TextKey IMAGEMAP_LOAD = TextKey.of("iris.desktop.imagemap.load", "Load");
|
||||
public static final TextKey IMAGEMAP_IMPORT_PNG = TextKey.of("iris.desktop.imagemap.import_png", "Import PNG");
|
||||
public static final TextKey IMAGEMAP_REPLACE_PNG = TextKey.of("iris.desktop.imagemap.replace_png", "Replace PNG");
|
||||
public static final TextKey IMAGEMAP_PREVIEW = TextKey.of("iris.desktop.imagemap.preview", "Preview");
|
||||
public static final TextKey IMAGEMAP_EXPORT = TextKey.of("iris.desktop.imagemap.export", "Export to active pack");
|
||||
public static final TextKey IMAGEMAP_METADATA = TextKey.of("iris.desktop.imagemap.metadata", "Source metadata");
|
||||
public static final TextKey IMAGEMAP_RESOURCE = TextKey.of("iris.desktop.imagemap.resource", "Resource binding");
|
||||
public static final TextKey IMAGEMAP_COORDINATES = TextKey.of("iris.desktop.imagemap.coordinates", "Coordinates and sampling");
|
||||
public static final TextKey IMAGEMAP_BINDING_KEY = TextKey.of("iris.desktop.imagemap.binding_key", "Binding key");
|
||||
public static final TextKey IMAGEMAP_MAP_KEY = TextKey.of("iris.desktop.imagemap.map_key", "Map key");
|
||||
public static final TextKey IMAGEMAP_IMAGE_KEY = TextKey.of("iris.desktop.imagemap.image_key", "Image key");
|
||||
public static final TextKey IMAGEMAP_TYPE = TextKey.of("iris.desktop.imagemap.type", "Type");
|
||||
public static final TextKey IMAGEMAP_APPLICATION = TextKey.of("iris.desktop.imagemap.application", "Application");
|
||||
public static final TextKey IMAGEMAP_BLOCKS_PER_PIXEL = TextKey.of("iris.desktop.imagemap.blocks_per_pixel", "Blocks per pixel");
|
||||
public static final TextKey IMAGEMAP_ORIGIN_X = TextKey.of("iris.desktop.imagemap.origin_x", "World origin X");
|
||||
public static final TextKey IMAGEMAP_ORIGIN_Z = TextKey.of("iris.desktop.imagemap.origin_z", "World origin Z");
|
||||
public static final TextKey IMAGEMAP_SOURCE_ORIGIN_X = TextKey.of("iris.desktop.imagemap.source_origin_x", "Source origin X");
|
||||
public static final TextKey IMAGEMAP_SOURCE_ORIGIN_Z = TextKey.of("iris.desktop.imagemap.source_origin_z", "Source origin Z");
|
||||
public static final TextKey IMAGEMAP_ROTATION = TextKey.of("iris.desktop.imagemap.rotation", "Rotation");
|
||||
public static final TextKey IMAGEMAP_MIRROR_X = TextKey.of("iris.desktop.imagemap.mirror_x", "Mirror X");
|
||||
public static final TextKey IMAGEMAP_MIRROR_Z = TextKey.of("iris.desktop.imagemap.mirror_z", "Mirror Z");
|
||||
public static final TextKey IMAGEMAP_SAMPLING = TextKey.of("iris.desktop.imagemap.sampling", "Sampling");
|
||||
public static final TextKey IMAGEMAP_OUT_OF_BOUNDS = TextKey.of("iris.desktop.imagemap.out_of_bounds", "Out of bounds");
|
||||
public static final TextKey IMAGEMAP_ALPHA = TextKey.of("iris.desktop.imagemap.alpha", "Alpha policy");
|
||||
public static final TextKey IMAGEMAP_FALLBACK_VALUE = TextKey.of("iris.desktop.imagemap.fallback_value", "Fallback value");
|
||||
public static final TextKey IMAGEMAP_FALLBACK_TARGET = TextKey.of("iris.desktop.imagemap.fallback_target", "Fallback target");
|
||||
public static final TextKey IMAGEMAP_MINIMUM_HEIGHT = TextKey.of("iris.desktop.imagemap.minimum_height", "Minimum height");
|
||||
public static final TextKey IMAGEMAP_MAXIMUM_HEIGHT = TextKey.of("iris.desktop.imagemap.maximum_height", "Maximum height");
|
||||
public static final TextKey IMAGEMAP_VERTICAL_OFFSET = TextKey.of("iris.desktop.imagemap.vertical_offset", "Vertical offset");
|
||||
public static final TextKey IMAGEMAP_CLAMP = TextKey.of("iris.desktop.imagemap.clamp", "Clamp height");
|
||||
public static final TextKey IMAGEMAP_INVERTED = TextKey.of("iris.desktop.imagemap.inverted", "Invert values");
|
||||
public static final TextKey IMAGEMAP_CURVE_EXPONENT = TextKey.of("iris.desktop.imagemap.curve_exponent", "Curve exponent");
|
||||
public static final TextKey IMAGEMAP_SMOOTHING_RADIUS = TextKey.of("iris.desktop.imagemap.smoothing_radius", "Smoothing radius");
|
||||
public static final TextKey IMAGEMAP_THRESHOLD = TextKey.of("iris.desktop.imagemap.threshold", "Threshold");
|
||||
public static final TextKey IMAGEMAP_FALLOFF = TextKey.of("iris.desktop.imagemap.falloff", "Falloff");
|
||||
public static final TextKey IMAGEMAP_COLOR_TOLERANCE = TextKey.of("iris.desktop.imagemap.color_tolerance", "Raw sRGB tolerance");
|
||||
public static final TextKey IMAGEMAP_UNKNOWN_COLOR = TextKey.of("iris.desktop.imagemap.unknown_color", "Unknown color");
|
||||
public static final TextKey IMAGEMAP_ADD_COLOR = TextKey.of("iris.desktop.imagemap.add_color", "Add legend color");
|
||||
public static final TextKey IMAGEMAP_REMOVE_COLOR = TextKey.of("iris.desktop.imagemap.remove_color", "Remove selected");
|
||||
public static final TextKey IMAGEMAP_COMPOSED_MASKS = TextKey.of("iris.desktop.imagemap.composed_masks", "Composed masks");
|
||||
public static final TextKey IMAGEMAP_ADD_MASK = TextKey.of("iris.desktop.imagemap.add_mask", "Add mask binding");
|
||||
public static final TextKey IMAGEMAP_HEIGHT = TextKey.of("iris.desktop.imagemap.height", "Height interpretation");
|
||||
public static final TextKey IMAGEMAP_MASK = TextKey.of("iris.desktop.imagemap.mask", "Mask interpretation");
|
||||
public static final TextKey IMAGEMAP_COLOR_MAP = TextKey.of("iris.desktop.imagemap.color_map", "Color legend");
|
||||
public static final TextKey IMAGEMAP_OVERLAYS = TextKey.of("iris.desktop.imagemap.overlays", "World overlays");
|
||||
public static final TextKey IMAGEMAP_CHUNKS = TextKey.of("iris.desktop.imagemap.chunks", "16-block chunks");
|
||||
public static final TextKey IMAGEMAP_REGIONS = TextKey.of("iris.desktop.imagemap.regions", "512-block regions");
|
||||
public static final TextKey IMAGEMAP_BOUNDARY = TextKey.of("iris.desktop.imagemap.boundary", "World boundary");
|
||||
public static final TextKey IMAGEMAP_COVERAGE = TextKey.of("iris.desktop.imagemap.coverage", "Source coverage");
|
||||
public static final TextKey IMAGEMAP_DIAGNOSTICS = TextKey.of("iris.desktop.imagemap.diagnostics", "Diagnostics");
|
||||
public static final TextKey IMAGEMAP_READY = TextKey.of("iris.desktop.imagemap.ready", "Ready");
|
||||
public static final TextKey IMAGEMAP_NO_SOURCE = TextKey.of("iris.desktop.imagemap.no_source", "Import a PNG source to begin.");
|
||||
public static final TextKey IMAGEMAP_LOADING = TextKey.of("iris.desktop.imagemap.loading", "Loading preset...");
|
||||
public static final TextKey IMAGEMAP_LOAD_FAILED = TextKey.of("iris.desktop.imagemap.load_failed", "Preset load failed");
|
||||
public static final TextKey IMAGEMAP_PREVIEWING = TextKey.of("iris.desktop.imagemap.previewing", "Compiling preview...");
|
||||
public static final TextKey IMAGEMAP_PREVIEW_VALID = TextKey.of("iris.desktop.imagemap.preview_valid", "Runtime compiler validation passed.");
|
||||
public static final TextKey IMAGEMAP_PREVIEW_FAILED = TextKey.of("iris.desktop.imagemap.preview_failed", "Preview failed");
|
||||
public static final TextKey IMAGEMAP_EXPORTING = TextKey.of("iris.desktop.imagemap.exporting", "Validating and exporting...");
|
||||
public static final TextKey IMAGEMAP_EXPORTED = TextKey.of("iris.desktop.imagemap.exported", "Exported image map, PNG, and dimension binding atomically.");
|
||||
public static final TextKey IMAGEMAP_EXPORT_FAILED = TextKey.of("iris.desktop.imagemap.export_failed", "Export failed");
|
||||
public static final TextKey IMAGEMAP_SOURCE = TextKey.of("iris.desktop.imagemap.source", "Source pixels");
|
||||
public static final TextKey IMAGEMAP_INTERPRETED = TextKey.of("iris.desktop.imagemap.interpreted", "Runtime interpretation");
|
||||
public static final TextKey IMAGEMAP_NO_PREVIEW = TextKey.of("iris.desktop.imagemap.no_preview", "Compile a valid preview to inspect world output.");
|
||||
public static final TextKey IMAGEMAP_PREVIEW_STATUS = TextKey.of("iris.desktop.imagemap.preview_status", "X {x} Z {z} | {value} | {scale} blocks/pixel");
|
||||
|
||||
private static final List<MessageKey> KEYS = List.of(
|
||||
VISION_TITLE, VISION_VIEW, VISION_GRID, VISION_FOLLOW,
|
||||
@@ -112,7 +178,24 @@ public final class DesktopUiMessages {
|
||||
NOISE_CATEGORY_UTILITY, NOISE_CATEGORY_OTHER, PREGEN_INITIALIZING, PREGEN_TITLE,
|
||||
PREGEN_METHOD_PENDING, PREGEN_PAUSED, PREGEN_RESUME_HINT, PREGEN_PAUSE_HINT,
|
||||
PREGEN_PROGRESS_PAUSED, PREGEN_PROGRESS_SAVING, PREGEN_PROGRESS_GENERATING, PREGEN_SPEED,
|
||||
PREGEN_SPEED_CACHED, PREGEN_TIME, PREGEN_METHOD, PREGEN_MEMORY
|
||||
PREGEN_SPEED_CACHED, PREGEN_TIME, PREGEN_METHOD, PREGEN_MEMORY,
|
||||
IMAGEMAP_TITLE, IMAGEMAP_PRESET, IMAGEMAP_LOAD, IMAGEMAP_IMPORT_PNG,
|
||||
IMAGEMAP_REPLACE_PNG, IMAGEMAP_PREVIEW, IMAGEMAP_EXPORT, IMAGEMAP_METADATA,
|
||||
IMAGEMAP_RESOURCE, IMAGEMAP_COORDINATES, IMAGEMAP_BINDING_KEY, IMAGEMAP_MAP_KEY,
|
||||
IMAGEMAP_IMAGE_KEY, IMAGEMAP_TYPE, IMAGEMAP_APPLICATION, IMAGEMAP_BLOCKS_PER_PIXEL,
|
||||
IMAGEMAP_ORIGIN_X, IMAGEMAP_ORIGIN_Z, IMAGEMAP_SOURCE_ORIGIN_X, IMAGEMAP_SOURCE_ORIGIN_Z,
|
||||
IMAGEMAP_ROTATION, IMAGEMAP_MIRROR_X, IMAGEMAP_MIRROR_Z, IMAGEMAP_SAMPLING,
|
||||
IMAGEMAP_OUT_OF_BOUNDS, IMAGEMAP_ALPHA, IMAGEMAP_FALLBACK_VALUE, IMAGEMAP_FALLBACK_TARGET,
|
||||
IMAGEMAP_MINIMUM_HEIGHT, IMAGEMAP_MAXIMUM_HEIGHT, IMAGEMAP_VERTICAL_OFFSET, IMAGEMAP_CLAMP,
|
||||
IMAGEMAP_INVERTED, IMAGEMAP_CURVE_EXPONENT, IMAGEMAP_SMOOTHING_RADIUS, IMAGEMAP_THRESHOLD,
|
||||
IMAGEMAP_FALLOFF, IMAGEMAP_COLOR_TOLERANCE, IMAGEMAP_UNKNOWN_COLOR, IMAGEMAP_ADD_COLOR,
|
||||
IMAGEMAP_REMOVE_COLOR, IMAGEMAP_COMPOSED_MASKS, IMAGEMAP_ADD_MASK,
|
||||
IMAGEMAP_HEIGHT, IMAGEMAP_MASK, IMAGEMAP_COLOR_MAP,
|
||||
IMAGEMAP_OVERLAYS, IMAGEMAP_CHUNKS, IMAGEMAP_REGIONS, IMAGEMAP_BOUNDARY,
|
||||
IMAGEMAP_COVERAGE, IMAGEMAP_DIAGNOSTICS, IMAGEMAP_READY, IMAGEMAP_NO_SOURCE,
|
||||
IMAGEMAP_LOADING, IMAGEMAP_LOAD_FAILED, IMAGEMAP_PREVIEWING, IMAGEMAP_PREVIEW_VALID,
|
||||
IMAGEMAP_PREVIEW_FAILED, IMAGEMAP_EXPORTING, IMAGEMAP_EXPORTED, IMAGEMAP_EXPORT_FAILED,
|
||||
IMAGEMAP_SOURCE, IMAGEMAP_INTERPRETED, IMAGEMAP_NO_PREVIEW, IMAGEMAP_PREVIEW_STATUS
|
||||
);
|
||||
|
||||
private DesktopUiMessages() {
|
||||
|
||||
@@ -907,6 +907,14 @@ public final class ModdedCommandMessages {
|
||||
"iris.modded.moddedstudiocommands.opening_vision_map_on_server_display",
|
||||
"Opening the Vision map for " + "{value}" + " on the server display."
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_IMAGE_MAP_REQUIRES_IRIS_DIMENSION = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension",
|
||||
"Stand in an active Iris or Studio dimension before opening the Image Map Studio."
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display",
|
||||
"Opening the Image Map Studio for " + "{value}" + " on the server display."
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.failed_write_workspace",
|
||||
"Failed to write workspace for " + "{value}" + ": " + "{value2}"
|
||||
@@ -1462,6 +1470,8 @@ public final class ModdedCommandMessages {
|
||||
MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED,
|
||||
MODDED_STUDIO_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_STAND_IRIS_STUDIO,
|
||||
MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY,
|
||||
MODDED_STUDIO_COMMANDS_IMAGE_MAP_REQUIRES_IRIS_DIMENSION,
|
||||
MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
||||
MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE,
|
||||
MODDED_STUDIO_COMMANDS_WORKSPACE_REGENERATED_WITH_JSON_SCHEMAS_AUTOCOMPLETE,
|
||||
MODDED_STUDIO_COMMANDS_COULD_NOT_OPEN,
|
||||
|
||||
@@ -104,10 +104,6 @@ public final class PackDownloadMessages {
|
||||
"iris.runtime.pack_download.downloading",
|
||||
"Downloading {url}"
|
||||
);
|
||||
public static final TextKey FAILED_TO_FIND = TextKey.of(
|
||||
"iris.runtime.pack_download.failed_to_find",
|
||||
"Failed to find pack at {url}"
|
||||
);
|
||||
public static final TextKey UNPACKING = TextKey.of(
|
||||
"iris.runtime.pack_download.unpacking",
|
||||
"Unpacking {repository}"
|
||||
@@ -213,7 +209,6 @@ public final class PackDownloadMessages {
|
||||
INVALID_BUILT_IN,
|
||||
SHUTTING_DOWN,
|
||||
DOWNLOADING,
|
||||
FAILED_TO_FIND,
|
||||
UNPACKING,
|
||||
UNPACK_FAILED,
|
||||
NO_EXTRACTED_FILES,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ImageMapPackageClosure {
|
||||
private ImageMapPackageClosure() {
|
||||
}
|
||||
|
||||
public static String writeAll(IrisData data, File targetRoot, boolean minify) throws IOException {
|
||||
String[] possibleKeys = data.getImageMapLoader().getPossibleKeys();
|
||||
Arrays.sort(possibleKeys);
|
||||
Set<String> imageKeys = new LinkedHashSet<>();
|
||||
StringBuilder hashes = new StringBuilder();
|
||||
for (String mapKey : possibleKeys) {
|
||||
IrisImageMap map = data.getImageMapLoader().load(mapKey);
|
||||
File mapFile = data.getImageMapLoader().findFile(mapKey);
|
||||
if (map == null || mapFile == null || !mapFile.isFile()) {
|
||||
throw new IOException("Image-map resource '" + mapKey + "' could not be packaged");
|
||||
}
|
||||
String json = new JSONObject(IO.readAll(mapFile)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(targetRoot, "image-maps/" + mapKey + ".json"), json);
|
||||
hashes.append(IO.hash(json));
|
||||
String source = map.getSource();
|
||||
if (source == null || source.isBlank()) {
|
||||
throw new IOException("Image-map resource '" + mapKey + "' has no source");
|
||||
}
|
||||
imageKeys.add(source);
|
||||
}
|
||||
for (String imageKey : imageKeys) {
|
||||
File imageFile = data.getImageLoader().findFile(imageKey);
|
||||
if (imageFile == null || !imageFile.isFile()) {
|
||||
throw new IOException("Image-map source '" + imageKey + "' could not be packaged");
|
||||
}
|
||||
IO.copyFile(imageFile, new File(targetRoot, "images/" + imageKey + ".png"));
|
||||
hashes.append(IO.hash(imageFile));
|
||||
}
|
||||
return IO.hash(hashes.toString());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimensionType;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
@@ -47,6 +48,7 @@ final class PackDimensionValidator {
|
||||
|
||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
@@ -279,6 +281,85 @@ final class PackDimensionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
static void validateWorldBoundary(String dimensionKey, JSONObject dimension, List<String> blockingErrors) {
|
||||
if (!dimension.has("worldBoundary")) {
|
||||
return;
|
||||
}
|
||||
if (dimension.isNull("worldBoundary")) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' worldBoundary must be an object.");
|
||||
return;
|
||||
}
|
||||
JSONObject boundary = dimension.optJSONObject("worldBoundary");
|
||||
if (boundary == null) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' worldBoundary must be an object.");
|
||||
return;
|
||||
}
|
||||
|
||||
String context = "Dimension '" + dimensionKey + "' worldBoundary";
|
||||
validateFiniteNumber(boundary, "size", 1D, IrisWorldBoundary.MAXIMUM_SIZE, context, blockingErrors);
|
||||
validateInteger(boundary, "warningDistance", 0, Integer.MAX_VALUE, context, blockingErrors);
|
||||
validateFiniteNumber(boundary, "damageBuffer", 0D, Double.MAX_VALUE, context, blockingErrors);
|
||||
validateFiniteNumber(boundary, "damageAmount", 0D, Double.MAX_VALUE, context, blockingErrors);
|
||||
|
||||
if (!boundary.has("center")) {
|
||||
return;
|
||||
}
|
||||
if (boundary.isNull("center")) {
|
||||
blockingErrors.add(context + ".center must be an object.");
|
||||
return;
|
||||
}
|
||||
JSONObject center = boundary.optJSONObject("center");
|
||||
if (center == null) {
|
||||
blockingErrors.add(context + ".center must be an object.");
|
||||
return;
|
||||
}
|
||||
validateFiniteNumber(center, "x", -IrisWorldBoundary.MAXIMUM_CENTER,
|
||||
IrisWorldBoundary.MAXIMUM_CENTER, context + ".center", blockingErrors);
|
||||
validateFiniteNumber(center, "z", -IrisWorldBoundary.MAXIMUM_CENTER,
|
||||
IrisWorldBoundary.MAXIMUM_CENTER, context + ".center", blockingErrors);
|
||||
}
|
||||
|
||||
private static void validateFiniteNumber(JSONObject owner, String field, double minimum, double maximum,
|
||||
String context, List<String> blockingErrors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
Object raw = owner.opt(field);
|
||||
if (!(raw instanceof Number number)) {
|
||||
blockingErrors.add(context + "." + field + " must be a number.");
|
||||
return;
|
||||
}
|
||||
double value = number.doubleValue();
|
||||
if (!Double.isFinite(value) || value < minimum || value > maximum) {
|
||||
blockingErrors.add(context + "." + field + " must be finite and between "
|
||||
+ decimalLabel(minimum) + " and " + decimalLabel(maximum) + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateInteger(JSONObject owner, String field, int minimum, int maximum,
|
||||
String context, List<String> blockingErrors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
Object raw = owner.opt(field);
|
||||
if (!(raw instanceof Number number)) {
|
||||
blockingErrors.add(context + "." + field + " must be an integer.");
|
||||
return;
|
||||
}
|
||||
double value = number.doubleValue();
|
||||
if (!Double.isFinite(value) || value != Math.rint(value) || value < minimum || value > maximum) {
|
||||
blockingErrors.add(context + "." + field + " must be an integer between "
|
||||
+ minimum + " and " + maximum + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static String decimalLabel(double value) {
|
||||
if (Math.abs(value) <= Long.MAX_VALUE && value == Math.rint(value)) {
|
||||
return Long.toString((long) value);
|
||||
}
|
||||
return Double.toString(value);
|
||||
}
|
||||
|
||||
private static JSONObject resolveDimensionHeight(File packFolder, JSONObject dimJson) {
|
||||
if (!dimJson.has("dimensionHeight") || dimJson.isNull("dimensionHeight")) {
|
||||
return null;
|
||||
|
||||
@@ -358,23 +358,25 @@ public final class PackDownloader {
|
||||
PackDownloadMessages.DOWNLOADING,
|
||||
MessageArgument.untrusted("url", source)
|
||||
) + " ");
|
||||
File zip = WebCache.getNonCachedFile(
|
||||
"pack-archive",
|
||||
url,
|
||||
ARCHIVE_LIMITS.maxArchiveBytes(),
|
||||
transfer -> sendProgress(progressListener, DownloadProgress.transfer(transfer))
|
||||
);
|
||||
File zip;
|
||||
try {
|
||||
zip = WebCache.getNonCachedFile(
|
||||
"pack-archive",
|
||||
url,
|
||||
ARCHIVE_LIMITS.maxArchiveBytes(),
|
||||
transfer -> sendProgress(progressListener, DownloadProgress.transfer(transfer))
|
||||
);
|
||||
} catch (InterruptedIOException exception) {
|
||||
cancellation.checkpoint();
|
||||
throw exception;
|
||||
}
|
||||
cancellation.checkpoint();
|
||||
File temp = WebCache.getTemp();
|
||||
File work = new File(temp, "dl-" + UUID.randomUUID());
|
||||
|
||||
try {
|
||||
if (zip == null || !zip.exists()) {
|
||||
sendFeedback(feedback, IrisLanguage.plain(
|
||||
PackDownloadMessages.FAILED_TO_FIND,
|
||||
MessageArgument.untrusted("url", source)
|
||||
));
|
||||
return null;
|
||||
if (!zip.exists()) {
|
||||
throw new IOException("Downloaded pack archive is missing before unpacking.");
|
||||
}
|
||||
sendProgress(progressListener, DownloadProgress.phase(DownloadPhase.UNPACKING));
|
||||
sendFeedback(feedback, IrisLanguage.plain(
|
||||
|
||||
@@ -0,0 +1,907 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.engine.image.IrisImageMapCompiler;
|
||||
import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapBinding;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRange;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
final class PackImageMapValidator {
|
||||
private static final byte[] PNG_SIGNATURE = {
|
||||
(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
|
||||
};
|
||||
|
||||
private PackImageMapValidator() {
|
||||
}
|
||||
|
||||
static Validation validate(File packFolder, File[] dimensionFiles, boolean validateLiveRegistries) {
|
||||
Set<String> blockingErrors = new LinkedHashSet<>();
|
||||
Set<String> warnings = new LinkedHashSet<>();
|
||||
try {
|
||||
if (!requiresValidation(packFolder)) {
|
||||
return new Validation(List.of(), List.of());
|
||||
}
|
||||
} catch (IOException error) {
|
||||
IrisLogging.reportError("Image-map feature detection failed for '" + packFolder.getName() + "'.", error);
|
||||
blockingErrors.add("Image-map validation could not inspect pack '" + packFolder.getName()
|
||||
+ "': " + failureMessage(error) + ".");
|
||||
return new Validation(List.copyOf(blockingErrors), List.of());
|
||||
}
|
||||
IrisData data = null;
|
||||
try {
|
||||
data = IrisData.openDatapackCompiler(packFolder);
|
||||
Map<String, MapResource> resources = compileResources(data, blockingErrors);
|
||||
PlatformRegistries registries = liveRegistries(validateLiveRegistries);
|
||||
validateDimensions(packFolder, dimensionFiles, data, resources, registries, blockingErrors, warnings);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Image-map pack validation failed for '" + packFolder.getName() + "'.", error);
|
||||
blockingErrors.add("Image-map validation could not inspect pack '" + packFolder.getName()
|
||||
+ "': " + failureMessage(error) + ".");
|
||||
} finally {
|
||||
if (data != null) {
|
||||
try {
|
||||
data.close();
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Failed to close detached image-map validation data for '"
|
||||
+ packFolder.getName() + "'.", error);
|
||||
blockingErrors.add("Image-map validation could not release detached pack data: "
|
||||
+ failureMessage(error) + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Validation(List.copyOf(blockingErrors), List.copyOf(warnings));
|
||||
}
|
||||
|
||||
private static boolean requiresValidation(File packFolder) throws IOException {
|
||||
File imageMapsFolder = new File(packFolder, "image-maps");
|
||||
if (imageMapsFolder.isDirectory()) {
|
||||
try (Stream<Path> paths = Files.walk(imageMapsFolder.toPath())) {
|
||||
if (paths.anyMatch(path -> Files.isRegularFile(path)
|
||||
&& path.getFileName().toString().endsWith(".json"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(packFolder.toPath())) {
|
||||
List<Path> jsonFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.toList();
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
try {
|
||||
if (containsImageMapKey(JsonParser.parseString(Files.readString(jsonFile)))) {
|
||||
return true;
|
||||
}
|
||||
} catch (JsonParseException error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsImageMapKey(JsonElement element) {
|
||||
if (element == null || element.isJsonNull() || element.isJsonPrimitive()) {
|
||||
return false;
|
||||
}
|
||||
if (element.isJsonArray()) {
|
||||
for (JsonElement child : element.getAsJsonArray()) {
|
||||
if (containsImageMapKey(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
if (object.has("imageMap") || object.has("imageMaps")) {
|
||||
return true;
|
||||
}
|
||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||
if (containsImageMapKey(entry.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Map<String, MapResource> compileResources(IrisData data, Set<String> blockingErrors) {
|
||||
String[] possibleKeys = data.getImageMapLoader().getPossibleKeys();
|
||||
Arrays.sort(possibleKeys);
|
||||
Set<String> imageKeys = new LinkedHashSet<>(Arrays.asList(data.getImageLoader().getPossibleKeys()));
|
||||
Map<String, MapResource> resources = new LinkedHashMap<>();
|
||||
for (String mapKey : possibleKeys) {
|
||||
IrisImageMap definition = data.getImageMapLoader().load(mapKey, false);
|
||||
if (definition == null) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' is invalid and could not be parsed.");
|
||||
resources.put(mapKey, new MapResource(null, null));
|
||||
continue;
|
||||
}
|
||||
String source = normalized(definition.getSource());
|
||||
if (source == null) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' source must not be blank.");
|
||||
resources.put(mapKey, new MapResource(definition, null));
|
||||
continue;
|
||||
}
|
||||
if (!imageKeys.contains(source)) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' references missing PNG source '"
|
||||
+ source + "'.");
|
||||
resources.put(mapKey, new MapResource(definition, null));
|
||||
continue;
|
||||
}
|
||||
File sourceFile = data.getImageLoader().findFile(source);
|
||||
if (sourceFile == null || !sourceFile.isFile()) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' references missing PNG source '"
|
||||
+ source + "'.");
|
||||
resources.put(mapKey, new MapResource(definition, null));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!hasPngSignature(sourceFile)) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' source '" + source
|
||||
+ "' is not a PNG file.");
|
||||
}
|
||||
} catch (IOException error) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' source '" + source
|
||||
+ "' could not be read: " + failureMessage(error) + ".");
|
||||
}
|
||||
IrisImage image = data.getImageLoader().load(source, false);
|
||||
if (image == null) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "' source '" + source
|
||||
+ "' is corrupt or unsupported.");
|
||||
resources.put(mapKey, new MapResource(definition, null));
|
||||
continue;
|
||||
}
|
||||
CompiledIrisImageMap compiled = null;
|
||||
try {
|
||||
compiled = IrisImageMapCompiler.compile(definition, image).withoutDecodedValues();
|
||||
} catch (IrisImageMapValidationException validation) {
|
||||
for (String diagnostic : validation.getDiagnostics()) {
|
||||
blockingErrors.add("Image-map resource '" + mapKey + "': " + diagnostic + ".");
|
||||
}
|
||||
} finally {
|
||||
data.getImageLoader().unload(source);
|
||||
}
|
||||
resources.put(mapKey, new MapResource(definition, compiled));
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
private static void validateDimensions(
|
||||
File packFolder,
|
||||
File[] dimensionFiles,
|
||||
IrisData data,
|
||||
Map<String, MapResource> resources,
|
||||
PlatformRegistries registries,
|
||||
Set<String> blockingErrors,
|
||||
Set<String> warnings
|
||||
) {
|
||||
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length);
|
||||
Arrays.sort(sortedFiles, (File first, File second) -> first.getName().compareTo(second.getName()));
|
||||
Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
|
||||
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
|
||||
for (File dimensionFile : sortedFiles) {
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
JSONObject raw = PackValidationIo.readJson(dimensionFile);
|
||||
if (raw == null) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
dimensions.put(dimensionKey, dimension);
|
||||
if (raw.has("imageMaps")) {
|
||||
JSONArray rawBindings = raw.optJSONArray("imageMaps");
|
||||
if (rawBindings == null) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' imageMaps must be an array.");
|
||||
} else if (dimension.getImageMaps() == null) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey
|
||||
+ "' imageMaps is invalid and could not be parsed.");
|
||||
} else {
|
||||
bindingReadyDimensions.add(dimensionKey);
|
||||
}
|
||||
} else {
|
||||
bindingReadyDimensions.add(dimensionKey);
|
||||
}
|
||||
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
|
||||
}
|
||||
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>();
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
String upperKey = normalized(entry.getValue().getUpperDimension());
|
||||
if (upperKey == null || upperKey.equalsIgnoreCase("none")) {
|
||||
continue;
|
||||
}
|
||||
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
if (!bindingReadyDimensions.contains(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey());
|
||||
if (parents == null || parents.isEmpty()) {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
continue;
|
||||
}
|
||||
if (entry.getValue().getWorldBoundary() != null) {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
}
|
||||
parents.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBindings(
|
||||
File packFolder,
|
||||
String dimensionContext,
|
||||
IrisDimension dimension,
|
||||
IrisWorldBoundary enforcedBoundary,
|
||||
IrisData data,
|
||||
Map<String, MapResource> resources,
|
||||
PlatformRegistries registries,
|
||||
Set<String> blockingErrors,
|
||||
Set<String> warnings
|
||||
) {
|
||||
Map<String, IrisImageMapBinding> bindings = new LinkedHashMap<>();
|
||||
Map<IrisImageMapApplication, String> applications = new EnumMap<>(IrisImageMapApplication.class);
|
||||
List<IrisImageMapBinding> declared = dimension.getImageMaps();
|
||||
Set<String> regionKeys = allRegionKeys(dimension, resources);
|
||||
for (int index = 0; index < declared.size(); index++) {
|
||||
IrisImageMapBinding binding = declared.get(index);
|
||||
String indexContext = dimensionContext + " imageMaps[" + index + "]";
|
||||
if (binding == null) {
|
||||
blockingErrors.add(indexContext + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
String key = normalized(binding.getKey());
|
||||
if (key == null) {
|
||||
blockingErrors.add(indexContext + ".key must not be blank.");
|
||||
} else if (bindings.putIfAbsent(key, binding) != null) {
|
||||
blockingErrors.add(dimensionContext + " declares duplicate image-map key '"
|
||||
+ key + "'.");
|
||||
}
|
||||
IrisImageMapApplication application = binding.getApplication();
|
||||
if (application == null) {
|
||||
blockingErrors.add(indexContext + ".application is required.");
|
||||
} else if (application != IrisImageMapApplication.MASK
|
||||
&& application != IrisImageMapApplication.CUSTOM) {
|
||||
String duplicate = applications.putIfAbsent(application, key == null ? indexContext : key);
|
||||
if (duplicate != null) {
|
||||
blockingErrors.add(dimensionContext + " declares more than one "
|
||||
+ application + " image-map binding.");
|
||||
}
|
||||
}
|
||||
validateMapReference(dimensionContext, indexContext, binding, key, dimension, enforcedBoundary, data,
|
||||
regionKeys, resources, registries, packFolder, blockingErrors, warnings);
|
||||
}
|
||||
for (int index = 0; index < declared.size(); index++) {
|
||||
IrisImageMapBinding binding = declared.get(index);
|
||||
if (binding != null) {
|
||||
validateMasks(dimensionContext, index, binding, bindings, blockingErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMapReference(
|
||||
String dimensionContext,
|
||||
String indexContext,
|
||||
IrisImageMapBinding binding,
|
||||
String bindingKey,
|
||||
IrisDimension dimension,
|
||||
IrisWorldBoundary enforcedBoundary,
|
||||
IrisData data,
|
||||
Set<String> regionKeys,
|
||||
Map<String, MapResource> resources,
|
||||
PlatformRegistries registries,
|
||||
File packFolder,
|
||||
Set<String> blockingErrors,
|
||||
Set<String> warnings
|
||||
) {
|
||||
String mapKey = normalized(binding.getMap());
|
||||
if (mapKey == null) {
|
||||
blockingErrors.add(indexContext + ".map must not be blank.");
|
||||
return;
|
||||
}
|
||||
MapResource resource = resources.get(mapKey);
|
||||
if (resource == null) {
|
||||
blockingErrors.add(indexContext + " references missing image-map resource '" + mapKey + "'.");
|
||||
return;
|
||||
}
|
||||
IrisImageMap definition = resource.definition();
|
||||
IrisImageMapApplication application = binding.getApplication();
|
||||
if (definition == null || application == null) {
|
||||
return;
|
||||
}
|
||||
IrisImageMapType type = definition.getType();
|
||||
String context = dimensionContext + " image-map '"
|
||||
+ (bindingKey == null ? mapKey : bindingKey) + "'";
|
||||
if (type != null && !compatible(application, type)) {
|
||||
blockingErrors.add(context + " type " + type + " is incompatible with " + application + ".");
|
||||
}
|
||||
if (application == IrisImageMapApplication.MASK
|
||||
&& binding.getMasks() != null && !binding.getMasks().isEmpty()) {
|
||||
blockingErrors.add(context + " is a MASK binding and cannot reference additional masks.");
|
||||
}
|
||||
validateLegendTargets(packFolder, context, regionKeys, data, application, definition,
|
||||
registries, blockingErrors);
|
||||
if (application == IrisImageMapApplication.TERRAIN_HEIGHT) {
|
||||
validateTerrainHeightRange(context, dimension, definition, blockingErrors);
|
||||
}
|
||||
if (resource.compiled() != null && isGenerationApplication(application)) {
|
||||
validateBoundaryCoverage(context, enforcedBoundary, definition,
|
||||
resource.compiled(), blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMasks(
|
||||
String dimensionContext,
|
||||
int bindingIndex,
|
||||
IrisImageMapBinding binding,
|
||||
Map<String, IrisImageMapBinding> bindings,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
if (binding.getMasks() == null) {
|
||||
return;
|
||||
}
|
||||
String bindingKey = normalized(binding.getKey());
|
||||
String context = bindingKey == null
|
||||
? dimensionContext + " imageMaps[" + bindingIndex + "]"
|
||||
: dimensionContext + " image-map '" + bindingKey + "'";
|
||||
for (int maskIndex = 0; maskIndex < binding.getMasks().size(); maskIndex++) {
|
||||
IrisImageMapMask mask = binding.getMasks().get(maskIndex);
|
||||
String maskContext = context + " masks[" + maskIndex + "]";
|
||||
if (mask == null) {
|
||||
blockingErrors.add(maskContext + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
String maskKey = normalized(mask.getMap());
|
||||
if (maskKey == null) {
|
||||
blockingErrors.add(maskContext + ".map must not be blank.");
|
||||
} else {
|
||||
IrisImageMapBinding referenced = bindings.get(maskKey);
|
||||
if (referenced == null) {
|
||||
blockingErrors.add(maskContext + " references missing MASK binding '" + maskKey + "'.");
|
||||
} else if (referenced.getApplication() != IrisImageMapApplication.MASK) {
|
||||
blockingErrors.add(maskContext + " references '" + maskKey + "', which is not a MASK binding.");
|
||||
}
|
||||
}
|
||||
if (mask.getOperation() == null) {
|
||||
blockingErrors.add(maskContext + ".operation is required.");
|
||||
}
|
||||
if (!unitRange(mask.getThreshold())) {
|
||||
blockingErrors.add(maskContext + ".threshold must be finite and within 0..1.");
|
||||
}
|
||||
if (!unitRange(mask.getFalloff())) {
|
||||
blockingErrors.add(maskContext + ".falloff must be finite and within 0..1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateLegendTargets(
|
||||
File packFolder,
|
||||
String context,
|
||||
Set<String> regionKeys,
|
||||
IrisData data,
|
||||
IrisImageMapApplication application,
|
||||
IrisImageMap definition,
|
||||
PlatformRegistries registries,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
if (definition.getType() != IrisImageMapType.COLOR_MAP) {
|
||||
return;
|
||||
}
|
||||
Set<String> targets = legendTargets(definition);
|
||||
if (application == IrisImageMapApplication.BIOME) {
|
||||
for (String target : targets) {
|
||||
String local = localIrisTarget(context, "biome", target, blockingErrors);
|
||||
if (local == null) {
|
||||
continue;
|
||||
}
|
||||
File biomeFile = resourceFile(packFolder, "biomes", local);
|
||||
IrisBiome biome = data.getBiomeLoader().load(local, false);
|
||||
if (biomeFile == null || !biomeFile.isFile() || biome == null) {
|
||||
blockingErrors.add(context + " references missing or invalid biome target '" + target + "'.");
|
||||
continue;
|
||||
}
|
||||
validateBiomeRole(packFolder, context, regionKeys, local, blockingErrors);
|
||||
}
|
||||
} else if (application == IrisImageMapApplication.REGION) {
|
||||
for (String target : targets) {
|
||||
String local = localIrisTarget(context, "region", target, blockingErrors);
|
||||
if (local == null) {
|
||||
continue;
|
||||
}
|
||||
File regionFile = resourceFile(packFolder, "regions", local);
|
||||
if (regionFile == null || !regionFile.isFile() || PackValidationIo.readJson(regionFile) == null) {
|
||||
blockingErrors.add(context + " references missing or invalid region target '" + target + "'.");
|
||||
}
|
||||
}
|
||||
} else if (application == IrisImageMapApplication.SURFACE_BLOCK && registries != null) {
|
||||
for (String target : targets) {
|
||||
try {
|
||||
PlatformBlockState block = registries.blockOrNull(target, false);
|
||||
if (block == null) {
|
||||
blockingErrors.add(context + " references unknown surface block target '" + target + "'.");
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Could not validate image-map surface block target '" + target + "'.", error);
|
||||
blockingErrors.add(context + " could not validate surface block target '" + target
|
||||
+ "': " + failureMessage(error) + ".");
|
||||
}
|
||||
}
|
||||
for (String issue : ContentKeyValidator.validateBlockStateProperties(registries, targets)) {
|
||||
blockingErrors.add(context + " surface block target is invalid: " + issue + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBiomeRole(
|
||||
File packFolder,
|
||||
String context,
|
||||
Set<String> regionKeys,
|
||||
String biomeKey,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
Set<String> roles = new LinkedHashSet<>();
|
||||
for (String regionKey : regionKeys) {
|
||||
File regionFile = resourceFile(packFolder, "regions", regionKey);
|
||||
JSONObject region = regionFile == null ? null : PackValidationIo.readJson(regionFile);
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
collectRole(region, "landBiomes", "LAND", biomeKey, roles);
|
||||
collectRole(region, "seaBiomes", "SEA", biomeKey, roles);
|
||||
collectRole(region, "shoreBiomes", "SHORE", biomeKey, roles);
|
||||
}
|
||||
if (roles.size() != 1) {
|
||||
blockingErrors.add(context + " biome target '" + biomeKey
|
||||
+ "' must occur in exactly one landBiomes, seaBiomes, or shoreBiomes role; found "
|
||||
+ roles + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectRole(
|
||||
JSONObject region,
|
||||
String field,
|
||||
String role,
|
||||
String biomeKey,
|
||||
Set<String> roles
|
||||
) {
|
||||
JSONArray values = region.optJSONArray(field);
|
||||
if (values == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < values.length(); index++) {
|
||||
if (biomeKey.equals(values.optString(index, null))) {
|
||||
roles.add(role);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> allRegionKeys(
|
||||
IrisDimension dimension,
|
||||
Map<String, MapResource> resources
|
||||
) {
|
||||
Set<String> regionKeys = new TreeSet<>();
|
||||
if (dimension.getRegions() != null) {
|
||||
for (String regionKey : dimension.getRegions()) {
|
||||
String normalized = normalized(regionKey);
|
||||
if (normalized != null) {
|
||||
regionKeys.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IrisImageMapBinding binding : dimension.getImageMaps()) {
|
||||
if (binding == null || binding.getApplication() != IrisImageMapApplication.REGION) {
|
||||
continue;
|
||||
}
|
||||
MapResource resource = resources.get(normalized(binding.getMap()));
|
||||
if (resource == null || resource.definition() == null) {
|
||||
continue;
|
||||
}
|
||||
for (String target : legendTargets(resource.definition())) {
|
||||
String local = localIrisTargetOrNull(target);
|
||||
if (local != null) {
|
||||
regionKeys.add(local);
|
||||
}
|
||||
}
|
||||
}
|
||||
return regionKeys;
|
||||
}
|
||||
|
||||
private static void validateGeneratorStyleReferences(
|
||||
String dimensionKey,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
Map<String, MapResource> resources,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
Map<String, Object> roots = new TreeMap<>();
|
||||
roots.put("Dimension '" + dimensionKey + "'", dimension);
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
if (region != null && normalized(region.getLoadKey()) != null) {
|
||||
roots.put("Region '" + region.getLoadKey() + "' reachable from dimension '" + dimensionKey + "'", region);
|
||||
}
|
||||
}
|
||||
Set<String> generatorKeys = new TreeSet<>();
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
if (biome == null || normalized(biome.getLoadKey()) == null) {
|
||||
continue;
|
||||
}
|
||||
roots.put("Biome '" + biome.getLoadKey() + "' reachable from dimension '" + dimensionKey + "'", biome);
|
||||
if (biome.getGenerators() == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeGeneratorLink link : biome.getGenerators()) {
|
||||
if (link == null) {
|
||||
continue;
|
||||
}
|
||||
String generatorKey = normalized(link.getGenerator());
|
||||
generatorKeys.add(generatorKey == null ? "default" : generatorKey);
|
||||
}
|
||||
}
|
||||
Set<String> availableGeneratorKeys = new TreeSet<>(Arrays.asList(
|
||||
data.getGeneratorLoader().getPossibleKeys()));
|
||||
for (String generatorKey : generatorKeys) {
|
||||
if (!availableGeneratorKeys.contains(generatorKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisGenerator generator = data.getGeneratorLoader().load(generatorKey, false);
|
||||
if (generator != null) {
|
||||
roots.put("Generator '" + generatorKey + "' reachable from dimension '" + dimensionKey + "'", generator);
|
||||
}
|
||||
}
|
||||
Set<String> visitedExpressions = new TreeSet<>();
|
||||
Set<String> availableExpressionKeys = new TreeSet<>(Arrays.asList(
|
||||
data.getExpressionLoader().getPossibleKeys()));
|
||||
for (Map.Entry<String, Object> entry : roots.entrySet()) {
|
||||
JSONObject root = new JSONObject(data.getGson().toJson(entry.getValue()));
|
||||
scanGeneratorStyles(entry.getKey(), root, data, resources, availableExpressionKeys,
|
||||
visitedExpressions, blockingErrors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanGeneratorStyles(
|
||||
String path,
|
||||
Object value,
|
||||
IrisData data,
|
||||
Map<String, MapResource> resources,
|
||||
Set<String> availableExpressionKeys,
|
||||
Set<String> visitedExpressions,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
if (value instanceof JSONObject object) {
|
||||
if (isGeneratorStyleObject(object)) {
|
||||
validateGeneratorStyleReference(path, object, resources, blockingErrors);
|
||||
String expressionKey = normalized(object.optString("expression", null));
|
||||
if (expressionKey != null && availableExpressionKeys.contains(expressionKey)
|
||||
&& visitedExpressions.add(expressionKey)) {
|
||||
IrisExpression expression = data.getExpressionLoader().load(expressionKey, false);
|
||||
if (expression != null) {
|
||||
JSONObject expressionRoot = new JSONObject(data.getGson().toJson(expression));
|
||||
scanGeneratorStyles("Expression '" + expressionKey + "' reachable from " + path,
|
||||
expressionRoot, data, resources, availableExpressionKeys,
|
||||
visitedExpressions, blockingErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<String> keys = new TreeSet<>(object.keySet());
|
||||
for (String key : keys) {
|
||||
scanGeneratorStyles(path + "." + key, object.opt(key), data, resources,
|
||||
availableExpressionKeys, visitedExpressions, blockingErrors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value instanceof JSONArray array) {
|
||||
for (int index = 0; index < array.length(); index++) {
|
||||
scanGeneratorStyles(path + "[" + index + "]", array.opt(index), data, resources,
|
||||
availableExpressionKeys, visitedExpressions, blockingErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGeneratorStyleObject(JSONObject object) {
|
||||
return object.has("cellularFrequency")
|
||||
&& object.has("cellularZoom")
|
||||
&& object.has("zoom")
|
||||
&& object.has("cacheSize");
|
||||
}
|
||||
|
||||
private static void validateGeneratorStyleReference(
|
||||
String context,
|
||||
JSONObject style,
|
||||
Map<String, MapResource> resources,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
if (!style.has("imageMap")) {
|
||||
return;
|
||||
}
|
||||
String mapKey = normalized(style.optString("imageMap", null));
|
||||
if (mapKey == null) {
|
||||
blockingErrors.add(context + " generator style imageMap must not be blank.");
|
||||
return;
|
||||
}
|
||||
MapResource resource = resources.get(mapKey);
|
||||
if (resource == null) {
|
||||
blockingErrors.add(context + " generator style references missing image-map resource '" + mapKey + "'.");
|
||||
return;
|
||||
}
|
||||
IrisImageMap definition = resource.definition();
|
||||
if (definition == null || definition.getType() == null) {
|
||||
return;
|
||||
}
|
||||
if (definition.getType() == IrisImageMapType.COLOR_MAP) {
|
||||
blockingErrors.add(context + " generator style image-map '" + mapKey
|
||||
+ "' must use a scalar map type, not COLOR_MAP.");
|
||||
}
|
||||
if (definition.getOutOfBounds() == IrisImageMapOutOfBounds.ERROR) {
|
||||
blockingErrors.add(context + " generator style image-map '" + mapKey
|
||||
+ "' cannot use outOfBounds=ERROR because its transformed sampling domain cannot be proven finite;"
|
||||
+ " use FALLBACK, CLAMP, REPEAT, or MIRROR.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTerrainHeightRange(
|
||||
String context,
|
||||
IrisDimension dimension,
|
||||
IrisImageMap definition,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
if (definition.getType() != IrisImageMapType.GRAYSCALE_HEIGHT
|
||||
&& definition.getType() != IrisImageMapType.RGB_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
IrisRange dimensionHeight = dimension.getDimensionHeight();
|
||||
double minimumHeight = definition.getMinimumHeight();
|
||||
double maximumHeight = definition.getMaximumHeight();
|
||||
double verticalOffset = definition.getVerticalOffset();
|
||||
if (dimensionHeight == null
|
||||
|| !Double.isFinite(minimumHeight)
|
||||
|| !Double.isFinite(maximumHeight)
|
||||
|| !Double.isFinite(verticalOffset)
|
||||
|| maximumHeight < minimumHeight) {
|
||||
return;
|
||||
}
|
||||
double effectiveMinimum = minimumHeight + verticalOffset;
|
||||
double effectiveMaximum = maximumHeight + verticalOffset;
|
||||
if (definition.isClamp()) {
|
||||
effectiveMinimum = Math.max(minimumHeight, Math.min(maximumHeight, effectiveMinimum));
|
||||
effectiveMaximum = Math.max(minimumHeight, Math.min(maximumHeight, effectiveMaximum));
|
||||
}
|
||||
if (effectiveMinimum < dimensionHeight.getMin() || effectiveMaximum > dimensionHeight.getMax()) {
|
||||
blockingErrors.add(context + " produces world Y " + effectiveMinimum + ".." + effectiveMaximum
|
||||
+ " after verticalOffset and clamp; the owning dimension allows "
|
||||
+ dimensionHeight.getMin() + ".." + dimensionHeight.getMax() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBoundaryCoverage(
|
||||
String context,
|
||||
IrisWorldBoundary configuredBoundary,
|
||||
IrisImageMap definition,
|
||||
CompiledIrisImageMap compiled,
|
||||
Set<String> blockingErrors,
|
||||
Set<String> warnings
|
||||
) {
|
||||
boolean requiresCoverage = definition.getOutOfBounds() == IrisImageMapOutOfBounds.ERROR;
|
||||
if (configuredBoundary == null) {
|
||||
if (requiresCoverage) {
|
||||
blockingErrors.add(context
|
||||
+ " uses outOfBounds=ERROR and requires a configured worldBoundary.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisWorldBoundary boundary;
|
||||
try {
|
||||
boundary = IrisWorldBoundary.snapshot(configuredBoundary);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return;
|
||||
}
|
||||
double insetX = boundaryInset(boundary.minimumX(), boundary.maximumX());
|
||||
double insetZ = boundaryInset(boundary.minimumZ(), boundary.maximumZ());
|
||||
double minimumX = boundary.minimumX() + insetX;
|
||||
double maximumX = boundary.maximumX() - insetX;
|
||||
double minimumZ = boundary.minimumZ() + insetZ;
|
||||
double maximumZ = boundary.maximumZ() - insetZ;
|
||||
boolean covers = compiled.containsWorldForSampling(minimumX, minimumZ)
|
||||
&& compiled.containsWorldForSampling(minimumX, maximumZ)
|
||||
&& compiled.containsWorldForSampling(maximumX, minimumZ)
|
||||
&& compiled.containsWorldForSampling(maximumX, maximumZ);
|
||||
if (!covers) {
|
||||
String issue = context + " source footprint does not cover the configured worldBoundary.";
|
||||
if (requiresCoverage) {
|
||||
blockingErrors.add(issue);
|
||||
} else {
|
||||
warnings.add(issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> legendTargets(IrisImageMap definition) {
|
||||
Set<String> targets = new TreeSet<>();
|
||||
if (definition.getColors() != null) {
|
||||
for (String target : definition.getColors().values()) {
|
||||
String normalized = normalized(target);
|
||||
if (normalized != null) {
|
||||
targets.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
String fallback = normalized(definition.getFallbackTarget());
|
||||
if (fallback != null) {
|
||||
targets.add(fallback);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static String localIrisTarget(
|
||||
String context,
|
||||
String targetType,
|
||||
String target,
|
||||
Set<String> blockingErrors
|
||||
) {
|
||||
int separator = target.indexOf(':');
|
||||
String local = target;
|
||||
if (separator >= 0) {
|
||||
if (!target.startsWith("iris:") || separator == target.length() - 1) {
|
||||
blockingErrors.add(context + " " + targetType
|
||||
+ " target must be a bare pack key or use the iris: namespace, got '" + target + "'.");
|
||||
return null;
|
||||
}
|
||||
local = target.substring(separator + 1);
|
||||
}
|
||||
if (local.isBlank() || local.startsWith("/") || local.contains("\\")) {
|
||||
blockingErrors.add(context + " " + targetType + " target '" + target + "' is not a valid pack key.");
|
||||
return null;
|
||||
}
|
||||
for (String segment : local.split("/")) {
|
||||
if (segment.isBlank() || segment.equals(".") || segment.equals("..")) {
|
||||
blockingErrors.add(context + " " + targetType + " target '" + target + "' is not a valid pack key.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
private static String localIrisTargetOrNull(String target) {
|
||||
int separator = target.indexOf(':');
|
||||
if (separator < 0) {
|
||||
return target;
|
||||
}
|
||||
if (!target.startsWith("iris:") || separator == target.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
return target.substring(separator + 1);
|
||||
}
|
||||
|
||||
private static File resourceFile(File packFolder, String folder, String key) {
|
||||
String normalizedKey = normalized(key);
|
||||
if (normalizedKey == null || normalizedKey.startsWith("/") || normalizedKey.contains("\\")) {
|
||||
return null;
|
||||
}
|
||||
File root = new File(packFolder, folder);
|
||||
File target = new File(root, normalizedKey + ".json");
|
||||
try {
|
||||
if (!target.getCanonicalFile().toPath().startsWith(root.getCanonicalFile().toPath())) {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException error) {
|
||||
return null;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static PlatformRegistries liveRegistries(boolean validateLiveRegistries) {
|
||||
if (!validateLiveRegistries || !IrisPlatforms.isBound()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
PlatformRegistries registries = IrisPlatforms.get().registries();
|
||||
if (registries == null || registries.blockKeys() == null || registries.blockKeys().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return registries;
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError("Could not read live block registries for image-map validation.", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compatible(IrisImageMapApplication application, IrisImageMapType type) {
|
||||
return switch (application) {
|
||||
case TERRAIN_HEIGHT -> type == IrisImageMapType.GRAYSCALE_HEIGHT
|
||||
|| type == IrisImageMapType.RGB_HEIGHT;
|
||||
case BIOME, REGION, SURFACE_BLOCK -> type == IrisImageMapType.COLOR_MAP;
|
||||
case MASK -> type == IrisImageMapType.BINARY_MASK
|
||||
|| type == IrisImageMapType.GRAYSCALE_MASK
|
||||
|| type == IrisImageMapType.ALPHA_MASK;
|
||||
case CUSTOM -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isGenerationApplication(IrisImageMapApplication application) {
|
||||
return application != IrisImageMapApplication.CUSTOM;
|
||||
}
|
||||
|
||||
private static double boundaryInset(double minimum, double maximum) {
|
||||
return Math.max(Math.ulp(minimum), Math.ulp(maximum)) * 8D;
|
||||
}
|
||||
|
||||
private static boolean hasPngSignature(File file) throws IOException {
|
||||
byte[] actual;
|
||||
try (InputStream input = Files.newInputStream(file.toPath())) {
|
||||
actual = input.readNBytes(PNG_SIGNATURE.length);
|
||||
}
|
||||
return Arrays.equals(PNG_SIGNATURE, actual);
|
||||
}
|
||||
|
||||
private static boolean unitRange(double value) {
|
||||
return Double.isFinite(value) && value >= 0D && value <= 1D;
|
||||
}
|
||||
|
||||
private static String normalized(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String failureMessage(Throwable error) {
|
||||
String message = error.getMessage();
|
||||
return message == null || message.isBlank() ? error.getClass().getSimpleName() : message;
|
||||
}
|
||||
|
||||
record Validation(List<String> errors, List<String> warnings) {
|
||||
}
|
||||
|
||||
private record MapResource(IrisImageMap definition, CompiledIrisImageMap compiled) {
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public final class PackValidator {
|
||||
return validate(packFolder, false);
|
||||
}
|
||||
|
||||
public static PackValidationResult validateForPackaging(File packFolder) {
|
||||
return validate(packFolder, false);
|
||||
}
|
||||
|
||||
private static PackValidationResult validate(File packFolder, boolean validateLiveRegistries) {
|
||||
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
|
||||
List<String> blockingErrors = new ArrayList<>();
|
||||
@@ -76,6 +80,10 @@ public final class PackValidator {
|
||||
}
|
||||
|
||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
|
||||
packFolder, dimensionFiles, validateLiveRegistries);
|
||||
addDistinct(blockingErrors, imageMaps.errors());
|
||||
addDistinct(warnings, imageMaps.warnings());
|
||||
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
|
||||
addDistinct(blockingErrors, riverValidation.errors());
|
||||
addDistinct(warnings, riverValidation.warnings());
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.engine.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.engine.image.IrisImageMapCompiler;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.image.IrisImageMapMaskSampler;
|
||||
import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapBinding;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
public final class ImageMapStudioExporter {
|
||||
private static final Pattern RESOURCE_KEY = Pattern.compile("[a-z0-9][a-z0-9/._-]*");
|
||||
|
||||
private ImageMapStudioExporter() {
|
||||
}
|
||||
|
||||
public static ExportResult export(ExportRequest request) throws IOException {
|
||||
Objects.requireNonNull(request, "Image-map Studio export request");
|
||||
Path pack = requirePack(request.packFolder());
|
||||
String dimensionKey = requireKey(request.dimensionKey(), "Dimension key");
|
||||
String bindingKey = requireKey(request.bindingKey(), "Binding key");
|
||||
String imageMapKey = requireKey(request.imageMapKey(), "Image-map key");
|
||||
String imageKey = requireKey(request.imageKey(), "Image key");
|
||||
IrisImageMapApplication application = Objects.requireNonNull(
|
||||
request.application(), "Image-map application"
|
||||
);
|
||||
IrisImageMap definition = Objects.requireNonNull(request.definition(), "Image-map definition");
|
||||
Path source = Objects.requireNonNull(request.sourcePng(), "Source PNG").toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(source) || Files.isSymbolicLink(source)) {
|
||||
throw new IOException("Source PNG is missing or unsafe: " + source);
|
||||
}
|
||||
|
||||
DecodedSource decoded = decodePng(source);
|
||||
definition.setSource(imageKey);
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(
|
||||
definition, new IrisImage(decoded.image(), decoded.format())
|
||||
);
|
||||
|
||||
Path dimensionTarget = safeTarget(pack, "dimensions", dimensionKey + ".json");
|
||||
Path mapTarget = safeTarget(pack, "image-maps", imageMapKey + ".json");
|
||||
Path imageTarget = safeTarget(pack, "images", imageKey + ".png");
|
||||
IrisDimension dimension;
|
||||
String mapJson;
|
||||
String dimensionJson;
|
||||
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||
try {
|
||||
dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IOException("Dimension resource '" + dimensionKey + "' could not be loaded from " + pack);
|
||||
}
|
||||
IrisDimension updated = data.getGson().fromJson(data.getGson().toJson(dimension), IrisDimension.class);
|
||||
bind(updated, bindingKey, imageMapKey, application, request.masks());
|
||||
Gson gson = new Gson();
|
||||
mapJson = new JSONObject(gson.toJson(definition)).toString(4);
|
||||
dimensionJson = new JSONObject(gson.toJson(updated)).toString(4);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
|
||||
AtomicFileSet files = new AtomicFileSet();
|
||||
try {
|
||||
files.stageCopy(source, imageTarget);
|
||||
files.stageText(mapJson, mapTarget);
|
||||
files.stageText(dimensionJson, dimensionTarget);
|
||||
files.publish();
|
||||
|
||||
PackValidationResult validation = validatePublished(pack, dimensionKey);
|
||||
if (!validation.isLoadable()) {
|
||||
throw new IrisImageMapValidationException(validation.getBlockingErrors());
|
||||
}
|
||||
files.commit();
|
||||
return new ExportResult(
|
||||
imageTarget,
|
||||
mapTarget,
|
||||
dimensionTarget,
|
||||
compiled.getContentHash(),
|
||||
validation.getWarnings()
|
||||
);
|
||||
} finally {
|
||||
files.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static PreviewResult preview(Path sourcePng, IrisImageMap definition) throws IOException {
|
||||
return preview(sourcePng, definition, null, null, List.of());
|
||||
}
|
||||
|
||||
public static PreviewResult preview(
|
||||
Path sourcePng,
|
||||
IrisImageMap definition,
|
||||
File packFolder,
|
||||
String dimensionKey,
|
||||
List<IrisImageMapMask> masks
|
||||
) throws IOException {
|
||||
Path source = Objects.requireNonNull(sourcePng, "Source PNG").toAbsolutePath().normalize();
|
||||
DecodedSource decoded = decodePng(source);
|
||||
IrisImage image = new IrisImage(decoded.image(), decoded.format());
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(definition, image);
|
||||
IrisImageMapMaskSampler maskSampler = compilePreviewMasks(packFolder, dimensionKey, masks);
|
||||
return new PreviewResult(decoded.image(), compiled, decoded.colorProfile(), maskSampler);
|
||||
}
|
||||
|
||||
public static SourceInspection inspectSource(Path sourcePng) throws IOException {
|
||||
Path source = Objects.requireNonNull(sourcePng, "Source PNG").toAbsolutePath().normalize();
|
||||
DecodedSource decoded = decodePng(source);
|
||||
IrisImage image = new IrisImage(decoded.image(), decoded.format());
|
||||
double minimumAlpha = 1D;
|
||||
double maximumAlpha = image.hasAlpha() ? 0D : 1D;
|
||||
if (image.hasAlpha()) {
|
||||
for (int sourceZ = 0; sourceZ < image.getHeight(); sourceZ++) {
|
||||
for (int sourceX = 0; sourceX < image.getWidth(); sourceX++) {
|
||||
double alpha = image.getAlphaNormalized(sourceX, sourceZ);
|
||||
minimumAlpha = Math.min(minimumAlpha, alpha);
|
||||
maximumAlpha = Math.max(maximumAlpha, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new SourceInspection(
|
||||
decoded.image(), decoded.format(), decoded.colorProfile(), minimumAlpha, maximumAlpha
|
||||
);
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler compilePreviewMasks(
|
||||
File packFolder,
|
||||
String dimensionKey,
|
||||
List<IrisImageMapMask> masks
|
||||
) throws IOException {
|
||||
if (masks == null || masks.isEmpty()) {
|
||||
return IrisImageMapMaskSampler.empty();
|
||||
}
|
||||
Path pack = requirePack(packFolder);
|
||||
String dimensionResource = requireKey(dimensionKey, "Dimension key");
|
||||
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||
try {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionResource);
|
||||
if (dimension == null) {
|
||||
throw new IOException("Dimension resource '" + dimensionResource + "' could not be loaded");
|
||||
}
|
||||
List<CompiledIrisImageMap> compiledMasks = new ArrayList<>(masks.size());
|
||||
for (IrisImageMapMask mask : masks) {
|
||||
IrisImageMapBinding binding = findBinding(dimension, mask.getMap());
|
||||
if (binding == null || binding.getApplication() != IrisImageMapApplication.MASK) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Composed mask '" + mask.getMap() + "' does not reference a MASK binding"
|
||||
);
|
||||
}
|
||||
IrisImageMap maskDefinition = data.getImageMapLoader().load(binding.getMap());
|
||||
if (maskDefinition == null) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Composed mask '" + mask.getMap() + "' references missing image-map '"
|
||||
+ binding.getMap() + "'"
|
||||
);
|
||||
}
|
||||
IrisImage maskImage = data.getImageLoader().load(maskDefinition.getSource());
|
||||
if (maskImage == null) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Composed mask '" + mask.getMap() + "' references missing PNG '"
|
||||
+ maskDefinition.getSource() + "'"
|
||||
);
|
||||
}
|
||||
try {
|
||||
compiledMasks.add(CompiledIrisImageMap.compile(maskDefinition, maskImage));
|
||||
} finally {
|
||||
data.getImageLoader().unload(maskDefinition.getSource());
|
||||
}
|
||||
}
|
||||
return IrisImageMapMaskSampler.of(compiledMasks, masks);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisImageMapBinding findBinding(IrisDimension dimension, String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
for (IrisImageMapBinding binding : dimension.getImageMaps()) {
|
||||
if (binding != null && key.equals(binding.getKey())) {
|
||||
return binding;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PackValidationResult validatePublished(Path pack, String dimensionKey) throws IOException {
|
||||
IrisData validationData = IrisData.openDatapackCompiler(pack.toFile());
|
||||
try {
|
||||
IrisDimension published = validationData.getDimensionLoader().load(dimensionKey);
|
||||
if (published == null) {
|
||||
throw new IOException("Published dimension '" + dimensionKey + "' could not be reloaded");
|
||||
}
|
||||
IrisImageMapRuntime.compile(validationData, published, published.getMinHeight());
|
||||
} finally {
|
||||
validationData.close();
|
||||
}
|
||||
return PackValidator.validate(pack.toFile());
|
||||
}
|
||||
|
||||
private static void bind(
|
||||
IrisDimension dimension,
|
||||
String bindingKey,
|
||||
String imageMapKey,
|
||||
IrisImageMapApplication application,
|
||||
List<IrisImageMapMask> masks
|
||||
) {
|
||||
IrisImageMapBinding existing = null;
|
||||
for (IrisImageMapBinding candidate : dimension.getImageMaps()) {
|
||||
if (candidate != null && bindingKey.equals(candidate.getKey())) {
|
||||
existing = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
KList<IrisImageMapMask> configuredMasks = new KList<>();
|
||||
configuredMasks.addAll(masks);
|
||||
if (existing == null) {
|
||||
dimension.getImageMaps().add(new IrisImageMapBinding()
|
||||
.setKey(bindingKey)
|
||||
.setMap(imageMapKey)
|
||||
.setApplication(application)
|
||||
.setMasks(configuredMasks));
|
||||
return;
|
||||
}
|
||||
existing.setMap(imageMapKey);
|
||||
existing.setApplication(application);
|
||||
existing.setMasks(configuredMasks);
|
||||
}
|
||||
|
||||
private static DecodedSource decodePng(Path source) throws IOException {
|
||||
if (!Files.isRegularFile(source)) {
|
||||
throw new IOException("Image source is not a file: " + source);
|
||||
}
|
||||
try (ImageInputStream input = ImageIO.createImageInputStream(source.toFile())) {
|
||||
if (input == null) {
|
||||
throw new IOException("Unable to open image source " + source);
|
||||
}
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
if (!readers.hasNext()) {
|
||||
throw new IOException("Unsupported or corrupt image source " + source);
|
||||
}
|
||||
ImageReader reader = readers.next();
|
||||
try {
|
||||
String format = reader.getFormatName().toLowerCase();
|
||||
if (!"png".equals(format)) {
|
||||
throw new IOException("Image-map data must be PNG, got " + format);
|
||||
}
|
||||
reader.setInput(input, true, false);
|
||||
validateDimensions(reader.getWidth(0), reader.getHeight(0));
|
||||
String colorProfile = colorProfile(reader);
|
||||
BufferedImage image = reader.read(0);
|
||||
if (image == null) {
|
||||
throw new IOException("PNG decoder returned no image for " + source);
|
||||
}
|
||||
return new DecodedSource(image, format, colorProfile);
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDimensions(int width, int height) {
|
||||
List<String> diagnostics = new ArrayList<>();
|
||||
if (width < IrisImageMapCompiler.MINIMUM_DIMENSION || width > IrisImageMapCompiler.MAXIMUM_DIMENSION) {
|
||||
diagnostics.add("Image width must be " + IrisImageMapCompiler.MINIMUM_DIMENSION + ".."
|
||||
+ IrisImageMapCompiler.MAXIMUM_DIMENSION + ", got " + width);
|
||||
}
|
||||
if (height < IrisImageMapCompiler.MINIMUM_DIMENSION || height > IrisImageMapCompiler.MAXIMUM_DIMENSION) {
|
||||
diagnostics.add("Image height must be " + IrisImageMapCompiler.MINIMUM_DIMENSION + ".."
|
||||
+ IrisImageMapCompiler.MAXIMUM_DIMENSION + ", got " + height);
|
||||
}
|
||||
long pixels = (long) width * height;
|
||||
if (pixels > IrisImageMapCompiler.MAXIMUM_PIXELS) {
|
||||
diagnostics.add("Image contains " + pixels + " pixels; maximum is "
|
||||
+ IrisImageMapCompiler.MAXIMUM_PIXELS);
|
||||
}
|
||||
if (!diagnostics.isEmpty()) {
|
||||
throw new IrisImageMapValidationException(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path requirePack(File folder) throws IOException {
|
||||
if (folder == null) {
|
||||
throw new IOException("Pack folder is required");
|
||||
}
|
||||
Path pack = folder.toPath().toAbsolutePath().normalize();
|
||||
if (!Files.isDirectory(pack) || Files.isSymbolicLink(pack)) {
|
||||
throw new IOException("Pack folder is missing or unsafe: " + pack);
|
||||
}
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static String colorProfile(ImageReader reader) {
|
||||
try {
|
||||
IIOMetadata metadata = reader.getImageMetadata(0);
|
||||
Node root = metadata.getAsTree("javax_imageio_png_1.0");
|
||||
Node embedded = findNode(root, "iCCP");
|
||||
if (embedded != null) {
|
||||
return "embedded ICC: " + attribute(embedded, "profileName", "unnamed");
|
||||
}
|
||||
Node standard = findNode(root, "sRGB");
|
||||
if (standard != null) {
|
||||
return "sRGB: " + attribute(standard, "renderingIntent", "intent unspecified");
|
||||
}
|
||||
Node gamma = findNode(root, "gAMA");
|
||||
if (gamma != null) {
|
||||
return "gamma: " + attribute(gamma, "value", "unspecified");
|
||||
}
|
||||
return "none declared";
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return "metadata unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
private static Node findNode(Node node, String name) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals(node.getNodeName())) {
|
||||
return node;
|
||||
}
|
||||
for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
|
||||
Node found = findNode(child, name);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String attribute(Node node, String name, String fallback) {
|
||||
NamedNodeMap attributes = node.getAttributes();
|
||||
Node value = attributes == null ? null : attributes.getNamedItem(name);
|
||||
return value == null || value.getNodeValue().isBlank() ? fallback : value.getNodeValue();
|
||||
}
|
||||
|
||||
private static String requireKey(String value, String name) {
|
||||
if (value == null || !RESOURCE_KEY.matcher(value).matches() || value.contains("..")) {
|
||||
throw new IllegalArgumentException(name + " must match " + RESOURCE_KEY.pattern() + " without '..'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Path safeTarget(Path pack, String folder, String relativeName) throws IOException {
|
||||
Path target = pack.resolve(folder).resolve(relativeName).normalize();
|
||||
if (!target.startsWith(pack) || target.equals(pack)) {
|
||||
throw new IOException("Export target escapes the pack: " + target);
|
||||
}
|
||||
if (Files.isSymbolicLink(target)) {
|
||||
throw new IOException("Export target must not be a symbolic link: " + target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
public record ExportRequest(
|
||||
File packFolder,
|
||||
String dimensionKey,
|
||||
String bindingKey,
|
||||
IrisImageMapApplication application,
|
||||
String imageMapKey,
|
||||
String imageKey,
|
||||
IrisImageMap definition,
|
||||
Path sourcePng,
|
||||
List<IrisImageMapMask> masks
|
||||
) {
|
||||
public ExportRequest {
|
||||
masks = masks == null ? List.of() : List.copyOf(masks);
|
||||
}
|
||||
}
|
||||
|
||||
public record ExportResult(
|
||||
Path imageFile,
|
||||
Path imageMapFile,
|
||||
Path dimensionFile,
|
||||
String contentHash,
|
||||
List<String> warnings
|
||||
) {
|
||||
public ExportResult {
|
||||
warnings = List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
public record PreviewResult(
|
||||
BufferedImage source,
|
||||
CompiledIrisImageMap compiled,
|
||||
String colorProfile,
|
||||
IrisImageMapMaskSampler maskSampler
|
||||
) {
|
||||
}
|
||||
|
||||
public record SourceInspection(
|
||||
BufferedImage source,
|
||||
String format,
|
||||
String colorProfile,
|
||||
double minimumAlpha,
|
||||
double maximumAlpha
|
||||
) {
|
||||
}
|
||||
|
||||
private record DecodedSource(BufferedImage image, String format, String colorProfile) {
|
||||
}
|
||||
|
||||
private static final class AtomicFileSet implements AutoCloseable {
|
||||
private final List<StagedFile> files = new ArrayList<>();
|
||||
private boolean published;
|
||||
private boolean committed;
|
||||
|
||||
private void stageCopy(Path source, Path target) throws IOException {
|
||||
Files.createDirectories(target.getParent());
|
||||
Path staged = Files.createTempFile(target.getParent(), "." + target.getFileName(), ".studio");
|
||||
Files.copy(source, staged, StandardCopyOption.REPLACE_EXISTING);
|
||||
files.add(new StagedFile(staged, target, null, false));
|
||||
}
|
||||
|
||||
private void stageText(String content, Path target) throws IOException {
|
||||
Files.createDirectories(target.getParent());
|
||||
Path staged = Files.createTempFile(target.getParent(), "." + target.getFileName(), ".studio");
|
||||
Files.writeString(staged, content, StandardCharsets.UTF_8);
|
||||
files.add(new StagedFile(staged, target, null, false));
|
||||
}
|
||||
|
||||
private void publish() throws IOException {
|
||||
published = true;
|
||||
for (int index = 0; index < files.size(); index++) {
|
||||
StagedFile file = files.get(index);
|
||||
Path backup = null;
|
||||
if (Files.exists(file.target())) {
|
||||
backup = file.target().resolveSibling("." + file.target().getFileName()
|
||||
+ ".backup-" + UUID.randomUUID());
|
||||
move(file.target(), backup, false);
|
||||
}
|
||||
files.set(index, new StagedFile(file.staged(), file.target(), backup, true));
|
||||
move(file.staged(), file.target(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private void commit() throws IOException {
|
||||
committed = true;
|
||||
for (StagedFile file : files) {
|
||||
if (file.backup() != null) {
|
||||
Files.deleteIfExists(file.backup());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IOException failure = null;
|
||||
if (published && !committed) {
|
||||
for (int index = files.size() - 1; index >= 0; index--) {
|
||||
StagedFile file = files.get(index);
|
||||
if (!file.published()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(file.target());
|
||||
if (file.backup() != null && Files.exists(file.backup())) {
|
||||
move(file.backup(), file.target(), true);
|
||||
}
|
||||
} catch (IOException rollbackFailure) {
|
||||
if (failure == null) {
|
||||
failure = rollbackFailure;
|
||||
} else {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (StagedFile file : files) {
|
||||
try {
|
||||
Files.deleteIfExists(file.staged());
|
||||
if (committed && file.backup() != null) {
|
||||
Files.deleteIfExists(file.backup());
|
||||
}
|
||||
} catch (IOException cleanupFailure) {
|
||||
if (failure == null) {
|
||||
failure = cleanupFailure;
|
||||
} else {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void move(Path source, Path target, boolean replace) throws IOException {
|
||||
try {
|
||||
if (replace) {
|
||||
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
if (replace) {
|
||||
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
Files.move(source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record StagedFile(Path staged, Path target, Path backup, boolean published) {
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ import com.google.gson.Gson;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.pack.ImageMapPackageClosure;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.pack.StructurePackageClosure;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
@@ -65,6 +68,14 @@ public class IrisPackageCompiler {
|
||||
}
|
||||
|
||||
public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) {
|
||||
PackValidationResult validation = PackValidator.validateForPackaging(project.getPath());
|
||||
if (!validation.isLoadable()) {
|
||||
IllegalStateException error = new IllegalStateException("Pack validation failed before packaging: "
|
||||
+ String.join("; ", validation.getBlockingErrors()));
|
||||
IrisLogging.reportError("Package validation failed for '" + project.getName() + "'.", error);
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED));
|
||||
return null;
|
||||
}
|
||||
// Detached loader on purpose: obfuscation rewrites the placement 'place' lists on the
|
||||
// loaded resources, which must never leak into the shared cached pack state — a second
|
||||
// export against a mutated cache produced archives with an empty objects/ directory.
|
||||
@@ -108,9 +119,10 @@ public class IrisPackageCompiler {
|
||||
addIfPresent(blocks, dm.getBlockLoader().load(i));
|
||||
}
|
||||
|
||||
dimension.getRegions().forEach((i) -> addIfPresent(regions, dm.getRegionLoader().load(i)));
|
||||
dimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
|
||||
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
|
||||
dimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
|
||||
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||
dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||
@@ -209,6 +221,7 @@ public class IrisPackageCompiler {
|
||||
throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors()));
|
||||
}
|
||||
b.append(structureClosure.writeTo(folder, minify));
|
||||
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
|
||||
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
|
||||
@@ -12,6 +12,7 @@ import art.arcane.volmlib.util.inventorygui.UIElement;
|
||||
import art.arcane.volmlib.util.inventorygui.UIPaneDecorator;
|
||||
import art.arcane.volmlib.util.inventorygui.UIWindow;
|
||||
import art.arcane.volmlib.util.inventorygui.WindowResolution;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -64,13 +65,13 @@ public final class JigsawStudioMenuController {
|
||||
Optional<JigsawStudioMenuState> available = actions.menuState(player);
|
||||
if (available.isEmpty()) {
|
||||
close(player);
|
||||
player.sendMessage(ChatColor.RED + "Iris Jigsaw Studio is not active in this world.");
|
||||
send(player, ChatColor.RED + "Iris Jigsaw Studio is not active in this world.");
|
||||
return false;
|
||||
}
|
||||
JigsawStudioMenuState state = available.get();
|
||||
if (state.workcells().isEmpty()) {
|
||||
close(player);
|
||||
player.sendMessage(ChatColor.RED + "This Jigsaw Studio has no workcells.");
|
||||
send(player, ChatColor.RED + "This Jigsaw Studio has no workcells.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -160,7 +161,7 @@ public final class JigsawStudioMenuController {
|
||||
return false;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
send(player, ChatColor.YELLOW
|
||||
+ "Vanilla-portable pieces cannot encode Iris theme or piece-rule metadata.");
|
||||
close(player);
|
||||
return false;
|
||||
@@ -1573,7 +1574,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (variant.active()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "That variant is already loaded. Right-click it for details.");
|
||||
send(player, ChatColor.YELLOW + "That variant is already loaded. Right-click it for details.");
|
||||
return;
|
||||
}
|
||||
if (actions.switchVariant(player, workcellId, pieceKey, false)) {
|
||||
@@ -1655,11 +1656,11 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!variant.active() || !variant.owned()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Load an owned variant before editing its rules.");
|
||||
send(player, ChatColor.YELLOW + "Load an owned variant before editing its rules.");
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
send(player, ChatColor.YELLOW
|
||||
+ "Vanilla-portable pieces cannot encode Iris theme or piece-rule metadata.");
|
||||
return;
|
||||
}
|
||||
@@ -1753,7 +1754,7 @@ public final class JigsawStudioMenuController {
|
||||
UUID playerId = player.getUniqueId();
|
||||
PendingWorkcellResize existing = pendingWorkcellResize(playerId, requestId, workcellId);
|
||||
if (existing != null && existing.applying()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "That cell size is already being applied.");
|
||||
send(player, ChatColor.YELLOW + "That cell size is already being applied.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions base = existing == null
|
||||
@@ -1762,7 +1763,7 @@ public final class JigsawStudioMenuController {
|
||||
Optional<JigsawStudioCellDimensions> adjusted = adjustedDimensions(
|
||||
base, axis, delta);
|
||||
if (adjusted.isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED + "That workcell size is outside Iris limits.");
|
||||
send(player, ChatColor.RED + "That workcell size is outside Iris limits.");
|
||||
return;
|
||||
}
|
||||
PendingWorkcellResize pending = new PendingWorkcellResize(
|
||||
@@ -1864,7 +1865,7 @@ public final class JigsawStudioMenuController {
|
||||
if (window != null) {
|
||||
renderWorkcellSettings(window, current.orElseThrow(), withCapacity(workcell, retry.dimensions()));
|
||||
}
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
send(player, ChatColor.YELLOW
|
||||
+ "Cell resizing is still pending; use Apply Cell Size to retry after the current operation settles.");
|
||||
}
|
||||
|
||||
@@ -1927,7 +1928,7 @@ public final class JigsawStudioMenuController {
|
||||
Optional<JigsawStudioCellDimensions> adjusted = adjustedDimensions(
|
||||
variant.dimensions().orElseThrow(), axis, delta);
|
||||
if (adjusted.isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED + "That variant size is outside Iris limits.");
|
||||
send(player, ChatColor.RED + "That variant size is outside Iris limits.");
|
||||
return;
|
||||
}
|
||||
resizeVariant(player, requestId, workcellId, pieceKey, adjusted.get());
|
||||
@@ -1953,7 +1954,7 @@ public final class JigsawStudioMenuController {
|
||||
if (dimensions.width() > workcell.capacity().width()
|
||||
|| dimensions.height() > workcell.capacity().height()
|
||||
|| dimensions.depth() > workcell.capacity().depth()) {
|
||||
player.sendMessage(ChatColor.RED + "Increase this workcell's capacity before making the variant larger.");
|
||||
send(player, ChatColor.RED + "Increase this workcell's capacity before making the variant larger.");
|
||||
return;
|
||||
}
|
||||
if (actions.resizeVariant(player, workcellId, pieceKey, dimensions)) {
|
||||
@@ -1967,11 +1968,11 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Mandatory caps require Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Mandatory caps require Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
if (current.get().requireCaps() == requireCaps) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Mandatory caps are already "
|
||||
send(player, ChatColor.YELLOW + "Mandatory caps are already "
|
||||
+ (requireCaps ? "enabled." : "disabled."));
|
||||
return;
|
||||
}
|
||||
@@ -1987,7 +1988,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Theme sets require Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Theme sets require Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
if (!nextThemeSetKey(current.get().themeSets()).equals(themeKey)) {
|
||||
@@ -2011,7 +2012,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Theme weights require Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Theme weights require Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioMenuState.ThemeSet themeSet = current.get().themeSet(expected.key());
|
||||
@@ -2021,7 +2022,7 @@ public final class JigsawStudioMenuController {
|
||||
}
|
||||
Optional<Integer> weight = adjustedPositiveValue(themeSet.weight(), delta);
|
||||
if (weight.isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED + "Theme weights must remain positive.");
|
||||
send(player, ChatColor.RED + "Theme weights must remain positive.");
|
||||
return;
|
||||
}
|
||||
if (actions.updateThemeSetWeight(player, themeSet.key(), weight.get())) {
|
||||
@@ -2041,7 +2042,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!workcell.dirty()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "This workcell has no pending changes to flush.");
|
||||
send(player, ChatColor.YELLOW + "This workcell has no pending changes to flush.");
|
||||
return;
|
||||
}
|
||||
if (actions.flushAutosave(player, workcellId)) {
|
||||
@@ -2085,7 +2086,7 @@ public final class JigsawStudioMenuController {
|
||||
}
|
||||
Optional<JigsawStudioPieceRules> rules = adjustedRules(variant.rules(), field, delta);
|
||||
if (rules.isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED + "That piece rule value is outside Iris limits.");
|
||||
send(player, ChatColor.RED + "That piece rule value is outside Iris limits.");
|
||||
return;
|
||||
}
|
||||
updateVariantRules(
|
||||
@@ -2108,7 +2109,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Piece rules require Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Piece rules require Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioMenuState.Variant variant = activeVariant(current.get(), workcellId);
|
||||
@@ -2134,7 +2135,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Theme membership requires Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Theme membership requires Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioMenuState.Variant variant = activeVariant(current.get(), workcell.stableId());
|
||||
@@ -2201,7 +2202,7 @@ public final class JigsawStudioMenuController {
|
||||
return;
|
||||
}
|
||||
if (!current.get().irisExtended()) {
|
||||
player.sendMessage(ChatColor.YELLOW + "Per-entry chance requires Iris compatibility.");
|
||||
send(player, ChatColor.YELLOW + "Per-entry chance requires Iris compatibility.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioMenuState.Variant active = activeVariant(current.get(), workcellId);
|
||||
@@ -2449,7 +2450,11 @@ public final class JigsawStudioMenuController {
|
||||
|
||||
private void stale(Player player) {
|
||||
closeAfterAction(player);
|
||||
player.sendMessage(ChatColor.RED + "This Jigsaw Studio menu is stale. Open the control chest again.");
|
||||
send(player, ChatColor.RED + "This Jigsaw Studio menu is stale. Open the control chest again.");
|
||||
}
|
||||
|
||||
private static void send(Player player, String message) {
|
||||
ComponentMessenger.sendSection(player, message);
|
||||
}
|
||||
|
||||
private void closeAfterAction(Player player) {
|
||||
|
||||
@@ -63,6 +63,7 @@ import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Chunk;
|
||||
@@ -7803,7 +7804,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
|
||||
|
||||
private static void message(Player player, String text) {
|
||||
if (player != null) {
|
||||
J.runEntity(player, () -> player.sendMessage("[Iris Jigsaw Studio] " + text));
|
||||
J.runEntity(player, () -> ComponentMessenger.sendLiteral(player, "[Iris Jigsaw Studio] " + text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
public class ObjectStudioSaveService implements IrisService {
|
||||
private static ObjectStudioSaveService INSTANCE;
|
||||
|
||||
@@ -142,11 +143,11 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
Player player = event.getPlayer();
|
||||
GridCell cell = findCellNear(studio, clicked.getX(), clicked.getZ());
|
||||
if (cell == null) {
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CELL_UNDER_CLICK_X_Z, MessageArgument.untrusted("x", String.valueOf(clicked.getX())), MessageArgument.untrusted("z", String.valueOf(clicked.getZ()))));
|
||||
send(player, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CELL_UNDER_CLICK_X_Z, MessageArgument.untrusted("x", String.valueOf(clicked.getX())), MessageArgument.untrusted("z", String.valueOf(clicked.getZ()))));
|
||||
return;
|
||||
}
|
||||
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("w", String.valueOf(cell.w())), MessageArgument.untrusted("h", String.valueOf(cell.h())), MessageArgument.untrusted("d", String.valueOf(cell.d()))));
|
||||
send(player, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVING_X_X, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("w", String.valueOf(cell.w())), MessageArgument.untrusted("h", String.valueOf(cell.h())), MessageArgument.untrusted("d", String.valueOf(cell.d()))));
|
||||
IrisLogging.debug("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key());
|
||||
J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> {
|
||||
try {
|
||||
@@ -236,7 +237,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
Long prior = studio.hashes.get(hashKey);
|
||||
if (prior != null && prior == hash) {
|
||||
if (notify != null) {
|
||||
notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CHANGES, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
send(notify, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_CHANGES, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -244,7 +245,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
if (!anyBlock && prior == null) {
|
||||
studio.hashes.put(hashKey, hash);
|
||||
if (notify != null) {
|
||||
notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_EMPTY_CELL_NOTHING_WRITE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
send(notify, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_EMPTY_CELL_NOTHING_WRITE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -254,7 +255,7 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
File targetFile = objectFileFor(studio, cell);
|
||||
if (targetFile == null) {
|
||||
if (notify != null) {
|
||||
notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_TARGET_FILE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
send(notify, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_NO_TARGET_FILE, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key()))));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -269,17 +270,21 @@ public class ObjectStudioSaveService implements IrisService {
|
||||
IrisLogging.debug("Object Studio saved: %s/%s (%dx%dx%d)",
|
||||
cell.pack(), cell.key(), cell.w(), cell.h(), cell.d());
|
||||
if (notify != null) {
|
||||
J.runEntity(notify, () -> notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))));
|
||||
J.runEntity(notify, () -> send(notify, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())))));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
if (notify != null) {
|
||||
J.runEntity(notify, () -> notify.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVE_FAILED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))));
|
||||
J.runEntity(notify, () -> send(notify, IrisLanguage.text(BukkitRuntimeMessages.OBJECT_STUDIO_SAVE_SERVICE_OBJECT_STUDIO_SAVE_FAILED, MessageArgument.untrusted("pack", String.valueOf(cell.pack())), MessageArgument.untrusted("key", String.valueOf(cell.key())), MessageArgument.untrusted("error", String.valueOf(e.getMessage())))));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void send(Player player, String message) {
|
||||
ComponentMessenger.sendSection(player, message);
|
||||
}
|
||||
|
||||
private boolean allChunksLoaded(World world, GridCell cell) {
|
||||
for (int cx = cell.chunkMinX(); cx <= cell.chunkMaxX(); cx++) {
|
||||
for (int cz = cell.chunkMinZ(); cz <= cell.chunkMaxZ(); cz++) {
|
||||
|
||||
@@ -139,6 +139,7 @@ final class EngineHotloader {
|
||||
throw new IllegalStateException("Iris background task admission closed before workspace refresh.");
|
||||
}
|
||||
}
|
||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||
broadcastStudioHotload(false, "");
|
||||
} catch (Throwable e) {
|
||||
if (!published) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||
@@ -70,8 +71,8 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
@@ -136,6 +137,7 @@ public class IrisComplex implements DataProvider {
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
private IrisRiverRuntime riverRuntime;
|
||||
private transient IrisImageMapRuntime imageMapRuntime;
|
||||
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
|
||||
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
|
||||
// per insert is cheap. Identity keying is load-bearing (IrisBiome is mutable/value-hashed).
|
||||
@@ -152,6 +154,7 @@ public class IrisComplex implements DataProvider {
|
||||
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
||||
this.rng = new RNG(engine.getSeedManager().getComplex());
|
||||
this.data = engine.getData();
|
||||
imageMapRuntime = IrisImageMapRuntime.compile(engine);
|
||||
double height = engine.getMaxHeight();
|
||||
fluidHeight = engine.getDimension().getFluidHeight();
|
||||
generators = new HashMap<>();
|
||||
@@ -178,6 +181,11 @@ public class IrisComplex implements DataProvider {
|
||||
prepareInferredBiomes(region);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
});
|
||||
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
|
||||
prepareInferredBiomes(region);
|
||||
region.getNaturalBiomes(this).forEach(this::registerGenerators);
|
||||
}
|
||||
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
||||
}
|
||||
int interpolatorCount = generators.size();
|
||||
frozenInterpolators = new IrisInterpolator[interpolatorCount];
|
||||
@@ -208,12 +216,18 @@ public class IrisComplex implements DataProvider {
|
||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||
.zoom(engine.getDimension().getRegionZoom());
|
||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
regionStream = focusRegion != null ?
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = focusRegion != null ?
|
||||
ProceduralStream.of((x, z) -> focusRegion,
|
||||
Interpolated.of(a -> 0D, a -> focusRegion))
|
||||
: regionStyleStream
|
||||
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()))
|
||||
.cache2D("regionStream", engine, cacheSize);
|
||||
regionStream = focusRegion != null ? proceduralRegionStream : proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> {
|
||||
IrisRegion mapped = imageMapRuntime.sampleRegion(x, z);
|
||||
return mapped == null ? region : mapped;
|
||||
})
|
||||
.cache2D("imageMappedRegionStream", engine, cacheSize);
|
||||
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
|
||||
String.valueOf(i * 38445).hashCode() * 3245556666L));
|
||||
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
@@ -259,22 +273,33 @@ public class IrisComplex implements DataProvider {
|
||||
.bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream()
|
||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND)
|
||||
.cache2D("bridgeStream", engine, cacheSize);
|
||||
baseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome,
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome,
|
||||
Interpolated.of(a -> 0D, a -> focusBiome)) :
|
||||
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode)
|
||||
.cache2D("baseBiomeStream", engine, cacheSize);
|
||||
baseBiomeStream = focusBiome != null ? proceduralBaseBiomeStream : proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? biome : mapped;
|
||||
})
|
||||
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
naturalHeightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
|
||||
return getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||
double proceduralHeight = getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
|
||||
naturalSlopeStream = naturalHeightStream.slope(3)
|
||||
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||
b -> focusBiome))
|
||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||
.convertAware2D((h, x, z) ->
|
||||
fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight))
|
||||
.convertAware2D((h, x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
|
||||
: mapped;
|
||||
})
|
||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
||||
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
||||
@@ -469,6 +494,13 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
public double sampleProceduralTerrainHeight(Engine engine, double worldX, double worldZ) {
|
||||
if (engine == null) {
|
||||
throw new IllegalArgumentException("Engine is required to sample procedural terrain height");
|
||||
}
|
||||
return getHeight(engine, null, worldX, worldZ, engine.getSeedManager().getHeight());
|
||||
}
|
||||
|
||||
private IrisRegion findRegion(IrisBiome focus, Engine engine) {
|
||||
for (IrisRegion i : engine.getDimension().getAllRegions(engine)) {
|
||||
if (i.getAllBiomeIds().contains(focus.getLoadKey())) {
|
||||
@@ -506,6 +538,10 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mapped != null && (riverRuntime == null || !sample.river().present())) {
|
||||
return mapped;
|
||||
}
|
||||
if (riverRuntime != null) {
|
||||
if (sample.subterranean()) {
|
||||
return fixBiomeType(
|
||||
@@ -734,9 +770,11 @@ public class IrisComplex implements DataProvider {
|
||||
minimum += Math.min(style.getMin(), style.getMax());
|
||||
maximum += Math.max(style.getMin(), style.getMax());
|
||||
}
|
||||
minimum = imageMapRuntime.sampleTerrainHeight(x, z, minimum);
|
||||
maximum = imageMapRuntime.sampleTerrainHeight(x, z, maximum);
|
||||
return new NoiseBounds(
|
||||
Math.max(0D, Math.min(engine.getHeight(), minimum)),
|
||||
Math.max(0D, Math.min(engine.getHeight(), maximum))
|
||||
Math.max(0D, Math.min(engine.getHeight(), Math.min(minimum, maximum))),
|
||||
Math.max(0D, Math.min(engine.getHeight(), Math.max(minimum, maximum)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
@@ -35,22 +37,19 @@ public class UpperDimensionContext implements DataProvider {
|
||||
private final ProceduralStream<IrisBiome> biomeStream;
|
||||
private final ProceduralStream<IrisRegion> regionStream;
|
||||
private final ProceduralStream<PlatformBlockState> rockStream;
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
|
||||
private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
boolean selfReferencing) {
|
||||
this.dimension = dimension;
|
||||
this.data = data;
|
||||
this.chunkHeight = chunkHeight;
|
||||
this.heightStream = heightStream;
|
||||
this.biomeStream = biomeStream;
|
||||
this.regionStream = regionStream;
|
||||
this.rockStream = rockStream;
|
||||
this.selfReferencing = selfReferencing;
|
||||
private UpperDimensionContext(ContextState state) {
|
||||
this.dimension = state.dimension();
|
||||
this.data = state.data();
|
||||
this.chunkHeight = state.chunkHeight();
|
||||
this.heightStream = state.heightStream();
|
||||
this.biomeStream = state.biomeStream();
|
||||
this.regionStream = state.regionStream();
|
||||
this.rockStream = state.rockStream();
|
||||
this.imageMapRuntime = state.imageMapRuntime();
|
||||
this.selfReferencing = state.selfReferencing();
|
||||
}
|
||||
|
||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
|
||||
@@ -64,7 +63,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
|
||||
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
return new UpperDimensionContext(
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
@@ -72,8 +71,9 @@ public class UpperDimensionContext implements DataProvider {
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
complex.getImageMapRuntime(),
|
||||
true
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
|
||||
@@ -82,9 +82,15 @@ public class UpperDimensionContext implements DataProvider {
|
||||
resolvedData = engine.getData();
|
||||
}
|
||||
IrisData upperData = resolvedData;
|
||||
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
|
||||
upperData,
|
||||
upperDim,
|
||||
engine.getMinHeight()
|
||||
);
|
||||
long seedOffset = upperDim.getLoadKey().hashCode();
|
||||
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||
double fluidHeight = upperDim.getFluidHeight();
|
||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
DataProvider dataProvider = () -> upperData;
|
||||
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||
@@ -94,17 +100,17 @@ public class UpperDimensionContext implements DataProvider {
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator gen = link.getCachedGenerator(dataProvider);
|
||||
if (gen != null) {
|
||||
generators.computeIfAbsent(gen.getInterpolator(), k -> new HashSet<>()).add(gen);
|
||||
}
|
||||
});
|
||||
});
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
});
|
||||
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
||||
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
@@ -128,8 +134,11 @@ public class UpperDimensionContext implements DataProvider {
|
||||
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
|
||||
.create(rng.nextParallelRNG(883), upperData).stream()
|
||||
.zoom(upperDim.getRegionZoom());
|
||||
ProceduralStream<IrisRegion> regionStream = regionStyleStream
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = regionStyleStream
|
||||
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
|
||||
ProceduralStream<IrisRegion> regionStream = proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
|
||||
.cache2D("upperImageMappedRegionStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getLandBiomeStyle()
|
||||
@@ -165,13 +174,16 @@ public class UpperDimensionContext implements DataProvider {
|
||||
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
|
||||
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = bridgeStream
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = bridgeStream
|
||||
.convertAware2D((t, x, z) -> {
|
||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
||||
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
||||
})
|
||||
.convertAware2D((biome, x, z) -> implode(
|
||||
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
|
||||
.cache2D("upperImageMappedBaseBiomeStream", engine, cacheSize);
|
||||
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||
@@ -189,7 +201,7 @@ public class UpperDimensionContext implements DataProvider {
|
||||
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = baseBiomeStream.get(x, z);
|
||||
if (b == null) {
|
||||
return fluidHeight;
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0;
|
||||
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||
@@ -233,10 +245,18 @@ public class UpperDimensionContext implements DataProvider {
|
||||
}
|
||||
interpolatedHeight += d / gens.size();
|
||||
}
|
||||
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
|
||||
}, Interpolated.DOUBLE);
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
|
||||
0D
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("upperImageMappedHeightStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mappedBiome != null) {
|
||||
return mappedBiome;
|
||||
}
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
|
||||
height,
|
||||
@@ -251,13 +271,13 @@ public class UpperDimensionContext implements DataProvider {
|
||||
return resolved == baseBiome
|
||||
? baseBiome
|
||||
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||
});
|
||||
}).cache2D("upperImageMappedFinalBiomeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
||||
.select(upperDim.getRockPalette().getBlockData(upperData));
|
||||
|
||||
return new UpperDimensionContext(
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
upperDim,
|
||||
upperData,
|
||||
chunkHeight,
|
||||
@@ -265,8 +285,63 @@ public class UpperDimensionContext implements DataProvider {
|
||||
finalBiomeStream,
|
||||
regionStream,
|
||||
rockStream,
|
||||
imageMapRuntime,
|
||||
false
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerBiomeGenerators(
|
||||
IrisBiome biome,
|
||||
DataProvider dataProvider,
|
||||
Set<IrisBiome> allBiomes,
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators
|
||||
) {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator generator = link.getCachedGenerator(dataProvider);
|
||||
if (generator != null) {
|
||||
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisRegion proceduralRegion,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
||||
}
|
||||
|
||||
static double mappedTerrainHeight(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
double proceduralHeight,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
||||
}
|
||||
|
||||
static IrisBiome mappedBiome(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisBiome proceduralBiome,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
||||
}
|
||||
|
||||
static PlatformBlockState mappedSurfaceBlock(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
PlatformBlockState proceduralBlock,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
||||
}
|
||||
|
||||
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
|
||||
@@ -340,6 +415,10 @@ public class UpperDimensionContext implements DataProvider {
|
||||
return rockStream.get((double) x, (double) z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getSurfaceBlock(int x, int z) {
|
||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return dimension;
|
||||
}
|
||||
@@ -352,4 +431,17 @@ public class UpperDimensionContext implements DataProvider {
|
||||
public boolean isSelfReferencing() {
|
||||
return selfReferencing;
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int chunkHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
int topY = Math.min(hf, chunkHeight - 1);
|
||||
PlatformBlockState fluid = fluidCache.get(xf, zf);
|
||||
PlatformBlockState rock = rockCache.get(xf, zf);
|
||||
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
||||
KList<IrisOreGenerator> regionSurfaceOres = hideOres ? null : region.getSurfaceOreGenerators();
|
||||
KList<IrisOreGenerator> biomeUndergroundOres = hideOres ? null : biome.getUndergroundOreGenerators();
|
||||
@@ -173,6 +174,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
|
||||
if (i <= he) {
|
||||
int depth = he - i;
|
||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||
h.setRaw(xf, i, zf, mappedSurfaceBlock);
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, localRng, he, he, data, complex);
|
||||
}
|
||||
@@ -210,6 +215,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
if (upperSurfaceY < chunkHeight - 1) {
|
||||
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
|
||||
PlatformBlockState upperRock = upperContext.getRockBlock(realX, realZ);
|
||||
PlatformBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ);
|
||||
int upperThickness = chunkHeight - 1 - upperSurfaceY;
|
||||
KList<PlatformBlockState> upperBlocks = upperBiome != null
|
||||
? upperBiome.generateLayers(upperContext.getDimension(),
|
||||
@@ -222,6 +228,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
h.setRaw(xf, y, zf, BEDROCK);
|
||||
continue;
|
||||
}
|
||||
if (y == upperSurfaceY && upperMappedSurface != null) {
|
||||
h.setRaw(xf, y, zf, upperMappedSurface);
|
||||
continue;
|
||||
}
|
||||
int depthFromFace = y - upperSurfaceY;
|
||||
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||
|
||||
@@ -45,6 +45,9 @@ public interface EnginePlatformHooks {
|
||||
default void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
}
|
||||
|
||||
default void applyWorldBoundary(Engine engine) {
|
||||
}
|
||||
|
||||
default boolean isPregeneratorActive(Engine engine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapSampling;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CompiledIrisImageMap {
|
||||
private final State state;
|
||||
|
||||
CompiledIrisImageMap(State state) {
|
||||
this.state = Objects.requireNonNull(state, "Compiled image-map state");
|
||||
}
|
||||
|
||||
public static CompiledIrisImageMap compile(IrisImageMap definition, IrisImage image) {
|
||||
return IrisImageMapCompiler.compile(definition, image);
|
||||
}
|
||||
|
||||
public IrisImageMap getDefinition() {
|
||||
return state.definition();
|
||||
}
|
||||
|
||||
public IrisImageMapSourceMetadata getSourceMetadata() {
|
||||
return state.sourceMetadata();
|
||||
}
|
||||
|
||||
public IrisImageMapType getType() {
|
||||
return state.type();
|
||||
}
|
||||
|
||||
public int getSourceWidth() {
|
||||
return state.sourceMetadata().width();
|
||||
}
|
||||
|
||||
public int getSourceHeight() {
|
||||
return state.sourceMetadata().height();
|
||||
}
|
||||
|
||||
public String getContentHash() {
|
||||
return state.contentHash();
|
||||
}
|
||||
|
||||
public String contentHash() {
|
||||
return state.contentHash();
|
||||
}
|
||||
|
||||
public CompiledIrisImageMap withoutDecodedValues() {
|
||||
return state.scalarValues() == null && state.targetIndices() == null
|
||||
? this
|
||||
: new CompiledIrisImageMap(state.withoutDecodedValues());
|
||||
}
|
||||
|
||||
public int getClippedPixelCount() {
|
||||
return state.clippedPixelCount();
|
||||
}
|
||||
|
||||
public int getUnknownColorPixelCount() {
|
||||
return state.unknownColorPixelCount();
|
||||
}
|
||||
|
||||
public boolean containsWorld(double worldX, double worldZ) {
|
||||
if (!Double.isFinite(worldX) || !Double.isFinite(worldZ)) {
|
||||
return false;
|
||||
}
|
||||
double sourceX = state.sourceTransform().sourceX(worldX, worldZ);
|
||||
double sourceZ = state.sourceTransform().sourceZ(worldX, worldZ);
|
||||
return validSourceCoordinate(sourceX, sourceZ)
|
||||
&& sourceX >= 0D
|
||||
&& sourceZ >= 0D
|
||||
&& sourceX < state.sourceMetadata().width()
|
||||
&& sourceZ < state.sourceMetadata().height();
|
||||
}
|
||||
|
||||
public boolean containsWorldForSampling(double worldX, double worldZ) {
|
||||
if (!Double.isFinite(worldX) || !Double.isFinite(worldZ)) {
|
||||
return false;
|
||||
}
|
||||
double sourceX = state.sourceTransform().sourceX(worldX, worldZ);
|
||||
double sourceZ = state.sourceTransform().sourceZ(worldX, worldZ);
|
||||
if (!validSourceCoordinate(sourceX, sourceZ)) {
|
||||
return false;
|
||||
}
|
||||
long baseX = floorToLong(sourceX);
|
||||
long baseZ = floorToLong(sourceZ);
|
||||
return switch (state.sampling()) {
|
||||
case NEAREST -> sourceIndexInBounds(baseX, baseZ);
|
||||
case BILINEAR -> bilinearKernelInBounds(sourceX, sourceZ, baseX, baseZ);
|
||||
case BICUBIC -> bicubicKernelInBounds(sourceX, sourceZ, baseX, baseZ);
|
||||
};
|
||||
}
|
||||
|
||||
public double sampleNormalized(double worldX, double worldZ) {
|
||||
requireScalarType();
|
||||
requireWorldCoordinate(worldX, worldZ);
|
||||
double sourceX = state.sourceTransform().sourceX(worldX, worldZ);
|
||||
double sourceZ = state.sourceTransform().sourceZ(worldX, worldZ);
|
||||
requireSourceCoordinate(sourceX, sourceZ);
|
||||
double sampled = switch (state.sampling()) {
|
||||
case NEAREST -> scalarAt(floorToLong(sourceX), floorToLong(sourceZ));
|
||||
case BILINEAR -> bilinear(sourceX, sourceZ);
|
||||
case BICUBIC -> bicubic(sourceX, sourceZ);
|
||||
};
|
||||
return clamp01(sampled);
|
||||
}
|
||||
|
||||
public double sampleHeight(double worldX, double worldZ) {
|
||||
if (!isHeightType()) {
|
||||
throw new IrisImageMapValidationException("Image map type " + state.type() + " does not produce terrain heights");
|
||||
}
|
||||
double normalized = sampleNormalized(worldX, worldZ);
|
||||
double height = state.minimumHeight()
|
||||
+ (normalized * (state.maximumHeight() - state.minimumHeight()))
|
||||
+ state.verticalOffset();
|
||||
if (!state.clampHeight()) {
|
||||
return height;
|
||||
}
|
||||
return Math.max(state.minimumHeight(), Math.min(state.maximumHeight(), height));
|
||||
}
|
||||
|
||||
public String sampleTarget(double worldX, double worldZ) {
|
||||
if (state.type() != IrisImageMapType.COLOR_MAP) {
|
||||
throw new IrisImageMapValidationException("Image map type " + state.type() + " does not produce legend targets");
|
||||
}
|
||||
if (state.targetIndices() == null) {
|
||||
throw new IrisImageMapValidationException("Compiled image-map view does not retain decoded target data");
|
||||
}
|
||||
requireWorldCoordinate(worldX, worldZ);
|
||||
double transformedX = state.sourceTransform().sourceX(worldX, worldZ);
|
||||
double transformedZ = state.sourceTransform().sourceZ(worldX, worldZ);
|
||||
requireSourceCoordinate(transformedX, transformedZ);
|
||||
long sourceX = floorToLong(transformedX);
|
||||
long sourceZ = floorToLong(transformedZ);
|
||||
int resolvedX = resolveIndex(sourceX, state.sourceMetadata().width());
|
||||
int resolvedZ = resolveIndex(sourceZ, state.sourceMetadata().height());
|
||||
if (resolvedX < 0 || resolvedZ < 0) {
|
||||
return state.fallbackTarget();
|
||||
}
|
||||
int targetIndex = state.targetIndices().get((resolvedZ * state.sourceMetadata().width()) + resolvedX);
|
||||
return targetIndex < 0 ? null : state.targets()[targetIndex];
|
||||
}
|
||||
|
||||
private void requireWorldCoordinate(double worldX, double worldZ) {
|
||||
if (!Double.isFinite(worldX) || !Double.isFinite(worldZ)) {
|
||||
throw new IrisImageMapValidationException("World coordinates must be finite, got " + worldX + "," + worldZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireSourceCoordinate(double sourceX, double sourceZ) {
|
||||
if (!validSourceCoordinate(sourceX, sourceZ)) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"World coordinates transform outside the supported source-coordinate range: "
|
||||
+ sourceX + "," + sourceZ
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bilinearKernelInBounds(double sourceX, double sourceZ, long baseX, long baseZ) {
|
||||
long maximumX = sourceX == baseX ? baseX : baseX + 1L;
|
||||
long maximumZ = sourceZ == baseZ ? baseZ : baseZ + 1L;
|
||||
return baseX >= 0L
|
||||
&& baseZ >= 0L
|
||||
&& maximumX < state.sourceMetadata().width()
|
||||
&& maximumZ < state.sourceMetadata().height();
|
||||
}
|
||||
|
||||
private boolean bicubicKernelInBounds(double sourceX, double sourceZ, long baseX, long baseZ) {
|
||||
if (sourceX == baseX && sourceZ == baseZ) {
|
||||
return sourceIndexInBounds(baseX, baseZ);
|
||||
}
|
||||
return baseX >= 1L
|
||||
&& baseZ >= 1L
|
||||
&& baseX + 2L < state.sourceMetadata().width()
|
||||
&& baseZ + 2L < state.sourceMetadata().height();
|
||||
}
|
||||
|
||||
private boolean sourceIndexInBounds(long sourceX, long sourceZ) {
|
||||
return sourceX >= 0L
|
||||
&& sourceZ >= 0L
|
||||
&& sourceX < state.sourceMetadata().width()
|
||||
&& sourceZ < state.sourceMetadata().height();
|
||||
}
|
||||
|
||||
private double bilinear(double sourceX, double sourceZ) {
|
||||
long x0 = floorToLong(sourceX);
|
||||
long z0 = floorToLong(sourceZ);
|
||||
double fractionX = sourceX - x0;
|
||||
double fractionZ = sourceZ - z0;
|
||||
if (fractionX == 0D && fractionZ == 0D) {
|
||||
return scalarAt(x0, z0);
|
||||
}
|
||||
if (fractionZ == 0D) {
|
||||
return lerp(scalarAt(x0, z0), scalarAt(x0 + 1L, z0), fractionX);
|
||||
}
|
||||
if (fractionX == 0D) {
|
||||
return lerp(scalarAt(x0, z0), scalarAt(x0, z0 + 1L), fractionZ);
|
||||
}
|
||||
double top = lerp(scalarAt(x0, z0), scalarAt(x0 + 1L, z0), fractionX);
|
||||
double bottom = lerp(scalarAt(x0, z0 + 1L), scalarAt(x0 + 1L, z0 + 1L), fractionX);
|
||||
return lerp(top, bottom, fractionZ);
|
||||
}
|
||||
|
||||
private double bicubic(double sourceX, double sourceZ) {
|
||||
long baseX = floorToLong(sourceX);
|
||||
long baseZ = floorToLong(sourceZ);
|
||||
double fractionX = sourceX - baseX;
|
||||
double fractionZ = sourceZ - baseZ;
|
||||
if (fractionX == 0D && fractionZ == 0D) {
|
||||
return scalarAt(baseX, baseZ);
|
||||
}
|
||||
double row0 = cubicRow(baseX, baseZ - 1L, fractionX);
|
||||
double row1 = cubicRow(baseX, baseZ, fractionX);
|
||||
double row2 = cubicRow(baseX, baseZ + 1L, fractionX);
|
||||
double row3 = cubicRow(baseX, baseZ + 2L, fractionX);
|
||||
return cubic(row0, row1, row2, row3, fractionZ);
|
||||
}
|
||||
|
||||
private double cubicRow(long baseX, long sourceZ, double fractionX) {
|
||||
return cubic(
|
||||
scalarAt(baseX - 1L, sourceZ),
|
||||
scalarAt(baseX, sourceZ),
|
||||
scalarAt(baseX + 1L, sourceZ),
|
||||
scalarAt(baseX + 2L, sourceZ),
|
||||
fractionX
|
||||
);
|
||||
}
|
||||
|
||||
private double scalarAt(long sourceX, long sourceZ) {
|
||||
int resolvedX = resolveIndex(sourceX, state.sourceMetadata().width());
|
||||
int resolvedZ = resolveIndex(sourceZ, state.sourceMetadata().height());
|
||||
if (resolvedX < 0 || resolvedZ < 0) {
|
||||
return state.fallbackValue();
|
||||
}
|
||||
return state.scalarValues().get((resolvedZ * state.sourceMetadata().width()) + resolvedX);
|
||||
}
|
||||
|
||||
private int resolveIndex(long coordinate, int length) {
|
||||
if (coordinate >= 0L && coordinate < length) {
|
||||
return (int) coordinate;
|
||||
}
|
||||
IrisImageMapOutOfBounds outOfBounds = state.outOfBounds();
|
||||
return switch (outOfBounds) {
|
||||
case FALLBACK -> -1;
|
||||
case CLAMP -> coordinate < 0L ? 0 : length - 1;
|
||||
case REPEAT -> (int) Math.floorMod(coordinate, (long) length);
|
||||
case MIRROR -> mirrorIndex(coordinate, length);
|
||||
case ERROR -> throw new IrisImageMapValidationException(
|
||||
"Source coordinate " + coordinate + " is outside 0.." + (length - 1)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private int mirrorIndex(long coordinate, int length) {
|
||||
if (length == 1) {
|
||||
return 0;
|
||||
}
|
||||
long period = (long) length * 2L;
|
||||
long mirrored = Math.floorMod(coordinate, period);
|
||||
if (mirrored >= length) {
|
||||
mirrored = period - 1L - mirrored;
|
||||
}
|
||||
return (int) mirrored;
|
||||
}
|
||||
|
||||
private boolean isHeightType() {
|
||||
return state.type() == IrisImageMapType.GRAYSCALE_HEIGHT
|
||||
|| state.type() == IrisImageMapType.RGB_HEIGHT;
|
||||
}
|
||||
|
||||
private void requireScalarType() {
|
||||
if (state.scalarValues() == null) {
|
||||
if (isHeightType()
|
||||
|| state.type() == IrisImageMapType.BINARY_MASK
|
||||
|| state.type() == IrisImageMapType.GRAYSCALE_MASK
|
||||
|| state.type() == IrisImageMapType.ALPHA_MASK) {
|
||||
throw new IrisImageMapValidationException("Compiled image-map view does not retain decoded scalar data");
|
||||
}
|
||||
throw new IrisImageMapValidationException("Image map type " + state.type() + " does not produce normalized scalar data");
|
||||
}
|
||||
}
|
||||
|
||||
private static long floorToLong(double value) {
|
||||
return (long) Math.floor(value);
|
||||
}
|
||||
|
||||
private static boolean validSourceCoordinate(double sourceX, double sourceZ) {
|
||||
double safeMinimum = -9_000_000_000_000_000_000D;
|
||||
double safeMaximum = 9_000_000_000_000_000_000D;
|
||||
return Double.isFinite(sourceX)
|
||||
&& Double.isFinite(sourceZ)
|
||||
&& sourceX >= safeMinimum
|
||||
&& sourceX <= safeMaximum
|
||||
&& sourceZ >= safeMinimum
|
||||
&& sourceZ <= safeMaximum;
|
||||
}
|
||||
|
||||
private static double lerp(double a, double b, double factor) {
|
||||
return a + ((b - a) * factor);
|
||||
}
|
||||
|
||||
private static double cubic(double p0, double p1, double p2, double p3, double factor) {
|
||||
double squared = factor * factor;
|
||||
double cubed = squared * factor;
|
||||
return 0.5D * ((2D * p1)
|
||||
+ ((-p0 + p2) * factor)
|
||||
+ (((2D * p0) - (5D * p1) + (4D * p2) - p3) * squared)
|
||||
+ ((-p0 + (3D * p1) - (3D * p2) + p3) * cubed));
|
||||
}
|
||||
|
||||
private static double clamp01(double value) {
|
||||
return Math.max(0D, Math.min(1D, value));
|
||||
}
|
||||
|
||||
static record State(
|
||||
IrisImageMap definition,
|
||||
IrisImageMapSourceMetadata sourceMetadata,
|
||||
IrisImageMapType type,
|
||||
FloatTiles scalarValues,
|
||||
IntTiles targetIndices,
|
||||
String[] targets,
|
||||
SourceTransform sourceTransform,
|
||||
IrisImageMapSampling sampling,
|
||||
IrisImageMapOutOfBounds outOfBounds,
|
||||
double fallbackValue,
|
||||
String fallbackTarget,
|
||||
double minimumHeight,
|
||||
double maximumHeight,
|
||||
double verticalOffset,
|
||||
boolean clampHeight,
|
||||
int clippedPixelCount,
|
||||
int unknownColorPixelCount,
|
||||
String contentHash
|
||||
) {
|
||||
State withoutDecodedValues() {
|
||||
return new State(
|
||||
definition,
|
||||
sourceMetadata,
|
||||
type,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
sourceTransform,
|
||||
sampling,
|
||||
outOfBounds,
|
||||
fallbackValue,
|
||||
fallbackTarget,
|
||||
minimumHeight,
|
||||
maximumHeight,
|
||||
verticalOffset,
|
||||
clampHeight,
|
||||
clippedPixelCount,
|
||||
unknownColorPixelCount,
|
||||
contentHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static record SourceTransform(
|
||||
double sourceXFromWorldX,
|
||||
double sourceXFromWorldZ,
|
||||
double sourceXOffset,
|
||||
double sourceZFromWorldX,
|
||||
double sourceZFromWorldZ,
|
||||
double sourceZOffset
|
||||
) {
|
||||
double sourceX(double worldX, double worldZ) {
|
||||
return (sourceXFromWorldX * worldX) + (sourceXFromWorldZ * worldZ) + sourceXOffset;
|
||||
}
|
||||
|
||||
double sourceZ(double worldX, double worldZ) {
|
||||
return (sourceZFromWorldX * worldX) + (sourceZFromWorldZ * worldZ) + sourceZOffset;
|
||||
}
|
||||
}
|
||||
|
||||
static final class FloatTiles {
|
||||
private static final int TILE_SHIFT = 20;
|
||||
private static final int TILE_SIZE = 1 << TILE_SHIFT;
|
||||
private static final int TILE_MASK = TILE_SIZE - 1;
|
||||
private final float[][] tiles;
|
||||
|
||||
FloatTiles(int size) {
|
||||
int tileCount = (size + TILE_SIZE - 1) >>> TILE_SHIFT;
|
||||
tiles = new float[tileCount][];
|
||||
for (int tile = 0; tile < tileCount; tile++) {
|
||||
int remaining = size - (tile << TILE_SHIFT);
|
||||
tiles[tile] = new float[Math.min(TILE_SIZE, remaining)];
|
||||
}
|
||||
}
|
||||
|
||||
float get(int index) {
|
||||
return tiles[index >>> TILE_SHIFT][index & TILE_MASK];
|
||||
}
|
||||
|
||||
void set(int index, float value) {
|
||||
tiles[index >>> TILE_SHIFT][index & TILE_MASK] = value;
|
||||
}
|
||||
}
|
||||
|
||||
static final class IntTiles {
|
||||
private static final int TILE_SHIFT = 20;
|
||||
private static final int TILE_SIZE = 1 << TILE_SHIFT;
|
||||
private static final int TILE_MASK = TILE_SIZE - 1;
|
||||
private final int[][] tiles;
|
||||
|
||||
IntTiles(int size) {
|
||||
int tileCount = (size + TILE_SIZE - 1) >>> TILE_SHIFT;
|
||||
tiles = new int[tileCount][];
|
||||
for (int tile = 0; tile < tileCount; tile++) {
|
||||
int remaining = size - (tile << TILE_SHIFT);
|
||||
tiles[tile] = new int[Math.min(TILE_SIZE, remaining)];
|
||||
}
|
||||
}
|
||||
|
||||
int get(int index) {
|
||||
return tiles[index >>> TILE_SHIFT][index & TILE_MASK];
|
||||
}
|
||||
|
||||
void set(int index, int value) {
|
||||
tiles[index >>> TILE_SHIFT][index & TILE_MASK] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,825 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageColorMode;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapAlpha;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOrigin;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapSampling;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisImageMapUnknownColor;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class IrisImageMapCompiler {
|
||||
public static final int MINIMUM_DIMENSION = 1;
|
||||
public static final int MAXIMUM_DIMENSION = 16_384;
|
||||
public static final long MAXIMUM_PIXELS = 16_777_216L;
|
||||
|
||||
private static final Pattern COLOR_PATTERN = Pattern.compile("#[0-9a-fA-F]{6}");
|
||||
|
||||
private IrisImageMapCompiler() {
|
||||
}
|
||||
|
||||
public static CompiledIrisImageMap compile(IrisImageMap definition, IrisImage image) {
|
||||
if (definition == null) {
|
||||
throw new IrisImageMapValidationException("Image-map definition is required");
|
||||
}
|
||||
if (image == null) {
|
||||
throw new IrisImageMapValidationException("Decoded image source is required");
|
||||
}
|
||||
|
||||
List<String> diagnostics = validate(definition, image);
|
||||
if (!diagnostics.isEmpty()) {
|
||||
throw new IrisImageMapValidationException(diagnostics);
|
||||
}
|
||||
|
||||
try {
|
||||
SourceDigest sourceDigest = hashSource(image);
|
||||
IrisImageMapSourceMetadata metadata = new IrisImageMapSourceMetadata(
|
||||
image.getFormat(),
|
||||
image.getColorMode(),
|
||||
image.getWidth(),
|
||||
image.getHeight(),
|
||||
image.getColorComponentCount(),
|
||||
image.getChannelCount(),
|
||||
image.getBitDepth(),
|
||||
image.hasAlpha(),
|
||||
sourceDigest.minimumAlpha(),
|
||||
sourceDigest.maximumAlpha(),
|
||||
sourceDigest.hash()
|
||||
);
|
||||
IrisImageMap snapshot = snapshot(definition);
|
||||
CompiledIrisImageMap.FloatTiles scalarValues = null;
|
||||
CompiledIrisImageMap.IntTiles targetIndices = null;
|
||||
String[] targets = null;
|
||||
int clippedPixelCount = 0;
|
||||
int unknownColorPixelCount = 0;
|
||||
|
||||
if (definition.getType() == IrisImageMapType.COLOR_MAP) {
|
||||
CompiledTargets compiledTargets = compileTargets(definition, image);
|
||||
targetIndices = compiledTargets.indices();
|
||||
targets = compiledTargets.targets();
|
||||
unknownColorPixelCount = compiledTargets.unknownColorPixelCount();
|
||||
} else {
|
||||
scalarValues = compileScalars(definition, image);
|
||||
if (definition.getSmoothingRadius() > 0) {
|
||||
smooth(scalarValues, image.getWidth(), image.getHeight(), definition.getSmoothingRadius());
|
||||
}
|
||||
clippedPixelCount = countClippedPixels(
|
||||
definition, scalarValues, Math.multiplyExact(image.getWidth(), image.getHeight())
|
||||
);
|
||||
}
|
||||
|
||||
String contentHash = hashContent(snapshot, metadata);
|
||||
String fallbackTarget = definition.getFallbackTarget() == null || definition.getFallbackTarget().isBlank()
|
||||
? null
|
||||
: definition.getFallbackTarget();
|
||||
CompiledIrisImageMap.SourceTransform sourceTransform = compileSourceTransform(definition);
|
||||
CompiledIrisImageMap.State state = new CompiledIrisImageMap.State(
|
||||
snapshot,
|
||||
metadata,
|
||||
definition.getType(),
|
||||
scalarValues,
|
||||
targetIndices,
|
||||
targets,
|
||||
sourceTransform,
|
||||
definition.getSampling(),
|
||||
definition.getOutOfBounds(),
|
||||
definition.getFallbackValue(),
|
||||
fallbackTarget,
|
||||
definition.getMinimumHeight(),
|
||||
definition.getMaximumHeight(),
|
||||
definition.getVerticalOffset(),
|
||||
definition.isClamp(),
|
||||
clippedPixelCount,
|
||||
unknownColorPixelCount,
|
||||
contentHash
|
||||
);
|
||||
return new CompiledIrisImageMap(state);
|
||||
} catch (IrisImageMapValidationException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Failed to compile image map '" + definition.getSource() + "': " + exception.getMessage(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> validate(IrisImageMap definition, IrisImage image) {
|
||||
List<String> diagnostics = new ArrayList<>();
|
||||
if (definition.getSource() == null || definition.getSource().isBlank()) {
|
||||
diagnostics.add("Image-map source must not be blank");
|
||||
}
|
||||
validateSource(image, diagnostics);
|
||||
validateCoordinates(definition, diagnostics);
|
||||
validateScalarSettings(definition, diagnostics);
|
||||
validateType(definition, image, diagnostics);
|
||||
validateLegend(definition, diagnostics);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private static void validateSource(IrisImage image, List<String> diagnostics) {
|
||||
if (!"png".equals(image.getFormat())) {
|
||||
diagnostics.add("Image format must be PNG, got " + image.getFormat());
|
||||
}
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
if (width < MINIMUM_DIMENSION || width > MAXIMUM_DIMENSION) {
|
||||
diagnostics.add("Image width must be " + MINIMUM_DIMENSION + ".." + MAXIMUM_DIMENSION + ", got " + width);
|
||||
}
|
||||
if (height < MINIMUM_DIMENSION || height > MAXIMUM_DIMENSION) {
|
||||
diagnostics.add("Image height must be " + MINIMUM_DIMENSION + ".." + MAXIMUM_DIMENSION + ", got " + height);
|
||||
}
|
||||
long pixels = (long) width * height;
|
||||
if (pixels > MAXIMUM_PIXELS) {
|
||||
diagnostics.add("Image contains " + pixels + " pixels; maximum is " + MAXIMUM_PIXELS);
|
||||
}
|
||||
if (image.getColorMode() == IrisImageColorMode.INDEXED) {
|
||||
diagnostics.add("Indexed PNG images are unsupported; convert the source to grayscale, RGB, or RGBA");
|
||||
} else if (image.getColorMode() == IrisImageColorMode.UNSUPPORTED) {
|
||||
diagnostics.add("Unsupported PNG color mode with " + image.getColorComponentCount()
|
||||
+ " color components and " + image.getChannelCount() + " channels");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateCoordinates(IrisImageMap definition, List<String> diagnostics) {
|
||||
if (!finiteAtLeast(definition.getBlocksPerPixel(), IrisImageMap.MINIMUM_SCALE)) {
|
||||
diagnostics.add("blocksPerPixel must be finite and at least " + IrisImageMap.MINIMUM_SCALE);
|
||||
}
|
||||
IrisImageMapOrigin origin = definition.getOrigin();
|
||||
if (origin == null || !Double.isFinite(origin.getX()) || !Double.isFinite(origin.getZ())) {
|
||||
diagnostics.add("origin X and Z must be finite");
|
||||
}
|
||||
IrisImageMapOrigin sourceOrigin = definition.getSourceOrigin();
|
||||
if (sourceOrigin == null || !Double.isFinite(sourceOrigin.getX()) || !Double.isFinite(sourceOrigin.getZ())) {
|
||||
diagnostics.add("sourceOrigin X and Z must be finite");
|
||||
}
|
||||
if (definition.getRotation() == null) {
|
||||
diagnostics.add("rotation is required");
|
||||
}
|
||||
if (definition.getSampling() == null) {
|
||||
diagnostics.add("sampling is required");
|
||||
}
|
||||
if (definition.getOutOfBounds() == null) {
|
||||
diagnostics.add("outOfBounds is required");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScalarSettings(IrisImageMap definition, List<String> diagnostics) {
|
||||
if (!range(definition.getFallbackValue(), 0D, 1D)) {
|
||||
diagnostics.add("fallbackValue must be finite and within 0..1");
|
||||
}
|
||||
if (!Double.isFinite(definition.getMinimumHeight()) || !Double.isFinite(definition.getMaximumHeight())) {
|
||||
diagnostics.add("minimumHeight and maximumHeight must be finite");
|
||||
} else if (definition.getMaximumHeight() < definition.getMinimumHeight()) {
|
||||
diagnostics.add("maximumHeight must be greater than or equal to minimumHeight");
|
||||
}
|
||||
if (!Double.isFinite(definition.getVerticalOffset())) {
|
||||
diagnostics.add("verticalOffset must be finite");
|
||||
}
|
||||
if (!finiteAtLeast(definition.getCurveExponent(), IrisImageMap.MINIMUM_SCALE)) {
|
||||
diagnostics.add("curveExponent must be finite and at least " + IrisImageMap.MINIMUM_SCALE);
|
||||
}
|
||||
if (definition.getSmoothingRadius() < 0 || definition.getSmoothingRadius() > 32) {
|
||||
diagnostics.add("smoothingRadius must be within 0..32");
|
||||
}
|
||||
if (!range(definition.getThreshold(), 0D, 1D)) {
|
||||
diagnostics.add("threshold must be finite and within 0..1");
|
||||
}
|
||||
if (!range(definition.getFalloff(), 0D, 1D)) {
|
||||
diagnostics.add("falloff must be finite and within 0..1");
|
||||
}
|
||||
if (!range(definition.getColorTolerance(), 0D, IrisImageMap.MAXIMUM_COLOR_TOLERANCE)) {
|
||||
diagnostics.add("colorTolerance must be finite and within 0.."
|
||||
+ IrisImageMap.MAXIMUM_COLOR_TOLERANCE);
|
||||
}
|
||||
if (definition.getAlpha() == null) {
|
||||
diagnostics.add("alpha policy is required");
|
||||
}
|
||||
if (definition.getUnknownColor() == null) {
|
||||
diagnostics.add("unknownColor policy is required");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateType(IrisImageMap definition, IrisImage image, List<String> diagnostics) {
|
||||
IrisImageMapType type = definition.getType();
|
||||
if (type == null) {
|
||||
diagnostics.add("Image-map type is required");
|
||||
return;
|
||||
}
|
||||
|
||||
IrisImageColorMode colorMode = image.getColorMode();
|
||||
int bitDepth = image.getBitDepth();
|
||||
switch (type) {
|
||||
case GRAYSCALE_HEIGHT, BINARY_MASK, GRAYSCALE_MASK -> {
|
||||
if (colorMode != IrisImageColorMode.GRAYSCALE) {
|
||||
diagnostics.add(type + " requires a grayscale PNG, got " + colorMode);
|
||||
}
|
||||
if (bitDepth != 8 && bitDepth != 16) {
|
||||
diagnostics.add(type + " requires 8-bit or 16-bit grayscale samples, got " + bitDepth + "-bit");
|
||||
}
|
||||
}
|
||||
case RGB_HEIGHT, COLOR_MAP -> {
|
||||
if (colorMode != IrisImageColorMode.RGB && colorMode != IrisImageColorMode.RGBA) {
|
||||
diagnostics.add(type + " requires an RGB or RGBA PNG, got " + colorMode);
|
||||
}
|
||||
if (bitDepth != 8) {
|
||||
diagnostics.add(type + " requires canonical 8-bit RGB channels, got " + bitDepth + "-bit");
|
||||
}
|
||||
}
|
||||
case ALPHA_MASK -> {
|
||||
if (colorMode != IrisImageColorMode.RGBA || !image.hasAlpha()) {
|
||||
diagnostics.add("ALPHA_MASK requires an 8-bit RGBA PNG");
|
||||
}
|
||||
if (bitDepth != 8) {
|
||||
diagnostics.add("ALPHA_MASK requires an 8-bit alpha channel, got " + bitDepth + "-bit color channels");
|
||||
}
|
||||
if (definition.getAlpha() != null && definition.getAlpha() != IrisImageMapAlpha.IGNORE) {
|
||||
diagnostics.add("ALPHA_MASK requires alpha=IGNORE because alpha is the map data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((type == IrisImageMapType.COLOR_MAP || type == IrisImageMapType.BINARY_MASK)
|
||||
&& definition.getSampling() != null
|
||||
&& definition.getSampling() != IrisImageMapSampling.NEAREST) {
|
||||
diagnostics.add(type + " requires NEAREST sampling");
|
||||
}
|
||||
if (type == IrisImageMapType.COLOR_MAP && definition.getSmoothingRadius() != 0) {
|
||||
diagnostics.add("COLOR_MAP does not support numeric smoothing");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateLegend(IrisImageMap definition, List<String> diagnostics) {
|
||||
KMap<String, String> colors = definition.getColors();
|
||||
if (definition.getType() != IrisImageMapType.COLOR_MAP) {
|
||||
if (colors != null && !colors.isEmpty()) {
|
||||
diagnostics.add("colors may only be set for COLOR_MAP definitions");
|
||||
}
|
||||
if (definition.getColorTolerance() != 0D) {
|
||||
diagnostics.add("colorTolerance may only be set for COLOR_MAP definitions");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (colors == null || colors.isEmpty()) {
|
||||
diagnostics.add("COLOR_MAP requires at least one #RRGGBB legend entry");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Integer, String> decoded = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : colors.entrySet()) {
|
||||
String encoded = entry.getKey();
|
||||
String target = entry.getValue();
|
||||
if (encoded == null || !COLOR_PATTERN.matcher(encoded).matches()) {
|
||||
diagnostics.add("Color legend key must use exact #RRGGBB syntax, got " + encoded);
|
||||
continue;
|
||||
}
|
||||
if (target == null || target.isBlank()) {
|
||||
diagnostics.add("Color legend target for " + encoded + " must not be blank");
|
||||
}
|
||||
int rgb = Integer.parseInt(encoded.substring(1), 16);
|
||||
String previous = decoded.putIfAbsent(rgb, encoded);
|
||||
if (previous != null) {
|
||||
diagnostics.add("Color legend duplicates raw color " + previous + " as " + encoded);
|
||||
}
|
||||
}
|
||||
|
||||
boolean fallbackRequired = definition.getOutOfBounds() == IrisImageMapOutOfBounds.FALLBACK
|
||||
|| definition.getUnknownColor() == IrisImageMapUnknownColor.FALLBACK
|
||||
|| definition.getAlpha() == IrisImageMapAlpha.TRANSPARENT_IS_FALLBACK
|
||||
|| definition.getAlpha() == IrisImageMapAlpha.MASK;
|
||||
if (fallbackRequired && (definition.getFallbackTarget() == null || definition.getFallbackTarget().isBlank())) {
|
||||
diagnostics.add("fallbackTarget is required by the configured color-map fallback policy");
|
||||
}
|
||||
}
|
||||
|
||||
private static CompiledIrisImageMap.FloatTiles compileScalars(IrisImageMap definition, IrisImage image) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
CompiledIrisImageMap.FloatTiles values = new CompiledIrisImageMap.FloatTiles(
|
||||
Math.multiplyExact(width, height)
|
||||
);
|
||||
for (int sourceZ = 0; sourceZ < height; sourceZ++) {
|
||||
for (int sourceX = 0; sourceX < width; sourceX++) {
|
||||
double alpha = image.getAlphaNormalized(sourceX, sourceZ);
|
||||
validateAlphaPixel(definition, sourceX, sourceZ, alpha);
|
||||
double value;
|
||||
if (definition.getAlpha() == IrisImageMapAlpha.TRANSPARENT_IS_FALLBACK && alpha == 0D) {
|
||||
value = definition.getFallbackValue();
|
||||
} else {
|
||||
value = rawScalar(definition.getType(), image, sourceX, sourceZ);
|
||||
if (definition.isInverted()) {
|
||||
value = 1D - value;
|
||||
}
|
||||
value = Math.pow(clamp01(value), definition.getCurveExponent());
|
||||
if (definition.getType() == IrisImageMapType.BINARY_MASK) {
|
||||
value = threshold(value, definition.getThreshold(), definition.getFalloff());
|
||||
}
|
||||
if (definition.getAlpha() == IrisImageMapAlpha.MASK) {
|
||||
value *= alpha;
|
||||
}
|
||||
}
|
||||
values.set((sourceZ * width) + sourceX, (float) clamp01(value));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static double rawScalar(IrisImageMapType type, IrisImage image, int sourceX, int sourceZ) {
|
||||
return switch (type) {
|
||||
case GRAYSCALE_HEIGHT, BINARY_MASK, GRAYSCALE_MASK -> image.getBandNormalized(sourceX, sourceZ, 0);
|
||||
case RGB_HEIGHT -> {
|
||||
int red = image.getBandSample(sourceX, sourceZ, 0);
|
||||
int green = image.getBandSample(sourceX, sourceZ, 1);
|
||||
int blue = image.getBandSample(sourceX, sourceZ, 2);
|
||||
int encoded = (red << 16) | (green << 8) | blue;
|
||||
yield encoded / 16_777_215D;
|
||||
}
|
||||
case ALPHA_MASK -> image.getAlphaNormalized(sourceX, sourceZ);
|
||||
case COLOR_MAP -> throw new IrisImageMapValidationException("COLOR_MAP does not produce scalar pixels");
|
||||
};
|
||||
}
|
||||
|
||||
private static CompiledTargets compileTargets(IrisImageMap definition, IrisImage image) {
|
||||
Legend legend = compileLegend(definition);
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
CompiledIrisImageMap.IntTiles indices = new CompiledIrisImageMap.IntTiles(
|
||||
Math.multiplyExact(width, height)
|
||||
);
|
||||
int unknownColorPixelCount = 0;
|
||||
for (int sourceZ = 0; sourceZ < height; sourceZ++) {
|
||||
for (int sourceX = 0; sourceX < width; sourceX++) {
|
||||
double alpha = image.getAlphaNormalized(sourceX, sourceZ);
|
||||
validateAlphaPixel(definition, sourceX, sourceZ, alpha);
|
||||
int targetIndex;
|
||||
if (definition.getAlpha() == IrisImageMapAlpha.TRANSPARENT_IS_FALLBACK && alpha == 0D) {
|
||||
targetIndex = legend.fallbackIndex();
|
||||
} else if (definition.getAlpha() == IrisImageMapAlpha.MASK && alpha < 1D) {
|
||||
if (alpha == 0D) {
|
||||
targetIndex = legend.fallbackIndex();
|
||||
} else {
|
||||
throw new IrisImageMapValidationException(
|
||||
"COLOR_MAP alpha=MASK requires binary alpha; pixel " + sourceX + "," + sourceZ
|
||||
+ " has alpha " + alpha
|
||||
);
|
||||
}
|
||||
} else {
|
||||
int rgb = image.getRawRgb8(sourceX, sourceZ);
|
||||
TargetMatch match = matchTarget(definition, legend, rgb, sourceX, sourceZ);
|
||||
targetIndex = match.targetIndex();
|
||||
if (match.unknown()) {
|
||||
unknownColorPixelCount++;
|
||||
}
|
||||
}
|
||||
indices.set((sourceZ * width) + sourceX, targetIndex);
|
||||
}
|
||||
}
|
||||
return new CompiledTargets(indices, legend.targets(), unknownColorPixelCount);
|
||||
}
|
||||
|
||||
private static Legend compileLegend(IrisImageMap definition) {
|
||||
List<MutableLegendEntry> entries = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : definition.getColors().entrySet()) {
|
||||
int rgb = Integer.parseInt(entry.getKey().substring(1), 16);
|
||||
entries.add(new MutableLegendEntry(rgb, entry.getKey().toUpperCase(), entry.getValue()));
|
||||
}
|
||||
entries.sort(Comparator.comparingInt(MutableLegendEntry::rgb).thenComparing(MutableLegendEntry::target));
|
||||
|
||||
LinkedHashMap<String, Integer> targetIndexes = new LinkedHashMap<>();
|
||||
Map<Integer, LegendEntry> exact = new HashMap<>();
|
||||
List<LegendEntry> compiled = new ArrayList<>();
|
||||
for (MutableLegendEntry entry : entries) {
|
||||
int targetIndex = targetIndexes.computeIfAbsent(entry.target(), ignored -> targetIndexes.size());
|
||||
LegendEntry compiledEntry = new LegendEntry(entry.rgb(), entry.encoded(), entry.target(), targetIndex);
|
||||
exact.put(entry.rgb(), compiledEntry);
|
||||
compiled.add(compiledEntry);
|
||||
}
|
||||
|
||||
int fallbackIndex = -1;
|
||||
if (definition.getFallbackTarget() != null && !definition.getFallbackTarget().isBlank()) {
|
||||
fallbackIndex = targetIndexes.computeIfAbsent(
|
||||
definition.getFallbackTarget(),
|
||||
ignored -> targetIndexes.size()
|
||||
);
|
||||
}
|
||||
String[] targets = new String[targetIndexes.size()];
|
||||
for (Map.Entry<String, Integer> entry : targetIndexes.entrySet()) {
|
||||
targets[entry.getValue()] = entry.getKey();
|
||||
}
|
||||
return new Legend(List.copyOf(compiled), Map.copyOf(exact), targets, fallbackIndex);
|
||||
}
|
||||
|
||||
private static TargetMatch matchTarget(
|
||||
IrisImageMap definition,
|
||||
Legend legend,
|
||||
int rgb,
|
||||
int sourceX,
|
||||
int sourceZ
|
||||
) {
|
||||
LegendEntry exact = legend.exact().get(rgb);
|
||||
if (exact != null) {
|
||||
return new TargetMatch(exact.targetIndex(), false);
|
||||
}
|
||||
|
||||
double toleranceSquared = definition.getColorTolerance() * definition.getColorTolerance();
|
||||
LegendEntry matched = null;
|
||||
List<String> ambiguous = new ArrayList<>();
|
||||
if (toleranceSquared > 0D) {
|
||||
for (LegendEntry candidate : legend.entries()) {
|
||||
if (colorDistanceSquared(rgb, candidate.rgb()) <= toleranceSquared) {
|
||||
if (matched == null) {
|
||||
matched = candidate;
|
||||
}
|
||||
ambiguous.add(candidate.encoded());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambiguous.size() > 1) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Color " + encodeColor(rgb) + " at " + sourceX + "," + sourceZ
|
||||
+ " ambiguously matches " + String.join(", ", ambiguous)
|
||||
);
|
||||
}
|
||||
if (matched != null) {
|
||||
return new TargetMatch(matched.targetIndex(), false);
|
||||
}
|
||||
|
||||
int targetIndex = switch (definition.getUnknownColor()) {
|
||||
case ERROR -> throw new IrisImageMapValidationException(
|
||||
"Color " + encodeColor(rgb) + " at " + sourceX + "," + sourceZ + " is absent from the legend"
|
||||
);
|
||||
case FALLBACK -> legend.fallbackIndex();
|
||||
case IGNORE -> -1;
|
||||
};
|
||||
return new TargetMatch(targetIndex, true);
|
||||
}
|
||||
|
||||
private static int countClippedPixels(
|
||||
IrisImageMap definition,
|
||||
CompiledIrisImageMap.FloatTiles values,
|
||||
int size
|
||||
) {
|
||||
if (!definition.isClamp()
|
||||
|| (definition.getType() != IrisImageMapType.GRAYSCALE_HEIGHT
|
||||
&& definition.getType() != IrisImageMapType.RGB_HEIGHT)) {
|
||||
return 0;
|
||||
}
|
||||
double minimum = definition.getMinimumHeight();
|
||||
double maximum = definition.getMaximumHeight();
|
||||
double range = maximum - minimum;
|
||||
double offset = definition.getVerticalOffset();
|
||||
int clipped = 0;
|
||||
for (int index = 0; index < size; index++) {
|
||||
double height = minimum + (values.get(index) * range) + offset;
|
||||
if (height < minimum || height > maximum) {
|
||||
clipped++;
|
||||
}
|
||||
}
|
||||
return clipped;
|
||||
}
|
||||
|
||||
private static void validateAlphaPixel(IrisImageMap definition, int sourceX, int sourceZ, double alpha) {
|
||||
if (definition.getAlpha() == IrisImageMapAlpha.ERROR && alpha < 1D) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Transparent pixel at " + sourceX + "," + sourceZ + " violates alpha=ERROR"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static double threshold(double value, double threshold, double falloff) {
|
||||
if (falloff == 0D) {
|
||||
return value >= threshold ? 1D : 0D;
|
||||
}
|
||||
return clamp01((value - threshold) / falloff);
|
||||
}
|
||||
|
||||
private static void smooth(CompiledIrisImageMap.FloatTiles values, int width, int height, int radius) {
|
||||
float[] line = new float[Math.max(width, height)];
|
||||
int diameter = (radius * 2) + 1;
|
||||
for (int sourceZ = 0; sourceZ < height; sourceZ++) {
|
||||
int rowOffset = sourceZ * width;
|
||||
for (int sourceX = 0; sourceX < width; sourceX++) {
|
||||
line[sourceX] = values.get(rowOffset + sourceX);
|
||||
}
|
||||
double sum = 0D;
|
||||
for (int offset = -radius; offset <= radius; offset++) {
|
||||
int sourceX = Math.max(0, Math.min(width - 1, offset));
|
||||
sum += line[sourceX];
|
||||
}
|
||||
for (int sourceX = 0; sourceX < width; sourceX++) {
|
||||
values.set(rowOffset + sourceX, (float) (sum / diameter));
|
||||
int leavingX = Math.max(0, Math.min(width - 1, sourceX - radius));
|
||||
int enteringX = Math.max(0, Math.min(width - 1, sourceX + radius + 1));
|
||||
sum += line[enteringX] - line[leavingX];
|
||||
}
|
||||
}
|
||||
|
||||
for (int sourceX = 0; sourceX < width; sourceX++) {
|
||||
for (int sourceZ = 0; sourceZ < height; sourceZ++) {
|
||||
line[sourceZ] = values.get((sourceZ * width) + sourceX);
|
||||
}
|
||||
double sum = 0D;
|
||||
for (int offset = -radius; offset <= radius; offset++) {
|
||||
int sourceZ = Math.max(0, Math.min(height - 1, offset));
|
||||
sum += line[sourceZ];
|
||||
}
|
||||
for (int sourceZ = 0; sourceZ < height; sourceZ++) {
|
||||
values.set((sourceZ * width) + sourceX, (float) (sum / diameter));
|
||||
int leavingZ = Math.max(0, Math.min(height - 1, sourceZ - radius));
|
||||
int enteringZ = Math.max(0, Math.min(height - 1, sourceZ + radius + 1));
|
||||
sum += line[enteringZ] - line[leavingZ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SourceDigest hashSource(IrisImage image) {
|
||||
HashAccumulator hash = new HashAccumulator();
|
||||
hash.addString(image.getFormat());
|
||||
hash.addString(image.getColorMode().name());
|
||||
hash.addInt(image.getWidth());
|
||||
hash.addInt(image.getHeight());
|
||||
hash.addInt(image.getColorComponentCount());
|
||||
hash.addInt(image.getChannelCount());
|
||||
hash.addInt(image.getBitDepth());
|
||||
hash.addBoolean(image.hasAlpha());
|
||||
double minimumAlpha = 1D;
|
||||
double maximumAlpha = image.hasAlpha() ? 0D : 1D;
|
||||
for (int sourceZ = 0; sourceZ < image.getHeight(); sourceZ++) {
|
||||
for (int sourceX = 0; sourceX < image.getWidth(); sourceX++) {
|
||||
double alpha = image.getAlphaNormalized(sourceX, sourceZ);
|
||||
minimumAlpha = Math.min(minimumAlpha, alpha);
|
||||
maximumAlpha = Math.max(maximumAlpha, alpha);
|
||||
for (int band = 0; band < image.getChannelCount(); band++) {
|
||||
hash.addInt(image.getBandSample(sourceX, sourceZ, band));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new SourceDigest(hash.finish(), minimumAlpha, maximumAlpha);
|
||||
}
|
||||
|
||||
private static String hashContent(IrisImageMap definition, IrisImageMapSourceMetadata metadata) {
|
||||
HashAccumulator hash = new HashAccumulator();
|
||||
hash.addString(metadata.decodedContentHash());
|
||||
hash.addString(definition.getSource());
|
||||
hash.addString(definition.getType().name());
|
||||
hash.addDouble(definition.getBlocksPerPixel());
|
||||
hash.addDouble(definition.getOrigin().getX());
|
||||
hash.addDouble(definition.getOrigin().getZ());
|
||||
hash.addDouble(definition.getSourceOrigin().getX());
|
||||
hash.addDouble(definition.getSourceOrigin().getZ());
|
||||
hash.addString(definition.getRotation().name());
|
||||
hash.addBoolean(definition.isMirrorX());
|
||||
hash.addBoolean(definition.isMirrorZ());
|
||||
hash.addString(definition.getSampling().name());
|
||||
hash.addString(definition.getOutOfBounds().name());
|
||||
hash.addDouble(definition.getFallbackValue());
|
||||
hash.addString(definition.getFallbackTarget());
|
||||
hash.addString(definition.getAlpha().name());
|
||||
hash.addDouble(definition.getMinimumHeight());
|
||||
hash.addDouble(definition.getMaximumHeight());
|
||||
hash.addDouble(definition.getVerticalOffset());
|
||||
hash.addBoolean(definition.isClamp());
|
||||
hash.addBoolean(definition.isInverted());
|
||||
hash.addDouble(definition.getCurveExponent());
|
||||
hash.addInt(definition.getSmoothingRadius());
|
||||
hash.addDouble(definition.getThreshold());
|
||||
hash.addDouble(definition.getFalloff());
|
||||
hash.addDouble(definition.getColorTolerance());
|
||||
hash.addString(definition.getUnknownColor().name());
|
||||
|
||||
KMap<String, String> configuredColors = definition.getColors();
|
||||
List<Map.Entry<String, String>> colors = configuredColors == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(configuredColors.entrySet());
|
||||
colors.sort(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER));
|
||||
hash.addInt(colors.size());
|
||||
for (Map.Entry<String, String> entry : colors) {
|
||||
hash.addString(entry.getKey().toUpperCase());
|
||||
hash.addString(entry.getValue());
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
|
||||
private static CompiledIrisImageMap.SourceTransform compileSourceTransform(IrisImageMap definition) {
|
||||
double sourceXFromWorldX;
|
||||
double sourceXFromWorldZ;
|
||||
double sourceZFromWorldX;
|
||||
double sourceZFromWorldZ;
|
||||
switch (definition.getRotation()) {
|
||||
case DEG_0 -> {
|
||||
sourceXFromWorldX = 1D;
|
||||
sourceXFromWorldZ = 0D;
|
||||
sourceZFromWorldX = 0D;
|
||||
sourceZFromWorldZ = 1D;
|
||||
}
|
||||
case DEG_90 -> {
|
||||
sourceXFromWorldX = 0D;
|
||||
sourceXFromWorldZ = 1D;
|
||||
sourceZFromWorldX = -1D;
|
||||
sourceZFromWorldZ = 0D;
|
||||
}
|
||||
case DEG_180 -> {
|
||||
sourceXFromWorldX = -1D;
|
||||
sourceXFromWorldZ = 0D;
|
||||
sourceZFromWorldX = 0D;
|
||||
sourceZFromWorldZ = -1D;
|
||||
}
|
||||
case DEG_270 -> {
|
||||
sourceXFromWorldX = 0D;
|
||||
sourceXFromWorldZ = -1D;
|
||||
sourceZFromWorldX = 1D;
|
||||
sourceZFromWorldZ = 0D;
|
||||
}
|
||||
default -> throw new IrisImageMapValidationException(
|
||||
"Unsupported image-map rotation " + definition.getRotation()
|
||||
);
|
||||
}
|
||||
if (definition.isMirrorX()) {
|
||||
sourceXFromWorldX = -sourceXFromWorldX;
|
||||
sourceXFromWorldZ = -sourceXFromWorldZ;
|
||||
}
|
||||
if (definition.isMirrorZ()) {
|
||||
sourceZFromWorldX = -sourceZFromWorldX;
|
||||
sourceZFromWorldZ = -sourceZFromWorldZ;
|
||||
}
|
||||
double inverseScale = 1D / definition.getBlocksPerPixel();
|
||||
sourceXFromWorldX *= inverseScale;
|
||||
sourceXFromWorldZ *= inverseScale;
|
||||
sourceZFromWorldX *= inverseScale;
|
||||
sourceZFromWorldZ *= inverseScale;
|
||||
double sourceXOffset = definition.getSourceOrigin().getX()
|
||||
- (sourceXFromWorldX * definition.getOrigin().getX())
|
||||
- (sourceXFromWorldZ * definition.getOrigin().getZ());
|
||||
double sourceZOffset = definition.getSourceOrigin().getZ()
|
||||
- (sourceZFromWorldX * definition.getOrigin().getX())
|
||||
- (sourceZFromWorldZ * definition.getOrigin().getZ());
|
||||
if (!Double.isFinite(sourceXFromWorldX)
|
||||
|| !Double.isFinite(sourceXFromWorldZ)
|
||||
|| !Double.isFinite(sourceXOffset)
|
||||
|| !Double.isFinite(sourceZFromWorldX)
|
||||
|| !Double.isFinite(sourceZFromWorldZ)
|
||||
|| !Double.isFinite(sourceZOffset)) {
|
||||
throw new IrisImageMapValidationException("Image-map coordinate transform must remain finite");
|
||||
}
|
||||
return new CompiledIrisImageMap.SourceTransform(
|
||||
sourceXFromWorldX,
|
||||
sourceXFromWorldZ,
|
||||
sourceXOffset,
|
||||
sourceZFromWorldX,
|
||||
sourceZFromWorldZ,
|
||||
sourceZOffset
|
||||
);
|
||||
}
|
||||
|
||||
private static IrisImageMap snapshot(IrisImageMap definition) {
|
||||
KMap<String, String> configuredColors = definition.getColors();
|
||||
KMap<String, String> colors = configuredColors == null ? new KMap<>() : new KMap<>(configuredColors);
|
||||
return new IrisImageMap()
|
||||
.setSource(definition.getSource())
|
||||
.setType(definition.getType())
|
||||
.setBlocksPerPixel(definition.getBlocksPerPixel())
|
||||
.setOrigin(new IrisImageMapOrigin(definition.getOrigin().getX(), definition.getOrigin().getZ()))
|
||||
.setSourceOrigin(new IrisImageMapOrigin(
|
||||
definition.getSourceOrigin().getX(),
|
||||
definition.getSourceOrigin().getZ()
|
||||
))
|
||||
.setRotation(definition.getRotation())
|
||||
.setMirrorX(definition.isMirrorX())
|
||||
.setMirrorZ(definition.isMirrorZ())
|
||||
.setSampling(definition.getSampling())
|
||||
.setOutOfBounds(definition.getOutOfBounds())
|
||||
.setFallbackValue(definition.getFallbackValue())
|
||||
.setFallbackTarget(definition.getFallbackTarget())
|
||||
.setAlpha(definition.getAlpha())
|
||||
.setMinimumHeight(definition.getMinimumHeight())
|
||||
.setMaximumHeight(definition.getMaximumHeight())
|
||||
.setVerticalOffset(definition.getVerticalOffset())
|
||||
.setClamp(definition.isClamp())
|
||||
.setInverted(definition.isInverted())
|
||||
.setCurveExponent(definition.getCurveExponent())
|
||||
.setSmoothingRadius(definition.getSmoothingRadius())
|
||||
.setThreshold(definition.getThreshold())
|
||||
.setFalloff(definition.getFalloff())
|
||||
.setColorTolerance(definition.getColorTolerance())
|
||||
.setUnknownColor(definition.getUnknownColor())
|
||||
.setColors(colors);
|
||||
}
|
||||
|
||||
private static double colorDistanceSquared(int first, int second) {
|
||||
int red = ((first >>> 16) & 0xFF) - ((second >>> 16) & 0xFF);
|
||||
int green = ((first >>> 8) & 0xFF) - ((second >>> 8) & 0xFF);
|
||||
int blue = (first & 0xFF) - (second & 0xFF);
|
||||
return (red * red) + (green * green) + (blue * blue);
|
||||
}
|
||||
|
||||
private static String encodeColor(int rgb) {
|
||||
return String.format(Locale.ROOT, "#%06X", rgb & 0xFFFFFF);
|
||||
}
|
||||
|
||||
private static boolean finiteAtLeast(double value, double minimum) {
|
||||
return Double.isFinite(value) && value >= minimum;
|
||||
}
|
||||
|
||||
private static boolean range(double value, double minimum, double maximum) {
|
||||
return Double.isFinite(value) && value >= minimum && value <= maximum;
|
||||
}
|
||||
|
||||
private static double clamp01(double value) {
|
||||
return Math.max(0D, Math.min(1D, value));
|
||||
}
|
||||
|
||||
private record MutableLegendEntry(int rgb, String encoded, String target) {
|
||||
}
|
||||
|
||||
private record LegendEntry(int rgb, String encoded, String target, int targetIndex) {
|
||||
}
|
||||
|
||||
private record Legend(
|
||||
List<LegendEntry> entries,
|
||||
Map<Integer, LegendEntry> exact,
|
||||
String[] targets,
|
||||
int fallbackIndex
|
||||
) {
|
||||
}
|
||||
|
||||
private record CompiledTargets(
|
||||
CompiledIrisImageMap.IntTiles indices,
|
||||
String[] targets,
|
||||
int unknownColorPixelCount
|
||||
) {
|
||||
}
|
||||
|
||||
private record TargetMatch(int targetIndex, boolean unknown) {
|
||||
}
|
||||
|
||||
private record SourceDigest(String hash, double minimumAlpha, double maximumAlpha) {
|
||||
}
|
||||
|
||||
private static final class HashAccumulator {
|
||||
private final MessageDigest digest;
|
||||
|
||||
private HashAccumulator() {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void addBoolean(boolean value) {
|
||||
digest.update((byte) (value ? 1 : 0));
|
||||
}
|
||||
|
||||
private void addInt(int value) {
|
||||
digest.update((byte) (value >>> 24));
|
||||
digest.update((byte) (value >>> 16));
|
||||
digest.update((byte) (value >>> 8));
|
||||
digest.update((byte) value);
|
||||
}
|
||||
|
||||
private void addLong(long value) {
|
||||
digest.update((byte) (value >>> 56));
|
||||
digest.update((byte) (value >>> 48));
|
||||
digest.update((byte) (value >>> 40));
|
||||
digest.update((byte) (value >>> 32));
|
||||
digest.update((byte) (value >>> 24));
|
||||
digest.update((byte) (value >>> 16));
|
||||
digest.update((byte) (value >>> 8));
|
||||
digest.update((byte) value);
|
||||
}
|
||||
|
||||
private void addDouble(double value) {
|
||||
addLong(Double.doubleToLongBits(value));
|
||||
}
|
||||
|
||||
private void addString(String value) {
|
||||
if (value == null) {
|
||||
addInt(-1);
|
||||
return;
|
||||
}
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
addInt(bytes.length);
|
||||
digest.update(bytes);
|
||||
}
|
||||
|
||||
private String finish() {
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class IrisImageMapMaskSampler {
|
||||
private static final IrisImageMapMaskSampler EMPTY = new IrisImageMapMaskSampler(List.of());
|
||||
|
||||
private final List<Layer> layers;
|
||||
|
||||
public IrisImageMapMaskSampler(List<Layer> layers) {
|
||||
this.layers = List.copyOf(Objects.requireNonNull(layers, "Image-map mask layers"));
|
||||
}
|
||||
|
||||
public static IrisImageMapMaskSampler empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public static Layer layer(CompiledIrisImageMap compiled, IrisImageMapMask definition) {
|
||||
Objects.requireNonNull(definition, "Image-map mask definition");
|
||||
if (definition.getOperation() == null) {
|
||||
throw new IrisImageMapValidationException("Image-map mask operation is required");
|
||||
}
|
||||
if (!unitRange(definition.getThreshold()) || !unitRange(definition.getFalloff())) {
|
||||
throw new IrisImageMapValidationException("Image-map mask threshold and falloff must be within 0..1");
|
||||
}
|
||||
return new Layer(
|
||||
Objects.requireNonNull(compiled, "Compiled image-map mask"),
|
||||
definition.getOperation(),
|
||||
definition.isInverted(),
|
||||
definition.getThreshold(),
|
||||
definition.getFalloff()
|
||||
);
|
||||
}
|
||||
|
||||
public static IrisImageMapMaskSampler of(
|
||||
List<CompiledIrisImageMap> compiled,
|
||||
List<IrisImageMapMask> definitions
|
||||
) {
|
||||
if (compiled.size() != definitions.size()) {
|
||||
throw new IllegalArgumentException("Compiled image-map masks and definitions must have equal sizes");
|
||||
}
|
||||
List<Layer> layers = new ArrayList<>(compiled.size());
|
||||
for (int index = 0; index < compiled.size(); index++) {
|
||||
layers.add(layer(compiled.get(index), definitions.get(index)));
|
||||
}
|
||||
return layers.isEmpty() ? empty() : new IrisImageMapMaskSampler(layers);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return layers.isEmpty();
|
||||
}
|
||||
|
||||
public double sample(double worldX, double worldZ) {
|
||||
double weight = 1D;
|
||||
for (Layer layer : layers) {
|
||||
double value = layer.sample(worldX, worldZ);
|
||||
weight = switch (layer.operation()) {
|
||||
case MULTIPLY -> weight * value;
|
||||
case MINIMUM -> Math.min(weight, value);
|
||||
case MAXIMUM -> Math.max(weight, value);
|
||||
case ADD -> weight + value;
|
||||
case SUBTRACT -> weight - value;
|
||||
};
|
||||
weight = clamp01(weight);
|
||||
}
|
||||
return weight;
|
||||
}
|
||||
|
||||
private static boolean unitRange(double value) {
|
||||
return Double.isFinite(value) && value >= 0D && value <= 1D;
|
||||
}
|
||||
|
||||
private static double clamp01(double value) {
|
||||
return Math.max(0D, Math.min(1D, value));
|
||||
}
|
||||
|
||||
public record Layer(
|
||||
CompiledIrisImageMap compiled,
|
||||
IrisImageMapMaskOperation operation,
|
||||
boolean inverted,
|
||||
double threshold,
|
||||
double falloff
|
||||
) {
|
||||
public Layer {
|
||||
Objects.requireNonNull(compiled, "Compiled image-map mask");
|
||||
Objects.requireNonNull(operation, "Image-map mask operation");
|
||||
if (!unitRange(threshold) || !unitRange(falloff)) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Image-map mask threshold and falloff must be within 0..1"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private double sample(double worldX, double worldZ) {
|
||||
double value = compiled.sampleNormalized(worldX, worldZ);
|
||||
if (inverted) {
|
||||
value = 1D - value;
|
||||
}
|
||||
if (threshold == 0D && falloff == 0D) {
|
||||
return value;
|
||||
}
|
||||
if (falloff == 0D) {
|
||||
return value >= threshold ? 1D : 0D;
|
||||
}
|
||||
return clamp01((value - threshold) / falloff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapBinding;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class IrisImageMapRuntime {
|
||||
private final int minimumWorldHeight;
|
||||
private final Map<String, RuntimeBinding> bindings;
|
||||
private final Map<IrisImageMapApplication, RuntimeBinding> applications;
|
||||
private final Set<IrisRegion> mappedRegions;
|
||||
private final Set<IrisBiome> mappedBiomes;
|
||||
|
||||
private IrisImageMapRuntime(
|
||||
int minimumWorldHeight,
|
||||
Map<String, RuntimeBinding> bindings,
|
||||
Map<IrisImageMapApplication, RuntimeBinding> applications,
|
||||
Set<IrisRegion> mappedRegions,
|
||||
Set<IrisBiome> mappedBiomes
|
||||
) {
|
||||
this.minimumWorldHeight = minimumWorldHeight;
|
||||
this.bindings = Map.copyOf(bindings);
|
||||
this.applications = Map.copyOf(applications);
|
||||
this.mappedRegions = Set.copyOf(mappedRegions);
|
||||
this.mappedBiomes = Set.copyOf(mappedBiomes);
|
||||
}
|
||||
|
||||
public static IrisImageMapRuntime compile(Engine engine) {
|
||||
Objects.requireNonNull(engine, "Image-map runtime requires an engine");
|
||||
return compile(engine.getData(), engine.getDimension(), engine.getMinHeight());
|
||||
}
|
||||
|
||||
public static IrisImageMapRuntime compile(IrisData data, IrisDimension dimension, int minimumWorldHeight) {
|
||||
Objects.requireNonNull(data, "Image-map runtime requires Iris data");
|
||||
Objects.requireNonNull(dimension, "Image-map runtime requires a dimension");
|
||||
Map<String, RuntimeBinding> bindings = new LinkedHashMap<>();
|
||||
Map<IrisImageMapApplication, RuntimeBinding> applications = new EnumMap<>(IrisImageMapApplication.class);
|
||||
Set<IrisRegion> mappedRegions = new LinkedHashSet<>();
|
||||
Set<IrisBiome> mappedBiomes = new LinkedHashSet<>();
|
||||
|
||||
for (IrisImageMapBinding binding : dimension.getImageMaps()) {
|
||||
RuntimeBinding runtimeBinding = compileBinding(binding, data, dimension, mappedRegions, mappedBiomes);
|
||||
RuntimeBinding duplicate = bindings.putIfAbsent(runtimeBinding.key, runtimeBinding);
|
||||
if (duplicate != null) {
|
||||
throw validation("Duplicate dimension image-map key '" + runtimeBinding.key + "'");
|
||||
}
|
||||
IrisImageMapApplication application = runtimeBinding.application;
|
||||
if (application != IrisImageMapApplication.MASK && application != IrisImageMapApplication.CUSTOM) {
|
||||
RuntimeBinding applicationDuplicate = applications.putIfAbsent(application, runtimeBinding);
|
||||
if (applicationDuplicate != null) {
|
||||
throw validation("Dimension declares more than one " + application + " image-map binding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (RuntimeBinding binding : bindings.values()) {
|
||||
binding.masks = resolveMasks(binding, bindings);
|
||||
}
|
||||
|
||||
return new IrisImageMapRuntime(
|
||||
minimumWorldHeight, bindings, applications, mappedRegions, mappedBiomes
|
||||
);
|
||||
}
|
||||
|
||||
public boolean has(IrisImageMapApplication application) {
|
||||
return applications.containsKey(application);
|
||||
}
|
||||
|
||||
public Set<IrisRegion> getMappedRegions() {
|
||||
return mappedRegions;
|
||||
}
|
||||
|
||||
public Set<IrisBiome> getMappedBiomes() {
|
||||
return mappedBiomes;
|
||||
}
|
||||
|
||||
public double sampleTerrainHeight(double worldX, double worldZ, double proceduralLocalHeight) {
|
||||
RuntimeBinding binding = applications.get(IrisImageMapApplication.TERRAIN_HEIGHT);
|
||||
if (binding == null) {
|
||||
return proceduralLocalHeight;
|
||||
}
|
||||
double mappedLocalHeight = binding.compiled.sampleHeight(worldX, worldZ) - minimumWorldHeight;
|
||||
double weight = binding.maskWeight(worldX, worldZ);
|
||||
return blendTerrainHeight(mappedLocalHeight, proceduralLocalHeight, weight);
|
||||
}
|
||||
|
||||
public IrisRegion sampleRegion(double worldX, double worldZ) {
|
||||
RuntimeBinding binding = applications.get(IrisImageMapApplication.REGION);
|
||||
if (binding == null || !selectCategorical(binding.maskWeight(worldX, worldZ))) {
|
||||
return null;
|
||||
}
|
||||
String target = binding.compiled.sampleTarget(worldX, worldZ);
|
||||
return target == null ? null : binding.regions.get(target);
|
||||
}
|
||||
|
||||
public IrisBiome sampleBiome(double worldX, double worldZ) {
|
||||
RuntimeBinding binding = applications.get(IrisImageMapApplication.BIOME);
|
||||
if (binding == null || !selectCategorical(binding.maskWeight(worldX, worldZ))) {
|
||||
return null;
|
||||
}
|
||||
String target = binding.compiled.sampleTarget(worldX, worldZ);
|
||||
return target == null ? null : binding.biomes.get(target);
|
||||
}
|
||||
|
||||
public PlatformBlockState sampleSurfaceBlock(double worldX, double worldZ) {
|
||||
RuntimeBinding binding = applications.get(IrisImageMapApplication.SURFACE_BLOCK);
|
||||
if (binding == null || !selectCategorical(binding.maskWeight(worldX, worldZ))) {
|
||||
return null;
|
||||
}
|
||||
String target = binding.compiled.sampleTarget(worldX, worldZ);
|
||||
return target == null ? null : binding.blocks.get(target);
|
||||
}
|
||||
|
||||
public CompiledIrisImageMap getCompiled(String key) {
|
||||
RuntimeBinding binding = bindings.get(key);
|
||||
return binding == null ? null : binding.compiled;
|
||||
}
|
||||
|
||||
public static double blendTerrainHeight(double mappedLocalHeight, double proceduralLocalHeight, double weight) {
|
||||
return proceduralLocalHeight + ((mappedLocalHeight - proceduralLocalHeight) * weight);
|
||||
}
|
||||
|
||||
public static boolean selectCategorical(double weight) {
|
||||
return weight >= 0.5D;
|
||||
}
|
||||
|
||||
private static RuntimeBinding compileBinding(
|
||||
IrisImageMapBinding binding,
|
||||
IrisData data,
|
||||
IrisDimension dimension,
|
||||
Set<IrisRegion> mappedRegions,
|
||||
Set<IrisBiome> mappedBiomes
|
||||
) {
|
||||
if (binding == null) {
|
||||
throw validation("Dimension imageMaps contains a null binding");
|
||||
}
|
||||
String key = requireText(binding.getKey(), "Dimension image-map key");
|
||||
String mapKey = requireText(binding.getMap(), "Dimension image-map resource for '" + key + "'");
|
||||
IrisImageMapApplication application = binding.getApplication();
|
||||
if (application == null) {
|
||||
throw validation("Dimension image-map '" + key + "' requires an application");
|
||||
}
|
||||
IrisImageMap definition = data.getImageMapLoader().load(mapKey);
|
||||
if (definition == null) {
|
||||
throw validation("Dimension image-map '" + key + "' references missing image-map resource '" + mapKey + "'");
|
||||
}
|
||||
String source = requireText(definition.getSource(), "Image-map resource '" + mapKey + "' source");
|
||||
IrisImage image = data.getImageLoader().load(source);
|
||||
if (image == null) {
|
||||
throw validation("Image-map resource '" + mapKey + "' references missing or invalid PNG '" + source + "'");
|
||||
}
|
||||
CompiledIrisImageMap compiled;
|
||||
try {
|
||||
validateApplication(key, application, definition.getType(), binding.getMasks());
|
||||
compiled = CompiledIrisImageMap.compile(definition, image);
|
||||
} finally {
|
||||
data.getImageLoader().unload(source);
|
||||
}
|
||||
Map<String, IrisRegion> regions = new LinkedHashMap<>();
|
||||
Map<String, IrisBiome> biomes = new LinkedHashMap<>();
|
||||
Map<String, PlatformBlockState> blocks = new LinkedHashMap<>();
|
||||
Set<String> targets = legendTargets(definition);
|
||||
|
||||
if (application == IrisImageMapApplication.REGION) {
|
||||
for (String target : targets) {
|
||||
IrisRegion region = data.getRegionLoader().load(localResourceKey(target));
|
||||
if (region == null) {
|
||||
throw validation("Image-map '" + key + "' references missing region target '" + target + "'");
|
||||
}
|
||||
regions.put(target, region);
|
||||
mappedRegions.add(region);
|
||||
}
|
||||
} else if (application == IrisImageMapApplication.BIOME) {
|
||||
for (String target : targets) {
|
||||
String localTarget = localResourceKey(target);
|
||||
IrisBiome biome = data.getBiomeLoader().load(localTarget);
|
||||
if (biome == null) {
|
||||
throw validation("Image-map '" + key + "' references missing biome target '" + target + "'");
|
||||
}
|
||||
IrisBiome typedBiome = biome.withInferredType(resolveBiomeType(localTarget, dimension, data));
|
||||
biomes.put(target, typedBiome);
|
||||
mappedBiomes.add(typedBiome);
|
||||
}
|
||||
} else if (application == IrisImageMapApplication.SURFACE_BLOCK) {
|
||||
for (String target : targets) {
|
||||
PlatformBlockState block = B.getStateOrNull(target, false);
|
||||
if (block == null) {
|
||||
throw validation("Image-map '" + key + "' references unknown surface block target '" + target + "'");
|
||||
}
|
||||
blocks.put(target, block);
|
||||
}
|
||||
}
|
||||
|
||||
return new RuntimeBinding(
|
||||
key,
|
||||
application,
|
||||
compiled,
|
||||
Map.copyOf(regions),
|
||||
Map.copyOf(biomes),
|
||||
Map.copyOf(blocks),
|
||||
binding.getMasks() == null ? List.of() : List.copyOf(binding.getMasks())
|
||||
);
|
||||
}
|
||||
|
||||
private static void validateApplication(
|
||||
String key,
|
||||
IrisImageMapApplication application,
|
||||
IrisImageMapType type,
|
||||
List<IrisImageMapMask> masks
|
||||
) {
|
||||
if (type == null) {
|
||||
throw validation("Image-map '" + key + "' requires a type");
|
||||
}
|
||||
boolean valid = switch (application) {
|
||||
case TERRAIN_HEIGHT -> type == IrisImageMapType.GRAYSCALE_HEIGHT
|
||||
|| type == IrisImageMapType.RGB_HEIGHT;
|
||||
case BIOME, REGION, SURFACE_BLOCK -> type == IrisImageMapType.COLOR_MAP;
|
||||
case MASK -> isMaskType(type);
|
||||
case CUSTOM -> true;
|
||||
};
|
||||
if (!valid) {
|
||||
throw validation("Image-map '" + key + "' type " + type + " is incompatible with " + application);
|
||||
}
|
||||
if (application == IrisImageMapApplication.MASK && masks != null && !masks.isEmpty()) {
|
||||
throw validation("MASK image-map '" + key + "' cannot reference additional masks");
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler resolveMasks(
|
||||
RuntimeBinding binding,
|
||||
Map<String, RuntimeBinding> bindings
|
||||
) {
|
||||
if (binding.maskDefinitions.isEmpty()) {
|
||||
return IrisImageMapMaskSampler.empty();
|
||||
}
|
||||
List<IrisImageMapMaskSampler.Layer> masks = new ArrayList<>(binding.maskDefinitions.size());
|
||||
for (IrisImageMapMask mask : binding.maskDefinitions) {
|
||||
if (mask == null) {
|
||||
throw validation("Image-map '" + binding.key + "' contains a null mask reference");
|
||||
}
|
||||
String maskKey = requireText(mask.getMap(), "Mask reference on image-map '" + binding.key + "'");
|
||||
RuntimeBinding maskBinding = bindings.get(maskKey);
|
||||
if (maskBinding == null) {
|
||||
throw validation("Image-map '" + binding.key + "' references missing MASK binding '" + maskKey + "'");
|
||||
}
|
||||
if (maskBinding.application != IrisImageMapApplication.MASK) {
|
||||
throw validation("Image-map '" + binding.key + "' references '" + maskKey + "', which is not a MASK binding");
|
||||
}
|
||||
if (mask.getOperation() == null) {
|
||||
throw validation("Mask '" + maskKey + "' on image-map '" + binding.key + "' requires an operation");
|
||||
}
|
||||
if (!range(mask.getThreshold()) || !range(mask.getFalloff())) {
|
||||
throw validation("Mask '" + maskKey + "' threshold and falloff must be finite values within 0..1");
|
||||
}
|
||||
masks.add(IrisImageMapMaskSampler.layer(maskBinding.compiled, mask));
|
||||
}
|
||||
return new IrisImageMapMaskSampler(masks);
|
||||
}
|
||||
|
||||
private static Set<String> legendTargets(IrisImageMap definition) {
|
||||
Set<String> targets = new LinkedHashSet<>();
|
||||
if (definition.getColors() != null) {
|
||||
for (String target : definition.getColors().values()) {
|
||||
if (target != null && !target.isBlank()) {
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
String fallback = definition.getFallbackTarget();
|
||||
if (fallback != null && !fallback.isBlank()) {
|
||||
targets.add(fallback);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static InferredType resolveBiomeType(String biomeKey, IrisDimension dimension, IrisData data) {
|
||||
Set<InferredType> types = new LinkedHashSet<>();
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
if (region.getLandBiomes().contains(biomeKey)) {
|
||||
types.add(InferredType.LAND);
|
||||
}
|
||||
if (region.getSeaBiomes().contains(biomeKey)) {
|
||||
types.add(InferredType.SEA);
|
||||
}
|
||||
if (region.getShoreBiomes().contains(biomeKey)) {
|
||||
types.add(InferredType.SHORE);
|
||||
}
|
||||
}
|
||||
if (types.size() != 1) {
|
||||
throw validation("Image-mapped biome '" + biomeKey
|
||||
+ "' must occur in exactly one landBiomes, seaBiomes, or shoreBiomes role across the dimension; found "
|
||||
+ types);
|
||||
}
|
||||
return types.iterator().next();
|
||||
}
|
||||
|
||||
private static String localResourceKey(String target) {
|
||||
String normalized = requireText(target, "Image-map Iris resource target");
|
||||
int separator = normalized.indexOf(':');
|
||||
if (separator < 0) {
|
||||
return normalized;
|
||||
}
|
||||
if (!normalized.startsWith("iris:") || separator == normalized.length() - 1) {
|
||||
throw validation("Iris biome and region targets must be bare pack keys or use the iris: namespace, got '"
|
||||
+ target + "'");
|
||||
}
|
||||
return normalized.substring(separator + 1);
|
||||
}
|
||||
|
||||
private static String requireText(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw validation(name + " must not be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static boolean range(double value) {
|
||||
return Double.isFinite(value) && value >= 0D && value <= 1D;
|
||||
}
|
||||
|
||||
private static boolean isMaskType(IrisImageMapType type) {
|
||||
return type == IrisImageMapType.BINARY_MASK
|
||||
|| type == IrisImageMapType.GRAYSCALE_MASK
|
||||
|| type == IrisImageMapType.ALPHA_MASK;
|
||||
}
|
||||
|
||||
private static IrisImageMapValidationException validation(String message) {
|
||||
return new IrisImageMapValidationException(message);
|
||||
}
|
||||
|
||||
private static final class RuntimeBinding {
|
||||
private final String key;
|
||||
private final IrisImageMapApplication application;
|
||||
private final CompiledIrisImageMap compiled;
|
||||
private final Map<String, IrisRegion> regions;
|
||||
private final Map<String, IrisBiome> biomes;
|
||||
private final Map<String, PlatformBlockState> blocks;
|
||||
private final List<IrisImageMapMask> maskDefinitions;
|
||||
private IrisImageMapMaskSampler masks = IrisImageMapMaskSampler.empty();
|
||||
|
||||
private RuntimeBinding(
|
||||
String key,
|
||||
IrisImageMapApplication application,
|
||||
CompiledIrisImageMap compiled,
|
||||
Map<String, IrisRegion> regions,
|
||||
Map<String, IrisBiome> biomes,
|
||||
Map<String, PlatformBlockState> blocks,
|
||||
List<IrisImageMapMask> maskDefinitions
|
||||
) {
|
||||
this.key = key;
|
||||
this.application = application;
|
||||
this.compiled = compiled;
|
||||
this.regions = regions;
|
||||
this.biomes = biomes;
|
||||
this.blocks = blocks;
|
||||
this.maskDefinitions = maskDefinitions;
|
||||
}
|
||||
|
||||
private double maskWeight(double worldX, double worldZ) {
|
||||
return masks.sample(worldX, worldZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImageColorMode;
|
||||
|
||||
public record IrisImageMapSourceMetadata(
|
||||
String format,
|
||||
IrisImageColorMode colorMode,
|
||||
int width,
|
||||
int height,
|
||||
int colorComponents,
|
||||
int channels,
|
||||
int bitDepth,
|
||||
boolean alpha,
|
||||
double minimumAlpha,
|
||||
double maximumAlpha,
|
||||
String decodedContentHash
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class IrisImageMapValidationException extends IllegalArgumentException {
|
||||
private final List<String> diagnostics;
|
||||
|
||||
public IrisImageMapValidationException(String diagnostic) {
|
||||
this(List.of(diagnostic));
|
||||
}
|
||||
|
||||
public IrisImageMapValidationException(List<String> diagnostics) {
|
||||
super(message(diagnostics));
|
||||
if (diagnostics == null || diagnostics.isEmpty()) {
|
||||
throw new IllegalArgumentException("Image-map validation requires at least one diagnostic");
|
||||
}
|
||||
this.diagnostics = List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
public IrisImageMapValidationException(String diagnostic, Throwable cause) {
|
||||
super(diagnostic, cause);
|
||||
this.diagnostics = List.of(diagnostic);
|
||||
}
|
||||
|
||||
public List<String> getDiagnostics() {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private static String message(List<String> diagnostics) {
|
||||
if (diagnostics == null || diagnostics.isEmpty()) {
|
||||
return "Image-map validation failed";
|
||||
}
|
||||
return String.join("; ", diagnostics);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -188,6 +189,11 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private IrisRange dimensionHeight = new IrisRange(-64, 320);
|
||||
@Desc("Define options for this dimension")
|
||||
private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions();
|
||||
@Desc("Configure and enforce the native Minecraft world border. When omitted, Iris leaves the native border unchanged.")
|
||||
private IrisWorldBoundary worldBoundary = null;
|
||||
@ArrayType(type = IrisImageMapBinding.class)
|
||||
@Desc("Bind reusable image-map resources to typed world-generation inputs")
|
||||
private KList<IrisImageMapBinding> imageMaps = new KList<>();
|
||||
@Desc("When true, sets the dimension's ambient light to maximum, making all blocks fully lit regardless of light level.")
|
||||
private boolean fullbright = false;
|
||||
@RegistryListResource(IrisDimension.class)
|
||||
@@ -441,21 +447,39 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public KList<IrisRegion> getAllRegions(DataProvider g) {
|
||||
KList<IrisRegion> r = new KList<>();
|
||||
KMap<String, IrisRegion> regions = new KMap<>();
|
||||
|
||||
if (g == null) {
|
||||
return r;
|
||||
return regions.v();
|
||||
}
|
||||
IrisData data = g.getData();
|
||||
if (data == null || data.getRegionLoader() == null) {
|
||||
return r;
|
||||
return regions.v();
|
||||
}
|
||||
|
||||
for (String i : getRegions()) {
|
||||
r.add(data.getRegionLoader().load(i));
|
||||
for (String key : getRegions()) {
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
if (region != null) {
|
||||
regions.put(key, region);
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
for (IrisImageMapBinding binding : getImageMaps()) {
|
||||
if (binding == null || binding.getApplication() != IrisImageMapApplication.REGION) {
|
||||
continue;
|
||||
}
|
||||
IrisImageMap map = data.getImageMapLoader().load(binding.getMap());
|
||||
if (map == null || map.getColors() == null) {
|
||||
continue;
|
||||
}
|
||||
for (String target : imageMapTargets(map)) {
|
||||
String key = localImageMapTarget(target);
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
if (region != null) {
|
||||
regions.put(key, region);
|
||||
}
|
||||
}
|
||||
}
|
||||
return regions.v();
|
||||
}
|
||||
|
||||
public KList<IrisRegion> getAllAnyRegions() {
|
||||
@@ -496,15 +520,23 @@ public class IrisDimension extends IrisRegistrant {
|
||||
if (riversEnabled && riverNetwork.getBiomes() != null) {
|
||||
addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds());
|
||||
}
|
||||
KList<String> regionKeys = getRegions();
|
||||
if (regionKeys != null) {
|
||||
for (String regionKey : regionKeys) {
|
||||
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
addReachableBiomeKeys(pending,
|
||||
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
|
||||
for (IrisRegion region : getAllRegions(g)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
addReachableBiomeKeys(pending,
|
||||
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
|
||||
}
|
||||
for (IrisImageMapBinding binding : getImageMaps()) {
|
||||
if (binding == null || binding.getApplication() != IrisImageMapApplication.BIOME) {
|
||||
continue;
|
||||
}
|
||||
IrisImageMap map = data.getImageMapLoader().load(binding.getMap());
|
||||
if (map == null) {
|
||||
continue;
|
||||
}
|
||||
for (String target : imageMapTargets(map)) {
|
||||
addReachableBiomeKey(pending, localImageMapTarget(target));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +593,25 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return biomes.v();
|
||||
}
|
||||
|
||||
private Set<String> imageMapTargets(IrisImageMap map) {
|
||||
Set<String> targets = new LinkedHashSet<>();
|
||||
if (map.getColors() != null) {
|
||||
for (String target : map.getColors().values()) {
|
||||
if (target != null && !target.isBlank()) {
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (map.getFallbackTarget() != null && !map.getFallbackTarget().isBlank()) {
|
||||
targets.add(map.getFallbackTarget());
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private String localImageMapTarget(String target) {
|
||||
return target.startsWith("iris:") ? target.substring("iris:".length()) : target;
|
||||
}
|
||||
|
||||
private void addReachableBiomeKeys(Deque<String> pending, Iterable<String> biomeKeys) {
|
||||
if (biomeKeys == null) {
|
||||
return;
|
||||
|
||||
@@ -71,7 +71,8 @@ public class IrisGeneratorStyle {
|
||||
@RegistryListResource(IrisExpression.class)
|
||||
private String expression = null;
|
||||
@Desc("Use an Image map instead of a generated value")
|
||||
private IrisImageMap imageMap = null;
|
||||
@RegistryListResource(IrisImageMap.class)
|
||||
private String imageMap = null;
|
||||
@MinNumber(0.00001)
|
||||
@Desc("The Output multiplier. Only used if parent is fracture.")
|
||||
private double multiplier = 1;
|
||||
@@ -109,13 +110,7 @@ public class IrisGeneratorStyle {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Objects.hash(imageMap.getImage(),
|
||||
imageMap.getCoordinateScale(),
|
||||
imageMap.getInterpolationMethod(),
|
||||
imageMap.getChannel(),
|
||||
imageMap.isInverted(),
|
||||
imageMap.isTiled(),
|
||||
imageMap.isCentered());
|
||||
return imageMap.hashCode();
|
||||
}
|
||||
|
||||
private int hash() {
|
||||
@@ -178,8 +173,9 @@ public class IrisGeneratorStyle {
|
||||
e.getLoadFile() == null ? 0L : e.getLoadFile().lastModified()));
|
||||
}
|
||||
} else if (getImageMap() != null) {
|
||||
cng = new CNG(rng, new ImageNoise(data, getImageMap()), 1D, 1).bake();
|
||||
sourceStamp = Integer.toUnsignedLong(imageMapHash());
|
||||
ImageNoise imageNoise = new ImageNoise(data, getImageMap());
|
||||
cng = new CNG(rng, imageNoise, 1D, 1).bake();
|
||||
sourceStamp = Integer.toUnsignedLong(imageNoise.getContentHash().hashCode());
|
||||
}
|
||||
|
||||
if (cng == null) {
|
||||
|
||||
@@ -20,19 +20,31 @@ package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.awt.image.Raster;
|
||||
import java.util.Objects;
|
||||
|
||||
public class IrisImage extends IrisRegistrant {
|
||||
public final class IrisImage extends IrisRegistrant {
|
||||
private final BufferedImage image;
|
||||
private final Raster raster;
|
||||
private final ColorModel colorModel;
|
||||
private final String format;
|
||||
|
||||
public IrisImage() {
|
||||
this(new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB));
|
||||
this(new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB), "png");
|
||||
}
|
||||
|
||||
public IrisImage(BufferedImage image) {
|
||||
this(image, "png");
|
||||
}
|
||||
|
||||
public IrisImage(BufferedImage image, String format) {
|
||||
this.image = Objects.requireNonNull(image, "IrisImage requires a decoded image (the source file was unreadable or not an image)");
|
||||
this.raster = image.getRaster();
|
||||
this.colorModel = image.getColorModel();
|
||||
this.format = Objects.requireNonNull(format, "Image format").toLowerCase();
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
@@ -43,63 +55,104 @@ public class IrisImage extends IrisRegistrant {
|
||||
return image.getHeight();
|
||||
}
|
||||
|
||||
public int getRawValue(int x, int z) {
|
||||
if (x >= getWidth() || z >= getHeight() || x < 0 || z < 0) {
|
||||
return 0;
|
||||
}
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public IrisImageColorMode getColorMode() {
|
||||
if (colorModel instanceof IndexColorModel) {
|
||||
return IrisImageColorMode.INDEXED;
|
||||
}
|
||||
int colorComponents = colorModel.getNumColorComponents();
|
||||
if (colorComponents == 1) {
|
||||
return colorModel.hasAlpha() ? IrisImageColorMode.UNSUPPORTED : IrisImageColorMode.GRAYSCALE;
|
||||
}
|
||||
if (colorComponents == 3) {
|
||||
return colorModel.hasAlpha() ? IrisImageColorMode.RGBA : IrisImageColorMode.RGB;
|
||||
}
|
||||
return IrisImageColorMode.UNSUPPORTED;
|
||||
}
|
||||
|
||||
public int getColorComponentCount() {
|
||||
return colorModel.getNumColorComponents();
|
||||
}
|
||||
|
||||
public int getChannelCount() {
|
||||
return raster.getNumBands();
|
||||
}
|
||||
|
||||
public int getBitDepth() {
|
||||
int[] sampleSizes = raster.getSampleModel().getSampleSize();
|
||||
int maximum = 0;
|
||||
int colorBands = Math.min(getColorComponentCount(), sampleSizes.length);
|
||||
for (int index = 0; index < colorBands; index++) {
|
||||
maximum = Math.max(maximum, sampleSizes[index]);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public boolean hasAlpha() {
|
||||
return colorModel.hasAlpha();
|
||||
}
|
||||
|
||||
public double getBandNormalized(int x, int z, int band) {
|
||||
requireCoordinate(x, z);
|
||||
if (band < 0 || band >= raster.getNumBands()) {
|
||||
throw new IllegalArgumentException("Image band " + band + " is outside 0.." + (raster.getNumBands() - 1));
|
||||
}
|
||||
int bits = raster.getSampleModel().getSampleSize(band);
|
||||
long maximum = (1L << bits) - 1L;
|
||||
return raster.getSample(x, z, band) / (double) maximum;
|
||||
}
|
||||
|
||||
public int getBandSample(int x, int z, int band) {
|
||||
requireCoordinate(x, z);
|
||||
if (band < 0 || band >= raster.getNumBands()) {
|
||||
throw new IllegalArgumentException("Image band " + band + " is outside 0.." + (raster.getNumBands() - 1));
|
||||
}
|
||||
return raster.getSample(x, z, band);
|
||||
}
|
||||
|
||||
public double getAlphaNormalized(int x, int z) {
|
||||
requireCoordinate(x, z);
|
||||
if (!hasAlpha()) {
|
||||
return 1D;
|
||||
}
|
||||
int alphaBand = raster.getNumBands() - 1;
|
||||
return getBandNormalized(x, z, alphaBand);
|
||||
}
|
||||
|
||||
public int getRawRgb8(int x, int z) {
|
||||
requireCoordinate(x, z);
|
||||
IrisImageColorMode mode = getColorMode();
|
||||
if (mode != IrisImageColorMode.RGB && mode != IrisImageColorMode.RGBA) {
|
||||
throw new IllegalStateException("Raw RGB sampling requires an RGB or RGBA image, not " + mode);
|
||||
}
|
||||
int red = scaleTo8Bit(x, z, 0);
|
||||
int green = scaleTo8Bit(x, z, 1);
|
||||
int blue = scaleTo8Bit(x, z, 2);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
public int getPreviewArgb(int x, int z) {
|
||||
requireCoordinate(x, z);
|
||||
return image.getRGB(x, z);
|
||||
}
|
||||
|
||||
public double getValue(IrisImageChannel channel, int x, int z) {
|
||||
int color = getRawValue(x, z);
|
||||
|
||||
switch (channel) {
|
||||
case RED -> {
|
||||
return ((color >> 16) & 0xFF) / 255D;
|
||||
}
|
||||
case GREEN -> {
|
||||
return ((color >> 8) & 0xFF) / 255D;
|
||||
}
|
||||
case BLUE -> {
|
||||
return ((color) & 0xFF) / 255D;
|
||||
}
|
||||
case SATURATION -> {
|
||||
return Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null)[1];
|
||||
}
|
||||
case HUE -> {
|
||||
return Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null)[0];
|
||||
}
|
||||
case BRIGHTNESS -> {
|
||||
return Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null)[2];
|
||||
}
|
||||
case COMPOSITE_ADD_RGB -> {
|
||||
return ((((color >> 16) & 0xFF) / 255D) + (((color >> 8) & 0xFF) / 255D) + (((color) & 0xFF) / 255D)) / 3D;
|
||||
}
|
||||
case COMPOSITE_MUL_RGB -> {
|
||||
return (((color >> 16) & 0xFF) / 255D) * (((color >> 8) & 0xFF) / 255D) * (((color) & 0xFF) / 255D);
|
||||
}
|
||||
case COMPOSITE_MAX_RGB -> {
|
||||
return Math.max(Math.max((((color >> 16) & 0xFF) / 255D), (((color >> 8) & 0xFF) / 255D)), (((color) & 0xFF) / 255D));
|
||||
}
|
||||
case COMPOSITE_ADD_HSB -> {
|
||||
float[] hsb = Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null);
|
||||
return (hsb[0] + hsb[1] + hsb[2]) / 3D;
|
||||
}
|
||||
case COMPOSITE_MUL_HSB -> {
|
||||
float[] hsb = Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null);
|
||||
return hsb[0] * hsb[1] * hsb[2];
|
||||
}
|
||||
case COMPOSITE_MAX_HSB -> {
|
||||
float[] hsb = Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, (color) & 0xFF, null);
|
||||
return Math.max(hsb[0], Math.max(hsb[1], hsb[2]));
|
||||
}
|
||||
case RAW -> {
|
||||
return color;
|
||||
}
|
||||
private int scaleTo8Bit(int x, int z, int band) {
|
||||
int bits = raster.getSampleModel().getSampleSize(band);
|
||||
int sample = raster.getSample(x, z, band);
|
||||
if (bits == 8) {
|
||||
return sample;
|
||||
}
|
||||
long maximum = (1L << bits) - 1L;
|
||||
return (int) Math.round(sample * 255D / maximum);
|
||||
}
|
||||
|
||||
return color;
|
||||
private void requireCoordinate(int x, int z) {
|
||||
if (x < 0 || z < 0 || x >= getWidth() || z >= getHeight()) {
|
||||
throw new IndexOutOfBoundsException("Image coordinate " + x + "," + z + " is outside " + getWidth() + "x" + getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Determines a derived channel of an image to read")
|
||||
public enum IrisImageChannel {
|
||||
@Desc("The red channel of the image")
|
||||
RED,
|
||||
@Desc("Thge green channel of the image")
|
||||
GREEN,
|
||||
@Desc("The blue channel of the image")
|
||||
BLUE,
|
||||
@Desc("The saturation as a channel of the image")
|
||||
SATURATION,
|
||||
@Desc("The hue as a channel of the image")
|
||||
HUE,
|
||||
@Desc("The brightness as a channel of the image")
|
||||
BRIGHTNESS,
|
||||
@Desc("The composite of RGB as a channel of the image. Takes the average channel value (adding)")
|
||||
COMPOSITE_ADD_RGB,
|
||||
@Desc("The composite of RGB as a channel of the image. Multiplies the channels")
|
||||
COMPOSITE_MUL_RGB,
|
||||
@Desc("The composite of RGB as a channel of the image. Picks the highest channel")
|
||||
COMPOSITE_MAX_RGB,
|
||||
@Desc("The composite of HSB as a channel of the image Takes the average channel value (adding)")
|
||||
COMPOSITE_ADD_HSB,
|
||||
@Desc("The composite of HSB as a channel of the image Multiplies the channels")
|
||||
COMPOSITE_MUL_HSB,
|
||||
@Desc("The composite of HSB as a channel of the image Picks the highest channel")
|
||||
COMPOSITE_MAX_HSB,
|
||||
@Desc("The raw value as a channel (probably doesnt look very good)")
|
||||
RAW
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
public enum IrisImageColorMode {
|
||||
GRAYSCALE,
|
||||
RGB,
|
||||
RGBA,
|
||||
INDEXED,
|
||||
UNSUPPORTED
|
||||
}
|
||||
@@ -1,105 +1,120 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
|
||||
import lombok.AllArgsConstructor;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Snippet("image-map")
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("Represents an image map")
|
||||
@Desc("A reusable typed image-map definition")
|
||||
@Data
|
||||
public class IrisImageMap {
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisImageMap extends IrisRegistrant {
|
||||
public static final double MINIMUM_SCALE = 0.000001D;
|
||||
public static final double MAXIMUM_COLOR_TOLERANCE = 441.672956D;
|
||||
|
||||
@RegistryListResource(IrisImage.class)
|
||||
@Desc("Define the png image to read in this noise map")
|
||||
private String image = "";
|
||||
@Desc("PNG source under images/, without the file extension")
|
||||
private String source = "";
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The amount of distance a single pixel is when reading this map, reading x=13, would still read pixel 0 if the scale is 32. You can zoom this externally through noise styles for zooming out.")
|
||||
private double coordinateScale = 32;
|
||||
@Desc("How source pixels are decoded")
|
||||
private IrisImageMapType type = IrisImageMapType.GRAYSCALE_HEIGHT;
|
||||
|
||||
@Desc("The interpolation method if the coordinateScale is greater than 1. This blends the image into noise. For nearest neighbor, use NONE.")
|
||||
private InterpolationMethod interpolationMethod = InterpolationMethod.BILINEAR_STARCAST_6;
|
||||
@MinNumber(MINIMUM_SCALE)
|
||||
@Desc("Minecraft blocks represented by one source pixel")
|
||||
private double blocksPerPixel = 1D;
|
||||
|
||||
@Desc("The channel of the image to read from. This basically converts image data into a number betwen 0 to 1 per pixel using a certain 'channel/filter'")
|
||||
private IrisImageChannel channel = IrisImageChannel.COMPOSITE_ADD_HSB;
|
||||
@Desc("Minecraft X/Z coordinate that maps to sourceOrigin")
|
||||
private IrisImageMapOrigin origin = new IrisImageMapOrigin();
|
||||
|
||||
@Desc("Invert the channel input")
|
||||
@Desc("Source pixel X/Y coordinate placed at origin")
|
||||
private IrisImageMapOrigin sourceOrigin = new IrisImageMapOrigin();
|
||||
|
||||
@Desc("Clockwise quarter-turn rotation around sourceOrigin")
|
||||
private IrisImageMapRotation rotation = IrisImageMapRotation.DEG_0;
|
||||
|
||||
@Desc("Mirror image X around sourceOrigin before rotation")
|
||||
private boolean mirrorX = false;
|
||||
|
||||
@Desc("Mirror image Y around sourceOrigin before rotation")
|
||||
private boolean mirrorZ = false;
|
||||
|
||||
@Desc("Numeric sampling filter; exact color and binary maps require NEAREST")
|
||||
private IrisImageMapSampling sampling = IrisImageMapSampling.NEAREST;
|
||||
|
||||
@Desc("Behavior outside the source rectangle")
|
||||
private IrisImageMapOutOfBounds outOfBounds = IrisImageMapOutOfBounds.FALLBACK;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Normalized scalar used by FALLBACK coordinates")
|
||||
private double fallbackValue = 0D;
|
||||
|
||||
@Desc("Legend target used by FALLBACK or unknown color-map pixels")
|
||||
private String fallbackTarget = "";
|
||||
|
||||
@Desc("How alpha affects decoded data")
|
||||
private IrisImageMapAlpha alpha = IrisImageMapAlpha.IGNORE;
|
||||
|
||||
@Desc("Minimum absolute world Y for height maps")
|
||||
private double minimumHeight = -64D;
|
||||
|
||||
@Desc("Maximum absolute world Y for height maps")
|
||||
private double maximumHeight = 320D;
|
||||
|
||||
@Desc("Vertical block offset applied after height decoding")
|
||||
private double verticalOffset = 0D;
|
||||
|
||||
@Desc("Clamp decoded height to minimumHeight and maximumHeight after offset")
|
||||
private boolean clamp = true;
|
||||
|
||||
@Desc("Invert decoded scalar values before curve evaluation")
|
||||
private boolean inverted = false;
|
||||
|
||||
@Desc("Tile the image coordinates")
|
||||
private boolean tiled = false;
|
||||
@MinNumber(MINIMUM_SCALE)
|
||||
@Desc("Power curve applied after optional inversion; 1 is linear")
|
||||
private double curveExponent = 1D;
|
||||
|
||||
@Desc("Center 0,0 to the center of the image instead of the top left.")
|
||||
private boolean centered = true;
|
||||
@MinNumber(0)
|
||||
@MaxNumber(32)
|
||||
@Desc("Load-time box smoothing radius in source pixels")
|
||||
private int smoothingRadius = 0;
|
||||
|
||||
private transient AtomicCache<IrisImage> imageCache = new AtomicCache<IrisImage>();
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Binary mask threshold")
|
||||
private double threshold = 0.5D;
|
||||
|
||||
public double getNoise(IrisData data, int x, int z) {
|
||||
IrisImage i = imageCache.aquire(() -> data.getImageLoader().load(image));
|
||||
if (i == null) {
|
||||
// Reached per sample for as long as the pack points at an image that will not load, so the
|
||||
// pack key is what the operator needs and one statement of it is enough.
|
||||
IrisLogging.warnOnce("image-map:" + image, "No image %s in the pack; that noise samples as zero.", image);
|
||||
return 0;
|
||||
}
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Mask transition width above threshold")
|
||||
private double falloff = 0D;
|
||||
|
||||
return IrisInterpolation.getNoise(interpolationMethod, x, z, coordinateScale, (xx, zz) -> rawNoise(i, xx, zz));
|
||||
@MinNumber(0)
|
||||
@MaxNumber(MAXIMUM_COLOR_TOLERANCE)
|
||||
@Desc("Euclidean raw sRGB distance accepted by tolerant color matching; zero is exact")
|
||||
private double colorTolerance = 0D;
|
||||
|
||||
@Desc("How colors absent from the legend are handled")
|
||||
private IrisImageMapUnknownColor unknownColor = IrisImageMapUnknownColor.ERROR;
|
||||
|
||||
@Desc("Exact #RRGGBB colors mapped to Iris resource or Minecraft block keys")
|
||||
private KMap<String, String> colors = new KMap<>();
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "image-maps";
|
||||
}
|
||||
|
||||
private double rawNoise(IrisImage i, double x, double z) {
|
||||
x /= coordinateScale;
|
||||
z /= coordinateScale;
|
||||
|
||||
// X and Z are now scaled to the image
|
||||
|
||||
// Add half the image width & height if centered
|
||||
if (isCentered()) {
|
||||
x += i.getWidth() / 2D;
|
||||
z += i.getHeight() / 2D;
|
||||
}
|
||||
|
||||
// If tiled modulo over width and height
|
||||
if (isTiled()) {
|
||||
x = x % i.getWidth();
|
||||
x = x < 0 ? x + i.getWidth() : x; // Fix java's negative modulo shit
|
||||
z = z % i.getHeight();
|
||||
z = z < 0 ? z + i.getHeight() : z; // Fix java's negative modulo shit
|
||||
}
|
||||
|
||||
// Retrieve value from image
|
||||
double v = i.getValue(getChannel(), (int) x, (int) z);
|
||||
|
||||
// Return value, or 1 - value if inverted (value is in double set [0, 1] so this will return [0, 1])
|
||||
return isInverted() ? 1D - v : v;
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Image Map";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How alpha participates in decoded data")
|
||||
public enum IrisImageMapAlpha {
|
||||
IGNORE,
|
||||
MASK,
|
||||
TRANSPARENT_IS_FALLBACK,
|
||||
ERROR
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The generation input controlled by an image map")
|
||||
public enum IrisImageMapApplication {
|
||||
TERRAIN_HEIGHT,
|
||||
BIOME,
|
||||
REGION,
|
||||
SURFACE_BLOCK,
|
||||
MASK,
|
||||
CUSTOM
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Binds a reusable image map to one dimension generation role")
|
||||
@Data
|
||||
public class IrisImageMapBinding {
|
||||
@Desc("Unique name used by Studio previews and custom map lookups")
|
||||
private String key = "";
|
||||
|
||||
@RegistryListResource(IrisImageMap.class)
|
||||
@Desc("Image-map resource key under image-maps/")
|
||||
private String map = "";
|
||||
|
||||
@Desc("Generation input controlled by this binding")
|
||||
private IrisImageMapApplication application = IrisImageMapApplication.CUSTOM;
|
||||
|
||||
@ArrayType(type = IrisImageMapMask.class)
|
||||
@Desc("Named mask maps composed in declaration order")
|
||||
private KList<IrisImageMapMask> masks = new KList<>();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("A composable reference to a named MASK image map")
|
||||
@Data
|
||||
public class IrisImageMapMask {
|
||||
@Desc("The key of a named imageMaps entry whose application is MASK")
|
||||
private String map = "";
|
||||
|
||||
@Desc("How this mask combines with masks before it")
|
||||
private IrisImageMapMaskOperation operation = IrisImageMapMaskOperation.MULTIPLY;
|
||||
|
||||
@Desc("Invert this mask before combining it")
|
||||
private boolean inverted = false;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Values below this threshold become zero")
|
||||
private double threshold = 0D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Soft transition width above threshold; zero is a hard edge")
|
||||
private double falloff = 0D;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How a referenced mask combines with masks before it")
|
||||
public enum IrisImageMapMaskOperation {
|
||||
MULTIPLY,
|
||||
MINIMUM,
|
||||
MAXIMUM,
|
||||
ADD,
|
||||
SUBTRACT
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("A two-dimensional image-map coordinate")
|
||||
@Data
|
||||
public class IrisImageMapOrigin {
|
||||
private double x = 0D;
|
||||
private double z = 0D;
|
||||
|
||||
public IrisImageMapOrigin(double x, double z) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How coordinates outside an image are resolved")
|
||||
public enum IrisImageMapOutOfBounds {
|
||||
FALLBACK,
|
||||
CLAMP,
|
||||
REPEAT,
|
||||
MIRROR,
|
||||
ERROR
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Clockwise image-map rotation around sourceOrigin")
|
||||
public enum IrisImageMapRotation {
|
||||
DEG_0,
|
||||
DEG_90,
|
||||
DEG_180,
|
||||
DEG_270
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The filter used between source pixels")
|
||||
public enum IrisImageMapSampling {
|
||||
NEAREST,
|
||||
BILINEAR,
|
||||
BICUBIC
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How Iris decodes each source pixel")
|
||||
public enum IrisImageMapType {
|
||||
GRAYSCALE_HEIGHT,
|
||||
RGB_HEIGHT,
|
||||
COLOR_MAP,
|
||||
BINARY_MASK,
|
||||
GRAYSCALE_MASK,
|
||||
ALPHA_MASK
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How a color absent from a color-map legend is handled")
|
||||
public enum IrisImageMapUnknownColor {
|
||||
ERROR,
|
||||
FALLBACK,
|
||||
IGNORE
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("The native Minecraft world border applied to worlds using this dimension")
|
||||
@Data
|
||||
public class IrisWorldBoundary {
|
||||
public static final double DEFAULT_DAMAGE_AMOUNT = 0.2D;
|
||||
public static final double DEFAULT_DAMAGE_BUFFER = 5D;
|
||||
public static final double MAXIMUM_CENTER = 29_999_984D;
|
||||
public static final double MAXIMUM_SIZE = 59_999_968D;
|
||||
|
||||
@Desc("The center of the boundary")
|
||||
private IrisWorldBoundaryCenter center = new IrisWorldBoundaryCenter();
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(59_999_968)
|
||||
@Desc("The full border diameter in blocks, matching Minecraft world-border terminology")
|
||||
private double size = 16_384D;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(Integer.MAX_VALUE)
|
||||
@Desc("Distance from the border at which the client warning begins")
|
||||
private int warningDistance = 16;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Safe distance outside the border before damage begins")
|
||||
private double damageBuffer = DEFAULT_DAMAGE_BUFFER;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Damage per block beyond damageBuffer")
|
||||
private double damageAmount = DEFAULT_DAMAGE_AMOUNT;
|
||||
|
||||
public static IrisWorldBoundary snapshot(IrisWorldBoundary configured) {
|
||||
IrisWorldBoundary source = Objects.requireNonNull(configured, "Configured worldBoundary");
|
||||
source.validate();
|
||||
return new IrisWorldBoundary()
|
||||
.setCenter(new IrisWorldBoundaryCenter(source.getCenter().getX(), source.getCenter().getZ()))
|
||||
.setSize(source.getSize())
|
||||
.setWarningDistance(source.getWarningDistance())
|
||||
.setDamageBuffer(source.getDamageBuffer())
|
||||
.setDamageAmount(source.getDamageAmount());
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (center == null) {
|
||||
throw new IllegalArgumentException("worldBoundary.center is required");
|
||||
}
|
||||
if (!Double.isFinite(center.getX()) || Math.abs(center.getX()) > MAXIMUM_CENTER
|
||||
|| !Double.isFinite(center.getZ()) || Math.abs(center.getZ()) > MAXIMUM_CENTER) {
|
||||
throw new IllegalArgumentException("worldBoundary.center must be finite and within +/-" + MAXIMUM_CENTER);
|
||||
}
|
||||
if (!Double.isFinite(size) || size < 1D || size > MAXIMUM_SIZE) {
|
||||
throw new IllegalArgumentException("worldBoundary.size must be between 1 and " + MAXIMUM_SIZE);
|
||||
}
|
||||
if (warningDistance < 0) {
|
||||
throw new IllegalArgumentException("worldBoundary.warningDistance cannot be negative");
|
||||
}
|
||||
if (!Double.isFinite(damageBuffer) || damageBuffer < 0D) {
|
||||
throw new IllegalArgumentException("worldBoundary.damageBuffer must be finite and non-negative");
|
||||
}
|
||||
if (!Double.isFinite(damageAmount) || damageAmount < 0D) {
|
||||
throw new IllegalArgumentException("worldBoundary.damageAmount must be finite and non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
public double minimumX() {
|
||||
return center.getX() - size / 2D;
|
||||
}
|
||||
|
||||
public double maximumX() {
|
||||
return center.getX() + size / 2D;
|
||||
}
|
||||
|
||||
public double minimumZ() {
|
||||
return center.getZ() - size / 2D;
|
||||
}
|
||||
|
||||
public double maximumZ() {
|
||||
return center.getZ() + size / 2D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("The world-border center in block coordinates")
|
||||
@Data
|
||||
public class IrisWorldBoundaryCenter {
|
||||
@MinNumber(-29_999_984)
|
||||
@MaxNumber(29_999_984)
|
||||
@Desc("The world-border center X coordinate")
|
||||
private double x = 0D;
|
||||
|
||||
@MinNumber(-29_999_984)
|
||||
@MaxNumber(29_999_984)
|
||||
@Desc("The world-border center Z coordinate")
|
||||
private double z = 0D;
|
||||
|
||||
public IrisWorldBoundaryCenter(double x, double z) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return false;
|
||||
}
|
||||
INMS.get().inject(world.getSeed(), engine, world);
|
||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
||||
if (!studio) {
|
||||
J.s(() -> updateSpawnLocation(world), 1);
|
||||
|
||||
@@ -52,6 +52,7 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
@@ -551,7 +552,7 @@ public final class EngineBukkitOps {
|
||||
|
||||
public static void gotoRegion(Engine engine, IrisRegion r, Player player, boolean teleport) {
|
||||
if (!engine.getDimension().getRegions().contains(r.getLoadKey())) {
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("name", String.valueOf(r.getName()))));
|
||||
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("name", String.valueOf(r.getName()))));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,14 +570,14 @@ public final class EngineBukkitOps {
|
||||
private static void find(Engine engine, Locator<?> locator, Player player, boolean teleport, String message) {
|
||||
find(engine, locator, player, 120_000, location -> {
|
||||
if (location == null) {
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message))));
|
||||
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_COULD_NOT_FIND_WITHIN_SEARCH_RANGE, MessageArgument.untrusted("message", String.valueOf(message))));
|
||||
return;
|
||||
}
|
||||
if (teleport) {
|
||||
J.runEntity(player, () -> teleportAsyncSafely(player, location));
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message))));
|
||||
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_TELEPORTING, MessageArgument.untrusted("message", String.valueOf(message))));
|
||||
} else {
|
||||
player.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_AT, MessageArgument.untrusted("message", String.valueOf(message)), MessageArgument.untrusted("blockX", String.valueOf(location.getBlockX())), MessageArgument.untrusted("blockY", String.valueOf(location.getBlockY())), MessageArgument.untrusted("blockZ", String.valueOf(location.getBlockZ()))));
|
||||
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_AT, MessageArgument.untrusted("message", String.valueOf(message)), MessageArgument.untrusted("blockX", String.valueOf(location.getBlockX())), MessageArgument.untrusted("blockY", String.valueOf(location.getBlockY())), MessageArgument.untrusted("blockZ", String.valueOf(location.getBlockZ()))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import art.arcane.iris.util.common.misc.Bindings;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -65,7 +64,6 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public final class BukkitPlatform implements IrisPlatform {
|
||||
private static volatile Plugin PLUGIN;
|
||||
private static volatile Bindings.Adventure AUDIENCES;
|
||||
private static volatile HudActionBar HUD_BAR;
|
||||
private static volatile HudBossBarLane HUD_LANES;
|
||||
private static volatile Supplier<VolmitSender> CONSOLE;
|
||||
@@ -124,18 +122,6 @@ public final class BukkitPlatform implements IrisPlatform {
|
||||
throw new IllegalStateException("Hosted Iris plugin is not a VolmitPlugin");
|
||||
}
|
||||
|
||||
public static void hostAudiences(Bindings.Adventure adventure) {
|
||||
AUDIENCES = adventure;
|
||||
}
|
||||
|
||||
public static Bindings.Adventure audiences() {
|
||||
Bindings.Adventure adventure = AUDIENCES;
|
||||
if (adventure == null) {
|
||||
throw new IllegalStateException("No Iris adventure audiences are hosted");
|
||||
}
|
||||
return adventure;
|
||||
}
|
||||
|
||||
public static void hostHud(HudActionBar hudBar, HudBossBarLane hudLanes) {
|
||||
HUD_BAR = hudBar;
|
||||
HUD_LANES = hudLanes;
|
||||
|
||||
@@ -14,21 +14,14 @@ import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.json.JSONException;
|
||||
import art.arcane.volmlib.util.reflect.ShadeFix;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
|
||||
import io.sentry.Sentry;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import net.kyori.adventure.text.serializer.ComponentSerializer;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.DrilldownPie;
|
||||
import org.bstats.charts.SingleLineChart;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -123,21 +116,4 @@ public class Bindings {
|
||||
plugin.postShutdown(metrics::shutdown);
|
||||
});
|
||||
}
|
||||
|
||||
public static class Adventure {
|
||||
private final BukkitAudiences audiences;
|
||||
|
||||
public Adventure(Plugin plugin) {
|
||||
ShadeFix.fix(ComponentSerializer.class);
|
||||
this.audiences = BukkitAudiences.create(plugin);
|
||||
}
|
||||
|
||||
public Audience player(Player player) {
|
||||
return audiences.player(player);
|
||||
}
|
||||
|
||||
public Audience sender(CommandSender sender) {
|
||||
return audiences.sender(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
@@ -38,6 +38,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -48,14 +49,16 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public final class WebCache {
|
||||
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10L);
|
||||
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(120L);
|
||||
private static final DownloadPolicy DOWNLOAD_POLICY = new DownloadPolicy(
|
||||
Duration.ofSeconds(10L),
|
||||
3,
|
||||
Duration.ofSeconds(1L)
|
||||
);
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
private static final long PROGRESS_INTERVAL_NANOS = Duration.ofMillis(250L).toNanos();
|
||||
private static final TransferProgressListener NO_TRANSFER_PROGRESS = progress -> {
|
||||
};
|
||||
|
||||
private static volatile HttpClient client;
|
||||
|
||||
private WebCache() {
|
||||
}
|
||||
|
||||
@@ -89,24 +92,31 @@ public final class WebCache {
|
||||
}
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url) {
|
||||
public static File getNonCachedFile(String name, String url) throws IOException {
|
||||
return getNonCachedFile(name, url, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url, long maxBytes) {
|
||||
public static File getNonCachedFile(String name, String url, long maxBytes) throws IOException {
|
||||
return getNonCachedFile(name, url, maxBytes, NO_TRANSFER_PROGRESS);
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url, TransferProgressListener progressListener) {
|
||||
public static File getNonCachedFile(String name, String url,
|
||||
TransferProgressListener progressListener) throws IOException {
|
||||
return getNonCachedFile(name, url, Long.MAX_VALUE, progressListener);
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url, long maxBytes,
|
||||
TransferProgressListener progressListener) {
|
||||
TransferProgressListener progressListener) throws IOException {
|
||||
return getNonCachedFile(name, url, maxBytes, DOWNLOAD_POLICY, progressListener);
|
||||
}
|
||||
|
||||
static File getNonCachedFile(String name, String url, long maxBytes, DownloadPolicy policy,
|
||||
TransferProgressListener progressListener) throws IOException {
|
||||
String h = IO.hash(name + "*" + url);
|
||||
File f = IrisPlatforms.get().dataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
IrisLogging.debug("Download " + name);
|
||||
return download(name, url, f, maxBytes, progressListener) ? f : null;
|
||||
download(name, url, f, maxBytes, policy, progressListener);
|
||||
return f;
|
||||
}
|
||||
|
||||
private static boolean download(String name, String url, File target) {
|
||||
@@ -119,103 +129,79 @@ public final class WebCache {
|
||||
|
||||
private static boolean download(String name, String url, File target, long maxBytes,
|
||||
TransferProgressListener progressListener) {
|
||||
try {
|
||||
download(name, url, target, maxBytes, DOWNLOAD_POLICY, progressListener);
|
||||
return true;
|
||||
} catch (InterruptedIOException exception) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
IrisLogging.debug("Download interrupted for " + name);
|
||||
} else {
|
||||
IrisLogging.reportError(exception);
|
||||
}
|
||||
return false;
|
||||
} catch (IOException exception) {
|
||||
IrisLogging.reportError(exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void download(String name, String url, File target, long maxBytes, DownloadPolicy policy,
|
||||
TransferProgressListener progressListener) throws IOException {
|
||||
if (maxBytes < 1L) {
|
||||
throw new IllegalArgumentException("Download size limit must be positive.");
|
||||
}
|
||||
DownloadPolicy downloadPolicy = Objects.requireNonNull(policy, "policy");
|
||||
TransferProgressListener progress = progressListener == null ? NO_TRANSFER_PROGRESS : progressListener;
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.GET()
|
||||
.build();
|
||||
DownloadFailure lastFailure = null;
|
||||
for (int attempt = 1; attempt <= downloadPolicy.attempts(); attempt++) {
|
||||
checkInterrupted();
|
||||
try {
|
||||
downloadAttempt(name, url, target, maxBytes, downloadPolicy.readTimeout(), progress);
|
||||
return;
|
||||
} catch (DownloadFailure failure) {
|
||||
lastFailure = failure;
|
||||
if (!failure.retryable() || attempt == downloadPolicy.attempts()) {
|
||||
if (failure.retryable() && attempt > 1) {
|
||||
throw new DownloadFailure(
|
||||
failure.getMessage() + " Download failed after " + attempt + " attempts.",
|
||||
false,
|
||||
failure
|
||||
);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
awaitRetry(downloadPolicy.retryDelay().multipliedBy(attempt));
|
||||
}
|
||||
}
|
||||
throw Objects.requireNonNull(lastFailure, "lastFailure");
|
||||
}
|
||||
|
||||
private static void downloadAttempt(String name, String url, File target, long maxBytes, Duration readTimeout,
|
||||
TransferProgressListener progress) throws IOException {
|
||||
Path staged = null;
|
||||
try {
|
||||
checkInterrupted();
|
||||
HttpResponse<InputStream> response = client()
|
||||
.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() / 100 != 2) {
|
||||
response.body().close();
|
||||
IrisLogging.reportError(new IOException("HTTP " + response.statusCode()
|
||||
+ " downloading " + name));
|
||||
return false;
|
||||
}
|
||||
long declaredBytes = response.headers().firstValueAsLong("Content-Length").orElse(-1L);
|
||||
if (declaredBytes > maxBytes) {
|
||||
response.body().close();
|
||||
throw new IOException("Download exceeds the size limit for " + name + ".");
|
||||
}
|
||||
Path destination = target.toPath().toAbsolutePath().normalize();
|
||||
Path parent = destination.getParent();
|
||||
if (parent == null) {
|
||||
response.body().close();
|
||||
throw new IOException("Download target has no parent: " + destination);
|
||||
throw new DownloadFailure("Download target has no parent: " + destination, false);
|
||||
}
|
||||
Files.createDirectories(parent);
|
||||
staged = Files.createTempFile(parent, ".download-", ".tmp");
|
||||
long startedNanos = System.nanoTime();
|
||||
long lastProgressNanos = startedNanos;
|
||||
sendProgress(progress, new TransferProgress(0L, declaredBytes, 0L, false));
|
||||
long downloadedBytes = 0L;
|
||||
try (InputStream in = response.body();
|
||||
OutputStream out = Files.newOutputStream(staged, StandardOpenOption.WRITE)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while (true) {
|
||||
checkInterrupted();
|
||||
read = in.read(buffer);
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
checkInterrupted();
|
||||
if (read > maxBytes - downloadedBytes) {
|
||||
throw new IOException("Download exceeds the size limit for " + name + ".");
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
downloadedBytes += read;
|
||||
long currentNanos = System.nanoTime();
|
||||
if (currentNanos - lastProgressNanos >= PROGRESS_INTERVAL_NANOS) {
|
||||
sendProgress(progress, new TransferProgress(
|
||||
downloadedBytes,
|
||||
declaredBytes,
|
||||
elapsedMillis(startedNanos, currentNanos),
|
||||
false
|
||||
));
|
||||
lastProgressNanos = currentNanos;
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
try {
|
||||
Files.createDirectories(parent);
|
||||
staged = Files.createTempFile(parent, ".download-", ".tmp");
|
||||
} catch (IOException exception) {
|
||||
throw new DownloadFailure("Unable to stage download " + name + ".", false, exception);
|
||||
}
|
||||
sendProgress(progress, new TransferProgress(
|
||||
downloadedBytes,
|
||||
declaredBytes,
|
||||
elapsedMillis(startedNanos),
|
||||
true
|
||||
));
|
||||
transfer(name, url, staged, maxBytes, readTimeout, progress);
|
||||
checkInterrupted();
|
||||
try {
|
||||
Files.move(staged, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException unsupported) {
|
||||
Files.move(staged, destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
throw new DownloadFailure("Unable to publish download " + name + ".", false, exception);
|
||||
}
|
||||
staged = null;
|
||||
return true;
|
||||
} catch (InterruptedIOException e) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
IrisLogging.debug("Download interrupted for " + name);
|
||||
} else {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
IrisLogging.debug("Download interrupted for " + name);
|
||||
return false;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
return false;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
IrisLogging.debug("Download interrupted for " + name);
|
||||
return false;
|
||||
} finally {
|
||||
if (staged != null) {
|
||||
try {
|
||||
@@ -227,6 +213,165 @@ public final class WebCache {
|
||||
}
|
||||
}
|
||||
|
||||
private static void transfer(String name, String url, Path staged, long maxBytes, Duration readTimeout,
|
||||
TransferProgressListener progress) throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
Thread interruptionWatchdog = null;
|
||||
boolean responseStarted = false;
|
||||
try {
|
||||
URLConnection opened = URI.create(url).toURL().openConnection();
|
||||
if (!(opened instanceof HttpURLConnection httpConnection)) {
|
||||
throw new DownloadFailure("Download URL is not HTTP or HTTPS.", false);
|
||||
}
|
||||
connection = httpConnection;
|
||||
connection.setConnectTimeout(timeoutMillis(CONNECT_TIMEOUT));
|
||||
connection.setReadTimeout(timeoutMillis(readTimeout));
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setRequestMethod("GET");
|
||||
interruptionWatchdog = startInterruptionWatchdog(connection);
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (statusCode / 100 != 2) {
|
||||
closeErrorResponse(connection);
|
||||
throw new DownloadFailure(
|
||||
"HTTP " + statusCode + " while downloading " + name + ".",
|
||||
isRetryableStatus(statusCode)
|
||||
);
|
||||
}
|
||||
responseStarted = true;
|
||||
long declaredBytes = connection.getContentLengthLong();
|
||||
if (declaredBytes > maxBytes) {
|
||||
throw new DownloadFailure("Download exceeds the size limit for " + name + ".", false);
|
||||
}
|
||||
streamResponse(name, connection, staged, maxBytes, declaredBytes, progress);
|
||||
} catch (DownloadFailure exception) {
|
||||
throw exception;
|
||||
} catch (InterruptedIOException exception) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw exception;
|
||||
}
|
||||
String message = responseStarted && exception instanceof SocketTimeoutException
|
||||
? "Download of " + name + " stalled for " + readTimeout.toSeconds()
|
||||
+ " seconds without receiving data."
|
||||
: "Connection timed out or was interrupted while downloading " + name + ".";
|
||||
throw new DownloadFailure(message, true, exception);
|
||||
} catch (IOException exception) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
InterruptedIOException interrupted = new InterruptedIOException("Download interrupted.");
|
||||
interrupted.initCause(exception);
|
||||
throw interrupted;
|
||||
}
|
||||
throw new DownloadFailure("Network failure while downloading " + name + ": "
|
||||
+ errorDetail(exception), true, exception);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DownloadFailure("Invalid download URL for " + name + ".", false, exception);
|
||||
} finally {
|
||||
if (interruptionWatchdog != null) {
|
||||
interruptionWatchdog.interrupt();
|
||||
}
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void streamResponse(String name, HttpURLConnection connection, Path staged, long maxBytes,
|
||||
long declaredBytes, TransferProgressListener progress) throws IOException {
|
||||
long startedNanos = System.nanoTime();
|
||||
long lastProgressNanos = startedNanos;
|
||||
sendProgress(progress, new TransferProgress(0L, declaredBytes, 0L, false));
|
||||
long downloadedBytes = 0L;
|
||||
try (InputStream in = connection.getInputStream();
|
||||
OutputStream out = Files.newOutputStream(staged, StandardOpenOption.WRITE)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while (true) {
|
||||
checkInterrupted();
|
||||
read = in.read(buffer);
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
checkInterrupted();
|
||||
if (read > maxBytes - downloadedBytes) {
|
||||
throw new DownloadFailure("Download exceeds the size limit for " + name + ".", false);
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
downloadedBytes += read;
|
||||
long currentNanos = System.nanoTime();
|
||||
if (currentNanos - lastProgressNanos >= PROGRESS_INTERVAL_NANOS) {
|
||||
sendProgress(progress, new TransferProgress(
|
||||
downloadedBytes,
|
||||
declaredBytes,
|
||||
elapsedMillis(startedNanos, currentNanos),
|
||||
false
|
||||
));
|
||||
lastProgressNanos = currentNanos;
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
if (declaredBytes >= 0L && downloadedBytes != declaredBytes) {
|
||||
throw new DownloadFailure(
|
||||
"Download of " + name + " ended after " + downloadedBytes + " of " + declaredBytes + " bytes.",
|
||||
true
|
||||
);
|
||||
}
|
||||
sendProgress(progress, new TransferProgress(
|
||||
downloadedBytes,
|
||||
declaredBytes,
|
||||
elapsedMillis(startedNanos),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
private static void awaitRetry(Duration delay) throws InterruptedIOException {
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(delay.toMillis());
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
InterruptedIOException interrupted = new InterruptedIOException("Download retry interrupted.");
|
||||
interrupted.initCause(exception);
|
||||
throw interrupted;
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startInterruptionWatchdog(HttpURLConnection connection) {
|
||||
Thread worker = Thread.currentThread();
|
||||
Thread watchdog = new Thread(() -> {
|
||||
while (!worker.isInterrupted()) {
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
connection.disconnect();
|
||||
}, "Iris Web Download Watchdog");
|
||||
watchdog.setDaemon(true);
|
||||
watchdog.start();
|
||||
return watchdog;
|
||||
}
|
||||
|
||||
private static boolean isRetryableStatus(int statusCode) {
|
||||
return statusCode == 408 || statusCode == 425 || statusCode == 429 || statusCode >= 500;
|
||||
}
|
||||
|
||||
private static int timeoutMillis(Duration timeout) {
|
||||
return Math.toIntExact(Math.min(Integer.MAX_VALUE, timeout.toMillis()));
|
||||
}
|
||||
|
||||
private static void closeErrorResponse(HttpURLConnection connection) throws IOException {
|
||||
InputStream response = connection.getErrorStream();
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String errorDetail(IOException exception) {
|
||||
String message = exception.getMessage();
|
||||
return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message;
|
||||
}
|
||||
|
||||
private static void checkInterrupted() throws InterruptedIOException {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedIOException("Download interrupted.");
|
||||
@@ -249,27 +394,39 @@ public final class WebCache {
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient client() {
|
||||
HttpClient current = client;
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
synchronized (WebCache.class) {
|
||||
if (client == null) {
|
||||
client = HttpClient.newBuilder()
|
||||
.connectTimeout(CONNECT_TIMEOUT)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
public record TransferProgress(long transferredBytes, long contentLength, long elapsedMillis, boolean complete) {
|
||||
}
|
||||
|
||||
public record TransferProgress(long transferredBytes, long contentLength, long elapsedMillis, boolean complete) {
|
||||
record DownloadPolicy(Duration readTimeout, int attempts, Duration retryDelay) {
|
||||
DownloadPolicy {
|
||||
Objects.requireNonNull(readTimeout, "readTimeout");
|
||||
Objects.requireNonNull(retryDelay, "retryDelay");
|
||||
if (readTimeout.isZero() || readTimeout.isNegative() || attempts < 1 || retryDelay.isNegative()) {
|
||||
throw new IllegalArgumentException("Download policy values are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TransferProgressListener {
|
||||
void onProgress(TransferProgress progress);
|
||||
}
|
||||
|
||||
private static final class DownloadFailure extends IOException {
|
||||
private final boolean retryable;
|
||||
|
||||
private DownloadFailure(String message, boolean retryable) {
|
||||
super(message);
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
private DownloadFailure(String message, boolean retryable, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
private boolean retryable() {
|
||||
return retryable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
|
||||
package art.arcane.iris.util.common.plugin;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.hud.HudPriority;
|
||||
import art.arcane.volmlib.util.hud.HudSegment;
|
||||
import art.arcane.volmlib.util.hud.HudSlot;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import art.arcane.volmlib.util.plugin.ComponentText;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
@@ -32,10 +33,6 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.command.CommandSender;
|
||||
@@ -217,7 +214,7 @@ public class VolmitSender implements CommandSender {
|
||||
}
|
||||
|
||||
public void hr() {
|
||||
s.sendMessage("========================================================");
|
||||
ComponentMessenger.sendLiteral(s, "========================================================");
|
||||
}
|
||||
|
||||
public void sendProgress(double percent, String thing) {
|
||||
@@ -228,21 +225,22 @@ public class VolmitSender implements CommandSender {
|
||||
|
||||
public void sendAction(String action) {
|
||||
try {
|
||||
deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action)));
|
||||
deliverAction(createNoPrefixComponent(action));
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendActionNoProcessing(String action) {
|
||||
try {
|
||||
deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action)));
|
||||
deliverAction(createNoPrefixComponentNoProcessing(action));
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverAction(String legacy) {
|
||||
private void deliverAction(ComponentText message) {
|
||||
Player player = player();
|
||||
if (BukkitPlatform.hasHud()) {
|
||||
String legacy = message.legacy();
|
||||
if (legacy.isBlank()) {
|
||||
BukkitPlatform.hudBar().clear(player, "iris:action");
|
||||
} else {
|
||||
@@ -250,54 +248,56 @@ public class VolmitSender implements CommandSender {
|
||||
}
|
||||
return;
|
||||
}
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(legacy));
|
||||
ComponentMessenger.sendActionBar(player, message);
|
||||
}
|
||||
|
||||
private Component createNoPrefixComponent(String message) {
|
||||
private ComponentText createNoPrefixComponent(String message) {
|
||||
if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) {
|
||||
String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(message));
|
||||
return MiniMessage.miniMessage().deserialize(C.mini(t));
|
||||
return ComponentText.markup(C.mini(t));
|
||||
}
|
||||
|
||||
String t = C.translateAlternateColorCodes('&', message);
|
||||
String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb(), 0.36);
|
||||
return MiniMessage.miniMessage().deserialize(a);
|
||||
return ComponentText.markup(a);
|
||||
}
|
||||
|
||||
private Component createNoPrefixComponentNoProcessing(String message) {
|
||||
return MiniMessage.builder().postProcessor(c -> c).build().deserialize(C.mini(message));
|
||||
private ComponentText createNoPrefixComponentNoProcessing(String message) {
|
||||
return ComponentText.markup(C.mini(message));
|
||||
}
|
||||
|
||||
private Component createComponent(String message) {
|
||||
private ComponentText createComponent(String message) {
|
||||
if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) {
|
||||
String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message));
|
||||
return MiniMessage.miniMessage().deserialize(C.mini(t));
|
||||
return ComponentText.markup(C.mini(t));
|
||||
}
|
||||
|
||||
String t = C.translateAlternateColorCodes('&', getTag() + message);
|
||||
String a = C.aura(t, IrisSettings.get().getGeneral().getSpinh(), IrisSettings.get().getGeneral().getSpins(), IrisSettings.get().getGeneral().getSpinb());
|
||||
return MiniMessage.miniMessage().deserialize(a);
|
||||
return ComponentText.markup(a);
|
||||
}
|
||||
|
||||
private Component createComponentRaw(String message) {
|
||||
private ComponentText createComponentRaw(String message) {
|
||||
if (!IrisSettings.get().getGeneral().canUseCustomColors(this)) {
|
||||
String t = C.translateAlternateColorCodes('&', MiniMessage.miniMessage().stripTags(getTag() + message));
|
||||
return MiniMessage.miniMessage().deserialize(C.mini(t));
|
||||
return ComponentText.markup(C.mini(t));
|
||||
}
|
||||
|
||||
String t = C.translateAlternateColorCodes('&', getTag() + message);
|
||||
return MiniMessage.miniMessage().deserialize(C.mini(t));
|
||||
return ComponentText.markup(C.mini(t));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) {
|
||||
s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message));
|
||||
ComponentMessenger.sendSection(s, C.translateAlternateColorCodes('&', getTag() + message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.contains("<NOMINI>")) {
|
||||
s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message.replaceAll("\\Q<NOMINI>\\E", "")));
|
||||
ComponentMessenger.sendSection(
|
||||
s,
|
||||
C.translateAlternateColorCodes('&', getTag() + message.replace("<NOMINI>", "")));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -305,17 +305,17 @@ public class VolmitSender implements CommandSender {
|
||||
}
|
||||
|
||||
public void sendMessageBasic(String message) {
|
||||
s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message));
|
||||
ComponentMessenger.sendSection(s, C.translateAlternateColorCodes('&', getTag() + message));
|
||||
}
|
||||
|
||||
public void sendMessageRaw(String message) {
|
||||
if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) {
|
||||
s.sendMessage(C.translateAlternateColorCodes('&', message));
|
||||
ComponentMessenger.sendSection(s, C.translateAlternateColorCodes('&', message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.contains("<NOMINI>")) {
|
||||
s.sendMessage(message.replaceAll("\\Q<NOMINI>\\E", ""));
|
||||
ComponentMessenger.sendSection(s, message.replace("<NOMINI>", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,103 +323,11 @@ public class VolmitSender implements CommandSender {
|
||||
}
|
||||
|
||||
public void sendComponent(Component component) {
|
||||
deliver(component);
|
||||
deliver(ComponentText.component(component));
|
||||
}
|
||||
|
||||
private void deliver(Component component) {
|
||||
if (sendNative(s, component)) {
|
||||
return;
|
||||
}
|
||||
s.sendMessage(LegacyComponentSerializer.legacySection().serialize(component));
|
||||
}
|
||||
|
||||
private static volatile boolean nativeProbed = false;
|
||||
private static Object nativeGson;
|
||||
private static java.lang.reflect.Method nativeDeserialize;
|
||||
private static java.lang.reflect.Method nativeSendMessage;
|
||||
private static final java.util.concurrent.atomic.AtomicBoolean SEND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
private static void adventureDebug(String message) {
|
||||
try {
|
||||
java.io.File f = IrisPlatforms.get().dataFile("adventure-debug.txt");
|
||||
java.nio.file.Files.writeString(f.toPath(), message + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static String nativeAdventure(String suffix) {
|
||||
return new String(new char[]{'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.', 't', 'e', 'x', 't', '.'}) + suffix;
|
||||
}
|
||||
|
||||
private static void probeNative() {
|
||||
String step = "start";
|
||||
try {
|
||||
String[] candidates = {nativeAdventure("serializer.gson.GsonComponentSerializer"), nativeAdventure("serializer.json.JSONComponentSerializer")};
|
||||
String[] accessors = {"gson", "json"};
|
||||
Class<?> serializer = null;
|
||||
for (int i = 0; i < candidates.length; i++) {
|
||||
try {
|
||||
step = "forName " + candidates[i];
|
||||
Class<?> c = Class.forName(candidates[i]);
|
||||
step = "accessor " + accessors[i] + " on " + candidates[i];
|
||||
nativeGson = c.getMethod(accessors[i]).invoke(null);
|
||||
serializer = c;
|
||||
break;
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
if (serializer == null) {
|
||||
throw new ClassNotFoundException("no native adventure json serializer exposed");
|
||||
}
|
||||
step = "find deserialize on " + serializer.getName();
|
||||
java.lang.reflect.Method exact = null;
|
||||
java.lang.reflect.Method loose = null;
|
||||
for (java.lang.reflect.Method m : serializer.getMethods()) {
|
||||
if (m.getName().equals("deserialize") && m.getParameterCount() == 1) {
|
||||
Class<?> p = m.getParameterTypes()[0];
|
||||
if (p == String.class) {
|
||||
exact = m;
|
||||
break;
|
||||
}
|
||||
if (p.isAssignableFrom(String.class)) {
|
||||
loose = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
nativeDeserialize = exact != null ? exact : loose;
|
||||
if (nativeDeserialize == null) {
|
||||
throw new NoSuchMethodException("deserialize(String-compatible) not found");
|
||||
}
|
||||
step = "forName Component";
|
||||
Class<?> componentType = Class.forName(nativeAdventure("Component"));
|
||||
step = "getMethod CommandSender.sendMessage(Component)";
|
||||
nativeSendMessage = CommandSender.class.getMethod("sendMessage", componentType);
|
||||
} catch (Throwable e) {
|
||||
nativeSendMessage = null;
|
||||
adventureDebug("PROBE FAIL at [" + step + "]: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
}
|
||||
nativeProbed = true;
|
||||
}
|
||||
|
||||
private static boolean sendNative(CommandSender target, Component component) {
|
||||
if (!nativeProbed) {
|
||||
probeNative();
|
||||
}
|
||||
if (nativeSendMessage == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
String json = GsonComponentSerializer.gson().serialize(component);
|
||||
Object nativeComponent = nativeDeserialize.invoke(nativeGson, json);
|
||||
nativeSendMessage.invoke(target, nativeComponent);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
if (SEND_LOGGED.compareAndSet(false, true)) {
|
||||
Throwable cause = e.getCause() != null ? e.getCause() : e;
|
||||
adventureDebug("SEND FAIL: " + cause.getClass().getName() + ": " + cause.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private void deliver(ComponentText message) {
|
||||
ComponentMessenger.send(s, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,15 +19,41 @@
|
||||
package art.arcane.iris.util.project.noise;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
|
||||
public class ImageNoise implements NoiseGenerator {
|
||||
private final IrisImageMap expression;
|
||||
private final IrisData data;
|
||||
private final CompiledIrisImageMap compiled;
|
||||
|
||||
public ImageNoise(IrisData data, IrisImageMap expression) {
|
||||
this.data = data;
|
||||
this.expression = expression;
|
||||
public ImageNoise(IrisData data, String imageMapKey) {
|
||||
IrisImageMap definition = data.getImageMapLoader().load(imageMapKey);
|
||||
if (definition == null) {
|
||||
throw new IrisImageMapValidationException("Missing image-map resource '" + imageMapKey + "'");
|
||||
}
|
||||
if (definition.getType() == IrisImageMapType.COLOR_MAP) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Generator-style image-map resource '" + imageMapKey + "' must produce normalized scalar data"
|
||||
);
|
||||
}
|
||||
IrisImage image = data.getImageLoader().load(definition.getSource());
|
||||
if (image == null) {
|
||||
throw new IrisImageMapValidationException(
|
||||
"Image-map resource '" + imageMapKey + "' references missing or invalid PNG '"
|
||||
+ definition.getSource() + "'"
|
||||
);
|
||||
}
|
||||
try {
|
||||
compiled = CompiledIrisImageMap.compile(definition, image);
|
||||
} finally {
|
||||
data.getImageLoader().unload(definition.getSource());
|
||||
}
|
||||
}
|
||||
|
||||
public String getContentHash() {
|
||||
return compiled.getContentHash();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,7 +63,7 @@ public class ImageNoise implements NoiseGenerator {
|
||||
|
||||
@Override
|
||||
public double noise(double x, double z) {
|
||||
return expression.getNoise(data, (int) x, (int) z);
|
||||
return compiled.sampleNormalized(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris bietet integrierte Downloads nur für 'overworld' und 'underworld' an.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris wird heruntergefahren und nimmt keine Pack-Downloads mehr an.",
|
||||
"iris.runtime.pack_download.downloading": "{url} wird heruntergeladen",
|
||||
"iris.runtime.pack_download.failed_to_find": "Unter {url} wurde kein Pack gefunden",
|
||||
"iris.runtime.pack_download.unpacking": "{repository} wird entpackt",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Beim Entpacken ist ein Problem aufgetreten. Bitte prüfe Folgendes:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris solo ofrece descargas integradas para 'overworld' y 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris se está cerrando y no acepta descargas de packs.",
|
||||
"iris.runtime.pack_download.downloading": "Descargando {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "No se encontró el pack en {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Descomprimiendo {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Se produjo un problema al descomprimir. Comprueba lo siguiente:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIriksen sisäiset lataukset ovat saatavilla vain paketeille 'overworld' ja 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIristä sammutetaan, eikä se ota vastaan pakettien latauksia.",
|
||||
"iris.runtime.pack_download.downloading": "Noudetaan {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Pakkausta ei löytynyt {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Purkaminen {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Purkamisen yhteydessä. Tarkista/ tee seuraava:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris ne propose de téléchargement intégré que pour 'overworld' et 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris est en cours d’arrêt et n’accepte plus de téléchargements de packs.",
|
||||
"iris.runtime.pack_download.downloading": "Téléchargement de {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Pack introuvable dans {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Décompression de {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Un problème est survenu pendant la décompression. Vérifiez les points suivants :",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris מספק הורדות מובנות רק עבור 'overworld' ו-'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris נכבה כעת ואינו מקבל הורדות של חבילות.",
|
||||
"iris.runtime.pack_download.downloading": "הורדה {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "נכשל למצוא חבילות {url}",
|
||||
"iris.runtime.pack_download.unpacking": "חבילות חילוץ {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"בעיה כאשר לא לארוז. בדוק/עשה את הפעולות הבאות:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris offre download integrati solo per 'overworld' e 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris si sta arrestando e non accetta download di pack.",
|
||||
"iris.runtime.pack_download.downloading": "Scaricamento {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Impossibile trovare il Pack in {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Disimballaggio {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Si è verificato un problema durante l'estrazione. Controlla quanto segue:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris の組み込みダウンロードは 'overworld' と 'underworld' のみです。",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris はシャットダウン中のため、パックのダウンロードを受け付けていません。",
|
||||
"iris.runtime.pack_download.downloading": "{url} をダウンロードしています",
|
||||
"iris.runtime.pack_download.failed_to_find": "{url} にパックが見つかりませんでした",
|
||||
"iris.runtime.pack_download.unpacking": "{repository} を展開しています",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"展開中に問題が発生しました。次の項目を確認してください:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris는 'overworld'와 'underworld'에 대해서만 기본 제공 다운로드를 지원합니다.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris가 종료 중이므로 팩 다운로드를 받을 수 없습니다.",
|
||||
"iris.runtime.pack_download.downloading": "다운로드 {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "팩을 찾기 위해 실패 {url}",
|
||||
"iris.runtime.pack_download.unpacking": "옵션 정보 {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"압축 해제 중 문제가 발생했습니다. 다음 항목을 확인하세요:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris integruotai leidžia atsisiųsti tik 'overworld' ir 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris išjungiamas ir nebepriima paketų atsisiuntimų.",
|
||||
"iris.runtime.pack_download.downloading": "Atsiunčiama {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Nepavyko rasti pakuotės {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Išpakavimas {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Išpakuojama. Prašome patikrinti / atlikti šiuos veiksmus:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris biedt alleen ingebouwde downloads voor 'overworld' en 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris wordt afgesloten en accepteert geen pakketdownloads.",
|
||||
"iris.runtime.pack_download.downloading": "Downloaden {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Kon pakket niet vinden op {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Uitpakken {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Uitpakken. Controleer/doe het volgende:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris udostępnia wbudowane pobieranie tylko dla 'overworld' i 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eTrwa wyłączanie Iris; pobieranie pakietów nie jest już przyjmowane.",
|
||||
"iris.runtime.pack_download.downloading": "Pobieranie {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Nie udało się znaleźć pakietu w {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Rozpakowanie {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Wydanie podczas rozpakowywania. Proszę sprawdzić / wykonać następujące czynności:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris apenas disponibiliza transferências integradas para 'overworld' e 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris está a encerrar e não aceita transferências de packs.",
|
||||
"iris.runtime.pack_download.downloading": "Baixando {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Não foi possível encontrar o pacote em {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Desembalar {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Problema ao desfazer as malas. Verifique/faça o seguinte:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cВстроенная загрузка Iris доступна только для 'overworld' и 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris завершает работу и не принимает загрузки пакетов.",
|
||||
"iris.runtime.pack_download.downloading": "Скачать {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Не удалось найти стаю {url}",
|
||||
"iris.runtime.pack_download.unpacking": "распаковка {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Проблемы при распаковке. Пожалуйста, проверьте / сделайте следующее:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris yalnızca 'overworld' ve 'underworld' için yerleşik indirmeler sunar.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris kapanıyor ve paket indirmelerini kabul etmiyor.",
|
||||
"iris.runtime.pack_download.downloading": "Downloading indir {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Paket bulmak için başarısız oldu {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Unpackinging {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Paket açılırken sorun oluştu. Lütfen şunları kontrol edin/yapın:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris chỉ cung cấp bản tải xuống tích hợp cho 'overworld' và 'underworld'.",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris đang tắt và không nhận yêu cầu tải gói.",
|
||||
"iris.runtime.pack_download.downloading": "Đang tải về {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "Không tìm thấy gói tại {url}",
|
||||
"iris.runtime.pack_download.unpacking": "Đang mở gói {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"Vấn đề là khi dỡ đồ ra. Vui lòng kiểm tra/ làm những điều sau:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris 仅为 'overworld' 和 'underworld' 提供内置下载。",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris 正在关闭,不再接受资源包下载。",
|
||||
"iris.runtime.pack_download.downloading": "下载 {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "找到包失败 {url}",
|
||||
"iris.runtime.pack_download.unpacking": "正在解压 {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"解包时发布. 请检查/做如下:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@
|
||||
"iris.runtime.pack_download.invalid_built_in": "§cIris 僅為 'overworld' 和 'underworld' 提供內建下載。",
|
||||
"iris.runtime.pack_download.shutting_down": "§eIris 正在關閉,不再接受資源包下載。",
|
||||
"iris.runtime.pack_download.downloading": "下載 {url}",
|
||||
"iris.runtime.pack_download.failed_to_find": "找到包失敗 {url}",
|
||||
"iris.runtime.pack_download.unpacking": "正在解壓縮 {repository}",
|
||||
"iris.runtime.pack_download.unpack_failed": [
|
||||
"解包時釋出. 請檢查/做如下:",
|
||||
@@ -1482,6 +1481,75 @@
|
||||
"iris.runtime.what.flag.ore": "ore",
|
||||
"iris.runtime.what.flag.block_entity": "block entity",
|
||||
"iris.runtime.world.height_range": "World height: {minY} to {maxY}",
|
||||
"iris.runtime.world.height_total": "Total height: {height}"
|
||||
"iris.runtime.world.height_total": "Total height: {height}",
|
||||
"iris.desktop.imagemap.title": "Image Map Studio",
|
||||
"iris.desktop.imagemap.preset": "Preset:",
|
||||
"iris.desktop.imagemap.load": "Load",
|
||||
"iris.desktop.imagemap.import_png": "Import PNG",
|
||||
"iris.desktop.imagemap.replace_png": "Replace PNG",
|
||||
"iris.desktop.imagemap.preview": "Preview",
|
||||
"iris.desktop.imagemap.export": "Export to active pack",
|
||||
"iris.desktop.imagemap.metadata": "Source metadata",
|
||||
"iris.desktop.imagemap.resource": "Resource binding",
|
||||
"iris.desktop.imagemap.coordinates": "Coordinates and sampling",
|
||||
"iris.desktop.imagemap.binding_key": "Binding key",
|
||||
"iris.desktop.imagemap.map_key": "Map key",
|
||||
"iris.desktop.imagemap.image_key": "Image key",
|
||||
"iris.desktop.imagemap.type": "Type",
|
||||
"iris.desktop.imagemap.application": "Application",
|
||||
"iris.desktop.imagemap.blocks_per_pixel": "Blocks per pixel",
|
||||
"iris.desktop.imagemap.origin_x": "World origin X",
|
||||
"iris.desktop.imagemap.origin_z": "World origin Z",
|
||||
"iris.desktop.imagemap.source_origin_x": "Source origin X",
|
||||
"iris.desktop.imagemap.source_origin_z": "Source origin Z",
|
||||
"iris.desktop.imagemap.rotation": "Rotation",
|
||||
"iris.desktop.imagemap.mirror_x": "Mirror X",
|
||||
"iris.desktop.imagemap.mirror_z": "Mirror Z",
|
||||
"iris.desktop.imagemap.sampling": "Sampling",
|
||||
"iris.desktop.imagemap.out_of_bounds": "Out of bounds",
|
||||
"iris.desktop.imagemap.alpha": "Alpha policy",
|
||||
"iris.desktop.imagemap.fallback_value": "Fallback value",
|
||||
"iris.desktop.imagemap.fallback_target": "Fallback target",
|
||||
"iris.desktop.imagemap.minimum_height": "Minimum height",
|
||||
"iris.desktop.imagemap.maximum_height": "Maximum height",
|
||||
"iris.desktop.imagemap.vertical_offset": "Vertical offset",
|
||||
"iris.desktop.imagemap.clamp": "Clamp height",
|
||||
"iris.desktop.imagemap.inverted": "Invert values",
|
||||
"iris.desktop.imagemap.curve_exponent": "Curve exponent",
|
||||
"iris.desktop.imagemap.smoothing_radius": "Smoothing radius",
|
||||
"iris.desktop.imagemap.threshold": "Threshold",
|
||||
"iris.desktop.imagemap.falloff": "Falloff",
|
||||
"iris.desktop.imagemap.color_tolerance": "Raw sRGB tolerance",
|
||||
"iris.desktop.imagemap.unknown_color": "Unknown color",
|
||||
"iris.desktop.imagemap.add_color": "Add legend color",
|
||||
"iris.desktop.imagemap.remove_color": "Remove selected",
|
||||
"iris.desktop.imagemap.composed_masks": "Composed masks",
|
||||
"iris.desktop.imagemap.add_mask": "Add mask binding",
|
||||
"iris.desktop.imagemap.height": "Height interpretation",
|
||||
"iris.desktop.imagemap.mask": "Mask interpretation",
|
||||
"iris.desktop.imagemap.color_map": "Color legend",
|
||||
"iris.desktop.imagemap.overlays": "World overlays",
|
||||
"iris.desktop.imagemap.chunks": "16-block chunks",
|
||||
"iris.desktop.imagemap.regions": "512-block regions",
|
||||
"iris.desktop.imagemap.boundary": "World boundary",
|
||||
"iris.desktop.imagemap.coverage": "Source coverage",
|
||||
"iris.desktop.imagemap.diagnostics": "Diagnostics",
|
||||
"iris.desktop.imagemap.ready": "Ready",
|
||||
"iris.desktop.imagemap.no_source": "Import a PNG source to begin.",
|
||||
"iris.desktop.imagemap.loading": "Loading preset...",
|
||||
"iris.desktop.imagemap.load_failed": "Preset load failed",
|
||||
"iris.desktop.imagemap.previewing": "Compiling preview...",
|
||||
"iris.desktop.imagemap.preview_valid": "Runtime compiler validation passed.",
|
||||
"iris.desktop.imagemap.preview_failed": "Preview failed",
|
||||
"iris.desktop.imagemap.exporting": "Validating and exporting...",
|
||||
"iris.desktop.imagemap.exported": "Exported image map, PNG, and dimension binding atomically.",
|
||||
"iris.desktop.imagemap.export_failed": "Export failed",
|
||||
"iris.desktop.imagemap.source": "Source pixels",
|
||||
"iris.desktop.imagemap.interpreted": "Runtime interpretation",
|
||||
"iris.desktop.imagemap.no_preview": "Compile a valid preview to inspect world output.",
|
||||
"iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel",
|
||||
"iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.",
|
||||
"iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ public class DiagnosticSeverityPolicyTest {
|
||||
assertLoggedWith("util/project/hunk/Hunk.java", "OUT OF BOUNDS ", "debug");
|
||||
assertLoggedWith("engine/mantle/MantleWriter.java", "No set? ", "debug");
|
||||
assertLoggedWith("engine/mantle/MantleWriter.java", "Mantle Writer Accessed chunk out of bounds", "debug");
|
||||
assertLoggedWith("engine/object/IrisImageMap.java", "No image ", "warnOnce");
|
||||
assertLoggedWith("engine/object/IrisDecorator.java", "Empty Block Data for ", "warnOnce");
|
||||
assertLoggedWith("engine/object/IrisCompat.java", "Can't find block data for ", "warnOnce");
|
||||
assertLoggedWith("engine/data/cache/AtomicCache.java", "Atomic cache supplier failed: %s: %s", "warnOnce");
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ImageMapStudioGUITest {
|
||||
@Test
|
||||
public void invalidatesPresetKeysBeforeHotloadingTheActiveEngine() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ResourceLoader<IrisImageMap> loader = mock(ResourceLoader.class);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(data.getImageMapLoader()).thenReturn(loader);
|
||||
|
||||
ImageMapStudioGUI.reloadActiveEngine(engine);
|
||||
|
||||
InOrder order = inOrder(loader, engine);
|
||||
order.verify(loader).clearCache();
|
||||
order.verify(engine).hotloadSilently();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOrigin;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapRotation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundaryCenter;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ImageMapStudioModelTest {
|
||||
private static final double EPSILON = 0.000001D;
|
||||
|
||||
@Test
|
||||
public void inspectsCanonicalSixteenBitGrayscaleMetadata() {
|
||||
BufferedImage image = new BufferedImage(3, 2, BufferedImage.TYPE_USHORT_GRAY);
|
||||
|
||||
ImageMapStudioModel.SourceMetadata metadata = ImageMapStudioModel.inspect(
|
||||
Path.of("source.png"), image, "png"
|
||||
);
|
||||
|
||||
assertEquals(3, metadata.width());
|
||||
assertEquals(2, metadata.height());
|
||||
assertEquals(6L, metadata.pixels());
|
||||
assertEquals("png", metadata.format());
|
||||
assertEquals("GRAYSCALE", metadata.colorMode());
|
||||
assertEquals(16, metadata.bitDepth());
|
||||
assertFalse(metadata.alpha());
|
||||
assertTrue(metadata.summary().contains("PNG"));
|
||||
assertTrue(metadata.summary().contains("profile not inspected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizesResourceKeysAndRejectsAmbiguousLegendRows() {
|
||||
assertEquals("terrain/my-map", ImageMapStudioModel.safeKey(" Terrain/My Map.PNG "));
|
||||
assertEquals("image-map", ImageMapStudioModel.safeKey(".."));
|
||||
|
||||
KMap<String, String> legend = ImageMapStudioModel.legend(List.of(
|
||||
new ImageMapStudioModel.LegendRow("#00aaFF", "minecraft:water")
|
||||
));
|
||||
assertEquals("minecraft:water", legend.get("#00AAFF"));
|
||||
assertThrows(IllegalArgumentException.class, () -> ImageMapStudioModel.legend(List.of(
|
||||
new ImageMapStudioModel.LegendRow("#000000", "minecraft:stone"),
|
||||
new ImageMapStudioModel.LegendRow("#000000", "minecraft:deepslate")
|
||||
)));
|
||||
assertThrows(IllegalArgumentException.class, () -> ImageMapStudioModel.legend(List.of(
|
||||
new ImageMapStudioModel.LegendRow("000000", "minecraft:stone")
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsSourceCoordinatesThroughMirrorRotationScaleAndOrigins() {
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setBlocksPerPixel(2D)
|
||||
.setOrigin(new IrisImageMapOrigin(10D, 20D))
|
||||
.setSourceOrigin(new IrisImageMapOrigin(1D, 1D))
|
||||
.setMirrorX(true)
|
||||
.setRotation(IrisImageMapRotation.DEG_90);
|
||||
|
||||
Point2D.Double world = ImageMapStudioModel.sourceToWorld(definition, 2D, 1D);
|
||||
|
||||
assertEquals(10D, world.x, EPSILON);
|
||||
assertEquals(18D, world.y, EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsScaleAndBoundaryCoverageWarnings() {
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setBlocksPerPixel(0.5D)
|
||||
.setOrigin(new IrisImageMapOrigin())
|
||||
.setSourceOrigin(new IrisImageMapOrigin());
|
||||
IrisWorldBoundary boundary = new IrisWorldBoundary()
|
||||
.setCenter(new IrisWorldBoundaryCenter(2D, 2D))
|
||||
.setSize(20D);
|
||||
|
||||
List<String> warnings = ImageMapStudioModel.warnings(definition, 4, 4, boundary);
|
||||
|
||||
assertTrue(warnings.stream().anyMatch(value -> value.contains("Sub-block")));
|
||||
assertTrue(warnings.stream().anyMatch(value -> value.contains("complete chunk")));
|
||||
assertTrue(warnings.stream().anyMatch(value -> value.contains("not fully covered")));
|
||||
assertTrue(warnings.stream().anyMatch(value -> value.contains("substantially smaller")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsAnExactSourceAndWorldBoundaryFit() {
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setBlocksPerPixel(1D)
|
||||
.setOrigin(new IrisImageMapOrigin())
|
||||
.setSourceOrigin(new IrisImageMapOrigin());
|
||||
IrisWorldBoundary boundary = new IrisWorldBoundary()
|
||||
.setCenter(new IrisWorldBoundaryCenter(8D, 8D))
|
||||
.setSize(16D);
|
||||
|
||||
List<String> warnings = ImageMapStudioModel.warnings(definition, 16, 16, boundary);
|
||||
|
||||
assertTrue(warnings.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearsSettingsThatAreInvalidForTheSelectedType() {
|
||||
IrisImageMap height = new IrisImageMap()
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setColorTolerance(12D)
|
||||
.setColors(new KMap<>(Map.of("#000000", "iris:test")));
|
||||
IrisImageMap color = new IrisImageMap()
|
||||
.setType(IrisImageMapType.COLOR_MAP)
|
||||
.setInverted(true)
|
||||
.setCurveExponent(2D)
|
||||
.setSmoothingRadius(4)
|
||||
.setColors(new KMap<>(Map.of("#000000", "iris:test")));
|
||||
IrisImageMap mask = new IrisImageMap()
|
||||
.setType(IrisImageMapType.GRAYSCALE_MASK)
|
||||
.setMinimumHeight(12D)
|
||||
.setMaximumHeight(42D)
|
||||
.setThreshold(0.7D)
|
||||
.setFalloff(0.2D)
|
||||
.setColors(new KMap<>(Map.of("#000000", "iris:test")));
|
||||
|
||||
ImageMapStudioModel.normalizeTypeSettings(height);
|
||||
ImageMapStudioModel.normalizeTypeSettings(color);
|
||||
ImageMapStudioModel.normalizeTypeSettings(mask);
|
||||
|
||||
assertEquals(0D, height.getColorTolerance(), 0D);
|
||||
assertTrue(height.getColors().isEmpty());
|
||||
assertFalse(color.isInverted());
|
||||
assertEquals(1D, color.getCurveExponent(), 0D);
|
||||
assertEquals(0, color.getSmoothingRadius());
|
||||
assertEquals("iris:test", color.getColors().get("#000000"));
|
||||
assertEquals(-64D, mask.getMinimumHeight(), 0D);
|
||||
assertEquals(320D, mask.getMaximumHeight(), 0D);
|
||||
assertEquals(0.5D, mask.getThreshold(), 0D);
|
||||
assertEquals(0D, mask.getFalloff(), 0D);
|
||||
assertTrue(mask.getColors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsesOrderedComposedMaskRowsWithUnitRangeValidation() {
|
||||
List<ImageMapStudioModel.MaskRow> rows = List.of(
|
||||
ImageMapStudioModel.maskRow("land", "multiply", false, "0", "1"),
|
||||
ImageMapStudioModel.maskRow("roads", IrisImageMapMaskOperation.SUBTRACT, true, 0.5D, 0.25D)
|
||||
);
|
||||
|
||||
List<IrisImageMapMask> masks = ImageMapStudioModel.masks(rows);
|
||||
|
||||
assertEquals(2, masks.size());
|
||||
assertEquals("land", masks.get(0).getMap());
|
||||
assertEquals(IrisImageMapMaskOperation.MULTIPLY, masks.get(0).getOperation());
|
||||
assertEquals(IrisImageMapMaskOperation.SUBTRACT, masks.get(1).getOperation());
|
||||
assertTrue(masks.get(1).isInverted());
|
||||
assertEquals("roads", ImageMapStudioModel.maskRows(masks).get(1).map());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> ImageMapStudioModel.maskRow("bad", "ADD", false, 1.1D, 0D));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.image.IrisImageMapMaskSampler;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class ImageMapStudioPreviewPanelTest {
|
||||
@Test
|
||||
public void rendersAnInspectedSourceWithoutACompiledSemanticType() {
|
||||
BufferedImage source = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
source.setRGB(0, 0, 0xFFFF0000);
|
||||
ImageMapStudioPreviewPanel panel = new ImageMapStudioPreviewPanel();
|
||||
try {
|
||||
panel.setSource(source);
|
||||
|
||||
BufferedImage rendered = panel.renderSourceSnapshot(64, 64);
|
||||
|
||||
assertEquals(0xFFFF0000, rendered.getRGB(32, 32));
|
||||
} finally {
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rendersRuntimeInterpretedHeightDataWithoutAFrame() {
|
||||
BufferedImage source = new BufferedImage(2, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
source.getRaster().setSample(0, 0, 0, 0);
|
||||
source.getRaster().setSample(1, 0, 0, 255);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("test")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP)
|
||||
.setMinimumHeight(0D)
|
||||
.setMaximumHeight(255D);
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(
|
||||
definition, new IrisImage(source, "png")
|
||||
);
|
||||
ImageMapStudioPreviewPanel panel = new ImageMapStudioPreviewPanel();
|
||||
try {
|
||||
panel.setPreview(
|
||||
source, compiled, IrisImageMapMaskSampler.empty(), null,
|
||||
IrisImageMapApplication.TERRAIN_HEIGHT, -64, (worldX, worldZ) -> 0D
|
||||
);
|
||||
|
||||
BufferedImage interpreted = panel.renderInterpretedSnapshot(64, 32);
|
||||
|
||||
assertNotEquals(interpreted.getRGB(4, 16), interpreted.getRGB(59, 16));
|
||||
} finally {
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blendsMaskedHeightAgainstTheProceduralRuntimeBaseline() {
|
||||
BufferedImage source = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
source.getRaster().setSample(0, 0, 0, 255);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("height")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP)
|
||||
.setMinimumHeight(0D)
|
||||
.setMaximumHeight(100D);
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(definition, new IrisImage(source, "png"));
|
||||
IrisImageMapMaskSampler maskSampler = maskSampler(128);
|
||||
ImageMapStudioPreviewPanel panel = new ImageMapStudioPreviewPanel();
|
||||
try {
|
||||
panel.setPreview(
|
||||
source, compiled, maskSampler, null,
|
||||
IrisImageMapApplication.TERRAIN_HEIGHT, -64, (worldX, worldZ) -> 20D
|
||||
);
|
||||
|
||||
BufferedImage interpreted = panel.renderInterpretedSnapshot(64, 32);
|
||||
double weight = 128D / 255D;
|
||||
double expectedHeight = -64D + IrisImageMapRuntime.blendTerrainHeight(164D, 20D, weight);
|
||||
|
||||
assertEquals(
|
||||
ImageMapStudioModel.heightColor(expectedHeight, 0D, 100D),
|
||||
interpreted.getRGB(32, 16)
|
||||
);
|
||||
} finally {
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesTheRuntimeCategoricalMaskCutoff() {
|
||||
BufferedImage source = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
source.setRGB(0, 0, 0xFFFF0000);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("biome")
|
||||
.setType(IrisImageMapType.COLOR_MAP)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP)
|
||||
.setColors(new KMap<>(Map.of("#FF0000", "iris:test")));
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(definition, new IrisImage(source, "png"));
|
||||
ImageMapStudioPreviewPanel below = new ImageMapStudioPreviewPanel();
|
||||
ImageMapStudioPreviewPanel above = new ImageMapStudioPreviewPanel();
|
||||
try {
|
||||
below.setPreview(
|
||||
source, compiled, maskSampler(127), null,
|
||||
IrisImageMapApplication.BIOME, -64, null
|
||||
);
|
||||
above.setPreview(
|
||||
source, compiled, maskSampler(128), null,
|
||||
IrisImageMapApplication.BIOME, -64, null
|
||||
);
|
||||
|
||||
assertEquals(new Color(12, 15, 22).getRGB(), below.renderInterpretedSnapshot(64, 32).getRGB(32, 16));
|
||||
assertEquals(
|
||||
ImageMapStudioModel.targetColor("iris:test"),
|
||||
above.renderInterpretedSnapshot(64, 32).getRGB(32, 16)
|
||||
);
|
||||
} finally {
|
||||
below.close();
|
||||
above.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler maskSampler(int sample) {
|
||||
BufferedImage source = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
source.getRaster().setSample(0, 0, 0, sample);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("mask")
|
||||
.setType(IrisImageMapType.GRAYSCALE_MASK)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
CompiledIrisImageMap compiled = CompiledIrisImageMap.compile(definition, new IrisImage(source, "png"));
|
||||
IrisImageMapMask mask = new IrisImageMapMask().setOperation(IrisImageMapMaskOperation.MULTIPLY);
|
||||
return new IrisImageMapMaskSampler(List.of(IrisImageMapMaskSampler.layer(compiled, mask)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ImageResourceLoaderTest {
|
||||
@Test
|
||||
public void preflightsDimensionsBeforeImageDecode() {
|
||||
assertTrue(ImageResourceLoader.supportedDimensions(1, 1));
|
||||
assertTrue(ImageResourceLoader.supportedDimensions(16_384, 1_024));
|
||||
assertFalse(ImageResourceLoader.supportedDimensions(16_384, 1_025));
|
||||
assertFalse(ImageResourceLoader.supportedDimensions(16_385, 1));
|
||||
assertFalse(ImageResourceLoader.supportedDimensions(0, 1));
|
||||
}
|
||||
}
|
||||
@@ -407,6 +407,54 @@ public class PackDownloaderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void networkFailureReportsItsCauseAndReopensAdmission() throws Exception {
|
||||
File packsFolder = temp.newFolder("failed-download-packs");
|
||||
byte[] followupArchive = packArchive("failed-followup.zip", "failed_followup");
|
||||
AtomicInteger failedRequests = new AtomicInteger();
|
||||
AtomicInteger followupRequests = new AtomicInteger();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/missing.zip", exchange -> {
|
||||
failedRequests.incrementAndGet();
|
||||
exchange.sendResponseHeaders(404, -1L);
|
||||
exchange.close();
|
||||
});
|
||||
server.createContext("/followup.zip", exchange -> {
|
||||
followupRequests.incrementAndGet();
|
||||
exchange.sendResponseHeaders(200, followupArchive.length);
|
||||
exchange.getResponseBody().write(followupArchive);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
String baseUrl = "http://127.0.0.1:" + server.getAddress().getPort();
|
||||
|
||||
IOException failure = assertThrows(IOException.class, () -> PackDownloader.downloadUrl(
|
||||
packsFolder,
|
||||
baseUrl + "/missing.zip",
|
||||
false,
|
||||
ignored -> {
|
||||
}
|
||||
));
|
||||
|
||||
assertTrue(failure.getMessage().contains("HTTP 404"));
|
||||
assertEquals(1, failedRequests.get());
|
||||
PackDownloader.PackInstallResult followup = PackDownloader.downloadUrl(
|
||||
packsFolder,
|
||||
baseUrl + "/followup.zip",
|
||||
false,
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
assertNotNull(followup);
|
||||
assertEquals("failed_followup", followup.key());
|
||||
assertEquals(1, followupRequests.get());
|
||||
assertEquals(0, PackDownloader.downloadLockCount());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builtInPackPresenceRequiresItsPrimaryDimension() throws Exception {
|
||||
File packsFolder = temp.newFolder("managed-presence");
|
||||
|
||||
@@ -62,6 +62,11 @@ public class PackExportClosureTest {
|
||||
assertTrue("Bukkit packager must export region objects alongside biome objects",
|
||||
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
|
||||
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
|
||||
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
|
||||
assertTrue("Bukkit packager must validate before opening or mutating package state",
|
||||
bukkitValidation >= 0
|
||||
&& bukkitValidation < bukkit.indexOf("IrisData.openRuntime(project.getPath())")
|
||||
&& bukkitValidation < bukkit.indexOf("IO.delete(folder)"));
|
||||
|
||||
String modded = Files.readString(Path.of(
|
||||
"../adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java")).replace("\r\n", "\n");
|
||||
@@ -69,5 +74,8 @@ public class PackExportClosureTest {
|
||||
assertTrue("modded packager must write markers/", modded.contains("\"markers\""));
|
||||
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
|
||||
assertTrue("modded packager must export region objects", modded.contains("region.getObjects()"));
|
||||
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
|
||||
assertTrue("modded packager must validate before mutating package state",
|
||||
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PackImageMapValidatorTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisPlatform previousPlatform;
|
||||
|
||||
@Before
|
||||
public void isolatePlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsCompiledTypedBindingsThroughPackValidator() throws Exception {
|
||||
File pack = createPack("valid", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 16},
|
||||
"imageMaps": [
|
||||
{
|
||||
"key": "terrain",
|
||||
"map": "terrain",
|
||||
"application": "TERRAIN_HEIGHT",
|
||||
"masks": [{"map": "weight", "operation": "MULTIPLY"}]
|
||||
},
|
||||
{"key": "weight", "map": "weight", "application": "MASK"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "terrain", 16, 16);
|
||||
writeGray(pack, "weight", 16, 16);
|
||||
write(pack, "image-maps/terrain.json", """
|
||||
{
|
||||
"source": "terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"origin": {"x": -8, "z": -8},
|
||||
"outOfBounds": "ERROR"
|
||||
}
|
||||
""");
|
||||
write(pack, "image-maps/weight.json", """
|
||||
{
|
||||
"source": "weight",
|
||||
"type": "GRAYSCALE_MASK",
|
||||
"origin": {"x": -8, "z": -8},
|
||||
"outOfBounds": "CLAMP"
|
||||
}
|
||||
""");
|
||||
|
||||
PackValidationResult result = PackValidator.validateForDatapackBootstrap(pack);
|
||||
|
||||
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
assertFalse(result.getWarnings().toString(), contains(result.getWarnings(), "image-map"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enforcesErrorCoverageAndWarnsForRecoverablePolicies() throws Exception {
|
||||
File noBoundary = createPack("no-boundary", dimensionWithTerrain(null));
|
||||
writeGray(noBoundary, "terrain", 2, 2);
|
||||
write(noBoundary, "image-maps/terrain.json", heightMap("ERROR"));
|
||||
|
||||
PackImageMapValidator.Validation missing = validate(noBoundary, false);
|
||||
|
||||
assertTrue(missing.errors().toString(), contains(missing.errors(),
|
||||
"outOfBounds=ERROR and requires a configured worldBoundary"));
|
||||
|
||||
File uncoveredError = createPack("uncovered-error", dimensionWithTerrain(16));
|
||||
writeGray(uncoveredError, "terrain", 2, 2);
|
||||
write(uncoveredError, "image-maps/terrain.json", heightMap("ERROR"));
|
||||
|
||||
PackImageMapValidator.Validation errorCoverage = validate(uncoveredError, false);
|
||||
|
||||
assertTrue(errorCoverage.errors().toString(), contains(errorCoverage.errors(),
|
||||
"source footprint does not cover the configured worldBoundary"));
|
||||
|
||||
File uncoveredClamp = createPack("uncovered-clamp", dimensionWithTerrain(16));
|
||||
writeGray(uncoveredClamp, "terrain", 2, 2);
|
||||
write(uncoveredClamp, "image-maps/terrain.json", heightMap("CLAMP"));
|
||||
|
||||
PackImageMapValidator.Validation clampCoverage = validate(uncoveredClamp, false);
|
||||
|
||||
assertTrue(clampCoverage.errors().toString(), clampCoverage.errors().isEmpty());
|
||||
assertTrue(clampCoverage.warnings().toString(), contains(clampCoverage.warnings(),
|
||||
"source footprint does not cover the configured worldBoundary"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsBindingReferenceAndApplicationFailuresDeterministically() throws Exception {
|
||||
File pack = createPack("bindings", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"imageMaps": [
|
||||
{
|
||||
"key": "duplicate",
|
||||
"map": "height",
|
||||
"application": "TERRAIN_HEIGHT",
|
||||
"masks": [
|
||||
{"map": "duplicate", "operation": "MULTIPLY"},
|
||||
{"map": "missing-mask", "operation": "MULTIPLY"}
|
||||
]
|
||||
},
|
||||
{"key": "duplicate", "map": "height-two", "application": "TERRAIN_HEIGHT"},
|
||||
{"key": "biome", "map": "height", "application": "BIOME"},
|
||||
{"key": "missing-map", "map": "absent", "application": "CUSTOM"},
|
||||
{
|
||||
"key": "mask",
|
||||
"map": "mask",
|
||||
"application": "MASK",
|
||||
"masks": [{"map": "mask", "operation": "MULTIPLY"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "height", 2, 2);
|
||||
writeGray(pack, "height-two", 2, 2);
|
||||
writeGray(pack, "mask", 2, 2);
|
||||
write(pack, "image-maps/height.json", heightMap("CLAMP"));
|
||||
write(pack, "image-maps/height-two.json", """
|
||||
{"source": "height-two", "type": "GRAYSCALE_HEIGHT", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
write(pack, "image-maps/mask.json", """
|
||||
{"source": "mask", "type": "GRAYSCALE_MASK", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation first = validate(pack, false);
|
||||
PackImageMapValidator.Validation second = validate(pack, false);
|
||||
|
||||
assertEquals(first, second);
|
||||
assertTrue(first.errors().toString(), contains(first.errors(), "duplicate image-map key 'duplicate'"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(),
|
||||
"more than one TERRAIN_HEIGHT image-map binding"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(), "incompatible with BIOME"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(),
|
||||
"references missing image-map resource 'absent'"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(), "which is not a MASK binding"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(),
|
||||
"references missing MASK binding 'missing-mask'"));
|
||||
assertTrue(first.errors().toString(), contains(first.errors(),
|
||||
"is a MASK binding and cannot reference additional masks"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesEveryFirstClassResourceBeforeItIsBound() throws Exception {
|
||||
File pack = createPack("resources", "{\"regions\":[\"region\"]}");
|
||||
writeGray(pack, "grayscale", 2, 2);
|
||||
writeRgb(pack, "not-png", 1, 1, "jpeg");
|
||||
Path corrupt = pack.toPath().resolve("images/corrupt.png");
|
||||
Files.createDirectories(corrupt.getParent());
|
||||
Files.write(corrupt, new byte[]{
|
||||
(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x01, 0x02
|
||||
});
|
||||
write(pack, "image-maps/wrong-type.json", """
|
||||
{"source": "grayscale", "type": "RGB_HEIGHT", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
write(pack, "image-maps/wrong-format.json", """
|
||||
{"source": "not-png", "type": "RGB_HEIGHT", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
write(pack, "image-maps/missing-source.json", """
|
||||
{"source": "absent", "type": "GRAYSCALE_HEIGHT", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
write(pack, "image-maps/corrupt-source.json", """
|
||||
{"source": "corrupt", "type": "GRAYSCALE_HEIGHT", "outOfBounds": "CLAMP"}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, false);
|
||||
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"RGB_HEIGHT requires an RGB or RGBA PNG"));
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(), "is not a PNG file"));
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"references missing PNG source 'absent'"));
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(), "is corrupt or unsupported"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesLegendFilesMappedRegionRolesAndLiveSurfaceBlocks() throws Exception {
|
||||
File pack = createPack("legends", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"imageMaps": [
|
||||
{"key": "biome", "map": "biome-map", "application": "BIOME"},
|
||||
{"key": "region", "map": "region-map", "application": "REGION"},
|
||||
{"key": "surface", "map": "surface-map", "application": "SURFACE_BLOCK"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeRgb(pack, "biome", 1, 1, "png");
|
||||
writeRgb(pack, "region-map", 1, 1, "png");
|
||||
writeRgb(pack, "surface", 1, 1, "png");
|
||||
write(pack, "biomes/mapped-biome.json", "{\"name\":\"Mapped\"}");
|
||||
write(pack, "regions/mapped-region.json", "{\"landBiomes\":[\"mapped-biome\"]}");
|
||||
write(pack, "image-maps/biome-map.json", colorMap("biome", "mapped-biome"));
|
||||
write(pack, "image-maps/region-map.json", colorMap("region-map", "mapped-region"));
|
||||
write(pack, "image-maps/surface-map.json", colorMap("surface", "minecraft:not_real"));
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
when(registries.blockOrNull("minecraft:not_real", false)).thenReturn(null);
|
||||
when(registries.blockStateProperties()).thenReturn(Map.of());
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, true);
|
||||
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"biome target 'mapped-biome' must occur"));
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"unknown surface block target 'minecraft:not_real'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesReachableGeneratorStyleMapReferences() throws Exception {
|
||||
File missingPack = createPack("generator-style-missing", "{\"regions\":[\"region\"]}");
|
||||
write(missingPack, "biomes/biome.json", """
|
||||
{"name":"Biome","generators":[{"generator":"mapped","min":0,"max":1}]}
|
||||
""");
|
||||
write(missingPack, "generators/mapped.json", """
|
||||
{"composite":[{"style":{"imageMap":"absent"}}]}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation missing = validate(missingPack, false);
|
||||
|
||||
assertTrue(missing.errors().toString(), contains(missing.errors(),
|
||||
"generator style references missing image-map resource 'absent'"));
|
||||
|
||||
File colorPack = createPack("generator-style-color", """
|
||||
{"regions":["region"],"regionStyle":{"imageMap":"colors"}}
|
||||
""");
|
||||
writeRgb(colorPack, "colors", 1, 1, "png");
|
||||
write(colorPack, "image-maps/colors.json", colorMap("colors", "biome"));
|
||||
|
||||
PackImageMapValidator.Validation color = validate(colorPack, false);
|
||||
|
||||
assertTrue(color.errors().toString(), contains(color.errors(),
|
||||
"must use a scalar map type, not COLOR_MAP"));
|
||||
|
||||
File errorPack = createPack("generator-style-error", """
|
||||
{"regions":["region"],"regionStyle":{"imageMap":"height"}}
|
||||
""");
|
||||
writeGray(errorPack, "height", 4, 4);
|
||||
write(errorPack, "image-maps/height.json", """
|
||||
{"source":"height","type":"GRAYSCALE_HEIGHT","outOfBounds":"ERROR"}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation error = validate(errorPack, false);
|
||||
|
||||
assertTrue(error.errors().toString(), contains(error.errors(),
|
||||
"cannot use outOfBounds=ERROR because its transformed sampling domain cannot be proven finite"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesUpperDimensionCoverageAgainstParentBoundary() throws Exception {
|
||||
File pack = createPack("upper-parent-boundary", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 16},
|
||||
"upperDimension": "upper"
|
||||
}
|
||||
""");
|
||||
write(pack, "dimensions/upper.json", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 2},
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "upper-terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "upper-terrain", 4, 4);
|
||||
write(pack, "image-maps/upper-terrain.json", """
|
||||
{
|
||||
"source": "upper-terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"origin": {"x": -2, "z": -2},
|
||||
"outOfBounds": "ERROR"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, false);
|
||||
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'main' upper dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
|
||||
File pack = createPack("upper-parent-only-boundary", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 4},
|
||||
"upperDimension": "upper"
|
||||
}
|
||||
""");
|
||||
write(pack, "dimensions/upper.json", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "upper-terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "upper-terrain", 4, 4);
|
||||
write(pack, "image-maps/upper-terrain.json", """
|
||||
{
|
||||
"source": "upper-terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"origin": {"x": -2, "z": -2},
|
||||
"outOfBounds": "ERROR"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, false);
|
||||
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"requires a configured worldBoundary"));
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"source footprint does not cover"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesDeclaredUpperBoundaryAlongsideParentBoundary() throws Exception {
|
||||
File pack = createPack("upper-own-and-parent-boundary", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 4},
|
||||
"upperDimension": "upper"
|
||||
}
|
||||
""");
|
||||
write(pack, "dimensions/upper.json", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 16},
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "upper-terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(pack, "upper-terrain", 4, 4);
|
||||
write(pack, "image-maps/upper-terrain.json", """
|
||||
{
|
||||
"source": "upper-terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"origin": {"x": -2, "z": -2},
|
||||
"outOfBounds": "ERROR"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation validation = validate(pack, false);
|
||||
|
||||
assertTrue(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
assertFalse(validation.errors().toString(), contains(validation.errors(),
|
||||
"Dimension 'main' upper dimension 'upper' image-map 'terrain' source footprint does not cover"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enforcesTerrainHeightRangeAfterOffsetAndClamp() throws Exception {
|
||||
File unclamped = createPack("height-unclamped", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": 0, "max": 64},
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(unclamped, "terrain", 2, 2);
|
||||
write(unclamped, "image-maps/terrain.json", """
|
||||
{
|
||||
"source": "terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"minimumHeight": 0,
|
||||
"maximumHeight": 64,
|
||||
"verticalOffset": 10,
|
||||
"clamp": false,
|
||||
"outOfBounds": "CLAMP"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation outside = validate(unclamped, false);
|
||||
|
||||
assertTrue(outside.errors().toString(), contains(outside.errors(),
|
||||
"produces world Y 10.0..74.0 after verticalOffset and clamp"));
|
||||
|
||||
File clamped = createPack("height-clamped", """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": 0, "max": 64},
|
||||
"imageMaps": [
|
||||
{"key": "terrain", "map": "terrain", "application": "TERRAIN_HEIGHT"}
|
||||
]
|
||||
}
|
||||
""");
|
||||
writeGray(clamped, "terrain", 2, 2);
|
||||
write(clamped, "image-maps/terrain.json", """
|
||||
{
|
||||
"source": "terrain",
|
||||
"type": "GRAYSCALE_HEIGHT",
|
||||
"minimumHeight": 0,
|
||||
"maximumHeight": 64,
|
||||
"verticalOffset": 10,
|
||||
"clamp": true,
|
||||
"outOfBounds": "CLAMP"
|
||||
}
|
||||
""");
|
||||
|
||||
PackImageMapValidator.Validation inside = validate(clamped, false);
|
||||
|
||||
assertFalse(inside.errors().toString(), contains(inside.errors(), "produces world Y"));
|
||||
}
|
||||
|
||||
private PackImageMapValidator.Validation validate(File pack, boolean liveRegistries) {
|
||||
File dimensions = new File(pack, "dimensions");
|
||||
File[] dimensionFiles = dimensions.listFiles((File file) -> file.isFile()
|
||||
&& file.getName().endsWith(".json"));
|
||||
assertTrue(dimensionFiles != null && dimensionFiles.length > 0);
|
||||
return PackImageMapValidator.validate(pack, dimensionFiles, liveRegistries);
|
||||
}
|
||||
|
||||
private File createPack(String name, String dimension) throws Exception {
|
||||
File pack = temporaryFolder.newFolder(name);
|
||||
write(pack, "dimensions/main.json", dimension);
|
||||
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private String dimensionWithTerrain(Integer boundarySize) {
|
||||
String boundary = boundarySize == null
|
||||
? ""
|
||||
: ",\"worldBoundary\":{\"center\":{\"x\":0,\"z\":0},\"size\":" + boundarySize + "}";
|
||||
return "{\"regions\":[\"region\"]" + boundary
|
||||
+ ",\"imageMaps\":[{\"key\":\"terrain\",\"map\":\"terrain\","
|
||||
+ "\"application\":\"TERRAIN_HEIGHT\"}]}";
|
||||
}
|
||||
|
||||
private String heightMap(String outOfBounds) {
|
||||
return "{\"source\":\"terrain\",\"type\":\"GRAYSCALE_HEIGHT\","
|
||||
+ "\"outOfBounds\":\"" + outOfBounds + "\"}";
|
||||
}
|
||||
|
||||
private String colorMap(String source, String target) {
|
||||
return "{\"source\":\"" + source + "\",\"type\":\"COLOR_MAP\","
|
||||
+ "\"outOfBounds\":\"CLAMP\",\"unknownColor\":\"IGNORE\","
|
||||
+ "\"colors\":{\"#FF0000\":\"" + target + "\"}}";
|
||||
}
|
||||
|
||||
private boolean contains(List<String> messages, String fragment) {
|
||||
return messages.stream().anyMatch((String message) -> message.contains(fragment));
|
||||
}
|
||||
|
||||
private void write(File root, String relative, String content) throws Exception {
|
||||
Path path = root.toPath().resolve(relative);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void writeGray(File pack, String key, int width, int height) throws Exception {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
|
||||
File file = imageFile(pack, key);
|
||||
assertTrue(ImageIO.write(image, "png", file));
|
||||
}
|
||||
|
||||
private void writeRgb(File pack, String key, int width, int height, String format) throws Exception {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
for (int z = 0; z < height; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.setRGB(x, z, 0xFFFF0000);
|
||||
}
|
||||
}
|
||||
File file = imageFile(pack, key);
|
||||
assertTrue(ImageIO.write(image, format, file));
|
||||
}
|
||||
|
||||
private File imageFile(File pack, String key) throws Exception {
|
||||
File images = new File(pack, "images");
|
||||
Files.createDirectories(images.toPath());
|
||||
return new File(images, key + ".png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackValidatorWorldBoundaryTest {
|
||||
@Test
|
||||
public void acceptsAbsentAndValidBoundaries() {
|
||||
List<String> absentErrors = validate("{}");
|
||||
List<String> configuredErrors = validate("{\"worldBoundary\":{\"center\":{\"x\":12.5,\"z\":-8},"
|
||||
+ "\"size\":16384,\"warningDistance\":16,\"damageBuffer\":5,\"damageAmount\":0.2}}");
|
||||
|
||||
assertTrue(absentErrors.toString(), absentErrors.isEmpty());
|
||||
assertTrue(configuredErrors.toString(), configuredErrors.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedBoundaryObjects() {
|
||||
assertEquals(List.of("Dimension 'main' worldBoundary must be an object."),
|
||||
validate("{\"worldBoundary\":null}"));
|
||||
assertEquals(List.of("Dimension 'main' worldBoundary must be an object."),
|
||||
validate("{\"worldBoundary\":42}"));
|
||||
assertEquals(List.of("Dimension 'main' worldBoundary.center must be an object."),
|
||||
validate("{\"worldBoundary\":{\"center\":null}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsValuesOutsideNativeLimits() {
|
||||
List<String> errors = validate("{\"worldBoundary\":{\"center\":{\"x\":29999985,\"z\":-29999985},"
|
||||
+ "\"size\":59999969,\"warningDistance\":1.5,\"damageBuffer\":-1,\"damageAmount\":-0.1}}");
|
||||
|
||||
assertEquals(6, errors.size());
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.size")));
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.warningDistance")));
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.damageBuffer")));
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.damageAmount")));
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.center.x")));
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch((String error) -> error.contains("worldBoundary.center.z")));
|
||||
}
|
||||
|
||||
private static List<String> validate(String json) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
PackDimensionValidator.validateWorldBoundary("main", new JSONObject(json), errors);
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapSampling;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class ImageMapStudioExporterTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void previewUsesTheRuntimeCompiler() throws Exception {
|
||||
File source = temporaryFolder.newFile("height.png");
|
||||
BufferedImage image = new BufferedImage(2, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
image.getRaster().setSample(0, 0, 0, 0);
|
||||
image.getRaster().setSample(1, 0, 0, 255);
|
||||
ImageIO.write(image, "png", source);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("height")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setMinimumHeight(-64D)
|
||||
.setMaximumHeight(320D);
|
||||
|
||||
ImageMapStudioExporter.PreviewResult preview = ImageMapStudioExporter.preview(
|
||||
source.toPath(), definition
|
||||
);
|
||||
|
||||
assertEquals(2, preview.compiled().getSourceWidth());
|
||||
assertEquals(-64D, preview.compiled().sampleHeight(0D, 0D), 0D);
|
||||
assertEquals(320D, preview.compiled().sampleHeight(1D, 0D), 0D);
|
||||
assertFalse(preview.colorProfile().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inspectsSourceMetadataWithoutACompatibleSemanticType() throws Exception {
|
||||
File source = temporaryFolder.newFile("inspect-rgb.png");
|
||||
BufferedImage image = new BufferedImage(2, 3, BufferedImage.TYPE_INT_ARGB);
|
||||
image.setRGB(0, 0, 0x40FF0000);
|
||||
image.setRGB(1, 2, 0xFFFF0000);
|
||||
ImageIO.write(image, "png", source);
|
||||
|
||||
ImageMapStudioExporter.SourceInspection inspection = ImageMapStudioExporter.inspectSource(source.toPath());
|
||||
IrisImage inspected = new IrisImage(inspection.source(), inspection.format());
|
||||
|
||||
assertEquals(2, inspected.getWidth());
|
||||
assertEquals(3, inspected.getHeight());
|
||||
assertTrue(inspected.hasAlpha());
|
||||
assertTrue(inspection.minimumAlpha() < 1D);
|
||||
assertEquals(1D, inspection.maximumAlpha(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readsEmbeddedPngColorProfileMetadata() throws Exception {
|
||||
File source = temporaryFolder.newFile("profile.png");
|
||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
ImageIO.write(image, "png", source);
|
||||
byte[] original = Files.readAllBytes(source.toPath());
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
output.write(original, 0, 33);
|
||||
output.write(pngChunk("sRGB", new byte[]{0}));
|
||||
output.write(original, 33, original.length - 33);
|
||||
Files.write(source.toPath(), output.toByteArray());
|
||||
|
||||
ImageMapStudioExporter.SourceInspection inspection = ImageMapStudioExporter.inspectSource(source.toPath());
|
||||
|
||||
assertTrue(inspection.colorProfile().startsWith("sRGB:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsOversizedPngDimensionsBeforePixelDecode() throws Exception {
|
||||
File source = temporaryFolder.newFile("oversized.png");
|
||||
writeHeaderOnlyPng(source, 16_385, 1);
|
||||
|
||||
IrisImageMapValidationException failure = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> ImageMapStudioExporter.inspectSource(source.toPath())
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("Image width must be 1..16384"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsLossyInputsBeforeProjectMutation() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack");
|
||||
File source = temporaryFolder.newFile("height.jpg");
|
||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
ImageIO.write(image, "jpg", source);
|
||||
ImageMapStudioExporter.ExportRequest request = new ImageMapStudioExporter.ExportRequest(
|
||||
pack,
|
||||
"overworld",
|
||||
"terrain",
|
||||
IrisImageMapApplication.TERRAIN_HEIGHT,
|
||||
"terrain",
|
||||
"terrain",
|
||||
new IrisImageMap().setType(IrisImageMapType.RGB_HEIGHT),
|
||||
source.toPath(),
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertThrows(IOException.class, () -> ImageMapStudioExporter.export(request));
|
||||
assertFalse(new File(pack, "images").exists());
|
||||
assertFalse(new File(pack, "image-maps").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsIncompatibleDefinitionsBeforeProjectMutation() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("invalid-pack");
|
||||
File source = temporaryFolder.newFile("map.png");
|
||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
ImageIO.write(image, "png", source);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setType(IrisImageMapType.COLOR_MAP)
|
||||
.setSampling(IrisImageMapSampling.BILINEAR);
|
||||
ImageMapStudioExporter.ExportRequest request = new ImageMapStudioExporter.ExportRequest(
|
||||
pack,
|
||||
"overworld",
|
||||
"biomes",
|
||||
IrisImageMapApplication.BIOME,
|
||||
"biomes",
|
||||
"biomes",
|
||||
definition,
|
||||
source.toPath(),
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertThrows(IrisImageMapValidationException.class, () -> ImageMapStudioExporter.export(request));
|
||||
assertFalse(new File(pack, "images").exists());
|
||||
assertFalse(new File(pack, "image-maps").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exportsAndReloadsAValidatedRuntimeBinding() throws Exception {
|
||||
File pack = minimalPack("export-pack");
|
||||
File source = temporaryFolder.newFile("export-height.png");
|
||||
BufferedImage image = new BufferedImage(2, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
image.getRaster().setSample(1, 0, 0, 255);
|
||||
ImageIO.write(image, "png", source);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
ImageMapStudioExporter.ExportRequest request = new ImageMapStudioExporter.ExportRequest(
|
||||
pack,
|
||||
"main",
|
||||
"terrain",
|
||||
IrisImageMapApplication.TERRAIN_HEIGHT,
|
||||
"terrain",
|
||||
"terrain",
|
||||
definition,
|
||||
source.toPath(),
|
||||
List.of()
|
||||
);
|
||||
|
||||
ImageMapStudioExporter.ExportResult result = ImageMapStudioExporter.export(request);
|
||||
|
||||
assertTrue(Files.isRegularFile(result.imageFile()));
|
||||
assertTrue(Files.isRegularFile(result.imageMapFile()));
|
||||
assertTrue(Files.readString(result.dimensionFile()).contains("\"key\": \"terrain\""));
|
||||
assertEquals(64, result.contentHash().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restoresEveryExistingTargetWhenPublishedRuntimeValidationFails() throws Exception {
|
||||
File pack = minimalPack("rollback-pack");
|
||||
File source = temporaryFolder.newFile("rollback-color.png");
|
||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
image.setRGB(0, 0, 0xFFFF0000);
|
||||
ImageIO.write(image, "png", source);
|
||||
File imageTarget = write(pack, "images/biomes.png", "old-image");
|
||||
File mapTarget = write(pack, "image-maps/biomes.json", "old-map");
|
||||
String originalDimension = Files.readString(new File(pack, "dimensions/main.json").toPath());
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setType(IrisImageMapType.COLOR_MAP)
|
||||
.setColors(new KMap<>(Map.of("#FF0000", "iris:missing")));
|
||||
ImageMapStudioExporter.ExportRequest request = new ImageMapStudioExporter.ExportRequest(
|
||||
pack,
|
||||
"main",
|
||||
"biomes",
|
||||
IrisImageMapApplication.BIOME,
|
||||
"biomes",
|
||||
"biomes",
|
||||
definition,
|
||||
source.toPath(),
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> ImageMapStudioExporter.export(request));
|
||||
|
||||
assertEquals("old-image", Files.readString(imageTarget.toPath()));
|
||||
assertEquals("old-map", Files.readString(mapTarget.toPath()));
|
||||
assertEquals(originalDimension, Files.readString(new File(pack, "dimensions/main.json").toPath()));
|
||||
}
|
||||
|
||||
private File minimalPack(String name) throws Exception {
|
||||
File pack = temporaryFolder.newFolder(name);
|
||||
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
|
||||
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private File write(File root, String relative, String content) throws Exception {
|
||||
File target = new File(root, relative);
|
||||
Files.createDirectories(target.toPath().getParent());
|
||||
Files.writeString(target.toPath(), content, StandardCharsets.UTF_8);
|
||||
return target;
|
||||
}
|
||||
|
||||
private void writeHeaderOnlyPng(File target, int width, int height) throws Exception {
|
||||
ByteArrayOutputStream headerBytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream header = new DataOutputStream(headerBytes)) {
|
||||
header.writeInt(width);
|
||||
header.writeInt(height);
|
||||
header.writeByte(8);
|
||||
header.writeByte(2);
|
||||
header.writeByte(0);
|
||||
header.writeByte(0);
|
||||
header.writeByte(0);
|
||||
}
|
||||
ByteArrayOutputStream png = new ByteArrayOutputStream();
|
||||
png.write(new byte[]{(byte) 137, 80, 78, 71, 13, 10, 26, 10});
|
||||
png.write(pngChunk("IHDR", headerBytes.toByteArray()));
|
||||
png.write(pngChunk("IEND", new byte[0]));
|
||||
Files.write(target.toPath(), png.toByteArray());
|
||||
}
|
||||
|
||||
private byte[] pngChunk(String type, byte[] data) throws Exception {
|
||||
byte[] typeBytes = type.getBytes(StandardCharsets.US_ASCII);
|
||||
CRC32 checksum = new CRC32();
|
||||
checksum.update(typeBytes);
|
||||
checksum.update(data);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
output.writeInt(data.length);
|
||||
output.write(typeBytes);
|
||||
output.write(data);
|
||||
output.writeInt((int) checksum.getValue());
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapBinding;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisImageMapSchemaTest {
|
||||
@Test
|
||||
public void dimensionAndGeneratorExposeResourceReferences() throws Exception {
|
||||
Field bindings = IrisDimension.class.getDeclaredField("imageMaps");
|
||||
Field generatorMap = IrisGeneratorStyle.class.getDeclaredField("imageMap");
|
||||
|
||||
assertEquals(IrisImageMapBinding.class, bindings.getAnnotation(ArrayType.class).type());
|
||||
assertEquals(String.class, generatorMap.getType());
|
||||
assertEquals("image-maps", new IrisImageMap().getFolderName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemasExposeTypedMapAndBindingObjects() {
|
||||
IrisData data = schemaData();
|
||||
JSONObject bindingSchema = new SchemaBuilder(IrisImageMapBinding.class, data).construct();
|
||||
JSONObject mapSchema = new SchemaBuilder(IrisImageMap.class, data).construct();
|
||||
|
||||
assertTrue(bindingSchema.getJSONObject("properties").has("application"));
|
||||
assertTrue(bindingSchema.getJSONObject("properties").has("map"));
|
||||
assertTrue(mapSchema.getJSONObject("properties").has("type"));
|
||||
assertTrue(mapSchema.getJSONObject("properties").has("source"));
|
||||
assertTrue(mapSchema.getJSONObject("properties").has("colors"));
|
||||
assertTrue(mapSchema.getJSONObject("properties").has("blocksPerPixel"));
|
||||
JSONObject properties = mapSchema.getJSONObject("properties");
|
||||
assertEquals(IrisImageMap.MINIMUM_SCALE,
|
||||
properties.getJSONObject("blocksPerPixel").getDouble("minimum"), 0D);
|
||||
assertEquals(IrisImageMap.MINIMUM_SCALE,
|
||||
properties.getJSONObject("curveExponent").getDouble("minimum"), 0D);
|
||||
assertEquals(IrisImageMap.MAXIMUM_COLOR_TOLERANCE,
|
||||
properties.getJSONObject("colorTolerance").getDouble("maximum"), 0D);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData schemaData() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
when(data.getLoaders()).thenReturn(loaders);
|
||||
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundary;
|
||||
import art.arcane.iris.engine.object.IrisWorldBoundaryCenter;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class IrisWorldBoundarySchemaTest {
|
||||
@Test
|
||||
public void dimensionExposesOptionalTypedBoundary() throws Exception {
|
||||
Field boundary = IrisDimension.class.getDeclaredField("worldBoundary");
|
||||
|
||||
assertEquals(IrisWorldBoundary.class, boundary.getType());
|
||||
assertNull(new IrisDimension().getWorldBoundary());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemaExposesNativeBoundaryLimits() {
|
||||
JSONObject schema = new SchemaBuilder(IrisWorldBoundary.class, null).construct();
|
||||
JSONObject definitions = schema.getJSONObject("definitions");
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
JSONObject centerReference = properties.getJSONObject("center");
|
||||
String centerKey = centerReference.getString("$ref").substring("#/definitions/".length());
|
||||
JSONObject center = definitions.getJSONObject(centerKey).getJSONObject("properties");
|
||||
|
||||
assertEquals("number", properties.getJSONObject("size").getString("type"));
|
||||
assertEquals(1D, properties.getJSONObject("size").getDouble("minimum"), 0D);
|
||||
assertEquals(IrisWorldBoundary.MAXIMUM_SIZE,
|
||||
properties.getJSONObject("size").getDouble("maximum"), 0D);
|
||||
assertEquals(Integer.MAX_VALUE, properties.getJSONObject("warningDistance").getInt("maximum"));
|
||||
assertEquals(-IrisWorldBoundary.MAXIMUM_CENTER, center.getJSONObject("x").getDouble("minimum"), 0D);
|
||||
assertEquals(IrisWorldBoundary.MAXIMUM_CENTER, center.getJSONObject("z").getDouble("maximum"), 0D);
|
||||
assertEquals(IrisWorldBoundaryCenter.class,
|
||||
new IrisWorldBoundary().getCenter().getClass());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class UpperDimensionImageMapRuntimeTest {
|
||||
@Test
|
||||
public void mappedRegionAndBiomeOverrideProceduralSelections() {
|
||||
IrisImageMapRuntime runtime = mock(IrisImageMapRuntime.class);
|
||||
IrisRegion proceduralRegion = new IrisRegion();
|
||||
IrisRegion mappedRegion = new IrisRegion();
|
||||
IrisBiome proceduralBiome = new IrisBiome();
|
||||
IrisBiome mappedBiome = new IrisBiome();
|
||||
when(runtime.sampleRegion(12D, -7D)).thenReturn(mappedRegion);
|
||||
when(runtime.sampleBiome(12D, -7D)).thenReturn(mappedBiome);
|
||||
|
||||
assertSame(mappedRegion, UpperDimensionContext.mappedRegion(
|
||||
runtime, proceduralRegion, 12D, -7D));
|
||||
assertSame(mappedBiome, UpperDimensionContext.mappedBiome(
|
||||
runtime, proceduralBiome, 12D, -7D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentMappingsPreserveProceduralSelections() {
|
||||
IrisImageMapRuntime runtime = mock(IrisImageMapRuntime.class);
|
||||
IrisRegion proceduralRegion = new IrisRegion();
|
||||
IrisBiome proceduralBiome = new IrisBiome();
|
||||
PlatformBlockState proceduralBlock = mock(PlatformBlockState.class);
|
||||
|
||||
assertSame(proceduralRegion, UpperDimensionContext.mappedRegion(
|
||||
runtime, proceduralRegion, -2D, 4D));
|
||||
assertSame(proceduralBiome, UpperDimensionContext.mappedBiome(
|
||||
runtime, proceduralBiome, -2D, 4D));
|
||||
assertSame(proceduralBlock, UpperDimensionContext.mappedSurfaceBlock(
|
||||
runtime, proceduralBlock, -2D, 4D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mappedHeightAndSurfaceBlockAreAuthoritative() {
|
||||
IrisImageMapRuntime runtime = mock(IrisImageMapRuntime.class);
|
||||
PlatformBlockState proceduralBlock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState mappedBlock = mock(PlatformBlockState.class);
|
||||
when(runtime.sampleTerrainHeight(3D, 9D, 80D)).thenReturn(144D);
|
||||
when(runtime.sampleSurfaceBlock(3D, 9D)).thenReturn(mappedBlock);
|
||||
|
||||
assertEquals(144D, UpperDimensionContext.mappedTerrainHeight(
|
||||
runtime, 80D, 3D, 9D), 0D);
|
||||
assertSame(mappedBlock, UpperDimensionContext.mappedSurfaceBlock(
|
||||
runtime, proceduralBlock, 3D, 9D));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapAlpha;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOrigin;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapRotation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapSampling;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.iris.engine.object.IrisImageMapUnknownColor;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisImageMapCompilerTest {
|
||||
private static final double EPSILON = 0.000001D;
|
||||
|
||||
@Test
|
||||
public void decodesEightAndSixteenBitGrayscaleEndpoints() {
|
||||
CompiledIrisImageMap eightBit = compile(
|
||||
scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT),
|
||||
grayscale8(new int[][]{{0, 255}})
|
||||
);
|
||||
CompiledIrisImageMap sixteenBit = compile(
|
||||
scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT),
|
||||
grayscale16(new int[][]{{0, 32768, 65535}})
|
||||
);
|
||||
|
||||
assertEquals(0D, eightBit.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(1D, eightBit.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(0D, sixteenBit.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(32768D / 65535D, sixteenBit.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(1D, sixteenBit.sampleNormalized(2D, 0D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodesCanonicalRgbHeightAndRawChannels() {
|
||||
BufferedImage image = rgb(new int[][]{{0x000000, 0x123456, 0xFFFFFF}});
|
||||
CompiledIrisImageMap compiled = compile(scalarDefinition(IrisImageMapType.RGB_HEIGHT), image);
|
||||
|
||||
assertEquals(0D, compiled.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(0x123456 / 16_777_215D, compiled.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(1D, compiled.sampleNormalized(2D, 0D), EPSILON);
|
||||
assertEquals(0x12, new IrisImage(image).getBandSample(1, 0, 0));
|
||||
assertEquals(0x34, new IrisImage(image).getBandSample(1, 0, 1));
|
||||
assertEquals(0x56, new IrisImage(image).getBandSample(1, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freezesDecodedValuesAndHashAtCompileTime() {
|
||||
BufferedImage image = grayscale8(new int[][]{{64}});
|
||||
CompiledIrisImageMap compiled = compile(
|
||||
scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT),
|
||||
image
|
||||
);
|
||||
double initialValue = compiled.sampleNormalized(0D, 0D);
|
||||
String initialHash = compiled.contentHash();
|
||||
|
||||
image.getRaster().setSample(0, 0, 0, 255);
|
||||
|
||||
assertEquals(initialValue, compiled.sampleNormalized(0D, 0D), 0D);
|
||||
assertEquals(initialHash, compiled.contentHash());
|
||||
CompiledIrisImageMap changed = compile(
|
||||
scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT),
|
||||
image
|
||||
);
|
||||
assertNotEquals(initialHash, changed.contentHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsOriginsScaleNegativeCoordinatesAndContainmentWithFloorSemantics() {
|
||||
IrisImageMap definition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setBlocksPerPixel(2D)
|
||||
.setOrigin(new IrisImageMapOrigin(10D, 20D))
|
||||
.setSourceOrigin(new IrisImageMapOrigin(1D, 0D))
|
||||
.setFallbackValue(0.75D);
|
||||
CompiledIrisImageMap compiled = compile(definition, grayscale8(new int[][]{{0, 128, 255}}));
|
||||
|
||||
assertEquals(0D, compiled.sampleNormalized(8D, 20D), EPSILON);
|
||||
assertEquals(128D / 255D, compiled.sampleNormalized(10D, 20D), EPSILON);
|
||||
assertEquals(1D, compiled.sampleNormalized(12D, 20D), EPSILON);
|
||||
assertEquals(0.75D, compiled.sampleNormalized(7.999D, 20D), EPSILON);
|
||||
assertTrue(compiled.containsWorld(8D, 20D));
|
||||
assertFalse(compiled.containsWorld(7.999D, 20D));
|
||||
assertFalse(compiled.containsWorld(Double.POSITIVE_INFINITY, 20D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsEveryQuarterTurnAndMirrorsAroundSourceOrigin() {
|
||||
BufferedImage image = grayscale8(new int[][]{
|
||||
{0, 0, 0},
|
||||
{0, 0, 255},
|
||||
{0, 0, 0}
|
||||
});
|
||||
IrisImageMap definition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSourceOrigin(new IrisImageMapOrigin(1D, 1D));
|
||||
|
||||
assertEquals(1D, compile(definition.setRotation(IrisImageMapRotation.DEG_0), image)
|
||||
.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(1D, compile(definition.setRotation(IrisImageMapRotation.DEG_90), image)
|
||||
.sampleNormalized(0D, 1D), EPSILON);
|
||||
assertEquals(1D, compile(definition.setRotation(IrisImageMapRotation.DEG_180), image)
|
||||
.sampleNormalized(-1D, 0D), EPSILON);
|
||||
assertEquals(1D, compile(definition.setRotation(IrisImageMapRotation.DEG_270), image)
|
||||
.sampleNormalized(0D, -1D), EPSILON);
|
||||
assertEquals(1D, compile(definition
|
||||
.setRotation(IrisImageMapRotation.DEG_90)
|
||||
.setMirrorX(true), image)
|
||||
.sampleNormalized(0D, -1D), EPSILON);
|
||||
|
||||
BufferedImage zImage = grayscale8(new int[][]{
|
||||
{0, 0, 0},
|
||||
{0, 0, 0},
|
||||
{0, 255, 0}
|
||||
});
|
||||
assertEquals(1D, compile(definition
|
||||
.setRotation(IrisImageMapRotation.DEG_0)
|
||||
.setMirrorX(false)
|
||||
.setMirrorZ(true), zImage)
|
||||
.sampleNormalized(0D, -1D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesEveryOutOfBoundsMode() {
|
||||
BufferedImage image = grayscale8(new int[][]{{0, 255}});
|
||||
IrisImageMap definition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT).setFallbackValue(0.25D);
|
||||
|
||||
assertEquals(0.25D, compile(definition.setOutOfBounds(IrisImageMapOutOfBounds.FALLBACK), image)
|
||||
.sampleNormalized(-1D, 0D), EPSILON);
|
||||
assertEquals(0D, compile(definition.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP), image)
|
||||
.sampleNormalized(-1D, 0D), EPSILON);
|
||||
assertEquals(1D, compile(definition.setOutOfBounds(IrisImageMapOutOfBounds.REPEAT), image)
|
||||
.sampleNormalized(-1D, 0D), EPSILON);
|
||||
assertEquals(1D, compile(definition.setOutOfBounds(IrisImageMapOutOfBounds.MIRROR), image)
|
||||
.sampleNormalized(-2D, 0D), EPSILON);
|
||||
assertEquals(1D, compile(definition, image).sampleNormalized(2D, 0D), EPSILON);
|
||||
CompiledIrisImageMap error = compile(definition.setOutOfBounds(IrisImageMapOutOfBounds.ERROR), image);
|
||||
assertThrows(IrisImageMapValidationException.class, () -> error.sampleNormalized(-1D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void samplesBilinearAndBicubicNumericMaps() {
|
||||
IrisImageMap bilinearDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSampling(IrisImageMapSampling.BILINEAR)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
CompiledIrisImageMap bilinear = compile(bilinearDefinition, grayscale8(new int[][]{
|
||||
{0, 255},
|
||||
{255, 0}
|
||||
}));
|
||||
assertEquals(0.5D, bilinear.sampleNormalized(0.5D, 0.5D), EPSILON);
|
||||
|
||||
IrisImageMap bicubicDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSampling(IrisImageMapSampling.BICUBIC)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
CompiledIrisImageMap bicubic = compile(
|
||||
bicubicDefinition,
|
||||
grayscale8(new int[][]{{0, 85, 170, 255}})
|
||||
);
|
||||
assertEquals(0.5D, bicubic.sampleNormalized(1.5D, 0D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsExactErrorSamplingKernelCoverage() {
|
||||
IrisImageMap bilinearDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSampling(IrisImageMapSampling.BILINEAR)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.ERROR);
|
||||
CompiledIrisImageMap bilinear = compile(bilinearDefinition, grayscale8(new int[][]{
|
||||
{0, 255},
|
||||
{255, 0}
|
||||
}));
|
||||
|
||||
assertTrue(bilinear.containsWorldForSampling(0.5D, 0.5D));
|
||||
assertTrue(bilinear.containsWorldForSampling(1D, 1D));
|
||||
assertFalse(bilinear.containsWorldForSampling(1.5D, 0.5D));
|
||||
assertThrows(IrisImageMapValidationException.class, () -> bilinear.sampleNormalized(1.5D, 0.5D));
|
||||
|
||||
IrisImageMap bicubicDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSampling(IrisImageMapSampling.BICUBIC)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.ERROR);
|
||||
CompiledIrisImageMap bicubic = compile(bicubicDefinition, grayscale8(new int[][]{
|
||||
{0, 0, 0, 0, 0},
|
||||
{0, 0, 0, 0, 0},
|
||||
{0, 0, 255, 0, 0},
|
||||
{0, 0, 0, 0, 0},
|
||||
{0, 0, 0, 0, 0}
|
||||
}));
|
||||
|
||||
assertTrue(bicubic.containsWorldForSampling(0D, 0D));
|
||||
assertTrue(bicubic.containsWorldForSampling(1.5D, 1.5D));
|
||||
assertFalse(bicubic.containsWorldForSampling(0.5D, 1.5D));
|
||||
assertFalse(bicubic.containsWorldForSampling(3.5D, 1.5D));
|
||||
assertThrows(IrisImageMapValidationException.class, () -> bicubic.sampleNormalized(0.5D, 1.5D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lightweightValidationViewRetainsCoverageWithoutDecodedValues() {
|
||||
IrisImageMap definition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSampling(IrisImageMapSampling.BILINEAR)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.ERROR);
|
||||
CompiledIrisImageMap compiled = compile(definition, grayscale8(new int[][]{
|
||||
{0, 255},
|
||||
{255, 0}
|
||||
}));
|
||||
CompiledIrisImageMap validationView = compiled.withoutDecodedValues();
|
||||
|
||||
assertEquals(compiled.contentHash(), validationView.contentHash());
|
||||
assertEquals(compiled.getSourceMetadata(), validationView.getSourceMetadata());
|
||||
assertEquals(compiled.containsWorldForSampling(0.5D, 0.5D),
|
||||
validationView.containsWorldForSampling(0.5D, 0.5D));
|
||||
assertEquals(compiled.containsWorldForSampling(1.5D, 0.5D),
|
||||
validationView.containsWorldForSampling(1.5D, 0.5D));
|
||||
assertThrows(IrisImageMapValidationException.class, () -> validationView.sampleNormalized(0D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesLoadTimeSmoothingCurveAndHeightClamp() {
|
||||
IrisImageMap smoothedDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setSmoothingRadius(1)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
CompiledIrisImageMap smoothed = compile(smoothedDefinition, grayscale8(new int[][]{{0, 255, 0}}));
|
||||
assertEquals(1D / 3D, smoothed.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(1D / 3D, smoothed.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(1D / 3D, smoothed.sampleNormalized(2D, 0D), EPSILON);
|
||||
|
||||
IrisImageMap curvedDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setCurveExponent(2D)
|
||||
.setMinimumHeight(10D)
|
||||
.setMaximumHeight(20D)
|
||||
.setVerticalOffset(5D)
|
||||
.setClamp(true);
|
||||
CompiledIrisImageMap curved = compile(curvedDefinition, grayscale8(new int[][]{{128, 255}}));
|
||||
double curvedValue = Math.pow(128D / 255D, 2D);
|
||||
assertEquals(curvedValue, curved.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(15D + (curvedValue * 10D), curved.sampleHeight(0D, 0D), EPSILON);
|
||||
assertEquals(20D, curved.sampleHeight(1D, 0D), EPSILON);
|
||||
assertEquals(1, curved.getClippedPixelCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodesContinuousAndBinaryMasks() {
|
||||
BufferedImage image = grayscale8(new int[][]{{64, 128, 192}});
|
||||
IrisImageMap continuousDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_MASK)
|
||||
.setInverted(true);
|
||||
CompiledIrisImageMap continuous = compile(continuousDefinition, image);
|
||||
assertEquals(1D - (64D / 255D), continuous.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertThrows(IrisImageMapValidationException.class, () -> continuous.sampleHeight(0D, 0D));
|
||||
|
||||
IrisImageMap binaryDefinition = scalarDefinition(IrisImageMapType.BINARY_MASK)
|
||||
.setThreshold(0.25D)
|
||||
.setFalloff(0.5D);
|
||||
CompiledIrisImageMap binary = compile(binaryDefinition, image);
|
||||
assertEquals(((64D / 255D) - 0.25D) / 0.5D, binary.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(((128D / 255D) - 0.25D) / 0.5D, binary.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(1D, binary.sampleNormalized(2D, 0D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesNumericAlphaPoliciesAndAlphaMasks() {
|
||||
BufferedImage image = rgba(new int[][]{{0xFFFFFFFF, 0x80FFFFFF, 0x00FFFFFF}});
|
||||
IrisImageMap definition = scalarDefinition(IrisImageMapType.RGB_HEIGHT);
|
||||
|
||||
assertEquals(1D, compile(definition.setAlpha(IrisImageMapAlpha.IGNORE), image)
|
||||
.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(128D / 255D, compile(definition.setAlpha(IrisImageMapAlpha.MASK), image)
|
||||
.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(0.25D, compile(definition
|
||||
.setAlpha(IrisImageMapAlpha.TRANSPARENT_IS_FALLBACK)
|
||||
.setFallbackValue(0.25D), image)
|
||||
.sampleNormalized(2D, 0D), EPSILON);
|
||||
assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(definition.setAlpha(IrisImageMapAlpha.ERROR), image)
|
||||
);
|
||||
|
||||
IrisImageMap alphaDefinition = scalarDefinition(IrisImageMapType.ALPHA_MASK)
|
||||
.setAlpha(IrisImageMapAlpha.IGNORE);
|
||||
CompiledIrisImageMap alpha = compile(alphaDefinition, image);
|
||||
assertEquals(1D, alpha.sampleNormalized(0D, 0D), EPSILON);
|
||||
assertEquals(128D / 255D, alpha.sampleNormalized(1D, 0D), EPSILON);
|
||||
assertEquals(0D, alpha.sampleNormalized(2D, 0D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesExactAndTolerantRawSrgbLegends() {
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
colors.put("#FF0000", "red");
|
||||
colors.put("#0000FF", "blue");
|
||||
IrisImageMap definition = colorDefinition(colors)
|
||||
.setColorTolerance(2D)
|
||||
.setUnknownColor(IrisImageMapUnknownColor.IGNORE);
|
||||
CompiledIrisImageMap compiled = compile(
|
||||
definition,
|
||||
rgb(new int[][]{{0xFF0000, 0xFE0100, 0x0000FF, 0x00FF00}})
|
||||
);
|
||||
|
||||
assertEquals("red", compiled.sampleTarget(0D, 0D));
|
||||
assertEquals("red", compiled.sampleTarget(1D, 0D));
|
||||
assertEquals("blue", compiled.sampleTarget(2D, 0D));
|
||||
assertNull(compiled.sampleTarget(3D, 0D));
|
||||
assertEquals(1, compiled.getUnknownColorPixelCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsAmbiguousTolerantLegendMatchesButExactWins() {
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
colors.put("#FF0000", "high");
|
||||
colors.put("#FD0000", "low");
|
||||
IrisImageMap definition = colorDefinition(colors).setColorTolerance(1D);
|
||||
|
||||
IrisImageMapValidationException ambiguity = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(definition, rgb(new int[][]{{0xFE0000}}))
|
||||
);
|
||||
assertTrue(ambiguity.getMessage().contains("ambiguously matches"));
|
||||
assertEquals("high", compile(definition, rgb(new int[][]{{0xFF0000}})).sampleTarget(0D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesColorAlphaFallbackAndRejectsPartialMaskAlpha() {
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
colors.put("#FF0000", "red");
|
||||
IrisImageMap definition = colorDefinition(colors)
|
||||
.setAlpha(IrisImageMapAlpha.MASK)
|
||||
.setFallbackTarget("fallback");
|
||||
|
||||
CompiledIrisImageMap binary = compile(definition, rgba(new int[][]{{0xFFFF0000, 0x00FF0000}}));
|
||||
assertEquals("red", binary.sampleTarget(0D, 0D));
|
||||
assertEquals("fallback", binary.sampleTarget(1D, 0D));
|
||||
assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(definition, rgba(new int[][]{{0x80FF0000}}))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void freezesCompiledLegendTargets() {
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
colors.put("#FF0000", "red");
|
||||
IrisImageMap definition = colorDefinition(colors);
|
||||
BufferedImage image = rgb(new int[][]{{0xFF0000}});
|
||||
CompiledIrisImageMap compiled = compile(definition, image);
|
||||
|
||||
colors.put("#FF0000", "changed");
|
||||
image.setRGB(0, 0, 0xFF0000FF);
|
||||
|
||||
assertEquals("red", compiled.sampleTarget(0D, 0D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesFormatModeBitDepthAndDimensionsWithTypedDiagnostics() {
|
||||
IrisImageMap grayscaleDefinition = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT);
|
||||
IrisImageMapValidationException format = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> CompiledIrisImageMap.compile(
|
||||
grayscaleDefinition,
|
||||
new IrisImage(grayscale8(new int[][]{{0}}), "jpeg")
|
||||
)
|
||||
);
|
||||
assertTrue(format.getDiagnostics().get(0).contains("PNG"));
|
||||
|
||||
IrisImageMapValidationException mode = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(grayscaleDefinition, rgb(new int[][]{{0}}))
|
||||
);
|
||||
assertTrue(mode.getMessage().contains("grayscale PNG"));
|
||||
|
||||
BufferedImage indexed = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED);
|
||||
IrisImageMapValidationException indexedMode = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(grayscaleDefinition, indexed)
|
||||
);
|
||||
assertTrue(indexedMode.getMessage().contains("Indexed PNG"));
|
||||
|
||||
BufferedImage lowBitRgb = new BufferedImage(1, 1, BufferedImage.TYPE_USHORT_565_RGB);
|
||||
IrisImageMapValidationException bitDepth = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(scalarDefinition(IrisImageMapType.RGB_HEIGHT), lowBitRgb)
|
||||
);
|
||||
assertTrue(bitDepth.getMessage().contains("8-bit RGB"));
|
||||
|
||||
BufferedImage tooWide = new BufferedImage(16_385, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
IrisImageMapValidationException dimensions = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(grayscaleDefinition, tooWide)
|
||||
);
|
||||
assertTrue(dimensions.getMessage().contains("16385"));
|
||||
|
||||
BufferedImage tooManyPixels = new ReportedSizeImage(4_097, 4_097);
|
||||
IrisImageMapValidationException pixels = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(grayscaleDefinition, tooManyPixels)
|
||||
);
|
||||
assertTrue(pixels.getMessage().contains("pixels"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enforcesSchemaNumericBoundsAtRuntime() {
|
||||
IrisImageMap tooSmallScale = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setBlocksPerPixel(IrisImageMap.MINIMUM_SCALE / 2D);
|
||||
IrisImageMapValidationException scale = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(tooSmallScale, grayscale8(new int[][]{{0}}))
|
||||
);
|
||||
assertTrue(scale.getMessage().contains("blocksPerPixel must be finite and at least"));
|
||||
|
||||
IrisImageMap minimums = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setBlocksPerPixel(IrisImageMap.MINIMUM_SCALE)
|
||||
.setCurveExponent(IrisImageMap.MINIMUM_SCALE);
|
||||
assertEquals(0D, compile(minimums, grayscale8(new int[][]{{0}}))
|
||||
.sampleNormalized(0D, 0D), EPSILON);
|
||||
|
||||
IrisImageMap tooSmallCurve = scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setCurveExponent(IrisImageMap.MINIMUM_SCALE / 2D);
|
||||
IrisImageMapValidationException curve = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(tooSmallCurve, grayscale8(new int[][]{{0}}))
|
||||
);
|
||||
assertTrue(curve.getMessage().contains("curveExponent must be finite and at least"));
|
||||
|
||||
KMap<String, String> colors = new KMap<>();
|
||||
colors.put("#FF0000", "red");
|
||||
IrisImageMap maximumTolerance = colorDefinition(colors)
|
||||
.setColorTolerance(IrisImageMap.MAXIMUM_COLOR_TOLERANCE);
|
||||
assertEquals("red", compile(maximumTolerance, rgb(new int[][]{{0xFF0000}}))
|
||||
.sampleTarget(0D, 0D));
|
||||
|
||||
IrisImageMap excessiveTolerance = colorDefinition(colors)
|
||||
.setColorTolerance(IrisImageMap.MAXIMUM_COLOR_TOLERANCE + 0.000001D);
|
||||
IrisImageMapValidationException tolerance = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> compile(excessiveTolerance, rgb(new int[][]{{0xFF0000}}))
|
||||
);
|
||||
assertTrue(tolerance.getMessage().contains("colorTolerance must be finite and within"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashesDecodedContentAndCanonicalConfigurationDeterministically() {
|
||||
KMap<String, String> firstColors = new KMap<>();
|
||||
firstColors.put("#FF0000", "red");
|
||||
firstColors.put("#0000FF", "blue");
|
||||
KMap<String, String> secondColors = new KMap<>();
|
||||
secondColors.put("#0000FF", "blue");
|
||||
secondColors.put("#FF0000", "red");
|
||||
BufferedImage image = rgb(new int[][]{{0xFF0000}});
|
||||
|
||||
CompiledIrisImageMap first = compile(colorDefinition(firstColors), image);
|
||||
CompiledIrisImageMap second = compile(colorDefinition(secondColors), image);
|
||||
assertEquals(first.contentHash(), second.contentHash());
|
||||
assertEquals(64, first.contentHash().length());
|
||||
assertEquals(64, first.getSourceMetadata().decodedContentHash().length());
|
||||
assertEquals(1, first.getSourceWidth());
|
||||
assertEquals(1, first.getSourceHeight());
|
||||
assertEquals(IrisImageMapType.COLOR_MAP, first.getType());
|
||||
|
||||
IrisImageMap changed = colorDefinition(firstColors).setBlocksPerPixel(2D);
|
||||
assertNotEquals(first.contentHash(), compile(changed, image).contentHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storesLargeRectangularMapsAcrossBoundedTileEdges() {
|
||||
BufferedImage image = new BufferedImage(1_025, 1_024, BufferedImage.TYPE_BYTE_GRAY);
|
||||
image.getRaster().setSample(1, 1_023, 0, 255);
|
||||
|
||||
CompiledIrisImageMap compiled = compile(
|
||||
scalarDefinition(IrisImageMapType.GRAYSCALE_HEIGHT), image
|
||||
);
|
||||
|
||||
assertEquals(1_025, compiled.getSourceWidth());
|
||||
assertEquals(1_024, compiled.getSourceHeight());
|
||||
assertEquals(0D, compiled.sampleNormalized(0D, 1_023D), 0D);
|
||||
assertEquals(1D, compiled.sampleNormalized(1D, 1_023D), 0D);
|
||||
}
|
||||
|
||||
private static CompiledIrisImageMap compile(IrisImageMap definition, BufferedImage image) {
|
||||
return CompiledIrisImageMap.compile(definition, new IrisImage(image, "png"));
|
||||
}
|
||||
|
||||
private static IrisImageMap scalarDefinition(IrisImageMapType type) {
|
||||
return new IrisImageMap()
|
||||
.setSource("test")
|
||||
.setType(type)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.FALLBACK);
|
||||
}
|
||||
|
||||
private static IrisImageMap colorDefinition(KMap<String, String> colors) {
|
||||
return new IrisImageMap()
|
||||
.setSource("test")
|
||||
.setType(IrisImageMapType.COLOR_MAP)
|
||||
.setColors(colors)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
}
|
||||
|
||||
private static BufferedImage grayscale8(int[][] values) {
|
||||
int height = values.length;
|
||||
int width = values[0].length;
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
|
||||
for (int z = 0; z < height; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.getRaster().setSample(x, z, 0, values[z][x]);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage grayscale16(int[][] values) {
|
||||
int height = values.length;
|
||||
int width = values[0].length;
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
|
||||
for (int z = 0; z < height; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.getRaster().setSample(x, z, 0, values[z][x]);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage rgb(int[][] values) {
|
||||
int height = values.length;
|
||||
int width = values[0].length;
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
for (int z = 0; z < height; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.setRGB(x, z, 0xFF000000 | values[z][x]);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage rgba(int[][] values) {
|
||||
int height = values.length;
|
||||
int width = values[0].length;
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int z = 0; z < height; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.setRGB(x, z, values[z][x]);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static final class ReportedSizeImage extends BufferedImage {
|
||||
private final int reportedWidth;
|
||||
private final int reportedHeight;
|
||||
|
||||
private ReportedSizeImage(int reportedWidth, int reportedHeight) {
|
||||
super(1, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
this.reportedWidth = reportedWidth;
|
||||
this.reportedHeight = reportedHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return reportedWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return reportedHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisImageMapMaskSamplerTest {
|
||||
private static final double EPSILON = 0.000001D;
|
||||
|
||||
@Test
|
||||
public void appliesEveryOperationInDeclaredOrder() {
|
||||
CompiledIrisImageMap half = compile(128);
|
||||
CompiledIrisImageMap quarter = compile(64);
|
||||
|
||||
assertEquals(128D / 255D, sampler(layer(half, IrisImageMapMaskOperation.MULTIPLY)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(128D / 255D, sampler(layer(half, IrisImageMapMaskOperation.MINIMUM)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(128D / 255D, sampler(
|
||||
layer(half, IrisImageMapMaskOperation.SUBTRACT),
|
||||
layer(half, IrisImageMapMaskOperation.MAXIMUM)
|
||||
).sample(0D, 0D), EPSILON);
|
||||
assertEquals(191D / 255D, sampler(
|
||||
layer(half, IrisImageMapMaskOperation.SUBTRACT),
|
||||
layer(quarter, IrisImageMapMaskOperation.ADD)
|
||||
).sample(0D, 0D), EPSILON);
|
||||
assertEquals(127D / 255D, sampler(layer(half, IrisImageMapMaskOperation.SUBTRACT)).sample(0D, 0D), EPSILON);
|
||||
|
||||
double subtractThenMultiply = sampler(
|
||||
layer(quarter, IrisImageMapMaskOperation.SUBTRACT),
|
||||
layer(half, IrisImageMapMaskOperation.MULTIPLY)
|
||||
).sample(0D, 0D);
|
||||
double multiplyThenSubtract = sampler(
|
||||
layer(half, IrisImageMapMaskOperation.MULTIPLY),
|
||||
layer(quarter, IrisImageMapMaskOperation.SUBTRACT)
|
||||
).sample(0D, 0D);
|
||||
assertEquals((1D - (64D / 255D)) * (128D / 255D), subtractThenMultiply, EPSILON);
|
||||
assertEquals((128D - 64D) / 255D, multiplyThenSubtract, EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesInversionThresholdFalloffAndClamping() {
|
||||
CompiledIrisImageMap quarter = compile(64);
|
||||
CompiledIrisImageMap threeQuarters = compile(192);
|
||||
|
||||
assertEquals(191D / 255D, sampler(layer(
|
||||
quarter, IrisImageMapMaskOperation.MULTIPLY, true, 0D, 0D
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(0D, sampler(layer(
|
||||
quarter, IrisImageMapMaskOperation.MULTIPLY, false, 0.5D, 0D
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(1D, sampler(layer(
|
||||
threeQuarters, IrisImageMapMaskOperation.MULTIPLY, false, 0.5D, 0D
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(((128D / 255D) - 0.25D) / 0.5D, sampler(layer(
|
||||
compile(128), IrisImageMapMaskOperation.MULTIPLY, false, 0.25D, 0.5D
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(1D, sampler(layer(
|
||||
threeQuarters, IrisImageMapMaskOperation.ADD
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
assertEquals(0D, sampler(layer(
|
||||
threeQuarters, IrisImageMapMaskOperation.SUBTRACT
|
||||
), layer(
|
||||
threeQuarters, IrisImageMapMaskOperation.SUBTRACT
|
||||
)).sample(0D, 0D), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidLayerDefinitionsAndMismatchedLists() {
|
||||
CompiledIrisImageMap compiled = compile(128);
|
||||
IrisImageMapMask invalid = new IrisImageMapMask().setMap("mask").setThreshold(Double.NaN);
|
||||
|
||||
assertThrows(IrisImageMapValidationException.class,
|
||||
() -> IrisImageMapMaskSampler.layer(compiled, invalid));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisImageMapMaskSampler.of(List.of(compiled), List.of()));
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler sampler(IrisImageMapMaskSampler.Layer... layers) {
|
||||
return new IrisImageMapMaskSampler(List.of(layers));
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler.Layer layer(
|
||||
CompiledIrisImageMap compiled,
|
||||
IrisImageMapMaskOperation operation
|
||||
) {
|
||||
return layer(compiled, operation, false, 0D, 0D);
|
||||
}
|
||||
|
||||
private static IrisImageMapMaskSampler.Layer layer(
|
||||
CompiledIrisImageMap compiled,
|
||||
IrisImageMapMaskOperation operation,
|
||||
boolean inverted,
|
||||
double threshold,
|
||||
double falloff
|
||||
) {
|
||||
return IrisImageMapMaskSampler.layer(compiled, new IrisImageMapMask()
|
||||
.setMap("mask")
|
||||
.setOperation(operation)
|
||||
.setInverted(inverted)
|
||||
.setThreshold(threshold)
|
||||
.setFalloff(falloff));
|
||||
}
|
||||
|
||||
private static CompiledIrisImageMap compile(int value) {
|
||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
image.getRaster().setSample(0, 0, 0, value);
|
||||
IrisImageMap definition = new IrisImageMap()
|
||||
.setSource("mask")
|
||||
.setType(IrisImageMapType.GRAYSCALE_MASK)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP);
|
||||
return CompiledIrisImageMap.compile(definition, new IrisImage(image, "png"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.engine.image;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapApplication;
|
||||
import art.arcane.iris.engine.object.IrisImageMapBinding;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMask;
|
||||
import art.arcane.iris.engine.object.IrisImageMapMaskOperation;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisImageMapRuntimeTest {
|
||||
private static final double EPSILON = 0.0001D;
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void compilesHeightBindingsAndKeepsReloadSamplingDeterministic() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("height-pack");
|
||||
writeMap(pack, "terrain", "terrain", grayscale(0, 255), new IrisImageMap()
|
||||
.setSource("terrain")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setMinimumHeight(-64D)
|
||||
.setMaximumHeight(320D)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP));
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.getImageMaps().add(new IrisImageMapBinding()
|
||||
.setKey("terrain")
|
||||
.setMap("terrain")
|
||||
.setApplication(IrisImageMapApplication.TERRAIN_HEIGHT));
|
||||
|
||||
Sample first = sample(pack, dimension);
|
||||
Sample second = sample(pack, dimension);
|
||||
|
||||
assertEquals(0D, first.minimum(), EPSILON);
|
||||
assertEquals(384D, first.maximum(), EPSILON);
|
||||
assertEquals(first.minimum(), second.minimum(), 0D);
|
||||
assertEquals(first.maximum(), second.maximum(), 0D);
|
||||
assertEquals(first.hash(), second.hash());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composesNamedMasksInDeclarationOrder() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("mask-pack");
|
||||
writeMap(pack, "terrain", "terrain", grayscale(255), new IrisImageMap()
|
||||
.setSource("terrain")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setMinimumHeight(-64D)
|
||||
.setMaximumHeight(320D));
|
||||
writeMap(pack, "weight", "weight", grayscale(128), new IrisImageMap()
|
||||
.setSource("weight")
|
||||
.setType(IrisImageMapType.GRAYSCALE_MASK));
|
||||
IrisImageMapMask mask = new IrisImageMapMask()
|
||||
.setMap("weight")
|
||||
.setOperation(IrisImageMapMaskOperation.MULTIPLY)
|
||||
.setThreshold(0D)
|
||||
.setFalloff(1D);
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setImageMaps(new KList<>());
|
||||
dimension.getImageMaps().add(new IrisImageMapBinding()
|
||||
.setKey("terrain")
|
||||
.setMap("terrain")
|
||||
.setApplication(IrisImageMapApplication.TERRAIN_HEIGHT)
|
||||
.setMasks(new KList<>(mask)));
|
||||
dimension.getImageMaps().add(new IrisImageMapBinding()
|
||||
.setKey("weight")
|
||||
.setMap("weight")
|
||||
.setApplication(IrisImageMapApplication.MASK));
|
||||
|
||||
IrisData data = IrisData.openDatapackCompiler(pack);
|
||||
try {
|
||||
IrisImageMapRuntime runtime = IrisImageMapRuntime.compile(data, dimension, -64);
|
||||
double weight = 128D / 255D;
|
||||
double expected = 10D + ((384D - 10D) * weight);
|
||||
assertEquals(expected, runtime.sampleTerrainHeight(0D, 0D, 10D), EPSILON);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadsDecodedImageWhenApplicationValidationFails() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("invalid-application-pack");
|
||||
writeMap(pack, "terrain", "terrain", grayscale(255), new IrisImageMap()
|
||||
.setSource("terrain")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT));
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.getImageMaps().add(new IrisImageMapBinding()
|
||||
.setKey("terrain")
|
||||
.setMap("terrain")
|
||||
.setApplication(IrisImageMapApplication.BIOME));
|
||||
|
||||
IrisData data = IrisData.openDatapackCompiler(pack);
|
||||
try {
|
||||
assertThrows(IrisImageMapValidationException.class,
|
||||
() -> IrisImageMapRuntime.compile(data, dimension, -64));
|
||||
assertEquals(0L, data.getImageLoader().getSize());
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Sample sample(File pack, IrisDimension dimension) {
|
||||
IrisData data = IrisData.openDatapackCompiler(pack);
|
||||
try {
|
||||
IrisImageMapRuntime runtime = IrisImageMapRuntime.compile(data, dimension, -64);
|
||||
CompiledIrisImageMap compiled = runtime.getCompiled("terrain");
|
||||
return new Sample(
|
||||
runtime.sampleTerrainHeight(0D, 0D, 25D),
|
||||
runtime.sampleTerrainHeight(1D, 0D, 25D),
|
||||
compiled.getContentHash()
|
||||
);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMap(
|
||||
File pack,
|
||||
String mapKey,
|
||||
String imageKey,
|
||||
BufferedImage image,
|
||||
IrisImageMap map
|
||||
) throws Exception {
|
||||
File images = new File(pack, "images");
|
||||
File maps = new File(pack, "image-maps");
|
||||
Files.createDirectories(images.toPath());
|
||||
Files.createDirectories(maps.toPath());
|
||||
ImageIO.write(image, "png", new File(images, imageKey + ".png"));
|
||||
Files.writeString(
|
||||
new File(maps, mapKey + ".json").toPath(),
|
||||
new Gson().toJson(map),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
||||
|
||||
private BufferedImage grayscale(int... samples) {
|
||||
BufferedImage image = new BufferedImage(samples.length, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
for (int x = 0; x < samples.length; x++) {
|
||||
image.getRaster().setSample(x, 0, 0, samples[x]);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private record Sample(double minimum, double maximum, String hash) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisWorldBoundaryTest {
|
||||
@Test
|
||||
public void absentConfigurationHasNoSyntheticBoundarySnapshot() {
|
||||
assertThrows(NullPointerException.class, () -> IrisWorldBoundary.snapshot(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredBoundarySnapshotsAllValues() {
|
||||
IrisWorldBoundary configured = new IrisWorldBoundary()
|
||||
.setCenter(new IrisWorldBoundaryCenter(128.5D, -64.25D))
|
||||
.setSize(16_384D)
|
||||
.setWarningDistance(16)
|
||||
.setDamageBuffer(7.5D)
|
||||
.setDamageAmount(0.75D);
|
||||
|
||||
IrisWorldBoundary snapshot = IrisWorldBoundary.snapshot(configured);
|
||||
configured.getCenter().setX(512D);
|
||||
|
||||
assertNotSame(configured, snapshot);
|
||||
assertNotSame(configured.getCenter(), snapshot.getCenter());
|
||||
assertEquals(128.5D, snapshot.getCenter().getX(), 0D);
|
||||
assertEquals(-64.25D, snapshot.getCenter().getZ(), 0D);
|
||||
assertEquals(16_384D, snapshot.getSize(), 0D);
|
||||
assertEquals(16, snapshot.getWarningDistance());
|
||||
assertEquals(7.5D, snapshot.getDamageBuffer(), 0D);
|
||||
assertEquals(0.75D, snapshot.getDamageAmount(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsValuesOutsideNativeLimits() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new IrisWorldBoundary().setSize(IrisWorldBoundary.MAXIMUM_SIZE + 1D).validate());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new IrisWorldBoundary().setWarningDistance(-1).validate());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new IrisWorldBoundary().setCenter(new IrisWorldBoundaryCenter(
|
||||
IrisWorldBoundary.MAXIMUM_CENTER + 1D, 0D)).validate());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new IrisWorldBoundary().setDamageAmount(Double.NaN).validate());
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ public class PackTypeBukkitPurityGateTest {
|
||||
"art.arcane.iris.engine.object.IrisExpression",
|
||||
"art.arcane.iris.engine.object.IrisObject",
|
||||
"art.arcane.iris.engine.object.IrisImage",
|
||||
"art.arcane.iris.engine.object.IrisImageMap",
|
||||
"art.arcane.iris.engine.object.matter.IrisMatterObject",
|
||||
"art.arcane.iris.engine.object.IrisStructure",
|
||||
"art.arcane.iris.engine.object.IrisJigsawPool",
|
||||
|
||||
@@ -17,14 +17,17 @@ import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -71,9 +74,12 @@ public class WebCacheTest {
|
||||
Files.createDirectories(existing.toPath().getParent());
|
||||
Files.writeString(existing.toPath(), "previous", StandardCharsets.UTF_8);
|
||||
|
||||
File downloaded = WebCache.getNonCachedFile(name, url, 8L);
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WebCache.getNonCachedFile(name, url, 8L)
|
||||
);
|
||||
|
||||
assertNull(downloaded);
|
||||
assertTrue(failure.getMessage().contains("size limit"));
|
||||
assertEquals("previous", Files.readString(existing.toPath(), StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
@@ -91,9 +97,12 @@ public class WebCacheTest {
|
||||
Files.createDirectories(existing.toPath().getParent());
|
||||
Files.writeString(existing.toPath(), "previous", StandardCharsets.UTF_8);
|
||||
|
||||
File downloaded = WebCache.getNonCachedFile(name, url, 8L);
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WebCache.getNonCachedFile(name, url, 8L)
|
||||
);
|
||||
|
||||
assertNull(downloaded);
|
||||
assertTrue(failure.getMessage().contains("size limit"));
|
||||
assertEquals("previous", Files.readString(existing.toPath(), StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
@@ -192,6 +201,127 @@ public class WebCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stalledResponseTimesOutAndPreservesThePreviousCacheEntry() throws Exception {
|
||||
byte[] body = "stalled-response".getBytes(StandardCharsets.UTF_8);
|
||||
CountDownLatch prefixSent = new CountDownLatch(1);
|
||||
CountDownLatch releaseResponse = new CountDownLatch(1);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/pack", exchange -> {
|
||||
try {
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body, 0, 4);
|
||||
exchange.getResponseBody().flush();
|
||||
prefixSent.countDown();
|
||||
releaseResponse.await(5L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
String name = "stalled-pack";
|
||||
String url = url(server);
|
||||
File existing = cachedFile(name, url);
|
||||
Files.createDirectories(existing.toPath().getParent());
|
||||
Files.writeString(existing.toPath(), "previous", StandardCharsets.UTF_8);
|
||||
WebCache.DownloadPolicy policy = new WebCache.DownloadPolicy(
|
||||
Duration.ofMillis(100L),
|
||||
1,
|
||||
Duration.ZERO
|
||||
);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WebCache.getNonCachedFile(name, url, body.length, policy, ignored -> {
|
||||
})
|
||||
);
|
||||
|
||||
assertTrue(prefixSent.await(1L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getMessage().contains("stalled"));
|
||||
assertEquals("previous", Files.readString(existing.toPath(), StandardCharsets.UTF_8));
|
||||
assertNoIncompleteDownloads(existing.getParentFile());
|
||||
} finally {
|
||||
releaseResponse.countDown();
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transientServerFailuresRetryAndPublishTheCompleteResponse() throws Exception {
|
||||
byte[] body = "retried-response".getBytes(StandardCharsets.UTF_8);
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/pack", exchange -> {
|
||||
if (requests.incrementAndGet() < 3) {
|
||||
exchange.sendResponseHeaders(503, -1L);
|
||||
exchange.close();
|
||||
return;
|
||||
}
|
||||
respond(exchange, body, true);
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
WebCache.DownloadPolicy policy = new WebCache.DownloadPolicy(
|
||||
Duration.ofSeconds(1L),
|
||||
3,
|
||||
Duration.ZERO
|
||||
);
|
||||
|
||||
File downloaded = WebCache.getNonCachedFile(
|
||||
"retried-pack",
|
||||
url(server),
|
||||
body.length,
|
||||
policy,
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(3, requests.get());
|
||||
assertEquals("retried-response", Files.readString(downloaded.toPath(), StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void permanentClientFailureDoesNotRetry() throws Exception {
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/pack", exchange -> {
|
||||
requests.incrementAndGet();
|
||||
exchange.sendResponseHeaders(404, -1L);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
WebCache.DownloadPolicy policy = new WebCache.DownloadPolicy(
|
||||
Duration.ofSeconds(1L),
|
||||
3,
|
||||
Duration.ZERO
|
||||
);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WebCache.getNonCachedFile(
|
||||
"missing-pack",
|
||||
url(server),
|
||||
64L,
|
||||
policy,
|
||||
ignored -> {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("HTTP 404"));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServer server(byte[] body, boolean declareLength) throws IOException {
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/pack", exchange -> respond(exchange, body, declareLength));
|
||||
@@ -214,6 +344,11 @@ public class WebCacheTest {
|
||||
return IrisPlatforms.get().dataFile("cache", hash.substring(0, 2), hash.substring(3, 5), hash);
|
||||
}
|
||||
|
||||
private void assertNoIncompleteDownloads(File folder) {
|
||||
File[] incomplete = folder.listFiles((File parent, String name) -> name.startsWith(".download-"));
|
||||
assertTrue(incomplete == null || incomplete.length == 0);
|
||||
}
|
||||
|
||||
private void assertProgressEndsAt(List<WebCache.TransferProgress> progress, long transferredBytes,
|
||||
long contentLength) {
|
||||
long previousBytes = -1L;
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package art.arcane.iris.util.common.plugin;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class VolmitSenderComponentDeliveryTest {
|
||||
private IrisSettings previousSettings;
|
||||
|
||||
@Before
|
||||
public void installSettings() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings settings = new IrisSettings();
|
||||
settings.getGeneral().setSpinh(0);
|
||||
settings.getGeneral().setSpins(0);
|
||||
settings.getGeneral().setSpinb(0);
|
||||
IrisSettings.settings = settings;
|
||||
}
|
||||
|
||||
@After
|
||||
public void restoreSettings() {
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sectionColorsReachTheRichSenderWithoutLiteralControlCodes() {
|
||||
List<String> messages = new ArrayList<>();
|
||||
VolmitSender sender = new VolmitSender(recordingSender(messages));
|
||||
|
||||
sender.sendMessage(C.RED + "Broken");
|
||||
|
||||
assertEquals(1, messages.size());
|
||||
assertFalse(messages.getFirst().contains("\u00a7"));
|
||||
assertEquals("Broken", plain(messages.getFirst()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMiniKeepsMiniMessageTagsLiteral() {
|
||||
List<String> messages = new ArrayList<>();
|
||||
VolmitSender sender = new VolmitSender(recordingSender(messages));
|
||||
|
||||
sender.sendMessage("<NOMINI><red>literal");
|
||||
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals("<red>literal", plain(messages.getFirst()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonPlayerFallbackReceivesPlainTextWithoutSectionSymbols() {
|
||||
List<String> messages = new ArrayList<>();
|
||||
VolmitSender sender = new VolmitSender(plainFallbackSender(messages));
|
||||
|
||||
sender.sendMessage(C.RED + "Broken");
|
||||
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals("Broken", messages.getFirst());
|
||||
assertFalse(messages.getFirst().contains("\u00a7"));
|
||||
}
|
||||
|
||||
private static String plain(String miniMessage) {
|
||||
Component component = MiniMessage.miniMessage().deserialize(miniMessage);
|
||||
return PlainTextComponentSerializer.plainText().serialize(component);
|
||||
}
|
||||
|
||||
private static CommandSender recordingSender(List<String> messages) {
|
||||
return (CommandSender) Proxy.newProxyInstance(
|
||||
VolmitSenderComponentDeliveryTest.class.getClassLoader(),
|
||||
new Class<?>[]{CommandSender.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (method.getName().equals("sendRichMessage") && arguments != null && arguments.length > 0) {
|
||||
messages.add(String.valueOf(arguments[0]));
|
||||
return null;
|
||||
}
|
||||
if (method.getName().equals("sendMessage") && arguments != null && arguments.length > 0) {
|
||||
messages.add(String.valueOf(arguments[0]));
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static CommandSender plainFallbackSender(List<String> messages) {
|
||||
return (CommandSender) Proxy.newProxyInstance(
|
||||
VolmitSenderComponentDeliveryTest.class.getClassLoader(),
|
||||
new Class<?>[]{CommandSender.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (method.getName().equals("sendRichMessage")) {
|
||||
throw new UnsupportedOperationException("rich delivery unavailable");
|
||||
}
|
||||
if (method.getName().equals("sendMessage") && arguments != null && arguments.length > 0) {
|
||||
messages.add(String.valueOf(arguments[0]));
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static Object defaultValue(Class<?> returnType) {
|
||||
if (returnType == boolean.class) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
if (returnType == int.class) {
|
||||
return Integer.valueOf(0);
|
||||
}
|
||||
if (returnType == long.class) {
|
||||
return Long.valueOf(0L);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.util.project.noise;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisImageMapOutOfBounds;
|
||||
import art.arcane.iris.engine.object.IrisImageMapType;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class ImageNoiseTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void samplesScalarImageMapsWithoutRetainingDecodedSources() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("scalar-pack");
|
||||
BufferedImage image = new BufferedImage(2, 1, BufferedImage.TYPE_BYTE_GRAY);
|
||||
image.getRaster().setSample(1, 0, 0, 255);
|
||||
writeImage(pack, "height", image);
|
||||
writeMap(pack, "height", new IrisImageMap()
|
||||
.setSource("height")
|
||||
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
|
||||
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP));
|
||||
|
||||
IrisData data = IrisData.openDatapackCompiler(pack);
|
||||
try {
|
||||
ImageNoise noise = new ImageNoise(data, "height");
|
||||
|
||||
assertEquals(0D, noise.noise(0D, 0D), 0D);
|
||||
assertEquals(1D, noise.noise(1D, 0D), 0D);
|
||||
assertEquals(0L, data.getImageLoader().getSize());
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsColorMapsBeforeLoadingTheirSource() throws Exception {
|
||||
File pack = temporaryFolder.newFolder("color-pack");
|
||||
writeMap(pack, "colors", new IrisImageMap()
|
||||
.setSource("missing")
|
||||
.setType(IrisImageMapType.COLOR_MAP));
|
||||
|
||||
IrisData data = IrisData.openDatapackCompiler(pack);
|
||||
try {
|
||||
IrisImageMapValidationException failure = assertThrows(
|
||||
IrisImageMapValidationException.class,
|
||||
() -> new ImageNoise(data, "colors")
|
||||
);
|
||||
|
||||
assertEquals("Generator-style image-map resource 'colors' must produce normalized scalar data",
|
||||
failure.getMessage());
|
||||
assertEquals(0L, data.getImageLoader().getSize());
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeImage(File pack, String key, BufferedImage image) throws Exception {
|
||||
File folder = new File(pack, "images");
|
||||
Files.createDirectories(folder.toPath());
|
||||
ImageIO.write(image, "png", new File(folder, key + ".png"));
|
||||
}
|
||||
|
||||
private void writeMap(File pack, String key, IrisImageMap definition) throws Exception {
|
||||
File folder = new File(pack, "image-maps");
|
||||
Files.createDirectories(folder.toPath());
|
||||
Files.writeString(
|
||||
new File(folder, key + ".json").toPath(),
|
||||
new Gson().toJson(definition),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user