This commit is contained in:
Brian Neumann-Fopiano
2026-08-21 02:03:24 -04:00
parent f43df6c232
commit 34092da6be
43 changed files with 1308 additions and 279 deletions
@@ -21,48 +21,118 @@ package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
public final class ModdedSettingsHotloadService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long POLL_PERIOD_MILLIS = 3_000L;
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 long lastPollAt;
private long lastModified;
private ConfigHotloadEngine hotloadEngine;
private long lastPollAtNanos;
@Override
public void onEnable() {
lastPollAt = 0L;
lastModified = settingsFile().lastModified();
File settingsFile = 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;
}
@Override
public void onDisable() {
ConfigHotloadEngine active = hotloadEngine;
hotloadEngine = null;
if (active != null) {
active.clear();
}
}
@Override
public void onServerTick(MinecraftServer server) {
long now = System.currentTimeMillis();
if (now - lastPollAt < POLL_PERIOD_MILLIS) {
ConfigHotloadEngine active = hotloadEngine;
if (active == null) {
return;
}
lastPollAt = now;
long modified = settingsFile().lastModified();
if (modified == lastModified) {
long now = System.nanoTime();
if (now - lastPollAtNanos < TimeUnit.MILLISECONDS.toNanos(POLL_PERIOD_MILLIS)) {
return;
}
lastPollAtNanos = now;
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();
return;
} catch (RuntimeException failure) {
LOGGER.error("Iris settings hotload watcher failed", failure);
}
if (IrisSettings.settings != null) {
IrisSettings.invalidate();
}
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;
}
IrisSettings.get();
IrisLanguage.reload();
lastModified = settingsFile().lastModified();
LOGGER.info("Hotloaded settings.json");
}
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() {
@@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
@@ -82,6 +83,9 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
if (active != null) {
active.shutdownNow();
}
for (Watch watch : watches.values()) {
watch.close();
}
watches.clear();
}
@@ -172,7 +176,13 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
}
if (!watches.isEmpty()) {
Set<String> keep = seen == null ? Set.of() : seen;
watches.keySet().removeIf((String key) -> !keep.contains(key));
watches.entrySet().removeIf((Map.Entry<String, Watch> entry) -> {
if (keep.contains(entry.getKey())) {
return false;
}
entry.getValue().close();
return true;
});
}
}
@@ -212,6 +222,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
try {
if (watch.engine != engine) {
watch.close();
watch.engine = engine;
watch.folder = new ReactiveFolder(
engine.getData().getDataFolder(),
@@ -241,6 +252,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
throw new IllegalStateException("Iris studio hotload failed for " + dimensionId, e);
}
}
@@ -272,5 +284,14 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
private final AtomicBoolean busy = new AtomicBoolean(false);
private volatile Engine engine;
private volatile ReactiveFolder folder;
private void close() {
ReactiveFolder active = folder;
folder = null;
if (active != null) {
active.clear();
}
engine = null;
}
}
}