mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Failed closed on unreadable takeovers
- Modded config reports unparseable files, non-object takeover sections, and invalid takeover entries instead of returning silent defaults - Takeover reconciliation refuses startup when takeovers are unreadable and the world has an applied takeover state file - Config writes are refused while takeovers are unreadable so fallback defaults never overwrite the operator's file
This commit is contained in:
1 parent
79e220298f
commit
4244aa4ea7
4 files changed
+253
-37
No files matched your search
@@ -24,8 +24,11 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class ModdedModConfig {
|
||||
private static final Object LOCK = new Object();
|
||||
@@ -39,17 +42,19 @@ public final class ModdedModConfig {
|
||||
private final long mainWorldSeed;
|
||||
private final boolean mainWorldAutoRestart;
|
||||
private final Map<String, String> dimensionTakeovers;
|
||||
private final List<String> takeoverProblems;
|
||||
|
||||
private ModdedModConfig(String defaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld,
|
||||
String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart,
|
||||
Map<String, String> dimensionTakeovers) {
|
||||
Takeovers takeovers) {
|
||||
this.defaultPack = defaultPack;
|
||||
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
|
||||
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
|
||||
this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim();
|
||||
this.mainWorldSeed = mainWorldSeed;
|
||||
this.mainWorldAutoRestart = mainWorldAutoRestart;
|
||||
this.dimensionTakeovers = Map.copyOf(dimensionTakeovers);
|
||||
this.dimensionTakeovers = Map.copyOf(takeovers.entries());
|
||||
this.takeoverProblems = List.copyOf(takeovers.problems());
|
||||
}
|
||||
|
||||
public static ModdedModConfig get() {
|
||||
@@ -68,9 +73,9 @@ public final class ModdedModConfig {
|
||||
|
||||
public static void setPrimaryWorld(String dimensionId) {
|
||||
synchronized (LOCK) {
|
||||
ModdedModConfig current = get();
|
||||
ModdedModConfig current = writable();
|
||||
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, dimensionId, current.routePlayersToPrimaryWorld,
|
||||
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart, current.dimensionTakeovers);
|
||||
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart, current.takeovers());
|
||||
instance = updated;
|
||||
write(configFile(), updated);
|
||||
}
|
||||
@@ -78,9 +83,9 @@ public final class ModdedModConfig {
|
||||
|
||||
public static void setMainWorld(String packRef, long seed) {
|
||||
synchronized (LOCK) {
|
||||
ModdedModConfig current = get();
|
||||
ModdedModConfig current = writable();
|
||||
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld,
|
||||
packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart, current.dimensionTakeovers);
|
||||
packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart, current.takeovers());
|
||||
instance = updated;
|
||||
write(configFile(), updated);
|
||||
}
|
||||
@@ -122,16 +127,59 @@ public final class ModdedModConfig {
|
||||
return dimensionTakeovers;
|
||||
}
|
||||
|
||||
public List<String> takeoverProblems() {
|
||||
return takeoverProblems;
|
||||
}
|
||||
|
||||
static ModdedModConfig read(Path file) {
|
||||
ModdedModConfig defaults = defaults();
|
||||
JSONObject json;
|
||||
try {
|
||||
json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
} catch (RuntimeException | IOException unreadable) {
|
||||
ModdedIrisLog.error("Iris modded config at {} is unreadable; using defaults", file, unreadable);
|
||||
return new ModdedModConfig(defaults.defaultPack, defaults.primaryWorld, defaults.routePlayersToPrimaryWorld,
|
||||
defaults.mainWorldPack, defaults.mainWorldSeed, defaults.mainWorldAutoRestart,
|
||||
new Takeovers(Map.of(), List.of("Iris could not read " + file + ": " + unreadable.getMessage())));
|
||||
}
|
||||
return new ModdedModConfig(
|
||||
json.optString("defaultPack", defaults.defaultPack),
|
||||
json.optString("primaryWorld", defaults.primaryWorld),
|
||||
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld),
|
||||
json.optString("mainWorldPack", defaults.mainWorldPack),
|
||||
json.optLong("mainWorldSeed", defaults.mainWorldSeed),
|
||||
json.optBoolean("mainWorldAutoRestart", defaults.mainWorldAutoRestart),
|
||||
readDimensionTakeovers(json, file));
|
||||
}
|
||||
|
||||
private Takeovers takeovers() {
|
||||
return new Takeovers(dimensionTakeovers, takeoverProblems);
|
||||
}
|
||||
|
||||
private static ModdedModConfig writable() {
|
||||
ModdedModConfig current = get();
|
||||
if (!current.takeoverProblems.isEmpty()) {
|
||||
throw new IllegalStateException("Iris will not rewrite " + configFile()
|
||||
+ " while its dimension takeovers are unreadable: " + String.join("; ", current.takeoverProblems)
|
||||
+ ". Fix the file and restart first.");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static ModdedModConfig defaults() {
|
||||
return new ModdedModConfig("overworld", "", true, "", 0L, false, new Takeovers(Map.of(), List.of()));
|
||||
}
|
||||
|
||||
private static boolean updateDimensionTakeover(String levelKey, String packRef) {
|
||||
synchronized (LOCK) {
|
||||
ModdedModConfig current = get();
|
||||
ModdedModConfig current = writable();
|
||||
Map<String, String> takeovers = new TreeMap<>(current.dimensionTakeovers);
|
||||
String previous = packRef == null ? takeovers.remove(levelKey) : takeovers.put(levelKey, packRef);
|
||||
if (packRef == null ? previous == null : packRef.equals(previous)) {
|
||||
return false;
|
||||
}
|
||||
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld,
|
||||
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart, takeovers);
|
||||
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart, new Takeovers(takeovers, List.of()));
|
||||
instance = updated;
|
||||
write(configFile(), updated);
|
||||
return true;
|
||||
@@ -144,41 +192,41 @@ public final class ModdedModConfig {
|
||||
|
||||
private static ModdedModConfig load() {
|
||||
Path file = configFile();
|
||||
ModdedModConfig defaults = new ModdedModConfig("overworld", "", true, "", 0L, false, Map.of());
|
||||
if (!Files.isRegularFile(file)) {
|
||||
ModdedModConfig defaults = defaults();
|
||||
write(file, defaults);
|
||||
return defaults;
|
||||
}
|
||||
try {
|
||||
JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
return new ModdedModConfig(
|
||||
json.optString("defaultPack", defaults.defaultPack),
|
||||
json.optString("primaryWorld", defaults.primaryWorld),
|
||||
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld),
|
||||
json.optString("mainWorldPack", defaults.mainWorldPack),
|
||||
json.optLong("mainWorldSeed", defaults.mainWorldSeed),
|
||||
json.optBoolean("mainWorldAutoRestart", defaults.mainWorldAutoRestart),
|
||||
readDimensionTakeovers(json.optJSONObject(DIMENSION_TAKEOVERS), file));
|
||||
} catch (RuntimeException | IOException e) {
|
||||
ModdedIrisLog.error("Iris modded config at {} is invalid; using defaults", file, e);
|
||||
return defaults;
|
||||
}
|
||||
return read(file);
|
||||
}
|
||||
|
||||
private static Map<String, String> readDimensionTakeovers(JSONObject json, Path file) {
|
||||
if (json == null) {
|
||||
return Map.of();
|
||||
private static Takeovers readDimensionTakeovers(JSONObject json, Path file) {
|
||||
if (!json.has(DIMENSION_TAKEOVERS) || json.isNull(DIMENSION_TAKEOVERS)) {
|
||||
return new Takeovers(Map.of(), List.of());
|
||||
}
|
||||
JSONObject section = json.optJSONObject(DIMENSION_TAKEOVERS);
|
||||
if (section == null) {
|
||||
return new Takeovers(Map.of(), List.of("Iris could not read " + DIMENSION_TAKEOVERS + " in " + file
|
||||
+ ": it must be an object mapping dimension keys to pack references"));
|
||||
}
|
||||
Map<String, String> takeovers = new TreeMap<>();
|
||||
for (String levelKey : json.keySet()) {
|
||||
List<String> problems = new ArrayList<>();
|
||||
for (String levelKey : new TreeSet<>(section.keySet())) {
|
||||
Object value = section.opt(levelKey);
|
||||
try {
|
||||
ModdedDimensionTakeover takeover = ModdedDimensionTakeover.parse(levelKey, json.optString(levelKey, ""));
|
||||
if (!(value instanceof String packRef)) {
|
||||
throw new IllegalArgumentException("the pack reference must be a string");
|
||||
}
|
||||
ModdedDimensionTakeover takeover = ModdedDimensionTakeover.parse(levelKey, packRef);
|
||||
takeovers.put(takeover.levelKey(), takeover.packRef());
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
ModdedIrisLog.error("Iris ignored dimension takeover '{}' in {}: {}", levelKey, file, invalid.getMessage());
|
||||
problems.add("Iris could not read dimension takeover '" + levelKey + "' in " + file + ": " + invalid.getMessage());
|
||||
}
|
||||
}
|
||||
return takeovers;
|
||||
for (String problem : problems) {
|
||||
ModdedIrisLog.error(problem);
|
||||
}
|
||||
return new Takeovers(takeovers, problems);
|
||||
}
|
||||
|
||||
private static void write(Path file, ModdedModConfig config) {
|
||||
@@ -201,4 +249,7 @@ public final class ModdedModConfig {
|
||||
ModdedIrisLog.error("Iris failed to write modded config at {}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private record Takeovers(Map<String, String> entries, List<String> problems) {
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -44,7 +44,8 @@ public final class ModdedTakeoverReconciler {
|
||||
|
||||
public static void reconcileEarly() {
|
||||
try {
|
||||
Map<String, String> configured = ModdedModConfig.get().dimensionTakeovers();
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
Map<String, String> configured = config.dimensionTakeovers();
|
||||
Path worldRoot = MainWorldService.configuredWorldRootIfPresent();
|
||||
if (worldRoot == null) {
|
||||
if (!configured.isEmpty()) {
|
||||
@@ -54,7 +55,7 @@ public final class ModdedTakeoverReconciler {
|
||||
return;
|
||||
}
|
||||
Path recoveryParent = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen");
|
||||
Result result = reconcile(worldRoot, configured, recoveryParent);
|
||||
Result result = reconcile(worldRoot, configured, config.takeoverProblems(), recoveryParent);
|
||||
if (result.changedLevels().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +86,9 @@ public final class ModdedTakeoverReconciler {
|
||||
}
|
||||
}
|
||||
|
||||
static Result reconcile(Path worldRoot, Map<String, String> configured, Path recoveryParent) throws IOException {
|
||||
static Result reconcile(Path worldRoot, Map<String, String> configured, List<String> configProblems,
|
||||
Path recoveryParent) throws IOException {
|
||||
requireReadableConfig(worldRoot, configProblems);
|
||||
Map<String, String> applied = readApplied(worldRoot);
|
||||
Set<String> changed = ModdedDimensionTakeovers.changedLevels(applied, configured);
|
||||
if (changed.isEmpty()) {
|
||||
@@ -104,6 +107,19 @@ public final class ModdedTakeoverReconciler {
|
||||
return new Result(changed, moved.isEmpty() ? null : recovery);
|
||||
}
|
||||
|
||||
private static void requireReadableConfig(Path worldRoot, List<String> configProblems) throws IOException {
|
||||
if (configProblems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Path state = worldRoot.resolve(STATE_FILE);
|
||||
if (!Files.exists(state)) {
|
||||
return;
|
||||
}
|
||||
throw new IOException("Iris refused to reconcile dimension takeovers because the configured takeovers are unreadable while "
|
||||
+ state + " records applied takeovers: " + String.join("; ", configProblems)
|
||||
+ ". Fix the modded config so every takeover entry is valid; Iris never treats an unreadable takeover as removed.");
|
||||
}
|
||||
|
||||
static Map<String, String> readApplied(Path worldRoot) throws IOException {
|
||||
Path state = worldRoot.resolve(STATE_FILE);
|
||||
if (!Files.isRegularFile(state)) {
|
||||
|
||||
Reference in new issue
Block a user