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
+1 -1
View File
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
.get()
dependencies {
@@ -669,7 +669,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
watch::readSettingsContent,
watch::normalizeSettingsContent
);
configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of());
configHotloadEngine.configure(500L, 3_000L, List.of(watch.settingsFile()), List.of());
// 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
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
@@ -714,7 +714,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
pendingWorldReplacements.captureVanillaLevelContext();
pendingWorldReplacements.verifyLoadedPublishedWorlds();
J.a(this::bstats);
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 10);
J.sr(this::tickQueue, 0);
J.s(this::setupPapi);
if (IrisStartupValidation.isReady()) {
@@ -21,9 +21,12 @@ 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 art.arcane.volmlib.util.io.IO;
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;
/**
@@ -31,6 +34,8 @@ import java.util.List;
* {@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) {
@@ -46,13 +51,17 @@ public final class SettingsHotloadWatch {
return;
}
for (File file : engine.pollTouchedFiles()) {
engine.processFileChange(file, ignored -> {
IrisSettings.invalidate();
IrisSettings.get();
IrisLanguage.reload();
return true;
}, ignored -> Iris.info("Hotloaded settings.json "));
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();
}
@@ -76,8 +85,12 @@ public final class SettingsHotloadWatch {
return null;
}
try {
return IO.readAll(file);
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(),
@@ -95,4 +108,19 @@ public final class SettingsHotloadWatch {
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;
}
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
@@ -960,6 +961,11 @@ public class CommandJigsaw implements DirectorExecutor {
Throwable failure = unwrapCompletionFailure(throwable == null
? new IllegalStateException("Studio open completed without a result")
: throwable);
if (isExpectedOpenDenial(failure)) {
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
structureKey, failure.getMessage());
return;
}
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
}
@@ -1022,6 +1028,10 @@ public class CommandJigsaw implements DirectorExecutor {
return current;
}
static boolean isExpectedOpenDenial(Throwable failure) {
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
}
private void sendError(String message) {
sender().sendMessage(C.RED + message);
}
@@ -101,7 +101,7 @@ public class WandSVC implements IrisService {
var latch = new CountDownLatch(1);
var holder = Iris.tickets.getHolder(p.getWorld());
new Job() {
private int i;
private volatile int i;
private Chunk chunk;
@Override
@@ -1,5 +1,6 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
@@ -7,6 +8,7 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
@@ -17,7 +19,6 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind;
import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
@@ -44,6 +45,7 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
@@ -101,6 +103,18 @@ public class CommandJigsawContractTest {
CommandJigsaw.STUDIO_OPEN_KIND.datapackPreparation());
}
@Test
public void packValidationDenialsAreExpectedOpenFailures() {
BrokenPackException failure = new BrokenPackException(
"overworld", List.of("Biome 'broken' has no resolvable regions."));
assertTrue(CommandJigsaw.isExpectedOpenDenial(failure));
assertTrue(CommandJigsaw.isExpectedOpenDenial(
new CompletionException(failure)));
assertFalse(CommandJigsaw.isExpectedOpenDenial(
new IllegalStateException("Unexpected Studio failure")));
}
@Test
public void committedActivationStartsInitialEvaluationBeforePlayerBinding() throws Exception {
String source = Files.readString(Path.of(
@@ -129,6 +143,10 @@ public class CommandJigsawContractTest {
NamespacedKey source = CommandJigsaw.parseRegisteredStructureKey("minecraft:village_plains");
assertEquals("minecraft_village_plains", CommandJigsaw.resolveConversionTarget(source, "auto"));
assertEquals("villages/plains", CommandJigsaw.resolveConversionTarget(source, "iris:villages/plains"));
NamespacedKey ancientCity = CommandJigsaw.parseRegisteredStructureKey(
"minecraft:ancient_city");
assertEquals("minecraft_ancient_city",
CommandJigsaw.resolveConversionTarget(ancientCity, "auto"));
assertThrows(IllegalArgumentException.class,
() -> CommandJigsaw.resolveConversionTarget(source, "custom:village"));
}