mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
object cleanup
This commit is contained in:
@@ -96,7 +96,6 @@ import art.arcane.volmlib.util.collection.KMap;
|
|||||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
import art.arcane.volmlib.util.function.NastyRunnable;
|
import art.arcane.volmlib.util.function.NastyRunnable;
|
||||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
|
||||||
import art.arcane.volmlib.util.hud.HudActionBar;
|
import art.arcane.volmlib.util.hud.HudActionBar;
|
||||||
import art.arcane.volmlib.util.hud.HudBossBarLane;
|
import art.arcane.volmlib.util.hud.HudBossBarLane;
|
||||||
import art.arcane.volmlib.util.io.IO;
|
import art.arcane.volmlib.util.io.IO;
|
||||||
@@ -165,7 +164,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
public static Bindings.Adventure audiences;
|
public static Bindings.Adventure audiences;
|
||||||
public static MultiverseCoreLink linkMultiverseCore;
|
public static MultiverseCoreLink linkMultiverseCore;
|
||||||
public static IrisCompat compat;
|
public static IrisCompat compat;
|
||||||
public static ConfigHotloadEngine configHotloadEngine;
|
|
||||||
public static ChunkTickets tickets;
|
public static ChunkTickets tickets;
|
||||||
private static VolmitSender sender;
|
private static VolmitSender sender;
|
||||||
private static Thread shutdownHook;
|
private static Thread shutdownHook;
|
||||||
@@ -663,13 +661,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
|
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
|
||||||
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
|
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
|
||||||
settingsHotloadWatch = watch;
|
settingsHotloadWatch = watch;
|
||||||
configHotloadEngine = new ConfigHotloadEngine(
|
|
||||||
watch::isSettingsFile,
|
|
||||||
watch::knownSettingsFiles,
|
|
||||||
watch::readSettingsContent,
|
|
||||||
watch::normalizeSettingsContent
|
|
||||||
);
|
|
||||||
configHotloadEngine.configure(500L, 3_000L, List.of(watch.settingsFile()), List.of());
|
|
||||||
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
|
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
|
||||||
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
||||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
||||||
@@ -714,7 +705,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
pendingWorldReplacements.captureVanillaLevelContext();
|
pendingWorldReplacements.captureVanillaLevelContext();
|
||||||
pendingWorldReplacements.verifyLoadedPublishedWorlds();
|
pendingWorldReplacements.verifyLoadedPublishedWorlds();
|
||||||
J.a(this::bstats);
|
J.a(this::bstats);
|
||||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 10);
|
J.ar(watch::checkConfigHotload, 10);
|
||||||
J.sr(this::tickQueue, 0);
|
J.sr(this::tickQueue, 0);
|
||||||
J.s(this::setupPapi);
|
J.s(this::setupPapi);
|
||||||
if (IrisStartupValidation.isReady()) {
|
if (IrisStartupValidation.isReady()) {
|
||||||
@@ -883,9 +874,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
BukkitPlatform.hudBar().shutdown();
|
BukkitPlatform.hudBar().shutdown();
|
||||||
BukkitPlatform.hudLanes().shutdown();
|
BukkitPlatform.hudLanes().shutdown();
|
||||||
}
|
}
|
||||||
if (configHotloadEngine != null) {
|
SettingsHotloadWatch activeSettingsHotloadWatch = settingsHotloadWatch;
|
||||||
configHotloadEngine.clear();
|
settingsHotloadWatch = null;
|
||||||
configHotloadEngine = null;
|
if (activeSettingsHotloadWatch != null) {
|
||||||
|
activeSettingsHotloadWatch.close();
|
||||||
}
|
}
|
||||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||||
super.onDisable();
|
super.onDisable();
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/*
|
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
|
||||||
* Copyright (c) 2026 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.core;
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
|
||||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identity and hotload handling for settings.json. Supplies the predicates the
|
|
||||||
* {@link ConfigHotloadEngine} is built from and drains the touched-file queue.
|
|
||||||
*/
|
|
||||||
public final class SettingsHotloadWatch {
|
|
||||||
private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
private final File settingsFile;
|
|
||||||
|
|
||||||
public SettingsHotloadWatch(File settingsFile) {
|
|
||||||
this.settingsFile = settingsFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File settingsFile() {
|
|
||||||
return settingsFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkConfigHotload(ConfigHotloadEngine engine) {
|
|
||||||
if (engine == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ConfigHotloadEngine.StableContentSnapshot snapshot : engine.pollTouchedSnapshots()) {
|
|
||||||
if ("missing".equals(snapshot.signature())) {
|
|
||||||
engine.processSnapshotChange(snapshot, ignored -> true, null);
|
|
||||||
Iris.warn("settings.json was removed; retaining the last valid runtime settings.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
engine.processSnapshotChange(
|
|
||||||
snapshot,
|
|
||||||
stable -> applySettingsSnapshot(stable.file(), stable.normalizedContent()),
|
|
||||||
ignored -> Iris.info("Hotloaded settings.json ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
IrisLanguage.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSettingsFile(File file) {
|
|
||||||
if (file == null || settingsFile == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<File> knownSettingsFiles() {
|
|
||||||
if (settingsFile == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return List.of(settingsFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String readSettingsContent(File file) {
|
|
||||||
if (file == null || !file.exists() || !file.isFile()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream input = Files.newInputStream(file.toPath())) {
|
|
||||||
byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1);
|
|
||||||
if (content.length > MAX_SETTINGS_BYTES) {
|
|
||||||
throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file);
|
|
||||||
}
|
|
||||||
return new String(content, StandardCharsets.UTF_8);
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
Iris.warn("Failed to read settings file %s: %s%s",
|
|
||||||
file.getAbsolutePath(),
|
|
||||||
ex.getClass().getSimpleName(),
|
|
||||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
|
||||||
Iris.reportError(ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String normalizeSettingsContent(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.replace("\r\n", "\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean applySettingsSnapshot(File file, String content) {
|
|
||||||
if (content == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload);
|
|
||||||
} catch (RuntimeException failure) {
|
|
||||||
Iris.warn("Rejected invalid settings hotload from %s: %s",
|
|
||||||
file.getAbsolutePath(),
|
|
||||||
failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage());
|
|
||||||
Iris.reportError(failure);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-85
@@ -18,121 +18,45 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.service;
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.SettingsHotloadWatch;
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class ModdedSettingsHotloadService implements ModdedTickableService {
|
public final class ModdedSettingsHotloadService implements ModdedTickableService {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
private SettingsHotloadWatch hotloadWatch;
|
||||||
private static final long POLL_PERIOD_MILLIS = 500L;
|
|
||||||
private static final long HOTLOAD_COOLDOWN_MILLIS = 3_000L;
|
|
||||||
private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
private ConfigHotloadEngine hotloadEngine;
|
|
||||||
private long lastPollAtNanos;
|
private long lastPollAtNanos;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
File settingsFile = settingsFile();
|
hotloadWatch = new SettingsHotloadWatch(settingsFile());
|
||||||
hotloadEngine = new ConfigHotloadEngine(
|
|
||||||
this::isSettingsFile,
|
|
||||||
() -> List.of(settingsFile),
|
|
||||||
this::readSettings,
|
|
||||||
this::normalizeSettings
|
|
||||||
);
|
|
||||||
hotloadEngine.configure(
|
|
||||||
POLL_PERIOD_MILLIS,
|
|
||||||
HOTLOAD_COOLDOWN_MILLIS,
|
|
||||||
List.of(settingsFile),
|
|
||||||
List.of()
|
|
||||||
);
|
|
||||||
lastPollAtNanos = 0L;
|
lastPollAtNanos = 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
ConfigHotloadEngine active = hotloadEngine;
|
SettingsHotloadWatch active = hotloadWatch;
|
||||||
hotloadEngine = null;
|
hotloadWatch = null;
|
||||||
if (active != null) {
|
if (active != null) {
|
||||||
active.clear();
|
active.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServerTick(MinecraftServer server) {
|
public void onServerTick(MinecraftServer server) {
|
||||||
ConfigHotloadEngine active = hotloadEngine;
|
SettingsHotloadWatch active = hotloadWatch;
|
||||||
if (active == null) {
|
if (active == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long now = System.nanoTime();
|
long now = System.nanoTime();
|
||||||
if (now - lastPollAtNanos < TimeUnit.MILLISECONDS.toNanos(POLL_PERIOD_MILLIS)) {
|
if (now - lastPollAtNanos < TimeUnit.MILLISECONDS.toNanos(SettingsHotloadWatch.POLL_PERIOD_MILLIS)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastPollAtNanos = now;
|
lastPollAtNanos = now;
|
||||||
|
active.checkConfigHotload();
|
||||||
try {
|
|
||||||
for (ConfigHotloadEngine.StableContentSnapshot snapshot : active.pollTouchedSnapshots()) {
|
|
||||||
if ("missing".equals(snapshot.signature())) {
|
|
||||||
active.processSnapshotChange(snapshot, ignored -> true, null);
|
|
||||||
LOGGER.warn("settings.json was removed; retaining the last valid runtime settings");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
active.processSnapshotChange(snapshot, this::reloadSettings, ignored -> LOGGER.info("Hotloaded settings.json"));
|
|
||||||
}
|
|
||||||
IrisLanguage.update();
|
|
||||||
} catch (RuntimeException failure) {
|
|
||||||
LOGGER.error("Iris settings hotload watcher failed", failure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean reloadSettings(ConfigHotloadEngine.StableContentSnapshot snapshot) {
|
|
||||||
try {
|
|
||||||
String content = snapshot.normalizedContent();
|
|
||||||
if (content == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload);
|
|
||||||
} catch (RuntimeException failure) {
|
|
||||||
LOGGER.error("Iris settings hotload failed; keeping the previous runtime settings", failure);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSettingsFile(File file) {
|
|
||||||
return file != null && settingsFile().getAbsoluteFile().equals(file.getAbsoluteFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readSettings(File file) {
|
|
||||||
if (file == null || !file.isFile()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try (InputStream input = Files.newInputStream(file.toPath())) {
|
|
||||||
byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1);
|
|
||||||
if (content.length > MAX_SETTINGS_BYTES) {
|
|
||||||
throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file);
|
|
||||||
}
|
|
||||||
return new String(content, StandardCharsets.UTF_8);
|
|
||||||
} catch (IOException failure) {
|
|
||||||
throw new UncheckedIOException("Failed to read Iris settings from " + file, failure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeSettings(String content) {
|
|
||||||
return content == null ? null : content.replace("\r\n", "\n").trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File settingsFile() {
|
private static File settingsFile() {
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.CharacterCodingException;
|
||||||
|
import java.nio.charset.CodingErrorAction;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class SettingsHotloadWatch implements AutoCloseable {
|
||||||
|
public static final long POLL_PERIOD_MILLIS = 500L;
|
||||||
|
public static final long HOTLOAD_COOLDOWN_MILLIS = 3_000L;
|
||||||
|
private static final int MAX_HOTLOAD_BYTES = 2 * 1024 * 1024;
|
||||||
|
private static final Timing PRODUCTION_TIMING = new Timing(
|
||||||
|
POLL_PERIOD_MILLIS,
|
||||||
|
HOTLOAD_COOLDOWN_MILLIS,
|
||||||
|
ConfigHotloadEngine.DEFAULT_FULL_WATCH_SCAN_WINDOW_MS,
|
||||||
|
ConfigHotloadEngine.DEFAULT_SIGNATURE_SCAN_WINDOW_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
private final File settingsFile;
|
||||||
|
private final File localeOverrideFolder;
|
||||||
|
private final ConfigHotloadEngine hotloadEngine;
|
||||||
|
private final Map<String, String> reportedCaptureFailures = new ConcurrentHashMap<>();
|
||||||
|
private final Consumer<ConfigHotloadEngine.StableContentSnapshot> beforeSnapshotApply;
|
||||||
|
private final BiConsumer<File, String> manualReloadListener;
|
||||||
|
private volatile boolean closed;
|
||||||
|
|
||||||
|
public SettingsHotloadWatch(File settingsFile) {
|
||||||
|
this(settingsFile, PRODUCTION_TIMING);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsHotloadWatch(File settingsFile, Timing timing) {
|
||||||
|
this(settingsFile, timing, snapshot -> {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsHotloadWatch(
|
||||||
|
File settingsFile,
|
||||||
|
Timing timing,
|
||||||
|
Consumer<ConfigHotloadEngine.StableContentSnapshot> beforeSnapshotApply
|
||||||
|
) {
|
||||||
|
this.settingsFile = Objects.requireNonNull(settingsFile, "Settings file cannot be null").getAbsoluteFile();
|
||||||
|
File dataFolder = Objects.requireNonNull(this.settingsFile.getParentFile(), "Settings data folder cannot be null");
|
||||||
|
localeOverrideFolder = new File(dataFolder, "languages/overrides").getAbsoluteFile();
|
||||||
|
Timing resolvedTiming = Objects.requireNonNull(timing, "Hotload timing cannot be null");
|
||||||
|
this.beforeSnapshotApply = Objects.requireNonNull(beforeSnapshotApply, "Snapshot apply observer cannot be null");
|
||||||
|
manualReloadListener = this::acknowledgeManualLocaleReload;
|
||||||
|
hotloadEngine = new ConfigHotloadEngine(
|
||||||
|
this::isManagedFile,
|
||||||
|
this::knownFiles,
|
||||||
|
this::readContent,
|
||||||
|
this::normalizeContent,
|
||||||
|
resolvedTiming.fullScanWindowMillis(),
|
||||||
|
resolvedTiming.signatureScanWindowMillis()
|
||||||
|
);
|
||||||
|
hotloadEngine.configure(
|
||||||
|
resolvedTiming.pollPeriodMillis(),
|
||||||
|
resolvedTiming.cooldownMillis(),
|
||||||
|
List.of(this.settingsFile),
|
||||||
|
List.of(localeOverrideFolder)
|
||||||
|
);
|
||||||
|
IrisLanguage.addManualReloadListener(manualReloadListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void checkConfigHotload() {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (IrisLanguage.class) {
|
||||||
|
try {
|
||||||
|
for (ConfigHotloadEngine.StableContentSnapshot snapshot : hotloadEngine.pollTouchedSnapshots()) {
|
||||||
|
beforeSnapshotApply.accept(snapshot);
|
||||||
|
hotloadEngine.processSnapshotChange(snapshot, this::applySnapshot, this::reportApplied);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
IrisLogging.error("Iris settings and locale hotload watcher failed: " + failureDetail(failure));
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
failure.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
IrisLanguage.removeManualReloadListener(manualReloadListener);
|
||||||
|
hotloadEngine.clear();
|
||||||
|
reportedCaptureFailures.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean applySnapshot(ConfigHotloadEngine.StableContentSnapshot snapshot) {
|
||||||
|
File file = snapshot.file();
|
||||||
|
boolean missing = "missing".equals(snapshot.signature());
|
||||||
|
if (isSettingsFile(file)) {
|
||||||
|
if (missing) {
|
||||||
|
IrisLogging.warn("settings.json was removed; retaining the last valid runtime settings.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (snapshot.normalizedContent() == null) {
|
||||||
|
reportUnavailableSnapshot(file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return applySettingsSnapshot(file, snapshot.normalizedContent());
|
||||||
|
}
|
||||||
|
if (!isLocaleOverrideFile(file) || !IrisLanguage.isActiveOverrideFile(file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!missing && snapshot.normalizedContent() == null) {
|
||||||
|
reportUnavailableSnapshot(file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return applyLocaleSnapshot(file, missing ? null : snapshot.normalizedContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSettingsFile(File file) {
|
||||||
|
return file != null && settingsFile.equals(file.getAbsoluteFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLocaleOverrideFile(File file) {
|
||||||
|
if (file == null || !file.getName().toLowerCase(Locale.ROOT).endsWith(".json")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
File parent = file.getAbsoluteFile().getParentFile();
|
||||||
|
return localeOverrideFolder.equals(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isManagedFile(File file) {
|
||||||
|
return isSettingsFile(file) || isLocaleOverrideFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<File> knownFiles() {
|
||||||
|
List<File> files = new ArrayList<>();
|
||||||
|
files.add(settingsFile);
|
||||||
|
File[] overrides = localeOverrideFolder.listFiles();
|
||||||
|
if (overrides == null) {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
for (File override : overrides) {
|
||||||
|
if (isLocaleOverrideFile(override) && override.isFile()) {
|
||||||
|
files.add(override.getAbsoluteFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readContent(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!file.isFile()) {
|
||||||
|
clearCaptureFailure(file);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (InputStream input = Files.newInputStream(file.toPath())) {
|
||||||
|
byte[] content = input.readNBytes(MAX_HOTLOAD_BYTES + 1);
|
||||||
|
if (content.length > MAX_HOTLOAD_BYTES) {
|
||||||
|
throw new IOException("Hotload file exceeds " + MAX_HOTLOAD_BYTES + " bytes: " + file);
|
||||||
|
}
|
||||||
|
String decoded = decodeUtf8(content, file);
|
||||||
|
clearCaptureFailure(file);
|
||||||
|
return decoded;
|
||||||
|
} catch (NoSuchFileException missing) {
|
||||||
|
clearCaptureFailure(file);
|
||||||
|
return null;
|
||||||
|
} catch (IOException | SecurityException failure) {
|
||||||
|
reportCaptureFailure(file, failure);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeContent(String content) {
|
||||||
|
return content == null ? null : content.replace("\r\n", "\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decodeUtf8(byte[] content, File file) throws IOException {
|
||||||
|
try {
|
||||||
|
return StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||||
|
.decode(ByteBuffer.wrap(content))
|
||||||
|
.toString();
|
||||||
|
} catch (CharacterCodingException failure) {
|
||||||
|
throw new IOException("Hotload file is not valid UTF-8: " + file, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applySettingsSnapshot(File file, String content) {
|
||||||
|
try {
|
||||||
|
return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload);
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
IrisLogging.error("Rejected invalid settings hotload from " + file.getAbsolutePath() + ": " + failureDetail(failure));
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
failure.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyLocaleSnapshot(File file, String content) {
|
||||||
|
try {
|
||||||
|
return IrisLanguage.reloadOverride(file, content);
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
IrisLogging.error("Rejected invalid locale hotload from " + file.getAbsolutePath() + ": " + failureDetail(failure));
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
failure.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acknowledgeManualLocaleReload(File file, String content) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hotloadEngine.noteSelfWrite(file, content);
|
||||||
|
clearCaptureFailure(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportUnavailableSnapshot(File file) {
|
||||||
|
if (file.isFile() && file.length() > MAX_HOTLOAD_BYTES) {
|
||||||
|
reportCaptureFailure(
|
||||||
|
file,
|
||||||
|
new IOException("Hotload file exceeds " + MAX_HOTLOAD_BYTES + " bytes: " + file)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportCaptureFailure(File file, Throwable failure) {
|
||||||
|
String path = file.getAbsolutePath();
|
||||||
|
String failureKey = failure.getClass().getName() + ":" + failureDetail(failure);
|
||||||
|
if (Objects.equals(reportedCaptureFailures.put(path, failureKey), failureKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisLogging.error("Failed to read watched Iris file " + path + ": " + failureDetail(failure));
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
failure.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearCaptureFailure(File file) {
|
||||||
|
reportedCaptureFailures.remove(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportApplied(ConfigHotloadEngine.ContentDelta delta) {
|
||||||
|
File file = delta.file();
|
||||||
|
if (isSettingsFile(file)) {
|
||||||
|
if (delta.after() != null) {
|
||||||
|
IrisLogging.info("Hotloaded settings.json");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IrisLanguage.isActiveOverrideFile(file)) {
|
||||||
|
IrisLogging.info("Hotloaded locale override " + file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String failureDetail(Throwable failure) {
|
||||||
|
String message = failure.getMessage();
|
||||||
|
return failure.getClass().getSimpleName() + (message == null || message.isBlank() ? "" : " - " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Timing(
|
||||||
|
long pollPeriodMillis,
|
||||||
|
long cooldownMillis,
|
||||||
|
long fullScanWindowMillis,
|
||||||
|
long signatureScanWindowMillis
|
||||||
|
) {
|
||||||
|
Timing {
|
||||||
|
if (pollPeriodMillis <= 0L
|
||||||
|
|| cooldownMillis <= 0L
|
||||||
|
|| fullScanWindowMillis <= 0L
|
||||||
|
|| signatureScanWindowMillis <= 0L) {
|
||||||
|
throw new IllegalArgumentException("Hotload timing values must be positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,15 +45,13 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class IrisLanguage {
|
public final class IrisLanguage {
|
||||||
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
||||||
private static final long HOTLOAD_CONTENT_STABILITY_NANOS = TimeUnit.MILLISECONDS.toNanos(250L);
|
|
||||||
private static final long HOTLOAD_DELETION_GRACE_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
|
||||||
private static final long HOTLOAD_COOLDOWN_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
|
||||||
private static final int MAX_REPORTED_ISSUES = 12;
|
private static final int MAX_REPORTED_ISSUES = 12;
|
||||||
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
||||||
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
||||||
@@ -68,17 +66,9 @@ public final class IrisLanguage {
|
|||||||
* new snapshot and the whole memo is discarded. Bounded by the catalog size.
|
* new snapshot and the whole memo is discarded. Bounded by the catalog size.
|
||||||
*/
|
*/
|
||||||
private static final AtomicReference<PlainMemo> PLAIN_MEMO = new AtomicReference<>(null);
|
private static final AtomicReference<PlainMemo> PLAIN_MEMO = new AtomicReference<>(null);
|
||||||
private static final LocaleHotloadGate HOTLOAD_GATE = new LocaleHotloadGate(
|
private static final CopyOnWriteArrayList<BiConsumer<File, String>> MANUAL_RELOAD_LISTENERS =
|
||||||
new LocaleHotloadGate.Timing(
|
new CopyOnWriteArrayList<>();
|
||||||
HOTLOAD_CONTENT_STABILITY_NANOS,
|
|
||||||
HOTLOAD_DELETION_GRACE_NANOS,
|
|
||||||
HOTLOAD_COOLDOWN_NANOS
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private static volatile File dataFolder;
|
private static volatile File dataFolder;
|
||||||
private static volatile String lastCaptureFailureKey;
|
|
||||||
private static volatile String lastInvalidHotloadLocale;
|
|
||||||
private static volatile String activeLocale = CATALOG.englishLocale();
|
private static volatile String activeLocale = CATALOG.englishLocale();
|
||||||
|
|
||||||
private IrisLanguage() {
|
private IrisLanguage() {
|
||||||
@@ -113,10 +103,22 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
IrisSettings.IrisSettingsGeneral general = resolvedCandidate.getGeneral();
|
IrisSettings.IrisSettingsGeneral general = resolvedCandidate.getGeneral();
|
||||||
String locale = general == null ? CATALOG.englishLocale() : general.getLanguage();
|
String locale = general == null ? CATALOG.englishLocale() : general.getLanguage();
|
||||||
return reload(root, locale);
|
return reloadResolved(root, locale, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized boolean reload(File root, String locale) {
|
public static synchronized boolean reload(File root, String locale) {
|
||||||
|
return reloadResolved(root, locale, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addManualReloadListener(BiConsumer<File, String> listener) {
|
||||||
|
MANUAL_RELOAD_LISTENERS.add(Objects.requireNonNull(listener, "Manual reload listener cannot be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeManualReloadListener(BiConsumer<File, String> listener) {
|
||||||
|
MANUAL_RELOAD_LISTENERS.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean reloadResolved(File root, String locale, boolean notifyManualReload) {
|
||||||
File resolvedRoot = root == null ? null : root.getAbsoluteFile();
|
File resolvedRoot = root == null ? null : root.getAbsoluteFile();
|
||||||
if (resolvedRoot == null) {
|
if (resolvedRoot == null) {
|
||||||
throw new IllegalArgumentException("Iris locale data folder cannot be null");
|
throw new IllegalArgumentException("Iris locale data folder cannot be null");
|
||||||
@@ -126,7 +128,6 @@ public final class IrisLanguage {
|
|||||||
requestedLocale = normalizeLocale(locale);
|
requestedLocale = normalizeLocale(locale);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
dataFolder = resolvedRoot;
|
dataFolder = resolvedRoot;
|
||||||
HOTLOAD_GATE.reset(null);
|
|
||||||
IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + ".");
|
IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + ".");
|
||||||
IrisLogging.reportError(exception);
|
IrisLogging.reportError(exception);
|
||||||
exception.printStackTrace();
|
exception.printStackTrace();
|
||||||
@@ -135,17 +136,77 @@ public final class IrisLanguage {
|
|||||||
|
|
||||||
File override = overrideFile(resolvedRoot, requestedLocale);
|
File override = overrideFile(resolvedRoot, requestedLocale);
|
||||||
SnapshotCapture capture = captureForReload(override, requestedLocale);
|
SnapshotCapture capture = captureForReload(override, requestedLocale);
|
||||||
|
boolean applied = applyReload(resolvedRoot, requestedLocale, capture);
|
||||||
|
if (applied && notifyManualReload) {
|
||||||
|
notifyManualReload(capture.snapshot());
|
||||||
|
}
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized boolean reloadOverride(File override, String rawContent) {
|
||||||
|
File root = dataFolder;
|
||||||
|
if (root == null && IrisPlatforms.isBound()) {
|
||||||
|
root = IrisPlatforms.get().dataFolder();
|
||||||
|
}
|
||||||
|
if (root == null || override == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String configured = configuredLocale();
|
||||||
|
String requestedLocale;
|
||||||
|
try {
|
||||||
|
requestedLocale = normalizeLocale(configured);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
IrisLogging.error("Rejected locale setting '" + configured + "'; continuing with " + activeLocale + ".");
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
exception.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File expected = overrideFile(root, requestedLocale);
|
||||||
|
if (!expected.equals(override.getAbsoluteFile())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocaleHotloadSnapshot snapshot = rawContent == null
|
||||||
|
? LocaleHotloadSnapshot.missing(expected, requestedLocale)
|
||||||
|
: LocaleHotloadSnapshot.present(
|
||||||
|
expected,
|
||||||
|
requestedLocale,
|
||||||
|
rawContent,
|
||||||
|
sha256(rawContent.getBytes(StandardCharsets.UTF_8))
|
||||||
|
);
|
||||||
|
return applyReload(root.getAbsoluteFile(), requestedLocale, new SnapshotCapture(snapshot, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isActiveOverrideFile(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
File root = dataFolder;
|
||||||
|
if (root == null && IrisPlatforms.isBound()) {
|
||||||
|
root = IrisPlatforms.get().dataFolder();
|
||||||
|
}
|
||||||
|
if (root == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return overrideFile(root, configuredLocale()).equals(file.getAbsoluteFile());
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean applyReload(File root, String requestedLocale, SnapshotCapture capture) {
|
||||||
LocalizationReloadResult result;
|
LocalizationReloadResult result;
|
||||||
if (capture.failure() == null) {
|
if (capture.failure() == null) {
|
||||||
result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale, capture.snapshot()));
|
result = MANAGER.reload(() -> loadCandidate(root, requestedLocale, capture.snapshot()));
|
||||||
} else {
|
} else {
|
||||||
result = MANAGER.reload(() -> {
|
result = MANAGER.reload(() -> {
|
||||||
throw capture.failure();
|
throw capture.failure();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dataFolder = resolvedRoot;
|
dataFolder = root;
|
||||||
HOTLOAD_GATE.reset(capture.snapshot());
|
|
||||||
lastCaptureFailureKey = null;
|
|
||||||
if (!result.applied()) {
|
if (!result.applied()) {
|
||||||
reportRejectedReload(requestedLocale, result);
|
reportRejectedReload(requestedLocale, result);
|
||||||
return false;
|
return false;
|
||||||
@@ -158,56 +219,18 @@ public final class IrisLanguage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized boolean update() {
|
private static void notifyManualReload(LocaleHotloadSnapshot snapshot) {
|
||||||
File root = dataFolder;
|
for (BiConsumer<File, String> listener : MANUAL_RELOAD_LISTENERS) {
|
||||||
if (root == null) {
|
try {
|
||||||
return initialize();
|
listener.accept(snapshot.file(), snapshot.content());
|
||||||
}
|
} catch (RuntimeException failure) {
|
||||||
String locale;
|
IrisLogging.error("Failed to acknowledge a manual locale reload: "
|
||||||
try {
|
+ failure.getClass().getSimpleName()
|
||||||
locale = normalizeLocale(configuredLocale());
|
+ (failure.getMessage() == null ? "" : " - " + failure.getMessage()));
|
||||||
} catch (RuntimeException exception) {
|
IrisLogging.reportError(failure);
|
||||||
String invalidLocale = configuredLocale();
|
failure.printStackTrace();
|
||||||
if (Objects.equals(lastInvalidHotloadLocale, invalidLocale)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
lastInvalidHotloadLocale = invalidLocale;
|
|
||||||
return reload(root, invalidLocale);
|
|
||||||
}
|
}
|
||||||
lastInvalidHotloadLocale = null;
|
|
||||||
File expected = overrideFile(root, locale);
|
|
||||||
LocaleHotloadSnapshot snapshot;
|
|
||||||
try {
|
|
||||||
snapshot = captureHotloadSnapshot(expected, locale);
|
|
||||||
} catch (IOException | RuntimeException failure) {
|
|
||||||
HOTLOAD_GATE.unavailable();
|
|
||||||
reportCaptureFailure(expected, failure);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (snapshot == null) {
|
|
||||||
HOTLOAD_GATE.unavailable();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
lastCaptureFailureKey = null;
|
|
||||||
|
|
||||||
LocaleHotloadGate.Attempt attempt = HOTLOAD_GATE.observe(snapshot, System.nanoTime());
|
|
||||||
if (attempt == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(root, locale, attempt.snapshot()));
|
|
||||||
boolean applied = result.applied();
|
|
||||||
HOTLOAD_GATE.complete(attempt, System.nanoTime(), applied);
|
|
||||||
if (!applied) {
|
|
||||||
reportRejectedReload(locale, result);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeLocale = locale;
|
|
||||||
int warnings = result.validation().warnings().size();
|
|
||||||
IrisLogging.info("Loaded locale " + locale + " with " + warnings + " fallback "
|
|
||||||
+ (warnings == 1 ? "entry" : "entries") + ".");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String activeLocale() {
|
public static String activeLocale() {
|
||||||
@@ -606,18 +629,6 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void reportCaptureFailure(File file, Exception failure) {
|
|
||||||
String detail = failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
|
|
||||||
String failureKey = file.getAbsolutePath() + ":" + failure.getClass().getName() + ":" + detail;
|
|
||||||
if (Objects.equals(failureKey, lastCaptureFailureKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastCaptureFailureKey = failureKey;
|
|
||||||
IrisLogging.error("Failed to capture stable locale override " + file.getPath() + ": " + detail);
|
|
||||||
IrisLogging.reportError(failure);
|
|
||||||
failure.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void reportRejectedReload(String locale, LocalizationReloadResult result) {
|
private static void reportRejectedReload(String locale, LocalizationReloadResult result) {
|
||||||
IrisLogging.error("Rejected locale reload for " + locale + "; continuing with " + activeLocale + ".");
|
IrisLogging.error("Rejected locale reload for " + locale + "; continuing with " + activeLocale + ".");
|
||||||
List<LocalizationIssue> issues = result.validation().errors();
|
List<LocalizationIssue> issues = result.validation().errors();
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
package art.arcane.iris.core.localization;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
final class LocaleHotloadGate {
|
|
||||||
private final Timing timing;
|
|
||||||
|
|
||||||
private long generation;
|
|
||||||
private LocaleHotloadSnapshot baseline;
|
|
||||||
private LocaleHotloadSnapshot observed;
|
|
||||||
private long observedSinceNanos;
|
|
||||||
private LocaleHotloadSnapshot pending;
|
|
||||||
private Attempt inFlight;
|
|
||||||
private long lastCompletedAtNanos;
|
|
||||||
private boolean hasCompleted;
|
|
||||||
|
|
||||||
LocaleHotloadGate(Timing timing) {
|
|
||||||
this.timing = Objects.requireNonNull(timing, "Locale hotload timing cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized Attempt observe(LocaleHotloadSnapshot snapshot, long nowNanos) {
|
|
||||||
LocaleHotloadSnapshot current = Objects.requireNonNull(snapshot, "Locale hotload snapshot cannot be null");
|
|
||||||
if (!current.equals(observed)) {
|
|
||||||
observed = current;
|
|
||||||
observedSinceNanos = nowNanos;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long stabilityNanos = current.missing()
|
|
||||||
? timing.deletionGraceNanos()
|
|
||||||
: timing.contentStabilityNanos();
|
|
||||||
if (!elapsed(nowNanos, observedSinceNanos, stabilityNanos)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = current.equals(baseline) ? null : current;
|
|
||||||
if (pending == null || inFlight != null || !pending.equals(observed)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (hasCompleted && !elapsed(nowNanos, lastCompletedAtNanos, timing.cooldownNanos())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Attempt attempt = new Attempt(generation, pending);
|
|
||||||
inFlight = attempt;
|
|
||||||
return attempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void unavailable() {
|
|
||||||
observed = null;
|
|
||||||
observedSinceNanos = 0L;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void complete(Attempt attempt, long nowNanos, boolean applied) {
|
|
||||||
if (attempt == null || inFlight == null || !inFlight.equals(attempt) || attempt.generation() != generation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inFlight = null;
|
|
||||||
lastCompletedAtNanos = nowNanos;
|
|
||||||
hasCompleted = true;
|
|
||||||
if (!applied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
baseline = attempt.snapshot();
|
|
||||||
if (attempt.snapshot().equals(pending)) {
|
|
||||||
pending = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized void reset(LocaleHotloadSnapshot snapshot) {
|
|
||||||
generation++;
|
|
||||||
baseline = snapshot;
|
|
||||||
observed = snapshot;
|
|
||||||
observedSinceNanos = 0L;
|
|
||||||
pending = null;
|
|
||||||
inFlight = null;
|
|
||||||
lastCompletedAtNanos = 0L;
|
|
||||||
hasCompleted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean elapsed(long nowNanos, long startNanos, long durationNanos) {
|
|
||||||
return nowNanos - startNanos >= durationNanos;
|
|
||||||
}
|
|
||||||
|
|
||||||
record Timing(long contentStabilityNanos, long deletionGraceNanos, long cooldownNanos) {
|
|
||||||
Timing {
|
|
||||||
if (contentStabilityNanos < 0L || deletionGraceNanos < 0L || cooldownNanos < 0L) {
|
|
||||||
throw new IllegalArgumentException("Locale hotload timing cannot be negative");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record Attempt(long generation, LocaleHotloadSnapshot snapshot) {
|
|
||||||
Attempt {
|
|
||||||
snapshot = Objects.requireNonNull(snapshot, "Locale hotload attempt snapshot cannot be null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,7 @@ import art.arcane.volmlib.util.math.Position2;
|
|||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@@ -61,10 +62,14 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, PlatformBlockState> listener, CarveResult c, IrisData rdata) {
|
int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, PlatformBlockState> listener, CarveResult c, IrisData rdata) {
|
||||||
|
Objects.requireNonNull(oplacer, "Object placer is required.");
|
||||||
|
Objects.requireNonNull(config, "Object placement config is required.");
|
||||||
|
Objects.requireNonNull(rng, "Object placement RNG is required.");
|
||||||
|
Objects.requireNonNull(rdata, "Object placement data is required.");
|
||||||
IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer;
|
IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer;
|
||||||
|
|
||||||
boolean evaluateSlopeCondition = !config.isForcePlace() && !config.getSlopeCondition().isDefault();
|
boolean evaluateSlopeCondition = !config.isForcePlace() && !config.getSlopeCondition().isDefault();
|
||||||
if (rdata != null && (evaluateSlopeCondition || config.isRotateTowardsSlope())) {
|
if (evaluateSlopeCondition || config.isRotateTowardsSlope()) {
|
||||||
Engine placementEngine = requireSlopeEngine(placer);
|
Engine placementEngine = requireSlopeEngine(placer);
|
||||||
if (evaluateSlopeCondition &&
|
if (evaluateSlopeCondition &&
|
||||||
!config.getSlopeCondition().isValid(placementEngine.getComplex().getSlopeStream().get(x, z))) {
|
!config.getSlopeCondition().isValid(placementEngine.getComplex().getSlopeStream().get(x, z))) {
|
||||||
@@ -108,7 +113,7 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.isSmartBore()) {
|
if (config.isSmartBore()) {
|
||||||
IrisObjectShaping.ensureSmartBored(self, placer.isDebugSmartBore());
|
IrisObjectShaping.ensureSmartBored(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean warped = !config.getWarp().isFlat();
|
boolean warped = !config.getWarp().isFlat();
|
||||||
@@ -148,6 +153,8 @@ final class IrisObjectPlacementRunner {
|
|||||||
int xx, zz;
|
int xx, zz;
|
||||||
int yrand = config.getTranslate().getYRandom();
|
int yrand = config.getTranslate().getYRandom();
|
||||||
yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand;
|
yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand;
|
||||||
|
int warpMargin = warped ? (int) Math.ceil(Math.abs(config.getWarp().getMultiplier()) / 2D) : 0;
|
||||||
|
TransformedBounds placementBounds = transformedBounds(spin, translating, translateOffset, ceilingHang, warpMargin);
|
||||||
boolean bail = false;
|
boolean bail = false;
|
||||||
|
|
||||||
if (config.isFromBottom()) {
|
if (config.isFromBottom()) {
|
||||||
@@ -168,14 +175,10 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) {
|
} else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) {
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
int minX = x + placementBounds.minX();
|
||||||
IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone();
|
int maxX = x + placementBounds.maxX();
|
||||||
int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX();
|
int minZ = z + placementBounds.minZ();
|
||||||
int minX = Math.min(x - xLength, x + xLength);
|
int maxZ = z + placementBounds.maxZ();
|
||||||
int maxX = Math.max(x - xLength, x + xLength);
|
|
||||||
int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ();
|
|
||||||
int minZ = Math.min(z - zLength, z + zLength);
|
|
||||||
int maxZ = Math.max(z - zLength, z + zLength);
|
|
||||||
for (int i = minX; i <= maxX; i++) {
|
for (int i = minX; i <= maxX; i++) {
|
||||||
for (int ii = minZ; ii <= maxZ; ii++) {
|
for (int ii = minZ; ii <= maxZ; ii++) {
|
||||||
int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty;
|
int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty;
|
||||||
@@ -190,17 +193,12 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) {
|
} else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) {
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
int minX = x + placementBounds.minX();
|
||||||
IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone();
|
int maxX = x + placementBounds.maxX();
|
||||||
|
int minZ = z + placementBounds.minZ();
|
||||||
int xRadius = (rotatedDimensions.getBlockX() / 2);
|
int maxZ = z + placementBounds.maxZ();
|
||||||
int xLength = xRadius + offset.getBlockX();
|
int xRadius = Math.max(0, (maxX - minX) / 2);
|
||||||
int minX = Math.min(x - xLength, x + xLength);
|
int zRadius = Math.max(0, (maxZ - minZ) / 2);
|
||||||
int maxX = Math.max(x - xLength, x + xLength);
|
|
||||||
int zRadius = (rotatedDimensions.getBlockZ() / 2);
|
|
||||||
int zLength = zRadius + offset.getBlockZ();
|
|
||||||
int minZ = Math.min(z - zLength, z + zLength);
|
|
||||||
int maxZ = Math.max(z - zLength, z + zLength);
|
|
||||||
|
|
||||||
for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) {
|
for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) {
|
||||||
for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) {
|
for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) {
|
||||||
@@ -216,16 +214,11 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) {
|
} else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) {
|
||||||
y = rdata.getEngine().getHeight() + 1;
|
y = requireDataEngine(rdata, "minimum-height mode").getHeight() + 1;
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
int minX = x + placementBounds.minX();
|
||||||
IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone();
|
int maxX = x + placementBounds.maxX();
|
||||||
|
int minZ = z + placementBounds.minZ();
|
||||||
int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX();
|
int maxZ = z + placementBounds.maxZ();
|
||||||
int minX = Math.min(x - xLength, x + xLength);
|
|
||||||
int maxX = Math.max(x - xLength, x + xLength);
|
|
||||||
int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ();
|
|
||||||
int minZ = Math.min(z - zLength, z + zLength);
|
|
||||||
int maxZ = Math.max(z - zLength, z + zLength);
|
|
||||||
for (int i = minX; i <= maxX; i++) {
|
for (int i = minX; i <= maxX; i++) {
|
||||||
for (int ii = minZ; ii <= maxZ; ii++) {
|
for (int ii = minZ; ii <= maxZ; ii++) {
|
||||||
int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty;
|
int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty;
|
||||||
@@ -241,18 +234,13 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) {
|
} else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) {
|
||||||
y = rdata.getEngine().getHeight() + 1;
|
y = requireDataEngine(rdata, "fast minimum-height mode").getHeight() + 1;
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
int minX = x + placementBounds.minX();
|
||||||
IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone();
|
int maxX = x + placementBounds.maxX();
|
||||||
|
int minZ = z + placementBounds.minZ();
|
||||||
int xRadius = (rotatedDimensions.getBlockX() / 2);
|
int maxZ = z + placementBounds.maxZ();
|
||||||
int xLength = xRadius + offset.getBlockX();
|
int xRadius = Math.max(0, (maxX - minX) / 2);
|
||||||
int minX = Math.min(x - xLength, x + xLength);
|
int zRadius = Math.max(0, (maxZ - minZ) / 2);
|
||||||
int maxX = Math.max(x - xLength, x + xLength);
|
|
||||||
int zRadius = (rotatedDimensions.getBlockZ() / 2);
|
|
||||||
int zLength = zRadius + offset.getBlockZ();
|
|
||||||
int minZ = Math.min(z - zLength, z + zLength);
|
|
||||||
int maxZ = Math.max(z - zLength, z + zLength);
|
|
||||||
|
|
||||||
for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) {
|
for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) {
|
||||||
for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) {
|
for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) {
|
||||||
@@ -352,18 +340,23 @@ final class IrisObjectPlacementRunner {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int warpMargin = warped ? (int) Math.ceil(Math.abs(config.getWarp().getMultiplier()) / 2D) : 0;
|
|
||||||
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
|
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
|
||||||
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
|
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean paint = yv < 0 && config.getMode() == ObjectPlaceMode.PAINT;
|
||||||
|
WorldBounds worldBounds = null;
|
||||||
|
if (config.isBore() || (!config.isForcePlace() && !rawStructurePiece
|
||||||
|
&& (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty()))) {
|
||||||
|
worldBounds = resolveWorldBounds(placer, config, placementBounds, paint, x, y + yrand, z);
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
|
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
|
||||||
Engine engine = rdata.getEngine();
|
Engine engine = requireDataEngine(rdata, "collision settings");
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) {
|
||||||
for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) {
|
for (int j = worldBounds.minY(); j <= worldBounds.maxY(); j++) {
|
||||||
for (int j = y - Math.floorDiv(self.h, 2) + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) {
|
for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) {
|
||||||
for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) {
|
|
||||||
PlacedObject p = engine.getObjectPlacement(i, j, k);
|
PlacedObject p = engine.getObjectPlacement(i, j, k);
|
||||||
if (p == null) continue;
|
if (p == null) continue;
|
||||||
IrisObject o = p.getObject();
|
IrisObject o = p.getObject();
|
||||||
@@ -379,11 +372,12 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y += yrand;
|
||||||
|
|
||||||
if (config.isBore()) {
|
if (config.isBore()) {
|
||||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
for (int i = worldBounds.minX(); i <= worldBounds.maxX(); i++) {
|
||||||
for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) {
|
for (int j = worldBounds.minY() - config.getBoreExtendMinY(); j <= worldBounds.maxY() + config.getBoreExtendMaxY(); j++) {
|
||||||
for (int j = y - Math.floorDiv(self.h, 2) - config.getBoreExtendMinY() + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) + config.getBoreExtendMaxY() - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) {
|
for (int k = worldBounds.minZ(); k <= worldBounds.maxZ(); k++) {
|
||||||
for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) {
|
|
||||||
placer.set(i, j, k, IrisObject.States.AIR);
|
placer.set(i, j, k, IrisObject.States.AIR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +388,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
int topLayer = Integer.MIN_VALUE;
|
int topLayer = Integer.MIN_VALUE;
|
||||||
int vacuumLowest = Integer.MAX_VALUE;
|
int vacuumLowest = Integer.MAX_VALUE;
|
||||||
int vacuumHighest = Integer.MIN_VALUE;
|
int vacuumHighest = Integer.MIN_VALUE;
|
||||||
y += yrand;
|
|
||||||
self.readLock.lock();
|
self.readLock.lock();
|
||||||
|
|
||||||
KMap<IrisBlockVector, String> markers = null;
|
KMap<IrisBlockVector, String> markers = null;
|
||||||
@@ -407,7 +400,7 @@ final class IrisObjectPlacementRunner {
|
|||||||
|
|
||||||
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
|
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
|
||||||
markers = new KMap<>();
|
markers = new KMap<>();
|
||||||
var list = StreamSupport.stream(blocks.keys().spliterator(), false)
|
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
|
||||||
.collect(KList.collector());
|
.collect(KList.collector());
|
||||||
// Marker selection persists into the mantle, so it must be seed-deterministic.
|
// Marker selection persists into the mantle, so it must be seed-deterministic.
|
||||||
// Derive a side stream keyed on the placement position instead of consuming the
|
// Derive a side stream keyed on the placement position instead of consuming the
|
||||||
@@ -476,6 +469,10 @@ final class IrisObjectPlacementRunner {
|
|||||||
d = IrisObject.States.AIR;
|
d = IrisObject.States.AIR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (placer.isDebugSmartBore() && IrisObject.States.VAIR.equals(d)) {
|
||||||
|
d = IrisObject.States.VAIR_DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
PlatformBlockState data = d;
|
PlatformBlockState data = d;
|
||||||
IrisBlockVector i = g.clone();
|
IrisBlockVector i = g.clone();
|
||||||
spin.rotate(i);
|
spin.rotate(i);
|
||||||
@@ -536,18 +533,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater());
|
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heightmap != null) {
|
|
||||||
Position2 pos = new Position2(xx, zz);
|
|
||||||
|
|
||||||
if (!heightmap.containsKey(pos)) {
|
|
||||||
heightmap.put(pos, yy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heightmap.get(pos) < yy) {
|
|
||||||
heightmap.put(pos, yy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) {
|
if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -569,6 +554,13 @@ final class IrisObjectPlacementRunner {
|
|||||||
|
|
||||||
if (data.isCustom() || place) {
|
if (data.isCustom() || place) {
|
||||||
placer.set(xx, yy, zz, data);
|
placer.set(xx, yy, zz, data);
|
||||||
|
if (heightmap != null) {
|
||||||
|
Position2 pos = new Position2(xx, zz);
|
||||||
|
Integer currentHeight = heightmap.get(pos);
|
||||||
|
if (currentHeight == null || currentHeight < yy) {
|
||||||
|
heightmap.put(pos, yy);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (tile != null) {
|
if (tile != null) {
|
||||||
placer.setTile(xx, yy, zz, tile);
|
placer.setTile(xx, yy, zz, tile);
|
||||||
}
|
}
|
||||||
@@ -586,9 +578,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
} finally {
|
} finally {
|
||||||
self.readLock.unlock();
|
self.readLock.unlock();
|
||||||
}
|
}
|
||||||
@@ -832,14 +821,8 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
||||||
IrisBlockVector rotDim = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone();
|
vacuumTerrain(placer, config, x, z, placementBounds.minX(), placementBounds.maxX(),
|
||||||
int lowX = IrisObjectVacuum.footprintLow(rotDim.getBlockX());
|
placementBounds.minZ(), placementBounds.maxZ(), vacuumLowest, vacuumHighest);
|
||||||
int highX = IrisObjectVacuum.footprintHigh(rotDim.getBlockX());
|
|
||||||
int lowZ = IrisObjectVacuum.footprintLow(rotDim.getBlockZ());
|
|
||||||
int highZ = IrisObjectVacuum.footprintHigh(rotDim.getBlockZ());
|
|
||||||
int centerX = x + config.getTranslate().getX();
|
|
||||||
int centerZ = z + config.getTranslate().getZ();
|
|
||||||
vacuumTerrain(placer, config, centerX, centerZ, lowX, highX, lowZ, highZ, vacuumLowest, vacuumHighest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heightmap != null) {
|
if (heightmap != null) {
|
||||||
@@ -864,10 +847,91 @@ final class IrisObjectPlacementRunner {
|
|||||||
return !wouldReplace && (rawStructurePiece || !air);
|
return !wouldReplace && (rawStructurePiece || !air);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TransformedBounds transformedBounds(SpinKernel spin, boolean translating, IrisBlockVector translateOffset,
|
||||||
|
boolean ceilingHang, int margin) {
|
||||||
|
int sourceMinX = -self.getCenter().getBlockX();
|
||||||
|
int sourceMaxX = self.getW() - self.getCenter().getBlockX() - 1;
|
||||||
|
int sourceMinY = -self.getCenter().getBlockY();
|
||||||
|
int sourceMaxY = self.getH() - self.getCenter().getBlockY() - 1;
|
||||||
|
int sourceMinZ = -self.getCenter().getBlockZ();
|
||||||
|
int sourceMaxZ = self.getD() - self.getCenter().getBlockZ() - 1;
|
||||||
|
int minX = Integer.MAX_VALUE;
|
||||||
|
int maxX = Integer.MIN_VALUE;
|
||||||
|
int minY = Integer.MAX_VALUE;
|
||||||
|
int maxY = Integer.MIN_VALUE;
|
||||||
|
int minZ = Integer.MAX_VALUE;
|
||||||
|
int maxZ = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (int xCorner = 0; xCorner < 2; xCorner++) {
|
||||||
|
int sourceX = xCorner == 0 ? sourceMinX : sourceMaxX;
|
||||||
|
for (int yCorner = 0; yCorner < 2; yCorner++) {
|
||||||
|
int sourceY = yCorner == 0 ? sourceMinY : sourceMaxY;
|
||||||
|
for (int zCorner = 0; zCorner < 2; zCorner++) {
|
||||||
|
int sourceZ = zCorner == 0 ? sourceMinZ : sourceMaxZ;
|
||||||
|
IrisBlockVector corner = new IrisBlockVector(sourceX, sourceY, sourceZ);
|
||||||
|
spin.rotate(corner);
|
||||||
|
if (ceilingHang) {
|
||||||
|
corner.setY(-corner.getBlockY());
|
||||||
|
}
|
||||||
|
if (translating) {
|
||||||
|
corner.add(translateOffset);
|
||||||
|
}
|
||||||
|
int transformedX = (int) Math.round(corner.getX());
|
||||||
|
int transformedY = (int) Math.round(corner.getY());
|
||||||
|
int transformedZ = (int) Math.round(corner.getZ());
|
||||||
|
minX = Math.min(minX, transformedX);
|
||||||
|
maxX = Math.max(maxX, transformedX);
|
||||||
|
minY = Math.min(minY, transformedY);
|
||||||
|
maxY = Math.max(maxY, transformedY);
|
||||||
|
minZ = Math.min(minZ, transformedZ);
|
||||||
|
maxZ = Math.max(maxZ, transformedZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TransformedBounds(minX - margin, maxX + margin, minY, maxY, minZ - margin, maxZ + margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldBounds resolveWorldBounds(IObjectPlacer placer, IrisObjectPlacement config, TransformedBounds bounds,
|
||||||
|
boolean paint, int x, int y, int z) {
|
||||||
|
int minX = x + bounds.minX();
|
||||||
|
int maxX = x + bounds.maxX();
|
||||||
|
int minZ = z + bounds.minZ();
|
||||||
|
int maxZ = z + bounds.maxZ();
|
||||||
|
if (!paint) {
|
||||||
|
return new WorldBounds(minX, maxX, y + bounds.minY(), y + bounds.maxY(), minZ, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
int minimumSurface = Integer.MAX_VALUE;
|
||||||
|
int maximumSurface = Integer.MIN_VALUE;
|
||||||
|
for (int worldX = minX; worldX <= maxX; worldX++) {
|
||||||
|
for (int worldZ = minZ; worldZ <= maxZ; worldZ++) {
|
||||||
|
int surface = placer.getHighest(worldX, worldZ, self.getLoader(), config.isUnderwater());
|
||||||
|
minimumSurface = Math.min(minimumSurface, surface);
|
||||||
|
maximumSurface = Math.max(maximumSurface, surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int paintOffset = Math.floorDiv(self.h, 2);
|
||||||
|
return new WorldBounds(minX, maxX, minimumSurface + bounds.minY() + paintOffset,
|
||||||
|
maximumSurface + bounds.maxY() + paintOffset, minZ, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
private static Engine requireSlopeEngine(IObjectPlacer placer) {
|
private static Engine requireSlopeEngine(IObjectPlacer placer) {
|
||||||
|
return requirePlacementEngine(placer, "slope settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine requirePlacementEngine(IObjectPlacer placer, String feature) {
|
||||||
Engine engine = placer.getEngine();
|
Engine engine = placer.getEngine();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
throw new IllegalStateException("Object placement slope settings require an active Iris engine for the target world.");
|
throw new IllegalStateException("Object placement requires an active Iris engine for " + feature + ".");
|
||||||
|
}
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine requireDataEngine(IrisData data, String feature) {
|
||||||
|
Engine engine = data.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
throw new IllegalStateException("Object placement requires active Iris data for " + feature + ".");
|
||||||
}
|
}
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
@@ -1229,4 +1293,10 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record TransformedBounds(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record WorldBounds(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class IrisObjectShaping {
|
|||||||
private IrisObjectShaping() {
|
private IrisObjectShaping() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ensureSmartBored(IrisObject self, boolean debug) {
|
static void ensureSmartBored(IrisObject self) {
|
||||||
if (self.smartBored) {
|
if (self.smartBored) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,15 +53,15 @@ final class IrisObjectShaping {
|
|||||||
if (self.smartBored) {
|
if (self.smartBored) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureSmartBoredLocked(self, debug);
|
ensureSmartBoredLocked(self);
|
||||||
} finally {
|
} finally {
|
||||||
self.writeLock.unlock();
|
self.writeLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureSmartBoredLocked(IrisObject self, boolean debug) {
|
private static void ensureSmartBoredLocked(IrisObject self) {
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
PlatformBlockState vair = debug ? IrisObject.States.VAIR_DEBUG : IrisObject.States.VAIR;
|
PlatformBlockState vair = IrisObject.States.VAIR;
|
||||||
AtomicInteger applied = new AtomicInteger();
|
AtomicInteger applied = new AtomicInteger();
|
||||||
IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE);
|
IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE);
|
||||||
IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.core.localization.IrisMessages;
|
||||||
|
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
||||||
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SettingsHotloadWatchTest {
|
||||||
|
private static final String PERMISSION = "iris.all";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private IrisSettings previousSettings;
|
||||||
|
private File dataFolder;
|
||||||
|
private File settingsFile;
|
||||||
|
private File overrideFolder;
|
||||||
|
private SettingsHotloadWatch watch;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
previousSettings = IrisSettings.settings;
|
||||||
|
dataFolder = temporaryFolder.newFolder("iris-hotload");
|
||||||
|
settingsFile = new File(dataFolder, "settings.json");
|
||||||
|
overrideFolder = new File(dataFolder, "languages/overrides");
|
||||||
|
Files.createDirectories(overrideFolder.toPath());
|
||||||
|
String settings = settings("en_US");
|
||||||
|
Files.writeString(settingsFile.toPath(), settings, StandardCharsets.UTF_8);
|
||||||
|
IrisSettings.settings = IrisSettings.parseHotloadSnapshot(settings);
|
||||||
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
|
watch = new SettingsHotloadWatch(
|
||||||
|
settingsFile,
|
||||||
|
new SettingsHotloadWatch.Timing(100L, 100L, 100L, 100L)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
if (watch != null) {
|
||||||
|
watch.close();
|
||||||
|
}
|
||||||
|
IrisLanguage.reload(dataFolder, "en_US");
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void activeLocaleSnapshotAppliesAndInvalidSnapshotRetainsLastGood() {
|
||||||
|
File override = override("en_US");
|
||||||
|
String valid = locale("en_US", "Active {permission}");
|
||||||
|
|
||||||
|
assertTrue(watch.applySnapshot(present(override, valid)));
|
||||||
|
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||||
|
|
||||||
|
assertFalse(watch.applySnapshot(present(override, "{ invalid")));
|
||||||
|
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void activeLocaleDeletionFallsBackToCodeOwnedEnglish() {
|
||||||
|
File override = override("en_US");
|
||||||
|
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Temporary {permission}"))));
|
||||||
|
assertEquals("Temporary " + PERMISSION, permissionMessage());
|
||||||
|
|
||||||
|
assertTrue(watch.applySnapshot(missing(override)));
|
||||||
|
|
||||||
|
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void inactiveLocaleSnapshotDoesNotChangeTheRuntimeCatalog() {
|
||||||
|
String before = permissionMessage();
|
||||||
|
|
||||||
|
assertTrue(watch.applySnapshot(present(
|
||||||
|
override("de_DE"),
|
||||||
|
locale("de_DE", "Inaktiv {permission}")
|
||||||
|
)));
|
||||||
|
|
||||||
|
assertEquals(before, permissionMessage());
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void settingsSnapshotChangesTheActiveLocaleFromItsImmutableOverride() throws Exception {
|
||||||
|
Files.writeString(
|
||||||
|
override("de_DE").toPath(),
|
||||||
|
locale("de_DE", "Berechtigung {permission}"),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
|
||||||
|
|
||||||
|
assertEquals("de_DE", IrisSettings.get().getGeneral().getLanguage());
|
||||||
|
assertEquals("de_DE", IrisLanguage.activeLocale());
|
||||||
|
assertEquals("Berechtigung " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidLocaleBlocksItsSettingsSwitchAndRetainsThePreviousRuntime() throws Exception {
|
||||||
|
Files.writeString(override("de_DE").toPath(), "{ invalid", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
assertFalse(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
|
||||||
|
|
||||||
|
assertEquals("en_US", IrisSettings.get().getGeneral().getLanguage());
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 8_000L)
|
||||||
|
public void sameMetadataLocaleReplacementStillHotloads() throws Exception {
|
||||||
|
File override = override("en_US");
|
||||||
|
FileTime fixedTime = FileTime.fromMillis(10_000L);
|
||||||
|
String first = locale("en_US", "First {permission}");
|
||||||
|
String second = locale("en_US", "Other {permission}");
|
||||||
|
assertEquals(first.getBytes(StandardCharsets.UTF_8).length, second.getBytes(StandardCharsets.UTF_8).length);
|
||||||
|
|
||||||
|
Files.writeString(override.toPath(), first, StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||||
|
awaitPermissionMessage("First " + PERMISSION);
|
||||||
|
|
||||||
|
Files.writeString(override.toPath(), second, StandardCharsets.UTF_8);
|
||||||
|
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||||
|
awaitPermissionMessage("Other " + PERMISSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 8_000L)
|
||||||
|
public void closeWaitsForInFlightSnapshotAndStopsLaterApplies() throws Exception {
|
||||||
|
File override = override("en_US");
|
||||||
|
CountDownLatch applyEntered = new CountDownLatch(1);
|
||||||
|
CountDownLatch releaseApply = new CountDownLatch(1);
|
||||||
|
AtomicInteger automaticApplies = new AtomicInteger();
|
||||||
|
replaceWatch(new SettingsHotloadWatch.Timing(100L, 100L, 100L, 100L), snapshot -> {
|
||||||
|
if (!override.equals(snapshot.file())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
automaticApplies.incrementAndGet();
|
||||||
|
applyEntered.countDown();
|
||||||
|
try {
|
||||||
|
releaseApply.await();
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while holding the hotload apply boundary", failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Files.writeString(override.toPath(), locale("en_US", "During {permission}"), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
Thread checker = new Thread(() -> checkUntilEntered(applyEntered), "Iris-Hotload-Lifecycle-Test");
|
||||||
|
checker.start();
|
||||||
|
assertTrue(applyEntered.await(5L, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
CountDownLatch closeStarted = new CountDownLatch(1);
|
||||||
|
AtomicBoolean closeCompleted = new AtomicBoolean();
|
||||||
|
Thread closer = new Thread(() -> {
|
||||||
|
closeStarted.countDown();
|
||||||
|
watch.close();
|
||||||
|
closeCompleted.set(true);
|
||||||
|
}, "Iris-Hotload-Close-Test");
|
||||||
|
closer.start();
|
||||||
|
assertTrue(closeStarted.await(1L, TimeUnit.SECONDS));
|
||||||
|
awaitThreadState(closer, Thread.State.BLOCKED);
|
||||||
|
assertFalse(closeCompleted.get());
|
||||||
|
|
||||||
|
releaseApply.countDown();
|
||||||
|
checker.join(2_000L);
|
||||||
|
closer.join(2_000L);
|
||||||
|
assertFalse(checker.isAlive());
|
||||||
|
assertFalse(closer.isAlive());
|
||||||
|
assertTrue(closeCompleted.get());
|
||||||
|
assertEquals("During " + PERMISSION, permissionMessage());
|
||||||
|
|
||||||
|
Files.writeString(override.toPath(), locale("en_US", "After {permission}"), StandardCharsets.UTF_8);
|
||||||
|
watch.checkConfigHotload();
|
||||||
|
assertEquals(1, automaticApplies.get());
|
||||||
|
assertEquals("During " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 8_000L)
|
||||||
|
public void manualReloadInvalidatesPendingAutomaticLocaleWork() throws Exception {
|
||||||
|
File override = override("en_US");
|
||||||
|
AtomicInteger automaticApplies = new AtomicInteger();
|
||||||
|
replaceWatch(
|
||||||
|
new SettingsHotloadWatch.Timing(100L, 400L, 100L, 100L),
|
||||||
|
snapshot -> {
|
||||||
|
if (override.equals(snapshot.file())) {
|
||||||
|
automaticApplies.incrementAndGet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Files.writeString(override.toPath(), locale("en_US", "First {permission}"), StandardCharsets.UTF_8);
|
||||||
|
awaitPermissionMessage("First " + PERMISSION);
|
||||||
|
assertEquals(1, automaticApplies.get());
|
||||||
|
|
||||||
|
Files.writeString(override.toPath(), locale("en_US", "Manual {permission}"), StandardCharsets.UTF_8);
|
||||||
|
pollFor(125L);
|
||||||
|
assertEquals("First " + PERMISSION, permissionMessage());
|
||||||
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
|
assertEquals("Manual " + PERMISSION, permissionMessage());
|
||||||
|
|
||||||
|
pollFor(650L);
|
||||||
|
assertEquals(1, automaticApplies.get());
|
||||||
|
assertEquals("Manual " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 8_000L)
|
||||||
|
public void oversizedActiveOverrideReportsOnceAndRetainsLastGoodCatalog() throws Exception {
|
||||||
|
File override = override("en_US");
|
||||||
|
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
|
||||||
|
byte[] oversized = new byte[2 * 1024 * 1024 + 1];
|
||||||
|
Files.write(override.toPath(), oversized);
|
||||||
|
|
||||||
|
String diagnostic = captureErrorsWhilePolling("Hotload file exceeds 2097152 bytes", 1_200L);
|
||||||
|
|
||||||
|
assertEquals(1, countOccurrences(
|
||||||
|
diagnostic,
|
||||||
|
"Failed to read watched Iris file " + override.getAbsolutePath()
|
||||||
|
));
|
||||||
|
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 8_000L)
|
||||||
|
public void malformedUtf8FailureIsDeduplicatedAndRetainsLastGoodCatalog() throws Exception {
|
||||||
|
File override = override("en_US");
|
||||||
|
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
|
||||||
|
Files.write(override.toPath(), new byte[]{(byte) 0xC3, 0x28});
|
||||||
|
|
||||||
|
String diagnostic = captureErrorsWhilePolling("Hotload file is not valid UTF-8", 1_200L);
|
||||||
|
|
||||||
|
assertEquals(1, countOccurrences(
|
||||||
|
diagnostic,
|
||||||
|
"Failed to read watched Iris file " + override.getAbsolutePath()
|
||||||
|
));
|
||||||
|
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bukkitAndModdedUseTheSameCoreCoordinator() throws Exception {
|
||||||
|
String bukkit = Files.readString(Path.of(
|
||||||
|
"../adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java"
|
||||||
|
));
|
||||||
|
String modded = Files.readString(Path.of(
|
||||||
|
"../adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java"
|
||||||
|
));
|
||||||
|
|
||||||
|
assertTrue(bukkit.contains("new SettingsHotloadWatch("));
|
||||||
|
assertTrue(modded.contains("new SettingsHotloadWatch("));
|
||||||
|
assertFalse(bukkit.contains("new ConfigHotloadEngine("));
|
||||||
|
assertFalse(modded.contains("new ConfigHotloadEngine("));
|
||||||
|
assertFalse(bukkit.contains("IrisLanguage.update()"));
|
||||||
|
assertFalse(modded.contains("IrisLanguage.update()"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitPermissionMessage(String expected) throws Exception {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(6L);
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
watch.checkConfigHotload();
|
||||||
|
if (expected.equals(permissionMessage())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Thread.sleep(25L);
|
||||||
|
}
|
||||||
|
assertEquals(expected, permissionMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceWatch(
|
||||||
|
SettingsHotloadWatch.Timing timing,
|
||||||
|
Consumer<ConfigHotloadEngine.StableContentSnapshot> beforeSnapshotApply
|
||||||
|
) {
|
||||||
|
watch.close();
|
||||||
|
watch = new SettingsHotloadWatch(settingsFile, timing, beforeSnapshotApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkUntilEntered(CountDownLatch entered) {
|
||||||
|
try {
|
||||||
|
while (entered.getCount() > 0L) {
|
||||||
|
watch.checkConfigHotload();
|
||||||
|
Thread.sleep(10L);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while waiting for a hotload snapshot", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
|
||||||
|
while (System.nanoTime() < deadline && thread.getState() != expected) {
|
||||||
|
Thread.sleep(5L);
|
||||||
|
}
|
||||||
|
assertEquals(expected, thread.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollFor(long durationMillis) throws InterruptedException {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
watch.checkConfigHotload();
|
||||||
|
Thread.sleep(25L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String captureErrorsWhilePolling(String expected, long durationMillis) throws Exception {
|
||||||
|
PrintStream originalError = System.err;
|
||||||
|
ByteArrayOutputStream captured = new ByteArrayOutputStream();
|
||||||
|
try (PrintStream capture = new PrintStream(captured, true, StandardCharsets.UTF_8)) {
|
||||||
|
System.setErr(capture);
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
|
||||||
|
boolean found = false;
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
watch.checkConfigHotload();
|
||||||
|
found |= captured.toString(StandardCharsets.UTF_8).contains(expected);
|
||||||
|
Thread.sleep(25L);
|
||||||
|
}
|
||||||
|
assertTrue(found);
|
||||||
|
} finally {
|
||||||
|
System.setErr(originalError);
|
||||||
|
}
|
||||||
|
return captured.toString(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countOccurrences(String value, String target) {
|
||||||
|
int count = 0;
|
||||||
|
int cursor = 0;
|
||||||
|
while ((cursor = value.indexOf(target, cursor)) >= 0) {
|
||||||
|
count++;
|
||||||
|
cursor += target.length();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigHotloadEngine.StableContentSnapshot present(File file, String content) {
|
||||||
|
return new ConfigHotloadEngine.StableContentSnapshot(file, "present", content.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigHotloadEngine.StableContentSnapshot missing(File file) {
|
||||||
|
return new ConfigHotloadEngine.StableContentSnapshot(file, "missing", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File override(String locale) {
|
||||||
|
return new File(overrideFolder, locale + ".json").getAbsoluteFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String permissionMessage() {
|
||||||
|
return IrisLanguage.plain(
|
||||||
|
IrisMessages.COMMAND_PERMISSION_DENIED,
|
||||||
|
MessageArgument.untrusted("permission", PERMISSION)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String settings(String locale) {
|
||||||
|
return "{\"general\":{\"language\":\"" + locale + "\"}}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String locale(String locale, String permissionMessage) {
|
||||||
|
return "{\"locale\":\"" + locale + "\",\"messages\":{"
|
||||||
|
+ "\"iris.command.permission_denied\":\"" + permissionMessage + "\"}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package art.arcane.iris.core.localization;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNotEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
|
|
||||||
public class LocaleHotloadGateTest {
|
|
||||||
@Test
|
|
||||||
public void anchorsCooldownAtCompletedApplication() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot initial = snapshot("initial");
|
|
||||||
LocaleHotloadSnapshot first = snapshot("first");
|
|
||||||
LocaleHotloadSnapshot second = snapshot("second");
|
|
||||||
gate.reset(initial);
|
|
||||||
|
|
||||||
assertNull(gate.observe(first, 0L));
|
|
||||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
|
||||||
assertNotNull(firstAttempt);
|
|
||||||
gate.complete(firstAttempt, 500L, true);
|
|
||||||
|
|
||||||
assertNull(gate.observe(second, 600L));
|
|
||||||
assertNull(gate.observe(second, 700L));
|
|
||||||
assertNull(gate.observe(second, 3_499L));
|
|
||||||
LocaleHotloadGate.Attempt secondAttempt = gate.observe(second, 3_500L);
|
|
||||||
|
|
||||||
assertNotNull(secondAttempt);
|
|
||||||
assertEquals(second, secondAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void coalescesCooldownBurstToLatestStableSnapshot() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot first = snapshot("first");
|
|
||||||
LocaleHotloadSnapshot intermediate = snapshot("intermediate");
|
|
||||||
LocaleHotloadSnapshot latest = snapshot("latest");
|
|
||||||
gate.reset(snapshot("initial"));
|
|
||||||
|
|
||||||
assertNull(gate.observe(first, 0L));
|
|
||||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
|
||||||
assertNotNull(firstAttempt);
|
|
||||||
gate.complete(firstAttempt, 200L, true);
|
|
||||||
|
|
||||||
assertNull(gate.observe(intermediate, 250L));
|
|
||||||
assertNull(gate.observe(intermediate, 350L));
|
|
||||||
assertNull(gate.observe(latest, 400L));
|
|
||||||
assertNull(gate.observe(latest, 500L));
|
|
||||||
assertNull(gate.observe(latest, 3_199L));
|
|
||||||
LocaleHotloadGate.Attempt latestAttempt = gate.observe(latest, 3_200L);
|
|
||||||
|
|
||||||
assertNotNull(latestAttempt);
|
|
||||||
assertEquals(latest, latestAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void requiresDeletionGraceAndCancelsTransientMissingSnapshot() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot initial = snapshot("initial");
|
|
||||||
LocaleHotloadSnapshot replacement = snapshot("replacement");
|
|
||||||
LocaleHotloadSnapshot missing = LocaleHotloadSnapshot.missing(initial.file(), "en_US");
|
|
||||||
gate.reset(initial);
|
|
||||||
|
|
||||||
assertNull(gate.observe(missing, 0L));
|
|
||||||
assertNull(gate.observe(missing, 499L));
|
|
||||||
assertNull(gate.observe(replacement, 500L));
|
|
||||||
LocaleHotloadGate.Attempt replacementAttempt = gate.observe(replacement, 600L);
|
|
||||||
assertNotNull(replacementAttempt);
|
|
||||||
assertEquals(replacement, replacementAttempt.snapshot());
|
|
||||||
gate.complete(replacementAttempt, 600L, true);
|
|
||||||
|
|
||||||
assertNull(gate.observe(missing, 700L));
|
|
||||||
assertNull(gate.observe(missing, 1_199L));
|
|
||||||
assertNull(gate.observe(missing, 1_200L));
|
|
||||||
LocaleHotloadGate.Attempt deletionAttempt = gate.observe(missing, 3_600L);
|
|
||||||
|
|
||||||
assertNotNull(deletionAttempt);
|
|
||||||
assertEquals(missing, deletionAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unavailableReadBlocksStalePendingSnapshot() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot first = snapshot("first");
|
|
||||||
LocaleHotloadSnapshot pending = snapshot("pending");
|
|
||||||
gate.reset(snapshot("initial"));
|
|
||||||
|
|
||||||
assertNull(gate.observe(first, 0L));
|
|
||||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
|
||||||
assertNotNull(firstAttempt);
|
|
||||||
gate.complete(firstAttempt, 200L, true);
|
|
||||||
assertNull(gate.observe(pending, 300L));
|
|
||||||
assertNull(gate.observe(pending, 400L));
|
|
||||||
|
|
||||||
gate.unavailable();
|
|
||||||
assertNull(gate.observe(pending, 3_200L));
|
|
||||||
LocaleHotloadGate.Attempt recoveredAttempt = gate.observe(pending, 3_300L);
|
|
||||||
|
|
||||||
assertNotNull(recoveredAttempt);
|
|
||||||
assertEquals(pending, recoveredAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void failedApplicationRetriesOnlyAfterCooldown() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot changed = snapshot("changed");
|
|
||||||
gate.reset(snapshot("initial"));
|
|
||||||
|
|
||||||
assertNull(gate.observe(changed, 0L));
|
|
||||||
LocaleHotloadGate.Attempt failedAttempt = gate.observe(changed, 100L);
|
|
||||||
assertNotNull(failedAttempt);
|
|
||||||
gate.complete(failedAttempt, 500L, false);
|
|
||||||
|
|
||||||
assertNull(gate.observe(changed, 3_499L));
|
|
||||||
LocaleHotloadGate.Attempt retryAttempt = gate.observe(changed, 3_500L);
|
|
||||||
|
|
||||||
assertNotNull(retryAttempt);
|
|
||||||
assertEquals(changed, retryAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void manualResetInvalidatesInFlightAutomaticAttempt() {
|
|
||||||
LocaleHotloadGate gate = gate();
|
|
||||||
LocaleHotloadSnapshot automatic = snapshot("automatic");
|
|
||||||
LocaleHotloadSnapshot manual = snapshot("manual");
|
|
||||||
LocaleHotloadSnapshot changed = snapshot("changed");
|
|
||||||
gate.reset(snapshot("initial"));
|
|
||||||
|
|
||||||
assertNull(gate.observe(automatic, 0L));
|
|
||||||
LocaleHotloadGate.Attempt staleAttempt = gate.observe(automatic, 100L);
|
|
||||||
assertNotNull(staleAttempt);
|
|
||||||
gate.reset(manual);
|
|
||||||
gate.complete(staleAttempt, 200L, true);
|
|
||||||
|
|
||||||
assertNull(gate.observe(manual, 200L));
|
|
||||||
assertNull(gate.observe(changed, 300L));
|
|
||||||
LocaleHotloadGate.Attempt currentAttempt = gate.observe(changed, 400L);
|
|
||||||
|
|
||||||
assertNotNull(currentAttempt);
|
|
||||||
assertNotEquals(staleAttempt.generation(), currentAttempt.generation());
|
|
||||||
assertEquals(changed, currentAttempt.snapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocaleHotloadGate gate() {
|
|
||||||
return new LocaleHotloadGate(new LocaleHotloadGate.Timing(100L, 500L, 3_000L));
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocaleHotloadSnapshot snapshot(String content) {
|
|
||||||
return LocaleHotloadSnapshot.present(
|
|
||||||
new File("locale.json"),
|
|
||||||
"en_US",
|
|
||||||
content,
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+326
@@ -0,0 +1,326 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.PlacedObject;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
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.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisObjectPlacementRunnerRegressionTest {
|
||||||
|
private static final int ANCHOR_Y = 80;
|
||||||
|
|
||||||
|
private IrisData data;
|
||||||
|
private Engine engine;
|
||||||
|
private PlatformBlockState solid;
|
||||||
|
private PlatformBlockState schematicAir;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
Map<String, PlatformBlockState> registryStates = new HashMap<>();
|
||||||
|
when(registries.block(anyString())).thenAnswer(invocation -> {
|
||||||
|
String key = invocation.getArgument(0);
|
||||||
|
return registryStates.computeIfAbsent(key, value -> state(value.toLowerCase(), !value.toLowerCase().contains("air")));
|
||||||
|
});
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
|
||||||
|
engine = mock(Engine.class);
|
||||||
|
when(engine.getHeight()).thenReturn(256);
|
||||||
|
data = mock(IrisData.class);
|
||||||
|
when(data.getEngine()).thenReturn(engine);
|
||||||
|
solid = state("minecraft:stone", true);
|
||||||
|
schematicAir = state("minecraft:air", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void placementFailurePropagatesToTheCaller() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(null);
|
||||||
|
placer.failAfterWrites(1);
|
||||||
|
IrisObject object = lineObject(3);
|
||||||
|
|
||||||
|
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||||
|
() -> object.place(0, ANCHOR_Y, 0, placer, placement(), new RNG(2L), data));
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("write failed"));
|
||||||
|
assertEquals(1, placer.writes().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boreUsesRotatedTranslationAndRandomizedAnchor() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(null);
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setBore(true);
|
||||||
|
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
|
||||||
|
placement.setTranslate(new IrisObjectTranslate().setX(4).setYRandom(3));
|
||||||
|
|
||||||
|
int resultY = lineObject(3).place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||||
|
|
||||||
|
List<BlockWrite> airWrites = placer.writesOf(IrisObject.States.AIR);
|
||||||
|
assertFalse(airWrites.isEmpty());
|
||||||
|
assertTrue(airWrites.stream().allMatch(write -> write.y() >= resultY && write.y() <= resultY));
|
||||||
|
assertTrue(airWrites.stream().allMatch(write -> write.x() == 0));
|
||||||
|
assertTrue(airWrites.stream().allMatch(write -> write.z() <= -3 && write.z() >= -5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collisionCheckUsesTheTransformedBounds() {
|
||||||
|
PlacedObject forbidden = new PlacedObject(null, object("forbidden"), 1, 0, 0);
|
||||||
|
when(engine.getObjectPlacement(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
int x = invocation.getArgument(0);
|
||||||
|
int y = invocation.getArgument(1);
|
||||||
|
int z = invocation.getArgument(2);
|
||||||
|
return x == 0 && y == ANCHOR_Y && z == -4 ? forbidden : null;
|
||||||
|
});
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
|
||||||
|
placement.setTranslate(new IrisObjectTranslate().setX(4));
|
||||||
|
placement.getForbiddenCollisions().add("forbidden");
|
||||||
|
|
||||||
|
int result = lineObject(3).place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||||
|
|
||||||
|
assertEquals(-1, result);
|
||||||
|
assertTrue(placer.writes().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void arbitraryRotationSamplesEveryTransformedFootprintColumn() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(null);
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setMode(ObjectPlaceMode.MAX_HEIGHT);
|
||||||
|
placement.setRotation(IrisObjectRotation.of(0, 45, 0));
|
||||||
|
|
||||||
|
boxObject(5, 1, 3).place(0, -1, 0, placer, placement, new RNG(2L), data);
|
||||||
|
|
||||||
|
assertTrue(placer.sampledColumns().contains("-2:-2"));
|
||||||
|
assertTrue(placer.sampledColumns().contains("2:2"));
|
||||||
|
assertTrue(placer.sampledColumns().size() >= 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void snowUsesOnlyBlocksThatWereWritten() {
|
||||||
|
RecordingPlacer placer = new RecordingPlacer(null);
|
||||||
|
IrisObject object = new IrisObject(1, 3, 1);
|
||||||
|
object.setUnsigned(0, 0, 0, solid);
|
||||||
|
object.setUnsigned(0, 2, 0, schematicAir);
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setSnow(0.1);
|
||||||
|
|
||||||
|
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||||
|
|
||||||
|
List<BlockWrite> snowWrites = placer.writesOf(IrisObject.States.SNOW_LAYERS[0]);
|
||||||
|
assertEquals(1, snowWrites.size());
|
||||||
|
assertEquals(ANCHOR_Y, snowWrites.get(0).y());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void debugPlacementDoesNotMutateSmartBoreCache() {
|
||||||
|
IrisObject object = new IrisObject(1, 1, 1);
|
||||||
|
object.setUnsigned(0, 0, 0, IrisObject.States.VAIR);
|
||||||
|
object.setSmartBored(true);
|
||||||
|
IrisObjectPlacement placement = placement();
|
||||||
|
placement.setSmartBore(true);
|
||||||
|
RecordingPlacer debug = new RecordingPlacer(null);
|
||||||
|
debug.setDebugSmartBore(true);
|
||||||
|
RecordingPlacer normal = new RecordingPlacer(null);
|
||||||
|
|
||||||
|
object.place(0, ANCHOR_Y, 0, debug, placement, new RNG(2L), data);
|
||||||
|
object.place(0, ANCHOR_Y, 0, normal, placement, new RNG(2L), data);
|
||||||
|
|
||||||
|
PlatformBlockState cachedState = object.getBlocks().get(object.getSigned(0, 0, 0));
|
||||||
|
assertSame(IrisObject.States.VAIR, cachedState);
|
||||||
|
if (IrisObject.States.VAIR != IrisObject.States.VAIR_DEBUG) {
|
||||||
|
assertTrue(cachedState != IrisObject.States.VAIR_DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void placementDataIsRequiredAtTheBoundary() {
|
||||||
|
NullPointerException error = assertThrows(NullPointerException.class,
|
||||||
|
() -> lineObject(1).place(0, ANCHOR_Y, 0, new RecordingPlacer(null), placement(), new RNG(2L), null));
|
||||||
|
|
||||||
|
assertEquals("Object placement data is required.", error.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObjectPlacement placement() {
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||||
|
placement.setRequireSurfaceSupport(false);
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject lineObject(int width) {
|
||||||
|
IrisObject object = new IrisObject(width, 1, 1);
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
object.setUnsigned(x, 0, 0, solid);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject boxObject(int width, int height, int depth) {
|
||||||
|
IrisObject object = new IrisObject(width, height, depth);
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int z = 0; z < depth; z++) {
|
||||||
|
object.setUnsigned(x, y, z, solid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject object(String key) {
|
||||||
|
IrisObject object = lineObject(1);
|
||||||
|
object.setLoadKey(key);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlatformBlockState state(String key, boolean solid) {
|
||||||
|
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||||
|
when(state.key()).thenReturn(key);
|
||||||
|
when(state.materialKey()).thenReturn(key);
|
||||||
|
when(state.isSolid()).thenReturn(solid);
|
||||||
|
when(state.isOccluding()).thenReturn(solid);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingPlacer implements IObjectPlacer {
|
||||||
|
private final List<BlockWrite> writes = new ArrayList<>();
|
||||||
|
private final Map<String, PlatformBlockState> world = new HashMap<>();
|
||||||
|
private final List<String> sampledColumns = new ArrayList<>();
|
||||||
|
private final Engine engine;
|
||||||
|
private int failAfterWrites = Integer.MAX_VALUE;
|
||||||
|
private boolean debugSmartBore;
|
||||||
|
|
||||||
|
private RecordingPlacer(Engine engine) {
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failAfterWrites(int writes) {
|
||||||
|
failAfterWrites = writes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDebugSmartBore(boolean debugSmartBore) {
|
||||||
|
this.debugSmartBore = debugSmartBore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BlockWrite> writes() {
|
||||||
|
return writes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BlockWrite> writesOf(PlatformBlockState state) {
|
||||||
|
return writes.stream().filter(write -> write.state() == state).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> sampledColumns() {
|
||||||
|
return sampledColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data) {
|
||||||
|
sampledColumns.add(x + ":" + z);
|
||||||
|
return ANCHOR_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||||
|
sampledColumns.add(x + ":" + z);
|
||||||
|
return ANCHOR_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int x, int y, int z, PlatformBlockState state) {
|
||||||
|
if (writes.size() >= failAfterWrites) {
|
||||||
|
throw new IllegalStateException("write failed");
|
||||||
|
}
|
||||||
|
writes.add(new BlockWrite(x, y, z, state));
|
||||||
|
world.put(x + ":" + y + ":" + z, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlatformBlockState get(int x, int y, int z) {
|
||||||
|
return world.get(x + ":" + y + ":" + z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPreventingDecay() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCarved(int x, int y, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSolid(int x, int y, int z) {
|
||||||
|
PlatformBlockState state = get(x, y, z);
|
||||||
|
return state != null && state.isSolid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUnderwater(int x, int z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFluidHeight() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDebugSmartBore() {
|
||||||
|
return debugSmartBore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTile(int x, int y, int z, TileData tile) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void setData(int x, int y, int z, T data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine getEngine() {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record BlockWrite(int x, int y, int z, PlatformBlockState state) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -200,7 +200,7 @@ public class IrisObjectSurfaceSupportPlacementTest {
|
|||||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||||
() -> place(placer, placement, SURFACE_Y));
|
() -> place(placer, placement, SURFACE_Y));
|
||||||
|
|
||||||
assertTrue(error.getMessage().contains("require an active Iris engine"));
|
assertTrue(error.getMessage().contains("requires an active Iris engine"));
|
||||||
assertTrue(placer.written().isEmpty());
|
assertTrue(placer.written().isEmpty());
|
||||||
verify(data, never()).getEngine();
|
verify(data, never()).getEngine();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user