From f6f381f6e4378659415e8577da069b0341cd9eed Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 20:14:11 -0400 Subject: [PATCH 1/2] Added modded worldgen catalog - Captured the worldgen catalog after server start, scanning registries on the server thread and encoding and writing on a worker into config/irisworldgen/catalog/worldgen-catalog.json - Added WorldgenCatalogFingerprint over namespace versions and registry keys - Added /iris catalog [namespace|refresh] and /iris ingest mods with keyed dimension, namespaces and replace options - Added catalog messages and help entries for every shipped locale - Added tests for catalog assembly, namespace summaries, ingest arguments, the registry scan and command registration --- .../iris/modded/ModdedEngineBootstrap.java | 2 + .../modded/catalog/CatalogCaptureContext.java | 32 ++ .../catalog/ModdedWorldgenCatalogs.java | 139 ++++++++ .../catalog/WorldgenCatalogAssembler.java | 146 +++++++++ .../catalog/WorldgenCatalogSummary.java | 173 ++++++++++ .../modded/command/ModIngestArguments.java | 125 +++++++ .../modded/command/ModdedCatalogCommands.java | 309 ++++++++++++++++++ .../modded/command/ModdedCommandHelp.java | 6 + .../modded/command/ModdedCommandTree.java | 3 + .../localization/ModdedCatalogMessages.java | 145 ++++++++ .../ModdedMessageContributor.java | 1 + .../iris/modded/ModdedPlatformPathsTest.java | 7 + .../catalog/WorldgenCatalogAssemblerTest.java | 129 ++++++++ .../catalog/WorldgenCatalogSummaryTest.java | 117 +++++++ .../command/ModIngestArgumentsTest.java | 80 +++++ .../command/ModdedCatalogCommandsTest.java | 73 +++++ .../modded/NativeWorldgenScanTest.java | 129 ++++++++ .../catalog/WorldgenCatalogFingerprint.java | 66 ++++ core/src/main/resources/languages/de_DE.toml | 23 ++ core/src/main/resources/languages/es_ES.toml | 23 ++ core/src/main/resources/languages/fi_FI.toml | 23 ++ core/src/main/resources/languages/fr_FR.toml | 23 ++ core/src/main/resources/languages/he_IL.toml | 23 ++ core/src/main/resources/languages/it_IT.toml | 23 ++ core/src/main/resources/languages/ja-JP.toml | 23 ++ core/src/main/resources/languages/ko_KR.toml | 23 ++ core/src/main/resources/languages/lt_LT.toml | 23 ++ core/src/main/resources/languages/nl_NL.toml | 23 ++ core/src/main/resources/languages/pl_PL.toml | 23 ++ core/src/main/resources/languages/pt_PT.toml | 23 ++ core/src/main/resources/languages/ru_RU.toml | 23 ++ core/src/main/resources/languages/tr_TR.toml | 23 ++ core/src/main/resources/languages/vi_VI.toml | 23 ++ core/src/main/resources/languages/zh_CN.toml | 23 ++ core/src/main/resources/languages/zh_TW.toml | 23 ++ .../WorldgenCatalogFingerprintTest.java | 77 +++++ 36 files changed, 2150 insertions(+) create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/CatalogCaptureContext.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/ModdedWorldgenCatalogs.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssembler.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummary.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssemblerTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummaryTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeWorldgenScanTest.java create mode 100644 core/src/main/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprint.java create mode 100644 core/src/test/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprintTest.java diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java index 5ccf22f50..b4fea6254 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java @@ -58,6 +58,7 @@ import art.arcane.iris.generation.block.BlockDataMergeSupport; import art.arcane.iris.structure.object.IrisObjectRotation; import art.arcane.iris.generation.block.TileData; import art.arcane.iris.modded.api.ModdedCustomContentRegistry; +import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs; import art.arcane.iris.modded.command.IrisModdedCommands; import art.arcane.iris.modded.command.ModdedGuiHost; import art.arcane.iris.modded.command.ModdedObjectUndo; @@ -173,6 +174,7 @@ public final class ModdedEngineBootstrap { ModdedStartup.runOnce(server); reconcileSpawn(server); ModdedWorldCheck.serverStarted(server); + ModdedWorldgenCatalogs.capture(server); } public static void levelLoaded(NativeWorld world) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/CatalogCaptureContext.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/CatalogCaptureContext.java new file mode 100644 index 000000000..1ab3a0512 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/CatalogCaptureContext.java @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModMetadata; + +import java.util.List; +import java.util.Objects; + +public record CatalogCaptureContext(String minecraftVersion, String platform, long capturedAtEpochMillis, List mods) { + public CatalogCaptureContext { + Objects.requireNonNull(minecraftVersion, "minecraftVersion"); + Objects.requireNonNull(platform, "platform"); + mods = List.copyOf(Objects.requireNonNull(mods, "mods")); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/ModdedWorldgenCatalogs.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/ModdedWorldgenCatalogs.java new file mode 100644 index 000000000..5de99124e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/ModdedWorldgenCatalogs.java @@ -0,0 +1,139 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedIrisLog; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogStore; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenScan; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.EncodeFailures; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public final class ModdedWorldgenCatalogs { + private static final AtomicReference LATEST = new AtomicReference<>(); + private static final AtomicReference> RUNNING = new AtomicReference<>(); + + private ModdedWorldgenCatalogs() { + } + + public static Optional latest() { + return Optional.ofNullable(LATEST.get()); + } + + public static Path catalogFile() { + return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("catalog") + .resolve(WorldgenCatalogStore.FILE_NAME); + } + + public static CompletableFuture capture(NativeModdedServer server) { + Objects.requireNonNull(server, "server"); + CompletableFuture pending = new CompletableFuture<>(); + CompletableFuture running = RUNNING.compareAndExchange(null, pending); + if (running != null) { + return running; + } + long startedAt = System.nanoTime(); + NativeModdedLoader loader = ModdedEngineBootstrap.loader(); + NativeWorldgenScan scan; + CatalogCaptureContext context; + try { + scan = NativeWorldgenScan.scan(server, loader); + context = new CatalogCaptureContext(loader.minecraftVersion(), loader.platformName(), + System.currentTimeMillis(), loader.mods()); + } catch (RuntimeException failure) { + ModdedIrisLog.error("Iris could not read the worldgen registries for the catalog", failure); + finish(pending, null, failure); + return pending; + } + long scanMillis = elapsedMillis(startedAt); + Path file = catalogFile(); + Thread worker = new Thread(() -> encodeAndWrite(new PendingCapture(scan, context, file, startedAt, scanMillis), pending), + "Iris Worldgen Catalog"); + worker.setDaemon(true); + worker.start(); + return pending; + } + + private static void encodeAndWrite(PendingCapture capture, CompletableFuture pending) { + try { + NativeWorldgenSnapshot snapshot = capture.scan().encode(); + WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot, capture.context()); + WorldgenCatalogStore.write(capture.file(), catalog); + LATEST.set(catalog); + CaptureResult result = new CaptureResult(catalog, capture.file(), elapsedMillis(capture.startedAt())); + ModdedIrisLog.info("Iris worldgen catalog: {} namespaces, {} biomes, {} placed features, {} structures, " + + "{} dimension types, {} level stems -> {} ({}ms server thread, {}ms total)", + catalog.namespaces().size(), catalog.biomes().size(), catalog.placedFeatures().size(), + catalog.structures().size(), catalog.dimensionTypes().size(), catalog.levelStems().size(), + capture.file(), capture.scanMillis(), result.millis()); + reportEncodeFailures(snapshot.failures()); + finish(pending, result, null); + } catch (IOException failure) { + ModdedIrisLog.error("Iris could not write the worldgen catalog to " + capture.file(), failure); + finish(pending, null, new UncheckedIOException(failure)); + } catch (RuntimeException failure) { + ModdedIrisLog.error("Iris could not build the worldgen catalog", failure); + finish(pending, null, failure); + } + } + + private static void reportEncodeFailures(EncodeFailures failures) { + if (failures.count() == 0) { + return; + } + String first = failures.samples().isEmpty() ? "" : failures.samples().getFirst(); + if (failures.firstException() != null) { + ModdedIrisLog.warn("Iris worldgen catalog left {} entries without JSON; first: {}", failures.count(), first, + failures.firstException()); + return; + } + ModdedIrisLog.warn("Iris worldgen catalog left {} entries without JSON; first: {}", failures.count(), first); + } + + private static void finish(CompletableFuture pending, CaptureResult result, RuntimeException failure) { + RUNNING.compareAndSet(pending, null); + if (failure != null) { + pending.completeExceptionally(failure); + return; + } + pending.complete(result); + } + + private static long elapsedMillis(long startedAt) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt); + } + + public record CaptureResult(WorldgenCatalog catalog, Path file, long millis) { + } + + private record PendingCapture(NativeWorldgenScan scan, CatalogCaptureContext context, Path file, long startedAt, long scanMillis) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssembler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssembler.java new file mode 100644 index 000000000..e04c78b57 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssembler.java @@ -0,0 +1,146 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.iris.pack.ingest.catalog.CatalogBiome; +import art.arcane.iris.pack.ingest.catalog.CatalogDimensionType; +import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem; +import art.arcane.iris.pack.ingest.catalog.CatalogNamespace; +import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature; +import art.arcane.iris.pack.ingest.catalog.CatalogSpawn; +import art.arcane.iris.pack.ingest.catalog.CatalogStructure; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogFingerprint; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModMetadata; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenBiome; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenDimensionType; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenLevelStem; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenPlacedFeature; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenSpawn; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenStructure; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +public final class WorldgenCatalogAssembler { + private static final String MINECRAFT = "minecraft"; + + private WorldgenCatalogAssembler() { + } + + public static WorldgenCatalog assemble(NativeWorldgenSnapshot snapshot, CatalogCaptureContext context) { + Objects.requireNonNull(snapshot, "snapshot"); + Objects.requireNonNull(context, "context"); + List biomes = biomes(snapshot.biomes()); + List placedFeatures = placedFeatures(snapshot.placedFeatures()); + List structures = structures(snapshot.structures()); + List dimensionTypes = dimensionTypes(snapshot.dimensionTypes()); + List levelStems = levelStems(snapshot.levelStems()); + Map> registryKeys = new LinkedHashMap<>(); + registryKeys.put("biome", biomes.stream().map(CatalogBiome::key).toList()); + registryKeys.put("placed_feature", placedFeatures.stream().map(CatalogPlacedFeature::key).toList()); + registryKeys.put("structure", structures.stream().map(CatalogStructure::key).toList()); + registryKeys.put("dimension_type", dimensionTypes.stream().map(CatalogDimensionType::key).toList()); + registryKeys.put("level_stem", levelStems.stream().map(CatalogLevelStem::key).toList()); + List namespaces = namespaces(registryKeys, context); + return new WorldgenCatalog(WorldgenCatalog.FORMAT, context.minecraftVersion(), context.platform(), + context.capturedAtEpochMillis(), WorldgenCatalogFingerprint.of(namespaces, registryKeys), namespaces, + biomes, placedFeatures, structures, dimensionTypes, levelStems); + } + + private static List namespaces(Map> registryKeys, CatalogCaptureContext context) { + Map namespaces = new TreeMap<>(); + for (NativeModMetadata mod : context.mods()) { + namespaces.put(mod.id(), new CatalogNamespace(mod.id(), mod.displayName(), mod.version())); + } + for (List keys : registryKeys.values()) { + for (String key : keys) { + String namespace = WorldgenCatalogIndex.namespaceOf(key); + if (!namespaces.containsKey(namespace)) { + namespaces.put(namespace, fallbackNamespace(namespace, context)); + } + } + } + if (!namespaces.containsKey(MINECRAFT)) { + namespaces.put(MINECRAFT, fallbackNamespace(MINECRAFT, context)); + } + return List.copyOf(namespaces.values()); + } + + private static CatalogNamespace fallbackNamespace(String namespace, CatalogCaptureContext context) { + if (MINECRAFT.equals(namespace)) { + return new CatalogNamespace(MINECRAFT, "Minecraft", context.minecraftVersion()); + } + return new CatalogNamespace(namespace, namespace, ""); + } + + private static List biomes(List biomes) { + List result = new ArrayList<>(biomes.size()); + for (WorldgenBiome biome : biomes) { + List spawns = new ArrayList<>(biome.spawns().size()); + for (WorldgenSpawn spawn : biome.spawns()) { + spawns.add(new CatalogSpawn(spawn.category(), spawn.entity(), spawn.weight(), spawn.minCount(), spawn.maxCount())); + } + result.add(new CatalogBiome(biome.key(), biome.temperature(), biome.downfall(), biome.hasPrecipitation(), + biome.tags(), biome.featureSteps(), spawns, biome.definition())); + } + return result; + } + + private static List placedFeatures(List features) { + List result = new ArrayList<>(features.size()); + for (WorldgenPlacedFeature feature : features) { + result.add(new CatalogPlacedFeature(feature.key(), feature.feature(), feature.featureType(), + feature.config(), feature.placement())); + } + return result; + } + + private static List structures(List structures) { + List result = new ArrayList<>(structures.size()); + for (WorldgenStructure structure : structures) { + result.add(new CatalogStructure(structure.key(), structure.type(), structure.step(), + structure.terrainAdaptation(), structure.biomeTag(), structure.biomes(), structure.structureSets())); + } + return result; + } + + private static List dimensionTypes(List types) { + List result = new ArrayList<>(types.size()); + for (WorldgenDimensionType type : types) { + result.add(new CatalogDimensionType(type.key(), type.minY(), type.height(), type.logicalHeight(), type.definition())); + } + return result; + } + + private static List levelStems(List stems) { + List result = new ArrayList<>(stems.size()); + for (WorldgenLevelStem stem : stems) { + result.add(new CatalogLevelStem(stem.key(), stem.dimensionType(), stem.generatorType(), + stem.biomeSourceType(), stem.generator())); + } + return result; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummary.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummary.java new file mode 100644 index 000000000..26bdb24ea --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummary.java @@ -0,0 +1,173 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.iris.pack.ingest.catalog.CatalogBiome; +import art.arcane.iris.pack.ingest.catalog.CatalogDimensionType; +import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem; +import art.arcane.iris.pack.ingest.catalog.CatalogNamespace; +import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature; +import art.arcane.iris.pack.ingest.catalog.CatalogStructure; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.Function; + +public final class WorldgenCatalogSummary { + private static final String VANILLA_PREFIX = "minecraft:"; + + private WorldgenCatalogSummary() { + } + + public static List namespaces(WorldgenCatalog catalog) { + Map builders = new TreeMap<>(); + for (CatalogNamespace namespace : catalog.namespaces()) { + builders.put(namespace.namespace(), new Builder(namespace)); + } + collect(builders, catalog.biomes(), CatalogBiome::key, Builder::biomes); + collect(builders, catalog.placedFeatures(), CatalogPlacedFeature::key, Builder::placedFeatures); + collect(builders, catalog.structures(), CatalogStructure::key, Builder::structures); + collect(builders, catalog.dimensionTypes(), CatalogDimensionType::key, Builder::dimensionTypes); + collect(builders, catalog.levelStems(), CatalogLevelStem::key, Builder::levelStems); + for (CatalogPlacedFeature feature : catalog.placedFeatures()) { + builder(builders, WorldgenCatalogIndex.namespaceOf(feature.key())).featureTypes() + .merge(feature.featureType(), 1, Integer::sum); + } + List summaries = new ArrayList<>(builders.size()); + for (Builder builder : builders.values()) { + summaries.add(builder.build()); + } + return summaries; + } + + public static Optional namespace(WorldgenCatalog catalog, String namespace) { + for (NamespaceSummary summary : namespaces(catalog)) { + if (summary.namespace().namespace().equals(namespace)) { + return Optional.of(summary); + } + } + return Optional.empty(); + } + + public static String featureTypes(List types, int limit) { + List labels = new ArrayList<>(Math.min(types.size(), limit)); + for (int index = 0; index < types.size() && index < limit; index++) { + TypeCount type = types.get(index); + labels.add(shortType(type.type()) + " " + type.count()); + } + return withRemainder(String.join(", ", labels), types.size() - labels.size()); + } + + public static String limited(List keys, String namespace, int limit) { + List labels = new ArrayList<>(Math.min(keys.size(), limit)); + String ownPrefix = namespace + ":"; + for (int index = 0; index < keys.size() && index < limit; index++) { + String key = keys.get(index); + labels.add(key.startsWith(ownPrefix) ? key.substring(ownPrefix.length()) : key); + } + return withRemainder(String.join(", ", labels), keys.size() - labels.size()); + } + + private static String shortType(String type) { + return type.startsWith(VANILLA_PREFIX) ? type.substring(VANILLA_PREFIX.length()) : type; + } + + private static String withRemainder(String joined, int remainder) { + return remainder > 0 ? joined + " (+" + remainder + " more)" : joined; + } + + private static void collect(Map builders, List entries, Function key, + Function> target) { + for (T entry : entries) { + String entryKey = key.apply(entry); + target.apply(builder(builders, WorldgenCatalogIndex.namespaceOf(entryKey))).add(entryKey); + } + } + + private static Builder builder(Map builders, String namespace) { + return builders.computeIfAbsent(namespace, (String missing) -> new Builder(new CatalogNamespace(missing, missing, ""))); + } + + public record TypeCount(String type, int count) { + public TypeCount { + Objects.requireNonNull(type, "type"); + } + } + + public record NamespaceSummary( + CatalogNamespace namespace, + List biomes, + List placedFeatures, + List featureTypes, + List structures, + List dimensionTypes, + List levelStems) { + public NamespaceSummary { + Objects.requireNonNull(namespace, "namespace"); + biomes = List.copyOf(biomes); + placedFeatures = List.copyOf(placedFeatures); + featureTypes = List.copyOf(featureTypes); + structures = List.copyOf(structures); + dimensionTypes = List.copyOf(dimensionTypes); + levelStems = List.copyOf(levelStems); + } + + public boolean hasContent() { + return !biomes.isEmpty() || !placedFeatures.isEmpty() || !structures.isEmpty() + || !dimensionTypes.isEmpty() || !levelStems.isEmpty(); + } + } + + private record Builder( + CatalogNamespace namespace, + List biomes, + List placedFeatures, + Map featureTypes, + List structures, + List dimensionTypes, + List levelStems) { + private Builder(CatalogNamespace namespace) { + this(namespace, new ArrayList<>(), new ArrayList<>(), new TreeMap<>(), new ArrayList<>(), new ArrayList<>(), + new ArrayList<>()); + } + + private NamespaceSummary build() { + List types = new ArrayList<>(featureTypes.size()); + for (Map.Entry entry : featureTypes.entrySet()) { + types.add(new TypeCount(entry.getKey(), entry.getValue())); + } + types.sort(Comparator.comparingInt(TypeCount::count).reversed().thenComparing(TypeCount::type)); + return new NamespaceSummary(namespace, sorted(biomes), sorted(placedFeatures), types, sorted(structures), + sorted(dimensionTypes), sorted(levelStems)); + } + + private static List sorted(List keys) { + List copy = new ArrayList<>(keys); + copy.sort(null); + return copy; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java new file mode 100644 index 000000000..237dbf274 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java @@ -0,0 +1,125 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.command; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +record ModIngestArguments(String sourcePack, String targetPack, String dimension, Set namespaces, boolean replace) { + static final List OPTION_SUGGESTIONS = List.of("dimension=", "namespaces=", "replace=true"); + private static final Pattern PACK = Pattern.compile("[A-Za-z0-9_.-]+"); + private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+"); + private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+"); + private static final String DIMENSION_KEY = "dimension"; + private static final String NAMESPACES_KEY = "namespaces"; + private static final String REPLACE_KEY = "replace"; + + ModIngestArguments { + Objects.requireNonNull(sourcePack, "sourcePack"); + Objects.requireNonNull(targetPack, "targetPack"); + Objects.requireNonNull(dimension, "dimension"); + namespaces = Set.copyOf(Objects.requireNonNull(namespaces, "namespaces")); + } + + static ModIngestArguments parse(String raw) { + String trimmed = raw == null ? "" : raw.trim(); + String[] tokens = trimmed.isEmpty() ? new String[0] : trimmed.split("\\s+"); + String source = null; + String target = null; + Map options = new HashMap<>(4); + for (String token : tokens) { + int equals = token.indexOf('='); + if (equals < 0) { + if (source == null) { + source = token; + } else if (target == null) { + target = token; + } else { + throw new IllegalArgumentException("unexpected argument '" + token + "'"); + } + continue; + } + String key = token.substring(0, equals); + if (!key.equals(DIMENSION_KEY) && !key.equals(NAMESPACES_KEY) && !key.equals(REPLACE_KEY)) { + throw new IllegalArgumentException("unknown option '" + key + "'"); + } + if (options.putIfAbsent(key, token.substring(equals + 1)) != null) { + throw new IllegalArgumentException("option '" + key + "' given twice"); + } + } + if (source == null) { + throw new IllegalArgumentException("missing source pack"); + } + if (target == null) { + throw new IllegalArgumentException("missing target pack"); + } + requirePack(source); + requirePack(target); + if (source.equals(target)) { + throw new IllegalArgumentException("source and target must be different packs"); + } + String dimension = options.getOrDefault(DIMENSION_KEY, source); + if (!DIMENSION.matcher(dimension).matches() || dimension.contains("..")) { + throw new IllegalArgumentException("invalid dimension key '" + dimension + "'"); + } + return new ModIngestArguments(source, target, dimension, namespaces(options.get(NAMESPACES_KEY)), + replace(options.get(REPLACE_KEY))); + } + + private static void requirePack(String pack) { + if (!PACK.matcher(pack).matches() || pack.contains("..")) { + throw new IllegalArgumentException("invalid pack name '" + pack + "'"); + } + } + + private static Set namespaces(String raw) { + if (raw == null) { + return Set.of(); + } + Set namespaces = new LinkedHashSet<>(); + for (String part : raw.split(",")) { + if (part.isEmpty()) { + continue; + } + if (!NAMESPACE.matcher(part).matches()) { + throw new IllegalArgumentException("invalid namespace '" + part + "'"); + } + namespaces.add(part); + } + if (namespaces.isEmpty()) { + throw new IllegalArgumentException("namespaces must not be empty"); + } + return namespaces; + } + + private static boolean replace(String raw) { + if (raw == null || raw.equals("false")) { + return false; + } + if (raw.equals("true")) { + return true; + } + throw new IllegalArgumentException("replace must be true or false"); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java new file mode 100644 index 000000000..48fea1dbb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java @@ -0,0 +1,309 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.localization.IrisLanguage; +import art.arcane.iris.modded.ModdedIrisLog; +import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs; +import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs.CaptureResult; +import art.arcane.iris.modded.catalog.WorldgenCatalogSummary; +import art.arcane.iris.modded.catalog.WorldgenCatalogSummary.NamespaceSummary; +import art.arcane.iris.modded.localization.ModdedCatalogMessages; +import art.arcane.iris.pack.PackDirectoryResolver; +import art.arcane.iris.pack.ingest.ModIngestReport; +import art.arcane.iris.pack.ingest.ModIngestRequest; +import art.arcane.iris.pack.ingest.ModIngester; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer; +import art.arcane.volmlib.util.localization.MessageArgument; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + +final class ModdedCatalogCommands { + private static final Predicate GATE = NativeCommandRegistration.GAMEMASTERS; + private static final int LIST_FEATURE_TYPES = 3; + private static final int DETAIL_LIMIT = 24; + private static final String NONE = "-"; + private static final AtomicReference NAMESPACE_CACHE = new AtomicReference<>(); + private static final SuggestionProvider NAMESPACES = (CommandContext context, + SuggestionsBuilder builder) -> + NativeCommandRegistration.suggest(contentNamespaces(), builder); + private static final SuggestionProvider INGEST_ARGUMENTS = ModdedCatalogCommands::suggestIngestArguments; + + private ModdedCatalogCommands() { + } + + static LiteralArgumentBuilder catalogTree() { + return NativeCommandRegistration.literal("catalog").requires(GATE) + .executes(ModdedCommandTree.localized((CommandContext context) -> list(context.getSource()))) + .then(NativeCommandRegistration.literal("refresh") + .executes(ModdedCommandTree.localized((CommandContext context) -> refresh(context.getSource())))) + .then(NativeCommandRegistration.argument("namespace", StringArgumentType.word()).suggests(NAMESPACES) + .executes(ModdedCommandTree.localized((CommandContext context) -> + detail(context.getSource(), StringArgumentType.getString(context, "namespace"))))); + } + + static LiteralArgumentBuilder ingestTree() { + return NativeCommandRegistration.literal("ingest").requires(GATE) + .executes(ModdedCommandTree.localized((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "ingest"))) + .then(NativeCommandRegistration.literal("mods") + .executes(ModdedCommandTree.localized((CommandContext context) -> ingest(context.getSource(), ""))) + .then(NativeCommandRegistration.argument("arguments", StringArgumentType.greedyString()).suggests(INGEST_ARGUMENTS) + .executes(ModdedCommandTree.localized((CommandContext context) -> + ingest(context.getSource(), StringArgumentType.getString(context, "arguments")))))); + } + + private static int list(NativeCommandSource source) { + Optional latest = ModdedWorldgenCatalogs.latest(); + if (latest.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.MISSING)); + return 0; + } + WorldgenCatalog catalog = latest.get(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.HEADER, + MessageArgument.untrusted("name", catalog.platform()), + MessageArgument.untrusted("version", catalog.minecraftVersion()), + MessageArgument.untrusted("count", catalog.namespaces().size()), + MessageArgument.untrusted("value", catalog.biomes().size()), + MessageArgument.untrusted("value2", catalog.placedFeatures().size()), + MessageArgument.untrusted("value3", catalog.structures().size()), + MessageArgument.untrusted("value4", catalog.dimensionTypes().size()), + MessageArgument.untrusted("value5", catalog.levelStems().size()))); + int quiet = 0; + for (NamespaceSummary summary : WorldgenCatalogSummary.namespaces(catalog)) { + if (!summary.hasContent()) { + quiet++; + continue; + } + IrisModdedCommands.ok(source, namespaceLine(summary)); + } + if (quiet > 0) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.QUIET, MessageArgument.untrusted("count", quiet))); + } + return 1; + } + + private static int detail(NativeCommandSource source, String namespace) { + Optional latest = ModdedWorldgenCatalogs.latest(); + if (latest.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.MISSING)); + return 0; + } + Optional found = WorldgenCatalogSummary.namespace(latest.get(), namespace); + if (found.isEmpty() || !found.get().hasContent()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.UNKNOWN_NAMESPACE, + MessageArgument.untrusted("name", namespace))); + return 0; + } + NamespaceSummary summary = found.get(); + IrisModdedCommands.ok(source, namespaceLine(summary)); + if (!summary.biomes().isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.DETAIL_BIOMES, + MessageArgument.untrusted("count", summary.biomes().size()), + MessageArgument.untrusted("value", WorldgenCatalogSummary.limited(summary.biomes(), namespace, DETAIL_LIMIT)))); + } + if (!summary.placedFeatures().isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.DETAIL_FEATURES, + MessageArgument.untrusted("count", summary.placedFeatures().size()), + MessageArgument.untrusted("value", WorldgenCatalogSummary.featureTypes(summary.featureTypes(), DETAIL_LIMIT)))); + } + if (!summary.structures().isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.DETAIL_STRUCTURES, + MessageArgument.untrusted("count", summary.structures().size()), + MessageArgument.untrusted("value", WorldgenCatalogSummary.limited(summary.structures(), namespace, DETAIL_LIMIT)))); + } + if (!summary.dimensionTypes().isEmpty() || !summary.levelStems().isEmpty()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.DETAIL_DIMENSIONS, + MessageArgument.untrusted("value", orNone(WorldgenCatalogSummary.limited(summary.dimensionTypes(), namespace, DETAIL_LIMIT))), + MessageArgument.untrusted("value2", orNone(WorldgenCatalogSummary.limited(summary.levelStems(), namespace, DETAIL_LIMIT))))); + } + return 1; + } + + private static int refresh(NativeCommandSource source) { + NativeModdedServer server = source.server(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.REFRESHING)); + ModdedWorldgenCatalogs.capture(server).whenComplete((CaptureResult result, Throwable failure) -> + source.execute(() -> reportRefresh(source, result, failure))); + return 1; + } + + private static void reportRefresh(NativeCommandSource source, CaptureResult result, Throwable failure) { + if (failure != null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.REFRESH_FAILED, + MessageArgument.untrusted("reason", reason(failure)))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.REFRESHED, + MessageArgument.untrusted("duration", result.millis() + " ms"), + MessageArgument.untrusted("path", result.file().toString()))); + } + + private static int ingest(NativeCommandSource source, String raw) { + ModIngestArguments arguments; + try { + arguments = ModIngestArguments.parse(raw); + } catch (IllegalArgumentException invalid) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_INVALID, + MessageArgument.untrusted("reason", invalid.getMessage()))); + return 0; + } + Optional catalog = ModdedWorldgenCatalogs.latest(); + if (catalog.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.MISSING)); + return 0; + } + File packsRoot = ModdedPackCommands.packsRoot(); + File sourcePack = PackDirectoryResolver.resolveExisting(packsRoot, arguments.sourcePack()); + if (sourcePack == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_PACK_MISSING, + MessageArgument.untrusted("pack", arguments.sourcePack()), + MessageArgument.untrusted("path", packsRoot.getAbsolutePath()))); + return 0; + } + Path targetPack = packsRoot.toPath().toAbsolutePath().normalize().resolve(arguments.targetPack()); + ModIngestRequest request = new ModIngestRequest(sourcePack.toPath(), targetPack, arguments.dimension(), + catalog.get(), arguments.namespaces(), arguments.replace()); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_STARTED, + MessageArgument.untrusted("source", arguments.sourcePack()), + MessageArgument.untrusted("target", arguments.targetPack()), + MessageArgument.untrusted("dimension", arguments.dimension()))); + Thread worker = new Thread(() -> runIngest(source, request), "Iris Mod Ingest"); + worker.setDaemon(true); + worker.start(); + return 1; + } + + private static void runIngest(NativeCommandSource source, ModIngestRequest request) { + try { + ModIngestReport report = ModIngester.ingest(request); + source.execute(() -> reportIngest(source, report)); + } catch (IOException | RuntimeException failure) { + ModdedIrisLog.error("Iris mod ingest into " + request.targetPack() + " failed", failure); + String reason = reason(failure); + source.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_FAILED, + MessageArgument.untrusted("reason", reason)))); + } + } + + private static void reportIngest(NativeCommandSource source, ModIngestReport report) { + for (String line : report.summaryLines()) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_LINE, MessageArgument.untrusted("summary", line))); + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_COMPLETE, + MessageArgument.untrusted("path", report.targetPack().toString()))); + } + + private static String namespaceLine(NamespaceSummary summary) { + String version = summary.namespace().version().isEmpty() ? NONE : summary.namespace().version(); + return IrisLanguage.plain(ModdedCatalogMessages.NAMESPACE, + MessageArgument.untrusted("name", summary.namespace().namespace()), + MessageArgument.untrusted("version", version), + MessageArgument.untrusted("value", summary.biomes().size()), + MessageArgument.untrusted("value2", summary.placedFeatures().size()), + MessageArgument.untrusted("detail", orNone(WorldgenCatalogSummary.featureTypes(summary.featureTypes(), LIST_FEATURE_TYPES))), + MessageArgument.untrusted("value3", summary.structures().size()), + MessageArgument.untrusted("value4", summary.dimensionTypes().size()), + MessageArgument.untrusted("value5", summary.levelStems().size())); + } + + private static String orNone(String value) { + return value.isEmpty() ? NONE : value; + } + + private static String reason(Throwable failure) { + Throwable cause = failure instanceof CompletionException && failure.getCause() != null ? failure.getCause() : failure; + String message = cause.getMessage(); + return message == null || message.isBlank() ? cause.getClass().getSimpleName() : message; + } + + private static List contentNamespaces() { + Optional latest = ModdedWorldgenCatalogs.latest(); + if (latest.isEmpty()) { + return List.of(); + } + NamespaceCache cached = NAMESPACE_CACHE.get(); + if (cached != null && cached.catalog() == latest.get()) { + return cached.namespaces(); + } + List namespaces = new ArrayList<>(); + for (NamespaceSummary summary : WorldgenCatalogSummary.namespaces(latest.get())) { + if (summary.hasContent()) { + namespaces.add(summary.namespace().namespace()); + } + } + NamespaceCache created = new NamespaceCache(latest.get(), List.copyOf(namespaces)); + NAMESPACE_CACHE.set(created); + return created.namespaces(); + } + + private static CompletableFuture suggestIngestArguments(CommandContext context, + SuggestionsBuilder builder) { + String remaining = builder.getRemaining(); + int lastSpace = remaining.lastIndexOf(' '); + int bareArguments = 0; + List usedOptions = new ArrayList<>(3); + for (String token : remaining.substring(0, Math.max(0, lastSpace)).trim().split("\\s+")) { + if (token.isEmpty()) { + continue; + } + int equals = token.indexOf('='); + if (equals < 0) { + bareArguments++; + } else { + usedOptions.add(token.substring(0, equals + 1)); + } + } + SuggestionsBuilder current = builder.createOffset(builder.getStart() + lastSpace + 1); + if (bareArguments < 2) { + List packs = new ArrayList<>(); + for (File pack : PackDirectoryResolver.listVisiblePackDirectories(ModdedPackCommands.packsRoot())) { + packs.add(pack.getName()); + } + return NativeCommandRegistration.suggest(packs, current); + } + List options = new ArrayList<>(ModIngestArguments.OPTION_SUGGESTIONS.size()); + for (String option : ModIngestArguments.OPTION_SUGGESTIONS) { + if (!usedOptions.contains(option.substring(0, option.indexOf('=') + 1))) { + options.add(option); + } + } + return NativeCommandRegistration.suggest(options, current); + } + + private record NamespaceCache(WorldgenCatalog catalog, List namespaces) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index 36474a93f..faf6b395d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -20,6 +20,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.localization.IrisLanguage; import art.arcane.iris.localization.IrisMessages; +import art.arcane.iris.modded.localization.ModdedCatalogMessages; import art.arcane.iris.modded.localization.ModdedHelpMessages; import art.arcane.volmlib.util.director.help.DirectorHelpMessages; import art.arcane.volmlib.util.localization.MessageArgument; @@ -68,6 +69,8 @@ final class ModdedCommandHelp { Entry.command("debug", "", ModdedHelpMessages.COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON), Entry.command("reload", "", ModdedHelpMessages.COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S), Entry.command("download", "> [overwrite=true]", ModdedHelpMessages.COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, "dl"), + Entry.command("catalog", "[namespace|refresh]", ModdedCatalogMessages.HELP_CATALOG), + Entry.group("ingest", ModdedCatalogMessages.HELP_INGEST), Entry.command("metrics", "", ModdedHelpMessages.COMMAND_METRICS_PRINT_GENERATION_METRICS_FOR_YOUR_CURRENT_IRIS_DIMENSION, "measure"), Entry.command("regen", "[radius]", ModdedHelpMessages.COMMAND_REGEN_DELETE_AND_REGENERATE_NEARBY_CHUNKS_IN_PLACE, "rg"), Entry.group("pregen", ModdedHelpMessages.GROUP_PREGEN_PREGENERATE_AN_IRIS_DIMENSION, "pregenerate"), @@ -165,6 +168,9 @@ final class ModdedCommandHelp { Entry.command("compat", "[pack]", ModdedHelpMessages.COMMAND_COMPAT_SHOW_CONTENT_THAT_CANNOT_GENERATE_ON_THIS_MINECRAFT_VERSION, "cp") )); SECTIONS.put("pk", SECTIONS.get("pack")); + SECTIONS.put("ingest", List.of( + Entry.command("mods", " [dimension=] [namespaces=] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS) + )); SECTIONS.put("world", List.of( Entry.command("enable", " [seed|random]", ModdedHelpMessages.COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, "create"), Entry.command("replace-overworld", " [seed|random]", ModdedHelpMessages.COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java index b8120e1ec..389baf91c 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java @@ -114,6 +114,9 @@ final class ModdedCommandTree { root.then(downloadTree("download")); root.then(downloadTree("dl")); + root.then(ModdedCatalogCommands.catalogTree()); + root.then(ModdedCatalogCommands.ingestTree()); + root.then(metricsTree("metrics")); root.then(metricsTree("measure")); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java new file mode 100644 index 000000000..02f3c2fcc --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java @@ -0,0 +1,145 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.localization; + +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.TextKey; + +import java.util.List; + +public final class ModdedCatalogMessages { + public static final TextKey MISSING = TextKey.of( + "iris.modded.catalog.missing", + "No worldgen catalog has been captured yet. Run /iris catalog refresh." + ); + public static final TextKey HEADER = TextKey.of( + "iris.modded.catalog.header", + "Worldgen catalog ({name} {version}): {count} namespaces, {value} biomes, {value2} placed features, " + + "{value3} structures, {value4} dimension types, {value5} level stems" + ); + public static final TextKey NAMESPACE = TextKey.of( + "iris.modded.catalog.namespace", + "{name} {version}: {value} biomes, {value2} placed features ({detail}), {value3} structures, " + + "{value4} dimension types, {value5} level stems" + ); + public static final TextKey QUIET = TextKey.of( + "iris.modded.catalog.quiet", + "{count} more loaded mods have no worldgen content." + ); + public static final TextKey UNKNOWN_NAMESPACE = TextKey.of( + "iris.modded.catalog.unknown_namespace", + "The worldgen catalog has no content from namespace {name}." + ); + public static final TextKey DETAIL_BIOMES = TextKey.of( + "iris.modded.catalog.detail_biomes", + "Biomes ({count}): {value}" + ); + public static final TextKey DETAIL_FEATURES = TextKey.of( + "iris.modded.catalog.detail_features", + "Placed features by type ({count}): {value}" + ); + public static final TextKey DETAIL_STRUCTURES = TextKey.of( + "iris.modded.catalog.detail_structures", + "Structures ({count}): {value}" + ); + public static final TextKey DETAIL_DIMENSIONS = TextKey.of( + "iris.modded.catalog.detail_dimensions", + "Dimension types: {value}; level stems: {value2}" + ); + public static final TextKey REFRESHING = TextKey.of( + "iris.modded.catalog.refreshing", + "Capturing the worldgen catalog..." + ); + public static final TextKey REFRESHED = TextKey.of( + "iris.modded.catalog.refreshed", + "Worldgen catalog captured in {duration}: {path}" + ); + public static final TextKey REFRESH_FAILED = TextKey.of( + "iris.modded.catalog.refresh_failed", + "Worldgen catalog capture failed: {reason}" + ); + public static final TextKey INGEST_INVALID = TextKey.of( + "iris.modded.catalog.ingest_invalid", + "Invalid arguments: {reason}. Usage: /iris ingest mods [dimension=] " + + "[namespaces=] [replace=true]" + ); + public static final TextKey INGEST_PACK_MISSING = TextKey.of( + "iris.modded.catalog.ingest_pack_missing", + "Pack '{pack}' was not found under {path}." + ); + public static final TextKey INGEST_STARTED = TextKey.of( + "iris.modded.catalog.ingest_started", + "Ingesting mod worldgen from '{source}' into '{target}' (dimension {dimension})..." + ); + public static final TextKey INGEST_FAILED = TextKey.of( + "iris.modded.catalog.ingest_failed", + "Mod ingest failed: {reason}" + ); + public static final TextKey INGEST_LINE = TextKey.of( + "iris.modded.catalog.ingest_line", + " {summary}" + ); + public static final TextKey INGEST_COMPLETE = TextKey.of( + "iris.modded.catalog.ingest_complete", + "Mod ingest finished: {path}" + ); + public static final TextKey HELP_CATALOG = TextKey.of( + "iris.modded.help.entry.command.catalog", + "List the worldgen content of loaded mods, show one namespace, or capture it again" + ); + public static final TextKey HELP_INGEST = TextKey.of( + "iris.modded.help.entry.group.ingest", + "Build Iris packs from the worldgen content of loaded mods" + ); + public static final TextKey HELP_INGEST_MODS = TextKey.of( + "iris.modded.help.entry.command.ingest_mods", + "Copy a source pack into a new target pack and adopt the worldgen of loaded mods" + ); + + private static final List KEYS = List.of( + MISSING, + HEADER, + NAMESPACE, + QUIET, + UNKNOWN_NAMESPACE, + DETAIL_BIOMES, + DETAIL_FEATURES, + DETAIL_STRUCTURES, + DETAIL_DIMENSIONS, + REFRESHING, + REFRESHED, + REFRESH_FAILED, + INGEST_INVALID, + INGEST_PACK_MISSING, + INGEST_STARTED, + INGEST_FAILED, + INGEST_LINE, + INGEST_COMPLETE, + HELP_CATALOG, + HELP_INGEST, + HELP_INGEST_MODS + ); + + private ModdedCatalogMessages() { + } + + public static List keys() { + return KEYS; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java index 9a738a938..4a9654994 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java @@ -12,6 +12,7 @@ public final class ModdedMessageContributor implements IrisMessageContributor { List keys = new ArrayList<>(); keys.addAll(ModdedCommandMessages.keys()); keys.addAll(ModdedHelpMessages.keys()); + keys.addAll(ModdedCatalogMessages.keys()); keys.addAll(ClientUiMessages.keys()); return List.copyOf(keys); } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java index 8fc4445a9..9351cda28 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedPlatformPathsTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.modded; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModMetadata; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader; import art.arcane.volmlib.nativelib.terrain.NativeWorld; @@ -20,6 +21,7 @@ import org.junit.rules.TemporaryFolder; import java.io.File; import java.nio.file.Path; +import java.util.List; import static org.junit.Assert.assertEquals; @@ -45,6 +47,11 @@ public class ModdedPlatformPathsTest { return "0.0.0"; } + @Override + public List mods() { + return List.of(); + } + @Override public NativeModdedServer currentServer() { return null; diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssemblerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssemblerTest.java new file mode 100644 index 000000000..5641b0e42 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogAssemblerTest.java @@ -0,0 +1,129 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.iris.pack.ingest.catalog.CatalogBiome; +import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem; +import art.arcane.iris.pack.ingest.catalog.CatalogNamespace; +import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogFingerprint; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModMetadata; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.EncodeFailures; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenBiome; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenDimensionType; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenLevelStem; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenPlacedFeature; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenSpawn; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenStructure; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class WorldgenCatalogAssemblerTest { + private static final CatalogCaptureContext CONTEXT = new CatalogCaptureContext("26.3", "neoforge", 1234L, List.of( + new NativeModMetadata("minecraft", "Minecraft", "26.3"), + new NativeModMetadata("examplemod", "Example Mod", "1.0.0"), + new NativeModMetadata("quietmod", "Quiet Mod", "2.0.0"))); + + @Test + public void namespacesCombineLoaderModsWithRegistryKeyNamespaces() { + WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot(), CONTEXT); + + assertEquals(List.of( + new CatalogNamespace("datapack", "datapack", ""), + new CatalogNamespace("examplemod", "Example Mod", "1.0.0"), + new CatalogNamespace("minecraft", "Minecraft", "26.3"), + new CatalogNamespace("quietmod", "Quiet Mod", "2.0.0")), catalog.namespaces()); + } + + @Test + public void minecraftNamespaceFallsBackToTheRunningVersion() { + CatalogCaptureContext context = new CatalogCaptureContext("26.3", "forge", 1L, List.of()); + + WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot(), context); + + assertEquals(new CatalogNamespace("minecraft", "Minecraft", "26.3"), catalog.namespaces().get(2)); + } + + @Test + public void recordsAreCopiedWithTheirJson() { + NativeWorldgenSnapshot snapshot = snapshot(); + + WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot, CONTEXT); + + assertEquals(WorldgenCatalog.FORMAT, catalog.format()); + assertEquals("26.3", catalog.minecraftVersion()); + assertEquals("neoforge", catalog.platform()); + assertEquals(1234L, catalog.capturedAtEpochMillis()); + CatalogBiome glade = catalog.biomes().getFirst(); + assertEquals("examplemod:glade", glade.key()); + assertEquals(0.7F, glade.temperature(), 0F); + assertEquals(0.4F, glade.downfall(), 0F); + assertEquals(List.of(List.of(), List.of("examplemod:ore_glimmer")), glade.featureSteps()); + assertEquals("creature", glade.spawns().getFirst().category()); + assertSame(snapshot.biomes().getFirst().definition(), glade.definition()); + CatalogPlacedFeature ore = catalog.placedFeatures().getFirst(); + assertEquals("minecraft:ore", ore.featureType()); + assertSame(snapshot.placedFeatures().getFirst().config(), ore.config()); + CatalogLevelStem stem = catalog.levelStems().getFirst(); + assertEquals("minecraft:multi_noise", stem.biomeSourceType()); + assertEquals("examplemod:towers", catalog.structures().getFirst().structureSets().getFirst()); + assertEquals(384, catalog.dimensionTypes().getFirst().height()); + } + + @Test + public void fingerprintCoversNamespacesAndEveryRegistryKey() { + WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot(), CONTEXT); + + String expected = WorldgenCatalogFingerprint.of(catalog.namespaces(), Map.of( + "biome", List.of("datapack:dunes", "examplemod:glade"), + "placed_feature", List.of("examplemod:ore_glimmer"), + "structure", List.of("examplemod:tower"), + "dimension_type", List.of("minecraft:overworld"), + "level_stem", List.of("minecraft:overworld"))); + assertEquals(expected, catalog.contentFingerprint()); + } + + static NativeWorldgenSnapshot snapshot() { + JsonObject definition = new JsonObject(); + definition.add("downfall", new JsonPrimitive(0.4F)); + JsonObject config = new JsonObject(); + config.add("type", new JsonPrimitive("minecraft:ore")); + return new NativeWorldgenSnapshot( + List.of(new WorldgenBiome("examplemod:glade", 0.7F, 0.4F, true, List.of("minecraft:is_overworld"), + List.of(List.of(), List.of("examplemod:ore_glimmer")), + List.of(new WorldgenSpawn("creature", "minecraft:sheep", 12, 2, 4)), definition), + new WorldgenBiome("datapack:dunes", 2.0F, 0F, false, List.of(), List.of(), List.of(), null)), + List.of(new WorldgenPlacedFeature("examplemod:ore_glimmer", "examplemod:ore_glimmer", "minecraft:ore", config, null)), + List.of(new WorldgenStructure("examplemod:tower", "minecraft:jigsaw", "surface_structures", "beard_thin", + "examplemod:has_tower", List.of("examplemod:glade"), List.of("examplemod:towers"))), + List.of(new WorldgenDimensionType("minecraft:overworld", -64, 384, 384, null)), + List.of(new WorldgenLevelStem("minecraft:overworld", "minecraft:overworld", "minecraft:noise", + "minecraft:multi_noise", null)), + new EncodeFailures(0, List.of(), null)); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummaryTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummaryTest.java new file mode 100644 index 000000000..583cbe162 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/catalog/WorldgenCatalogSummaryTest.java @@ -0,0 +1,117 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.catalog; + +import art.arcane.iris.modded.catalog.WorldgenCatalogSummary.NamespaceSummary; +import art.arcane.iris.modded.catalog.WorldgenCatalogSummary.TypeCount; +import art.arcane.iris.pack.ingest.catalog.CatalogBiome; +import art.arcane.iris.pack.ingest.catalog.CatalogDimensionType; +import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem; +import art.arcane.iris.pack.ingest.catalog.CatalogNamespace; +import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature; +import art.arcane.iris.pack.ingest.catalog.CatalogStructure; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class WorldgenCatalogSummaryTest { + @Test + public void summariesCountEveryRegistryPerNamespace() { + List summaries = WorldgenCatalogSummary.namespaces(catalog()); + + assertEquals(List.of("examplemod", "minecraft", "quietmod"), + summaries.stream().map((NamespaceSummary summary) -> summary.namespace().namespace()).toList()); + NamespaceSummary example = summaries.getFirst(); + assertEquals(List.of("examplemod:glade", "examplemod:marsh"), example.biomes()); + assertEquals(6, example.placedFeatures().size()); + assertEquals(List.of("examplemod:tower"), example.structures()); + assertEquals(List.of("examplemod:sky"), example.dimensionTypes()); + assertEquals(List.of("examplemod:sky"), example.levelStems()); + assertTrue(example.hasContent()); + assertFalse(summaries.get(2).hasContent()); + } + + @Test + public void featureTypesAreRankedByCountThenName() { + NamespaceSummary example = WorldgenCatalogSummary.namespace(catalog(), "examplemod").orElseThrow(); + + assertEquals(List.of(new TypeCount("minecraft:ore", 3), new TypeCount("minecraft:random_patch", 2), + new TypeCount("examplemod:big_tree", 1)), example.featureTypes()); + assertEquals("ore 3, random_patch 2 (+1 more)", WorldgenCatalogSummary.featureTypes(example.featureTypes(), 2)); + assertEquals("ore 3, random_patch 2, examplemod:big_tree 1", + WorldgenCatalogSummary.featureTypes(example.featureTypes(), 5)); + } + + @Test + public void unknownNamespaceHasNoSummary() { + Optional missing = WorldgenCatalogSummary.namespace(catalog(), "missingmod"); + + assertTrue(missing.isEmpty()); + } + + @Test + public void keyNamespacesWithoutModMetadataStillSummarize() { + WorldgenCatalog catalog = new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1L, "x", + List.of(), List.of(biome("datapack:dunes")), List.of(), List.of(), List.of(), List.of()); + + NamespaceSummary summary = WorldgenCatalogSummary.namespace(catalog, "datapack").orElseThrow(); + + assertEquals(new CatalogNamespace("datapack", "datapack", ""), summary.namespace()); + assertEquals(List.of("datapack:dunes"), summary.biomes()); + } + + @Test + public void limitedListsShortenOwnKeysAndCountTheRest() { + assertEquals("glade, marsh (+1 more)", + WorldgenCatalogSummary.limited(List.of("examplemod:glade", "examplemod:marsh", "examplemod:moor"), "examplemod", 2)); + assertEquals("glade, minecraft:plains", + WorldgenCatalogSummary.limited(List.of("examplemod:glade", "minecraft:plains"), "examplemod", 5)); + assertEquals("", WorldgenCatalogSummary.limited(List.of(), "examplemod", 5)); + } + + private static WorldgenCatalog catalog() { + return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "neoforge", 1L, "x", + List.of(new CatalogNamespace("examplemod", "Example Mod", "1.0.0"), + new CatalogNamespace("minecraft", "Minecraft", "26.3"), + new CatalogNamespace("quietmod", "Quiet Mod", "2.0.0")), + List.of(biome("examplemod:marsh"), biome("examplemod:glade"), biome("minecraft:plains")), + List.of(feature("examplemod:ore_a", "minecraft:ore"), feature("examplemod:ore_b", "minecraft:ore"), + feature("examplemod:ore_c", "minecraft:ore"), feature("examplemod:patch_a", "minecraft:random_patch"), + feature("examplemod:patch_b", "minecraft:random_patch"), feature("examplemod:tree", "examplemod:big_tree"), + feature("minecraft:ore_iron", "minecraft:ore")), + List.of(new CatalogStructure("examplemod:tower", "minecraft:jigsaw", "surface_structures", "none", "", + List.of(), List.of())), + List.of(new CatalogDimensionType("examplemod:sky", 0, 256, 256, null)), + List.of(new CatalogLevelStem("examplemod:sky", "examplemod:sky", "minecraft:noise", "minecraft:fixed", null))); + } + + private static CatalogBiome biome(String key) { + return new CatalogBiome(key, 0.5F, 0.5F, true, List.of(), List.of(), List.of(), null); + } + + private static CatalogPlacedFeature feature(String key, String type) { + return new CatalogPlacedFeature(key, key, type, null, null); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java new file mode 100644 index 000000000..d484410b9 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java @@ -0,0 +1,80 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.command; + +import org.junit.Test; + +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ModIngestArgumentsTest { + @Test + public void requiredPacksBindPositionallyAndOptionalsDefault() { + ModIngestArguments arguments = ModIngestArguments.parse("overworld modded_overworld"); + + assertEquals("overworld", arguments.sourcePack()); + assertEquals("modded_overworld", arguments.targetPack()); + assertEquals("overworld", arguments.dimension()); + assertEquals(Set.of(), arguments.namespaces()); + assertFalse(arguments.replace()); + } + + @Test + public void keyedOptionalsAreParsedInAnyOrder() { + ModIngestArguments arguments = ModIngestArguments.parse( + " overworld modded replace=true namespaces=biomesoplenty,terralith dimension=overworld_alt "); + + assertEquals("overworld_alt", arguments.dimension()); + assertEquals(Set.of("biomesoplenty", "terralith"), arguments.namespaces()); + assertTrue(arguments.replace()); + } + + @Test + public void missingTargetIsRejected() { + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> ModIngestArguments.parse("overworld")); + + assertEquals("missing target pack", failure.getMessage()); + } + + @Test + public void strayPositionalsAndUnknownKeysAreRejected() { + assertEquals("unexpected argument 'extra'", reason("overworld modded extra")); + assertEquals("unknown option 'seed'", reason("overworld modded seed=1")); + assertEquals("option 'replace' given twice", reason("overworld modded replace=true replace=false")); + } + + @Test + public void valuesAreValidated() { + assertEquals("replace must be true or false", reason("overworld modded replace=yes")); + assertEquals("invalid pack name '../evil'", reason("../evil modded")); + assertEquals("invalid dimension key 'a..b'", reason("overworld modded dimension=a..b")); + assertEquals("invalid namespace 'Bad'", reason("overworld modded namespaces=ok,Bad")); + assertEquals("namespaces must not be empty", reason("overworld modded namespaces=,")); + assertEquals("source and target must be different packs", reason("overworld overworld")); + } + + private static String reason(String raw) { + return assertThrows(IllegalArgumentException.class, () -> ModIngestArguments.parse(raw)).getMessage(); + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java new file mode 100644 index 000000000..434fd750b --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import net.minecraft.SharedConstants; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.Bootstrap; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ModdedCatalogCommandsTest { + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void catalogAndIngestCommandsAreRegistered() { + CommandDispatcher dispatcher = new CommandDispatcher<>(); + IrisModdedCommands.register(new NativeCommandRegistration(dispatcher)); + + CommandNode iris = child(dispatcher.getRoot(), "iris"); + CommandNode catalog = child(iris, "catalog"); + assertNotNull(catalog.getCommand()); + assertNotNull(child(catalog, "refresh").getCommand()); + assertNotNull(child(catalog, "namespace").getCommand()); + CommandNode ingest = child(iris, "ingest"); + assertNotNull(ingest.getCommand()); + CommandNode mods = child(ingest, "mods"); + assertNotNull(child(mods, "arguments").getCommand()); + } + + @Test + public void helpDocumentsCatalogAndIngest() { + assertTrue(ModdedCommandHelp.documents("", "catalog")); + assertTrue(ModdedCommandHelp.documents("", "ingest")); + assertTrue(ModdedCommandHelp.documents("ingest", "mods")); + List lines = ModdedCommandHelp.consoleLines("ingest"); + assertTrue(lines.stream().anyMatch((String line) -> + line.startsWith("/iris ingest mods [dimension=] [namespaces=] [replace=true] - "))); + } + + private static CommandNode child(CommandNode parent, String name) { + CommandNode child = parent.getChild(name); + assertNotNull(name, child); + return child; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeWorldgenScanTest.java b/adapters/modded-common/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeWorldgenScanTest.java new file mode 100644 index 000000000..10dab083c --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeWorldgenScanTest.java @@ -0,0 +1,129 @@ +/* + * 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 . + */ + +package art.arcane.volmlib.nativelib.minecraft26_2.modded; + +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenBiome; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenPlacedFeature; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.Lifecycle; +import net.minecraft.SharedConstants; +import net.minecraft.core.Holder; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.RegistrationInfo; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.carver.WorldCarver; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeWorldgenScanTest { + private static final String BIOME_JSON = "{\"has_precipitation\":true,\"temperature\":0.8,\"downfall\":0.4," + + "\"effects\":{\"water_color\":\"#3f76e4\"},\"carvers\":[]," + + "\"features\":[[],[],[],[],[],[],[\"examplemod:ore_glimmer\"]]}"; + + @BeforeClass + public static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void scanReadsLiveFeatureStepsAndEncodesDefinitions() { + NativeWorldgenSnapshot snapshot = NativeWorldgenScan.scan(server(), loader()).encode(); + + WorldgenBiome biome = snapshot.biomes().getFirst(); + assertEquals("examplemod:glade", biome.key()); + assertEquals(0.8F, biome.temperature(), 0F); + assertEquals(0.4F, biome.downfall(), 0.0001F); + assertTrue(biome.hasPrecipitation()); + assertEquals(GenerationStep.Decoration.values().length, biome.featureSteps().size()); + assertEquals(List.of("examplemod:ore_glimmer"), + biome.featureSteps().get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal())); + assertNotNull(biome.definition()); + assertEquals(0.4F, biome.definition().getAsJsonObject().get("downfall").getAsFloat(), 0.0001F); + } + + @Test + public void unencodableFeaturesStayInTheCatalogWithoutJson() { + NativeWorldgenSnapshot snapshot = NativeWorldgenScan.scan(server(), loader()).encode(); + + WorldgenPlacedFeature feature = snapshot.placedFeatures().getFirst(); + assertEquals("examplemod:ore_glimmer", feature.key()); + assertEquals("", feature.feature()); + assertNull(feature.config()); + assertTrue(feature.placement().isJsonArray()); + assertEquals(1, snapshot.failures().count()); + assertTrue(snapshot.failures().samples().getFirst().startsWith("feature of examplemod:ore_glimmer")); + assertTrue(snapshot.levelStems().isEmpty()); + } + + private static NativeModdedServer server() { + MappedRegistry features = new MappedRegistry<>(Registries.PLACED_FEATURE, Lifecycle.stable()); + features.register(ResourceKey.create(Registries.PLACED_FEATURE, Identifier.parse("examplemod:ore_glimmer")), + new PlacedFeature(Holder.direct(mock(Feature.class)), List.of()), RegistrationInfo.BUILT_IN); + features.freeze(); + MappedRegistry carvers = new MappedRegistry<>(Registries.CARVER, Lifecycle.stable()); + carvers.freeze(); + MappedRegistry biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable()); + MappedRegistry structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable()); + structures.freeze(); + MappedRegistry structureSets = new MappedRegistry<>(Registries.STRUCTURE_SET, Lifecycle.stable()); + structureSets.freeze(); + MappedRegistry dimensionTypes = new MappedRegistry<>(Registries.DIMENSION_TYPE, Lifecycle.stable()); + dimensionTypes.freeze(); + RegistryAccess access = new RegistryAccess.ImmutableRegistryAccess( + List.of(biomes, features, carvers, structures, structureSets, dimensionTypes)); + Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, access), + JsonParser.parseString(BIOME_JSON)).getOrThrow(); + biomes.register(ResourceKey.create(Registries.BIOME, Identifier.parse("examplemod:glade")), biome, RegistrationInfo.BUILT_IN); + biomes.freeze(); + MinecraftServer server = mock(MinecraftServer.class); + when(server.registryAccess()).thenReturn(access.freeze()); + return NativeModdedServer.fromHandle(server); + } + + private static NativeModdedLoader loader() { + NativeModdedLoader loader = mock(NativeModdedLoader.class); + when(loader.unmodifiedBiome(any())).thenAnswer(invocation -> invocation.getArgument(0)); + return loader; + } +} diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprint.java b/core/src/main/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprint.java new file mode 100644 index 000000000..3597e84bd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprint.java @@ -0,0 +1,66 @@ +/* + * 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 . + */ + +package art.arcane.iris.pack.ingest.catalog; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +public final class WorldgenCatalogFingerprint { + private WorldgenCatalogFingerprint() { + } + + public static String of(List namespaces, Map> registryKeys) { + Objects.requireNonNull(namespaces, "namespaces"); + Objects.requireNonNull(registryKeys, "registryKeys"); + MessageDigest digest = sha256(); + List versions = new ArrayList<>(namespaces.size()); + for (CatalogNamespace namespace : namespaces) { + versions.add(namespace.namespace() + ":" + namespace.version()); + } + update(digest, "namespaces", versions); + for (Map.Entry> registry : new TreeMap<>(registryKeys).entrySet()) { + update(digest, "registry " + registry.getKey(), new ArrayList<>(registry.getValue())); + } + return HexFormat.of().formatHex(digest.digest()); + } + + private static void update(MessageDigest digest, String section, List values) { + values.sort(null); + digest.update((section + "\n").getBytes(StandardCharsets.UTF_8)); + for (String value : values) { + digest.update((value + "\n").getBytes(StandardCharsets.UTF_8)); + } + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException missing) { + throw new IllegalStateException("SHA-256 is unavailable", missing); + } + } +} diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index 55472401e..7cae93127 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -361,6 +361,9 @@ operator_instruction = "Führe {command} in der Konsole aus oder aktiviere Cheat "entry.command.debug" = "Iris-Debugprotokollierung umschalten und iris.json speichern" "entry.command.reload" = "iris.json neu laden; die Datei wird außerdem automatisch alle 3 Sekunden direkt neu geladen" "entry.command.download" = "Ein Pack-Projekt herunterladen" +"entry.command.catalog" = "Worldgen-Inhalte geladener Mods auflisten, einen Namespace anzeigen oder neu erfassen" +"entry.group.ingest" = "Iris-Packs aus den Worldgen-Inhalten geladener Mods erstellen" +"entry.command.ingest_mods" = "Ein Quell-Pack in ein neues Ziel-Pack kopieren und das Worldgen geladener Mods übernehmen" "entry.command.metrics" = "Generierungsmetriken der aktuellen Iris-Dimension anzeigen" "entry.command.regen" = "Löschen und regenerieren Sie nahe gelegene Chunks an Ort und Stelle" "entry.group.pregen" = "Eine Iris-Dimension vorgenerieren" @@ -455,6 +458,26 @@ operator_instruction = "Führe {command} in der Konsole aus oder aktiviere Cheat "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Inhalte anzeigen, die in dieser Minecraft-Version nicht generiert werden können" +[iris.modded.catalog] +missing = "Es wurde noch kein Worldgen-Katalog erfasst. Führe /iris catalog refresh aus." +header = "Worldgen-Katalog ({name} {version}): {count} Namespaces, {value} Biome, {value2} platzierte Features, {value3} Strukturen, {value4} Dimensionstypen, {value5} Level-Stems" +namespace = "{name} {version}: {value} Biome, {value2} platzierte Features ({detail}), {value3} Strukturen, {value4} Dimensionstypen, {value5} Level-Stems" +quiet = "{count} weitere geladene Mods haben keine Worldgen-Inhalte." +unknown_namespace = "Der Worldgen-Katalog enthält keine Inhalte aus dem Namespace {name}." +detail_biomes = "Biome ({count}): {value}" +detail_features = "Platzierte Features nach Typ ({count}): {value}" +detail_structures = "Strukturen ({count}): {value}" +detail_dimensions = "Dimensionstypen: {value}; Level-Stems: {value2}" +refreshing = "Worldgen-Katalog wird erfasst..." +refreshed = "Worldgen-Katalog in {duration} erfasst: {path}" +refresh_failed = "Erfassung des Worldgen-Katalogs fehlgeschlagen: {reason}" +ingest_invalid = "Ungültige Argumente: {reason}. Verwendung: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Pack '{pack}' wurde unter {path} nicht gefunden." +ingest_started = "Übernehme Mod-Worldgen aus '{source}' in '{target}' (Dimension {dimension})..." +ingest_failed = "Mod-Übernahme fehlgeschlagen: {reason}" +ingest_line = " {summary}" +ingest_complete = "Mod-Übernahme abgeschlossen: {path}" + [iris.modded.command] "properties.none" = "Eigenschaften: (keine)" "properties.list" = "Eigenschaften: {properties}" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index 70b48a67d..326db86d4 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -361,6 +361,9 @@ operator_instruction = "Ejecuta {command} desde la consola (o activa los trucos "entry.command.debug" = "Alternar el registro de depuración de Iris y guardar iris.json" "entry.command.reload" = "Recargar iris.json (también se recarga en caliente automáticamente cada 3s)" "entry.command.download" = "Descargar un proyecto de pack" +"entry.command.catalog" = "Listar el contenido de generación de los mods cargados, mostrar un espacio de nombres o volver a capturarlo" +"entry.group.ingest" = "Crear packs de Iris a partir del contenido de generación de los mods cargados" +"entry.command.ingest_mods" = "Copiar un pack de origen en un nuevo pack de destino e incorporar la generación de los mods cargados" "entry.command.metrics" = "Mostrar métricas de generación de tu dimensión de Iris actual" "entry.command.regen" = "Eliminar y regenerar chunks cercanos en el mismo lugar" "entry.group.pregen" = "Pregenerar una dimensión de Iris" @@ -455,6 +458,26 @@ operator_instruction = "Ejecuta {command} desde la consola (o activa los trucos "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Muestra el contenido que no puede generarse en esta versión de Minecraft" +[iris.modded.catalog] +missing = "Todavía no se ha capturado ningún catálogo de generación de mundo. Ejecuta /iris catalog refresh." +header = "Catálogo de generación ({name} {version}): {count} espacios de nombres, {value} biomas, {value2} features colocadas, {value3} estructuras, {value4} tipos de dimensión, {value5} level stems" +namespace = "{name} {version}: {value} biomas, {value2} features colocadas ({detail}), {value3} estructuras, {value4} tipos de dimensión, {value5} level stems" +quiet = "Otros {count} mods cargados no tienen contenido de generación de mundo." +unknown_namespace = "El catálogo de generación no tiene contenido del espacio de nombres {name}." +detail_biomes = "Biomas ({count}): {value}" +detail_features = "Features colocadas por tipo ({count}): {value}" +detail_structures = "Estructuras ({count}): {value}" +detail_dimensions = "Tipos de dimensión: {value}; level stems: {value2}" +refreshing = "Capturando el catálogo de generación de mundo..." +refreshed = "Catálogo de generación capturado en {duration}: {path}" +refresh_failed = "Error al capturar el catálogo de generación: {reason}" +ingest_invalid = "Argumentos no válidos: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "No se encontró el pack '{pack}' en {path}." +ingest_started = "Incorporando la generación de mods de '{source}' a '{target}' (dimensión {dimension})..." +ingest_failed = "La incorporación de mods falló: {reason}" +ingest_line = " {summary}" +ingest_complete = "Incorporación de mods terminada: {path}" + [iris.modded.command] "properties.none" = "Propiedades: (ninguno)" "properties.list" = "Propiedades: {properties}" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index 5616b83eb..a464b615f 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -366,6 +366,9 @@ operator_instruction = "Juokse {command} konsolilta (tai mahdollista huijaukset "entry.command.debug" = "Vaihda Iris vianetsintä ja tallenna iris.json" "entry.command.reload" = "Lataa uudelleen iris.json (myös hotloaded automaattisesti joka 3.s)" "entry.command.download" = "Lataa pakettiprojekti" +"entry.command.catalog" = "Listaa ladattujen modien maailmangenerointisisältö, näytä yksi nimiavaruus tai kerää se uudelleen" +"entry.group.ingest" = "Luo Iris-paketteja ladattujen modien maailmangenerointisisällöstä" +"entry.command.ingest_mods" = "Kopioi lähdepaketti uudeksi kohdepaketiksi ja ota ladattujen modien maailmangenerointi käyttöön" "entry.command.metrics" = "Tulosta nykyisen sukupolven mittareita Iris ulottuvuus" "entry.command.regen" = "Poista ja uudista lähistön palaset paikoilleen" "entry.group.pregen" = "Esiaste Iris ulottuvuus" @@ -460,6 +463,26 @@ operator_instruction = "Juokse {command} konsolilta (tai mahdollista huijaukset "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Näytä sisältö, jota ei voi luoda tässä Minecraft-versiossa" +[iris.modded.catalog] +missing = "Maailmangeneroinnin luetteloa ei ole vielä kerätty. Suorita /iris catalog refresh." +header = "Maailmangeneroinnin luettelo ({name} {version}): {count} nimiavaruutta, {value} biomia, {value2} sijoitettua featurea, {value3} rakennelmaa, {value4} ulottuvuustyyppiä, {value5} level stemiä" +namespace = "{name} {version}: {value} biomia, {value2} sijoitettua featurea ({detail}), {value3} rakennelmaa, {value4} ulottuvuustyyppiä, {value5} level stemiä" +quiet = "{count} muulla ladatulla modilla ei ole maailmangenerointisisältöä." +unknown_namespace = "Maailmangeneroinnin luettelossa ei ole sisältöä nimiavaruudesta {name}." +detail_biomes = "Biomit ({count}): {value}" +detail_features = "Sijoitetut featuret tyypeittäin ({count}): {value}" +detail_structures = "Rakennelmat ({count}): {value}" +detail_dimensions = "Ulottuvuustyypit: {value}; level stemit: {value2}" +refreshing = "Kerätään maailmangeneroinnin luetteloa..." +refreshed = "Maailmangeneroinnin luettelo kerätty ajassa {duration}: {path}" +refresh_failed = "Maailmangeneroinnin luettelon kerääminen epäonnistui: {reason}" +ingest_invalid = "Virheelliset argumentit: {reason}. Käyttö: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Pakettia '{pack}' ei löytynyt kohteesta {path}." +ingest_started = "Tuodaan modien maailmangenerointi paketista '{source}' pakettiin '{target}' (ulottuvuus {dimension})..." +ingest_failed = "Modien tuonti epäonnistui: {reason}" +ingest_line = " {summary}" +ingest_complete = "Modien tuonti valmis: {path}" + [iris.modded.command] "properties.none" = "Ominaisuudet: (ei)" "properties.list" = "Ominaisuudet: {properties}" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index 20e4834fd..0865508ee 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -361,6 +361,9 @@ operator_instruction = "Exécutez {command} depuis la console (ou activez les co "entry.command.debug" = "Basculer les journaux de débogage Iris et enregistrer iris.json" "entry.command.reload" = "Recharger iris.json (également rechargé à chaud automatiquement toutes les 3s)" "entry.command.download" = "Télécharger un projet de pack" +"entry.command.catalog" = "Lister le contenu de génération des mods chargés, afficher un espace de noms ou le capturer à nouveau" +"entry.group.ingest" = "Créer des packs Iris à partir du contenu de génération des mods chargés" +"entry.command.ingest_mods" = "Copier un pack source dans un nouveau pack cible et y intégrer la génération des mods chargés" "entry.command.metrics" = "Afficher les métriques de génération de votre dimension Iris actuelle" "entry.command.regen" = "Supprimer et régénérer sur place les chunks proches" "entry.group.pregen" = "Prégénérer une dimension Iris" @@ -455,6 +458,26 @@ operator_instruction = "Exécutez {command} depuis la console (ou activez les co "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Afficher le contenu qui ne peut pas être généré sur cette version de Minecraft" +[iris.modded.catalog] +missing = "Aucun catalogue de génération n'a encore été capturé. Exécutez /iris catalog refresh." +header = "Catalogue de génération ({name} {version}) : {count} espaces de noms, {value} biomes, {value2} features placées, {value3} structures, {value4} types de dimension, {value5} level stems" +namespace = "{name} {version} : {value} biomes, {value2} features placées ({detail}), {value3} structures, {value4} types de dimension, {value5} level stems" +quiet = "{count} autres mods chargés n'ont aucun contenu de génération." +unknown_namespace = "Le catalogue de génération ne contient rien de l'espace de noms {name}." +detail_biomes = "Biomes ({count}) : {value}" +detail_features = "Features placées par type ({count}) : {value}" +detail_structures = "Structures ({count}) : {value}" +detail_dimensions = "Types de dimension : {value} ; level stems : {value2}" +refreshing = "Capture du catalogue de génération..." +refreshed = "Catalogue de génération capturé en {duration} : {path}" +refresh_failed = "Échec de la capture du catalogue de génération : {reason}" +ingest_invalid = "Arguments invalides : {reason}. Utilisation : /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Le pack '{pack}' est introuvable dans {path}." +ingest_started = "Intégration de la génération des mods de '{source}' vers '{target}' (dimension {dimension})..." +ingest_failed = "L'intégration des mods a échoué : {reason}" +ingest_line = " {summary}" +ingest_complete = "Intégration des mods terminée : {path}" + [iris.modded.command] "properties.none" = "Propriétés: (aucune)" "properties.list" = "Propriétés : {properties}" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index 92ce65eb7..5e45e71e4 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -366,6 +366,9 @@ operator_instruction = "לרוץ {command} מן הקונסולה (או לאפש "entry.command.debug" = "עקבו אחרי Iris debug bug , שמירה iris.json" "entry.command.reload" = "המונחים טעינה מחדש iris.json (גם חם כל 3s)" "entry.command.download" = "הורד פרויקט חבילה" +"entry.command.catalog" = "הצג את תוכן יצירת העולם של המודים הטעונים, הצג מרחב שמות אחד או אסוף אותו מחדש" +"entry.group.ingest" = "בנה חבילות Iris מתוכן יצירת העולם של המודים הטעונים" +"entry.command.ingest_mods" = "העתק חבילת מקור לחבילת יעד חדשה ואמץ את יצירת העולם של המודים הטעונים" "entry.command.metrics" = "הדפסת דור מדדים עבור הנוכחי שלך Iris מימד" "entry.command.regen" = "למחוק ולחדש חלקים סמוכים במקום" "entry.group.pregen" = "עקבו אחרי a Iris מימד" @@ -460,6 +463,26 @@ operator_instruction = "לרוץ {command} מן הקונסולה (או לאפש "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "הצג תוכן שלא ניתן ליצור בגרסת Minecraft הזו" +[iris.modded.catalog] +missing = "עדיין לא נאסף קטלוג יצירת עולם. הרץ /iris catalog refresh." +header = "קטלוג יצירת עולם ({name} {version}): {count} מרחבי שמות, {value} ביומים, {value2} פיצ'רים ממוקמים, {value3} מבנים, {value4} סוגי ממדים, {value5} level stems" +namespace = "{name} {version}: {value} ביומים, {value2} פיצ'רים ממוקמים ({detail}), {value3} מבנים, {value4} סוגי ממדים, {value5} level stems" +quiet = "ל-{count} מודים טעונים נוספים אין תוכן יצירת עולם." +unknown_namespace = "בקטלוג יצירת העולם אין תוכן ממרחב השמות {name}." +detail_biomes = "ביומים ({count}): {value}" +detail_features = "פיצ'רים ממוקמים לפי סוג ({count}): {value}" +detail_structures = "מבנים ({count}): {value}" +detail_dimensions = "סוגי ממדים: {value}; level stems: {value2}" +refreshing = "אוסף את קטלוג יצירת העולם..." +refreshed = "קטלוג יצירת העולם נאסף תוך {duration}: {path}" +refresh_failed = "איסוף קטלוג יצירת העולם נכשל: {reason}" +ingest_invalid = "ארגומנטים לא תקינים: {reason}. שימוש: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "החבילה '{pack}' לא נמצאה תחת {path}." +ingest_started = "מייבא את יצירת העולם של המודים מ-'{source}' אל '{target}' (ממד {dimension})..." +ingest_failed = "ייבוא המודים נכשל: {reason}" +ingest_line = " {summary}" +ingest_complete = "ייבוא המודים הסתיים: {path}" + [iris.modded.command] "properties.none" = "נכסים: (אין)" "properties.list" = "נכסים: {properties}" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index 898f5b296..4e835edf0 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -361,6 +361,9 @@ operator_instruction = "Esegui {command} dalla console oppure abilita i trucchi "entry.command.debug" = "Attiva o disattiva la registrazione di debug Iris e salva iris.json" "entry.command.reload" = "Ricarica iris.json; il file viene anche ricaricato automaticamente ogni 3 secondi" "entry.command.download" = "Scarica un progetto Pack" +"entry.command.catalog" = "Elenca i contenuti di generazione delle mod caricate, mostra un namespace o acquisiscilo di nuovo" +"entry.group.ingest" = "Crea pack Iris dai contenuti di generazione delle mod caricate" +"entry.command.ingest_mods" = "Copia un pack di origine in un nuovo pack di destinazione e adotta la generazione delle mod caricate" "entry.command.metrics" = "Mostra le metriche di generazione della dimensione Iris corrente" "entry.command.regen" = "Eliminare e rigenerare i chunk vicini al posto" "entry.group.pregen" = "Pregenera una dimensione Iris" @@ -455,6 +458,26 @@ operator_instruction = "Esegui {command} dalla console oppure abilita i trucchi "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Mostra i contenuti che non possono essere generati su questa versione di Minecraft" +[iris.modded.catalog] +missing = "Nessun catalogo di generazione è stato ancora acquisito. Esegui /iris catalog refresh." +header = "Catalogo di generazione ({name} {version}): {count} namespace, {value} biomi, {value2} feature piazzate, {value3} strutture, {value4} tipi di dimensione, {value5} level stem" +namespace = "{name} {version}: {value} biomi, {value2} feature piazzate ({detail}), {value3} strutture, {value4} tipi di dimensione, {value5} level stem" +quiet = "Altre {count} mod caricate non hanno contenuti di generazione." +unknown_namespace = "Il catalogo di generazione non contiene nulla dal namespace {name}." +detail_biomes = "Biomi ({count}): {value}" +detail_features = "Feature piazzate per tipo ({count}): {value}" +detail_structures = "Strutture ({count}): {value}" +detail_dimensions = "Tipi di dimensione: {value}; level stem: {value2}" +refreshing = "Acquisizione del catalogo di generazione..." +refreshed = "Catalogo di generazione acquisito in {duration}: {path}" +refresh_failed = "Acquisizione del catalogo di generazione non riuscita: {reason}" +ingest_invalid = "Argomenti non validi: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Il pack '{pack}' non è stato trovato in {path}." +ingest_started = "Importazione della generazione delle mod da '{source}' a '{target}' (dimensione {dimension})..." +ingest_failed = "Importazione delle mod non riuscita: {reason}" +ingest_line = " {summary}" +ingest_complete = "Importazione delle mod completata: {path}" + [iris.modded.command] "properties.none" = "Proprietà: (nessuna)" "properties.list" = "Proprietà: {properties}" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index 2f3d4fc21..1e6b8026b 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -364,6 +364,9 @@ operator_instruction = "コンソールから {command} を実行してくださ "entry.command.debug" = "Iris のデバッグログを切り替えて iris.json を保存します" "entry.command.reload" = "iris.json を再読み込みします(3s ごとの自動ホットロードにも対応)" "entry.command.download" = "パックプロジェクトをダウンロードします" +"entry.command.catalog" = "読み込まれた Mod のワールド生成コンテンツを一覧表示し、1 つの名前空間を表示し、または再取得します" +"entry.group.ingest" = "読み込まれた Mod のワールド生成コンテンツから Iris パックを作成します" +"entry.command.ingest_mods" = "元のパックを新しいパックにコピーし、読み込まれた Mod のワールド生成を取り込みます" "entry.command.metrics" = "現在の Iris ディメンションの生成メトリクスを表示します" "entry.command.regen" = "周囲のチャンクを削除し、その場で再生成します" "entry.group.pregen" = "Iris ディメンションを事前生成します" @@ -458,6 +461,26 @@ operator_instruction = "コンソールから {command} を実行してくださ "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "この Minecraft バージョンでは生成できない内容を表示します" +[iris.modded.catalog] +missing = "ワールド生成カタログはまだ取得されていません。/iris catalog refresh を実行してください。" +header = "ワールド生成カタログ ({name} {version}): 名前空間 {count}、バイオーム {value}、配置済みフィーチャー {value2}、構造物 {value3}、ディメンションタイプ {value4}、レベルステム {value5}" +namespace = "{name} {version}: バイオーム {value}、配置済みフィーチャー {value2} ({detail})、構造物 {value3}、ディメンションタイプ {value4}、レベルステム {value5}" +quiet = "ほかに読み込まれた {count} 個の Mod にはワールド生成コンテンツがありません。" +unknown_namespace = "ワールド生成カタログには名前空間 {name} のコンテンツがありません。" +detail_biomes = "バイオーム ({count}): {value}" +detail_features = "種類別の配置済みフィーチャー ({count}): {value}" +detail_structures = "構造物 ({count}): {value}" +detail_dimensions = "ディメンションタイプ: {value}、レベルステム: {value2}" +refreshing = "ワールド生成カタログを取得しています..." +refreshed = "ワールド生成カタログを {duration} で取得しました: {path}" +refresh_failed = "ワールド生成カタログの取得に失敗しました: {reason}" +ingest_invalid = "引数が無効です: {reason}。使い方: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "パック '{pack}' が {path} に見つかりません。" +ingest_started = "'{source}' から '{target}' へ Mod のワールド生成を取り込んでいます (ディメンション {dimension})..." +ingest_failed = "Mod の取り込みに失敗しました: {reason}" +ingest_line = " {summary}" +ingest_complete = "Mod の取り込みが完了しました: {path}" + [iris.modded.command] "properties.none" = "プロパティ:(なし)" "properties.list" = "プロパティ: {properties}" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 967099f9d..e8e66319c 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -364,6 +364,9 @@ operator_instruction = "콘솔에서 {command}을(를) 실행하세요(싱글 "entry.command.debug" = "Iris 디버그 로깅을 전환하고 iris.json을 저장합니다" "entry.command.reload" = "iris.json을 다시 로드합니다(3s마다 자동 핫로드도 수행)" "entry.command.download" = "팩 프로젝트를 다운로드합니다" +"entry.command.catalog" = "로드된 모드의 월드 생성 콘텐츠를 나열하거나, 네임스페이스 하나를 표시하거나, 다시 수집합니다" +"entry.group.ingest" = "로드된 모드의 월드 생성 콘텐츠로 Iris 팩을 만듭니다" +"entry.command.ingest_mods" = "원본 팩을 새 대상 팩으로 복사하고 로드된 모드의 월드 생성을 적용합니다" "entry.command.metrics" = "현재 Iris 차원의 생성 측정값을 표시합니다" "entry.command.regen" = "주변 청크를 삭제하고 제자리에서 다시 생성합니다" "entry.group.pregen" = "Iris 차원을 사전 생성합니다" @@ -458,6 +461,26 @@ operator_instruction = "콘솔에서 {command}을(를) 실행하세요(싱글 "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "이 Minecraft 버전에서 생성할 수 없는 콘텐츠를 표시합니다" +[iris.modded.catalog] +missing = "아직 월드 생성 카탈로그가 수집되지 않았습니다. /iris catalog refresh를 실행하세요." +header = "월드 생성 카탈로그 ({name} {version}): 네임스페이스 {count}개, 바이옴 {value}개, 배치된 지형지물 {value2}개, 구조물 {value3}개, 차원 유형 {value4}개, 레벨 스템 {value5}개" +namespace = "{name} {version}: 바이옴 {value}개, 배치된 지형지물 {value2}개 ({detail}), 구조물 {value3}개, 차원 유형 {value4}개, 레벨 스템 {value5}개" +quiet = "로드된 다른 모드 {count}개에는 월드 생성 콘텐츠가 없습니다." +unknown_namespace = "월드 생성 카탈로그에 네임스페이스 {name}의 콘텐츠가 없습니다." +detail_biomes = "바이옴 ({count}): {value}" +detail_features = "유형별 배치된 지형지물 ({count}): {value}" +detail_structures = "구조물 ({count}): {value}" +detail_dimensions = "차원 유형: {value}; 레벨 스템: {value2}" +refreshing = "월드 생성 카탈로그를 수집하는 중..." +refreshed = "월드 생성 카탈로그를 {duration} 만에 수집했습니다: {path}" +refresh_failed = "월드 생성 카탈로그 수집에 실패했습니다: {reason}" +ingest_invalid = "잘못된 인수: {reason}. 사용법: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "{path}에서 팩 '{pack}'을(를) 찾을 수 없습니다." +ingest_started = "'{source}'에서 '{target}'(으)로 모드 월드 생성을 가져오는 중 (차원 {dimension})..." +ingest_failed = "모드 가져오기에 실패했습니다: {reason}" +ingest_line = " {summary}" +ingest_complete = "모드 가져오기 완료: {path}" + [iris.modded.command] "properties.none" = "속성: (없음)" "properties.list" = "속성: {properties}" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index 9fdbf193e..ad98e567b 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -366,6 +366,9 @@ operator_instruction = "Vykdyti {command} iš konsolės (arba leidžia cheats į "entry.command.debug" = "Perjungti Iris Derinti prisijungimą ir įrašymą iris.json" "entry.command.reload" = "Įkelti iš naujo iris.json (taip pat įkeliamas automatiškai kas 3s)" "entry.command.download" = "Parsisiųsti paketo projektą" +"entry.command.catalog" = "Išvardyti įkeltų modų pasaulio generavimo turinį, parodyti vieną vardų sritį arba surinkti jį iš naujo" +"entry.group.ingest" = "Kurti Iris paketus iš įkeltų modų pasaulio generavimo turinio" +"entry.command.ingest_mods" = "Nukopijuoti šaltinio paketą į naują paskirties paketą ir perimti įkeltų modų pasaulio generavimą" "entry.command.metrics" = "Spausdinti kartos metrikos savo dabartinę Iris matmuo" "entry.command.regen" = "Pašalinti ir regeneruoti netoliese gabaliukus vietoje" "entry.group.pregen" = "Regeneruoti Iris matmuo" @@ -460,6 +463,26 @@ operator_instruction = "Vykdyti {command} iš konsolės (arba leidžia cheats į "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Rodyti turinį, kurio negalima sugeneruoti šioje Minecraft versijoje" +[iris.modded.catalog] +missing = "Pasaulio generavimo katalogas dar nesurinktas. Paleisk /iris catalog refresh." +header = "Pasaulio generavimo katalogas ({name} {version}): vardų sritys {count}, biomai {value}, išdėstyti elementai {value2}, struktūros {value3}, dimensijų tipai {value4}, lygio stiebai {value5}" +namespace = "{name} {version}: biomai {value}, išdėstyti elementai {value2} ({detail}), struktūros {value3}, dimensijų tipai {value4}, lygio stiebai {value5}" +quiet = "Dar {count} įkeltų modų neturi pasaulio generavimo turinio." +unknown_namespace = "Pasaulio generavimo kataloge nėra turinio iš vardų srities {name}." +detail_biomes = "Biomai ({count}): {value}" +detail_features = "Išdėstyti elementai pagal tipą ({count}): {value}" +detail_structures = "Struktūros ({count}): {value}" +detail_dimensions = "Dimensijų tipai: {value}; lygio stiebai: {value2}" +refreshing = "Renkamas pasaulio generavimo katalogas..." +refreshed = "Pasaulio generavimo katalogas surinktas per {duration}: {path}" +refresh_failed = "Nepavyko surinkti pasaulio generavimo katalogo: {reason}" +ingest_invalid = "Netinkami argumentai: {reason}. Naudojimas: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Paketas '{pack}' nerastas {path}." +ingest_started = "Perkeliamas modų pasaulio generavimas iš '{source}' į '{target}' (dimensija {dimension})..." +ingest_failed = "Modų perkėlimas nepavyko: {reason}" +ingest_line = " {summary}" +ingest_complete = "Modų perkėlimas baigtas: {path}" + [iris.modded.command] "properties.none" = "Savybės: (nėra)" "properties.list" = "Savybės: {properties}" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index 3817f6169..6d8ec3700 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -361,6 +361,9 @@ operator_instruction = "Start {command} vanaf de console (of schakel bedrog in s "entry.command.debug" = "Schakelen Iris debug loggen en opslaan iris.json" "entry.command.reload" = "Herladen iris.json (ook automatisch gehotload elke 3s)" "entry.command.download" = "Een pakketproject downloaden" +"entry.command.catalog" = "De wereldgeneratie-inhoud van geladen mods tonen, één namespace weergeven of opnieuw vastleggen" +"entry.group.ingest" = "Iris-packs maken van de wereldgeneratie-inhoud van geladen mods" +"entry.command.ingest_mods" = "Een bronpack naar een nieuw doelpack kopiëren en de wereldgeneratie van geladen mods overnemen" "entry.command.metrics" = "Generatiegegevens afdrukken voor uw huidige Iris dimensie" "entry.command.regen" = "Verwijderen en regenereren nabijgelegen chunks in plaats" "entry.group.pregen" = "Pregeneer een Iris dimensie" @@ -455,6 +458,26 @@ operator_instruction = "Start {command} vanaf de console (of schakel bedrog in s "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Toon inhoud die niet kan worden gegenereerd op deze Minecraft-versie" +[iris.modded.catalog] +missing = "Er is nog geen wereldgeneratiecatalogus vastgelegd. Voer /iris catalog refresh uit." +header = "Wereldgeneratiecatalogus ({name} {version}): {count} namespaces, {value} biomen, {value2} geplaatste features, {value3} structuren, {value4} dimensietypen, {value5} level stems" +namespace = "{name} {version}: {value} biomen, {value2} geplaatste features ({detail}), {value3} structuren, {value4} dimensietypen, {value5} level stems" +quiet = "{count} andere geladen mods hebben geen wereldgeneratie-inhoud." +unknown_namespace = "De wereldgeneratiecatalogus bevat geen inhoud uit namespace {name}." +detail_biomes = "Biomen ({count}): {value}" +detail_features = "Geplaatste features per type ({count}): {value}" +detail_structures = "Structuren ({count}): {value}" +detail_dimensions = "Dimensietypen: {value}; level stems: {value2}" +refreshing = "Wereldgeneratiecatalogus wordt vastgelegd..." +refreshed = "Wereldgeneratiecatalogus vastgelegd in {duration}: {path}" +refresh_failed = "Vastleggen van de wereldgeneratiecatalogus mislukt: {reason}" +ingest_invalid = "Ongeldige argumenten: {reason}. Gebruik: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Pack '{pack}' is niet gevonden in {path}." +ingest_started = "Wereldgeneratie van mods wordt overgenomen van '{source}' naar '{target}' (dimensie {dimension})..." +ingest_failed = "Overnemen van mods mislukt: {reason}" +ingest_line = " {summary}" +ingest_complete = "Overnemen van mods voltooid: {path}" + [iris.modded.command] "properties.none" = "Eigenschappen: (geen)" "properties.list" = "Eigenschappen: {properties}" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index 32922cf6e..e60fcc9ee 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -366,6 +366,9 @@ operator_instruction = "Biegnij {command} z konsoli (lub włączyć cheats w sin "entry.command.debug" = "Włączenie / wyłączenie Iris debugowanie logowania i zapisywanie iris.json" "entry.command.reload" = "Wczytaj ponownie iris.json (także automatycznie naświetlane co 3)" "entry.command.download" = "Pobierz projekt pakietu" +"entry.command.catalog" = "Wyświetl zawartość generowania świata wczytanych modów, pokaż jedną przestrzeń nazw lub zbierz ją ponownie" +"entry.group.ingest" = "Twórz paczki Iris z zawartości generowania świata wczytanych modów" +"entry.command.ingest_mods" = "Skopiuj paczkę źródłową do nowej paczki docelowej i przejmij generowanie świata wczytanych modów" "entry.command.metrics" = "Wydrukuj wskaźniki generacji dla bieżącego Iris wymiar" "entry.command.regen" = "Usuń i zregeneruj pobliskie chunki w miejscu" "entry.group.pregen" = "Przed Iris wymiar" @@ -460,6 +463,26 @@ operator_instruction = "Biegnij {command} z konsoli (lub włączyć cheats w sin "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Pokaż zawartość, której nie można wygenerować w tej wersji Minecraft" +[iris.modded.catalog] +missing = "Katalog generowania świata nie został jeszcze zebrany. Uruchom /iris catalog refresh." +header = "Katalog generowania świata ({name} {version}): przestrzenie nazw: {count}, biomy: {value}, umieszczone obiekty: {value2}, struktury: {value3}, typy wymiarów: {value4}, level stemy: {value5}" +namespace = "{name} {version}: biomy: {value}, umieszczone obiekty: {value2} ({detail}), struktury: {value3}, typy wymiarów: {value4}, level stemy: {value5}" +quiet = "Kolejne wczytane mody ({count}) nie mają zawartości generowania świata." +unknown_namespace = "Katalog generowania świata nie zawiera nic z przestrzeni nazw {name}." +detail_biomes = "Biomy ({count}): {value}" +detail_features = "Umieszczone obiekty według typu ({count}): {value}" +detail_structures = "Struktury ({count}): {value}" +detail_dimensions = "Typy wymiarów: {value}; level stemy: {value2}" +refreshing = "Zbieranie katalogu generowania świata..." +refreshed = "Katalog generowania świata zebrany w {duration}: {path}" +refresh_failed = "Nie udało się zebrać katalogu generowania świata: {reason}" +ingest_invalid = "Nieprawidłowe argumenty: {reason}. Użycie: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Nie znaleziono paczki '{pack}' w {path}." +ingest_started = "Przenoszenie generowania świata modów z '{source}' do '{target}' (wymiar {dimension})..." +ingest_failed = "Przenoszenie modów nie powiodło się: {reason}" +ingest_line = " {summary}" +ingest_complete = "Przenoszenie modów zakończone: {path}" + [iris.modded.command] "properties.none" = "Właściwości: (brak)" "properties.list" = "Właściwości: {properties}" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index d2a4a1335..d047de3a4 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -361,6 +361,9 @@ operator_instruction = "Executar {command} a partir do console (ou habilitar fra "entry.command.debug" = "Alternar Iris debug registrar e salvar iris.json" "entry.command.reload" = "Recarregar iris.json (também carregado automaticamente a cada 3s)" "entry.command.download" = "Baixar um projeto de pacote" +"entry.command.catalog" = "Listar o conteúdo de geração dos mods carregados, mostrar um namespace ou capturá-lo novamente" +"entry.group.ingest" = "Criar packs Iris a partir do conteúdo de geração dos mods carregados" +"entry.command.ingest_mods" = "Copiar um pack de origem para um novo pack de destino e adotar a geração dos mods carregados" "entry.command.metrics" = "Imprimir métricas de geração para o seu atual Iris dimensão" "entry.command.regen" = "Apagar e regenerar chunks próximos no lugar" "entry.group.pregen" = "Pré- Gerar uma Iris dimensão" @@ -455,6 +458,26 @@ operator_instruction = "Executar {command} a partir do console (ou habilitar fra "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Mostrar o conteúdo que não pode ser gerado nesta versão do Minecraft" +[iris.modded.catalog] +missing = "Ainda não foi capturado nenhum catálogo de geração de mundo. Executa /iris catalog refresh." +header = "Catálogo de geração ({name} {version}): {count} namespaces, {value} biomas, {value2} features colocadas, {value3} estruturas, {value4} tipos de dimensão, {value5} level stems" +namespace = "{name} {version}: {value} biomas, {value2} features colocadas ({detail}), {value3} estruturas, {value4} tipos de dimensão, {value5} level stems" +quiet = "Mais {count} mods carregados não têm conteúdo de geração de mundo." +unknown_namespace = "O catálogo de geração não tem conteúdo do namespace {name}." +detail_biomes = "Biomas ({count}): {value}" +detail_features = "Features colocadas por tipo ({count}): {value}" +detail_structures = "Estruturas ({count}): {value}" +detail_dimensions = "Tipos de dimensão: {value}; level stems: {value2}" +refreshing = "A capturar o catálogo de geração de mundo..." +refreshed = "Catálogo de geração capturado em {duration}: {path}" +refresh_failed = "Falha ao capturar o catálogo de geração: {reason}" +ingest_invalid = "Argumentos inválidos: {reason}. Utilização: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "O pack '{pack}' não foi encontrado em {path}." +ingest_started = "A importar a geração de mods de '{source}' para '{target}' (dimensão {dimension})..." +ingest_failed = "A importação de mods falhou: {reason}" +ingest_line = " {summary}" +ingest_complete = "Importação de mods concluída: {path}" + [iris.modded.command] "properties.none" = "Propriedades: (nenhum)" "properties.list" = "Propriedades: {properties}" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index bb948faf5..18611ac30 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -366,6 +366,9 @@ operator_instruction = "Беги. {command} от консоли (или вклю "entry.command.debug" = "переключаться Iris Отладка и сохранение iris.json" "entry.command.reload" = "Перезагрузить iris.json (также автоматически загружается каждые 3 секунды)" "entry.command.download" = "Скачать пакетный проект" +"entry.command.catalog" = "Показать генерацию мира загруженных модов, одно пространство имён или собрать каталог заново" +"entry.group.ingest" = "Создать паки Iris из генерации мира загруженных модов" +"entry.command.ingest_mods" = "Скопировать исходный пак в новый целевой пак и перенести в него генерацию мира загруженных модов" "entry.command.metrics" = "Метрики генерации печати для вашего текущего Iris измерение" "entry.command.regen" = "Удалить и регенерировать близлежащие чанки на месте" "entry.group.pregen" = "Прегенерировать Iris измерение" @@ -460,6 +463,26 @@ operator_instruction = "Беги. {command} от консоли (или вклю "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Показать содержимое, которое нельзя сгенерировать в этой версии Minecraft" +[iris.modded.catalog] +missing = "Каталог генерации мира ещё не собран. Выполните /iris catalog refresh." +header = "Каталог генерации мира ({name} {version}): пространств имён {count}, биомов {value}, размещённых объектов {value2}, структур {value3}, типов измерений {value4}, level stem {value5}" +namespace = "{name} {version}: биомов {value}, размещённых объектов {value2} ({detail}), структур {value3}, типов измерений {value4}, level stem {value5}" +quiet = "Ещё {count} загруженных модов не содержат генерации мира." +unknown_namespace = "В каталоге генерации мира нет содержимого из пространства имён {name}." +detail_biomes = "Биомы ({count}): {value}" +detail_features = "Размещённые объекты по типам ({count}): {value}" +detail_structures = "Структуры ({count}): {value}" +detail_dimensions = "Типы измерений: {value}; level stem: {value2}" +refreshing = "Сбор каталога генерации мира..." +refreshed = "Каталог генерации мира собран за {duration}: {path}" +refresh_failed = "Не удалось собрать каталог генерации мира: {reason}" +ingest_invalid = "Неверные аргументы: {reason}. Использование: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Пак '{pack}' не найден в {path}." +ingest_started = "Перенос генерации мира модов из '{source}' в '{target}' (измерение {dimension})..." +ingest_failed = "Не удалось перенести моды: {reason}" +ingest_line = " {summary}" +ingest_complete = "Перенос модов завершён: {path}" + [iris.modded.command] "properties.none" = "Свойства: (нет)" "properties.list" = "Свойства: {properties}" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index f53c5c664..434917837 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -366,6 +366,9 @@ operator_instruction = "{command} komutunu konsoldan çalıştırın (veya tek o "entry.command.debug" = "değiştir Iris Debug login ve kaydet iris.json" "entry.command.reload" = "Yeniden yükleme iris.json (Ayrıca her 3s otomatik olarak ısındı)" "entry.command.download" = "Bir paket projesi indirin" +"entry.command.catalog" = "Yüklü modların dünya oluşturma içeriğini listele, tek bir ad alanını göster veya yeniden topla" +"entry.group.ingest" = "Yüklü modların dünya oluşturma içeriğinden Iris paketleri oluştur" +"entry.command.ingest_mods" = "Bir kaynak paketi yeni bir hedef pakete kopyala ve yüklü modların dünya oluşturmasını uygula" "entry.command.metrics" = "Mevcut nesil ölçümler için Iris boyut boyutu" "entry.command.regen" = "Yakınlarda silme ve yenidenjenere" "entry.group.pregen" = "Pregenerate a an Iris boyut boyutu" @@ -460,6 +463,26 @@ operator_instruction = "{command} komutunu konsoldan çalıştırın (veya tek o "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Bu Minecraft sürümünde üretilemeyen içeriği göster" +[iris.modded.catalog] +missing = "Henüz bir dünya oluşturma kataloğu toplanmadı. /iris catalog refresh komutunu çalıştırın." +header = "Dünya oluşturma kataloğu ({name} {version}): {count} ad alanı, {value} biyom, {value2} yerleştirilmiş özellik, {value3} yapı, {value4} boyut türü, {value5} level stem" +namespace = "{name} {version}: {value} biyom, {value2} yerleştirilmiş özellik ({detail}), {value3} yapı, {value4} boyut türü, {value5} level stem" +quiet = "Yüklü {count} mod daha dünya oluşturma içeriğine sahip değil." +unknown_namespace = "Dünya oluşturma kataloğunda {name} ad alanından içerik yok." +detail_biomes = "Biyomlar ({count}): {value}" +detail_features = "Türe göre yerleştirilmiş özellikler ({count}): {value}" +detail_structures = "Yapılar ({count}): {value}" +detail_dimensions = "Boyut türleri: {value}; level stem'ler: {value2}" +refreshing = "Dünya oluşturma kataloğu toplanıyor..." +refreshed = "Dünya oluşturma kataloğu {duration} içinde toplandı: {path}" +refresh_failed = "Dünya oluşturma kataloğu toplanamadı: {reason}" +ingest_invalid = "Geçersiz argümanlar: {reason}. Kullanım: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "'{pack}' paketi {path} altında bulunamadı." +ingest_started = "Mod dünya oluşturması '{source}' paketinden '{target}' paketine aktarılıyor (boyut {dimension})..." +ingest_failed = "Mod aktarımı başarısız oldu: {reason}" +ingest_line = " {summary}" +ingest_complete = "Mod aktarımı tamamlandı: {path}" + [iris.modded.command] "properties.none" = "Özellikler: (hiçbiri)" "properties.list" = "Özellikler: {properties}" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index c8ace9fa5..42ae19f29 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -364,6 +364,9 @@ operator_instruction = "Chạy {command} từ bàn giao tiếp (hoặc kích ho "entry.command.debug" = "Bật/ tắt Iris @ info/ rich iris.json" "entry.command.reload" = "Nạp lại iris.json (đã nạp quá nóng tự động mỗi 3)" "entry.command.download" = "Tải về dự án gói" +"entry.command.catalog" = "Liệt kê nội dung tạo thế giới của các mod đã tải, hiển thị một không gian tên hoặc thu thập lại" +"entry.group.ingest" = "Tạo gói Iris từ nội dung tạo thế giới của các mod đã tải" +"entry.command.ingest_mods" = "Sao chép gói nguồn sang gói đích mới và áp dụng phần tạo thế giới của các mod đã tải" "entry.command.metrics" = "Số đo cỡ ảnh hiện thời Iris Chiều rộng" "entry.command.regen" = "Xóa và tái tạo các mẫu gần đó" "entry.group.pregen" = "Tiền tố a Iris Chiều rộng" @@ -458,6 +461,26 @@ operator_instruction = "Chạy {command} từ bàn giao tiếp (hoặc kích ho "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "Hiển thị nội dung không thể tạo trên phiên bản Minecraft này" +[iris.modded.catalog] +missing = "Chưa thu thập danh mục tạo thế giới nào. Hãy chạy /iris catalog refresh." +header = "Danh mục tạo thế giới ({name} {version}): {count} không gian tên, {value} quần xã, {value2} feature đã đặt, {value3} công trình, {value4} loại chiều, {value5} level stem" +namespace = "{name} {version}: {value} quần xã, {value2} feature đã đặt ({detail}), {value3} công trình, {value4} loại chiều, {value5} level stem" +quiet = "{count} mod đã tải khác không có nội dung tạo thế giới." +unknown_namespace = "Danh mục tạo thế giới không có nội dung từ không gian tên {name}." +detail_biomes = "Quần xã ({count}): {value}" +detail_features = "Feature đã đặt theo loại ({count}): {value}" +detail_structures = "Công trình ({count}): {value}" +detail_dimensions = "Loại chiều: {value}; level stem: {value2}" +refreshing = "Đang thu thập danh mục tạo thế giới..." +refreshed = "Đã thu thập danh mục tạo thế giới trong {duration}: {path}" +refresh_failed = "Thu thập danh mục tạo thế giới thất bại: {reason}" +ingest_invalid = "Đối số không hợp lệ: {reason}. Cách dùng: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "Không tìm thấy gói '{pack}' trong {path}." +ingest_started = "Đang đưa phần tạo thế giới của mod từ '{source}' vào '{target}' (chiều {dimension})..." +ingest_failed = "Đưa mod vào thất bại: {reason}" +ingest_line = " {summary}" +ingest_complete = "Đã đưa mod vào xong: {path}" + [iris.modded.command] "properties.none" = "Thuộc tính: (không có)" "properties.list" = "Thuộc tính {properties}" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index 4156dd8d7..870a5ff02 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -364,6 +364,9 @@ operator_instruction = "请从控制台执行 {command}(或在单人游戏中 "entry.command.debug" = "切换 Iris 调试日志并保存 iris.json" "entry.command.reload" = "重新加载 iris.json(也会每 3 秒自动热加载)" "entry.command.download" = "下载包项目" +"entry.command.catalog" = "列出已加载模组的世界生成内容、显示单个命名空间或重新采集" +"entry.group.ingest" = "根据已加载模组的世界生成内容创建 Iris 包" +"entry.command.ingest_mods" = "将源包复制为新的目标包,并采用已加载模组的世界生成" "entry.command.metrics" = "显示当前 Iris 维度的生成指标" "entry.command.regen" = "原地删除并重新生成附近区块" "entry.group.pregen" = "预生成 Iris 维度" @@ -458,6 +461,26 @@ operator_instruction = "请从控制台执行 {command}(或在单人游戏中 "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "显示在当前 Minecraft 版本中无法生成的内容" +[iris.modded.catalog] +missing = "尚未采集世界生成目录。请运行 /iris catalog refresh。" +header = "世界生成目录({name} {version}):{count} 个命名空间,{value} 个生物群系,{value2} 个已放置地物,{value3} 个结构,{value4} 个维度类型,{value5} 个 level stem" +namespace = "{name} {version}:{value} 个生物群系,{value2} 个已放置地物({detail}),{value3} 个结构,{value4} 个维度类型,{value5} 个 level stem" +quiet = "另有 {count} 个已加载的模组没有世界生成内容。" +unknown_namespace = "世界生成目录中没有来自命名空间 {name} 的内容。" +detail_biomes = "生物群系({count}):{value}" +detail_features = "按类型的已放置地物({count}):{value}" +detail_structures = "结构({count}):{value}" +detail_dimensions = "维度类型:{value};level stem:{value2}" +refreshing = "正在采集世界生成目录..." +refreshed = "世界生成目录已在 {duration} 内采集完成:{path}" +refresh_failed = "采集世界生成目录失败:{reason}" +ingest_invalid = "参数无效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "在 {path} 下未找到包 '{pack}'。" +ingest_started = "正在将模组世界生成从 '{source}' 导入 '{target}'(维度 {dimension})..." +ingest_failed = "模组导入失败:{reason}" +ingest_line = " {summary}" +ingest_complete = "模组导入完成:{path}" + [iris.modded.command] "properties.none" = "属性:(无)" "properties.list" = "属性 : {properties}" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index 55cdb61cc..a021f09ee 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -364,6 +364,9 @@ operator_instruction = "請從主控台執行 {command}(或在單人遊戲中 "entry.command.debug" = "切換 Iris 偵錯記錄並儲存 iris.json" "entry.command.reload" = "重新載入 iris.json(也會每 3 秒自動熱載入)" "entry.command.download" = "下載包專案" +"entry.command.catalog" = "列出已載入模組的世界生成內容、顯示單一命名空間或重新擷取" +"entry.group.ingest" = "根據已載入模組的世界生成內容建立 Iris 包" +"entry.command.ingest_mods" = "將來源包複製為新的目標包,並採用已載入模組的世界生成" "entry.command.metrics" = "顯示目前 Iris 維度的生成指標" "entry.command.regen" = "原地刪除並重新生成附近區塊" "entry.group.pregen" = "預先生成 Iris 維度" @@ -458,6 +461,26 @@ operator_instruction = "請從主控台執行 {command}(或在單人遊戲中 "entry.command.studio.objects" = "Explain why the Bukkit chunk object report is unavailable" "entry.command.compat_content" = "顯示在目前 Minecraft 版本中無法生成的內容" +[iris.modded.catalog] +missing = "尚未擷取世界生成目錄。請執行 /iris catalog refresh。" +header = "世界生成目錄({name} {version}):{count} 個命名空間,{value} 個生態域,{value2} 個已放置地物,{value3} 個結構,{value4} 個維度類型,{value5} 個 level stem" +namespace = "{name} {version}:{value} 個生態域,{value2} 個已放置地物({detail}),{value3} 個結構,{value4} 個維度類型,{value5} 個 level stem" +quiet = "另有 {count} 個已載入的模組沒有世界生成內容。" +unknown_namespace = "世界生成目錄中沒有來自命名空間 {name} 的內容。" +detail_biomes = "生態域({count}):{value}" +detail_features = "依類型的已放置地物({count}):{value}" +detail_structures = "結構({count}):{value}" +detail_dimensions = "維度類型:{value};level stem:{value2}" +refreshing = "正在擷取世界生成目錄..." +refreshed = "世界生成目錄已在 {duration} 內擷取完成:{path}" +refresh_failed = "擷取世界生成目錄失敗:{reason}" +ingest_invalid = "參數無效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_pack_missing = "在 {path} 下找不到包 '{pack}'。" +ingest_started = "正在將模組世界生成從 '{source}' 匯入 '{target}'(維度 {dimension})..." +ingest_failed = "模組匯入失敗:{reason}" +ingest_line = " {summary}" +ingest_complete = "模組匯入完成:{path}" + [iris.modded.command] "properties.none" = "屬性:(無)" "properties.list" = "屬性 : {properties}" diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprintTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprintTest.java new file mode 100644 index 000000000..773fdca1c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/pack/ingest/catalog/WorldgenCatalogFingerprintTest.java @@ -0,0 +1,77 @@ +/* + * 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 . + */ + +package art.arcane.iris.pack.ingest.catalog; + +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class WorldgenCatalogFingerprintTest { + private static final List NAMESPACES = List.of( + new CatalogNamespace("minecraft", "Minecraft", "26.3"), + new CatalogNamespace("examplemod", "Example Mod", "1.0.0")); + + @Test + public void fingerprintIsLowercaseSha256Hex() { + String fingerprint = WorldgenCatalogFingerprint.of(NAMESPACES, Map.of("biome", List.of("minecraft:plains"))); + + assertEquals(64, fingerprint.length()); + assertTrue(fingerprint.matches("[0-9a-f]{64}")); + } + + @Test + public void fingerprintIgnoresNamespaceAndKeyOrder() { + String ordered = WorldgenCatalogFingerprint.of(NAMESPACES, Map.of( + "biome", List.of("examplemod:glade", "minecraft:plains"), + "placed_feature", List.of("examplemod:ore_glimmer", "minecraft:ore_iron"))); + String shuffled = WorldgenCatalogFingerprint.of(List.of(NAMESPACES.get(1), NAMESPACES.get(0)), Map.of( + "placed_feature", List.of("minecraft:ore_iron", "examplemod:ore_glimmer"), + "biome", List.of("minecraft:plains", "examplemod:glade"))); + + assertEquals(ordered, shuffled); + } + + @Test + public void fingerprintChangesWithModVersion() { + Map> keys = Map.of("biome", List.of("examplemod:glade")); + String original = WorldgenCatalogFingerprint.of(NAMESPACES, keys); + String updated = WorldgenCatalogFingerprint.of(List.of(NAMESPACES.get(0), + new CatalogNamespace("examplemod", "Example Mod", "1.0.1")), keys); + + assertNotEquals(original, updated); + } + + @Test + public void fingerprintChangesWhenAKeyIsAddedOrMovesRegistry() { + String original = WorldgenCatalogFingerprint.of(NAMESPACES, Map.of( + "biome", List.of("examplemod:glade"), "structure", List.of())); + String added = WorldgenCatalogFingerprint.of(NAMESPACES, Map.of( + "biome", List.of("examplemod:glade", "examplemod:marsh"), "structure", List.of())); + String moved = WorldgenCatalogFingerprint.of(NAMESPACES, Map.of( + "biome", List.of(), "structure", List.of("examplemod:glade"))); + + assertNotEquals(original, added); + assertNotEquals(original, moved); + } +} From 769e0770784f2e32c4ccaf3ffab2631d6dc3538c Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 20:18:50 -0400 Subject: [PATCH 2/2] Clarified ingest target errors - Rejected target pack names outside letters, digits, underscores and hyphens before ingest starts - Reported an existing target pack with a translated replace=true hint instead of the raw path --- .../art/arcane/iris/modded/command/ModIngestArguments.java | 4 ++++ .../arcane/iris/modded/command/ModdedCatalogCommands.java | 4 ++++ .../iris/modded/localization/ModdedCatalogMessages.java | 5 +++++ .../arcane/iris/modded/command/ModIngestArgumentsTest.java | 1 + core/src/main/resources/languages/de_DE.toml | 1 + core/src/main/resources/languages/es_ES.toml | 1 + core/src/main/resources/languages/fi_FI.toml | 1 + core/src/main/resources/languages/fr_FR.toml | 1 + core/src/main/resources/languages/he_IL.toml | 1 + core/src/main/resources/languages/it_IT.toml | 1 + core/src/main/resources/languages/ja-JP.toml | 1 + core/src/main/resources/languages/ko_KR.toml | 1 + core/src/main/resources/languages/lt_LT.toml | 1 + core/src/main/resources/languages/nl_NL.toml | 1 + core/src/main/resources/languages/pl_PL.toml | 1 + core/src/main/resources/languages/pt_PT.toml | 1 + core/src/main/resources/languages/ru_RU.toml | 1 + core/src/main/resources/languages/tr_TR.toml | 1 + core/src/main/resources/languages/vi_VI.toml | 1 + core/src/main/resources/languages/zh_CN.toml | 1 + core/src/main/resources/languages/zh_TW.toml | 1 + 21 files changed, 31 insertions(+) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java index 237dbf274..03b885d11 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java @@ -29,6 +29,7 @@ import java.util.regex.Pattern; record ModIngestArguments(String sourcePack, String targetPack, String dimension, Set namespaces, boolean replace) { static final List OPTION_SUGGESTIONS = List.of("dimension=", "namespaces=", "replace=true"); private static final Pattern PACK = Pattern.compile("[A-Za-z0-9_.-]+"); + private static final Pattern TARGET_PACK = Pattern.compile("[A-Za-z0-9_-]+"); private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+"); private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+"); private static final String DIMENSION_KEY = "dimension"; @@ -76,6 +77,9 @@ record ModIngestArguments(String sourcePack, String targetPack, String dimension } requirePack(source); requirePack(target); + if (!TARGET_PACK.matcher(target).matches()) { + throw new IllegalArgumentException("invalid target pack name '" + target + "'"); + } if (source.equals(target)) { throw new IllegalArgumentException("source and target must be different packs"); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java index 48fea1dbb..c8560f906 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java @@ -43,6 +43,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import java.io.File; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -211,6 +212,9 @@ final class ModdedCatalogCommands { try { ModIngestReport report = ModIngester.ingest(request); source.execute(() -> reportIngest(source, report)); + } catch (FileAlreadyExistsException exists) { + source.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_TARGET_EXISTS, + MessageArgument.untrusted("target", request.targetPack().getFileName().toString())))); } catch (IOException | RuntimeException failure) { ModdedIrisLog.error("Iris mod ingest into " + request.targetPack() + " failed", failure); String reason = reason(failure); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java index 02f3c2fcc..5482f9183 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java @@ -91,6 +91,10 @@ public final class ModdedCatalogMessages { "iris.modded.catalog.ingest_failed", "Mod ingest failed: {reason}" ); + public static final TextKey INGEST_TARGET_EXISTS = TextKey.of( + "iris.modded.catalog.ingest_target_exists", + "Target pack '{target}' already exists. Add replace=true to overwrite it." + ); public static final TextKey INGEST_LINE = TextKey.of( "iris.modded.catalog.ingest_line", " {summary}" @@ -129,6 +133,7 @@ public final class ModdedCatalogMessages { INGEST_PACK_MISSING, INGEST_STARTED, INGEST_FAILED, + INGEST_TARGET_EXISTS, INGEST_LINE, INGEST_COMPLETE, HELP_CATALOG, diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java index d484410b9..c90dcca25 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java @@ -68,6 +68,7 @@ public class ModIngestArgumentsTest { public void valuesAreValidated() { assertEquals("replace must be true or false", reason("overworld modded replace=yes")); assertEquals("invalid pack name '../evil'", reason("../evil modded")); + assertEquals("invalid target pack name 'modded.v2'", reason("overworld modded.v2")); assertEquals("invalid dimension key 'a..b'", reason("overworld modded dimension=a..b")); assertEquals("invalid namespace 'Bad'", reason("overworld modded namespaces=ok,Bad")); assertEquals("namespaces must not be empty", reason("overworld modded namespaces=,")); diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index 7cae93127..25189f6f8 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -475,6 +475,7 @@ ingest_invalid = "Ungültige Argumente: {reason}. Verwendung: /iris ingest mods ingest_pack_missing = "Pack '{pack}' wurde unter {path} nicht gefunden." ingest_started = "Übernehme Mod-Worldgen aus '{source}' in '{target}' (Dimension {dimension})..." ingest_failed = "Mod-Übernahme fehlgeschlagen: {reason}" +ingest_target_exists = "Ziel-Pack '{target}' existiert bereits. Füge replace=true hinzu, um es zu überschreiben." ingest_line = " {summary}" ingest_complete = "Mod-Übernahme abgeschlossen: {path}" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index 326db86d4..f126ae8d7 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -475,6 +475,7 @@ ingest_invalid = "Argumentos no válidos: {reason}. Uso: /iris ingest mods