mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
Merged replace target refusals
- Modded /iris replace refuses dimensions no level stem defines - Replacements count as applied only when the live level generates from Iris - Unknown replace types list installed packs
This commit is contained in:
+112
-3
@@ -18,6 +18,10 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs;
|
||||
import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem;
|
||||
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
|
||||
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogStore;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
@@ -27,15 +31,20 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class ModdedDimensionReplacements {
|
||||
private static final String GENERATOR_TYPE = "irisworldgen:iris";
|
||||
public static final String IRIS_GENERATOR = "irisworldgen:iris";
|
||||
private static final Set<String> VANILLA_LEVELS = Set.of("minecraft:overworld", "minecraft:the_nether",
|
||||
"minecraft:the_end");
|
||||
private static volatile RecordedCatalog recordedCatalog;
|
||||
private static final String PLACEHOLDER_BIOME_SOURCE = "minecraft:fixed";
|
||||
private static final String PLACEHOLDER_BIOME = "minecraft:plains";
|
||||
private static final String DIMENSIONS = "dimensions";
|
||||
@@ -49,7 +58,7 @@ public final class ModdedDimensionReplacements {
|
||||
biomeSource.put("type", PLACEHOLDER_BIOME_SOURCE);
|
||||
biomeSource.put("biome", PLACEHOLDER_BIOME);
|
||||
JSONObject generator = new JSONObject();
|
||||
generator.put("type", GENERATOR_TYPE);
|
||||
generator.put("type", IRIS_GENERATOR);
|
||||
generator.put("biome_source", biomeSource);
|
||||
generator.put("dimension", replacement.packRef());
|
||||
JSONObject stem = new JSONObject();
|
||||
@@ -72,8 +81,13 @@ public final class ModdedDimensionReplacements {
|
||||
}
|
||||
|
||||
public static void writeLevelStems(Path datapackRoot, Map<String, ModdedDimensionReplacement> configured,
|
||||
StagedTypes staged, Path worldRoot) throws IOException {
|
||||
StagedTypes staged, Path worldRoot, ReplaceableLevels levels) throws IOException {
|
||||
for (ModdedDimensionReplacement replacement : new TreeMap<>(configured).values()) {
|
||||
if (!levels.replaceable(replacement.levelKey())) {
|
||||
ModdedIrisLog.error(unreplaceableReason(replacement.levelKey(), levels)
|
||||
+ " Iris writes no dimension override for it.");
|
||||
continue;
|
||||
}
|
||||
String typeRef = staged.typeRef(replacement, worldRoot);
|
||||
if (typeRef == null) {
|
||||
skipUnstaged(replacement, staged, worldRoot);
|
||||
@@ -89,6 +103,68 @@ public final class ModdedDimensionReplacements {
|
||||
return canonical(ModdedModConfig.get().dimensionReplacements(), IrisPlatforms.get().packsFolderNoCreate().toPath());
|
||||
}
|
||||
|
||||
public static Map<String, ModdedDimensionReplacement> stageable(Map<String, ModdedDimensionReplacement> configured,
|
||||
ReplaceableLevels levels) {
|
||||
Map<String, ModdedDimensionReplacement> stageable = new TreeMap<>();
|
||||
for (ModdedDimensionReplacement replacement : configured.values()) {
|
||||
if (levels.replaceable(replacement.levelKey())) {
|
||||
stageable.put(replacement.levelKey(), replacement);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(stageable);
|
||||
}
|
||||
|
||||
public static ReplaceableLevels replaceableLevels(Path worldRoot, Map<String, ModdedDimensionReplacement> configured)
|
||||
throws IOException {
|
||||
if (configured.isEmpty()) {
|
||||
return ReplaceableLevels.unknown();
|
||||
}
|
||||
return replaceableLevels(worldRoot);
|
||||
}
|
||||
|
||||
public static ReplaceableLevels replaceableLevels(Path worldRoot) throws IOException {
|
||||
Set<String> applied = worldRoot == null
|
||||
? Set.of()
|
||||
: ModdedReplacementReconciler.readState(worldRoot).applied().keySet();
|
||||
Optional<WorldgenCatalog> catalog = ModdedWorldgenCatalogs.latest();
|
||||
if (catalog.isEmpty()) {
|
||||
catalog = recordedCatalog();
|
||||
}
|
||||
return catalog.isEmpty()
|
||||
? ReplaceableLevels.unknown()
|
||||
: ReplaceableLevels.of(catalog.get().levelStems(), applied);
|
||||
}
|
||||
|
||||
public static String unreplaceableReason(String levelKey, ReplaceableLevels levels) {
|
||||
if (!levels.known()) {
|
||||
return "Iris refuses to replace " + levelKey + ": it has no record of which dimensions this server loads"
|
||||
+ " from level stems yet. Start the server once so Iris can read the dimension registry.";
|
||||
}
|
||||
return "Iris refuses to replace " + levelKey + ": it is not a dimension the server loads from a level stem;"
|
||||
+ " a mod creates it while the server runs, so its data stays untouched.";
|
||||
}
|
||||
|
||||
private static Optional<WorldgenCatalog> recordedCatalog() {
|
||||
Path file = ModdedWorldgenCatalogs.catalogFile();
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
long modified = Files.getLastModifiedTime(file).toMillis();
|
||||
RecordedCatalog cached = recordedCatalog;
|
||||
if (cached != null && cached.file().equals(file) && cached.modified() == modified) {
|
||||
return Optional.of(cached.catalog());
|
||||
}
|
||||
WorldgenCatalog catalog = WorldgenCatalogStore.read(file);
|
||||
recordedCatalog = new RecordedCatalog(file, modified, catalog);
|
||||
return Optional.of(catalog);
|
||||
} catch (IOException | RuntimeException unreadable) {
|
||||
ModdedIrisLog.error("Iris could not read the recorded worldgen catalog at {}; only the vanilla dimensions are replaceable until the next start records it",
|
||||
file, unreadable);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static OptionalLong loadedSeed(String levelKey) {
|
||||
ModdedModConfig config = ModdedModConfig.loaded();
|
||||
if (config == null) {
|
||||
@@ -188,6 +264,39 @@ public final class ModdedDimensionReplacements {
|
||||
return canonical.toString();
|
||||
}
|
||||
|
||||
private record RecordedCatalog(Path file, long modified, WorldgenCatalog catalog) {
|
||||
}
|
||||
|
||||
public record ReplaceableLevels(boolean known, Map<String, String> stemGenerators, Set<String> applied) {
|
||||
public ReplaceableLevels {
|
||||
stemGenerators = Map.copyOf(stemGenerators);
|
||||
applied = Set.copyOf(applied);
|
||||
}
|
||||
|
||||
public static ReplaceableLevels unknown() {
|
||||
return new ReplaceableLevels(false, Map.of(), Set.of());
|
||||
}
|
||||
|
||||
public static ReplaceableLevels of(List<CatalogLevelStem> stems, Set<String> applied) {
|
||||
Map<String, String> generators = new TreeMap<>();
|
||||
for (CatalogLevelStem stem : stems) {
|
||||
generators.put(stem.key(), stem.generatorType());
|
||||
}
|
||||
return new ReplaceableLevels(true, generators, applied);
|
||||
}
|
||||
|
||||
public boolean replaceable(String levelKey) {
|
||||
if (VANILLA_LEVELS.contains(levelKey)) {
|
||||
return true;
|
||||
}
|
||||
String generator = stemGenerators.get(levelKey);
|
||||
if (generator == null) {
|
||||
return false;
|
||||
}
|
||||
return !IRIS_GENERATOR.equals(generator) || applied.contains(levelKey);
|
||||
}
|
||||
}
|
||||
|
||||
public record StagedTypes(Map<String, String> installed, Map<Path, String> retained) {
|
||||
public static StagedTypes create() {
|
||||
return new StagedTypes(new LinkedHashMap<>(), new LinkedHashMap<>());
|
||||
|
||||
@@ -178,6 +178,7 @@ public final class ModdedEngineBootstrap {
|
||||
reconcileSpawn(server, replacedOverworld);
|
||||
if (!loader().clientEnvironment()) {
|
||||
ModdedReplacementReconciler.recordApplied(server);
|
||||
ModdedReplacementReconciler.stopUnpublishedEngines(server);
|
||||
ModdedReplacementEntry.retireIfComplete();
|
||||
}
|
||||
ModdedWorldCheck.serverStarted(server);
|
||||
|
||||
+6
-3
@@ -324,7 +324,9 @@ public final class ModdedForcedDatapack {
|
||||
false
|
||||
);
|
||||
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
|
||||
digest.update(ModdedDimensionReplacements.fingerprint(ModdedDimensionReplacements.configured())
|
||||
Map<String, ModdedDimensionReplacement> replacements = ModdedDimensionReplacements.configured();
|
||||
digest.update(ModdedDimensionReplacements.fingerprint(ModdedDimensionReplacements.stageable(replacements,
|
||||
ModdedDimensionReplacements.replaceableLevels(worldRootOrNull(), replacements)))
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
@@ -346,8 +348,9 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
if (!ModdedEngineBootstrap.loader().clientEnvironment()) {
|
||||
ModdedDimensionReplacements.writeLevelStems(stagingDirectory,
|
||||
ModdedDimensionReplacements.configured(), stagedDimensionTypes, worldRoot);
|
||||
Map<String, ModdedDimensionReplacement> replacements = ModdedDimensionReplacements.configured();
|
||||
ModdedDimensionReplacements.writeLevelStems(stagingDirectory, replacements, stagedDimensionTypes, worldRoot,
|
||||
ModdedDimensionReplacements.replaceableLevels(worldRoot, replacements));
|
||||
}
|
||||
|
||||
IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer);
|
||||
|
||||
+56
-24
@@ -38,6 +38,7 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
@@ -72,9 +73,9 @@ public final class ModdedReplacementReconciler {
|
||||
Path packsRoot = IrisPlatforms.get().packsFolderNoCreate().toPath();
|
||||
Result result = reconcile(new Request(worldRoot, configured, config.replacementProblems(),
|
||||
(ModdedDimensionReplacement replacement) -> ModdedDimensionReplacements.installed(replacement, packsRoot),
|
||||
recoveryParent));
|
||||
recoveryParent, ModdedDimensionReplacements.replaceableLevels(worldRoot, configured)));
|
||||
if (!result.adoptedLevels().isEmpty()) {
|
||||
ModdedIrisLog.info("Iris dimension replacement recorded for {}: their existing Iris generation history already uses the configured pack and seed",
|
||||
ModdedIrisLog.info("Iris kept the existing terrain of {}: its Iris generation history already uses the configured pack and seed; the replacement is recorded once the level generates from it",
|
||||
result.adoptedLevels());
|
||||
}
|
||||
if (!result.replacedWorlds().isEmpty()) {
|
||||
@@ -101,7 +102,7 @@ public final class ModdedReplacementReconciler {
|
||||
}
|
||||
try {
|
||||
recordApplied(worldRoot.get(), ModdedDimensionReplacements.configured(),
|
||||
(String levelKey, String packRef) -> generatedSeed(server, levelKey, packRef));
|
||||
(String levelKey) -> liveLevel(server, levelKey));
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
ModdedIrisLog.error("Iris is stopping the server because it could not record the applied dimension replacements under {}; an unrecorded replacement would be moved aside again on the next start",
|
||||
worldRoot.get(), failure);
|
||||
@@ -109,15 +110,29 @@ public final class ModdedReplacementReconciler {
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, ModdedDimensionReplacement> recordApplied(Path worldRoot,
|
||||
public static void stopUnpublishedEngines(NativeModdedServer server) {
|
||||
for (String levelKey : ModdedWorldEngines.evictUnpublished(server,
|
||||
ModdedModConfig.get().dimensionReplacements().keySet())) {
|
||||
ModdedIrisLog.error("Iris stopped the engine it started for {}: a mod replaced that level with its own chunk generator while the server started",
|
||||
levelKey);
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized Map<String, ModdedDimensionReplacement> recordApplied(Path worldRoot,
|
||||
Map<String, ModdedDimensionReplacement> configured,
|
||||
GeneratedSeed generating) throws IOException {
|
||||
LiveLevels levels) throws IOException {
|
||||
State state = readState(worldRoot);
|
||||
Map<String, ModdedDimensionReplacement> recorded = new TreeMap<>(state.applied());
|
||||
for (ModdedDimensionReplacement replacement : new TreeMap<>(configured).values()) {
|
||||
OptionalLong seed = generating.seed(replacement.levelKey(), replacement.packRef());
|
||||
if (seed.isPresent() && (replacement.seed().isEmpty() || replacement.seed().equals(seed))) {
|
||||
recorded.put(replacement.levelKey(), replacement.withSeed(seed.getAsLong()));
|
||||
Optional<LiveLevel> live = levels.level(replacement.levelKey());
|
||||
if (live.isPresent() && live.get().generates(replacement)) {
|
||||
recorded.put(replacement.levelKey(), replacement.withSeed(live.get().seed()));
|
||||
continue;
|
||||
}
|
||||
if (live.isPresent() && !live.get().iris()) {
|
||||
recorded.remove(replacement.levelKey());
|
||||
ModdedIrisLog.error("Iris replacement of {} is configured but the loaded level uses another chunk generator; a mod creates that level while the server runs, so Iris cannot replace it. Remove its dimensionReplacements entry from modded.json",
|
||||
replacement.levelKey());
|
||||
continue;
|
||||
}
|
||||
ModdedIrisLog.warn("Iris replacement of {} is configured but the level does not generate from {}{}; it stays unapplied",
|
||||
@@ -130,7 +145,7 @@ public final class ModdedReplacementReconciler {
|
||||
return Map.copyOf(recorded);
|
||||
}
|
||||
|
||||
static Result reconcile(Request request) throws IOException {
|
||||
static synchronized Result reconcile(Request request) throws IOException {
|
||||
Path worldRoot = request.worldRoot();
|
||||
requireReadableConfig(worldRoot, request.configProblems());
|
||||
State state = readState(worldRoot);
|
||||
@@ -149,6 +164,7 @@ public final class ModdedReplacementReconciler {
|
||||
Map<String, ModdedDimensionReplacement> recorded = new TreeMap<>(applied);
|
||||
Set<String> quarantined = new TreeSet<>();
|
||||
Set<String> adopted = new TreeSet<>();
|
||||
Set<String> unreplaceable = new TreeSet<>();
|
||||
for (String level : changed) {
|
||||
ModdedDimensionReplacement replacement = request.configured().get(level);
|
||||
if (replacement == null) {
|
||||
@@ -156,12 +172,15 @@ public final class ModdedReplacementReconciler {
|
||||
quarantined.add(level);
|
||||
continue;
|
||||
}
|
||||
if (!request.levels().replaceable(level)) {
|
||||
unreplaceable.add(level);
|
||||
ModdedIrisLog.error(ModdedDimensionReplacements.unreplaceableReason(level, request.levels())
|
||||
+ " Remove its dimensionReplacements entry from modded.json.");
|
||||
continue;
|
||||
}
|
||||
Path storage = replacement.storageFolder(worldRoot);
|
||||
OptionalLong historySeed = refreshed.contains(level)
|
||||
? OptionalLong.empty()
|
||||
: matchingHistorySeed(storage, replacement);
|
||||
if (historySeed.isPresent()) {
|
||||
recorded.put(level, replacement.withSeed(historySeed.getAsLong()));
|
||||
boolean keepHistory = !refreshed.contains(level) && matchingHistorySeed(storage, replacement).isPresent();
|
||||
if (keepHistory) {
|
||||
adopted.add(level);
|
||||
} else if (request.installed().test(replacement)) {
|
||||
quarantined.add(level);
|
||||
@@ -205,7 +224,7 @@ public final class ModdedReplacementReconciler {
|
||||
writeState(worldRoot, reconciled);
|
||||
}
|
||||
return new Result(Set.copyOf(quarantined), Set.copyOf(adopted), Set.copyOf(replacedWorlds.keySet()),
|
||||
moved.isEmpty() ? null : recovery);
|
||||
Set.copyOf(unreplaceable), moved.isEmpty() ? null : recovery);
|
||||
}
|
||||
|
||||
public static boolean pending(Path worldRoot, String levelKey, Map<String, ModdedDimensionReplacement> configured)
|
||||
@@ -320,16 +339,16 @@ public final class ModdedReplacementReconciler {
|
||||
return packs.isEmpty() || packs.contains(replacement.pack());
|
||||
}
|
||||
|
||||
private static OptionalLong generatedSeed(NativeModdedServer server, String levelKey, String packRef) {
|
||||
private static Optional<LiveLevel> liveLevel(NativeModdedServer server, String levelKey) {
|
||||
NativeWorld world = server.world(levelKey);
|
||||
if (world == null) {
|
||||
return OptionalLong.empty();
|
||||
return Optional.empty();
|
||||
}
|
||||
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class);
|
||||
if (generator == null || !packRef.equals(generator.dimensionKey())) {
|
||||
return OptionalLong.empty();
|
||||
if (generator == null) {
|
||||
return Optional.of(new LiveLevel(false, "", 0L));
|
||||
}
|
||||
return OptionalLong.of(generator.visibleBiomeSeed());
|
||||
return Optional.of(new LiveLevel(true, generator.dimensionKey(), generator.visibleBiomeSeed()));
|
||||
}
|
||||
|
||||
private static void requireReadableConfig(Path worldRoot, List<String> configProblems) throws IOException {
|
||||
@@ -395,8 +414,19 @@ public final class ModdedReplacementReconciler {
|
||||
return section;
|
||||
}
|
||||
|
||||
interface GeneratedSeed {
|
||||
OptionalLong seed(String levelKey, String packRef);
|
||||
interface LiveLevels {
|
||||
Optional<LiveLevel> level(String levelKey);
|
||||
}
|
||||
|
||||
record LiveLevel(boolean iris, String packRef, long seed) {
|
||||
LiveLevel {
|
||||
Objects.requireNonNull(packRef, "packRef");
|
||||
}
|
||||
|
||||
boolean generates(ModdedDimensionReplacement replacement) {
|
||||
return iris && packRef.equals(replacement.packRef())
|
||||
&& (replacement.seed().isEmpty() || replacement.seed().getAsLong() == seed);
|
||||
}
|
||||
}
|
||||
|
||||
record State(Map<String, ModdedDimensionReplacement> applied, Map<String, ModdedDimensionReplacement> pending) {
|
||||
@@ -409,9 +439,11 @@ public final class ModdedReplacementReconciler {
|
||||
}
|
||||
|
||||
record Request(Path worldRoot, Map<String, ModdedDimensionReplacement> configured, List<String> configProblems,
|
||||
Predicate<ModdedDimensionReplacement> installed, Path recoveryParent) {
|
||||
Predicate<ModdedDimensionReplacement> installed, Path recoveryParent,
|
||||
ModdedDimensionReplacements.ReplaceableLevels levels) {
|
||||
}
|
||||
|
||||
record Result(Set<String> quarantinedLevels, Set<String> adoptedLevels, Set<String> replacedWorlds, Path recovery) {
|
||||
record Result(Set<String> quarantinedLevels, Set<String> adoptedLevels, Set<String> replacedWorlds,
|
||||
Set<String> unreplaceableLevels, Path recovery) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeDimensionRuntime;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||
@@ -49,8 +50,10 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedWorldEngines {
|
||||
@@ -92,6 +95,19 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> evictUnpublished(NativeModdedServer server, Set<String> levelKeys) {
|
||||
List<String> evicted = new ArrayList<>();
|
||||
for (NativeWorld level : new ArrayList<>(ENGINES.keySet())) {
|
||||
if (!levelKeys.contains(level.name())
|
||||
|| NativeDimensionRuntime.sameWorld(server.world(level.name()), level)) {
|
||||
continue;
|
||||
}
|
||||
evict(level);
|
||||
evicted.add(level.name());
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
static void evictOrThrow(NativeWorld level) {
|
||||
Engine[] removed = new Engine[1];
|
||||
ENGINES.computeIfPresent(level, (NativeWorld ignored, Engine current) -> {
|
||||
|
||||
+71
-2
@@ -30,7 +30,11 @@ import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.modded.ModdedModConfig;
|
||||
import art.arcane.iris.modded.ModdedReplacementReconciler;
|
||||
import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs;
|
||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackDownloader;
|
||||
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
@@ -46,10 +50,15 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -135,6 +144,7 @@ final class ModdedReplaceCommands {
|
||||
throw new IllegalArgumentException(MISSING_TARGET);
|
||||
}
|
||||
Path worldRoot = server.root();
|
||||
requireReplaceableLevel(worldRoot, requested.levelKey());
|
||||
requireNothingPending(worldRoot, requested.levelKey());
|
||||
long seed = requested.seed().orElse(ModdedReplacementReconciler.applied(worldRoot, requested.levelKey())
|
||||
.map((ModdedDimensionReplacement applied) -> applied.seed().getAsLong())
|
||||
@@ -166,6 +176,26 @@ final class ModdedReplaceCommands {
|
||||
return staged;
|
||||
}
|
||||
|
||||
private static void requireReplaceableLevel(Path worldRoot, String levelKey) throws IOException {
|
||||
Optional<WorldgenCatalog> catalog = ModdedWorldgenCatalogs.latest();
|
||||
Set<String> applied = ModdedReplacementReconciler.applied(worldRoot, levelKey).isPresent()
|
||||
? Set.of(levelKey)
|
||||
: Set.of();
|
||||
ModdedDimensionReplacements.ReplaceableLevels levels = catalog.isEmpty()
|
||||
? ModdedDimensionReplacements.ReplaceableLevels.unknown()
|
||||
: ModdedDimensionReplacements.ReplaceableLevels.of(catalog.get().levelStems(), applied);
|
||||
if (levels.replaceable(levelKey)) {
|
||||
return;
|
||||
}
|
||||
if (!levels.known()) {
|
||||
throw new IllegalArgumentException(IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY));
|
||||
}
|
||||
throw new IllegalArgumentException(IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL,
|
||||
MessageArgument.untrusted("dimensionId", levelKey)));
|
||||
}
|
||||
|
||||
private static void requireNothingPending(Path worldRoot, String levelKey) throws IOException {
|
||||
Map<String, ModdedDimensionReplacement> configured = ModdedModConfig.get().dimensionReplacements();
|
||||
if (ModdedReplacementReconciler.pending(worldRoot, levelKey, configured)) {
|
||||
@@ -230,13 +260,52 @@ final class ModdedReplaceCommands {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
|
||||
MessageArgument.untrusted("dimension", resolvedType)));
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
|
||||
List<String> installed = installedTypes(ModdedPackCommands.packsRoot());
|
||||
if (!installed.isEmpty()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES,
|
||||
MessageArgument.untrusted("value", String.join(", ", installed))));
|
||||
}
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
|
||||
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(packOf(resolvedType)))));
|
||||
}
|
||||
|
||||
static List<String> installedTypes(File packsRoot) {
|
||||
Set<String> types = new TreeSet<>();
|
||||
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsRoot)) {
|
||||
List<String> dimensionKeys = dimensionKeys(pack.toPath().resolve("dimensions"));
|
||||
if (dimensionKeys.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
types.add(pack.getName());
|
||||
for (String dimensionKey : dimensionKeys) {
|
||||
if (!dimensionKey.equals(pack.getName())) {
|
||||
types.add(pack.getName() + ":" + dimensionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(types);
|
||||
}
|
||||
|
||||
private static List<String> dimensionKeys(Path dimensions) {
|
||||
if (!Files.isDirectory(dimensions)) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> keys = new ArrayList<>();
|
||||
try (Stream<Path> files = Files.walk(dimensions)) {
|
||||
for (Path file : files.filter(Files::isRegularFile).toList()) {
|
||||
String relative = dimensions.relativize(file).toString().replace(File.separatorChar, '/');
|
||||
if (relative.endsWith(".json")) {
|
||||
keys.add(relative.substring(0, relative.length() - ".json".length()));
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException unreadable) {
|
||||
ModdedIrisLog.warn("Iris could not list the dimensions of pack folder {}", dimensions.getParent(), unreadable);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static String packOf(String type) {
|
||||
String value = type.trim();
|
||||
int colon = value.indexOf(':');
|
||||
|
||||
+16
-1
@@ -1260,6 +1260,18 @@ public final class ModdedCommandMessages {
|
||||
"iris.modded.moddedworldcommands.use_iris_world_create_name_pack_inject_one_without_restarting",
|
||||
"Use /iris world create <name> <pack> to inject one without restarting."
|
||||
);
|
||||
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL = TextKey.of(
|
||||
"iris.modded.moddedworldcommands.replace_runtime_level",
|
||||
"{dimensionId}" + " is created by a mod while the server runs and is not defined as a dimension, so Iris cannot replace it."
|
||||
);
|
||||
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY = TextKey.of(
|
||||
"iris.modded.moddedworldcommands.replace_registry_not_ready",
|
||||
"Iris is still reading this server's dimensions. Try again in a moment."
|
||||
);
|
||||
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES = TextKey.of(
|
||||
"iris.modded.moddedworldcommands.replace_installed_types",
|
||||
"Installed packs: " + "{value}"
|
||||
);
|
||||
|
||||
private static final List<MessageKey> KEYS = List.of(
|
||||
IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS,
|
||||
@@ -1574,7 +1586,10 @@ public final class ModdedCommandMessages {
|
||||
MODDED_WORLD_COMMANDS_NO_IRIS_DIMENSIONS_ARE_CURRENTLY_LOADED_CREATE_ONE_WITH_IRIS,
|
||||
MODDED_WORLD_COMMANDS_LOADED_IRIS_DIMENSIONS,
|
||||
MODDED_WORLD_COMMANDS_MESSAGE_2,
|
||||
MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING
|
||||
MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING,
|
||||
MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL,
|
||||
MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY,
|
||||
MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES
|
||||
);
|
||||
|
||||
private ModdedCommandMessages() {
|
||||
|
||||
+66
-6
@@ -37,6 +37,58 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedDimensionReplacementsTest {
|
||||
private static final ModdedDimensionReplacements.ReplaceableLevels STEMS = stems("aether:the_aether");
|
||||
|
||||
@Test
|
||||
public void onlyLevelStemsTheServerCreatesItselfAreReplaceable() {
|
||||
ModdedDimensionReplacements.ReplaceableLevels levels = new ModdedDimensionReplacements.ReplaceableLevels(true,
|
||||
Map.of("minecraft:overworld", "minecraft:noise",
|
||||
"examplemod:skylands", "examplemod:sky_generator",
|
||||
"examplemod:applied", ModdedDimensionReplacements.IRIS_GENERATOR,
|
||||
"ae2:spatial_storage", ModdedDimensionReplacements.IRIS_GENERATOR),
|
||||
Set.of("examplemod:applied"));
|
||||
|
||||
assertTrue(levels.replaceable("minecraft:overworld"));
|
||||
assertTrue(levels.replaceable("minecraft:the_end"));
|
||||
assertTrue(levels.replaceable("examplemod:skylands"));
|
||||
assertTrue(levels.replaceable("examplemod:applied"));
|
||||
assertFalse(levels.replaceable("ae2:spatial_storage"));
|
||||
assertFalse(levels.replaceable("ae2:runtime_only"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutARecordedRegistryOnlyTheVanillaDimensionsAreReplaceable() {
|
||||
ModdedDimensionReplacements.ReplaceableLevels unknown = ModdedDimensionReplacements.ReplaceableLevels.unknown();
|
||||
|
||||
assertFalse(unknown.known());
|
||||
assertTrue(unknown.replaceable("minecraft:overworld"));
|
||||
assertTrue(unknown.replaceable("minecraft:the_nether"));
|
||||
assertFalse(unknown.replaceable("examplemod:skylands"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreplaceableLevelsGetNoLevelStemOverride() throws IOException {
|
||||
Path temporary = Files.createTempDirectory("iris-replace-stage-runtime");
|
||||
try {
|
||||
Path datapack = temporary.resolve("datapack");
|
||||
ModdedDimensionReplacements.StagedTypes staged = ModdedDimensionReplacements.StagedTypes.create();
|
||||
staged.installed().put("overworld:overworld", "irisworldgen:packs/x/dimensions/y/dimension_type");
|
||||
|
||||
ModdedDimensionReplacements.writeLevelStems(datapack, Map.of(
|
||||
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld"),
|
||||
"minecraft:the_end", replacement("minecraft:the_end", "overworld")), staged,
|
||||
temporary.resolve("world"), STEMS);
|
||||
|
||||
assertFalse(Files.exists(datapack.resolve("data/ae2/dimension/spatial_storage.json")));
|
||||
assertTrue(Files.isRegularFile(datapack.resolve("data/minecraft/dimension/the_end.json")));
|
||||
assertEquals(Set.of("minecraft:the_end"), ModdedDimensionReplacements.stageable(Map.of(
|
||||
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld"),
|
||||
"minecraft:the_end", replacement("minecraft:the_end", "overworld")), STEMS).keySet());
|
||||
} finally {
|
||||
ModdedReplacementReconcilerTest.deleteTree(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsesPackAndPackDimensionReferences() {
|
||||
ModdedDimensionReplacement samePack = replacement("iristest:skylands", "skypack");
|
||||
@@ -170,7 +222,7 @@ public class ModdedDimensionReplacementsTest {
|
||||
|
||||
ModdedDimensionReplacements.writeLevelStems(datapack,
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged,
|
||||
temporary.resolve("world"));
|
||||
temporary.resolve("world"), STEMS);
|
||||
|
||||
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
|
||||
assertEquals("irisworldgen:packs/x/dimensions/y/dimension_type", stem.getString("type"));
|
||||
@@ -191,7 +243,7 @@ public class ModdedDimensionReplacementsTest {
|
||||
"aether:the_aether");
|
||||
|
||||
ModdedDimensionReplacements.writeLevelStems(datapack,
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged, worldRoot);
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged, worldRoot, STEMS);
|
||||
|
||||
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
|
||||
assertEquals("aether:the_aether", stem.getString("type"));
|
||||
@@ -211,9 +263,9 @@ public class ModdedDimensionReplacementsTest {
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky"));
|
||||
|
||||
ModdedDimensionReplacements.writeLevelStems(datapack, configured,
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot);
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS);
|
||||
ModdedDimensionReplacements.writeLevelStems(datapack, configured,
|
||||
ModdedDimensionReplacements.StagedTypes.create(), null);
|
||||
ModdedDimensionReplacements.StagedTypes.create(), null, STEMS);
|
||||
|
||||
assertFalse(Files.exists(datapack.resolve("data/aether/dimension/the_aether.json")));
|
||||
} finally {
|
||||
@@ -232,7 +284,7 @@ public class ModdedDimensionReplacementsTest {
|
||||
IOException refused = assertThrows(IOException.class, () -> ModdedDimensionReplacements.writeLevelStems(
|
||||
temporary.resolve("datapack"),
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")),
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot));
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS));
|
||||
|
||||
assertTrue(refused.getMessage(), refused.getMessage().contains("aether:the_aether"));
|
||||
assertTrue(refused.getMessage(), refused.getMessage().contains("skypack"));
|
||||
@@ -252,7 +304,7 @@ public class ModdedDimensionReplacementsTest {
|
||||
assertThrows(IOException.class, () -> ModdedDimensionReplacements.writeLevelStems(
|
||||
temporary.resolve("datapack"),
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")),
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot));
|
||||
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS));
|
||||
} finally {
|
||||
ModdedReplacementReconcilerTest.deleteTree(temporary);
|
||||
}
|
||||
@@ -280,6 +332,14 @@ public class ModdedDimensionReplacementsTest {
|
||||
}
|
||||
}
|
||||
|
||||
static ModdedDimensionReplacements.ReplaceableLevels stems(String... levelKeys) {
|
||||
Map<String, String> generators = new LinkedHashMap<>();
|
||||
for (String levelKey : levelKeys) {
|
||||
generators.put(levelKey, "minecraft:noise");
|
||||
}
|
||||
return new ModdedDimensionReplacements.ReplaceableLevels(true, generators, Set.of());
|
||||
}
|
||||
|
||||
static ModdedDimensionReplacement replacement(String levelKey, String type) {
|
||||
return ModdedDimensionReplacement.parse(levelKey, type, OptionalLong.empty());
|
||||
}
|
||||
|
||||
+78
-16
@@ -32,7 +32,6 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
@@ -46,6 +45,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedReplacementReconcilerTest {
|
||||
private static final ModdedDimensionReplacements.ReplaceableLevels STEMS = ModdedDimensionReplacementsTest.stems(
|
||||
"iristest:skylands", "iristest:unconfigured", "aether:the_aether");
|
||||
|
||||
@Test
|
||||
public void stateRoundTripsAppliedAndPendingEntriesAndDefaultsToEmpty() throws IOException {
|
||||
Path worldRoot = Files.createTempDirectory("iris-replace-state");
|
||||
@@ -260,8 +262,7 @@ public class ModdedReplacementReconcilerTest {
|
||||
assertEquals(Set.of("iristest:skylands"), result.adoptedLevels());
|
||||
assertNull(result.recovery());
|
||||
assertTrue(Files.isRegularFile(skylands.resolve("region/r.0.0.mca")));
|
||||
assertEquals(Map.of("iristest:skylands", seeded("iristest:skylands", "skypack:sky", 5L)),
|
||||
ModdedReplacementReconciler.readState(worldRoot).applied());
|
||||
assertEquals(Map.of(), ModdedReplacementReconciler.readState(worldRoot).applied());
|
||||
} finally {
|
||||
deleteTree(temporary);
|
||||
}
|
||||
@@ -433,29 +434,38 @@ public class ModdedReplacementReconcilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recordAppliedStoresOnlyReplacementsThatGenerateFromTheirPackAndSeed() throws IOException {
|
||||
public void recordAppliedStoresOnlyLevelsThatLiveGenerateFromTheirPackAndSeed() throws IOException {
|
||||
Path temporary = Files.createTempDirectory("iris-replace-record");
|
||||
Path worldRoot = temporary.resolve("world");
|
||||
try {
|
||||
writeApplied(worldRoot, seeded("iristest:kept", "overworld", 5L));
|
||||
ModdedReplacementReconciler.writeState(worldRoot, new ModdedReplacementReconciler.State(Map.of(
|
||||
"iristest:kept", seeded("iristest:kept", "overworld", 5L),
|
||||
"ae2:spatial_storage", seeded("ae2:spatial_storage", "overworld", 5L),
|
||||
"iristest:removed", seeded("iristest:removed", "overworld", 5L)), Map.of()));
|
||||
|
||||
Map<String, ModdedDimensionReplacement> recorded = ModdedReplacementReconciler.recordApplied(worldRoot,
|
||||
Map.of("iristest:kept", replacement("iristest:kept", "overworld"),
|
||||
"iristest:live", replacement("iristest:live", "skypack"),
|
||||
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L),
|
||||
"iristest:wrongseed", seeded("iristest:wrongseed", "skypack", 8L),
|
||||
"iristest:skipped", replacement("iristest:skipped", "skypack")),
|
||||
(String level, String packRef) -> switch (level) {
|
||||
case "iristest:skipped" -> OptionalLong.empty();
|
||||
case "iristest:wrongseed" -> OptionalLong.of(9L);
|
||||
case "iristest:seeded" -> OptionalLong.of(8L);
|
||||
default -> OptionalLong.of(5L);
|
||||
"iristest:wrongpack", replacement("iristest:wrongpack", "skypack"),
|
||||
"iristest:absent", replacement("iristest:absent", "skypack"),
|
||||
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld")),
|
||||
(String level) -> switch (level) {
|
||||
case "iristest:kept" -> Optional.of(iris("overworld", 5L));
|
||||
case "iristest:live" -> Optional.of(iris("skypack", 5L));
|
||||
case "iristest:seeded" -> Optional.of(iris("skypack", 8L));
|
||||
case "iristest:wrongseed" -> Optional.of(iris("skypack", 9L));
|
||||
case "iristest:wrongpack" -> Optional.of(iris("otherpack", 5L));
|
||||
case "ae2:spatial_storage" -> Optional.of(new ModdedReplacementReconciler.LiveLevel(false, "", 0L));
|
||||
default -> Optional.empty();
|
||||
});
|
||||
|
||||
Map<String, ModdedDimensionReplacement> expected = Map.of(
|
||||
"iristest:kept", seeded("iristest:kept", "overworld", 5L),
|
||||
"iristest:live", seeded("iristest:live", "skypack", 5L),
|
||||
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L));
|
||||
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L),
|
||||
"iristest:removed", seeded("iristest:removed", "overworld", 5L));
|
||||
assertEquals(expected, recorded);
|
||||
assertEquals(expected, ModdedReplacementReconciler.readState(worldRoot).applied());
|
||||
} finally {
|
||||
@@ -472,7 +482,55 @@ public class ModdedReplacementReconcilerTest {
|
||||
|
||||
assertThrows(IOException.class, () -> ModdedReplacementReconciler.recordApplied(worldRoot,
|
||||
Map.of("iristest:live", replacement("iristest:live", "skypack")),
|
||||
(String level, String packRef) -> OptionalLong.of(1L)));
|
||||
(String level) -> Optional.of(iris("skypack", 1L))));
|
||||
} finally {
|
||||
deleteTree(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeCreatedLevelIsNeverMovedRecordedOrAdopted() throws IOException {
|
||||
Path temporary = Files.createTempDirectory("iris-replace-runtime-level");
|
||||
Path worldRoot = temporary.resolve("world");
|
||||
try {
|
||||
Path spatial = worldRoot.resolve("dimensions/ae2/spatial_storage");
|
||||
touch(spatial.resolve("region/r.0.0.mca"));
|
||||
createHistory(temporary, spatial, ModdedWorldgenIds.dimensionTypeRef("overworld", "overworld"), "overworld",
|
||||
List.of());
|
||||
ModdedDimensionReplacement staged = seeded("ae2:spatial_storage", "overworld", 5L);
|
||||
ModdedReplacementReconciler.stagePending(worldRoot, staged);
|
||||
|
||||
ModdedReplacementReconciler.Result result = reconcile(worldRoot,
|
||||
Map.of("ae2:spatial_storage", staged), List.of(), temporary.resolve("config"));
|
||||
|
||||
assertEquals(Set.of("ae2:spatial_storage"), result.unreplaceableLevels());
|
||||
assertEquals(Set.of(), result.quarantinedLevels());
|
||||
assertEquals(Set.of(), result.adoptedLevels());
|
||||
assertNull(result.recovery());
|
||||
assertTrue(Files.isRegularFile(spatial.resolve("region/r.0.0.mca")));
|
||||
assertEquals(ModdedReplacementReconciler.State.EMPTY, ModdedReplacementReconciler.readState(worldRoot));
|
||||
} finally {
|
||||
deleteTree(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void levelStemOnlyDefinedByAnUnappliedIrisOverrideIsNotReplaceable() throws IOException {
|
||||
Path temporary = Files.createTempDirectory("iris-replace-stale-override");
|
||||
Path worldRoot = temporary.resolve("world");
|
||||
try {
|
||||
Path spatial = worldRoot.resolve("dimensions/ae2/spatial_storage");
|
||||
touch(spatial.resolve("region/r.0.0.mca"));
|
||||
ModdedDimensionReplacements.ReplaceableLevels levels = new ModdedDimensionReplacements.ReplaceableLevels(
|
||||
true, Map.of("ae2:spatial_storage", ModdedDimensionReplacements.IRIS_GENERATOR), Set.of());
|
||||
|
||||
ModdedReplacementReconciler.Result result = ModdedReplacementReconciler.reconcile(
|
||||
new ModdedReplacementReconciler.Request(worldRoot,
|
||||
Map.of("ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld")), List.of(),
|
||||
(ModdedDimensionReplacement replacement) -> true, temporary.resolve("config"), levels));
|
||||
|
||||
assertEquals(Set.of("ae2:spatial_storage"), result.unreplaceableLevels());
|
||||
assertTrue(Files.isRegularFile(spatial.resolve("region/r.0.0.mca")));
|
||||
} finally {
|
||||
deleteTree(temporary);
|
||||
}
|
||||
@@ -490,7 +548,7 @@ public class ModdedReplacementReconcilerTest {
|
||||
IOException refused = assertThrows(IOException.class, () -> ModdedReplacementReconciler.reconcile(
|
||||
new ModdedReplacementReconciler.Request(worldRoot,
|
||||
Map.of("iristest:skylands", replacement("iristest:skylands", "missing")), List.of(),
|
||||
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"))));
|
||||
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"), STEMS)));
|
||||
|
||||
assertTrue(refused.getMessage(), refused.getMessage().contains("missing"));
|
||||
assertTrue(Files.isRegularFile(region));
|
||||
@@ -511,7 +569,7 @@ public class ModdedReplacementReconcilerTest {
|
||||
ModdedReplacementReconciler.Result result = ModdedReplacementReconciler.reconcile(
|
||||
new ModdedReplacementReconciler.Request(worldRoot,
|
||||
Map.of("aether:the_aether", replacement("aether:the_aether", "missing")), List.of(),
|
||||
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config")));
|
||||
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"), STEMS));
|
||||
|
||||
assertEquals(Set.of(), result.quarantinedLevels());
|
||||
assertTrue(Files.isRegularFile(region));
|
||||
@@ -543,7 +601,11 @@ public class ModdedReplacementReconcilerTest {
|
||||
private static ModdedReplacementReconciler.Result reconcile(Path worldRoot, Map<String, ModdedDimensionReplacement> configured,
|
||||
List<String> problems, Path recoveryParent) throws IOException {
|
||||
return ModdedReplacementReconciler.reconcile(new ModdedReplacementReconciler.Request(
|
||||
worldRoot, configured, problems, (ModdedDimensionReplacement replacement) -> true, recoveryParent));
|
||||
worldRoot, configured, problems, (ModdedDimensionReplacement replacement) -> true, recoveryParent, STEMS));
|
||||
}
|
||||
|
||||
private static ModdedReplacementReconciler.LiveLevel iris(String packRef, long seed) {
|
||||
return new ModdedReplacementReconciler.LiveLevel(true, packRef, seed);
|
||||
}
|
||||
|
||||
private static void writeApplied(Path worldRoot, ModdedDimensionReplacement applied) throws IOException {
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft 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.modded.command;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ModdedReplaceCommandsTest {
|
||||
@Test
|
||||
public void installedTypesListEveryPackAndItsOtherDimensionKeys() throws IOException {
|
||||
Path packs = Files.createTempDirectory("iris-replace-types");
|
||||
try {
|
||||
touch(packs.resolve("overworld/dimensions/overworld.json"));
|
||||
touch(packs.resolve("skypack/dimensions/sky.json"));
|
||||
touch(packs.resolve("skypack/dimensions/floating/upper.json"));
|
||||
touch(packs.resolve("empty/readme.txt"));
|
||||
|
||||
assertEquals(List.of("overworld", "skypack", "skypack:floating/upper", "skypack:sky"),
|
||||
ModdedReplaceCommands.installedTypes(packs.toFile()));
|
||||
} finally {
|
||||
try (Stream<Path> walk = Files.walk(packs)) {
|
||||
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noInstalledPacksListNothing() throws IOException {
|
||||
Path packs = Files.createTempDirectory("iris-replace-types-empty");
|
||||
try {
|
||||
assertEquals(List.of(), ModdedReplaceCommands.installedTypes(packs.toFile()));
|
||||
assertEquals(List.of(), ModdedReplaceCommands.installedTypes(packs.resolve("missing").toFile()));
|
||||
} finally {
|
||||
Files.deleteIfExists(packs);
|
||||
}
|
||||
}
|
||||
|
||||
private static void touch(Path file) throws IOException {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, "{}", StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Derzeit sind kei
|
||||
loaded_iris_dimensions = "Geladene Iris-Dimensionen: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Verwende /iris world create <name> <pack>, um eine Dimension ohne Neustart einzubinden."
|
||||
replace_runtime_level = "{dimensionId} wird während der Laufzeit von einer Mod erstellt und ist nicht als Dimension definiert, daher kann Iris sie nicht ersetzen."
|
||||
replace_registry_not_ready = "Iris liest noch die Dimensionen dieses Servers. Versuche es gleich noch einmal."
|
||||
replace_installed_types = "Installierte Packs: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Externe Datapack-Importe von Modrinth herunterladen und verwalten"
|
||||
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "No hay dimension
|
||||
loaded_iris_dimensions = "Dimensiones de Iris cargadas: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Usa /iris world create <name> <pack> para inyectar una sin reiniciar."
|
||||
replace_runtime_level = "{dimensionId} la crea un mod mientras el servidor está en marcha y no está definida como dimensión, por lo que Iris no puede reemplazarla."
|
||||
replace_registry_not_ready = "Iris todavía está leyendo las dimensiones de este servidor. Inténtalo de nuevo en un momento."
|
||||
replace_installed_types = "Packs instalados: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Descargar y gestionar importaciones externas de datapacks (Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Ei Iris mitat ov
|
||||
loaded_iris_dimensions = "Ladattu Iris mitat: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Käyttö /iris world create <name> <pack> pistääksesi yhden ilman, että aloitat alusta."
|
||||
replace_runtime_level = "{dimensionId} on modin palvelimen ajon aikana luoma eikä sitä ole määritelty ulottuvuudeksi, joten Iris ei voi korvata sitä."
|
||||
replace_registry_not_ready = "Iris lukee vielä tämän palvelimen ulottuvuuksia. Yritä hetken päästä uudelleen."
|
||||
replace_installed_types = "Asennetut paketit: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Lataa & Hallinnoidaan ulkoisen datapaketin tuontia (Modrinth)"
|
||||
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Aucune dimension
|
||||
loaded_iris_dimensions = "Dimensions Iris chargées : {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Utilisez /iris world create <name> <pack> pour en injecter une sans redémarrer."
|
||||
replace_runtime_level = "{dimensionId} est créée par un mod pendant que le serveur tourne et n'est pas définie comme dimension ; Iris ne peut donc pas la remplacer."
|
||||
replace_registry_not_ready = "Iris lit encore les dimensions de ce serveur. Réessayez dans un instant."
|
||||
replace_installed_types = "Packs installés : {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Télécharger et gérer les importations externes de datapacks (Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "לא Iris המי
|
||||
loaded_iris_dimensions = "טעון Iris ממדים: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "שימוש בשימוש /iris world create <name> <pack> להזריק אחד מבלי להתחיל מחדש."
|
||||
replace_runtime_level = "{dimensionId} נוצר על ידי מוד בזמן שהשרת פועל ואינו מוגדר כממד, ולכן Iris אינו יכול להחליף אותו."
|
||||
replace_registry_not_ready = "Iris עדיין קורא את הממדים של השרת הזה. נסה שוב בעוד רגע."
|
||||
replace_installed_types = "חבילות מותקנות: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "הורד הורדה & ניהול יבוא נתונים חיצוני (Modrinth)"
|
||||
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nessuna dimensio
|
||||
loaded_iris_dimensions = "Dimensioni Iris caricate: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Usa /iris world create <name> <pack> per iniettarne una senza riavviare."
|
||||
replace_runtime_level = "{dimensionId} viene creata da una mod mentre il server è in esecuzione e non è definita come dimensione, quindi Iris non può sostituirla."
|
||||
replace_registry_not_ready = "Iris sta ancora leggendo le dimensioni di questo server. Riprova tra un momento."
|
||||
replace_installed_types = "Pack installati: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Scarica e gestisci le importazioni di datapack esterni da Modrinth"
|
||||
|
||||
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "現在読み込
|
||||
loaded_iris_dimensions = "読み込み済み Iris ディメンション: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "再起動せずに注入するには /iris world create <name> <pack> を使用してください。"
|
||||
replace_runtime_level = "{dimensionId} はサーバー実行中に Mod が作成するもので、ディメンションとして定義されていないため、Iris は置き換えられません。"
|
||||
replace_registry_not_ready = "Iris はまだこのサーバーのディメンションを読み込んでいます。しばらくしてから再試行してください。"
|
||||
replace_installed_types = "インストール済みのパック: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "外部データパックのインポートをダウンロードして管理します(Modrinth)"
|
||||
|
||||
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "현재 로드된
|
||||
loaded_iris_dimensions = "로드 중 ... Iris 크기: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "재시작하지 않고 주입하려면 /iris world create <name> <pack>을(를) 사용하세요."
|
||||
replace_runtime_level = "{dimensionId}은(는) 서버 실행 중에 모드가 만드는 것이며 차원으로 정의되어 있지 않으므로 Iris가 교체할 수 없습니다."
|
||||
replace_registry_not_ready = "Iris가 아직 이 서버의 차원을 읽는 중입니다. 잠시 후 다시 시도하세요."
|
||||
replace_installed_types = "설치된 팩: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "외부 데이터 팩 가져오기를 다운로드하고 관리합니다(Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Ne Iris šiuo me
|
||||
loaded_iris_dimensions = "Įkelta Iris matmenys: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Naudojimas /iris world create <name> <pack> švirkšti vieną nepradedant iš naujo."
|
||||
replace_runtime_level = "{dimensionId} kuria modas serveriui veikiant, ir ji neapibrėžta kaip dimensija, todėl Iris negali jos pakeisti."
|
||||
replace_registry_not_ready = "Iris vis dar skaito šio serverio dimensijas. Bandykite dar kartą po akimirkos."
|
||||
replace_installed_types = "Įdiegti paketai: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Parsisiųsti & valdyti išorės importą (Modrinth)"
|
||||
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nee Iris De afme
|
||||
loaded_iris_dimensions = "Geladen Iris afmetingen: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Gebruik /iris world create <name> <pack> om er één te injecteren zonder opnieuw te starten."
|
||||
replace_runtime_level = "{dimensionId} wordt door een mod gemaakt terwijl de server draait en is niet als dimensie gedefinieerd, dus Iris kan haar niet vervangen."
|
||||
replace_registry_not_ready = "Iris leest de dimensies van deze server nog in. Probeer het zo opnieuw."
|
||||
replace_installed_types = "Geïnstalleerde packs: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Downloaden & de invoer van externe datapacks beheren (Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nie. Iris wymiar
|
||||
loaded_iris_dimensions = "Załadowane Iris wymiary: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Stosowanie /iris world create <name> <pack> wykonać wstrzyknięcie bez ponownego uruchamiania."
|
||||
replace_runtime_level = "{dimensionId} jest tworzony przez moda podczas działania serwera i nie jest zdefiniowany jako wymiar, więc Iris nie może go zastąpić."
|
||||
replace_registry_not_ready = "Iris wciąż odczytuje wymiary tego serwera. Spróbuj ponownie za chwilę."
|
||||
replace_installed_types = "Zainstalowane paczki: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Pobierz & zarządzanie importem zewnętrznych danych (Modrinth)"
|
||||
|
||||
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Não Iris as dim
|
||||
loaded_iris_dimensions = "Carregado Iris dimensões: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Utilização /iris world create <name> <pack> injectar um sem reiniciar."
|
||||
replace_runtime_level = "{dimensionId} é criada por um mod enquanto o servidor está em execução e não está definida como dimensão, por isso o Iris não a pode substituir."
|
||||
replace_registry_not_ready = "O Iris ainda está a ler as dimensões deste servidor. Tente novamente daqui a pouco."
|
||||
replace_installed_types = "Packs instalados: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Baixar & gerir as importações de pacotes de dados externos (Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Нет. Iris В
|
||||
loaded_iris_dimensions = "загруженный Iris размеры: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Использовать /iris world create <name> <pack> Вводить один без перезапуска."
|
||||
replace_runtime_level = "{dimensionId} создаётся модом во время работы сервера и не определено как измерение, поэтому Iris не может его заменить."
|
||||
replace_registry_not_ready = "Iris ещё считывает измерения этого сервера. Повторите попытку чуть позже."
|
||||
replace_installed_types = "Установленные паки: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "& управляет внешним импортом пакетов данных (Modrinth)"
|
||||
|
||||
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Hayır Iris Boyu
|
||||
loaded_iris_dimensions = "Yüklendi Iris Boyut: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Kullanım Kullanımı /iris world create <name> <pack> Yeniden başlatmadan birini enjekte etmek."
|
||||
replace_runtime_level = "{dimensionId} sunucu çalışırken bir mod tarafından oluşturuluyor ve boyut olarak tanımlı değil, bu yüzden Iris onu değiştiremez."
|
||||
replace_registry_not_ready = "Iris bu sunucunun boyutlarını hâlâ okuyor. Birazdan tekrar deneyin."
|
||||
replace_installed_types = "Yüklü paketler: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Download indir & Dış veri paketi ithalatını yönetin (Modrinth)"
|
||||
|
||||
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Không Iris Kíc
|
||||
loaded_iris_dimensions = "Đã nạp Iris Kích thước: {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "Dùng /iris world create <name> <pack> tiêm một mũi mà không khởi động lại."
|
||||
replace_runtime_level = "{dimensionId} được một mod tạo ra khi máy chủ đang chạy và không được định nghĩa là chiều không gian, nên Iris không thể thay thế nó."
|
||||
replace_registry_not_ready = "Iris vẫn đang đọc các chiều không gian của máy chủ này. Hãy thử lại sau giây lát."
|
||||
replace_installed_types = "Các gói đã cài đặt: {value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "Tải về & Quản lý các tập dữ liệu bên ngoài (Modrinth)"
|
||||
|
||||
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "没有 Iris 维
|
||||
loaded_iris_dimensions = "已加载 Iris 维度 : {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "使用 /iris world create <name> <pack> 在不重新启动的情况下注入一个。"
|
||||
replace_runtime_level = "{dimensionId} 由模组在服务器运行时创建,并未定义为维度,因此 Iris 无法替换它。"
|
||||
replace_registry_not_ready = "Iris 仍在读取此服务器的维度。请稍后再试。"
|
||||
replace_installed_types = "已安装的包:{value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "下载 & 管理外部数据包进口(Modrinth)"
|
||||
|
||||
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "沒有 Iris 維
|
||||
loaded_iris_dimensions = "已載入 Iris 維度 : {value}"
|
||||
message_2 = " - {dimension}"
|
||||
use_iris_world_create_name_pack_inject_one_without_restarting = "使用 /iris world create <name> <pack> 在不重新啟動的情況下注入一個。"
|
||||
replace_runtime_level = "{dimensionId} 由模組在伺服器執行時建立,並未定義為維度,因此 Iris 無法取代它。"
|
||||
replace_registry_not_ready = "Iris 仍在讀取此伺服器的維度。請稍後再試。"
|
||||
replace_installed_types = "已安裝的包:{value}"
|
||||
|
||||
[iris.director.commanddatapack]
|
||||
"director.download_manage_external_datapack_imports_modrinth" = "下載 & 管理外部資料包進口(Modrinth)"
|
||||
|
||||
Reference in New Issue
Block a user