Full audit fixpass: 93 defect fixes, 14 perf wins, vestigial cleanup

This commit is contained in:
Brian Neumann-Fopiano
2026-08-13 01:01:01 -04:00
parent 13e02ecd2f
commit 14b6280668
215 changed files with 4604 additions and 2407 deletions
@@ -55,6 +55,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.core.BlockPos;
@@ -172,6 +173,7 @@ public class NMSBinding implements INMSBinding {
private final BlockData AIR = Material.AIR.createBlockData();
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer serverLevelTransformer;
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
@@ -1523,7 +1525,7 @@ public class NMSBinding implements INMSBinding {
}
try {
IrisLogging.info("Injecting Bukkit");
new AgentBuilder.Default()
serverLevelTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
@@ -1546,6 +1548,17 @@ public class NMSBinding implements INMSBinding {
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
// The ServerLevel transformer may already be installed when the ChunkAccess
// redefine throws; remove it or a retry would stack a second one and orphan
// this one permanently.
ResettableClassFileTransformer partial = serverLevelTransformer;
serverLevelTransformer = null;
if (partial != null) {
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable ignored) {
}
}
return false;
}
}
@@ -1562,6 +1575,24 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer transformer = serverLevelTransformer;
serverLevelTransformer = null;
injected.set(false);
if (transformer == null) {
return;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to remove ServerLevel injection");
e.printStackTrace();
}
}
}
@Override
public KMap<Material, List<BlockProperty>> getBlockProperties() {
KMap<Material, List<BlockProperty>> states = new KMap<>();
@@ -1624,24 +1655,56 @@ public class NMSBinding implements INMSBinding {
if (dimensionKey == null)
return;
// This advice is inlined into every ServerLevel construction on the server. Until a
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
// Iris being unloaded or half-loaded must never break other plugins' world creation.
String levelId;
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
String levelId = dimensionKey.identifier().getPath();
levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
return;
}
// From here Iris is present and its classes resolve; a failure resolving the
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader)
generator = stagingType
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
return;
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
gen = owned;
}
} catch (Throwable e) {
throw new RuntimeException("Iris failed to resolve the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (gen == null) {
return;
}
// Past the ownership gate the world is Iris-owned; silently handing back the
// vanilla stem would corrupt generation, so failures from here rethrow.
try {
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
+3
View File
@@ -14,6 +14,9 @@ dependencies {
transitive = false
}
compileOnly(libs.paper.api)
// LogFilterSVC (constructed in Iris.enable's explicit service list) implements the
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
compileOnly(libs.log4j.core)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api)
@@ -51,9 +51,27 @@ import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.CommandSVC;
import art.arcane.iris.core.service.DatapackStructureScopeSVC;
import art.arcane.iris.core.service.EditSVC;
import art.arcane.iris.core.service.EntityRiseSVC;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.IrisIntegrationService;
import art.arcane.iris.core.service.IrisProtocolService;
import art.arcane.iris.core.service.IrisTerrainSVC;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.core.service.LogFilterSVC;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.core.service.PreservationSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.TreeFellerSVC;
import art.arcane.iris.core.service.TreeSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.engine.framework.BlockEditAccess;
@@ -80,7 +98,6 @@ import art.arcane.volmlib.util.hud.HudBossBarLane;
import art.arcane.volmlib.util.hud.HudSlotService;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.io.InstanceState;
import art.arcane.volmlib.util.io.JarScanner;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.misc.Bindings;
@@ -98,7 +115,6 @@ import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -115,9 +131,9 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -165,6 +181,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private KMap<Class<? extends IrisService>, IrisService> services;
// Copy-on-write: mutated on the main thread during enable() and iterated by the JVM
// shutdown-hook thread during teardown; a plain list would CME and abort the teardown.
private final List<IrisService> enabledServices = new CopyOnWriteArrayList<>();
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
@@ -206,28 +225,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
private static <T> KList<T> initialize(String s, Class<T> requiredType) {
JarScanner js = new JarScanner(instance.getJarFile(), s);
KList<T> v = new KList<>();
J.attempt(js::scan);
for (Class<?> i : js.getClasses()) {
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)
@@ -235,27 +232,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
&& !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<>();
J.attempt(js::scan);
for (Class<?> i : js.getClasses()) {
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
try {
v.add(i);
} catch (Throwable ex) {
Iris.warn("Skipped class discovery entry for %s: %s%s",
i.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
}
return v;
}
public static void sq(Runnable r) {
synchronized (syncJobs) {
syncJobs.queue(r);
@@ -538,7 +514,21 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
private void enable() {
/**
* @return false when the bootstrap was aborted (unsupported server version); the caller
* must bail out of onEnable without touching any further setup.
*/
private boolean enable() {
if (!INMS.isBound()) {
Throwable bindFailure = INMS.bindFailure();
Iris.error("Iris cannot start: " + (bindFailure == null
? "no NMS binding is available for this server version."
: bindFailure.getMessage()));
// Deferred one tick: disablePlugin from inside onEnable re-enters onDisable
// synchronously and the loader then continues registering the half-enabled plugin.
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
return false;
}
alreadyDrained.set(false);
servicesDisabled.set(false);
sharedRuntimeClosed.set(false);
@@ -551,25 +541,50 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
setupAudience();
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane());
Bindings.setupSentry();
initialize("art.arcane.iris.core.service", IrisService.class).forEach((i) -> {
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
// enable/disable and paid a full-jar class sweep at boot. Infrastructure first,
// engine/world services next, commands last.
List<IrisService> orderedServices = List.of(
new PreservationSVC(),
new GlobalCacheSVC(),
new LogFilterSVC(),
new ExternalDataSVC(),
new EditSVC(),
new ObjectSVC(),
new ObjectStudioSaveService(),
new JigsawStudioService(),
new StudioSVC(),
new DatapackStructureScopeSVC(),
new IrisEngineSVC(),
new IrisTerrainSVC(),
new TreeSVC(),
new TreeFellerSVC(),
new EntityRiseSVC(),
new WandSVC(),
new BoardSVC(),
new IrisIntegrationService(),
new IrisProtocolService(),
new IrisApiEventSVC(),
new CommandSVC()
);
for (IrisService i : orderedServices) {
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));
IO.delete(new File("iris"));
compat = IrisCompat.configured(getDataFile("compat.json"));
IrisServices.register(IrisCompat.class, compat);
ServerConfigurator.configure();
IrisToolbelt.applyPregenPerformanceProfile();
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
if (datapackValidation == StartupValidationOutcome.READY) {
generatorResolver.validateAllPacks();
}
IrisSafeguard.execute();
getSender().setTag(getTag());
splash();
// A cosmetic banner must never abort the bootstrap.
J.attempt(this::splash);
IrisSafeguard.printReports();
IrisSafeguard.printFooter();
tickets = new ChunkTickets();
@@ -594,20 +609,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
// packs through cache/temp on an async thread, and a concurrent delete of that folder
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
IO.delete(getTemp());
services.values().forEach(IrisService::onEnable);
services.values().forEach(this::registerListener);
// One throwing service must not abort the bootstrap: the steps after this loop
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
// Only services that actually enabled get listeners and a later onDisable.
enabledServices.clear();
IrisService firstFailedService = null;
for (IrisService service : orderedServices) {
try {
service.onEnable();
enabledServices.add(service);
} catch (Throwable e) {
if (firstFailedService == null) {
firstFailedService = service;
}
Iris.reportError("Failed to enable " + service.getClass().getSimpleName() + "; continuing with a degraded runtime.", e);
// A failed service is excluded from the teardown loop, so clean up whatever
// its partial onEnable started right here, best-effort.
try {
service.onDisable();
} catch (Throwable cleanup) {
Iris.reportError("Failed to clean up partially enabled " + service.getClass().getSimpleName() + ".", cleanup);
}
}
}
if (firstFailedService != null) {
IrisStartupValidation.markDatapacksInvalid("Iris service "
+ firstFailedService.getClass().getSimpleName()
+ " failed to enable; world creation and player admission are locked. Check the log above.");
}
for (IrisService service : enabledServices) {
try {
registerListener(service);
} catch (Throwable e) {
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
}
}
addShutdownHook();
pendingWorldReplacements.processPendingStartupReplacements();
pendingWorldDeletes.processPendingStartupWorldDeletes();
WorldLifecycleService.get();
WorldRuntimeControlService.get();
if (J.isFolia() && IrisStartupValidation.isReady()) {
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
}
J.s(() -> {
pendingWorldReplacements.verifyLoadedPublishedWorlds();
pendingWorldReplacements.captureVanillaLevelContext();
// Off-main: the verify body takes the replacement-manager monitor and SHA-hashes
// whole pack trees; neither belongs on the tick thread.
J.a(pendingWorldReplacements::verifyLoadedPublishedWorlds);
J.a(this::bstats);
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
J.sr(this::tickQueue, 0);
@@ -620,20 +669,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
worldReconciler.checkForBukkitWorlds(s -> true);
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
// The mantle stores block values as PlatformBlockState, so a BlockData retention can never
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
});
return true;
}
public void addShutdownHook() {
if (shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (IllegalStateException ex) {
Iris.debug("Skipping shutdown hook replacement because JVM shutdown is already in progress.");
return;
}
}
removeShutdownHook();
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -642,6 +686,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
/**
* The static-field guard in addShutdownHook is dead across a plugin reload (fresh
* classloader, fresh static), so onDisable must deregister the hook explicitly or each
* reload stacks another hook pinning the previous plugin classloader for the JVM's life.
*/
public void removeShutdownHook() {
Thread hook = shutdownHook;
shutdownHook = null;
if (hook == null) {
return;
}
try {
Runtime.getRuntime().removeShutdownHook(hook);
} catch (IllegalStateException ignored) {
Iris.debug("Skipping shutdown hook removal because JVM shutdown is already in progress.");
}
}
public BukkitWorldReconciler worldReconciler() {
return worldReconciler;
}
@@ -692,16 +754,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisStartupValidation.begin();
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
enable();
boolean enabled = enable();
if (!enabled) {
return;
}
BukkitGuiHost.install();
// super.onEnable() already registers this instance as a listener.
super.onEnable();
Bukkit.getPluginManager().registerEvents(this, this);
}
public void onDisable() {
teardownPapi();
if (IrisSafeguard.isForceShutdown()) return;
teardownRuntime("onDisable", 30L);
removeShutdownHook();
J.attempt(() -> INMS.get().uninjectBukkit());
if (BukkitPlatform.hasHud()) {
BukkitPlatform.hudSlots().shutdown();
BukkitPlatform.hudLanes().shutdown();
@@ -710,12 +776,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
configHotloadEngine.clear();
configHotloadEngine = null;
}
J.cancelPluginTasks();
HandlerList.unregisterAll((Plugin) this);
runPostShutdown();
// super.onDisable() cancels plugin tasks and unregisters every listener.
super.onDisable();
J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll);
IrisPlatforms.unbind();
}
@@ -753,7 +816,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
for (IrisService service : services.values()) {
// Only services whose onEnable actually completed; disabling a service that
// never initialized runs teardown against uninitialized state.
for (IrisService service : enabledServices) {
try {
service.onDisable();
} catch (Throwable e) {
@@ -768,6 +833,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
clearQueues();
IrisServices.clear();
}
}
@@ -169,7 +169,7 @@ public final class IrisWorldGeneratorResolver {
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
File packsRoot = IrisPlatforms.get().dataFolderNoCreate(StudioSVC.WORKSPACE_NAME);
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
if (PackDownloader.isPackPresent(packsRoot, id)) {
Iris.error("Pack '" + id + "' exists at " + new File(packsRoot, id).getPath()
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
@@ -254,7 +254,10 @@ public final class PendingWorldReplacementManager implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldLoad(WorldLoadEvent event) {
World world = event.getWorld();
J.s(() -> verifyPublishedWorldIfPending(world), 1);
// One tick to let the load settle, then verify off-main: the body takes the manager
// monitor (held across pack staging by async threads) and SHA-hashes the whole pack
// tree — blocking the main thread on either froze the server.
J.s(() -> J.a(() -> verifyPublishedWorldIfPending(world)), 1);
}
private synchronized void verifyPublishedWorldIfPending(World world) {
@@ -506,10 +509,46 @@ public final class PendingWorldReplacementManager implements Listener {
}
}
/**
* Captures the primary level context on the main thread at startup. resolveEffectiveSeed
* reads this snapshot so staging never blocks on a main-thread hop while holding the
* manager monitor — that inversion froze the server for the full 30s hop timeout whenever
* another world loaded during staging. The context is stable for the process lifetime:
* slot replacements only commit across a restart.
*/
public void captureVanillaLevelContext() {
try {
vanillaLevelContext = new VanillaLevelContext(
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
.getSeed(),
Iris.instance.getServer().getAllowNether(),
Iris.instance.getServer().getAllowEnd()
);
} catch (Throwable failure) {
Iris.debug("Could not capture the primary level context yet: " + detail(failure));
}
}
private static long resolveEffectiveSeed(SlotKind slotKind, long requestedSeed) throws IOException {
if (slotKind == SlotKind.IRIS_MANAGED) {
return requestedSeed;
}
VanillaLevelContext context = vanillaLevelContext;
if (context == null) {
context = resolveVanillaLevelContext();
vanillaLevelContext = context;
}
if (slotKind == SlotKind.VANILLA_NETHER && !context.allowNether()) {
throw new IOException("allow-nether must be true before the vanilla Nether can be replaced.");
}
if (slotKind == SlotKind.VANILLA_END && !context.allowEnd()) {
throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced.");
}
return context.seed();
}
private static VanillaLevelContext resolveVanillaLevelContext() throws IOException {
CompletableFuture<VanillaLevelContext> contextFuture = J.sfut(() -> new VanillaLevelContext(
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
@@ -521,14 +560,7 @@ public final class PendingWorldReplacementManager implements Listener {
throw new IOException("Could not schedule primary level-seed resolution.");
}
try {
VanillaLevelContext context = contextFuture.get(30L, TimeUnit.SECONDS);
if (slotKind == SlotKind.VANILLA_NETHER && !context.allowNether()) {
throw new IOException("allow-nether must be true before the vanilla Nether can be replaced.");
}
if (slotKind == SlotKind.VANILLA_END && !context.allowEnd()) {
throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced.");
}
return context.seed();
return contextFuture.get(30L, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Primary level-seed resolution was interrupted.", failure);
@@ -580,6 +612,8 @@ public final class PendingWorldReplacementManager implements Listener {
}
}
private static volatile VanillaLevelContext vanillaLevelContext;
private record VanillaLevelContext(long seed, boolean allowNether, boolean allowEnd) {
}
@@ -394,7 +394,7 @@ public class CommandDeveloper implements DirectorExecutor {
}
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
public void goldenhash(
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true)
World world,
@@ -404,7 +404,7 @@ public class CommandDeveloper implements DirectorExecutor {
int centerX,
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
int centerZ,
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
@Param(name = "reset-mantle", description = "Delete the world's entire mantle folder first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
boolean resetMantle,
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8")
int threads,
@@ -705,7 +705,7 @@ public class CommandIris implements DirectorExecutor {
public void download(
@Param(name = "pack", description = "The pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", descriptionKey = "iris.director.commandiris.param.branch_download_from", defaultValue = "stable")
@Param(name = "branch", description = "The branch to download from", descriptionKey = "iris.director.commandiris.param.branch_download_from", defaultValue = PackDownloader.DEFAULT_BRANCH)
String branch,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", descriptionKey = "iris.director.commandiris.param.whether_not_overwrite_pack_with_downloaded_one", aliases = "force", defaultValue = "false")
boolean overwrite
@@ -803,7 +803,7 @@ public class CommandIris implements DirectorExecutor {
});
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
.whenComplete((unloaded, throwable) -> {
IrisToolbelt.endWorldMaintenance(world, "world-unload");
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
lease.close();
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
@@ -812,7 +812,7 @@ public class CommandIris implements DirectorExecutor {
J.s(response);
});
} catch (Exception e) {
IrisToolbelt.endWorldMaintenance(world, "world-unload");
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
lease.close();
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
@@ -37,7 +37,6 @@ import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.StudioMode;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.PlatformBlockState;
@@ -144,7 +143,8 @@ public class CommandObject implements DirectorExecutor {
return;
}
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
// ObjectStudioActivation carries the buffet state; mutating the shared cached
// IrisDimension here leaked OBJECT_BUFFET into later pack exports of this dimension.
ObjectStudioActivation.activate(hostDimension.getLoadKey());
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
@@ -637,6 +637,7 @@ public class CommandObject implements DirectorExecutor {
} catch (IOException e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
Iris.reportError(e);
return;
}
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
@@ -42,9 +42,18 @@ import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
public class CommandPack implements DirectorExecutor {
/**
* Director treats a blank defaultValue as "required", which made the all-packs branch
* unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank
* value (the documented "pack=" escape hatch) still means every pack.
*/
static boolean wantsAllPacks(String pack) {
return pack == null || pack.isBlank() || "*".equals(pack.trim());
}
@Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"})
public void validate(
@Param(description = "The pack folder name to validate (leave empty for all)", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "")
@Param(description = "The pack folder name to validate, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "*")
String pack
) {
VolmitSender s = sender();
@@ -54,7 +63,7 @@ public class CommandPack implements DirectorExecutor {
return;
}
if (pack == null || pack.isBlank()) {
if (wantsAllPacks(pack)) {
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
if (dirs.isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE));
@@ -196,11 +205,11 @@ public class CommandPack implements DirectorExecutor {
@Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"})
public void status(
@Param(description = "The pack folder name", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "")
@Param(description = "The pack folder name, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "*")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
if (wantsAllPacks(pack)) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
return;
@@ -33,6 +33,7 @@ import org.bukkit.util.Vector;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
@@ -59,6 +60,12 @@ public class CommandPregen implements DirectorExecutor {
return;
}
if (PregeneratorJob.getInstance() != null) {
// Reject like the modded adapter does; never silently kill a running job.
sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
@@ -328,35 +328,50 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
return;
}
if (radius <= 0 || radius > 2048) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
sender().sendMessage("Radius must be between 1 and 2048 chunks.");
return;
}
var sender = sender();
var player = player();
Thread.ofVirtual()
.start(() -> {
int d = radius * 2;
KMap<String, AtomicInteger> data = new KMap<>();
// Everything acquired below is released in the finally: a throw mid-scan
// previously leaked the whole sampler ForkJoinPool, the self-rescheduling
// progress task and both HUD claims for the rest of the server's uptime.
MultiBurst multiBurst = null;
HudSlotClaim titleClaim = null;
HudSlotClaim barClaim = null;
int c = -1;
try {
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
var multiBurst = new MultiBurst("Region Sampler");
var executor = multiBurst.burst(radius * radius);
multiBurst = new MultiBurst("Region Sampler");
var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20));
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
var loc = player.getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
HudSlotClaim titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
HudSlotClaim barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)));
titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE)));
barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)));
HudSlotClaim finalTitleClaim = titleClaim;
HudSlotClaim finalBarClaim = barClaim;
AtomicLong lastResolveMs = new AtomicLong(0L);
int c = J.ar(() -> {
c = J.ar(() -> {
long now = System.currentTimeMillis();
if (now - lastResolveMs.get() >= 250L) {
lastResolveMs.set(now);
titleClaim.resolve();
barClaim.resolve();
finalTitleClaim.resolve();
finalBarClaim.resolve();
}
double jobProgress = (double) completedTasks.get() / totalTasks;
HudSurface barSurface = barClaim.granted();
HudSurface barSurface = finalBarClaim.granted();
sender.sendProgress(
jobProgress,
IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS),
titleClaim.granted(),
finalTitleClaim.granted(),
barSurface
);
if (barSurface == HudSurface.BOSS_BAR) {
@@ -372,15 +387,28 @@ public class CommandStudio implements DirectorExecutor {
completedTasks.incrementAndGet();
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
executor.complete();
multiBurst.close();
J.car(c);
titleClaim.release();
barClaim.release();
BukkitPlatform.hudLanes().hide(player, "iris:job");
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE));
var loader = engine.getData().getRegionLoader();
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
} catch (Throwable e) {
Iris.reportError(e);
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
} finally {
if (c != -1) {
J.car(c);
}
if (titleClaim != null) {
titleClaim.release();
}
if (barClaim != null) {
barClaim.release();
BukkitPlatform.hudLanes().hide(player, "iris:job");
}
if (multiBurst != null) {
multiBurst.close();
}
}
});
}
@@ -437,7 +465,6 @@ public class CommandStudio implements DirectorExecutor {
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
KMap<String, Double> generatorTimings = new KMap<>();
KMap<String, Double> biomeTimings = new KMap<>();
KMap<String, Double> regionTimings = new KMap<>();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS));
@@ -561,23 +588,6 @@ public class CommandStudio implements DirectorExecutor {
fileText.add("");
for (String i : data.getRegionLoader().getPossibleKeys()) {
IrisRegion b = data.getRegionLoader().load(i);
double score = 0;
score += styleTimings.get(b.getLakeStyle().getStyle());
score += styleTimings.get(b.getRiverStyle().getStyle());
regionTimings.put(i, score);
}
fileText.add("Project Region Performance Impacts: ");
for (String i : regionTimings.sortKNumber()) {
fileText.add(i + ": " + regionTimings.get(i));
}
fileText.add("");
double m = 0;
for (double i : biomeTimings.v()) {
m += i;
@@ -589,12 +599,6 @@ public class CommandStudio implements DirectorExecutor {
}
mm /= generatorTimings.size();
m += mm;
double mmm = 0;
for (double i : regionTimings.v()) {
mmm += i;
}
mmm /= regionTimings.size();
m += mmm;
fileText.add("Average Score: " + m);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0))));
@@ -763,9 +767,8 @@ public class CommandStudio implements DirectorExecutor {
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
try {
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
PrintWriter pw = new PrintWriter(ff);
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
try (PrintWriter pw = new PrintWriter(ff)) {
pw.println("=== Iris Chunk Report ===");
pw.println("== General Info ==");
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
@@ -820,7 +823,8 @@ public class CommandStudio implements DirectorExecutor {
}
}
regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length;
String[] regionFiles = new File(world.getWorldFolder(), "region").list();
regions = regionFiles == null ? 0 : regionFiles.length;
pw.println();
pw.println("== World Info ==");
@@ -854,10 +858,9 @@ public class CommandStudio implements DirectorExecutor {
}
pw.println();
pw.close();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
} catch (FileNotFoundException e) {
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
@@ -26,7 +26,10 @@ import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
@@ -36,8 +39,10 @@ import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.matter.MatterMarker;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
@@ -47,7 +52,6 @@ import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
@@ -172,35 +176,73 @@ public class CommandWhat implements DirectorExecutor {
});
}
// Matches ModdedWhatCommands.MAX_MARKERS so both loaders truncate identically.
private static final int MAX_MARKER_HITS = 8_192;
private static final int MARKER_SIGNAL_BATCH = 128;
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
VolmitSender commandSender = sender();
Player player = player();
if (player == null) {
return;
}
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
// Only the chunk coordinates need the owning thread; the 81-chunk mantle sweep loads
// tectonic plates from disk and must never run on the tick thread (the modded adapter
// already scans async, leased and capped — mirror it).
onPlayerThread(player, () -> {
World world = player.getWorld();
Chunk c = player.getLocation().getChunk();
int chunkX = c.getX();
int chunkZ = c.getZ();
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
AtomicInteger v = new AtomicInteger(0);
if (!IrisToolbelt.isIrisWorld(world)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
J.a(() -> {
KList<Location> hits = new KList<>();
MatterMarker matterMarker = new MatterMarker(marker);
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
scan:
for (int xxx = chunkX - 4; xxx <= chunkX + 4; xxx++) {
for (int zzz = chunkZ - 4; zzz <= chunkZ + 4; zzz++) {
for (Location i : engine.getMantle().findMarkers(xxx, zzz, matterMarker)
.convert((p) -> BukkitPlatform.toLocation(p, world))) {
hits.add(i);
if (hits.size() >= MAX_MARKER_HITS) {
break scan;
}
}
}
}
} catch (GenerationSessionException e) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
return;
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
} else {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker)));
emitMarkerSignals(player, engine, hits, 0);
});
});
}
private void emitMarkerSignals(Player player, Engine engine, KList<Location> hits, int from) {
if (from >= hits.size() || !player.isOnline() || engine.isClosed() || engine.isClosing()) {
return;
}
int to = Math.min(from + MARKER_SIGNAL_BATCH, hits.size());
for (int i = from; i < to; i++) {
Location at = hits.get(i);
BlockSignal.of(at.getWorld(), at.getBlockX(), at.getBlockY(), at.getBlockZ(), 100);
}
J.s(() -> emitMarkerSignals(player, engine, hits, to), 1);
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
@@ -1,5 +1,6 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
@@ -29,13 +30,26 @@ public final class DatapackStructureScopeSVC implements IrisService {
scopeIndex = DatapackStructureScopeIndex.create(
DatapackIngestService.installedStructureScopeResources());
} catch (IOException e) {
throw new IllegalStateException(
"Iris could not establish ownership for installed datapack structure sets", e);
// A missing manifest entry is the condition validateOnStartup already classified as
// non-fatal; degrade to an empty scope and lock world creation instead of aborting
// the whole plugin bootstrap (which would skip listeners and journal reconciliation).
scopeIndex = DatapackStructureScopeIndex.create(null);
IrisLogging.error("Could not establish ownership for installed datapack structure sets; "
+ "datapack structure scoping is disabled until the manifest is repaired: " + e.getMessage());
IrisStartupValidation.markDatapacksInvalid(
"Iris could not establish ownership for installed datapack structure sets: " + e.getMessage());
}
for (World world : Bukkit.getWorlds()) {
IrisImportedStructureControl importedStructures = importedStructures(world);
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
applyScope(world, importedStructures);
try {
IrisImportedStructureControl importedStructures = importedStructures(world);
if (!scopeIndex.isEmpty() || importedStructures != null && importedStructures.hasFrequencyOverrides()) {
applyScope(world, importedStructures);
}
} catch (Throwable e) {
// Per-world containment: one world's scoping failure must not abort the
// service enable (and with it the whole bootstrap's containment contract).
IrisLogging.error("Could not scope datapack structures for world '" + world.getName() + "'.");
IrisLogging.reportError(e);
}
}
}
@@ -35,7 +35,6 @@ import org.bukkit.event.world.WorldUnloadEvent;
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
private KMap<World, BlockEditor> editors;
private int updateTaskId = -1;
public static boolean deletingWorld = false;
@Override
public void onEnable() {
@@ -84,7 +83,7 @@ public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, B
if (editors == null) {
return;
}
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
if (editors.containsKey(e.getWorld())) {
editors.remove(e.getWorld()).close();
}
}
@@ -22,7 +22,7 @@ public class IrisStartupOrderingTest {
@Test
public void externalDatapacksValidateBeforeDimensionPacks() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String enable = section(source, "private void enable()", "public void addShutdownHook()");
String enable = section(source, "private boolean enable()", "public void addShutdownHook()");
assertOrdered(enable,
"DatapackIngestService.validateOnStartup();",
@@ -0,0 +1,20 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class CommandIrisDownloadDefaultTest {
@Test
public void downloadBranchParamDefaultsToTheCoreConstant() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/commands/CommandIris.java"));
int branchParam = source.indexOf("name = \"branch\"");
assertTrue("CommandIris must declare the branch param", branchParam >= 0);
String declaration = source.substring(branchParam, source.indexOf(')', branchParam));
assertTrue("branch param default must be PackDownloader.DEFAULT_BRANCH, not a literal",
declaration.contains("defaultValue = PackDownloader.DEFAULT_BRANCH"));
}
}
@@ -0,0 +1,63 @@
package art.arcane.iris.core.commands;
import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility;
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CommandPackOptionalPackContractTest {
private static DirectorParameterDescriptor packParam(String method) throws Exception {
DirectorNodeDescriptor node = DirectorAnnotationCompatibility
.fromMethod(CommandPack.class.getDeclaredMethod(method, String.class))
.orElseThrow();
DirectorParameterDescriptor pack = node.getParameters().get(0);
assertEquals("pack", pack.getName());
return pack;
}
@Test
public void bareValidateAndStatusAreInvocableWithoutAPackArgument() throws Exception {
for (String method : List.of("validate", "status")) {
DirectorParameterDescriptor pack = packParam(method);
assertFalse(method + " must not require a pack", pack.isRequired());
assertFalse(method + " needs a non-blank default for Director to treat it as optional",
pack.getDefaultValue().isBlank());
}
}
@Test
public void cleanupAndRestoreStillRequireAPack() throws Exception {
for (String method : List.of("cleanup", "restore")) {
DirectorNodeDescriptor node = DirectorAnnotationCompatibility
.fromMethod(findMethod(method))
.orElseThrow();
DirectorParameterDescriptor pack = node.getParameters().get(0);
assertEquals("pack", pack.getName());
assertTrue(method + " must keep requiring a pack", pack.isRequired());
}
}
@Test
public void allPacksPredicateAcceptsSentinelBlankAndNull() {
assertTrue(CommandPack.wantsAllPacks(null));
assertTrue(CommandPack.wantsAllPacks(""));
assertTrue(CommandPack.wantsAllPacks(" "));
assertTrue(CommandPack.wantsAllPacks("*"));
assertFalse(CommandPack.wantsAllPacks("overworld"));
}
private static java.lang.reflect.Method findMethod(String name) {
for (java.lang.reflect.Method method : CommandPack.class.getDeclaredMethods()) {
if (method.getName().equals(name)) {
return method;
}
}
throw new AssertionError("CommandPack must declare " + name);
}
}
@@ -46,7 +46,10 @@ public final class IrisClientPregenState {
jobs.remove(jobId);
Long current = activeJobId;
if (current != null && current == jobId) {
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
// Promote nothing: the server runs a single live job, so any remaining entry is
// dead or orphaned and the access-ordered map's head is the STALEST entry,
// which is what the HUD used to promote.
activeJobId = null;
}
}
@@ -161,7 +161,9 @@ public final class NativeStructureVegetationClearer {
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
return true;
}
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
// Field reads instead of a registry byValue map hit this predicate runs per block
// probe on the hottest native-structure scans.
String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath();
return path.endsWith("_log") || path.endsWith("_wood")
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|| path.endsWith("_leaves");
@@ -815,10 +815,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int localY = blockY & 15;
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
if (blocks.isAir(x, bufferY, z)) {
PlatformBlockState state = blocks.rawOrNull(x, bufferY, z);
if (state == null) {
continue;
}
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
BlockState blockState = (BlockState) state.nativeHandle();
section.setBlockState(x, localY, z, blockState, false);
if (blockState.hasBlockEntity()) {
@@ -40,6 +40,14 @@ public final class ModdedBlockBuffer implements Hunk<PlatformBlockState> {
return data[index(x, y, z)] == null;
}
/**
* Direct slot read, null when unset lets writeBlocks pay one index + one array load per
* block instead of the isAir + getRaw pair.
*/
public PlatformBlockState rawOrNull(int x, int y, int z) {
return data[index(x, y, z)];
}
@Override
public int getWidth() {
return 16;
@@ -226,6 +226,8 @@ public final class ModdedDimensionManager {
ModdedWorldEngines.evictOrThrow(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key);
// Undo snapshots pin the ServerLevel and could replay into the dead level.
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
level.close();
HANDLES.remove(dimensionId);
if (wipeStorage) {
@@ -47,6 +47,7 @@ import art.arcane.iris.modded.service.ModdedTreeFellerService;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.parallel.MultiBurst;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -99,6 +100,11 @@ public final class ModdedEngineBootstrap {
captureInitialSpawn(server);
currentServer = server;
bind();
// Pair of the stop() burst-pools stage. Load-bearing on integrated servers: once
// closed, MultiBurst falls back to a same-thread executor, so a second world load
// without reopen() would silently run every burst inline.
MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
@@ -170,6 +176,7 @@ public final class ModdedEngineBootstrap {
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
failure = runStopStage(failure, "services", () -> services().disableAll());
failure = runStopStage(failure, "world engines", ModdedWorldEngines::shutdown);
failure = runStopStage(failure, "primary world router", ModdedPrimaryWorldRouter::clear);
@@ -183,6 +190,10 @@ public final class ModdedEngineBootstrap {
failure = runStopStage(failure, "level snapshot", ModdedServerLevels::forget);
}
failure = runStopStage(failure, "generation pool", IrisModdedChunkGenerator::shutdownGenPool);
failure = runStopStage(failure, "burst pools", () -> {
MultiBurst.burst.close();
MultiBurst.ioBurst.close();
});
failure = runStopStage(failure, "sentry", ModdedSentry::flush);
failure = runStopStage(failure, "startup state", ModdedStartup::reset);
failure = runStopStage(failure, "server state", () -> {
@@ -386,7 +397,12 @@ public final class ModdedEngineBootstrap {
ModdedCustomContentRegistry.Discovery customContentDiscovery =
ModdedCustomContentRegistry.discover();
rollback.add(customContentDiscovery::rollback);
ModdedIrisSplash.print(boundLoader);
try {
ModdedIrisSplash.print(boundLoader);
} catch (Throwable splashFailure) {
// A cosmetic banner must never roll back the platform bind.
LOGGER.warn("Iris splash could not be printed", splashFailure);
}
createdServices.enableAll();
runtime = new BoundRuntime(created, createdServices);
rollback.clear();
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
@@ -734,7 +735,7 @@ public final class ModdedForcedDatapack {
}
private static Path packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
return IrisPlatforms.get().packsFolderNoCreate().toPath();
}
private record PublishedState(Path directory, String packsHash) {
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.splash.IrisSplashRenderer;
@@ -38,7 +39,7 @@ public final class ModdedIrisSplash {
}
private static void printPacks(ModdedLoader loader) {
File packFolder = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) {
IrisLogging.info(line);
}
@@ -116,13 +116,68 @@ public final class ModdedPlatform implements IrisPlatform {
return folder;
}
/**
* Modded packs live under config/irisworldgen/packs, not the config/iris data folder. The
* packsFolder overrides are the source of truth; the dataFolder/dataFile overrides below
* re-root any path whose FIRST segment is exactly "packs" so no call site can regress onto
* the empty config/iris/packs directory. Settings, worlds.json, parity/, cache/ and every
* other name stay under config/iris.
*/
@Override
public File packsFolder(String... sub) {
File folder = packsFolderNoCreate(sub);
folder.mkdirs();
return folder;
}
@Override
public File packsFolderNoCreate(String... sub) {
File root = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
if (sub == null || sub.length == 0) {
return root;
}
return new File(root, String.join(File.separator, sub));
}
@Override
public File dataFolder(String... path) {
if (isPacksPath(path)) {
return packsFolder(stripPacksSegment(path));
}
return IrisPlatform.super.dataFolder(path);
}
@Override
public File dataFolderNoCreate(String... path) {
if (isPacksPath(path)) {
return packsFolderNoCreate(stripPacksSegment(path));
}
return IrisPlatform.super.dataFolderNoCreate(path);
}
@Override
public File dataFile(String... path) {
if (isPacksPath(path)) {
File file = packsFolderNoCreate(stripPacksSegment(path));
file.getParentFile().mkdirs();
return file;
}
File file = new File(dataFolder(), String.join(File.separator, path));
file.getParentFile().mkdirs();
return file;
}
private static boolean isPacksPath(String... path) {
// Exact-segment match only: "packbenchmarks" and "packsx" must stay under config/iris.
return path != null && path.length > 0 && "packs".equals(path[0]);
}
private static String[] stripPacksSegment(String... path) {
String[] sub = new String[path.length - 1];
System.arraycopy(path, 1, sub, 0, sub.length);
return sub;
}
@Override
public File pluginJar() {
File jar = loader.modJar();
@@ -50,6 +50,11 @@ final class ModdedSpawnTableMerger {
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
// on the server thread, and the generator monitor is contended by binds/hotloads.
if (vanillaSpawnBiomesInitialized) {
return;
}
synchronized (generator) {
if (vanillaSpawnBiomesInitialized) {
return;
@@ -59,9 +59,36 @@ public final class ModdedStartup {
if (!PREPARED.compareAndSet(false, true)) {
return;
}
reportLegacyPacksDirectory();
validateAllPacks();
}
/**
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
* the legacy directory holds packs, and quietly remove it when it is empty.
*/
private static void reportLegacyPacksDirectory() {
try {
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
if (!legacy.isDirectory()) {
return;
}
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
LOGGER.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
legacy.getAbsolutePath(), real.getAbsolutePath());
return;
}
String[] entries = legacy.list();
if (entries == null || entries.length == 0) {
legacy.delete();
}
} catch (Throwable e) {
LOGGER.debug("Iris legacy packs directory check failed", e);
}
}
/**
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
@@ -26,6 +27,7 @@ import art.arcane.iris.engine.framework.EngineTarget;
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.modded.command.ModdedGuiHost;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
@@ -76,6 +78,8 @@ public final class ModdedWorldEngines {
if (removed[0] == null) {
return;
}
// The GUI host holds strong Engine/ServerLevel references with no other remove path.
ModdedGuiHost.unbind(removed[0]);
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
}
@@ -89,6 +93,7 @@ public final class ModdedWorldEngines {
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
if (current != null && current != activeReplacement) {
close(current);
ModdedGuiHost.unbind(current);
}
return activeReplacement;
});
@@ -96,6 +101,7 @@ public final class ModdedWorldEngines {
static void closeUnregistered(Engine engine) {
close(engine);
ModdedGuiHost.unbind(engine);
}
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
@@ -167,11 +173,7 @@ public final class ModdedWorldEngines {
}
public static File packFolder(String pack) {
return ModdedEngineBootstrap.loader().configDir()
.resolve("irisworldgen")
.resolve("packs")
.resolve(pack)
.toFile();
return IrisPlatforms.get().packsFolderNoCreate(pack);
}
static File resolvePack(String pack, String dimensionKey) {
@@ -200,10 +202,18 @@ public final class ModdedWorldEngines {
ServerLevel level = entry.getKey();
Engine engine = entry.getValue();
try {
close(engine);
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
+ level.dimension().identifier());
// Latch the generator's unloading flag BEFORE closing (unbindEngine sets it,
// then evicts): chunk-system drain work running after this stage would
// otherwise see a closed engine and silently rebuild a fresh engine + Mantle
// that no teardown stage ever closes, writing plates after the final save.
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.unbindEngine(level);
} else {
close(engine);
if (!ENGINES.remove(level, engine) && ENGINES.containsKey(level)) {
throw new IllegalStateException("Iris engine mapping changed during shutdown for "
+ level.dimension().identifier());
}
}
LOGGER.info("Iris engine closed for {}", level.dimension().identifier());
} catch (Throwable e) {
@@ -565,9 +565,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
continue;
}
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -605,9 +603,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int worldZ = position.getZ();
int spawned = 0;
for (int i = 0; i < count; i++) {
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
// Rarity is applied exactly once, as pool weighting in rarityPick - never re-rolled per position (Bukkit parity).
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -774,18 +770,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
int totalRarity = 0;
for (IrisEntitySpawn entry : entries) {
totalRarity += IRare.get(entry);
}
if (totalRarity <= 0) {
return entries.getRandom();
}
KList<IrisEntitySpawn> weighted = new KList<>();
for (IrisEntitySpawn entry : entries) {
weighted.addMultiple(entry, totalRarity / IRare.get(entry));
}
return weighted.getRandom();
KList<IrisEntitySpawn> weighted = IRare.expandWeighted(entries);
return weighted.isEmpty() ? entries.getRandom() : weighted.getRandom();
}
private static long pack(int x, int z) {
@@ -178,10 +178,16 @@ public final class IrisModdedAPI {
/**
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
* <p>
* Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod
* writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
* Iris drops slices it does not need once a region's generation data has served its purpose - both the
* normal per-chunk trim and pregeneration's forced cleanup honor this registry. Any type a mod writes
* with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored.
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored, and the
* block-state slice can never be retained.
* <p>
* Retained data lives for the world's lifetime: it persists into the mantle region files and unloads
* with them, so region files grow with everything you retain. The mod owns the cleanup - call
* {@code deleteMantleData} when a value is no longer needed.
*/
public static void retainMantleDataForSlice(Class<?> sliceType) {
if (sliceType == null) {
@@ -18,6 +18,7 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
@@ -230,7 +231,7 @@ final class ModdedCommandSuggestions {
Set<String> names = new TreeSet<>();
names.add("overworld");
try {
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
File packs = IrisPlatforms.get().packsFolderNoCreate();
for (File child : PackDirectoryResolver.listVisiblePackDirectories(packs)) {
String packName = child.getName();
names.add(packName);
@@ -18,6 +18,7 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.pack.PackDownloader;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
@@ -190,15 +191,15 @@ final class ModdedCommandTree {
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", false))
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", true)))
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable",
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH,
BoolArgumentType.getBool(context, "overwrite"))))
.then(Commands.argument("branch", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
@@ -188,6 +188,9 @@ public final class ModdedDustRevealer {
private static void revealBatch(ModdedScheduler scheduler, RevealRun run,
List<BlockPos> hits, int from) {
if (!active(run)) {
// Drop the registry entry on abort too, or the run record pins the player, level
// and engine until server stop. No-op if a newer run already replaced it.
ACTIVE_RUNS.remove(run.playerId(), run);
return;
}
int to = Math.min(hits.size(), from + PARTICLE_BATCH_SIZE);
@@ -55,6 +55,29 @@ public final class ModdedGuiHost implements GuiHost.Provider {
}
}
/**
* Drops the GUI binding for an evicted engine. Without this the host pinned every
* GUI-bound Engine, its ServerLevel and transitively the MinecraftServer for the process
* lifetime there was no remove path at all.
*/
public static void unbind(Engine engine) {
if (engine == null) {
return;
}
INSTANCE.levels.remove(engine);
INSTANCE.openers.remove(engine);
if (INSTANCE.active == engine) {
INSTANCE.active = null;
}
}
public static void clear() {
INSTANCE.levels.clear();
INSTANCE.openers.clear();
INSTANCE.active = null;
INSTANCE.server = null;
}
public static boolean isGuiLaunchable() {
return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis();
}
@@ -543,6 +543,16 @@ final class ModdedLocateCommands {
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, Position2 at) {
// Same liveness guards the structure completion path has: the search can take up to
// two minutes, and the captured ServerPlayer may be gone or elsewhere by then.
if (player.hasDisconnected() || player.isRemoved()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED));
return;
}
if (player.level() != level) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN));
return;
}
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
@@ -291,30 +291,35 @@ public final class ModdedObjectCommands {
}
int[] tilesSkipped = {0};
int[] tilesSaved = {0};
// capture() must stay on the server thread (getBlockState/getBlockEntity are not
// async-safe), but the disk write of a local, unshared object is not tick work.
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try {
object.write(file);
} catch (IOException e) {
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
return 0;
}
StringBuilder tileNote = new StringBuilder();
if (tilesSaved[0] > 0) {
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
if (tilesSkipped[0] > 0) {
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
MinecraftServer server = source.getServer();
J.a(() -> {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
tileNote.append(")");
} else if (tilesSkipped[0] > 0) {
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, MessageArgument.untrusted("value", engine.getData().getDataFolder().getName()), MessageArgument.untrusted("name", name), MessageArgument.untrusted("w", w), MessageArgument.untrusted("h", h), MessageArgument.untrusted("d", d), MessageArgument.untrusted("value2", object.getBlocks().size()), MessageArgument.untrusted("tileNote", tileNote)));
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
try {
object.write(file);
} catch (IOException e) {
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", String.valueOf(e.getMessage())))));
return;
}
StringBuilder tileNote = new StringBuilder();
if (tilesSaved[0] > 0) {
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
if (tilesSkipped[0] > 0) {
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
}
tileNote.append(")");
} else if (tilesSkipped[0] > 0) {
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
}
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_OBJECT_COMMANDS_SAVED_OBJECTS_IOB_X_X_BLOCK_S, MessageArgument.untrusted("value", engine.getData().getDataFolder().getName()), MessageArgument.untrusted("name", name), MessageArgument.untrusted("w", w), MessageArgument.untrusted("h", h), MessageArgument.untrusted("d", d), MessageArgument.untrusted("value2", object.getBlocks().size()), MessageArgument.untrusted("tileNote", tileNote))));
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
});
return 1;
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded.command;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
@@ -88,10 +89,22 @@ public final class ModdedObjectUndo {
if (entry == null) {
break;
}
// Identity check, not just null: a studio closed and reopened under the same
// dimension id must never have blocks replayed into the dead ServerLevel.
MinecraftServer server = entry.level().getServer();
if (server == null || server.getLevel(entry.level().dimension()) != entry.level()) {
LOGGER.warn("Iris object undo: skipped a stale entry for removed dimension {}",
entry.level().dimension().identifier());
continue;
}
int writes = 0;
for (Map.Entry<BlockPos, BlockState> block : entry.blocks().entrySet()) {
entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
writes++;
try {
entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
writes++;
} catch (Throwable e) {
LOGGER.error("Iris object undo: failed to revert a block at {}", block.getKey(), e);
}
}
LOGGER.info("Iris object undo: reverted {} block(s) in {}", writes, entry.level().dimension().identifier());
reverted++;
@@ -99,6 +112,23 @@ public final class ModdedObjectUndo {
return reverted;
}
/**
* Drops every entry recorded against the given level. Called on dimension removal so a
* closed studio releases its block snapshots and the ServerLevel reference.
*/
public static void forget(ServerLevel level) {
if (level == null) {
return;
}
UNDOS.entrySet().removeIf((Map.Entry<UUID, Deque<Entry>> ownerEntry) -> {
Deque<Entry> queue = ownerEntry.getValue();
synchronized (queue) {
queue.removeIf((Entry entry) -> entry.level() == level);
return queue.isEmpty();
}
});
}
public static void clearAll() {
UNDOS.clear();
}
@@ -18,6 +18,7 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationRegistry;
@@ -98,7 +99,7 @@ public final class ModdedPackCommands {
}
public static File packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
return IrisPlatforms.get().packsFolderNoCreate();
}
private static int validate(CommandSourceStack source, String pack) {
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.StructurePackageClosure;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.engine.framework.Engine;
@@ -31,7 +32,9 @@ import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.core.pack.PackExportClosure;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement;
@@ -383,7 +386,7 @@ public final class ModdedStudioCommands {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
@@ -467,7 +470,9 @@ public final class ModdedStudioCommands {
ServerPlayer player = source.getPlayer();
MinecraftServer server = source.getServer();
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
String dimensionId = STUDIOS.remove(owner);
// Commit the ownership drop only after removal succeeds: dropping it first orphaned a
// still-registered studio that no command could ever remove again.
String dimensionId = STUDIOS.get(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
return 0;
@@ -479,6 +484,7 @@ public final class ModdedStudioCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
return 0;
}
STUDIOS.remove(owner, dimensionId);
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, MessageArgument.untrusted("dimensionId", dimensionId)));
return 1;
}
@@ -588,7 +594,7 @@ public final class ModdedStudioCommands {
File templateFolder = new File(packsRoot, template);
if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, "master", false, true,
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
@@ -655,6 +661,7 @@ public final class ModdedStudioCommands {
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
regionKeys.addAll(dimension.getRegions());
@@ -675,6 +682,8 @@ public final class ModdedStudioCommands {
lootKeys.addAll(region.getLoot().getTables());
spawnerKeys.addAll(region.getEntitySpawners());
collectStructureKeys(structureKeys, region.getStructures());
objectKeys.addAll(PackExportClosure.collectObjectKeys(region.getObjects()));
markerKeys.addAll(PackExportClosure.collectMarkerKeys(region.getObjects()));
}
for (String biomeKey : biomeKeys) {
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
@@ -685,9 +694,15 @@ public final class ModdedStudioCommands {
lootKeys.addAll(biome.getLoot().getTables());
spawnerKeys.addAll(biome.getEntitySpawners());
collectStructureKeys(structureKeys, biome.getStructures());
for (IrisObjectPlacement placement : biome.getObjects()) {
objectKeys.addAll(placement.getPlace());
objectKeys.addAll(PackExportClosure.collectObjectKeys(biome.getObjects()));
markerKeys.addAll(PackExportClosure.collectMarkerKeys(biome.getObjects()));
}
for (String markerKey : markerKeys) {
IrisMarker marker = dm.getMarkerLoader().load(markerKey);
if (marker == null) {
continue;
}
spawnerKeys.addAll(marker.getSpawners());
}
for (String spawnerKey : spawnerKeys) {
IrisSpawner spawner = dm.getSpawnerLoader().load(spawnerKey);
@@ -695,6 +710,14 @@ public final class ModdedStudioCommands {
continue;
}
spawner.getSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
spawner.getInitialSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
}
for (String entityKey : entityKeys) {
IrisEntity entity = dm.getEntityLoader().load(entityKey);
if (entity == null) {
continue;
}
lootKeys.addAll(entity.getLoot().getTables());
}
StringBuilder hashes = new StringBuilder();
@@ -732,6 +755,12 @@ public final class ModdedStudioCommands {
for (String key : lootKeys) {
hashes.append(copyJson(folder, "loot", key, dm.getLootLoader().findFile(key)));
}
for (String key : spawnerKeys) {
hashes.append(copyJson(folder, "spawners", key, dm.getSpawnerLoader().findFile(key)));
}
for (String key : markerKeys) {
hashes.append(copyJson(folder, "markers", key, dm.getMarkerLoader().findFile(key)));
}
JSONObject meta = new JSONObject();
meta.put("hash", IO.hash(hashes.toString()));
@@ -794,14 +823,19 @@ public final class ModdedStudioCommands {
int totalTasks = diameter * diameter;
KMap<String, AtomicInteger> counts = new KMap<>();
engine.getDimension().getRegions().forEach((String key) -> counts.put(key, new AtomicInteger(0)));
// finally-scoped: a throw mid-scan previously leaked the sampler's whole
// ForkJoinPool (close() is the only thing that shuts it down).
MultiBurst burst = new MultiBurst("Region Sampler");
BurstExecutor executor = burst.burst(totalTasks);
new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> {
IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet();
})).setOffset(blockX >> 4, blockZ >> 4).drain();
executor.complete();
burst.close();
try {
BurstExecutor executor = burst.burst(totalTasks);
new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> {
IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet();
})).setOffset(blockX >> 4, blockZ >> 4).drain();
executor.complete();
} finally {
burst.close();
}
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
IrisRegion region = engine.getData().getRegionLoader().load(key);
String rarity = region == null ? "?" : String.valueOf(region.getRarity());
@@ -451,6 +451,9 @@ public final class ModdedWhatCommands {
ModdedScheduler scheduler, MarkerRun run,
List<BlockPos> hits, int from) {
if (!active(run)) {
// Drop the registry entry on abort too, or the run record pins the player, level
// and engine until server stop. No-op if a newer run already replaced it.
ACTIVE_MARKER_RUNS.remove(run.playerId(), run);
return;
}
int to = Math.min(hits.size(), from + MARKER_BATCH_SIZE);
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -177,7 +178,7 @@ public final class ModdedWorldCommands {
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
@@ -280,7 +281,7 @@ public final class ModdedWorldCommands {
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master", false, true,
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
@@ -55,6 +55,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
public final class ModdedChunkUpdateService implements ModdedTickableService {
private static final long PASS_PERIOD_MILLIS = 3_000L;
@@ -84,7 +85,17 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
ExecutorService active = warmupExecutor;
warmupExecutor = null;
if (active != null) {
active.shutdown();
// Drop queued warm-ups (pure prefetch) and AWAIT: the very next shutdown stage
// closes every Mantle, and an in-flight mantle.getChunk would fault a plate back
// in after the close-time flush.
active.shutdownNow();
try {
if (!active.awaitTermination(5L, TimeUnit.SECONDS)) {
IrisLogging.warn("Iris mantle warm-up did not stop before engine close");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
warmupQueue.clear();
}
@@ -0,0 +1,31 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedDownloadBranchParityTest {
private static final List<String> DOWNLOAD_SOURCES = List.of(
"art/arcane/iris/modded/command/ModdedWorldCommands.java",
"art/arcane/iris/modded/command/ModdedStudioCommands.java",
"art/arcane/iris/modded/command/ModdedCommandTree.java");
@Test
public void implicitAndExplicitDownloadsShareTheCoreDefaultBranch() throws Exception {
for (String source : DOWNLOAD_SOURCES) {
Path path = Path.of(System.getProperty("iris.moddedCommonSources"), source);
String text = Files.readString(path);
assertFalse(source + " must not hardcode a \"master\" download branch",
text.contains("\"master\""));
assertFalse(source + " must not hardcode a \"stable\" download branch",
text.contains("\"stable\""));
assertTrue(source + " must use PackDownloader.DEFAULT_BRANCH",
text.contains("PackDownloader.DEFAULT_BRANCH"));
}
}
}
@@ -36,12 +36,12 @@ public class ModdedLootApplierTest {
}
@Test
public void clearRemovesNativeAndIrisSourcesBeforeAdding() {
public void clearRemovesEverythingAndContributesNothing() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
assertEquals(List.of("biome-iris"), sources);
assertEquals(List.of(), sources);
}
@Test
@@ -0,0 +1,106 @@
package art.arcane.iris.modded;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
public class ModdedPlatformPathsTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private ModdedPlatform platform() {
Path configDir = temporaryFolder.getRoot().toPath();
return new ModdedPlatform(new ModdedLoader() {
@Override
public String platformName() {
return "test";
}
@Override
public String minecraftVersion() {
return "26.2";
}
@Override
public String modVersion() {
return "0.0.0";
}
@Override
public MinecraftServer currentServer() {
return null;
}
@Override
public void invalidateLevelCache(MinecraftServer server) {
}
@Override
public boolean clientEnvironment() {
return false;
}
@Override
public Path configDir() {
return configDir;
}
@Override
public File modJar() {
return null;
}
@Override
public boolean hasTreeFellerPermission(ServerPlayer player) {
return false;
}
@Override
public boolean canTreeFellerBreak(ServerLevel level, ServerPlayer player, BlockPos position, BlockState state) {
return false;
}
});
}
@Test
public void packsResolveUnderIrisworldgen() {
ModdedPlatform platform = platform();
File root = temporaryFolder.getRoot();
File packsRoot = new File(new File(root, "irisworldgen"), "packs");
assertEquals(packsRoot, platform.packsFolder());
assertEquals(packsRoot, platform.dataFolder("packs"));
assertEquals(new File(packsRoot, "overworld"), platform.dataFolderNoCreate("packs", "overworld"));
assertEquals(new File(packsRoot, "overworld" + File.separator + "dimensions" + File.separator + "overworld.json"),
platform.dataFile("packs", "overworld", "dimensions", "overworld.json"));
}
@Test
public void everythingElseStaysUnderIris() {
ModdedPlatform platform = platform();
File iris = new File(temporaryFolder.getRoot(), "iris");
assertEquals(iris, platform.dataFolder());
assertEquals(new File(iris, "settings.json"), platform.dataFile("settings.json"));
assertEquals(new File(iris, "parity"), platform.dataFolder("parity"));
}
@Test
public void packsMatchIsExactSegmentOnly() {
ModdedPlatform platform = platform();
File iris = new File(temporaryFolder.getRoot(), "iris");
assertEquals(new File(iris, "packbenchmarks"), platform.dataFolder("packbenchmarks"));
assertEquals(new File(iris, "packsx"), platform.dataFolderNoCreate("packsx"));
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedSpawnerRarityParityTest {
private static IrisEntitySpawn spawn(int rarity) {
IrisEntitySpawn spawn = new IrisEntitySpawn();
spawn.setRarity(rarity);
return spawn;
}
@Test
public void rarityIsAppliedOnceAsPoolWeightOnly() {
IrisEntitySpawn common = spawn(1);
IrisEntitySpawn rare = spawn(4);
KList<IrisEntitySpawn> expanded = IRare.expandWeighted(List.of(common, rare));
// totalRarity 5 -> common appears 5/1 = 5 times, rare 5/4 = 1 time.
assertEquals(6, expanded.size());
assertEquals(5, expanded.stream().filter(entry -> entry == common).count());
assertEquals(1, expanded.stream().filter(entry -> entry == rare).count());
}
@Test
public void rarityZeroAndNegativeAreClampedToOne() {
assertEquals(1, IRare.get(spawn(0)));
assertEquals(1, IRare.get(spawn(-5)));
assertEquals(3, IRare.get(spawn(3)));
}
@Test
public void perPositionSpawnLoopsDoNotRerollEntryRarity() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedWorldManager.java"));
for (String method : List.of("private int spawnEntry(", "private int spawnEntryAt(")) {
String body = methodBody(source, method);
assertFalse(method + " must not re-roll rarity per position; rarityPick already weighted the pool",
body.contains("getRarity()"));
assertTrue(method + " must keep the min/max spawn-count roll",
body.contains("LootResolver.inclusive("));
}
}
private static String methodBody(String source, String declaration) {
int start = source.indexOf(declaration);
assertTrue("ModdedWorldManager must declare " + declaration, start >= 0);
int open = source.indexOf('{', start);
int depth = 0;
for (int index = open; index < source.length(); index++) {
char character = source.charAt(index);
if (character == '{') {
depth++;
} else if (character == '}') {
depth--;
if (depth == 0) {
return source.substring(open + 1, index);
}
}
}
throw new AssertionError(declaration + " is not brace balanced");
}
}