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
@@ -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");
}
}