mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Full audit fixpass: 93 defect fixes, 14 perf wins, vestigial cleanup
This commit is contained in:
+2
-2
@@ -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;
|
||||
|
||||
+2
@@ -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) {
|
||||
|
||||
+17
-1
@@ -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();
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+19
-9
@@ -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) {
|
||||
|
||||
+4
-18
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+4
-3
@@ -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) ->
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+23
@@ -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();
|
||||
}
|
||||
|
||||
+10
@@ -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");
|
||||
|
||||
+27
-22
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+32
-2
@@ -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();
|
||||
}
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+47
-13
@@ -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());
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+3
-2
@@ -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()) {
|
||||
|
||||
+12
-1
@@ -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();
|
||||
}
|
||||
|
||||
+31
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
|
||||
+106
@@ -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"));
|
||||
}
|
||||
}
|
||||
+75
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user