mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
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 <source> <target> 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
This commit is contained in:
1 parent
8afb445b1d
commit
f6f381f6e4
36 files changed
+2150
No files matched your search
@@ -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) {
|
||||
|
||||
+32
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<NativeModMetadata> mods) {
|
||||
public CatalogCaptureContext {
|
||||
Objects.requireNonNull(minecraftVersion, "minecraftVersion");
|
||||
Objects.requireNonNull(platform, "platform");
|
||||
mods = List.copyOf(Objects.requireNonNull(mods, "mods"));
|
||||
}
|
||||
}
|
||||
+139
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<WorldgenCatalog> LATEST = new AtomicReference<>();
|
||||
private static final AtomicReference<CompletableFuture<CaptureResult>> RUNNING = new AtomicReference<>();
|
||||
|
||||
private ModdedWorldgenCatalogs() {
|
||||
}
|
||||
|
||||
public static Optional<WorldgenCatalog> 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<CaptureResult> capture(NativeModdedServer server) {
|
||||
Objects.requireNonNull(server, "server");
|
||||
CompletableFuture<CaptureResult> pending = new CompletableFuture<>();
|
||||
CompletableFuture<CaptureResult> 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<CaptureResult> 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<CaptureResult> 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) {
|
||||
}
|
||||
}
|
||||
+146
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<CatalogBiome> biomes = biomes(snapshot.biomes());
|
||||
List<CatalogPlacedFeature> placedFeatures = placedFeatures(snapshot.placedFeatures());
|
||||
List<CatalogStructure> structures = structures(snapshot.structures());
|
||||
List<CatalogDimensionType> dimensionTypes = dimensionTypes(snapshot.dimensionTypes());
|
||||
List<CatalogLevelStem> levelStems = levelStems(snapshot.levelStems());
|
||||
Map<String, List<String>> 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<CatalogNamespace> 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<CatalogNamespace> namespaces(Map<String, List<String>> registryKeys, CatalogCaptureContext context) {
|
||||
Map<String, CatalogNamespace> namespaces = new TreeMap<>();
|
||||
for (NativeModMetadata mod : context.mods()) {
|
||||
namespaces.put(mod.id(), new CatalogNamespace(mod.id(), mod.displayName(), mod.version()));
|
||||
}
|
||||
for (List<String> 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<CatalogBiome> biomes(List<WorldgenBiome> biomes) {
|
||||
List<CatalogBiome> result = new ArrayList<>(biomes.size());
|
||||
for (WorldgenBiome biome : biomes) {
|
||||
List<CatalogSpawn> 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<CatalogPlacedFeature> placedFeatures(List<WorldgenPlacedFeature> features) {
|
||||
List<CatalogPlacedFeature> 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<CatalogStructure> structures(List<WorldgenStructure> structures) {
|
||||
List<CatalogStructure> 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<CatalogDimensionType> dimensionTypes(List<WorldgenDimensionType> types) {
|
||||
List<CatalogDimensionType> 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<CatalogLevelStem> levelStems(List<WorldgenLevelStem> stems) {
|
||||
List<CatalogLevelStem> 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;
|
||||
}
|
||||
}
|
||||
+173
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<NamespaceSummary> namespaces(WorldgenCatalog catalog) {
|
||||
Map<String, Builder> 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<NamespaceSummary> summaries = new ArrayList<>(builders.size());
|
||||
for (Builder builder : builders.values()) {
|
||||
summaries.add(builder.build());
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
public static Optional<NamespaceSummary> 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<TypeCount> types, int limit) {
|
||||
List<String> 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<String> keys, String namespace, int limit) {
|
||||
List<String> 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 <T> void collect(Map<String, Builder> builders, List<T> entries, Function<T, String> key,
|
||||
Function<Builder, List<String>> target) {
|
||||
for (T entry : entries) {
|
||||
String entryKey = key.apply(entry);
|
||||
target.apply(builder(builders, WorldgenCatalogIndex.namespaceOf(entryKey))).add(entryKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static Builder builder(Map<String, Builder> 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<String> biomes,
|
||||
List<String> placedFeatures,
|
||||
List<TypeCount> featureTypes,
|
||||
List<String> structures,
|
||||
List<String> dimensionTypes,
|
||||
List<String> 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<String> biomes,
|
||||
List<String> placedFeatures,
|
||||
Map<String, Integer> featureTypes,
|
||||
List<String> structures,
|
||||
List<String> dimensionTypes,
|
||||
List<String> levelStems) {
|
||||
private Builder(CatalogNamespace namespace) {
|
||||
this(namespace, new ArrayList<>(), new ArrayList<>(), new TreeMap<>(), new ArrayList<>(), new ArrayList<>(),
|
||||
new ArrayList<>());
|
||||
}
|
||||
|
||||
private NamespaceSummary build() {
|
||||
List<TypeCount> types = new ArrayList<>(featureTypes.size());
|
||||
for (Map.Entry<String, Integer> 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<String> sorted(List<String> keys) {
|
||||
List<String> copy = new ArrayList<>(keys);
|
||||
copy.sort(null);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> namespaces, boolean replace) {
|
||||
static final List<String> 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<String, String> 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<String> namespaces(String raw) {
|
||||
if (raw == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> 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");
|
||||
}
|
||||
}
|
||||
+309
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<NativeCommandSource> 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<NamespaceCache> NAMESPACE_CACHE = new AtomicReference<>();
|
||||
private static final SuggestionProvider<NativeCommandSource> NAMESPACES = (CommandContext<NativeCommandSource> context,
|
||||
SuggestionsBuilder builder) ->
|
||||
NativeCommandRegistration.suggest(contentNamespaces(), builder);
|
||||
private static final SuggestionProvider<NativeCommandSource> INGEST_ARGUMENTS = ModdedCatalogCommands::suggestIngestArguments;
|
||||
|
||||
private ModdedCatalogCommands() {
|
||||
}
|
||||
|
||||
static LiteralArgumentBuilder<NativeCommandSource> catalogTree() {
|
||||
return NativeCommandRegistration.literal("catalog").requires(GATE)
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> list(context.getSource())))
|
||||
.then(NativeCommandRegistration.literal("refresh")
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> refresh(context.getSource()))))
|
||||
.then(NativeCommandRegistration.argument("namespace", StringArgumentType.word()).suggests(NAMESPACES)
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) ->
|
||||
detail(context.getSource(), StringArgumentType.getString(context, "namespace")))));
|
||||
}
|
||||
|
||||
static LiteralArgumentBuilder<NativeCommandSource> ingestTree() {
|
||||
return NativeCommandRegistration.literal("ingest").requires(GATE)
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> ModdedCommandHelp.send(context.getSource(), "ingest")))
|
||||
.then(NativeCommandRegistration.literal("mods")
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> ingest(context.getSource(), "")))
|
||||
.then(NativeCommandRegistration.argument("arguments", StringArgumentType.greedyString()).suggests(INGEST_ARGUMENTS)
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) ->
|
||||
ingest(context.getSource(), StringArgumentType.getString(context, "arguments"))))));
|
||||
}
|
||||
|
||||
private static int list(NativeCommandSource source) {
|
||||
Optional<WorldgenCatalog> 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<WorldgenCatalog> latest = ModdedWorldgenCatalogs.latest();
|
||||
if (latest.isEmpty()) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCatalogMessages.MISSING));
|
||||
return 0;
|
||||
}
|
||||
Optional<NamespaceSummary> 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<WorldgenCatalog> 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<String> contentNamespaces() {
|
||||
Optional<WorldgenCatalog> 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<String> 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<Suggestions> suggestIngestArguments(CommandContext<NativeCommandSource> context,
|
||||
SuggestionsBuilder builder) {
|
||||
String remaining = builder.getRemaining();
|
||||
int lastSpace = remaining.lastIndexOf(' ');
|
||||
int bareArguments = 0;
|
||||
List<String> 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<String> packs = new ArrayList<>();
|
||||
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(ModdedPackCommands.packsRoot())) {
|
||||
packs.add(pack.getName());
|
||||
}
|
||||
return NativeCommandRegistration.suggest(packs, current);
|
||||
}
|
||||
List<String> 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<String> namespaces) {
|
||||
}
|
||||
}
|
||||
+6
@@ -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", "<pack=overworld|pack=underworld|link=<zip-url>> [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", "<source> <target> [dimension=<key>] [namespaces=<a,b>] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS)
|
||||
));
|
||||
SECTIONS.put("world", List.of(
|
||||
Entry.command("enable", "<dimension> <pack|pack:dimensionKey> [seed|random]", ModdedHelpMessages.COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, "create"),
|
||||
Entry.command("replace-overworld", "<pack|pack:dimensionKey> [seed|random]", ModdedHelpMessages.COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE),
|
||||
|
||||
+3
@@ -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"));
|
||||
|
||||
|
||||
+145
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <source> <target> [dimension=<key>] "
|
||||
+ "[namespaces=<a,b>] [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<MessageKey> 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<MessageKey> keys() {
|
||||
return KEYS;
|
||||
}
|
||||
}
|
||||
+1
@@ -12,6 +12,7 @@ public final class ModdedMessageContributor implements IrisMessageContributor {
|
||||
List<MessageKey> keys = new ArrayList<>();
|
||||
keys.addAll(ModdedCommandMessages.keys());
|
||||
keys.addAll(ModdedHelpMessages.keys());
|
||||
keys.addAll(ModdedCatalogMessages.keys());
|
||||
keys.addAll(ClientUiMessages.keys());
|
||||
return List.copyOf(keys);
|
||||
}
|
||||
|
||||
+7
@@ -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<NativeModMetadata> mods() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeModdedServer currentServer() {
|
||||
return null;
|
||||
|
||||
+129
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
+117
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<NamespaceSummary> 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<NamespaceSummary> 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);
|
||||
}
|
||||
}
|
||||
+80
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
+73
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<CommandSourceStack> dispatcher = new CommandDispatcher<>();
|
||||
IrisModdedCommands.register(new NativeCommandRegistration(dispatcher));
|
||||
|
||||
CommandNode<CommandSourceStack> iris = child(dispatcher.getRoot(), "iris");
|
||||
CommandNode<CommandSourceStack> catalog = child(iris, "catalog");
|
||||
assertNotNull(catalog.getCommand());
|
||||
assertNotNull(child(catalog, "refresh").getCommand());
|
||||
assertNotNull(child(catalog, "namespace").getCommand());
|
||||
CommandNode<CommandSourceStack> ingest = child(iris, "ingest");
|
||||
assertNotNull(ingest.getCommand());
|
||||
CommandNode<CommandSourceStack> 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<String> lines = ModdedCommandHelp.consoleLines("ingest");
|
||||
assertTrue(lines.stream().anyMatch((String line) ->
|
||||
line.startsWith("/iris ingest mods <source> <target> [dimension=<key>] [namespaces=<a,b>] [replace=true] - ")));
|
||||
}
|
||||
|
||||
private static CommandNode<CommandSourceStack> child(CommandNode<CommandSourceStack> parent, String name) {
|
||||
CommandNode<CommandSourceStack> child = parent.getChild(name);
|
||||
assertNotNull(name, child);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
+129
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<PlacedFeature> 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<WorldCarver> carvers = new MappedRegistry<>(Registries.CARVER, Lifecycle.stable());
|
||||
carvers.freeze();
|
||||
MappedRegistry<Biome> biomes = new MappedRegistry<>(Registries.BIOME, Lifecycle.stable());
|
||||
MappedRegistry<Structure> structures = new MappedRegistry<>(Registries.STRUCTURE, Lifecycle.stable());
|
||||
structures.freeze();
|
||||
MappedRegistry<StructureSet> structureSets = new MappedRegistry<>(Registries.STRUCTURE_SET, Lifecycle.stable());
|
||||
structureSets.freeze();
|
||||
MappedRegistry<DimensionType> 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user