This commit is contained in:
Brian Neumann-Fopiano
2026-07-13 21:33:20 -04:00
parent 4dea984d77
commit 45230c0689
234 changed files with 13818 additions and 2979 deletions
@@ -18,8 +18,12 @@
package art.arcane.iris;
import art.arcane.iris.engine.IrisEngineEffects;
import art.arcane.iris.engine.IrisWorldManager;
import art.arcane.iris.engine.framework.EngineComponentCleanup;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
@@ -29,6 +33,7 @@ import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
@@ -57,6 +62,7 @@ import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
@@ -116,6 +122,7 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
@@ -201,27 +208,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public static KList<Object> initialize(String s, Class<? extends Annotation> slicedClass) {
private static <T> KList<T> initialize(String s, Class<T> requiredType) {
JarScanner js = new JarScanner(instance.getJarFile(), s);
KList<Object> v = new KList<>();
KList<T> v = new KList<>();
J.attempt(js::scan);
for (Class<?> i : js.getClasses()) {
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
try {
v.add(i.getDeclaredConstructor().newInstance());
} catch (Throwable ex) {
Iris.warn("Skipped class initialization for %s: %s%s",
i.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
if (!isConcreteImplementation(i, requiredType)) {
continue;
}
try {
v.add(requiredType.cast(i.getDeclaredConstructor().newInstance()));
} catch (Throwable ex) {
Iris.warn("Skipped class initialization for %s: %s%s",
i.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
return v;
}
static boolean isConcreteImplementation(Class<?> candidate, Class<?> requiredType) {
int modifiers = candidate.getModifiers();
return requiredType.isAssignableFrom(candidate)
&& !candidate.isInterface()
&& !Modifier.isAbstract(modifiers);
}
public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
JarScanner js = new JarScanner(instance.getJarFile(), s);
KList<Class<?>> v = new KList<>();
@@ -243,10 +258,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return v;
}
public static KList<Object> initialize(String s) {
return initialize(s, null);
}
public static void sq(Runnable r) {
synchronized (syncJobs) {
syncJobs.queue(r);
@@ -599,9 +610,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
services = new KMap<>();
setupAudience();
Bindings.setupSentry();
initialize("art.arcane.iris.core.service").forEach((i) -> {
services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i);
IrisServices.register(i.getClass(), i);
initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
Class<? extends IrisService> serviceType = i.getClass().asSubclass(IrisService.class);
services.put(serviceType, i);
IrisServices.register(serviceType, i);
});
IrisServices.register(BlockEditAccess.class, services.get(EditSVC.class));
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
@@ -619,6 +631,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
tickets = new ChunkTickets();
linkMultiverseCore = new MultiverseCoreLink();
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
IrisServices.register(EngineComponentCleanup.class, (EngineComponentCleanup) BukkitPlatform::unregisterListener);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) IrisEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> {
IrisWorldManager manager = new IrisWorldManager(engine);
manager.startManager();
@@ -711,7 +726,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen)
.environment(dim.getEnvironment());
.environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) {
c.seed(stagedSeed);
@@ -1067,7 +1082,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private void setupPapi() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
new IrisPapiExpansion().register();
}
}
@@ -1215,10 +1230,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
NamespacedKey worldKey = IrisWorldStorage.keyFromLegacyName(worldName);
IrisWorld w = IrisWorld.builder()
.key(worldKey)
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.environment(dim.getEnvironment())
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
@@ -2,46 +2,19 @@ package art.arcane.iris;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import io.papermc.paper.datapack.Datapack;
import io.papermc.paper.datapack.DatapackRegistrar;
import io.papermc.paper.datapack.DiscoveredDatapack;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import java.io.IOException;
import java.nio.file.Path;
@SuppressWarnings("UnstableApiUsage")
public final class IrisBootstrap implements PluginBootstrap {
static final String PACK_ID = "generated";
@Override
public void bootstrap(BootstrapContext context) {
ProvisionResult provisioned = provision(context);
Path datapackRoot = provisioned.datapackRoot();
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
context.getLifecycleManager().registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
try {
discoverPack(event.registrar(), datapackRoot);
} catch (IOException e) {
throw new IllegalStateException("Unable to discover the Iris startup datapack at " + datapackRoot, e);
}
});
}
static DiscoveredDatapack discoverPack(DatapackRegistrar registrar, Path datapackRoot) throws IOException {
DiscoveredDatapack datapack = registrar.discoverPack(
datapackRoot,
PACK_ID,
configurer -> configurer
.autoEnableOnServerStart(true)
.position(true, Datapack.Position.TOP)
);
if (datapack == null) {
throw new IllegalStateException("Paper did not accept the Iris startup datapack at " + datapackRoot);
}
return datapack;
}
private static ProvisionResult provision(BootstrapContext context) {
@@ -0,0 +1,35 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.core.commands;
final class BukkitNativeStructureLocatePolicy {
private static final String MONUMENT_KEY = "minecraft:monument";
private static final String UNAVAILABLE_MESSAGE = "Native monument locating is unavailable on Paper, Purpur, and Folia because a cold search can stall the server thread. The monument lookup was skipped; monument generation is unaffected.";
private BukkitNativeStructureLocatePolicy() {
}
static boolean isUnavailable(String structureKey) {
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
}
static String unavailableMessage() {
return UNAVAILABLE_MESSAGE;
}
}
@@ -42,6 +42,7 @@ import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
@@ -98,7 +99,7 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)")
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", sync = true)
public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
String structure
@@ -117,7 +118,12 @@ public class CommandFind implements DirectorExecutor {
}
if (IrisStructureLocator.isPlaced(e, structure)) {
EngineBukkitOps.gotoStructure(e, structure, player(), true);
locateIrisStructure(e, structure, commandSender);
return;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
return;
}
@@ -127,6 +133,8 @@ public class CommandFind implements DirectorExecutor {
return;
}
World targetWorld = target.getWorld();
Location origin = target.getLocation();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.s(() -> {
try {
@@ -140,32 +148,66 @@ public class CommandFind implements DirectorExecutor {
}
}
if (match == null) {
commandSender.sendMessage(C.RED + "Unknown structure: " + structure);
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
return;
}
if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
sendStructureMessage(target, commandSender,
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
if (result == null || result.getLocation() == null) {
commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
return;
}
Location at = result.getLocation();
int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
BukkitPlatform.teleportAsync(target, dest);
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ());
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
} catch (Throwable t) {
commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structure + "'.", t);
}
});
}
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
Player target = player();
if (target == null) {
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
return;
}
World targetWorld = target.getWorld();
Location origin = target.getLocation();
int blockX = origin.getBlockX();
int blockZ = origin.getBlockZ();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
return;
}
if (!result.found()) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
return;
}
Location destination = new Location(
targetWorld, result.originX(), result.baseY(), result.originZ());
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
} catch (Throwable t) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
}
});
}
@Director(description = "Find an object")
public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
@@ -199,4 +241,44 @@ public class CommandFind implements DirectorExecutor {
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
}
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
}
});
}
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
try {
int y = useLocatedY
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
J.runEntity(target, () -> {
BukkitPlatform.teleportAsync(target, destination);
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ "
+ at.getBlockX() + ", " + y + ", " + at.getBlockZ());
});
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
}
}
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
J.runEntity(target, () -> commandSender.sendMessage(message));
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureCaptureImporter;
@@ -41,7 +42,9 @@ import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Bukkit;
@@ -51,12 +54,12 @@ import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -111,7 +114,7 @@ public class CommandStructure implements DirectorExecutor {
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
}
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
public void verify(
@Param(description = "The dimension to verify", aliases = "dim")
IrisDimension dimension,
@@ -124,85 +127,150 @@ public class CommandStructure implements DirectorExecutor {
return;
}
boolean senderIsPlayer = sender() != null && sender().isPlayer();
Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation();
Location center = senderIsPlayer && player().getWorld() == world
? player().getLocation()
: world.getSpawnLocation();
int searchRadius = Math.max(1, Math.min(radius, 1000));
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
Engine engine = null;
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null) {
engine = access.getEngine();
Engine engine = access == null ? null : access.getEngine();
if (engine == null) {
sender().sendMessage(C.RED + "The selected Iris world has no active generator engine.");
return;
}
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
int found = 0;
int missing = 0;
int unreachable = 0;
int irisPlaced = 0;
KList<String> notFound = new KList<>();
KList<String> cannotGenerate = new KList<>();
KList<String> structureKeys = new KList<>();
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure);
String keyName = key == null ? structure.toString() : key.toString();
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
if (engine != null && !isIrisPlaced && !isReachable) {
unreachable++;
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
if (key != null) {
structureKeys.add(key.toString());
}
}
VolmitSender commandSender = sender();
Player target = senderIsPlayer ? player() : null;
commandSender.sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName()
+ C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ()
+ " within " + searchRadius + " chunks...");
int centerX = center.getBlockX();
int centerZ = center.getBlockZ();
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
}
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
int searchRadius, VolmitSender commandSender, Player target) {
KList<String> messages = new KList<>();
Set<String> reachable;
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
}
int located = 0;
int nativeEligible = 0;
int disabled = 0;
int unreachable = 0;
int unavailable = 0;
int searchLimited = 0;
int errors = 0;
for (String keyName : structureKeys) {
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
if (irisPlaced) {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
searchLimited++;
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
+ ": unable to complete the density search before its safety limit was reached");
continue;
}
if (!result.found()) {
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
continue;
}
located++;
messages.add(C.AQUA + "[iris] " + C.WHITE + keyName + C.GREEN + " @ "
+ result.originX() + "," + result.baseY() + "," + result.originZ());
} catch (Throwable error) {
errors++;
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
+ error.getClass().getSimpleName());
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
}
continue;
}
if (isIrisPlaced) {
irisPlaced++;
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
}
try {
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
if (result != null && result.getLocation() != null) {
found++;
Location l = result.getLocation();
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
} else {
missing++;
notFound.add(keyName);
}
} catch (Throwable e) {
missing++;
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
unreachable++;
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
continue;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
unavailable++;
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
continue;
}
nativeEligible++;
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
}
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
sendVerificationMessages(commandSender, target, messages);
}
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
if (!cannotGenerate.isEmpty()) {
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
}
if (!notFound.isEmpty()) {
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
private void sendVerificationMessages(VolmitSender commandSender, Player target,
KList<String> messages) {
Runnable send = () -> {
for (String message : messages) {
commandSender.sendMessage(message);
}
};
if (target != null) {
J.runEntity(target, send);
} else {
J.s(send);
}
}
private World resolveIrisWorld(IrisDimension dimension) {
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
return player().getWorld();
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
return player().getWorld();
}
}
World fallback = null;
for (World w : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(w)) {
continue;
}
if (fallback == null) {
fallback = w;
}
PlatformChunkGenerator gen = IrisToolbelt.access(w);
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
if (matchesDimension(gen, dimension.getLoadKey())) {
return w;
}
}
return fallback;
return null;
}
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
return dimensionKey != null
&& generator != null
&& generator.getEngine() != null
&& generator.getEngine().getDimension() != null
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
}
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
@@ -18,6 +18,7 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
@@ -43,6 +44,7 @@ import art.arcane.iris.engine.object.IrisNoiseGenerator;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -621,14 +623,14 @@ public class CommandStudio implements DirectorExecutor {
}
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
var player = player();
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class)
Player player = player();
IrisWorld studioWorld = Iris.service(StudioSVC.class)
.getActiveProject()
.getActiveProvider()
.getTarget()
.getWorld()
.spawnLocation()
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
.getWorld();
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
.thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
}
@Director(description = "Update your dimension projects VSCode workspace")
@@ -18,6 +18,7 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
@@ -47,7 +48,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : world.getPlayers()) {
for (Player player : BukkitWorldBinding.players(world)) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
}
return markers;
@@ -58,7 +59,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
@@ -78,11 +79,11 @@ public final class BukkitVisionOverlay implements GuiOverlay {
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasRealWorld()) {
if (!world.hasPlatformWorld()) {
return;
}
J.s(() -> {
List<Player> players = world.getPlayers();
List<Player> players = BukkitWorldBinding.players(world);
if (players.isEmpty()) {
return;
}
@@ -0,0 +1,122 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.core.runtime;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void refreshWorkspace(Engine engine) {
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
DatapackIngestService.refreshWorkspace(engine.getData());
}
@Override
public void reloadDatapacks(Engine engine) {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
}
@Override
public void fireHotloadEvent(Engine engine) {
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
}
@Override
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
IrisDimensionRuntimeContract.requireHotloadCompatible(
"Bukkit Studio world '" + engine.getWorld().name() + "'",
engine.getDimension(),
replacement,
"iris");
}
@Override
public boolean isPregeneratorActive(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
}
@Override
public void shutdownPregenerator(Engine engine) {
PregeneratorJob.shutdownInstance();
}
@Override
public boolean shouldDisableChunkContextCache(Engine engine) {
IrisWorld world = engine.getWorld();
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
return false;
}
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
}
@Override
public boolean shouldSkipMantleCleanup(Engine engine) {
IrisWorld world = engine.getWorld();
return world != null
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
&& !isPregeneratorActive(engine);
}
@Override
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
IrisWorld irisWorld = engine.getWorld();
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(irisWorld);
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
}
@Override
public boolean shouldBypassMantleStages(Engine engine) {
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(engine.getWorld());
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.core.service;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -9,6 +10,7 @@ import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
@@ -312,7 +314,7 @@ public class IrisEngineSVC implements IrisService {
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
@@ -346,7 +348,7 @@ public class IrisEngineSVC implements IrisService {
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
@@ -446,7 +448,7 @@ public class IrisEngineSVC implements IrisService {
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
@@ -8,3 +8,65 @@ load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
dependencies:
server:
PlaceholderAPI:
load: BEFORE
required: false
join-classpath: true
CraftEngine:
load: BEFORE
required: false
join-classpath: true
Nexo:
load: BEFORE
required: false
join-classpath: true
ItemsAdder:
load: BEFORE
required: false
join-classpath: true
SCore:
load: BEFORE
required: false
join-classpath: true
ExecutableItems:
load: BEFORE
required: false
join-classpath: false
MythicLib:
load: BEFORE
required: false
join-classpath: true
MMOItems:
load: BEFORE
required: false
join-classpath: true
eco:
load: BEFORE
required: false
join-classpath: true
EcoItems:
load: BEFORE
required: false
join-classpath: true
MythicMobs:
load: BEFORE
required: false
join-classpath: true
MythicCrucible:
load: BEFORE
required: false
join-classpath: true
KGenerators:
load: BEFORE
required: false
join-classpath: true
Multiverse-Core:
load: AFTER
required: false
join-classpath: true
WorldEdit:
load: BEFORE
required: false
join-classpath: true
@@ -3,6 +3,22 @@ version: ${version}
main: ${main}
folia-supported: true
load: STARTUP
softdepend:
- PlaceholderAPI
- CraftEngine
- Nexo
- ItemsAdder
- SCore
- ExecutableItems
- MythicLib
- MMOItems
- eco
- EcoItems
- MythicMobs
- MythicCrucible
- KGenerators
- WorldEdit
loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
@@ -1,199 +0,0 @@
package art.arcane.iris;
import io.papermc.paper.datapack.Datapack;
import io.papermc.paper.datapack.DatapackRegistrar;
import io.papermc.paper.datapack.DatapackSource;
import io.papermc.paper.datapack.DiscoveredDatapack;
import io.papermc.paper.plugin.configuration.PluginMeta;
import net.kyori.adventure.text.Component;
import org.bukkit.FeatureFlag;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisBootstrapTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void discoverPackAutoEnablesAtFixedTop() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.ACCEPT);
DiscoveredDatapack discovered = IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath());
assertSame(registrar.discoveredDatapack, discovered);
assertEquals(datapackDirectory.toPath(), registrar.discoveredPath);
assertEquals(IrisBootstrap.PACK_ID, registrar.discoveredId);
assertTrue(registrar.configurer.autoEnableOnServerStart);
assertTrue(registrar.configurer.fixedPosition);
assertEquals(Datapack.Position.TOP, registrar.configurer.position);
}
@Test
public void rejectedDiscoveryReportsDatapackPath() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.REJECT);
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
);
assertTrue(failure.getMessage().contains(datapackDirectory.toPath().toString()));
}
@Test
public void discoveryIoFailurePropagates() throws Exception {
File datapackDirectory = temporaryFolder.newFolder("datapack");
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.FAIL);
IOException failure = assertThrows(
IOException.class,
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
);
assertEquals("discovery failed", failure.getMessage());
}
private enum DiscoveryBehavior {
ACCEPT,
REJECT,
FAIL
}
private static final class RecordingConfigurer implements DatapackRegistrar.Configurer {
private boolean autoEnableOnServerStart;
private boolean fixedPosition;
private Datapack.Position position;
@Override
public DatapackRegistrar.Configurer title(Component title) {
return this;
}
@Override
public DatapackRegistrar.Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart) {
this.autoEnableOnServerStart = autoEnableOnServerStart;
return this;
}
@Override
public DatapackRegistrar.Configurer position(boolean fixed, Datapack.Position position) {
this.fixedPosition = fixed;
this.position = position;
return this;
}
}
private static final class RecordingRegistrar implements DatapackRegistrar {
private final DiscoveryBehavior behavior;
private final RecordingConfigurer configurer;
private final DiscoveredDatapack discoveredDatapack;
private Path discoveredPath;
private String discoveredId;
private RecordingRegistrar(DiscoveryBehavior behavior) {
this.behavior = behavior;
this.configurer = new RecordingConfigurer();
this.discoveredDatapack = new StubDiscoveredDatapack();
}
@Override
public boolean hasPackDiscovered(String name) {
return false;
}
@Override
public DiscoveredDatapack getDiscoveredPack(String name) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeDiscoveredPack(String name) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, DiscoveredDatapack> getDiscoveredPacks() {
return Collections.emptyMap();
}
@Override
public DiscoveredDatapack discoverPack(URI uri, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
@Override
public DiscoveredDatapack discoverPack(Path path, String id, Consumer<Configurer> configurer) throws IOException {
this.discoveredPath = path;
this.discoveredId = id;
if (behavior == DiscoveryBehavior.FAIL) {
throw new IOException("discovery failed");
}
configurer.accept(this.configurer);
return behavior == DiscoveryBehavior.ACCEPT ? discoveredDatapack : null;
}
@Override
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
@Override
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> configurer) {
throw new UnsupportedOperationException();
}
}
private static final class StubDiscoveredDatapack implements DiscoveredDatapack {
@Override
public String getName() {
return IrisBootstrap.PACK_ID;
}
@Override
public Component getTitle() {
return Component.text(IrisBootstrap.PACK_ID);
}
@Override
public Component getDescription() {
return Component.empty();
}
@Override
public boolean isRequired() {
return true;
}
@Override
public Datapack.Compatibility getCompatibility() {
return Datapack.Compatibility.COMPATIBLE;
}
@Override
public Set<FeatureFlag> getRequiredFeatures() {
return Collections.emptySet();
}
@Override
public DatapackSource getSource() {
throw new UnsupportedOperationException();
}
}
}
@@ -1,6 +1,8 @@
package art.arcane.iris;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import art.arcane.iris.core.service.CommandSVC;
import art.arcane.iris.util.common.plugin.IrisService;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -13,9 +15,18 @@ import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisDiagnosticsTest {
@Test
public void serviceInitializationSkipsPackageHelperClasses() throws Exception {
Class<?> registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar");
assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class));
assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class));
}
@Test
public void reportErrorWithContextPrintsFullStacktrace() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -18,6 +18,30 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest {
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
"PlaceholderAPI",
"CraftEngine",
"Nexo",
"ItemsAdder",
"SCore",
"ExecutableItems",
"MythicLib",
"MMOItems",
"eco",
"EcoItems",
"MythicMobs",
"MythicCrucible",
"KGenerators",
"Multiverse-Core",
"WorldEdit"
);
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
.toList();
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
.toList();
@Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
String metadata;
@@ -30,6 +54,11 @@ public class PaperPluginMetadataTest {
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
for (String pluginId : JOINED_PLUGIN_IDS) {
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
}
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
}
@Test
@@ -45,6 +74,8 @@ public class PaperPluginMetadataTest {
Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
}
@Test
@@ -56,4 +87,11 @@ public class PaperPluginMetadataTest {
assertTrue(Files.isRegularFile(bukkitMetadata));
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
}
private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) {
return " " + pluginId + ":\n"
+ " load: " + loadOrder + "\n"
+ " required: false\n"
+ " join-classpath: " + joinClasspath + "\n";
}
}
@@ -0,0 +1,32 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitNativeStructureLocatePolicyTest {
@Test
public void nativeMonumentLocateIsUnavailable() {
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
}
@Test
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
}
@Test
public void rejectionExplainsSafetyAndGenerationBehavior() {
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
assertTrue(message.contains("unavailable"));
assertTrue(message.contains("stall the server thread"));
assertTrue(message.contains("generation is unaffected"));
}
}
@@ -0,0 +1,34 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisStructureLocateCommandContractTest {
@Test
public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(source.contains("the density search safety limit was reached"));
assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()"));
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
assertTrue(source.contains("[iris-search-limit]"));
assertTrue(source.contains("density searches safety-limited"));
assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()"));
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
}