This commit is contained in:
Brian Neumann-Fopiano
2026-08-04 11:13:16 -06:00
parent 10b2363226
commit aaccbacf32
66 changed files with 930 additions and 557 deletions
@@ -276,7 +276,13 @@ public class IrisSettings {
public boolean useCustomColorsIngame = true;
public boolean adjustVanillaHeight = false;
public boolean autoIngestDatapacks = true;
public boolean autoImportDatapackStructures = true;
/**
* Converting every registered datapack structure into editable Iris resources writes
* thousands of objects/pools/pieces into the pack folder. Native generation and
* nativeStructures placements never need those copies, so this stays opt-in; run
* /iris structure import <dimension> when you actually want editable copies.
*/
public boolean autoImportDatapackStructures = false;
/** Unresolved pack content keys and bad block-state properties become blocking pack errors. -Diris.strictContent overrides. */
public boolean strictContentKeys = false;
public int spinh = -20;
@@ -7,19 +7,64 @@ import org.bukkit.World;
import org.bukkit.generator.WorldInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
public final class IrisWorldStorage {
private static final String IRIS_NAMESPACE = "iris";
private static final String DEFAULT_LEVEL_NAME = "world";
/**
* Server#getLevelDirectory is Paper-API-only. Once a call throws NoSuchMethodError (plain
* Spigot/CraftBukkit) this flips and every later call goes straight to the fallback.
*/
private static volatile boolean levelDirectoryUnavailable;
private static volatile String cachedFallbackLevelName;
private IrisWorldStorage() {
}
public static File levelRoot() {
return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile();
if (!levelDirectoryUnavailable) {
try {
return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize().toFile();
} catch (NoSuchMethodError e) {
levelDirectoryUnavailable = true;
}
}
return new File(Bukkit.getWorldContainer(), fallbackLevelName()).getAbsoluteFile();
}
private static String fallbackLevelName() {
String cached = cachedFallbackLevelName;
if (cached == null) {
cached = levelNameFromProperties(new File("server.properties"));
cachedFallbackLevelName = cached;
}
return cached;
}
static String levelNameFromProperties(File serverProperties) {
Properties properties = new Properties();
if (Objects.requireNonNull(serverProperties, "serverProperties").isFile()) {
try (InputStream in = new FileInputStream(serverProperties)) {
properties.load(in);
} catch (IOException ignored) {
// Unreadable server.properties: fall through to the default level name.
}
}
return levelNameFromProperties(properties);
}
static String levelNameFromProperties(Properties properties) {
String levelName = Objects.requireNonNull(properties, "properties").getProperty("level-name", DEFAULT_LEVEL_NAME).trim();
return levelName.isEmpty() ? DEFAULT_LEVEL_NAME : levelName;
}
public static File levelRoot(File dimensionRoot) {
@@ -1,6 +1,7 @@
package art.arcane.iris.core;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.IrisDimension;
@@ -159,6 +160,12 @@ public class IrisWorlds {
dimension = IrisData.loadAnyDimension(id, null);
}
if (dimension == null) {
File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME);
if (PackDownloader.isPackPresent(packsRoot, id)) {
IrisLogging.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
return null;
}
IrisLogging.warn("Unable to find dimension type " + id + " Looking for online packs...");
IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
dimension = IrisData.loadAnyDimension(id, null);
@@ -363,6 +363,7 @@ public class ServerConfigurator {
File[] packs = IrisPlatforms.get().dataFolder("packs").listFiles(File::isDirectory);
Stream<File> locals = packs == null ? Stream.empty() : Arrays.stream(packs);
return Stream.concat(locals
.filter(base -> !base.getName().contains(".importing-"))
.filter( base -> {
var content = new File(base, "dimensions").listFiles();
return content != null && content.length > 0;
@@ -0,0 +1,47 @@
package art.arcane.iris.core;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
/**
* WorldCreator.ofKey and WorldCreator#key are Paper-API-only. Once a call throws
* NoSuchMethodError (plain Spigot/CraftBukkit) this flips and every later call goes
* straight to the fallback. The fallback derives names/keys through IrisWorldStorage's
* logical mapping so keyFromName(creator.name()) round-trips on Spigot.
*/
public final class WorldCreatorCompat {
private static volatile boolean keyedCreatorsUnavailable;
private WorldCreatorCompat() {
}
public static WorldCreator ofKey(NamespacedKey worldKey) {
if (!keyedCreatorsUnavailable) {
try {
return WorldCreator.ofKey(worldKey);
} catch (NoSuchMethodError e) {
keyedCreatorsUnavailable = true;
}
}
return new WorldCreator(IrisWorldStorage.logicalName(worldKey));
}
public static NamespacedKey keyOf(WorldCreator creator) {
if (!keyedCreatorsUnavailable) {
try {
return creator.key();
} catch (NoSuchMethodError e) {
keyedCreatorsUnavailable = true;
}
}
return IrisWorldStorage.keyFromName(creator.name());
}
static String fallbackName(NamespacedKey worldKey, String levelName) {
return IrisWorldStorage.logicalName(worldKey, levelName);
}
static NamespacedKey fallbackKey(String creatorName, String levelName) {
return IrisWorldStorage.keyFromName(creatorName, levelName);
}
}
@@ -145,7 +145,7 @@ public final class DatapackIngestService {
if (report.changed()) {
message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate.");
message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import <dimension> to import everything on demand. Reference an imported key from a 'structures' placement to position it manually.");
message(sender, C.GRAY + "After the restart they generate natively - no import needed. To get editable Iris copies (jigsaw pools, pieces & objects written into the pack) run /iris structure import <dimension>, or set general.autoImportDatapackStructures=true to do it on every ingest. Place any registered key directly with a 'structures' placement using nativeStructures.");
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys by default. Set 'importedStructures.datapackOverrides' to false to keep minecraft-namespaced structure definitions untouched; deny non-minecraft datapack and mod structures explicitly with importedStructures.disabled.");
if (restart) {
ServerConfigurator.restart();
@@ -1,5 +1,6 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.WorldCreatorCompat;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldCreator;
@@ -24,7 +25,7 @@ public record WorldLifecycleRequest(
public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) {
return new WorldLifecycleRequest(
creator.name(),
creator.key(),
WorldCreatorCompat.keyOf(creator),
creator.environment(),
creator.generator(),
creator.biomeProvider(),
@@ -39,7 +40,7 @@ public record WorldLifecycleRequest(
}
public WorldCreator toWorldCreator() {
WorldCreator creator = WorldCreator.ofKey(worldKey)
WorldCreator creator = WorldCreatorCompat.ofKey(worldKey)
.environment(environment)
.generateStructures(generateStructures)
.hardcore(hardcore)
@@ -94,6 +94,10 @@ public final class PackDownloadMessages {
"iris.runtime.pack_download.acquired",
"Successfully acquired {name}."
);
public static final TextKey ALREADY_INSTALLED = TextKey.of(
"iris.runtime.pack_download.already_installed",
"Pack {key} is already installed, skipping download."
);
public static final TextKey VALIDATION_FAILED = TextKey.of(
"iris.runtime.pack_download.validation_failed",
"Pack '{pack}' failed validation; world and Studio creation will be refused. Reasons:"
@@ -136,6 +140,7 @@ public final class PackDownloadMessages {
DIMENSION_KEY_CONFLICT,
PACK_KEY_CONFLICT,
ACQUIRED,
ALREADY_INSTALLED,
VALIDATION_FAILED,
VALIDATION_REASON,
VALIDATED_WITH_WARNINGS,
@@ -32,6 +32,7 @@ import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.regex.Pattern;
@@ -42,6 +43,7 @@ public final class PackDownloader {
private static final Pattern GITHUB_REPOSITORY = Pattern.compile("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+");
private static final Pattern GITHUB_REF = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._/-]*");
private static final Pattern COMMIT_SHA = Pattern.compile("[0-9a-fA-F]{40}");
private static final ConcurrentHashMap<String, Object> DOWNLOAD_LOCKS = new ConcurrentHashMap<>();
private PackDownloader() {
}
@@ -50,11 +52,51 @@ public final class PackDownloader {
return DEFAULT_OVERWORLD_PACK.equals(pack);
}
public static String downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer<String> feedback) throws IOException {
return download(packsFolder, DEFAULT_OVERWORLD_REPOSITORY, defaultOverworldReleaseUrl(), forceOverwrite, true, feedback);
public static String defaultOverworldPack() {
return DEFAULT_OVERWORLD_PACK;
}
public static String download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
/**
* Whether a pack folder for {@code key} already exists with at least one dimension file.
* Presence is judged on disk, not on loadability: a pack that exists but fails to parse must
* surface as an error, never as a redownload. A folder without any dimensions/*.json is a
* partial import (an interrupted copy) and counts as absent so it can be replaced.
*/
public static boolean isPackPresent(File packsFolder, String key) {
if (packsFolder == null || key == null || key.isBlank()) {
return false;
}
File[] dimensions = new File(new File(packsFolder, key), "dimensions")
.listFiles((File dir, String name) -> name.endsWith(".json"));
return dimensions != null && dimensions.length > 0;
}
public static String downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer<String> feedback) throws IOException {
return download(packsFolder, DEFAULT_OVERWORLD_REPOSITORY, defaultOverworldReleaseUrl(), forceOverwrite, true, DEFAULT_OVERWORLD_PACK, feedback);
}
/**
* Downloads and imports a pack. {@code expectedKey} is the pack key the caller is trying to
* obtain (null when unknown, e.g. arbitrary repo/branch downloads); when the key is already
* present on disk and {@code forceOverwrite} is false, the network is never touched. The
* per-repo lock keeps concurrent startup triggers (async default-pack install racing world
* resolution) from downloading the same archive twice.
*/
public static String download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, String expectedKey, Consumer<String> feedback) throws IOException {
// Lock on the destination pack key when known: concurrent triggers for the same pack can
// arrive with different refs (release URL vs listing branch) and must still serialize.
String lockKey = expectedKey != null && !expectedKey.isBlank() ? "key:" + expectedKey : "ref:" + repo + "|" + ref;
Object lock = DOWNLOAD_LOCKS.computeIfAbsent(lockKey, key -> new Object());
synchronized (lock) {
if (!forceOverwrite && isPackPresent(packsFolder, expectedKey)) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey)));
return expectedKey;
}
return downloadLocked(packsFolder, repo, ref, forceOverwrite, directUrl, feedback);
}
}
private static String downloadLocked(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
String url = directUrl ? ref : resolveGithubArchiveUrl(repo, ref);
feedback.accept(IrisLanguage.plain(PackDownloadMessages.DOWNLOADING, MessageArgument.untrusted("url", url)) + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = WebCache.getNonCachedFile("pack-" + repo, url);
@@ -122,6 +164,12 @@ public final class PackDownloader {
String key = d.getLoadKey();
feedback.accept(IrisLanguage.plain(PackDownloadMessages.IMPORTING, MessageArgument.untrusted("name", d.getName()), MessageArgument.untrusted("key", key)));
File packEntry = new File(packsFolder, key);
File[] staleStaging = packsFolder.listFiles((File parent, String name) -> name.startsWith(key + ".importing-"));
if (staleStaging != null) {
for (File stale : staleStaging) {
IO.delete(stale);
}
}
if (forceOverwrite) {
IO.delete(packEntry);
@@ -134,11 +182,29 @@ public final class PackDownloader {
File[] existingEntries = packEntry.listFiles();
if (packEntry.exists() && existingEntries != null && existingEntries.length > 0) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.PACK_KEY_CONFLICT, MessageArgument.untrusted("key", key)));
return null;
if (isPackPresent(packsFolder, key)) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.PACK_KEY_CONFLICT, MessageArgument.untrusted("key", key)));
return null;
}
// Non-empty but no dimension file: a partial import from an interrupted copy.
// Replace it instead of refusing forever.
IrisLogging.warn("Replacing partial pack folder " + packEntry.getPath() + " (no dimension files found).");
IO.delete(packEntry);
}
FileUtils.copyDirectory(dir, packEntry);
// Stage inside the packs folder and move into place so packs/<key> is never partial:
// an interrupted copy previously left a folder without dimensions/, which then blocked
// every future import as a key conflict.
File staging = new File(packsFolder, key + ".importing-" + UUID.randomUUID());
try {
FileUtils.copyDirectory(dir, staging);
if (!staging.renameTo(packEntry)) {
throw new IOException("Unable to move imported pack into place: " + packEntry.getPath());
}
} catch (IOException | RuntimeException e) {
IO.delete(staging);
throw e;
}
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
@@ -72,6 +72,8 @@ public class SchemaBuilder {
private static final String SYMBOL_LIMIT__N = "*";
private static final String SYMBOL_TYPE__N = "";
private static final String MINECRAFT_NAMESPACE = "minecraft:";
/** Namespaced key or family/namespace prefix: "minecraft:village_plains", "minecraft:village", "nova_structures:". */
private static final String VANILLA_STRUCTURE_PREFIX_PATTERN = "^[a-z0-9_.-]+:[a-z0-9_./-]*$";
private static volatile JSONArray fontTypes;
private final KMap<String, JSONObject> definitions;
private final Class<?> root;
@@ -249,6 +251,46 @@ public class SchemaBuilder {
}
}
/**
* A registry enum that ALSO accepts family/namespace prefixes ("minecraft:village",
* "nova_structures:") — the runtime prefix-matching contract of importedStructures.disabled and
* adjustments[].match. Emitted as anyOf(enum, pattern) so autocomplete still offers registered
* keys while prefix entries validate instead of being rejected.
*/
private void putRegistryEnumOrPrefixRef(JSONObject target, String definitionKey,
String enumDefinitionKey, Supplier<JSONArray> values,
String pattern) {
if (!definitions.containsKey(definitionKey)) {
JSONArray anyOf = new JSONArray();
JSONObject enumRef = new JSONObject();
try {
putRegistryEnumRef(enumRef, enumDefinitionKey, values);
} catch (RuntimeException e) {
IrisLogging.debug("Schema enum '" + enumDefinitionKey + "' unavailable ("
+ e.getMessage() + "); emitting prefix pattern only");
}
if (enumRef.has("$ref")) {
anyOf.put(enumRef);
}
JSONObject prefix = new JSONObject();
prefix.put("type", "string");
prefix.put("pattern", pattern);
anyOf.put(prefix);
JSONObject definition = new JSONObject();
definition.put("anyOf", anyOf);
definitions.put(definitionKey, definition);
}
target.put("$ref", "#/definitions/" + definitionKey);
}
private void putRegistryEnumOrPrefixItems(JSONObject prop, String definitionKey,
String enumDefinitionKey, Supplier<JSONArray> values,
String pattern) {
JSONObject items = new JSONObject();
putRegistryEnumOrPrefixRef(items, definitionKey, enumDefinitionKey, values, pattern);
prop.put("items", items);
}
private JSONArray itemTypes() {
JSONArray a = new JSONArray();
for (String key : IrisPlatforms.get().registries().itemKeys()) {
@@ -427,8 +469,14 @@ public class SchemaBuilder {
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
fancyType = "Vanilla Structure";
putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
putRegistryEnumOrPrefixRef(prop, "enum-vanilla-structure-or-prefix",
"enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN);
description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
} else {
putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
}
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
fancyType = "Vanilla Structure Set";
@@ -627,8 +675,14 @@ public class SchemaBuilder {
description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
fancyType = "List<Vanilla Structure>";
putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
putRegistryEnumOrPrefixItems(prop, "enum-vanilla-structure-or-prefix",
"enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN);
description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
} else {
putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures);
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
}
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
fancyType = "List<Vanilla Structure Set>";
putRegistryEnumItems(prop, "enum-vanilla-structure-set", this::vanillaStructureSets);
@@ -26,7 +26,6 @@ import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.IrisPack;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
@@ -63,6 +62,7 @@ import java.util.function.Consumer;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
public class StudioSVC implements IrisService {
public static final String LISTING = "https://raw.githubusercontent.com/IrisDimensions/_listing/main/listing-v2.json";
@@ -76,9 +76,10 @@ public class StudioSVC implements IrisService {
public void onEnable() {
J.a(() -> {
String pack = IrisSettings.get().getGenerator().getDefaultWorldType();
File f = IrisPack.packsPack(pack);
if (!f.exists()) {
// Presence means a non-empty pack folder: an empty leftover folder must still
// trigger the install instead of shadowing it forever.
if (!PackDownloader.isPackPresent(getWorkspaceFolder(), pack)) {
if (PackDownloader.isDefaultOverworld(pack)) {
IrisLogging.info("Downloading Default Pack " + pack + " (beta release)");
IrisServices.get(StudioSVC.class).downloadDefaultOverworld(BukkitPlatform.console(), false);
@@ -203,8 +204,8 @@ public class StudioSVC implements IrisService {
}
}
}
IO.delete(downloaded);
// The downloaded pack stays in the packs workspace: deleting it here made the
// next startup see a missing pack and download it again.
}
}
@@ -231,6 +232,14 @@ public class StudioSVC implements IrisService {
}
public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) {
// The default overworld always comes from the pinned release
// (PackDownloader.DEFAULT_OVERWORLD_RELEASE_URL), never from the listing,
// so every code path ships the same pack build.
if (PackDownloader.isDefaultOverworld(key)) {
downloadDefaultOverworld(sender, forceOverwrite);
return;
}
try {
String url = getListing(false).get(key);
@@ -244,7 +253,8 @@ public class StudioSVC implements IrisService {
String[] nodes = url.split("\\Q/\\E");
String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1];
String branch = nodes.length > 2 ? nodes[2] : "stable";
download(sender, repo, branch, forceOverwrite, false);
String expectedKey = key.contains("/") ? null : key;
download(sender, repo, branch, forceOverwrite, false, expectedKey);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
@@ -253,6 +263,13 @@ public class StudioSVC implements IrisService {
}
public void downloadDefaultOverworld(VolmitSender sender, boolean forceOverwrite) {
// Same guard as download(): a present pack must not reach installDataPacks(true),
// which can trigger an automatic restart.
if (!forceOverwrite && PackDownloader.isPackPresent(getWorkspaceFolder(), PackDownloader.defaultOverworldPack())) {
sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", PackDownloader.defaultOverworldPack())));
return;
}
try {
String key = PackDownloader.downloadDefaultOverworld(getWorkspaceFolder(), forceOverwrite, sender::sendMessage);
if (key != null) {
@@ -280,7 +297,18 @@ public class StudioSVC implements IrisService {
}
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, sender::sendMessage);
download(sender, repo, branch, forceOverwrite, directUrl, null);
}
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl, String expectedKey) throws JsonSyntaxException, IOException {
// Skip before PackDownloader so an already-present pack never reaches
// installDataPacks(true), which can trigger an automatic restart.
if (!forceOverwrite && PackDownloader.isPackPresent(getWorkspaceFolder(), expectedKey)) {
sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey)));
return;
}
String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, expectedKey, sender::sendMessage);
if (key == null) {
return;
@@ -22,12 +22,12 @@ public final class IrisSplashComposer {
prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + "]"),
prefix + style.label(" Version: ") + style.value(version),
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
prefix + style.label(" Web: ") + style.value("VolmitSoftware.com"),
prefix + style.label(" Server: ") + style.value(serverLine),
prefix + style.label(" Java: ") + style.value(String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()),
prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT),
"",
"",
"",
""
};
}
@@ -19,6 +19,7 @@
package art.arcane.iris.core.structure;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
@@ -301,7 +302,7 @@ public final class FeatureImporter {
if (existing != null) {
return existing;
}
WorldCreator creator = WorldCreator.ofKey(IrisWorldStorage.keyFromName(SCRATCH_WORLD_NAME))
WorldCreator creator = WorldCreatorCompat.ofKey(IrisWorldStorage.keyFromName(SCRATCH_WORLD_NAME))
.environment(World.Environment.NORMAL)
.type(WorldType.FLAT)
.generateStructures(false);
@@ -41,6 +41,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
@@ -49,9 +50,11 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class VillageImporter {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Set<String> PRINTED_FAILURE_SIGNATURES = ConcurrentHashMap.newKeySet();
public record Result(boolean success, String message, int pools, int pieces, List<StructureLoss> losses) {
public Result {
@@ -686,25 +689,62 @@ public final class VillageImporter {
return opt;
}
private static int readIntMember(Object value, String memberName) throws Exception {
static int readIntMember(Object value, String memberName) throws Exception {
Class<?> type = value.getClass();
while (type != null) {
try {
Field field = type.getDeclaredField(memberName);
field.setAccessible(true);
return field.getInt(value);
return coerceInt(field.get(value), memberName, value);
} catch (NoSuchFieldException ignored) {
type = type.getSuperclass();
}
}
Method method = findMethod(value.getClass(), memberName);
if (method != null && Number.class.isAssignableFrom(boxedType(method.getReturnType()))) {
if (method != null) {
method.setAccessible(true);
return ((Number) method.invoke(value)).intValue();
return coerceInt(method.invoke(value), memberName, value);
}
throw new NoSuchFieldException(memberName + " on " + value.getClass().getName());
}
/**
* Members that are plain numbers on one server build are wrapper objects on another
* (JigsawStructure.maxDistanceFromCenter became a MaxDistance{horizontal, vertical} record).
* Numbers pass through; a wrapper contributes its horizontal component, else its largest
* integral component, so a distance bound is never under-read.
*/
private static int coerceInt(Object member, String memberName, Object owner) throws Exception {
if (member instanceof Number n) {
return n.intValue();
}
if (member == null) {
throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName() + " is null");
}
Method horizontal = findMethod(member.getClass(), "horizontal");
if (horizontal != null && Number.class.isAssignableFrom(boxedType(horizontal.getReturnType()))) {
horizontal.setAccessible(true);
return ((Number) horizontal.invoke(member)).intValue();
}
Integer widest = null;
for (Field component : member.getClass().getDeclaredFields()) {
if (Modifier.isStatic(component.getModifiers())
|| !Number.class.isAssignableFrom(boxedType(component.getType()))) {
continue;
}
component.setAccessible(true);
Object componentValue = component.get(member);
if (componentValue instanceof Number n && (widest == null || n.intValue() > widest)) {
widest = n.intValue();
}
}
if (widest != null) {
return widest;
}
throw new NoSuchFieldException(memberName + " on " + owner.getClass().getName()
+ " is a " + member.getClass().getName() + " with no integral component");
}
private static Class<?> boxedType(Class<?> type) {
return type == int.class ? Integer.class : type;
}
@@ -828,7 +868,28 @@ public final class VillageImporter {
private static void reportFailure(Throwable failure) {
IrisLogging.reportError(failure);
failure.printStackTrace();
if (shouldPrintFullTrace(failure)) {
failure.printStackTrace();
}
}
/**
* True the first time a failure signature is seen. A bulk import repeats the same failure once
* per registered structure, so printing every trace buries the boot log in hundreds of copies
* of one problem; the per-structure "[fail] key: message" line still reports each occurrence.
*/
static boolean shouldPrintFullTrace(Throwable failure) {
if (failure == null) {
return false;
}
StackTraceElement[] trace = failure.getStackTrace();
String signature = failure.getClass().getName() + '|' + failure.getMessage()
+ '|' + (trace.length == 0 ? "" : trace[0].toString());
return PRINTED_FAILURE_SIGNATURES.add(signature);
}
static void resetFailureLogState() {
PRINTED_FAILURE_SIGNATURES.clear();
}
private static void reportWriteFailure(StructureWriteResult result) {
@@ -27,6 +27,7 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
@@ -204,7 +205,7 @@ public class IrisCreator {
.studio(studio)
.create();
if (!studio()) {
IrisWorlds.get().put(wc.key().toString(), dimension());
IrisWorlds.get().put(WorldCreatorCompat.keyOf(wc).toString(), dimension());
}
ServerConfigurator.installDataPacksIfChanged(!studio());
IrisLogging.debug("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)");
@@ -19,6 +19,7 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
@@ -92,7 +93,7 @@ public class IrisWorldCreator {
new File(w.worldFolder(), "iris/pack"), dimensionName);
return WorldCreator.ofKey(worldKey)
return WorldCreatorCompat.ofKey(worldKey)
.environment(environment)
.generateStructures(true)
.generator(g).seed(seed);
@@ -39,6 +39,13 @@ public final class NativeStructureGenerationPolicy {
"Dimension importedStructures must not be null");
IrisNativeStructureDecision decision = control.resolve(structureKey, undergroundStep);
if (!decision.generate()) {
// A disabled key with an active Iris placement is the "blanket-disable, re-place
// explicitly" pattern: the placement planner ignores the disable list, so generation
// places it — report REPLACED_BY_IRIS so find/goto/verify locate the placement.
if (decision.status() == NativeStructureGenerationStatus.DISABLED_BY_PACK
&& IrisStructureLocator.isPlaced(activeEngine, structureKey)) {
return decision.withStatus(NativeStructureGenerationStatus.REPLACED_BY_IRIS);
}
return decision;
}
if (IrisStructureLocator.suppressesVanilla(activeEngine, structureKey)) {
@@ -39,7 +39,7 @@ import java.util.Objects;
@Data
public class IrisImportedStructureControl {
@ArrayType(type = String.class, min = 1)
@RegistryListVanillaStructure
@RegistryListVanillaStructure(prefixes = true)
@Desc("Structure keys to deny explicitly, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' disables every village variant and 'minecraft:ruined_portal' disables every ruined portal. Every key not matched here remains enabled.")
private KList<String> disabled = new KList<>();
@@ -36,7 +36,7 @@ import lombok.experimental.Accessors;
@Data
public class IrisVanillaStructureAdjustment {
@ArrayType(type = String.class, min = 1)
@RegistryListVanillaStructure
@RegistryListVanillaStructure(prefixes = true)
@Desc("Structure keys this adjustment applies to, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' adjusts every village variant and 'minecraft:ruined_portal' adjusts every ruined portal. Empty matches nothing.")
private KList<String> match = new KList<>();
@@ -29,5 +29,12 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target({PARAMETER, TYPE, FIELD})
public @interface RegistryListVanillaStructure {
/**
* When true the field accepts family/namespace prefixes in addition to exact registered keys
* (the {@code IrisImportedStructureControl.matchesKey} contract: "minecraft:village" matches
* every village variant, "nova_structures:" matches a whole namespace). The generated editor
* schema then validates entries against the registry enum OR a key/prefix pattern instead of
* rejecting anything that is not an exact registered key.
*/
boolean prefixes() default false;
}
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Eine andere Dimension im Pack-Ordner verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!",
"iris.runtime.pack_download.pack_key_conflict": "Ein anderer Pack verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!",
"iris.runtime.pack_download.acquired": "{name} erfolgreich abgerufen.",
"iris.runtime.pack_download.already_installed": "Pack {key} ist bereits installiert, Download wird übersprungen.",
"iris.runtime.pack_download.validation_failed": "Pack '{pack}' hat die Validierung nicht bestanden; Welt- und Studio-Erstellung werden verweigert. Gründe:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Otra dimensión de la carpeta packs ya usa la clave {key}. ¡La importación falló!",
"iris.runtime.pack_download.pack_key_conflict": "Otro pack usa la clave {key}. ¡La importación falló!",
"iris.runtime.pack_download.acquired": "{name} se obtuvo correctamente.",
"iris.runtime.pack_download.already_installed": "El pack {key} ya está instalado, se omite la descarga.",
"iris.runtime.pack_download.validation_failed": "El pack '{pack}' no superó la validación; se rechazará la creación de mundos y de Studio. Motivos:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Toinen ulottuvuus pakkauskansiossa jo käyttää avainta {key}. Tuonti epäonnistui!",
"iris.runtime.pack_download.pack_key_conflict": "Toinen pakkaus käyttää avainta {key}. Tuonti epäonnistui!",
"iris.runtime.pack_download.acquired": "Onnistunut hankinta {name}.",
"iris.runtime.pack_download.already_installed": "Pack {key} on jo asennettu, lataus ohitetaan.",
"iris.runtime.pack_download.validation_failed": "Pakkaus{pack}' Epäonnistunut validointi; maailma ja Studio luominen hylätään. Perusteet:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Une autre dimension du dossier packs utilise déjà la clé {key}. Échec de l'importation !",
"iris.runtime.pack_download.pack_key_conflict": "Un autre pack utilise la clé {key}. Échec de l'importation !",
"iris.runtime.pack_download.acquired": "{name} obtenu avec succès.",
"iris.runtime.pack_download.already_installed": "Le pack {key} est déjà installé, téléchargement ignoré.",
"iris.runtime.pack_download.validation_failed": "Le pack '{pack}' a échoué à la validation ; la création de mondes et de Studio sera refusée. Raisons :",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "מימד נוסף בתיקיה של הלהקה כבר משתמש המפתח {key}. ייבוא נכשל!",
"iris.runtime.pack_download.pack_key_conflict": "חבילה נוספת משתמשת במפתח {key}. ייבוא נכשל!",
"iris.runtime.pack_download.acquired": "נרכשה בהצלחה {name}.",
"iris.runtime.pack_download.already_installed": "החבילה {key} כבר מותקנת, ההורדה מדולגת.",
"iris.runtime.pack_download.validation_failed": "Pack »{pack}\"התאימות הכושל; יצירת העולם והסטודיו לא תסרב. סיבות:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Un'altra dimensione nella cartella dei Pack usa già la chiave {key}. Importazione non riuscita!",
"iris.runtime.pack_download.pack_key_conflict": "Un altro Pack usa già la chiave {key}. Importazione non riuscita!",
"iris.runtime.pack_download.acquired": "{name} acquisito correttamente.",
"iris.runtime.pack_download.already_installed": "Il pack {key} è già installato, download saltato.",
"iris.runtime.pack_download.validation_failed": "Il Pack '{pack}' non ha superato la convalida; la creazione di mondi e Studio verrà rifiutata. Motivi:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "packs フォルダー内の別のディメンションがキー {key} をすでに使用しています。インポートに失敗しました!",
"iris.runtime.pack_download.pack_key_conflict": "別のパックがキー {key} を使用しています。インポートに失敗しました!",
"iris.runtime.pack_download.acquired": "{name} を取得しました。",
"iris.runtime.pack_download.already_installed": "パック {key} は既にインストールされているため、ダウンロードをスキップします。",
"iris.runtime.pack_download.validation_failed": "パック '{pack}' は検証に失敗しました。ワールドと Studio の作成を拒否します。理由:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "packs 폴더의 다른 차원이 이미 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!",
"iris.runtime.pack_download.pack_key_conflict": "다른 팩이 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!",
"iris.runtime.pack_download.acquired": "성공적으로 취득 {name}.",
"iris.runtime.pack_download.already_installed": "팩 {key}이(가) 이미 설치되어 있어 다운로드를 건너뜁니다.",
"iris.runtime.pack_download.validation_failed": "팩 '{pack}' 유효성 검사; 세계 및 스튜디오 생성은 거부됩니다. 이유:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Kitas matmuo paketų aplanke jau naudoja raktą {key}. Importuoti nepavyko!",
"iris.runtime.pack_download.pack_key_conflict": "Kita pakuotė naudoja raktą {key}. Importuoti nepavyko!",
"iris.runtime.pack_download.acquired": "Sėkmingai įgyta {name}.",
"iris.runtime.pack_download.already_installed": "Paketas {key} jau įdiegtas, atsisiuntimas praleidžiamas.",
"iris.runtime.pack_download.validation_failed": "Pakuotė \"{pack}\"nepavyko patvirtinimas; pasaulio ir Studio kūrimas bus atsisakyta. Motyvai:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Een andere dimensie in de packs-map gebruikt al de sleutel {key}. Importeren mislukt!",
"iris.runtime.pack_download.pack_key_conflict": "Een ander pakje gebruikt de sleutel {key}. Importeren mislukt!",
"iris.runtime.pack_download.acquired": "Succesvol verworven {name}.",
"iris.runtime.pack_download.already_installed": "Pack {key} is al geïnstalleerd, download wordt overgeslagen.",
"iris.runtime.pack_download.validation_failed": "Verpakking{pack}' mislukte validatie; wereld en Studio creatie zal worden geweigerd. Motivering:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Inny wymiar w folderze pakietów już używa klucza {key}. Import nie powiódł się!",
"iris.runtime.pack_download.pack_key_conflict": "Inny pakiet używa klucza {key}. Import nie powiódł się!",
"iris.runtime.pack_download.acquired": "Udane nabycie {name}.",
"iris.runtime.pack_download.already_installed": "Pakiet {key} jest już zainstalowany, pomijanie pobierania.",
"iris.runtime.pack_download.validation_failed": "Paczka \"{pack}\"nieudaną walidację; świat i tworzenie studia zostaną odrzucone. Uzasadnienie:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Outra dimensão na pasta pacotes já está usando a chave {key}. A importação falhou!",
"iris.runtime.pack_download.pack_key_conflict": "Outro pacote está usando a chave {key}. A importação falhou!",
"iris.runtime.pack_download.acquired": "Adquirido com sucesso {name}.",
"iris.runtime.pack_download.already_installed": "O pack {key} já está instalado, download ignorado.",
"iris.runtime.pack_download.validation_failed": "Embalar '{pack}' validação falhada; a criação de mundo e estúdio será recusada. Motivos:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Еще одно измерение в папке пакетов уже использует ключ {key}. Импорт провалился!",
"iris.runtime.pack_download.pack_key_conflict": "Другой пакет использует ключ. {key}. Импорт провалился!",
"iris.runtime.pack_download.acquired": "Успешно приобретенный {name}.",
"iris.runtime.pack_download.already_installed": "Пак {key} уже установлен, загрузка пропущена.",
"iris.runtime.pack_download.validation_failed": "Пакуй.{pack}Неудачная проверка; мир и создание студии будут отклонены. Причины:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Paketlerdeki başka bir boyut zaten anahtarı kullanıyor {key}. İthalat başarısız oldu!",
"iris.runtime.pack_download.pack_key_conflict": "Başka bir paket anahtarı kullanıyor {key}. İthalat başarısız oldu!",
"iris.runtime.pack_download.acquired": "Başarılı bir şekilde satın alındı {name}.",
"iris.runtime.pack_download.already_installed": "{key} paketi zaten kurulu, indirme atlanıyor.",
"iris.runtime.pack_download.validation_failed": "Pack \"{pack}“Başarısız doğrulama; dünya ve Stüdyo yaratımı reddedilecektir. Sebepler:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "Một chiều không gian khác trong thư mục gói là đã sử dụng phím {key}. Nhập thất bại!",
"iris.runtime.pack_download.pack_key_conflict": "Name {key}. Nhập thất bại!",
"iris.runtime.pack_download.acquired": "Được thành công {name}.",
"iris.runtime.pack_download.already_installed": "Gói {key} đã được cài đặt, bỏ qua tải xuống.",
"iris.runtime.pack_download.validation_failed": "Gói '{pack}'Đã thất bại trong việc xác nhận; thế giới và phòng thu sẽ bị từ chối. Lý do:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "包文件夹中的另一个维度已经使用密钥 {key}. 导入失败 !",
"iris.runtime.pack_download.pack_key_conflict": "另一个包是用钥匙 {key}. 导入失败 !",
"iris.runtime.pack_download.acquired": "已成功获取 {name}.",
"iris.runtime.pack_download.already_installed": "包 {key} 已安装,跳过下载。",
"iris.runtime.pack_download.validation_failed": "包{pack}' 验证失败; 世界和工作室的创建将被拒绝. 原因:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -1295,6 +1295,7 @@
"iris.runtime.pack_download.dimension_key_conflict": "包資料夾中的另一個維度已經使用金鑰 {key}. 匯入失敗 !",
"iris.runtime.pack_download.pack_key_conflict": "另一個包是用鑰匙 {key}. 匯入失敗 !",
"iris.runtime.pack_download.acquired": "已成功獲取 {name}.",
"iris.runtime.pack_download.already_installed": "套件 {key} 已安裝,跳過下載。",
"iris.runtime.pack_download.validation_failed": "包{pack}' 驗證失敗; 世界和工作室的建立將被拒絕. 原因:",
"iris.runtime.pack_download.validation_reason": " - {reason}",
"iris.runtime.pack_download.validated_with_warnings": {
@@ -6,6 +6,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
@@ -49,6 +52,37 @@ public class IrisWorldStorageTest {
assertEquals("iris_world", IrisWorldStorage.logicalName(new NamespacedKey("iris", "iris_world"), "world"));
}
@Test
public void readsLevelNameFromServerPropertiesFile() throws Exception {
File serverProperties = temporaryFolder.newFile("server.properties");
Files.write(serverProperties.toPath(), "level-name=psycho_world\nmotd=A Minecraft Server\n".getBytes(StandardCharsets.UTF_8));
assertEquals("psycho_world", IrisWorldStorage.levelNameFromProperties(serverProperties));
}
@Test
public void defaultsLevelNameWhenServerPropertiesMissing() {
File missing = new File(temporaryFolder.getRoot(), "missing/server.properties");
assertEquals("world", IrisWorldStorage.levelNameFromProperties(missing));
}
@Test
public void defaultsLevelNameWhenPropertyAbsentOrBlank() {
assertEquals("world", IrisWorldStorage.levelNameFromProperties(new Properties()));
Properties blank = new Properties();
blank.setProperty("level-name", " ");
assertEquals("world", IrisWorldStorage.levelNameFromProperties(blank));
}
@Test
public void trimsLevelNameFromProperties() {
Properties padded = new Properties();
padded.setProperty("level-name", " main_level ");
assertEquals("main_level", IrisWorldStorage.levelNameFromProperties(padded));
}
@Test
public void rejectsKeysThatEscapeNamespaceStorage() throws Exception {
File levelRoot = temporaryFolder.newFolder("world");
@@ -0,0 +1,40 @@
package art.arcane.iris.core;
import org.bukkit.NamespacedKey;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class WorldCreatorCompatTest {
@Test
public void keyedPathPreservesWorldKey() {
NamespacedKey key = new NamespacedKey("iris", "compat_world");
assertEquals(key, WorldCreatorCompat.ofKey(key).key());
}
@Test
public void fallbackNameDerivesLogicalNameFromKey() {
assertEquals("compat_world", WorldCreatorCompat.fallbackName(new NamespacedKey("iris", "compat_world"), "world"));
assertEquals("world", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("overworld"), "world"));
assertEquals("world_nether", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("the_nether"), "world"));
assertEquals("world_the_end", WorldCreatorCompat.fallbackName(NamespacedKey.minecraft("the_end"), "world"));
}
@Test
public void fallbackKeyRoundTripsCreatorName() {
assertEquals(new NamespacedKey("iris", "compat_world"), WorldCreatorCompat.fallbackKey("compat_world", "world"));
assertEquals(NamespacedKey.minecraft("overworld"), WorldCreatorCompat.fallbackKey("world", "world"));
assertEquals(NamespacedKey.minecraft("the_nether"), WorldCreatorCompat.fallbackKey("world_nether", "world"));
}
@Test
public void fallbackMappingIsStableAcrossRoundTrips() {
NamespacedKey key = new NamespacedKey("iris", "iris_world");
String name = WorldCreatorCompat.fallbackName(key, "world");
assertEquals(key, WorldCreatorCompat.fallbackKey(name, "world"));
assertEquals(name, WorldCreatorCompat.fallbackName(key, "world"));
}
}
@@ -18,7 +18,15 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -26,6 +34,8 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class PackDownloaderTest {
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void resolvesDefaultOverworldBetaRelease() {
assertEquals(
@@ -74,6 +84,55 @@ public class PackDownloaderTest {
);
}
@Test
public void isPackPresentRequiresNonEmptyFolder() throws IOException {
File packsFolder = temp.newFolder("packs");
assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld"));
assertFalse(PackDownloader.isPackPresent(packsFolder, null));
assertFalse(PackDownloader.isPackPresent(packsFolder, ""));
assertFalse(PackDownloader.isPackPresent(null, "overworld"));
File pack = new File(packsFolder, "overworld");
assertTrue(pack.mkdirs());
assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld"));
// A partial import (content but no dimension file) counts as absent so it can be replaced.
File biomes = new File(pack, "biomes");
assertTrue(biomes.mkdirs());
Files.writeString(new File(biomes, "plains.json").toPath(), "{}");
assertFalse(PackDownloader.isPackPresent(packsFolder, "overworld"));
File dimensions = new File(pack, "dimensions");
assertTrue(dimensions.mkdirs());
Files.writeString(new File(dimensions, "overworld.json").toPath(), "{}");
assertTrue(PackDownloader.isPackPresent(packsFolder, "overworld"));
}
@Test
public void downloadSkipsWhenExpectedPackAlreadyPresent() throws IOException {
File packsFolder = temp.newFolder("packs");
File dimensions = new File(packsFolder, "overworld/dimensions");
assertTrue(dimensions.mkdirs());
Files.writeString(new File(dimensions, "overworld.json").toPath(), "{}");
List<String> feedback = new ArrayList<>();
// The URL is unreachable on purpose: reaching the network would fail the download and
// return null, so a non-null key proves the presence check ran before any fetch.
String key = PackDownloader.download(
packsFolder,
"IrisDimensions/overworld",
"http://127.0.0.1:9/unreachable.zip",
false,
true,
"overworld",
feedback::add
);
assertEquals("overworld", key);
assertFalse(feedback.isEmpty());
}
@Test
public void rejectsUnsafeRepositoryAndReference() {
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld?raw=1", "master"));
@@ -0,0 +1,68 @@
package art.arcane.iris.core.project;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisVanillaStructureAdjustment;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import java.util.regex.Pattern;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* importedStructures.disabled and adjustments[].match accept family/namespace PREFIXES
* ("minecraft:village", "nova_structures:") per IrisImportedStructureControl.matchesKey, so the
* generated editor schema must not reject them with a strict registered-key enum. Prefix-capable
* fields emit an anyOf of the registry enum plus a key/prefix pattern; exact-key fields (e.g.
* nativeStructures[].structure) keep the strict enum.
*/
public class VanillaStructurePrefixSchemaTest {
@Test
public void prefixCapableFieldsDeclareThePrefixAnnotation() throws NoSuchFieldException {
assertTrue(IrisImportedStructureControl.class.getDeclaredField("disabled")
.getAnnotation(RegistryListVanillaStructure.class).prefixes());
assertTrue(IrisVanillaStructureAdjustment.class.getDeclaredField("match")
.getAnnotation(RegistryListVanillaStructure.class).prefixes());
}
@Test
public void prefixListSchemaAcceptsFamilyAndNamespacePrefixes() {
JSONObject schema = new SchemaBuilder(PrefixModel.class, null).construct();
JSONObject items = schema.getJSONObject("properties").getJSONObject("disabled")
.getJSONObject("items");
String definitionKey = items.getString("$ref").substring("#/definitions/".length());
JSONArray anyOf = schema.getJSONObject("definitions").getJSONObject(definitionKey)
.getJSONArray("anyOf");
String pattern = null;
for (int i = 0; i < anyOf.length(); i++) {
JSONObject branch = anyOf.getJSONObject(i);
if (branch.has("pattern")) {
pattern = branch.getString("pattern");
}
}
assertTrue("anyOf must contain a pattern branch for prefixes", pattern != null);
Pattern compiled = Pattern.compile(pattern);
assertTrue(compiled.matcher("minecraft:village").matches());
assertTrue(compiled.matcher("minecraft:pillager_outpost").matches());
assertTrue(compiled.matcher("nova_structures:").matches());
assertTrue(compiled.matcher("towns_and_towers:exclusives/village_piglin").matches());
assertFalse(compiled.matcher("village").matches());
assertFalse(compiled.matcher("Nova Structures:tavern").matches());
}
@Desc("Schema model for prefix-capable vanilla structure lists.")
public static class PrefixModel {
@RegistryListVanillaStructure(prefixes = true)
@ArrayType(type = String.class, min = 1)
@Desc("Prefix-capable deny list.")
private KList<String> disabled = new KList<>();
}
}
@@ -14,4 +14,14 @@ public class IrisSplashComposerTest {
assertEquals(" Version: 4.0.0-26.2", info[2]);
assertFalse(String.join("\n", info).contains("RC.1.1.6"));
}
@Test
public void composeInfoShowsWebsiteAndKeepsSplashHeight() {
String[] info = IrisSplashComposer.composeInfo("4.0.0-26.2", "Paper 26.2", IrisSplashComposer.InfoStyle.PLAIN);
assertEquals(11, info.length);
assertEquals(" By: Volmit Software (Arcane Arts)", info[3]);
assertEquals(" Web: VolmitSoftware.com", info[4]);
assertEquals(" Server: Paper 26.2", info[5]);
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.core.structure;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* A bulk import walks every registered datapack structure, so one broken server-side assumption
* fails hundreds of times in a row from the same throw site. Printing a full stack trace per
* structure buried a Leaf 26.2 boot under ~4,700 identical lines. The first occurrence of a failure
* signature prints in full; repeats are counted by the per-structure "[fail] key: message" line
* instead of re-printing.
*/
public class VillageImporterFailureLogTest {
/** One throw site, mirroring a repeated in-loop failure (same class, message, and top frame). */
private static Throwable raise(boolean illegalArgument, String message) {
return illegalArgument ? new IllegalArgumentException(message) : new IllegalStateException(message);
}
@Test
public void repeatedIdenticalFailuresPrintOnce() {
VillageImporter.resetFailureLogState();
assertTrue(VillageImporter.shouldPrintFullTrace(
raise(true, "illegal data type conversion to int")));
for (int i = 0; i < 200; i++) {
assertFalse(VillageImporter.shouldPrintFullTrace(
raise(true, "illegal data type conversion to int")));
}
}
@Test
public void distinctFailuresEachPrintOnce() {
VillageImporter.resetFailureLogState();
assertTrue(VillageImporter.shouldPrintFullTrace(raise(true, "a")));
assertTrue(VillageImporter.shouldPrintFullTrace(raise(false, "a")));
assertTrue(VillageImporter.shouldPrintFullTrace(raise(true, "b")));
assertFalse(VillageImporter.shouldPrintFullTrace(raise(true, "b")));
}
@Test
public void nullFailureIsNotPrinted() {
VillageImporter.resetFailureLogState();
assertFalse(VillageImporter.shouldPrintFullTrace(null));
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.core.structure;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* JigsawStructure.maxDistanceFromCenter is a plain int on some server builds and a
* JigsawStructure$MaxDistance record ({int horizontal, int vertical}) on others (e.g. Leaf
* 26.2-33). The reflective member reader must handle both the wrapper shape previously threw
* IllegalArgumentException from Field.getInt, failing the jigsaw import of every datapack
* structure ("Failed to read jigsaw structure graph").
*/
public class VillageImporterMaxDistanceReadTest {
private static final class IntShape {
private final int maxDistanceFromCenter = 80;
}
private record MaxDistance(int horizontal, int vertical) {
}
private static final class WrapperShape {
private final MaxDistance maxDistanceFromCenter = new MaxDistance(96, 48);
}
private static final class BoxedShape {
private final Integer maxDistanceFromCenter = 64;
}
@Test
public void readsPlainIntField() throws Exception {
assertEquals(80, VillageImporter.readIntMember(new IntShape(), "maxDistanceFromCenter"));
}
@Test
public void readsLargestIntComponentFromWrapperRecord() throws Exception {
assertEquals(96, VillageImporter.readIntMember(new WrapperShape(), "maxDistanceFromCenter"));
}
@Test
public void readsBoxedNumberField() throws Exception {
assertEquals(64, VillageImporter.readIntMember(new BoxedShape(), "maxDistanceFromCenter"));
}
}
@@ -1,9 +1,20 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructureGenerationPolicyTest {
@Test
@@ -17,4 +28,53 @@ public class NativeStructureGenerationPolicyTest {
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS));
}
/**
* The "blanket-disable a namespace, re-place chosen structures via nativeStructures placements"
* pattern: generation honors the placement (the planner never consults the disable list), so the
* policy must report those keys as REPLACED_BY_IRIS making /iris find|goto and
* /iris structure verify locate the placement instead of claiming the key is disabled.
*/
@Test
public void disabledKeyWithActivePlacementResolvesAsReplacedByIris() {
Engine engine = engineWithDisabledNamespaceAndRegionPlacement("nova_structures:tavern_oak");
assertEquals(NativeStructureGenerationStatus.REPLACED_BY_IRIS,
NativeStructureGenerationPolicy.resolve(engine, "nova_structures:tavern_oak", false).status());
}
@Test
public void disabledKeyWithoutPlacementStaysDisabled() {
Engine engine = engineWithDisabledNamespaceAndRegionPlacement("nova_structures:tavern_oak");
IrisNativeStructureDecision decision =
NativeStructureGenerationPolicy.resolve(engine, "nova_structures:witch_villa", false);
assertEquals(NativeStructureGenerationStatus.DISABLED_BY_PACK, decision.status());
assertFalse(decision.generate());
}
private Engine engineWithDisabledNamespaceAndRegionPlacement(String placedKey) {
IrisData data = mock(IrisData.class);
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisImportedStructureControl control = new IrisImportedStructureControl();
control.getDisabled().add("nova_structures:");
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getNativeStructures().add(new IrisNativeStructure().setStructure(placedKey));
IrisRegion region = mock(IrisRegion.class);
KList<IrisStructurePlacement> regionPlacements = new KList<>();
regionPlacements.add(placement);
when(region.getStructures()).thenReturn(regionPlacements);
KList<IrisRegion> regions = new KList<>();
regions.add(region);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getImportedStructures()).thenReturn(control);
when(dimension.getStructures()).thenReturn(new KList<>());
when(dimension.getAllRegions(engine)).thenReturn(regions);
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
return engine;
}
}