This commit is contained in:
Brian Neumann-Fopiano
2026-07-03 15:10:16 -04:00
parent 75e8c22268
commit a8e46e3590
104 changed files with 6002 additions and 2692 deletions
@@ -1,5 +1,8 @@
package art.arcane.iris.core.nms.v26_2_R1;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.nms.INMSBinding;
@@ -32,6 +35,7 @@ import art.arcane.iris.util.common.scheduling.J;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.core.*;
@@ -50,6 +54,7 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Block;
@@ -77,7 +82,6 @@ import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.TreeFeature;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.*;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@@ -875,6 +879,34 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public boolean saveAndUnloadChunk(World world, int x, int z) {
try {
ServerLevel level = ((CraftWorld) world).getHandle();
ChunkHolderManager chm = level.moonrise$getChunkTaskScheduler().chunkHolderManager;
chm.processTicketUpdates();
NewChunkHolder holder = chm.getChunkHolder(x, z);
if (holder != null) {
holder.save(false);
}
chm.processUnloads();
return true;
} catch (Throwable e) {
IrisLogging.reportError(e);
return false;
}
}
@Override
public void flushChunkIO(World world) {
try {
ServerLevel level = ((CraftWorld) world).getHandle();
MoonriseRegionFileIO.flush(level);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
@Override
public boolean clearChunkBlocks(Chunk bukkitChunk) {
try {
@@ -1133,13 +1165,16 @@ public class NMSBinding implements INMSBinding {
return true;
try {
IrisLogging.info("Injecting Bukkit");
var buddy = new ByteBuddy();
buddy.redefine(ServerLevel.class)
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class))))
.make()
.load(ServerLevel.class.getClassLoader(), Agent.installed());
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
@@ -1155,6 +1190,17 @@ public class NMSBinding implements INMSBinding {
return false;
}
@Override
public void ensureServerLevelInjection() {
if (!injected.get())
return;
try {
Agent.getInstrumentation().retransformClasses(ServerLevel.class);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to re-apply ServerLevel injection");
}
}
@Override
public KMap<Material, List<BlockProperty>> getBlockProperties() {
KMap<Material, List<BlockProperty>> states = new KMap<>();
@@ -1210,14 +1256,14 @@ public class NMSBinding implements INMSBinding {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess,
@Advice.Argument(4) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
) {
if (levelStorageAccess == null)
if (dimensionKey == null)
return;
try {
String levelId = levelStorageAccess.getLevelId();
String levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
@@ -21,7 +21,7 @@ package art.arcane.iris;
import art.arcane.iris.engine.IrisWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.ServerConfigurator;
@@ -1265,55 +1265,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void printPacks() {
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
List<SplashPackMetadata> packs = collectSplashPacks(packFolder);
if (packs.isEmpty())
return;
Iris.info("Custom Dimensions: " + packs.size());
for (SplashPackMetadata pack : packs) {
printPack(pack);
}
}
static List<SplashPackMetadata> collectSplashPacks(File packFolder) {
List<IrisSplashPackScanner.SplashPackMetadata> scanned = IrisSplashPackScanner.collect(packFolder, Iris::reportError);
if (scanned.isEmpty()) {
return Collections.emptyList();
}
List<SplashPackMetadata> packs = new ArrayList<>(scanned.size());
for (IrisSplashPackScanner.SplashPackMetadata metadata : scanned) {
packs.add(new SplashPackMetadata(metadata.name(), metadata.version()));
}
return packs;
}
static SplashPackMetadata readSplashPack(File pack) {
IrisSplashPackScanner.SplashPackMetadata metadata = IrisSplashPackScanner.read(pack, Iris::reportError);
if (metadata == null) {
return null;
}
return new SplashPackMetadata(metadata.name(), metadata.version());
}
private void printPack(SplashPackMetadata pack) {
Iris.info(" " + pack.name() + " v" + pack.version());
}
static final class SplashPackMetadata {
private final String name;
private final String version;
SplashPackMetadata(String name, String version) {
this.name = name;
this.version = version;
}
String name() {
return name;
}
String version() {
return version;
for (String line : IrisSplashComposer.composePackLines(packFolder, Iris::reportError)) {
Iris.info(line);
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.World;
import org.bukkit.util.Vector;
@@ -83,4 +84,23 @@ public class CommandPregen implements DirectorExecutor {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
}
}
@Director(description = "Show the active pregeneration status")
public void status() {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
sender().sendMessage(C.YELLOW + "No active pregeneration task.");
return;
}
String world = progress.worldName() == null ? "?" : progress.worldName();
sender().sendMessage(C.GREEN + "Pregen " + C.GOLD + world + C.GREEN + ": " + C.GOLD + Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks())
+ C.GREEN + " (" + C.GOLD + String.format("%.1f", progress.percent()) + "%" + C.GREEN + ")"
+ (progress.paused() ? C.YELLOW + " PAUSED" : ""));
sender().sendMessage(C.GREEN + "Speed: " + C.GOLD + Form.f((int) progress.chunksPerSecond()) + "/s" + C.GREEN
+ " ETA: " + C.GOLD + Form.duration(progress.eta(), 2) + C.GREEN
+ " Elapsed: " + C.GOLD + Form.duration(progress.elapsed(), 2) + C.GREEN
+ " Method: " + C.GOLD + progress.method()
+ (progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""));
}
}
@@ -94,7 +94,7 @@ public class CommandWhat implements DirectorExecutor {
@Director(description = "What region am i in?", origin = DirectorOrigin.PLAYER)
public void region() {
try {
Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
@@ -20,6 +20,7 @@ import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import lombok.Synchronized;
import org.bukkit.Bukkit;
import org.bukkit.World;
@@ -317,7 +318,11 @@ public class IrisEngineSVC implements IrisService {
}
try {
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
engine.getMantle().trim(0L, 0);
} else {
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
@@ -349,7 +354,12 @@ public class IrisEngineSVC implements IrisService {
try {
long unloadStart = System.currentTimeMillis();
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engine));
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine);
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
if (heapPressure && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
if (count > 0) {
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
}
@@ -385,7 +395,7 @@ public class IrisEngineSVC implements IrisService {
return limit;
}
return Math.max(limit, IrisSettings.get().getPregen().getMaxResidentTectonicPlates());
return Math.max(limit, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
}
private long activeIdleDuration(Engine engine) {
@@ -433,7 +443,7 @@ public class IrisEngineSVC implements IrisService {
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldName(world.getName());
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -34,7 +35,7 @@ public class IrisDiagnosticsTest {
}
@Test
public void collectSplashPacksSkipsInternalAndInvalidFolders() throws Exception {
public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception {
Path root = Files.createTempDirectory("iris-splash");
try {
Path validPack = root.resolve("overworld");
@@ -50,9 +51,9 @@ public class IrisDiagnosticsTest {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
List<Iris.SplashPackMetadata> packs;
List<IrisSplashPackScanner.SplashPackMetadata> packs;
try {
packs = Iris.collectSplashPacks(root.toFile());
packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError);
} finally {
System.setErr(originalErr);
}
@@ -19,6 +19,7 @@
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.server.MinecraftServer;
@@ -52,6 +53,11 @@ public final class FabricModdedLoader implements ModdedLoader {
return instance instanceof MinecraftServer server ? server : null;
}
@Override
public boolean clientEnvironment() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
}
@Override
public Path configDir() {
return FabricLoader.getInstance().getConfigDir();
@@ -20,12 +20,7 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer;
@@ -34,8 +29,6 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
@@ -54,26 +47,10 @@ import net.minecraft.world.phys.BlockHitResult;
public final class IrisFabricBootstrap implements ModInitializer {
@Override
public void onInitialize() {
ModdedEngineBootstrap.initialize(new FabricModdedLoader());
FabricLoader loader = FabricLoader.getInstance();
String modVersion = loader.getModContainer("irisworldgen")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
String minecraftVersion = loader.getModContainer("minecraft")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + minecraftVersion + " (Fabric)");
ModdedEngineBootstrap.selfTest(IrisFabricBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC);
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
});
ModdedEngineBootstrap.bootCommon(new FabricModdedLoader(), "Fabric",
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
@@ -83,17 +60,5 @@ public final class IrisFabricBootstrap implements ModInitializer {
ModdedEngineBootstrap.tick(server);
ModdedWandService.serverTick(server);
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity);
}
String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) {
ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule();
}
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.forgespi.language.IModFileInfo;
@@ -52,6 +53,11 @@ public final class ForgeModdedLoader implements ModdedLoader {
return ServerLifecycleHooks.getCurrentServer();
}
@Override
public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient();
}
@Override
public Path configDir() {
return FMLPaths.CONFIGDIR.get();
@@ -21,12 +21,7 @@ package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
@@ -36,13 +31,11 @@ import net.minecraftforge.event.AddPackFindersEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.VersionInfo;
import net.minecraftforge.registries.DeferredRegister;
import java.util.function.Predicate;
@@ -50,27 +43,14 @@ import java.util.function.Predicate;
@Mod("irisworldgen")
public final class IrisForgeBootstrap {
public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
ModdedEngineBootstrap.initialize(new ForgeModdedLoader());
String modVersion = ModList.getModContainerById("irisworldgen")
.map((ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
VersionInfo versionInfo = FMLLoader.versionInfo();
ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + versionInfo.mcVersion() + " (Forge " + versionInfo.forgeVersion() + ")");
ModdedEngineBootstrap.selfTest(IrisForgeBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind();
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(context.getModBusGroup());
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
ModdedEngineBootstrap.bootCommon(new ForgeModdedLoader(), "Forge " + FMLLoader.versionInfo().forgeVersion(), () -> {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(context.getModBusGroup());
});
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
@@ -85,17 +65,5 @@ public final class IrisForgeBootstrap {
ModdedEngineBootstrap.tick(event.server());
ModdedWandService.serverTick(event.server());
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity);
}
String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) {
ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule();
}
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -64,6 +65,7 @@ import org.slf4j.LoggerFactory;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -76,28 +78,39 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
).apply(instance, IrisModdedChunkGenerator::new));
private final String dimensionKey;
private final String defaultPack;
private final String defaultDimensionKey;
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile Engine engine;
private volatile String activePackKey;
private volatile String activePack;
private volatile String activeDimensionKey;
private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
super(biomeSource);
this.dimensionKey = dimensionKey;
this.activePackKey = dimensionKey;
int colon = dimensionKey.indexOf(':');
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
this.activePack = defaultPack;
this.activeDimensionKey = defaultDimensionKey;
}
public synchronized void repoint(String packKey, long seed) {
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
this.activePackKey = packKey;
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
this.missingBiomeWarnings.clear();
}
public synchronized void unbindEngine() {
@@ -108,14 +121,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
this.missingBiomeWarnings.clear();
}
public synchronized void resetToDefault() {
repoint(dimensionKey, Long.MIN_VALUE);
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
}
public String activePackKey() {
return activePackKey;
public String activePack() {
return activePack;
}
public String activeDimensionKey() {
return activeDimensionKey;
}
@Override
@@ -149,7 +167,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (level == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
}
Engine created = ModdedWorldEngines.get(level, activePackKey, seedOverride);
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
engine = created;
return created;
}
@@ -179,10 +197,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return engine();
}
public long lastChunkGenAt() {
return lastChunkGenAt;
}
public void onHotload() {
biomeHolders.clear();
missingBiomeWarnings.clear();
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
Engine generationEngine = engine();
ChunkPos pos = chunk.getPos();
lastChunkGenAt = System.currentTimeMillis();
if (announced.compareAndSet(false, true)) {
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
@@ -197,6 +225,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
} catch (GenerationSessionException e) {
if (e.isExpectedTeardown()) {
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
return CompletableFuture.completedFuture(chunk);
}
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
} catch (Throwable e) {
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
@@ -232,7 +267,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Identifier identifier = Identifier.tryParse(key);
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> fallbackBiome(registry));
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
}
return fallbackBiome(registry);
});
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
return raced != null ? raced : resolved;
}
@@ -98,11 +98,11 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
if (owner == null) {
return -1;
}
org.bukkit.block.Biome derivative = owner.getVanillaDerivative();
if (derivative == null || derivative.getKey() == null) {
String derivativeKey = owner.getVanillaDerivativeKey();
if (derivativeKey == null) {
return -1;
}
return idForKey(registry, derivative.getKey().toString());
return idForKey(registry, derivativeKey);
}
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
@@ -18,7 +18,10 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
@@ -28,7 +31,9 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
@@ -45,9 +50,13 @@ import net.minecraft.world.level.storage.WorldData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@@ -55,6 +64,8 @@ public final class ModdedDimensionManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Object LOCK = new Object();
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
private static volatile ModdedServerAccess access;
private ModdedDimensionManager() {
@@ -64,6 +75,10 @@ public final class ModdedDimensionManager {
access = serverAccess;
}
public static void clear() {
HANDLES.clear();
}
public static Handle handle(String dimensionId) {
return HANDLES.get(dimensionId);
}
@@ -97,14 +112,16 @@ public final class ModdedDimensionManager {
return generator.commandEngine();
}
public static Handle create(MinecraftServer server, String dimensionId, String packKey, long seed) {
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) {
existing.generator().repoint(packKey, seed);
return existing;
existing.generator().repoint(pack, packDimensionKey, seed);
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed);
return refreshed;
}
if (serverAccess.hasLevel(server, key)) {
ServerLevel present = level(server, dimensionId);
@@ -112,40 +129,36 @@ public final class ModdedDimensionManager {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
}
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
generator.repoint(packKey, seed);
Handle handle = new Handle(dimensionId, packKey, seed, present, generator);
generator.repoint(pack, packDimensionKey, seed);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle);
return handle;
}
try {
Handle handle = inject(server, serverAccess, dimensionId, key, packKey, seed);
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
HANDLES.put(dimensionId, handle);
LOGGER.info("Iris injected runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed);
LOGGER.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
return handle;
} catch (Throwable e) {
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed, e);
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
}
}
}
public static Handle createPersistent(MinecraftServer server, String dimensionId, String packKey, long seed) {
Handle handle = create(server, dimensionId, packKey, seed);
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, packKey, seed));
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
Handle handle = create(server, dimensionId, pack, packDimensionKey, seed);
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
return handle;
}
public static boolean removePersistent(MinecraftServer server, String dimensionId) {
boolean removed = remove(server, dimensionId);
public static boolean removePersistent(MinecraftServer server, String dimensionId, boolean wipeStorage) {
boolean removed = remove(server, dimensionId, wipeStorage);
ModdedDimensionRegistryStore.remove(server, dimensionId);
return removed;
}
public static boolean remove(MinecraftServer server, String dimensionId) {
return remove(server, dimensionId, false);
}
public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) {
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
@@ -153,6 +166,9 @@ public final class ModdedDimensionManager {
ServerLevel level = level(server, dimensionId);
if (level == null) {
HANDLES.remove(dimensionId);
if (wipeStorage) {
ModdedDimensionStorage.wipe(server, key);
}
return false;
}
try {
@@ -184,29 +200,78 @@ public final class ModdedDimensionManager {
}
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
completeTeleport(player, level, x, y, z, blockX, blockZ);
return true;
}
UUID playerId = player.getUUID();
CompletableFuture
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
.thenCompose((CompletableFuture<?> inner) -> inner)
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
if (error != null) {
LOGGER.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
}
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
if (target == null) {
return;
}
completeTeleport(target, level, x, y, z, blockX, blockZ);
}));
return true;
}
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
double targetY = y;
if (y == Double.MIN_VALUE) {
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
}
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
return true;
}
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess) {
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
if (dimension != null) {
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
if (packType.isPresent()) {
return packType.get();
}
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
}
}
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
return registry.get(studioPool)
.map(reference -> (Holder<DimensionType>) reference)
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
}
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String packKey, long seed) {
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
try {
File packFolder = ModdedWorldEngines.packFolder(pack);
if (!packFolder.isDirectory()) {
return null;
}
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
} catch (Throwable e) {
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
return null;
}
}
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
RegistryAccess registryAccess = server.registryAccess();
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess);
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, packKey);
generator.repoint(packKey, seed);
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
generator.repoint(pack, packDimensionKey, seed);
LevelStem stem = new LevelStem(dimensionType, generator);
WorldData worldData = server.getWorldData();
@@ -231,20 +296,21 @@ public final class ModdedDimensionManager {
serverAccess.putLevel(server, key, level);
server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, packKey, seed, level, generator);
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
}
private static void evacuate(MinecraftServer server, ServerLevel from) {
public static int evacuate(MinecraftServer server, ServerLevel from) {
ServerLevel fallback = server.overworld();
if (fallback == from) {
return;
return 0;
}
int spawnX = 0;
int spawnZ = 0;
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawnX, spawnZ);
for (ServerPlayer player : new ArrayList<>(from.players())) {
player.teleportTo(fallback, spawnX + 0.5D, spawnY, spawnZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
BlockPos spawn = fallback.getRespawnData().pos();
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawn.getX(), spawn.getZ());
List<ServerPlayer> players = new ArrayList<>(from.players());
for (ServerPlayer player : players) {
player.teleportTo(fallback, spawn.getX() + 0.5D, spawnY, spawn.getZ() + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
}
return players.size();
}
private static ResourceKey<Level> levelKey(String dimensionId) {
@@ -260,6 +326,6 @@ public final class ModdedDimensionManager {
return bound;
}
public record Handle(String dimensionId, String packKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
}
}
@@ -57,12 +57,20 @@ public final class ModdedDimensionRegistryStore {
for (int index = 0; index < entries.length(); index++) {
JSONObject entry = entries.getJSONObject(index);
String id = entry.optString("id", null);
String packKey = entry.optString("packKey", null);
if (id == null || packKey == null) {
if (id == null) {
continue;
}
long seed = entry.optLong("seed", 0L);
deduplicated.put(id, new PersistentDimension(id, packKey, seed));
String pack = entry.optString("pack", null);
String dimension = entry.optString("dimension", null);
if (pack == null || dimension == null) {
LOGGER.error("Iris registry entry '{}' in {} has no pack/dimension fields; skipping it. Re-create the world with /iris world enable", id, file);
continue;
}
if (!entry.has("seed")) {
LOGGER.warn("Iris registry entry '{}' in {} has no seed; skipping it. Re-create the world with /iris world enable", id, file);
continue;
}
deduplicated.put(id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
}
return new ArrayList<>(deduplicated.values());
} catch (RuntimeException | IOException e) {
@@ -98,7 +106,8 @@ public final class ModdedDimensionRegistryStore {
for (PersistentDimension dimension : dimensions) {
JSONObject entry = new JSONObject();
entry.put("id", dimension.id());
entry.put("packKey", dimension.packKey());
entry.put("pack", dimension.pack());
entry.put("dimension", dimension.dimension());
entry.put("seed", dimension.seed());
entries.put(entry);
}
@@ -118,6 +127,6 @@ public final class ModdedDimensionRegistryStore {
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
}
public record PersistentDimension(String id, String packKey, long seed) {
public record PersistentDimension(String id, String pack, String dimension, long seed) {
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
@@ -28,8 +29,16 @@ import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.command.ModdedGuiHost;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedPregenJob;
import art.arcane.iris.modded.command.ModdedStudioCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.server.MinecraftServer;
@@ -51,6 +60,7 @@ public final class ModdedEngineBootstrap {
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform;
private static volatile MinecraftServer currentServer;
private ModdedEngineBootstrap() {
}
@@ -71,32 +81,76 @@ public final class ModdedEngineBootstrap {
SERVICE_MANAGER.tick(server);
}
public static void stop() {
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
public static void start(MinecraftServer server) {
currentServer = server;
bind();
ModdedStartup.reset();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.shutdown();
scheduler.reset();
}
SERVICE_MANAGER.enableAll();
}
public static void initialize(ModdedLoader moddedLoader) {
public static void stop() {
ModdedPregenJob.shutdown();
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
ModdedDimensionManager.clear();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
}
ModdedStartup.reset();
currentServer = null;
}
public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) {
loader = moddedLoader;
ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")");
selfTest(moddedLoader.getClass().getClassLoader());
bind();
chunkGeneratorRegistration.run();
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
armParityProbe();
armWorldCheck();
}
private static void armParityProbe() {
String parity = System.getProperty("iris.parity");
if (parity == null) {
return;
}
ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity);
}
private static void armWorldCheck() {
if (System.getProperty("iris.worldcheck") == null) {
return;
}
ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule();
}
public static ModdedLoader loader() {
ModdedLoader bound = loader;
if (bound == null) {
throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.initialize first");
throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.bootCommon first");
}
return bound;
}
public static MinecraftServer currentServer() {
return loader().currentServer();
MinecraftServer tracked = currentServer;
return tracked != null ? tracked : loader().currentServer();
}
public static void selfTest(ClassLoader classLoader) {
private static void selfTest(ClassLoader classLoader) {
int loadedClasses = 0;
for (String className : CORE_SELF_TEST_CLASSES) {
try {
@@ -124,6 +178,7 @@ public final class ModdedEngineBootstrap {
return platform;
}
ModdedLoader boundLoader = loader();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
ModdedPlatform created = new ModdedPlatform(boundLoader);
IrisPlatforms.bind(created);
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
@@ -135,11 +190,18 @@ public final class ModdedEngineBootstrap {
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
if (boundLoader.clientEnvironment()) {
ModdedStartup.prefetchDefaultPack();
}
ModdedIrisSplash.print(boundLoader);
return created;
}
@@ -121,10 +121,11 @@ public final class ModdedForcedDatapack {
IDataFixer fixer = DataVersion.getLatest().get();
int packCount = 0;
KList<String> presetIds = new KList<>();
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
if (packs != null) {
for (File pack : packs) {
if (installPack(pack, fixer, folders, seenBiomes)) {
if (installPack(pack, fixer, folders, seenBiomes, presetIds)) {
packCount++;
}
}
@@ -132,28 +133,113 @@ public final class ModdedForcedDatapack {
writePackMeta(packDirectory);
writeStudioPoolType(packDirectory);
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} custom biome(s) at {}", packCount, seenBiomes.size(), packDirectory);
if (!presetIds.isEmpty()) {
writeWorldPresetTag(packDirectory, presetIds);
}
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), seenBiomes.size(), packDirectory);
return packDirectory;
}
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes) {
String packKey = packFolder.getName();
if (!new File(packFolder, "dimensions/" + packKey + ".json").isFile()) {
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes, KList<String> presetIds) {
String packName = packFolder.getName();
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
return false;
}
try {
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(packKey);
if (dimension == null) {
return false;
boolean installed = false;
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
try {
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
continue;
}
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
presetIds.add("irisworldgen:" + presetKey);
installed = true;
} catch (Throwable e) {
LOGGER.error("Iris failed to install forced datapack content for pack '{}' dimension '{}'", packName, dimensionKey, e);
}
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
writeDimensionType(folders, fixer, dimension);
return true;
} catch (Throwable e) {
LOGGER.error("Iris failed to install forced datapack content for pack '{}'", packKey, e);
return false;
}
return installed;
}
public static String dimensionTypeRef(IrisDimension dimension) {
return fitsStudioPool(dimension)
? "irisworldgen:" + STUDIO_POOL_TYPE_KEY
: "irisworldgen:" + dimension.getDimensionTypeKey();
}
private static void writeWorldPreset(KList<File> folders, IrisDimension dimension, String packName, String dimensionKey, String presetKey) throws IOException {
String dimensionRef = dimensionKey.equals(packName) ? packName : packName + ":" + dimensionKey;
String json = worldPresetJson(dimensionRef, dimensionTypeRef(dimension));
for (File parent : folders) {
Path output = parent.toPath().resolve(PACK_FOLDER).resolve("data").resolve("irisworldgen").resolve("worldgen").resolve("world_preset").resolve(presetKey + ".json");
Files.createDirectories(output.getParent());
Files.writeString(output, json, StandardCharsets.UTF_8);
}
}
private static String worldPresetJson(String dimensionRef, String dimensionTypeRef) {
return "{\n"
+ " \"dimensions\": {\n"
+ " \"minecraft:overworld\": {\n"
+ " \"type\": \"" + dimensionTypeRef + "\",\n"
+ " \"generator\": {\n"
+ " \"type\": \"irisworldgen:iris\",\n"
+ " \"biome_source\": {\n"
+ " \"type\": \"minecraft:fixed\",\n"
+ " \"biome\": \"minecraft:plains\"\n"
+ " },\n"
+ " \"dimension\": \"" + dimensionRef + "\"\n"
+ " }\n"
+ " },\n"
+ " \"minecraft:the_nether\": {\n"
+ " \"type\": \"minecraft:the_nether\",\n"
+ " \"generator\": {\n"
+ " \"type\": \"minecraft:noise\",\n"
+ " \"settings\": \"minecraft:nether\",\n"
+ " \"biome_source\": {\n"
+ " \"type\": \"minecraft:multi_noise\",\n"
+ " \"preset\": \"minecraft:nether\"\n"
+ " }\n"
+ " }\n"
+ " },\n"
+ " \"minecraft:the_end\": {\n"
+ " \"type\": \"minecraft:the_end\",\n"
+ " \"generator\": {\n"
+ " \"type\": \"minecraft:noise\",\n"
+ " \"settings\": \"minecraft:end\",\n"
+ " \"biome_source\": {\n"
+ " \"type\": \"minecraft:the_end\"\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}\n";
}
private static void writeWorldPresetTag(Path packDirectory, KList<String> presetIds) throws IOException {
StringBuilder values = new StringBuilder();
for (int i = 0; i < presetIds.size(); i++) {
if (i > 0) {
values.append(",\n");
}
values.append(" \"").append(presetIds.get(i)).append("\"");
}
String json = "{\n"
+ " \"replace\": false,\n"
+ " \"values\": [\n"
+ values
+ "\n ]\n"
+ "}\n";
Path output = packDirectory.resolve("data").resolve("minecraft").resolve("tags").resolve("worldgen").resolve("world_preset").resolve("normal.json");
Files.createDirectories(output.getParent());
Files.writeString(output, json, StandardCharsets.UTF_8);
}
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
@@ -18,17 +18,12 @@
package art.arcane.iris.modded;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import art.arcane.iris.core.splash.IrisSplashPackScanner.SplashPackMetadata;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.splash.IrisSplashRenderer;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
public final class ModdedIrisSplash {
@@ -44,44 +39,16 @@ public final class ModdedIrisSplash {
private static void printPacks(ModdedLoader loader) {
File packFolder = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
List<SplashPackMetadata> packs = IrisSplashPackScanner.collect(packFolder, IrisLogging::reportError);
if (packs.isEmpty()) {
return;
}
IrisLogging.info("Custom Dimensions: " + packs.size());
for (SplashPackMetadata pack : packs) {
IrisLogging.info(" " + pack.name() + " v" + pack.version());
for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) {
IrisLogging.info(line);
}
}
private static void printLogo(ModdedLoader loader) {
String padding = " ".repeat(4);
String version = loader.modVersion();
String releaseTrain = getReleaseTrain(version);
String startupDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
int javaVersion = getJavaVersion();
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
String[] splash = IrisSplashRenderer.renderPlain();
String[] info = new String[]{
"",
" Iris, Dimension Engine [" + releaseTrain + " RC.1.1.6]",
" Version: " + version,
" By: Volmit Software (Arcane Arts)",
" Server: " + loader.platformName() + " / Minecraft " + loader.minecraftVersion(),
" Java: " + javaVersion + " | Date: " + startupDate,
" Commit: " + BuildConstants.COMMIT + "/" + BuildConstants.ENVIRONMENT,
"",
"",
"",
""
};
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(padding).append(splash[i]).append(info[i]).append('\n');
}
IrisLogging.info(builder.toString());
String[] info = IrisSplashComposer.composeInfo(loader.modVersion(), serverLine, IrisSplashComposer.InfoStyle.PLAIN);
IrisLogging.info(IrisSplashComposer.compose(splash, info));
}
private static boolean isLogoEnabled() {
@@ -92,30 +59,4 @@ public final class ModdedIrisSplash {
return true;
}
}
private static int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
private static String getReleaseTrain(String version) {
String value = version == null ? "unknown" : version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
}
@@ -0,0 +1,290 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisEnchantment;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.TagParser;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Unit;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.CustomModelData;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedItemTranslator {
private static final Set<String> WARNED = ConcurrentHashMap.newKeySet();
private static final Field TYPE_FIELD = lootField("type");
private static final Field DYE_COLOR_FIELD = lootField("dyeColor");
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
private ModdedItemTranslator() {
}
public static KList<ItemStack> loot(IrisLootTable table, RNG rng, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
KList<ItemStack> out = new KList<>();
KList<IrisLoot> entries = table.getLoot();
if (entries.isEmpty()) {
return out;
}
int m = 0;
int c = 0;
int mx = rng.i(table.getMinPicked(), table.getMaxPicked());
while (m < mx && c++ < table.getMaxTries()) {
IrisLoot entry = entries.get(rng.i(entries.size()));
if (entry.getSlotTypes() != slot) {
continue;
}
ItemStack item = item(entry, table, rng, level, x, y, z);
if (item != null && !item.isEmpty()) {
out.add(item);
m++;
}
}
return out;
}
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
return null;
}
try {
ItemStack stack = baseStack(loot, rng);
if (stack == null || stack.isEmpty()) {
return null;
}
applyComponents(loot, stack, rng, level);
applyCustomNbt(stack, loot.getCustomNbt());
return stack;
} catch (Throwable e) {
IrisLogging.reportError(e);
return null;
}
}
private static ItemStack baseStack(IrisLoot loot, RNG rng) {
String raw = readString(TYPE_FIELD, loot);
if (raw == null || raw.isBlank()) {
return null;
}
String key = raw.toLowerCase(Locale.ROOT);
Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key);
if (id == null || !BuiltInRegistries.ITEM.containsKey(id)) {
warnOnce("item:" + key, "Iris loot: unknown item '" + raw + "'");
return null;
}
Item item = BuiltInRegistries.ITEM.getValue(id);
return new ItemStack(item, Math.max(1, rng.i(loot.getMinAmount(), loot.getMaxAmount())));
}
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
applyEnchantments(loot, stack, rng, level);
consumeAttributeDraws(loot, rng);
if (loot.isUnbreakable()) {
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
}
if (loot.getItemFlags().isNotEmpty()) {
warnOnce("itemFlags", "Iris loot: itemFlags are not supported on modded; skipping flags");
}
if (loot.getCustomModel() != null) {
stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(List.of(loot.getCustomModel().floatValue()), List.of(), List.of(), List.of()));
}
if (stack.getMaxDamage() > 0) {
int max = stack.getMaxDamage();
int damage = (int) Math.round(Math.max(0, Math.min(max, (1D - rng.d(loot.getMinDurability(), loot.getMaxDurability())) * max)));
stack.set(DataComponents.DAMAGE, damage);
}
if (loot.getLeatherColor() != null) {
try {
int rgb = Integer.decode(loot.getLeatherColor()) & 0xFFFFFF;
stack.set(DataComponents.DYED_COLOR, new DyedItemColor(rgb));
} catch (NumberFormatException e) {
warnOnce("leatherColor:" + loot.getLeatherColor(), "Iris loot: invalid leatherColor '" + loot.getLeatherColor() + "'");
}
}
String dye = readString(DYE_COLOR_FIELD, loot);
if (dye != null) {
warnOnce("dyeColor", "Iris loot: dyeColor is not supported on modded; skipping");
}
if (loot.getDisplayName() != null) {
stack.set(DataComponents.CUSTOM_NAME, Component.literal(colorize(loot.getDisplayName())));
}
applyLore(loot, stack);
}
private static void applyEnchantments(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
if (loot.getEnchantments().isEmpty()) {
return;
}
Registry<Enchantment> registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
DataComponentType<ItemEnchantments> component = stack.is(Items.ENCHANTED_BOOK) ? DataComponents.STORED_ENCHANTMENTS : DataComponents.ENCHANTMENTS;
ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(stack.getOrDefault(component, ItemEnchantments.EMPTY));
boolean changed = false;
for (IrisEnchantment enchantment : loot.getEnchantments()) {
String name = enchantment.getEnchantment();
if (name == null || name.isBlank()) {
continue;
}
String key = name.toLowerCase(Locale.ROOT);
Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key);
Optional<Holder.Reference<Enchantment>> holder = id == null ? Optional.empty() : registry.get(id);
if (holder.isEmpty()) {
warnOnce("enchantment:" + key, "Iris loot: unknown enchantment '" + name + "'");
continue;
}
if (rng.nextDouble() < enchantment.getChance()) {
mutable.set(holder.get(), enchantment.getLevel(rng));
changed = true;
}
}
if (changed) {
stack.set(component, mutable.toImmutable());
}
}
private static void consumeAttributeDraws(IrisLoot loot, RNG rng) {
if (loot.getAttributes().isEmpty()) {
return;
}
warnOnce("attributes", "Iris loot: attribute modifiers are not supported on modded; skipping");
for (IrisAttributeModifier attribute : loot.getAttributes()) {
if (rng.nextDouble() < attribute.getChance()) {
attribute.getAmount(rng);
}
}
}
private static void applyLore(IrisLoot loot, ItemStack stack) {
if (loot.getLore().isEmpty()) {
return;
}
List<Component> lines = new ArrayList<>();
for (String line : loot.getLore()) {
String colored = colorize(line);
if (colored.length() > 24) {
for (String wrapped : Form.wrapWords(colored, 24).split("\\Q\n\\E")) {
lines.add(Component.literal(wrapped.trim()));
}
} else {
lines.add(Component.literal(colored));
}
}
stack.set(DataComponents.LORE, new ItemLore(lines));
}
private static void applyCustomNbt(ItemStack stack, KMap<String, Object> customNbt) {
if (customNbt == null || customNbt.isEmpty()) {
return;
}
try {
CompoundTag tag = TagParser.parseCompoundFully(new JSONObject(customNbt).toString());
tag.merge(stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag());
stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag));
} catch (CommandSyntaxException e) {
warnOnce("customNbt:" + e.getMessage(), "Iris loot: invalid customNbt: " + e.getMessage());
}
}
private static String colorize(String text) {
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) {
chars[i] = '§';
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
}
}
return new String(chars);
}
private static void warnOnce(String key, String message) {
if (WARNED.add(key)) {
IrisLogging.warn(message);
}
}
private static Field lootField(String name) {
try {
Field field = IrisLoot.class.getDeclaredField(name);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
throw new IllegalStateException("IrisLoot field missing: " + name, e);
}
}
private static String readString(Field field, IrisLoot loot) {
try {
Object value = field.get(loot);
return value == null ? null : value.toString();
} catch (IllegalAccessException e) {
return null;
}
}
}
@@ -32,6 +32,8 @@ public interface ModdedLoader {
MinecraftServer currentServer();
boolean clientEnvironment();
Path configDir();
File modJar();
@@ -0,0 +1,291 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.engine.object.IObjectLoot;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectLoot;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectVanillaLoot;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import java.util.ArrayList;
import java.util.List;
public final class ModdedLootApplier {
private ModdedLootApplier() {
}
private record Candidate(IrisLootTable table, Identifier vanillaId, int weight) {
}
private record Resolution(KList<IrisLootTable> tables, Identifier vanillaTable) {
boolean isEmpty() {
return tables.isEmpty() && vanillaTable == null;
}
}
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
Resolution resolution = resolveTables(engine, rng, pos, state, mc);
if (resolution.isEmpty()) {
return;
}
if (resolution.vanillaTable() != null) {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity instanceof RandomizableContainerBlockEntity randomizable) {
randomizable.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, resolution.vanillaTable()), rng.nextLong());
} else {
IrisLogging.debug("Iris loot: no randomizable container at " + pos + " for vanilla table " + resolution.vanillaTable());
}
}
if (resolution.tables().isEmpty()) {
return;
}
Container container = resolveContainer(level, pos, state);
if (container == null) {
IrisLogging.debug("Iris loot: no container at " + pos);
return;
}
KList<ItemStack> items = new KList<>();
for (IrisLootTable table : resolution.tables()) {
if (table == null) {
continue;
}
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
}
for (ItemStack item : items) {
addItem(container, item);
}
scramble(container, rng);
container.setChanged();
}
private static Resolution resolveTables(Engine engine, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
int rx = pos.getX();
int rz = pos.getZ();
int ry = pos.getY() - engine.getWorld().minHeight();
double he = engine.getComplex().getHeightStream().get(rx, rz);
KList<IrisLootTable> tables = new KList<>();
Identifier vanillaTable = null;
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
Candidate picked = pickPlacementTable(engine, po.getPlacement(), state, rng);
if (picked != null) {
if (picked.table() != null) {
tables.add(picked.table());
} else {
vanillaTable = picked.vanillaId();
}
if (po.getPlacement().isOverrideGlobalLoot()) {
return new Resolution(tables, vanillaTable);
}
}
}
IrisRegion region = engine.getComplex().getRegionStream().get(rx, rz);
IrisBiome biomeSurface = engine.getComplex().getTrueBiomeStream().get(rx, rz);
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
boolean fallback = tables.isEmpty() && vanillaTable == null;
engine.injectTables(tables, engine.getDimension().getLoot(), fallback);
engine.injectTables(tables, region.getLoot(), fallback);
engine.injectTables(tables, biomeSurface.getLoot(), fallback);
engine.injectTables(tables, biomeUnder.getLoot(), fallback);
if (tables.isNotEmpty()) {
int target = (int) Math.round(tables.size() * multiplier);
while (tables.size() < target && tables.isNotEmpty()) {
tables.add(tables.get(rng.i(tables.size() - 1)));
}
while (tables.size() > target && tables.isNotEmpty()) {
tables.remove(rng.i(tables.size() - 1));
}
}
return new Resolution(tables, vanillaTable);
}
private static Candidate pickPlacementTable(Engine engine, IrisObjectPlacement placement, BlockState state, RNG rng) {
List<Candidate> exact = new ArrayList<>();
List<Candidate> basic = new ArrayList<>();
List<Candidate> global = new ArrayList<>();
for (IrisObjectLoot loot : placement.getLoot()) {
if (loot == null) {
continue;
}
IrisLootTable table = engine.getData().getLootLoader().load(loot.getName());
if (table == null) {
IrisLogging.warn("Couldn't find loot table " + loot.getName());
continue;
}
bucket(engine, loot, new Candidate(table, null, loot.getWeight()), state, exact, basic, global);
}
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
if (loot == null) {
continue;
}
Identifier id = loot.getName() == null ? null : Identifier.tryParse(loot.getName());
if (id == null) {
IrisLogging.warn("Couldn't parse vanilla loot table " + loot.getName());
continue;
}
bucket(engine, loot, new Candidate(null, id, loot.getWeight()), state, exact, basic, global);
}
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
return pickWeighted(pool, rng);
}
private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List<Candidate> exact, List<Candidate> basic, List<Candidate> global) {
if (loot.getFilter().isEmpty()) {
global.add(candidate);
return;
}
for (PlatformBlockState filterState : loot.getFilter(engine.getData())) {
BlockState filter = (BlockState) filterState.nativeHandle();
if (loot.isExact() ? filter == state : filter.getBlock() == state.getBlock()) {
(loot.isExact() ? exact : basic).add(candidate);
return;
}
}
}
private static Candidate pickWeighted(List<Candidate> pool, RNG rng) {
if (pool.isEmpty()) {
return null;
}
int total = 0;
for (Candidate candidate : pool) {
total += Math.max(0, candidate.weight());
}
if (total <= 0) {
return pool.get(rng.nextInt(pool.size()));
}
int pull = rng.nextInt(total);
for (Candidate candidate : pool) {
pull -= Math.max(0, candidate.weight());
if (pull < 0) {
return candidate;
}
}
return pool.get(pool.size() - 1);
}
private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) {
if (state.getBlock() instanceof ChestBlock chestBlock) {
Container combined = ChestBlock.getContainer(chestBlock, state, level, pos, true);
if (combined != null) {
return combined;
}
}
BlockEntity blockEntity = level.getBlockEntity(pos);
return blockEntity instanceof Container container ? container : null;
}
private static void addItem(Container container, ItemStack stack) {
if (stack == null || stack.isEmpty()) {
return;
}
for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) {
ItemStack existing = container.getItem(i);
if (existing.isEmpty()) {
container.setItem(i, stack);
return;
}
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount());
existing.grow(move);
stack.shrink(move);
}
}
}
private static void scramble(Container container, RNG rng) {
int size = container.getContainerSize();
ItemStack[] items = new ItemStack[size];
for (int i = 0; i < size; i++) {
items[i] = container.getItem(i);
}
boolean packedFull = false;
splitting:
for (int i = 0; i < items.length; i++) {
ItemStack is = items[i];
if (!is.isEmpty() && is.getCount() > 1 && !packedFull) {
for (int j = 0; j < items.length; j++) {
if (items[j].isEmpty()) {
int take = rng.nextInt(is.getCount());
take = take == 0 ? 1 : take;
is.setCount(is.getCount() - take);
items[j] = is.copyWithCount(take);
continue splitting;
}
}
packedFull = true;
}
}
for (int i = items.length; i > 1; i--) {
int j = rng.nextInt(i);
ItemStack tmp = items[i - 1];
items[i - 1] = items[j];
items[j] = tmp;
}
for (int i = 0; i < size; i++) {
container.setItem(i, items[i]);
}
}
}
@@ -18,25 +18,15 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.pack.PackDownloader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class ModdedPackInstaller {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
@@ -56,87 +46,13 @@ public final class ModdedPackInstaller {
return false;
}
Path target = configDir.resolve("irisworldgen").resolve("packs").resolve(pack);
String url = "https://codeload.github.com/IrisDimensions/" + pack + "/zip/refs/heads/" + branch;
File packs = configDir.resolve("irisworldgen").resolve("packs").toFile();
try {
Path work = Files.createTempDirectory("iris-pack-dl");
Path zip = work.resolve(pack + ".zip");
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(30))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofMinutes(5))
.GET()
.build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
feedback.accept("Pack download failed: HTTP " + response.statusCode() + " from " + url);
return false;
}
try (InputStream in = response.body()) {
Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING);
}
Path extracted = work.resolve("extracted");
ZipUtil.unpack(zip.toFile(), extracted.toFile());
Path root = singleRoot(extracted);
Files.createDirectories(target.getParent());
deleteRecursively(target);
copyRecursively(root, target);
deleteRecursively(work);
feedback.accept("Iris installed pack '" + pack + "' (branch " + branch + ") into " + target);
return true;
} catch (IOException | InterruptedException error) {
return PackDownloader.download(packs, "IrisDimensions/" + pack, branch, true, false, feedback) != null;
} catch (IOException error) {
LOGGER.error("Iris pack download failed for IrisDimensions/{} ({})", pack, branch, error);
feedback.accept("Pack download failed: " + error.getClass().getSimpleName() + (error.getMessage() == null ? "" : " - " + error.getMessage()));
if (error instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return false;
}
}
private static Path singleRoot(Path extracted) throws IOException {
try (Stream<Path> entries = Files.list(extracted)) {
List<Path> children = entries.toList();
if (children.size() == 1 && Files.isDirectory(children.get(0))) {
return children.get(0);
}
return extracted;
}
}
private static void copyRecursively(Path source, Path target) throws IOException {
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.toList()) {
Path destination = target.resolve(source.relativize(path).toString());
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
private static void deleteRecursively(Path root) throws IOException {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}
@@ -21,21 +21,16 @@ package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformCapabilities;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
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.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import java.io.File;
import java.util.function.Consumer;
@@ -46,7 +41,6 @@ public final class ModdedPlatform implements IrisPlatform {
private final ModdedLoader loader;
private final ModdedRegistries registries;
private final ModdedScheduler scheduler;
private final ModdedCapabilities capabilities;
private final ModdedStructureHooks structureHooks;
private final ModdedBiomeWriter biomeWriter;
@@ -54,7 +48,6 @@ public final class ModdedPlatform implements IrisPlatform {
this.loader = loader;
this.registries = new ModdedRegistries(loader::currentServer);
this.scheduler = new ModdedScheduler();
this.capabilities = new ModdedCapabilities();
this.structureHooks = new ModdedStructureHooks(loader::currentServer);
this.biomeWriter = new ModdedBiomeWriter(loader::currentServer);
}
@@ -91,11 +84,6 @@ public final class ModdedPlatform implements IrisPlatform {
return scheduler;
}
@Override
public PlatformCapabilities capabilities() {
return capabilities;
}
@Override
public PlatformStructureHooks structureHooks() {
return structureHooks;
@@ -159,20 +147,6 @@ public final class ModdedPlatform implements IrisPlatform {
return entity != null;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
if (!(player instanceof ServerPlayer serverPlayer) || itemKey == null || amount <= 0) {
return false;
}
PlatformItem resolved = registries.item(itemKey);
if (resolved == null) {
return false;
}
Item item = (Item) resolved.nativeHandle();
ItemStack stack = new ItemStack(item, amount);
return serverPlayer.getInventory().add(stack);
}
@Override
public void log(LogLevel level, String message) {
ModdedIrisLog.log(level, message);
@@ -194,26 +168,4 @@ public final class ModdedPlatform implements IrisPlatform {
ModdedIrisLog.error("Iris reported error", error);
}
}
private static final class ModdedCapabilities implements PlatformCapabilities {
@Override
public boolean customBiomes() {
return true;
}
@Override
public boolean dataPacks() {
return true;
}
@Override
public boolean structurePlacement() {
return true;
}
@Override
public boolean regionizedThreading() {
return false;
}
}
}
@@ -114,10 +114,11 @@ public final class ModdedScheduler implements PlatformScheduler {
laterGlobal(task, ticks);
}
public void shutdown() {
asyncExecutor.shutdownNow();
public void reset() {
asyncExecutor.getQueue().clear();
mainQueue.clear();
delayedQueue.clear();
mainThread = null;
}
private void drain() {
@@ -77,7 +77,6 @@ public final class ModdedServiceManager {
}
enabled = false;
forEachReversed(this::disableService);
services.clear();
}
private void enableService(ModdedService service) {
@@ -18,6 +18,10 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.modded.command.ModdedPackCommands;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,10 +34,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStartup {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private static final Object PACK_LOCK = new Object();
private ModdedStartup() {
}
public static void reset() {
STARTED.set(false);
}
public static void prefetchDefaultPack() {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler != null) {
scheduler.async(ModdedStartup::ensureDefaultPack);
return;
}
Thread thread = new Thread(ModdedStartup::ensureDefaultPack, "iris-modded-pack-prefetch");
thread.setDaemon(true);
thread.start();
}
public static void runOnce(MinecraftServer server) {
if (server == null || !STARTED.compareAndSet(false, true)) {
return;
@@ -42,10 +62,44 @@ public final class ModdedStartup {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
validateAllPacks();
ensureDefaultPack();
return;
}
scheduler.async(ModdedStartup::ensureDefaultPack);
scheduler.async(() -> {
validateAllPacks();
ensureDefaultPack();
});
}
public static void validateAllPacks() {
File packsRoot = ModdedPackCommands.packsRoot();
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
LOGGER.error("Iris pack '{}' FAILED validation - world/studio creation will be refused. Reasons:", result.getPackName());
for (String reason : result.getBlockingErrors()) {
LOGGER.error(" - {}", reason);
}
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
LOGGER.info("Iris pack '{}' validated ({} unused file(s) quarantined to .iris-trash/, {} warning(s)).", result.getPackName(), result.getRemovedUnusedFiles().size(), result.getWarnings().size());
for (String warning : result.getWarnings()) {
LOGGER.warn(" [{}] {}", result.getPackName(), warning);
}
} else {
LOGGER.info("Iris pack '{}' validated.", result.getPackName());
}
} catch (Throwable e) {
LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e);
}
}
}
private static void reinjectPersistentDimensions(MinecraftServer server) {
@@ -56,30 +110,32 @@ public final class ModdedStartup {
int injected = 0;
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
try {
ModdedDimensionManager.create(server, dimension.id(), dimension.packKey(), dimension.seed());
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
injected++;
} catch (Throwable e) {
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} seed={})", dimension.id(), dimension.packKey(), dimension.seed(), e);
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e);
}
}
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
}
private static void ensureDefaultPack() {
ModdedModConfig config = ModdedModConfig.get();
if (!config.autoDownloadDefaultPack()) {
return;
}
String pack = config.defaultPack();
Path configDir = ModdedEngineBootstrap.loader().configDir();
File packFolder = configDir.resolve("irisworldgen").resolve("packs").resolve(pack).toFile();
if (new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
return;
}
LOGGER.info("Iris default pack '{}' missing; downloading IrisDimensions/{} (master)", pack, pack);
boolean installed = ModdedPackInstaller.install(configDir, pack, "master", (String line) -> LOGGER.info("Iris: {}", line));
if (!installed) {
LOGGER.warn("Iris default pack '{}' could not be downloaded; install it with /iris download {}", pack, pack);
public static void ensureDefaultPack() {
synchronized (PACK_LOCK) {
ModdedModConfig config = ModdedModConfig.get();
if (!config.autoDownloadDefaultPack()) {
return;
}
String pack = config.defaultPack();
Path configDir = ModdedEngineBootstrap.loader().configDir();
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
return;
}
LOGGER.info("Iris default pack '{}' missing; downloading IrisDimensions/{} (master)", pack, pack);
boolean installed = ModdedPackInstaller.install(configDir, pack, "master", (String line) -> LOGGER.info("Iris: {}", line));
if (!installed) {
LOGGER.warn("Iris default pack '{}' could not be downloaded; install it with /iris download {}", pack, pack);
}
}
}
}
@@ -43,12 +43,12 @@ public final class ModdedWorldEngines {
private ModdedWorldEngines() {
}
public static Engine get(ServerLevel level, String dimensionKey, long seedOverride) {
public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
Engine existing = ENGINES.get(level);
if (existing != null) {
return existing;
}
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey, seedOverride));
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, pack, dimensionKey, seedOverride));
}
public static Collection<Engine> activeEngines() {
@@ -70,15 +70,15 @@ public final class ModdedWorldEngines {
}
}
private static Engine create(ServerLevel level, String dimensionKey, long seedOverride) {
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
File pack = resolvePack(dimensionKey);
IrisData data = IrisData.get(pack);
File packDir = resolvePack(pack, dimensionKey);
IrisData data = IrisData.get(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
LOGGER.error("Iris pack at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
pack.getAbsolutePath(), dimensionKey, dimensionKey);
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + pack.getAbsolutePath());
LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey);
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath());
}
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
@@ -94,24 +94,28 @@ public final class ModdedWorldEngines {
int levelMinY = level.getMinY();
int levelMaxY = level.getMinY() + level.getHeight();
if (dimension.getMinHeight() != levelMinY || dimension.getMaxHeight() != levelMaxY) {
LOGGER.error("Iris pack height mismatch for {}: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; ship a matching dimension_type.",
if (dimension.getMinHeight() < levelMinY || dimension.getMaxHeight() > levelMaxY) {
LOGGER.warn("Iris pack height range for {} exceeds the level: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; restart so the forced datapack registers the pack dimension type.",
level.dimension().identifier(), dimension.getMinHeight(), dimension.getMaxHeight(), levelMinY, levelMaxY);
}
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), pack.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
return engine;
}
private static File resolvePack(String dimensionKey) {
File pack = ModdedEngineBootstrap.loader().configDir()
public static File packFolder(String pack) {
return ModdedEngineBootstrap.loader().configDir()
.resolve("irisworldgen")
.resolve("packs")
.resolve(dimensionKey)
.resolve(pack)
.toFile();
if (pack.isDirectory()) {
return pack;
}
private static File resolvePack(String pack, String dimensionKey) {
File packDir = packFolder(pack);
if (packDir.isDirectory()) {
return packDir;
}
String parity = System.getProperty("iris.parity");
@@ -127,17 +131,17 @@ public final class ModdedWorldEngines {
}
File parityPack = new File(parityPath);
if (parityPack.isDirectory()) {
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", pack.getAbsolutePath(), parityPack.getAbsolutePath());
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", packDir.getAbsolutePath(), parityPack.getAbsolutePath());
return parityPack;
}
}
LOGGER.error("===============================================================");
LOGGER.error("Iris pack for dimension '{}' is not installed.", dimensionKey);
LOGGER.error("Expected a pack folder at: {}", pack.getAbsolutePath());
LOGGER.error("Iris pack '{}' is not installed.", pack);
LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath());
LOGGER.error("Install an Iris pack there (the folder must contain dimensions/{}.json) and restart the server.", dimensionKey);
LOGGER.error("===============================================================");
throw new IllegalStateException("Iris pack not installed: " + pack.getAbsolutePath());
throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath());
}
public static void shutdown() {
@@ -22,17 +22,10 @@ import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.command.ModdedPregenJob;
import art.arcane.iris.modded.command.ModdedPregenMode;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public final class IrisModdedAPI {
private static final Map<ServerLevel, AtomicInteger> WORLD_MAINTENANCE_DEPTH = new ConcurrentHashMap<>();
private IrisModdedAPI() {
}
@@ -64,15 +57,15 @@ public final class IrisModdedAPI {
}
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
return pregenerate(level, radiusBlocks, 0, 0, ModdedPregenMode.ASYNC, false);
return pregenerate(level, radiusBlocks, 0, 0, false, true);
}
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, ModdedPregenMode mode, boolean cached) {
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) {
Engine engine = getEngine(level);
if (engine == null) {
return false;
}
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, mode, cached);
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached);
}
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
@@ -113,32 +106,4 @@ public final class IrisModdedAPI {
public static void registerCustomBlockData(String namespace, String key, String state) {
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
}
public static void beginWorldMaintenance(ServerLevel level) {
if (level == null) {
return;
}
WORLD_MAINTENANCE_DEPTH.computeIfAbsent(level, (ServerLevel l) -> new AtomicInteger()).incrementAndGet();
}
public static void endWorldMaintenance(ServerLevel level) {
if (level == null) {
return;
}
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
if (counter == null) {
return;
}
if (counter.decrementAndGet() <= 0) {
WORLD_MAINTENANCE_DEPTH.remove(level);
}
}
public static boolean isWorldMaintenanceActive(ServerLevel level) {
if (level == null) {
return false;
}
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
return counter != null && counter.get() > 0;
}
}
@@ -18,6 +18,9 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
@@ -27,6 +30,7 @@ import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller;
@@ -39,8 +43,11 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
@@ -48,14 +55,18 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
@@ -64,10 +75,10 @@ import net.minecraft.world.phys.HitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@@ -118,10 +129,29 @@ public final class IrisModdedCommands {
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource()))
.then(Commands.literal("block")
.executes((CommandContext<CommandSourceStack> context) -> whatBlock(context.getSource())))
.then(Commands.literal("hand")
.executes((CommandContext<CommandSourceStack> context) -> whatHand(context.getSource())))
.then(Commands.literal("markers")
.then(Commands.argument("marker", StringArgumentType.greedyString()).suggests(MARKER_TYPES)
.executes((CommandContext<CommandSourceStack> context) -> whatMarkers(context.getSource(), StringArgumentType.getString(context, "marker"))))));
root.then(Commands.literal("tp").requires(GATE)
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null))
.then(Commands.argument("player", EntityArgument.player())
.executes((CommandContext<CommandSourceStack> context) -> tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), EntityArgument.getPlayer(context, "player"))))));
root.then(Commands.literal("evacuate").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> evacuate(context.getSource(), null))
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension")))));
root.then(Commands.literal("debug").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> debug(context.getSource())));
root.then(Commands.literal("reload").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> reload(context.getSource())));
root.then(gotoTree("goto"));
root.then(gotoTree("find"));
@@ -171,7 +201,7 @@ public final class IrisModdedCommands {
private static LiteralArgumentBuilder<CommandSourceStack> createTree() {
return Commands.literal("create").requires(GATE)
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES)
.then(Commands.argument("pack", StringArgumentType.string()).suggests(PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
@@ -231,27 +261,20 @@ public final class IrisModdedCommands {
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree(String name) {
RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, false, false, false, false, false));
attachPregenCenter(radius, false);
attachPregenFlags(radius, false, false, false, false, false);
RequiredArgumentBuilder<CommandSourceStack, Identifier> dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, true, false, false, false, false));
attachPregenCenter(dimension, true);
attachPregenFlags(dimension, true, false, false, false, false);
radius.then(dimension);
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
.then(Commands.literal("start")
.then(Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 0, 0, false, ModdedPregenMode.ASYNC, false))
.then(pregenStartFlag("gui", false, true, ModdedPregenMode.ASYNC, false))
.then(pregenStartFlag("sync", false, false, ModdedPregenMode.SYNC, false))
.then(pregenStartFlag("cached", false, false, ModdedPregenMode.SYNC, true))
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(Commands.argument("z", IntegerArgumentType.integer())
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
IntegerArgumentType.getInteger(context, "radius"),
IntegerArgumentType.getInteger(context, "x"),
IntegerArgumentType.getInteger(context, "z"), false, ModdedPregenMode.ASYNC, false))
.then(pregenStartFlag("gui", true, true, ModdedPregenMode.ASYNC, false))
.then(pregenStartFlag("sync", true, false, ModdedPregenMode.SYNC, false))
.then(pregenStartFlag("cached", true, false, ModdedPregenMode.SYNC, true))))
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
IntegerArgumentType.getInteger(context, "radius"), 0, 0, false, ModdedPregenMode.ASYNC, false,
StringArgumentType.getString(context, "dimension"))))))
.then(radius))
.then(Commands.literal("stop")
.executes((CommandContext<CommandSourceStack> context) -> pregenStop(context.getSource())))
.then(Commands.literal("x")
@@ -264,12 +287,32 @@ public final class IrisModdedCommands {
.executes((CommandContext<CommandSourceStack> context) -> pregenStatus(context.getSource())));
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenStartFlag(String name, boolean withCenter, boolean gui, ModdedPregenMode mode, boolean cached) {
return Commands.literal(name).executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
IntegerArgumentType.getInteger(context, "radius"),
withCenter ? IntegerArgumentType.getInteger(context, "x") : 0,
withCenter ? IntegerArgumentType.getInteger(context, "z") : 0,
gui, mode, cached));
private static void attachPregenCenter(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension) {
RequiredArgumentBuilder<CommandSourceStack, Integer> z = Commands.argument("z", IntegerArgumentType.integer())
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, withDimension, true, false, false, false));
attachPregenFlags(z, withDimension, true, false, false, false);
node.then(Commands.literal("at")
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(z)));
}
private static void attachPregenFlags(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
if (!gui) {
node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache));
}
if (!sync) {
node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache));
}
if (!nocache) {
node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true));
}
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
LiteralArgumentBuilder<CommandSourceStack> flag = Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, withDimension, withCenter, gui, sync, nocache));
attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache);
return flag;
}
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
@@ -302,21 +345,176 @@ public final class IrisModdedCommands {
}
private static LiteralArgumentBuilder<CommandSourceStack> editTree() {
String message = "/iris edit opens pack JSON files in a desktop editor through the Bukkit studio toolchain; "
+ "on modded servers edit the pack files directly under config/irisworldgen/packs/<pack>/.";
LiteralArgumentBuilder<CommandSourceStack> node = Commands.literal("edit").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> {
fail(context.getSource(), message);
return 0;
});
for (String child : new String[]{"biome", "region", "dimension"}) {
node.then(Commands.literal(child)
.executes((CommandContext<CommandSourceStack> context) -> {
fail(context.getSource(), message);
return 0;
}));
return Commands.literal("edit").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "edit"))
.then(Commands.literal("biome")
.executes((CommandContext<CommandSourceStack> context) -> editBiome(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.executes((CommandContext<CommandSourceStack> context) -> editRegion(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("dimension")
.executes((CommandContext<CommandSourceStack> context) -> editDimension(context.getSource())));
}
private static int editBiome(CommandSourceStack source, String key) {
Engine engine = engineFor(source.getLevel());
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
return node;
IrisBiome biome;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "Console must name a biome: /iris edit biome <key>");
return 0;
}
BlockPos pos = player.blockPosition();
try {
biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) {
fail(source, "Biome lookup failed: " + e.getClass().getSimpleName());
return 0;
}
} else {
biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
fail(source, "Unknown biome: " + key);
return 0;
}
}
return openJson(source, biome);
}
private static int editRegion(CommandSourceStack source, String key) {
Engine engine = engineFor(source.getLevel());
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
IrisRegion region;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "Console must name a region: /iris edit region <key>");
return 0;
}
BlockPos pos = player.blockPosition();
try {
region = engine.getRegion(pos.getX(), pos.getZ());
} catch (Throwable e) {
fail(source, "Region lookup failed: " + e.getClass().getSimpleName());
return 0;
}
} else {
region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
fail(source, "Unknown region: " + key);
return 0;
}
}
return openJson(source, region);
}
private static int editDimension(CommandSourceStack source) {
Engine engine = engineFor(source.getLevel());
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
return openJson(source, engine.getDimension());
}
private static int openJson(CommandSourceStack source, IrisRegistrant registrant) {
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
fail(source, "Cannot open files here: " + ModdedGuiHost.guiUnavailableReason());
return 0;
}
if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) {
fail(source, "Cannot find the file; perhaps it was not loaded directly from a file?");
return 0;
}
File file = registrant.getLoadFile();
try {
Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOGGER.error("Iris edit failed to open {}", file, e);
fail(source, "Could not open " + file.getName() + ": " + e.getClass().getSimpleName());
return 0;
}
ok(source, "Opening " + registrant.getTypeName() + " " + file.getName() + " in your editor.");
return 1;
}
private static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) {
ServerPlayer player = target != null ? target : source.getPlayer();
if (player == null) {
fail(source, "Console must name a player: /iris tp <dimension> <player>");
return 0;
}
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
fail(source, level.dimension().identifier() + " is not generated by Iris.");
return 0;
}
String dimensionId = level.dimension().identifier().toString();
if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) {
fail(source, "Teleport failed: dimension " + dimensionId + " is not loaded.");
return 0;
}
ok(source, "Teleporting " + player.getScoreboardName() + " to " + dimensionId + "...");
return 1;
}
private static int evacuate(CommandSourceStack source, ServerLevel target) {
MinecraftServer server = source.getServer();
ServerLevel level = target != null ? target : source.getLevel();
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
fail(source, level.dimension().identifier() + " is not generated by Iris.");
return 0;
}
ServerLevel fallback = server.overworld();
if (fallback == level) {
fail(source, "Cannot evacuate the primary world; there is nowhere to send players.");
return 0;
}
int count = ModdedDimensionManager.evacuate(server, level);
ok(source, "Evacuated " + count + " player(s) from " + level.dimension().identifier() + " to " + fallback.dimension().identifier() + ".");
return 1;
}
private static int debug(CommandSourceStack source) {
boolean to = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
ok(source, "Set debug to: " + to);
return 1;
}
private static int reload(CommandSourceStack source) {
if (IrisSettings.settings != null) {
IrisSettings.invalidate();
}
IrisSettings.get();
ok(source, "Hotloaded settings");
return 1;
}
private static int whatHand(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players (it inspects your held item).");
return 0;
}
ItemStack stack = player.getMainHandItem();
if (stack.isEmpty()) {
fail(source, "Your main hand is empty.");
return 0;
}
ok(source, "Hand: " + BuiltInRegistries.ITEM.getKey(stack.getItem()) + " x" + stack.getCount());
return 1;
}
private static int regen(CommandSourceStack source, int radius) {
@@ -339,28 +537,23 @@ public final class IrisModdedCommands {
return 1;
}
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached) {
return pregenStart(source, radius, centerX, centerZ, gui, mode, cached, null);
}
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached, String dimension) {
ServerLevel level;
if (dimension == null || dimension.isBlank()) {
level = source.getLevel();
} else {
level = resolveIrisLevel(source.getServer(), dimension);
if (level == null) {
fail(source, "No loaded Iris dimension matches '" + dimension + "'. Use a bare name (irisworld) or full id (irisworldgen:irisworld); see /iris info for loaded dimensions.");
return 0;
}
}
private static int pregenStart(CommandContext<CommandSourceStack> context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
int radius = IntegerArgumentType.getInteger(context, "radius");
int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0;
int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0;
ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
if (withDimension) {
fail(source, level.dimension().identifier() + " is not generated by Iris; see /iris info for loaded Iris dimensions.");
} else {
fail(source, "The current dimension (" + level.dimension().identifier() + ") is not generated by Iris. Name one explicitly: /iris pregen start " + radius + " <dimension>; see /iris info for loaded Iris dimensions.");
}
return 0;
}
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, mode, cached)) {
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
fail(source, "A pregeneration task is already running. Stop it first with /iris pregen stop.");
return 0;
}
@@ -372,7 +565,7 @@ public final class IrisModdedCommands {
} else {
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
}
String modeNote = mode == ModdedPregenMode.SYNC ? " Mode: sync" + (cached ? " (checkpoint cache resumable)." : ".") : "";
String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache).");
ok(source, "Pregen started in " + level.dimension().identifier() + " of " + (radius * 2) + " by " + (radius * 2)
+ " blocks from " + centerX + "," + centerZ + "." + modeNote + " Progress logs to console; see /iris pregen status." + guiNote);
return 1;
@@ -786,14 +979,15 @@ public final class IrisModdedCommands {
private static int download(CommandSourceStack source, String pack, String branch) {
MinecraftServer server = source.getServer();
ok(source, "Downloading IrisDimensions/" + pack + " (branch " + branch + ")...");
String effectiveBranch = pack.equals("overworld") ? "master" : branch;
ok(source, "Downloading IrisDimensions/" + pack + " (branch " + effectiveBranch + ")...");
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch,
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, effectiveBranch,
(String message) -> server.execute(() -> ok(source, message)));
if (installed) {
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Restart or create a new world to use it."));
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its dimension types and custom biomes join the forced Iris datapack on the next server restart; worlds created before restarting run with fallback heights until then."));
} else {
server.execute(() -> fail(source, "Pack download failed for " + pack + "/" + branch + " (see console)."));
server.execute(() -> fail(source, "Pack download failed for " + pack + "/" + effectiveBranch + " (see console)."));
}
}, "Iris Pack Download");
thread.setDaemon(true);
@@ -843,29 +1037,6 @@ public final class IrisModdedCommands {
return count;
}
private static ServerLevel resolveIrisLevel(MinecraftServer server, String raw) {
String target = raw.trim().toLowerCase(Locale.ROOT);
boolean qualified = target.indexOf(':') >= 0;
for (ServerLevel level : server.getAllLevels()) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
continue;
}
String fullId = level.dimension().identifier().toString();
if (qualified) {
if (fullId.equals(target)) {
return level;
}
continue;
}
int separator = fullId.indexOf(':');
String path = separator >= 0 ? fullId.substring(separator + 1) : fullId;
if (path.equals(target)) {
return level;
}
}
return null;
}
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
@@ -52,18 +52,24 @@ final class ModdedCommandHelp {
SECTIONS.put("", List.of(
Entry.command("version", "", "Print version information"),
Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"),
Entry.command("what", "", "Inspect the Iris biome, region, cave biome, surface and chunk at your position"),
Entry.command("what", "[block|hand|markers]", "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers"),
Entry.group("find", "Find and teleport to Iris biomes, regions, objects, structures and points of interest", "goto"),
Entry.command("tp", "<dimension> [player]", "Teleport yourself or a named player into a loaded Iris dimension"),
Entry.command("evacuate", "[dimension]", "Teleport every player out of an Iris dimension to the primary world spawn"),
Entry.command("seed", "", "Print world and engine seed information"),
Entry.command("debug", "", "Toggle Iris debug logging and save settings.json"),
Entry.command("reload", "", "Reload settings.json (also hotloaded automatically every 3s)"),
Entry.command("download", "<pack> [branch]", "Download a pack project", "dl"),
Entry.command("metrics", "", "Print generation metrics for your current Iris dimension", "measure"),
Entry.command("regen", "[radius]", "Delete and regenerate nearby chunks in place", "rg"),
Entry.group("pregen", "Pregenerate an Iris dimension", "pregenerate"),
Entry.command("wand", "", "Get an Iris object wand"),
Entry.group("object", "Object wand, save, paste, analyze and undo tools", "o"),
Entry.group("edit", "Open pack biome, region and dimension json files in your desktop editor"),
Entry.command("create", "<name> <pack|pack:dimensionKey> [seed]", "Create and inject a persistent Iris dimension; quote pack:dimensionKey to pick a specific pack dimension"),
Entry.group("studio", "Pack project creation, packaging and reports", "std", "s"),
Entry.group("pack", "Pack validation and maintenance", "pk"),
Entry.group("world", "Explicit Iris dimension enablement and removal", "w"),
Entry.group("world", "Runtime Iris dimension creation, removal and status", "w"),
Entry.group("datapack", "World datapack install and status helpers", "datapacks", "dp"),
Entry.group("structure", "Iris structure index, info and placement tools", "struct", "str"),
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold")
@@ -76,8 +82,13 @@ final class ModdedCommandHelp {
Entry.command("poi", "<type>", "Find a supported point of interest")
));
SECTIONS.put("goto", SECTIONS.get("find"));
SECTIONS.put("edit", List.of(
Entry.command("biome", "[key]", "Open a biome json in your desktop editor; no key opens the biome at your position"),
Entry.command("region", "[key]", "Open a region json in your desktop editor; no key opens the region at your position"),
Entry.command("dimension", "", "Open the current pack's dimension json in your desktop editor")
));
SECTIONS.put("pregen", List.of(
Entry.command("start", "<radius> [x] [z]", "Start pregeneration"),
Entry.command("start", "<radius> [dimension] [at] [x] [z] [gui] [sync] [nocache]", "Start pregeneration; radius in blocks, resumable checkpoint cache on by default, center via 'at <x> <z>', flags compose in any order"),
Entry.command("stop", "", "Stop the active pregeneration task", "x"),
Entry.command("pause", "", "Pause or resume pregeneration", "resume"),
Entry.command("status", "", "Show pregeneration status")
@@ -86,15 +97,15 @@ final class ModdedCommandHelp {
SECTIONS.put("object", List.of(
Entry.command("wand", "", "Get an Iris object wand"),
Entry.command("dust", "", "Get dust that reveals object placements", "d"),
Entry.command("save", "<name>", "Save the selected wand volume as an object"),
Entry.command("paste", "<key>", "Paste an object at your position"),
Entry.command("save", "[overwrite] <name>", "Save the selected wand volume as an object"),
Entry.command("paste", "[at] [x] [y] [z] [rotate] [degrees] <key>", "Paste an object at your position or a given position, optionally rotated"),
Entry.command("expand", "[amount]", "Expand the wand selection in your looking direction"),
Entry.command("contract", "[amount]", "Contract the wand selection in your looking direction", "-"),
Entry.command("shift", "[amount]", "Shift the wand selection in your looking direction"),
Entry.command("position1", "[look]", "Set selection point 1", "p1"),
Entry.command("position2", "[look]", "Set selection point 2", "p2"),
Entry.command("x+y", "", "Autoselect up and out"),
Entry.command("x&y", "", "Autoselect up, down and out"),
Entry.command("x+y", "", "Autoselect up and out", "xpy"),
Entry.command("x&y", "", "Autoselect up, down and out", "xay"),
Entry.command("analyze", "<key>", "Show object composition"),
Entry.command("shrink", "<key>", "Shrink an object to its minimum size"),
Entry.command("undo", "[amount]", "Undo pasted objects", "u")
@@ -105,10 +116,14 @@ final class ModdedCommandHelp {
Entry.command("package", "[pack]", "Package a dimension into a compressed format"),
Entry.command("version", "[pack]", "Print a pack version"),
Entry.command("regions", "[radius]", "Calculate nearby region distribution"),
Entry.command("open", "<pack> [seed]", "Open or prepare a dimension pack studio workflow"),
Entry.command("close", "", "Explain modded studio workflow"),
Entry.command("vscode", "", "Explain editor workflow"),
Entry.command("update", "", "Explain workspace regeneration workflow"),
Entry.command("open", "<pack> [seed]", "Open a temporary studio dimension for a pack", "o"),
Entry.command("close", "", "Close the open studio dimension and discard its world", "x"),
Entry.command("tpstudio", "", "Teleport into the open studio dimension", "stp"),
Entry.command("status", "", "Show the open studio dimension and its pack"),
Entry.command("noise", "[generator] [seed]", "Open the Noise Explorer GUI on the server display", "nmap"),
Entry.command("map", "", "Open the Vision map GUI on the server display", "render"),
Entry.command("vscode", "[pack]", "Regenerate the .code-workspace for a pack and open it in your desktop editor", "vsc"),
Entry.command("update", "[pack]", "Regenerate the .code-workspace for a pack"),
Entry.command("importvanilla", "", "Explain vanilla import workflow", "importv", "iv")
));
SECTIONS.put("std", SECTIONS.get("studio"));
@@ -120,11 +135,12 @@ final class ModdedCommandHelp {
));
SECTIONS.put("pk", SECTIONS.get("pack"));
SECTIONS.put("world", List.of(
Entry.command("enable", "<dimension> <pack> [packDimension]", "Create an Iris dimension in world/datapacks/iris", "create"),
Entry.command("replace-overworld", "<pack> [packDimension]", "Explicitly make minecraft:overworld use an Iris pack"),
Entry.command("disable", "<dimension>", "Remove an Iris dimension definition from the world datapack", "remove", "rm"),
Entry.command("list", "", "List Iris dimensions staged in the world datapack", "ls"),
Entry.command("status", "", "Show staged and currently loaded Iris dimensions")
Entry.command("enable", "<dimension> <pack|pack:dimensionKey> [seed|random]", "Create and inject a persistent Iris dimension at runtime; downloads the pack if missing, quote pack:dimensionKey to pick a specific pack dimension", "create"),
Entry.command("replace-overworld", "<pack|pack:dimensionKey> [seed|random]", "Inject an Iris primary world and route players there instead of the vanilla overworld"),
Entry.command("disable", "<dimension>", "Evacuate and unload an Iris dimension; world data on disk is kept for re-enabling"),
Entry.command("delete", "<dimension>", "Disable an Iris dimension and wipe its chunk and mantle data from disk", "remove", "rm"),
Entry.command("list", "", "List loaded Iris dimensions", "ls"),
Entry.command("status", "", "Show loaded Iris dimensions and the configured primary world")
));
SECTIONS.put("w", SECTIONS.get("world"));
SECTIONS.put("datapack", List.of(
@@ -18,13 +18,13 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.runtime.GoldenHashEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.ModdedBlockBuffer;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.hunk.Hunk;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
@@ -33,21 +33,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -59,33 +44,38 @@ public final class ModdedGoldenHash {
}
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String FORMAT = "iris-goldenhash v1";
private static final int BIOME_STEP = 4;
private static final int MAX_REPORTED_MISMATCHES = 10;
private static final AtomicBoolean ACTIVE = new AtomicBoolean(false);
private final CommandSourceStack source;
private final MinecraftServer server;
private final ServerLevel level;
private final Engine engine;
private final int radius;
private final int threads;
private final Mode mode;
private final File goldenFile;
private final GoldenHashEngine hashEngine;
private ModdedGoldenHash(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) {
this.source = source;
this.server = source.getServer();
this.level = level;
this.engine = engine;
this.radius = Math.max(0, radius);
this.threads = Math.max(1, threads);
this.mode = mode;
GoldenHashEngine.Mode engineMode = switch (mode) {
case AUTO -> GoldenHashEngine.Mode.AUTO;
case CAPTURE -> GoldenHashEngine.Mode.CAPTURE;
case VERIFY -> GoldenHashEngine.Mode.VERIFY;
};
File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile();
goldenDir.mkdirs();
this.goldenFile = new File(goldenDir, engine.getDimension().getLoadKey()
+ "-s" + level.getSeed()
+ "-c0x0-r" + this.radius + ".hashes");
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
engine.getWorld().name(),
level.getSeed(),
ModdedEngineBootstrap.loader().minecraftVersion(),
engine.getMinHeight(),
engine.getMaxHeight(),
0,
0,
radius,
threads,
engineMode,
true,
false,
"minecraft:plains");
this.hashEngine = new GoldenHashEngine(engine, request, goldenDir, this::snapshot, feedback(), progress());
}
public static void start(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) {
@@ -94,16 +84,14 @@ public final class ModdedGoldenHash {
return;
}
ModdedGoldenHash scan = new ModdedGoldenHash(source, level, engine, radius, threads, mode);
int chunks = (scan.radius * 2 + 1) * (scan.radius * 2 + 1);
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + scan.threads + " mode=" + mode);
int boundedRadius = Math.max(0, radius);
int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1);
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + Math.max(1, threads) + " mode=" + mode);
LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}",
engine.getDimension().getLoadKey(), level.getSeed(), scan.radius, scan.threads, mode, scan.goldenFile.getName());
engine.getDimension().getLoadKey(), level.getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
Thread thread = new Thread(() -> {
try {
scan.run();
} catch (Throwable e) {
LOGGER.error("goldenhash failed", e);
scan.fail("GoldenHash failed: " + e);
scan.hashEngine.run();
} finally {
ACTIVE.set(false);
}
@@ -112,265 +100,77 @@ public final class ModdedGoldenHash {
thread.start();
}
private void run() throws Exception {
boolean exists = goldenFile.exists();
if (mode == Mode.VERIFY && !exists) {
fail("No golden capture at " + goldenFile.getAbsolutePath() + "; run '/iris goldenhash " + radius + " " + threads + " capture' first.");
return;
}
resetMantleFull();
List<int[]> targets = orderedTargets(0, 0, radius);
Map<Long, String> lines = scan(targets);
if (lines.size() != targets.size()) {
fail("GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
return;
}
if (mode == Mode.CAPTURE || (mode == Mode.AUTO && !exists)) {
capture(lines);
} else {
verify(lines);
}
}
private void resetMantleFull() {
try {
engine.getMantle().getMantle().saveAll();
File folder = engine.getMantle().getMantle().getDataFolder();
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
file.delete();
}
}
}
ok("Mantle reset (" + folder.getAbsolutePath() + ")");
} catch (Throwable e) {
LOGGER.error("goldenhash mantle reset failed", e);
ok("Mantle reset failed (" + e.getClass().getSimpleName() + "); continuing with existing mantle state.");
}
}
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
int stride = total <= 64 ? 1 : 32;
int height = engine.getMaxHeight() - engine.getMinHeight();
private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception {
int minY = engine.getMinHeight();
int height = engine.getMaxHeight() - minY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
return new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
return minY;
}
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
try {
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, blocks, biomes, height));
int doneCount = completed.incrementAndGet();
if (doneCount % stride == 0 || doneCount == total) {
ok("[" + doneCount + "/" + total + "] chunk " + chunkX + "," + chunkZ + " hashed");
}
} catch (Throwable e) {
LOGGER.error("goldenhash chunk {},{} failed", chunkX, chunkZ, e);
fail("Chunk " + chunkX + "," + chunkZ + " FAILED: " + e.getClass().getSimpleName());
} finally {
inFlight.release();
done.countDown();
}
});
}
@Override
public int maxY() {
return minY + height;
}
done.await();
return lines;
@Override
public PlatformBlockState block(int x, int y, int z) {
return blocks.get(x, y - minY, z);
}
@Override
public PlatformBiome biome(int x, int y, int z) {
return biomes.get(x, y - minY, z);
}
};
}
private String hashChunk(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<PlatformBiome> biomes, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
Map<PlatformBlockState, byte[]> blockCache = new HashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
byte[] plains = "minecraft:plains\n".getBytes(StandardCharsets.UTF_8);
private GoldenHashEngine.Feedback feedback() {
return new GoldenHashEngine.Feedback() {
@Override
public void ok(String message) {
ModdedGoldenHash.this.ok(message);
}
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(state, (PlatformBlockState s) -> (s.key() + "\n").getBytes(StandardCharsets.UTF_8));
blockDigest.update(bytes);
@Override
public void warn(String message) {
ModdedGoldenHash.this.ok(message);
}
@Override
public void fail(String message) {
ModdedGoldenHash.this.fail(message);
}
};
}
private GoldenHashEngine.Progress progress() {
return new GoldenHashEngine.Progress() {
private final AtomicInteger hashed = new AtomicInteger();
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
if (!ok) {
return;
}
int doneCount = hashed.incrementAndGet();
int stride = total <= 64 ? 1 : 32;
if (doneCount % stride == 0 || doneCount == total) {
ModdedGoldenHash.this.ok("[" + doneCount + "/" + total + "] chunk " + chunkX + "," + chunkZ + " hashed");
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = 0; y < height; y += BIOME_STEP) {
PlatformBiome biome = biomes.get(x, y, z);
byte[] bytes = biome == null
? plains
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8));
biomeDigest.update(bytes);
}
@Override
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
LOGGER.error("goldenhash chunk {},{} failed", chunkX, chunkZ, error);
ModdedGoldenHash.this.fail("Chunk " + chunkX + "," + chunkZ + " FAILED: " + error.getClass().getSimpleName());
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private void capture(Map<Long, String> lines) throws IOException {
List<String> body = orderedBody(lines);
String combined = combinedHash(body);
List<String> out = new ArrayList<>();
out.add("#" + FORMAT);
out.add("#world=" + engine.getWorld().name());
out.add("#dim=" + engine.getDimension().getLoadKey());
out.add("#seed=" + level.getSeed());
out.add("#mc=" + ModdedEngineBootstrap.loader().minecraftVersion());
out.add("#minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight());
out.add("#center=0,0");
out.add("#radius=" + radius);
out.addAll(body);
out.add("#combined=" + combined);
Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
ok("Golden captured: " + body.size() + " chunks combined=" + shortHash(combined));
ok(goldenFile.getAbsolutePath());
LOGGER.info("goldenhash captured: {} combined={}", goldenFile.getAbsolutePath(), combined);
}
private void verify(Map<Long, String> lines) throws IOException {
List<String> existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
Map<String, String> meta = new HashMap<>();
Map<String, String> goldenChunks = new HashMap<>();
for (String line : existing) {
if (line.startsWith("#")) {
int eq = line.indexOf('=');
if (eq > 0) {
meta.put(line.substring(1, eq), line.substring(eq + 1));
}
} else if (!line.isBlank()) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
goldenChunks.put(line.substring(0, second), line);
}
}
String expectedSeed = String.valueOf(level.getSeed());
String expectedDim = engine.getDimension().getLoadKey();
if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
fail("Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
return;
}
String mc = ModdedEngineBootstrap.loader().minecraftVersion();
if (!mc.equals(meta.get("mc"))) {
ok("Golden was captured on mc=" + meta.get("mc") + ", running mc=" + mc + ". Diffs may be version-induced.");
}
List<String> body = orderedBody(lines);
List<String> mismatches = new ArrayList<>();
for (String line : body) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
String key = line.substring(0, second);
String golden = goldenChunks.get(key);
if (!line.equals(golden)) {
mismatches.add(key + (golden == null ? " (missing in golden)" : ""));
}
}
String combined = combinedHash(body);
if (mismatches.isEmpty()) {
ok("GOLDEN MATCH: " + body.size() + "/" + goldenChunks.size() + " chunks, combined=" + shortHash(combined));
LOGGER.info("goldenhash MATCH: {} combined={}", goldenFile.getName(), combined);
return;
}
fail("GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
fail(" chunk " + mismatches.get(i));
}
if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
fail(" ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more");
}
File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new");
List<String> out = new ArrayList<>(body);
out.add("#combined=" + combined);
Files.write(current.toPath(), out, StandardCharsets.UTF_8);
ok("Current hashes written to " + current.getName());
LOGGER.info("goldenhash MISMATCH: {}/{} -> {}", mismatches.size(), body.size(), current.getAbsolutePath());
diagnose(mismatches.getFirst());
}
private void diagnose(String mismatchKey) {
try {
String[] parts = mismatchKey.trim().split(" ");
int chunkX = Integer.parseInt(parts[0]);
int chunkZ = Integer.parseInt(parts[1]);
int height = engine.getMaxHeight() - engine.getMinHeight();
PlatformBlockState air = IrisPlatforms.get().registries().air();
ModdedBlockBuffer first = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> firstBiomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, first, firstBiomes, false);
ModdedBlockBuffer second = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> secondBiomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, second, secondBiomes, false);
int diffs = 0;
for (int x = 0; x < 16 && diffs < 50; x++) {
for (int z = 0; z < 16 && diffs < 50; z++) {
for (int y = 0; y < height && diffs < 50; y++) {
if (!first.get(x, y, z).key().equals(second.get(x, y, z).key())) {
diffs++;
}
}
}
}
if (diffs == 0) {
ok("Repeat-gen STABLE for chunk " + chunkX + "," + chunkZ + " (nondeterminism is order/state-dependent, not per-call)");
} else {
fail("Repeat-gen UNSTABLE for chunk " + chunkX + "," + chunkZ + " (" + diffs + "+ block diffs between back-to-back generations)");
}
} catch (Throwable e) {
LOGGER.error("goldenhash diagnosis failed", e);
fail("Diagnosis failed: " + e.getMessage());
}
}
private static List<int[]> orderedTargets(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
private List<String> orderedBody(Map<Long, String> lines) {
Map<Long, String> sorted = new TreeMap<>(lines);
return new ArrayList<>(sorted.values());
}
private String combinedHash(List<String> body) {
MessageDigest digest = sha256();
for (String line : body) {
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
};
}
private void ok(String message) {
@@ -380,20 +180,4 @@ public final class ModdedGoldenHash {
private void fail(String message) {
server.execute(() -> IrisModdedCommands.fail(source, message));
}
private static String shortHash(String hex) {
return hex.substring(0, 12);
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -26,12 +26,14 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedGuiHost implements GuiHost.Provider {
private static final ModdedGuiHost INSTANCE = new ModdedGuiHost();
private final Map<Engine, ServerLevel> levels = new ConcurrentHashMap<>();
private final Map<Engine, UUID> openers = new ConcurrentHashMap<>();
private volatile Engine active;
private volatile MinecraftServer server;
@@ -42,10 +44,15 @@ public final class ModdedGuiHost implements GuiHost.Provider {
GuiHost.set(INSTANCE);
}
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine) {
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) {
INSTANCE.server = server;
INSTANCE.active = engine;
INSTANCE.levels.put(engine, level);
if (opener == null) {
INSTANCE.openers.remove(engine);
} else {
INSTANCE.openers.put(engine, opener);
}
}
public static boolean isGuiLaunchable() {
@@ -53,6 +60,9 @@ public final class ModdedGuiHost implements GuiHost.Provider {
}
public static String guiUnavailableReason() {
if (GuiHost.isDesktopSuppressed()) {
return "running inside a Minecraft client (desktop GUIs disabled to avoid a client crash)";
}
if (!GuiHost.isAvailable()) {
return "headless JVM (no display)";
}
@@ -82,6 +92,6 @@ public final class ModdedGuiHost implements GuiHost.Provider {
if (level == null || server == null) {
return null;
}
return new ModdedVisionOverlay(server, level, engine);
return new ModdedVisionOverlay(server, level, engine, openers.get(engine));
}
}
@@ -77,7 +77,7 @@ public final class ModdedPackCommands {
return root;
}
static File packsRoot() {
public static File packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
}
@@ -18,10 +18,8 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.gui.PregenRenderSource;
import art.arcane.iris.core.gui.PregenRenderer;
import art.arcane.iris.core.pregenerator.IrisPregenerator;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
@@ -35,60 +33,34 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
public final class ModdedPregenJob implements PregenListener, PregenRenderSource {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicReference<ModdedPregenJob> ACTIVE = new AtomicReference<>();
private static final Color COLOR_GENERATING = new Color(0x66967f);
private static final Color COLOR_GENERATED = new Color(0x65c295);
public final class ModdedPregenJob {
private static volatile String dimension = "?";
private final String dimension;
private final IrisPregenerator pregenerator;
private final Engine engine;
private final Position2 min;
private final Position2 max;
private final PregenRenderer renderer;
private volatile double chunksPerSecond;
private volatile long generated;
private volatile long totalChunks;
private volatile long eta;
private volatile long elapsed;
private volatile String method = "Modded";
private ModdedPregenJob() {
}
private ModdedPregenJob(MinecraftServer server, ServerLevel level, Engine engine, PregenTask task, boolean gui, ModdedPregenMode mode, boolean cached) {
this.dimension = level.dimension().identifier().toString();
this.engine = engine;
this.min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
this.max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE);
task.iterateAllChunks((int chunkX, int chunkZ) -> {
min.setX(Math.min(chunkX, min.getX()));
min.setZ(Math.min(chunkZ, min.getZ()));
max.setX(Math.max(chunkX, max.getX()));
max.setZ(Math.max(chunkZ, max.getZ()));
});
PregeneratorMethod baseMethod = new ModdedPregenMethod(level, engine, mode);
PregeneratorMethod resolvedMethod = baseMethod;
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, boolean sync, boolean cached) {
if (PregeneratorJob.getInstance() != null) {
return false;
}
PregenPerformanceProfile.apply(engine);
PregenTask task = PregenTask.builder()
.gui(gui)
.center(new Position2(centerBlockX, centerBlockZ))
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
PregeneratorMethod method = new ModdedPregenMethod(level, engine, sync);
if (cached) {
PregenCache cache = PregenCache.create(cacheDirectory(level)).sync();
resolvedMethod = new CachedPregenMethod(baseMethod, cache);
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
}
this.method = mode == ModdedPregenMode.SYNC ? "Modded Sync" : "Modded";
this.pregenerator = new IrisPregenerator(task, resolvedMethod, this);
PregenRenderer openedRenderer = null;
if (gui) {
try {
openedRenderer = PregenRenderer.open("Iris Pregen: " + dimension, this, ModdedPregenJob::pauseResume);
} catch (Throwable e) {
LOGGER.error("Iris pregen GUI failed to open for {}", dimension, e);
}
}
this.renderer = openedRenderer;
dimension = level.dimension().identifier().toString();
new PregeneratorJob(task, method, engine);
return true;
}
private static File cacheDirectory(ServerLevel level) {
@@ -96,119 +68,53 @@ public final class ModdedPregenJob implements PregenListener, PregenRenderSource
return new File(worldFolder, "iris" + File.separator + "pregen");
}
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui) {
return start(server, level, engine, radiusBlocks, centerBlockX, centerBlockZ, gui, ModdedPregenMode.ASYNC, false);
}
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, ModdedPregenMode mode, boolean cached) {
if (ACTIVE.get() != null) {
return false;
}
PregenTask task = PregenTask.builder()
.gui(false)
.center(new Position2(centerBlockX, centerBlockZ))
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
ModdedPregenJob job = new ModdedPregenJob(server, level, engine, task, gui, mode, cached);
if (!ACTIVE.compareAndSet(null, job)) {
job.closeRenderer();
return false;
}
Thread thread = new Thread(() -> {
try {
job.pregenerator.start();
} catch (Throwable e) {
LOGGER.error("Iris pregen failed for {}", job.dimension, e);
} finally {
job.closeRenderer();
ACTIVE.compareAndSet(job, null);
}
}, "Iris Pregen");
thread.setDaemon(true);
thread.start();
return true;
}
private void closeRenderer() {
if (renderer != null) {
renderer.close();
}
}
private void draw(int chunkX, int chunkZ, Color color) {
if (renderer == null || !renderer.isVisibleFrame()) {
return;
}
try {
Color resolved = color;
if (engine != null) {
resolved = engine.draw((chunkX << 4) + 8, (chunkZ << 4) + 8);
}
renderer.submit(chunkX, chunkZ, resolved);
} catch (Throwable e) {
renderer.submit(chunkX, chunkZ, color);
}
}
public static boolean stop() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return false;
}
job.pregenerator.close();
return true;
return PregeneratorJob.shutdownInstance();
}
public static void shutdown() {
PregeneratorJob.shutdownAndWait(10_000L);
}
public static Boolean pauseResume() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
if (PregeneratorJob.getInstance() == null) {
return null;
}
if (job.pregenerator.paused()) {
job.pregenerator.resume();
return Boolean.FALSE;
}
job.pregenerator.pause();
return Boolean.TRUE;
}
public static String status() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return null;
}
return job.statusText();
PregeneratorJob.pauseResume();
return PregeneratorJob.isPaused();
}
public static Component statusComponent() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
return null;
}
double percent = job.percent();
MutableComponent status = Component.empty();
status.append(ModdedCommandFeedback.header("Iris Pregen"));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("Dimension ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(job.dimension, ModdedCommandFeedback.PARAMETER_ALT));
status.append(ModdedCommandFeedback.text(dimension, ModdedCommandFeedback.PARAMETER_ALT));
status.append(ModdedCommandFeedback.text(" · Method ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(job.method, ModdedCommandFeedback.PARAMETER));
status.append(ModdedCommandFeedback.text(progress.method(), ModdedCommandFeedback.PARAMETER));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.progressBar(percent, 32));
status.append(ModdedCommandFeedback.text(" " + String.format("%.1f", percent) + "%", ModdedCommandFeedback.USAGE));
status.append(ModdedCommandFeedback.progressBar(progress.percent(), 32));
status.append(ModdedCommandFeedback.text(" " + String.format("%.1f", progress.percent()) + "%", ModdedCommandFeedback.USAGE));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("Chunks ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.f(job.generated) + "/" + Form.f(job.totalChunks), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks()), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(" · Speed ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.f((int) job.chunksPerSecond) + "/s", ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(Form.f((int) progress.chunksPerSecond()) + "/s", ModdedCommandFeedback.VALUE));
if (progress.failed() > 0) {
status.append(ModdedCommandFeedback.text(" · Failed ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.f(progress.failed()), ModdedCommandFeedback.REQUIRED));
}
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("ETA ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.duration(job.eta, 2), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(Form.duration(progress.eta(), 2), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(" · Elapsed ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.duration(job.elapsed, 2), ModdedCommandFeedback.VALUE));
if (job.pregenerator.paused()) {
status.append(ModdedCommandFeedback.text(Form.duration(progress.elapsed(), 2), ModdedCommandFeedback.VALUE));
if (progress.paused()) {
status.append(ModdedCommandFeedback.text(" · PAUSED", ModdedCommandFeedback.REQUIRED, true, false));
}
status.append(Component.literal("\n"));
@@ -219,118 +125,4 @@ public final class ModdedPregenJob implements PregenListener, PregenRenderSource
status.append(ModdedCommandFeedback.footer());
return status;
}
private String statusText() {
double percent = percent();
return "Pregen " + dimension + ": "
+ Form.f(generated) + "/" + Form.f(totalChunks)
+ " (" + String.format("%.1f", percent) + "%), "
+ Form.f((int) chunksPerSecond) + "/s"
+ ", ETA " + Form.duration(eta, 2)
+ ", elapsed " + Form.duration(elapsed, 2)
+ ", method " + method
+ (pregenerator.paused() ? ", PAUSED" : "");
}
private double percent() {
long total = Math.max(1L, totalChunks);
return ((double) generated / (double) total) * 100D;
}
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
this.chunksPerSecond = chunksPerSecond;
this.generated = generated;
this.totalChunks = totalChunks;
this.eta = eta;
this.elapsed = elapsed;
this.method = method;
}
@Override
public void onChunkGenerating(int x, int z) {
draw(x, z, COLOR_GENERATING);
}
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
draw(x, z, COLOR_GENERATED);
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
draw(x, z, COLOR_GENERATED);
}
@Override
public Position2 min() {
return min;
}
@Override
public Position2 max() {
return max;
}
@Override
public String[] progress() {
double percent = percent();
return new String[]{
(pregenerator.paused() ? "PAUSED " : "Generating ") + Form.f(generated) + " of " + Form.f(totalChunks)
+ " (" + String.format("%.1f", percent) + "%)",
"Speed: " + Form.f((int) chunksPerSecond) + " Chunks/s",
Form.duration(eta, 2) + " Remaining (" + Form.duration(elapsed, 2) + " Elapsed)",
"Dimension: " + dimension,
"Method: " + method
};
}
@Override
public boolean paused() {
return pregenerator.paused();
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenMantleBackpressure;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -42,45 +43,76 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private final ServerLevel level;
private final Engine engine;
private final ModdedPregenMode mode;
private final boolean sync;
private final Semaphore semaphore;
private final int permits;
private final int timeoutSeconds;
private final PregenMantleBackpressure backpressure;
public ModdedPregenMethod(ServerLevel level, Engine engine) {
this(level, engine, ModdedPregenMode.ASYNC);
this(level, engine, false);
}
public ModdedPregenMethod(ServerLevel level, Engine engine, ModdedPregenMode mode) {
public ModdedPregenMethod(ServerLevel level, Engine engine, boolean sync) {
this.level = level;
this.engine = engine;
this.mode = mode;
this.sync = sync;
this.permits = Math.min(96, Math.max(8, Runtime.getRuntime().availableProcessors() * 2));
this.semaphore = new Semaphore(permits, true);
this.timeoutSeconds = Math.max(120, IrisSettings.get().getPregen().getChunkLoadTimeoutSeconds());
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
this.backpressure = new PregenMantleBackpressure(
this::getMantle,
pregen.getEffectiveResidentTectonicPlates(engine.getHeight()),
pregen.getMantleBackpressureWaitMs(),
pregen.getMantleBackpressureTimeoutMs(),
() -> {
},
() -> "dim=" + level.dimension().identifier());
}
@Override
public void init() {
LOGGER.info("Iris modded pregen init: dim={} mode={} inFlightCap={} timeout={}s",
level.dimension().identifier(), mode, mode == ModdedPregenMode.ASYNC ? permits : 1, timeoutSeconds);
level.dimension().identifier(), sync ? "sync" : "async", sync ? 1 : permits, timeoutSeconds);
}
@Override
public void close() {
if (mode == ModdedPregenMode.ASYNC) {
semaphore.acquireUninterruptibly(permits);
if (!sync) {
try {
semaphore.tryAcquire(permits, 5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
saveLevel();
saveLevel(true);
}
@Override
public void save() {
saveLevel();
saveLevel(false);
}
private void saveLevel() {
level.getServer().execute(() -> level.save(null, false, false));
private void saveLevel(boolean wait) {
CompletableFuture<Void> saved = new CompletableFuture<>();
level.getServer().execute(() -> {
try {
level.save(null, false, false);
} finally {
saved.complete(null);
}
});
if (!wait) {
return;
}
try {
saved.get(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
}
}
@Override
@@ -95,7 +127,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public boolean isAsyncChunkMode() {
return mode == ModdedPregenMode.ASYNC;
return !sync;
}
@Override
@@ -105,7 +137,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public void generateChunk(int x, int z, PregenListener listener) {
if (mode == ModdedPregenMode.SYNC) {
backpressure.apply();
if (sync) {
generateChunkSync(x, z, listener);
return;
}
@@ -122,6 +155,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
@@ -131,6 +165,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
listener.onChunkFailed(x, z);
} finally {
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
}
@@ -155,10 +190,12 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
try {
if (error != null) {
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, error.toString());
listener.onChunkFailed(x, z);
return;
}
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
@@ -1,24 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
public enum ModdedPregenMode {
ASYNC,
SYNC
}
@@ -18,6 +18,7 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -25,6 +26,7 @@ import art.arcane.iris.modded.ModdedBlockBuffer;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.format.Form;
@@ -54,7 +56,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
@@ -105,9 +106,11 @@ public final class ModdedRegen {
private void run() {
long startedAt = M.ms();
String worldName = engine.getWorld() == null ? null : engine.getWorld().name();
IrisToolbelt.beginWorldMaintenance(worldName, "regen");
try {
resetMantleMargin();
List<int[]> targets = orderedTargets(centerX, centerZ, radius);
List<int[]> targets = ChunkSpiral.centerOut(centerX, centerZ, radius);
int applied = regenerate(targets);
ok("Regen finished: " + applied + "/" + targets.size() + " chunk(s) in " + Form.duration(M.ms() - startedAt, 2));
LOGGER.info("Iris regen done: {}/{} chunks in {}ms", applied, targets.size(), M.ms() - startedAt);
@@ -117,6 +120,7 @@ public final class ModdedRegen {
LOGGER.error("Iris regen failed", e);
fail("Regen failed: " + e);
} finally {
IrisToolbelt.endWorldMaintenance(worldName, "regen");
ACTIVE.set(false);
}
}
@@ -259,21 +263,6 @@ public final class ModdedRegen {
}
}
private static List<int[]> orderedTargets(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
private void ok(String message) {
server.execute(() -> IrisModdedCommands.ok(source, message));
}
@@ -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.project.IrisProjectCopier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
@@ -41,6 +42,7 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
@@ -63,13 +65,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
@@ -80,7 +79,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class ModdedStudioCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
@@ -150,9 +148,9 @@ public final class ModdedStudioCommands {
.executes((CommandContext<CommandSourceStack> context) -> tpStudio(context.getSource())));
root.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource())));
root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack."));
root.then(workspaceTree("vscode", true));
root.then(workspaceTree("vsc", true));
root.then(workspaceTree("update", false));
root.then(message("importvanilla", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
root.then(message("importv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
root.then(message("iv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
@@ -170,6 +168,10 @@ public final class ModdedStudioCommands {
return root;
}
public static void clear() {
STUDIOS.clear();
}
private static LiteralArgumentBuilder<CommandSourceStack> openTree(String name) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> openHelp(context.getSource()))
@@ -206,7 +208,8 @@ public final class ModdedStudioCommands {
return 0;
}
if (engine != null) {
ModdedGuiHost.bindContext(source.getServer(), level, engine);
ServerPlayer player = source.getPlayer();
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
}
if (generatorKey == null || generatorKey.isBlank()) {
NoiseExplorerGUI.launch();
@@ -240,13 +243,91 @@ public final class ModdedStudioCommands {
IrisModdedCommands.fail(source, guiUnavailableMessage());
return 0;
}
ModdedGuiHost.bindContext(source.getServer(), level, engine);
ServerPlayer player = source.getPlayer();
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
VisionGUI.launch(engine);
IrisModdedCommands.ok(source, "Opening the Vision map for " + level.dimension().identifier() + " on the server display.");
return 1;
}
private static LiteralArgumentBuilder<CommandSourceStack> workspaceTree(String name, boolean open) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> workspace(context.getSource(), null, open))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> workspace(context.getSource(), StringArgumentType.getString(context, "pack"), open)));
}
private static int workspace(CommandSourceStack source, String pack, boolean open) {
File folder = resolvePack(source, pack);
if (folder == null) {
return 0;
}
File workspace = new File(folder, folder.getName() + ".code-workspace");
try {
IO.writeAll(workspace, workspaceConfig().toString(4));
} catch (IOException e) {
LOGGER.error("Iris workspace write failed for {}", workspace, e);
IrisModdedCommands.fail(source, "Failed to write " + workspace.getAbsolutePath() + ": " + e.getMessage());
return 0;
}
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " (JSON schemas require the Bukkit studio toolchain).");
if (!open) {
return 1;
}
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
IrisModdedCommands.fail(source, guiUnavailableMessage());
return 0;
}
try {
Desktop.getDesktop().open(workspace);
} catch (Throwable e) {
LOGGER.error("Iris workspace open failed for {}", workspace, e);
IrisModdedCommands.fail(source, "Could not open " + workspace.getName() + ": " + e.getClass().getSimpleName());
return 0;
}
IrisModdedCommands.ok(source, "Opening " + workspace.getName() + " in your editor.");
return 1;
}
private static JSONObject workspaceConfig() {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject json = new JSONObject();
json.put("editor.autoIndent", "brackets");
json.put("editor.acceptSuggestionOnEnter", "smart");
json.put("editor.cursorSmoothCaretAnimation", true);
json.put("editor.dragAndDrop", false);
json.put("files.trimTrailingWhitespace", true);
json.put("diffEditor.ignoreTrimWhitespace", true);
json.put("files.trimFinalNewlines", true);
json.put("editor.suggest.showKeywords", false);
json.put("editor.suggest.showSnippets", false);
json.put("editor.suggest.showWords", false);
JSONObject quick = new JSONObject();
quick.put("strings", true);
json.put("editor.quickSuggestions", quick);
json.put("editor.suggest.insertMode", "replace");
settings.put("[json]", json);
settings.put("json.maxItemsComputed", 30000);
settings.put("json.schemas", new JSONArray());
ws.put("settings", settings);
return ws;
}
private static String guiUnavailableMessage() {
if (GuiHost.isDesktopSuppressed()) {
return "Iris desktop GUIs are disabled in singleplayer/client to avoid crashing the game client; use the in-game map and chat output instead.";
}
if (!GuiHost.isAvailable()) {
return "This server has no display (headless JVM); the Iris desktop GUIs need an AWT-capable session.";
}
@@ -346,7 +427,7 @@ public final class ModdedStudioCommands {
private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) {
ModdedDimensionManager.Handle handle;
try {
handle = ModdedDimensionManager.create(server, dimensionId, pack, seed);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
} catch (Throwable e) {
LOGGER.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e);
IrisModdedCommands.fail(source, "Studio injection failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
@@ -376,7 +457,7 @@ public final class ModdedStudioCommands {
}
ModdedDimensionManager.Handle handle;
try {
handle = ModdedDimensionManager.create(server, dimensionId, pack, seed);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
} catch (Throwable e) {
LOGGER.error("Iris studio injection failed for {} ({})", dimensionId, pack, e);
IrisModdedCommands.fail(source, "Studio injection failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
@@ -434,7 +515,7 @@ public final class ModdedStudioCommands {
for (ModdedDimensionManager.Handle handle : studios) {
UUID owner = ownerOf(handle.dimensionId());
String ownerName = owner == null ? "unclaimed" : ownerName(server, owner);
IrisModdedCommands.ok(source, " " + handle.dimensionId() + ": pack '" + handle.packKey() + "' seed " + handle.seed() + " owner " + ownerName);
IrisModdedCommands.ok(source, " " + handle.dimensionId() + ": pack '" + handle.pack() + "' seed " + handle.seed() + " owner " + ownerName);
}
return 1;
}
@@ -529,7 +610,7 @@ public final class ModdedStudioCommands {
return;
}
}
copyProject(templateFolder, target, template, name);
IrisProjectCopier.copyProject(templateFolder, target, template, name);
server.execute(() -> {
IrisModdedCommands.ok(source, "Created project '" + name + "' at " + target.getAbsolutePath());
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack, then create a world with it. VSCode workspaces are generated by the Bukkit studio toolchain only.");
@@ -544,39 +625,6 @@ public final class ModdedStudioCommands {
return 1;
}
private static void copyProject(File templateFolder, File target, String template, String name) throws IOException {
Path source = templateFolder.toPath();
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.sorted(Comparator.naturalOrder()).toList()) {
String relative = source.relativize(path).toString();
if (relative.isEmpty() || relative.equals(".git") || relative.startsWith(".git" + File.separator) || relative.endsWith(".code-workspace")) {
continue;
}
Path destination = target.toPath().resolve(relative);
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
File oldDimension = new File(target, "dimensions/" + template + ".json");
File newDimension = new File(target, "dimensions/" + name + ".json");
if (oldDimension.isFile()) {
Files.copy(oldDimension.toPath(), newDimension.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.delete(oldDimension.toPath());
}
if (newDimension.isFile()) {
JSONObject json = new JSONObject(IO.readAll(newDimension));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(name.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimension, json.toString(4));
}
}
}
private static int pkg(CommandSourceStack source, String pack) {
File folder = resolvePack(source, pack);
if (folder == null) {
@@ -738,7 +786,7 @@ public final class ModdedStudioCommands {
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, blockZ).drain();
})).setOffset(blockX >> 4, blockZ >> 4).drain();
executor.complete();
burst.close();
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
@@ -18,8 +18,10 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.gui.GuiMarker;
import art.arcane.iris.core.gui.GuiOverlay;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
import net.minecraft.server.MinecraftServer;
@@ -30,19 +32,24 @@ import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import java.awt.Desktop;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
public final class ModdedVisionOverlay implements GuiOverlay {
private final MinecraftServer server;
private final ServerLevel level;
private final Engine engine;
private final UUID opener;
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine) {
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) {
this.server = server;
this.level = level;
this.engine = engine;
this.opener = opener;
}
@Override
@@ -76,11 +83,14 @@ public final class ModdedVisionOverlay implements GuiOverlay {
int blockX = (int) worldX;
int blockZ = (int) worldZ;
server.execute(() -> {
List<ServerPlayer> players = level.players();
if (players.isEmpty()) {
return;
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
if (player == null) {
List<ServerPlayer> players = level.players();
if (players.isEmpty()) {
return;
}
player = players.get(0);
}
ServerPlayer player = players.get(0);
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1);
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false);
@@ -89,6 +99,19 @@ public final class ModdedVisionOverlay implements GuiOverlay {
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
return null;
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
return null;
}
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
}
@@ -19,19 +19,25 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.IdentifierArgument;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import org.slf4j.Logger;
@@ -41,12 +47,14 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
public final class ModdedWorldCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final String DEFAULT_NAMESPACE = "irisworldgen";
private static final long DEFAULT_SEED = 1337L;
private static final SuggestionProvider<CommandSourceStack> LOADED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(loadedIrisDimensions(context.getSource().getServer()), builder);
private ModdedWorldCommands() {
@@ -68,53 +76,68 @@ public final class ModdedWorldCommands {
root.then(enableTree("create"));
root.then(replaceOverworldTree());
root.then(disableTree("disable"));
root.then(disableTree("remove"));
root.then(disableTree("rm"));
root.then(disableTree());
root.then(deleteTree("delete"));
root.then(deleteTree("remove"));
root.then(deleteTree("rm"));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> enableTree(String name) {
return Commands.literal(name)
.then(Commands.argument("dimension", StringArgumentType.word())
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.then(Commands.argument("dimension", IdentifierArgument.id())
.then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
StringArgumentType.getString(context, "dimension"),
dimensionArgument(context),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "pack"),
1337L))
.then(Commands.argument("packDimension", StringArgumentType.word())
null))
.then(Commands.argument("seed", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
StringArgumentType.getString(context, "dimension"),
dimensionArgument(context),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "packDimension"),
1337L)))));
StringArgumentType.getString(context, "seed"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> disableTree(String name) {
private static LiteralArgumentBuilder<CommandSourceStack> disableTree() {
return Commands.literal("disable")
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false)));
}
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
return Commands.literal(name)
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(LOADED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))));
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), true)));
}
private static LiteralArgumentBuilder<CommandSourceStack> replaceOverworldTree() {
return Commands.literal("replace-overworld")
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "pack")))
.then(Commands.argument("packDimension", StringArgumentType.word())
null))
.then(Commands.argument("seed", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "packDimension")))));
StringArgumentType.getString(context, "seed")))));
}
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
return enable(source, name, pack, pack, seed);
String[] packRef = parsePackRef(pack);
return enable(source, name, packRef[0], packRef[1], seed);
}
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension, long seed) {
private static int enable(CommandSourceStack source, String targetDimension, String packRaw, String seedRaw) {
Long seed = parseSeed(source, seedRaw);
if (seed == null) {
return 0;
}
String[] packRef = parsePackRef(packRaw);
return enable(source, targetDimension, packRef[0], packRef[1], seed);
}
private static int enable(CommandSourceStack source, String targetDimension, String pack, String packDimension, long seed) {
MinecraftServer server = source.getServer();
String dimensionId;
try {
@@ -123,58 +146,146 @@ public final class ModdedWorldCommands {
IrisModdedCommands.fail(source, e.getMessage());
return 0;
}
if (!loadPackDimension(source, packName, packDimension)) {
if (!validPackRef(source, pack, packDimension)) {
return 0;
}
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (packFolder.isDirectory()) {
return enableInstalled(source, server, dimensionId, pack, packDimension, seed);
}
IrisModdedCommands.ok(source, "Pack '" + pack + "' is not installed; downloading IrisDimensions/" + pack + "...");
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master",
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + pack + "' could not be downloaded; check the name or install it with /iris download " + pack + ".");
return;
}
enableInstalled(source, server, dimensionId, pack, packDimension, seed);
});
}, "Iris World Pack Download");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
}
if (blockIfPackBroken(source, dimensionId, pack)) {
return 0;
}
try {
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, seed);
ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
} catch (Throwable e) {
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
IrisModdedCommands.fail(source, "Failed to inject Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
return 0;
}
IrisModdedCommands.ok(source, "Created Iris world " + dimensionId + " from pack '" + packName + "' dimension '" + packDimension + "' (seed " + seed + ").");
IrisModdedCommands.ok(source, "Created Iris world " + dimensionId + " from pack '" + pack + "' dimension '" + packDimension + "' (seed " + seed + ").");
IrisModdedCommands.ok(source, "It is live now and re-injected on every startup. Teleport in with /iris world status or a portal; no restart required.");
return 1;
}
private static int replaceOverworld(CommandSourceStack source, String packName, String packDimension) {
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
MinecraftServer server = source.getServer();
Long seed = parseSeed(source, seedRaw);
if (seed == null) {
return 0;
}
String[] packRef = parsePackRef(packRaw);
String pack = packRef[0];
String packDimension = packRef[1];
if (!validPackRef(source, pack, packDimension)) {
return 0;
}
String dimensionId = DEFAULT_NAMESPACE + ":primary";
if (!loadPackDimension(source, packName, packDimension)) {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
}
if (blockIfPackBroken(source, dimensionId, pack)) {
return 0;
}
try {
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, 1337L);
ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
} catch (Throwable e) {
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
IrisModdedCommands.fail(source, "Failed to inject Iris primary world: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
return 0;
}
ModdedModConfig.setPrimaryWorld(dimensionId);
ModdedPrimaryWorldRouter.clear();
IrisModdedCommands.ok(source, "Iris primary world set to " + dimensionId + " (pack '" + packName + "' dimension '" + packDimension + "').");
IrisModdedCommands.ok(source, "Iris primary world set to " + dimensionId + " (pack '" + pack + "' dimension '" + packDimension + "' seed " + seed + ").");
IrisModdedCommands.ok(source, "The vanilla overworld generator cannot be hot-swapped, so this does NOT regenerate the existing overworld.");
IrisModdedCommands.ok(source, "Instead, " + dimensionId + " is now the configured primary world: players in the vanilla overworld are routed there on join, and it re-injects on every startup.");
return 1;
}
private static boolean loadPackDimension(CommandSourceStack source, String packName, String packDimension) {
File packFolder = new File(ModdedPackCommands.packsRoot(), packName);
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
PackValidationResult validation = PackValidationRegistry.get(pack);
if (validation == null || validation.isLoadable()) {
return false;
}
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId + "' using broken pack '" + pack + "':");
for (String reason : validation.getBlockingErrors()) {
IrisModdedCommands.fail(source, " - " + reason);
}
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
return true;
}
private static boolean loadPackDimension(CommandSourceStack source, String pack, String packDimension) {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!packFolder.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + packName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
IrisModdedCommands.fail(source, "Pack '" + pack + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
return false;
}
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(packDimension);
if (dimension == null) {
IrisModdedCommands.fail(source, "Pack '" + packName + "' does not contain dimensions/" + packDimension + ".json");
IrisModdedCommands.fail(source, "Pack '" + pack + "' does not contain dimensions/" + packDimension + ".json");
return false;
}
return true;
}
private static int disable(CommandSourceStack source, String targetDimension) {
private static String[] parsePackRef(String raw) {
String value = raw.trim();
int colon = value.indexOf(':');
if (colon >= 0) {
return new String[]{value.substring(0, colon), value.substring(colon + 1)};
}
return new String[]{value, value};
}
private static boolean validPackRef(CommandSourceStack source, String pack, String packDimension) {
if (pack.matches("[A-Za-z0-9_.-]+") && !pack.contains("..")
&& packDimension.matches("[A-Za-z0-9_/.-]+") && !packDimension.contains("..")) {
return true;
}
IrisModdedCommands.fail(source, "Invalid pack reference '" + pack + (pack.equals(packDimension) ? "" : ":" + packDimension) + "'. Use pack or pack:dimensionKey.");
return false;
}
private static Long parseSeed(CommandSourceStack source, String seedRaw) {
if (seedRaw == null || seedRaw.isBlank()) {
return DEFAULT_SEED;
}
String value = seedRaw.trim();
if (value.equalsIgnoreCase("random")) {
return ThreadLocalRandom.current().nextLong();
}
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
IrisModdedCommands.fail(source, "Invalid seed '" + seedRaw + "'. Use a number or 'random'.");
return null;
}
}
private static int disable(CommandSourceStack source, String targetDimension, boolean wipeStorage) {
MinecraftServer server = source.getServer();
String dimensionId;
try {
@@ -185,7 +296,7 @@ public final class ModdedWorldCommands {
}
boolean removed;
try {
removed = ModdedDimensionManager.removePersistent(server, dimensionId);
removed = ModdedDimensionManager.removePersistent(server, dimensionId, wipeStorage);
} catch (Throwable e) {
LOGGER.error("Iris world removal failed for {}", dimensionId, e);
IrisModdedCommands.fail(source, "Failed to remove Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
@@ -196,10 +307,19 @@ public final class ModdedWorldCommands {
ModdedPrimaryWorldRouter.clear();
}
if (!removed) {
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry.");
if (wipeStorage) {
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry and wiped its stored chunk/mantle data.");
} else {
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry. World data on disk is kept.");
}
return 1;
}
IrisModdedCommands.ok(source, "Removed Iris world '" + dimensionId + "': evacuated, unloaded, region data deleted, and dropped from the startup registry.");
if (wipeStorage) {
IrisModdedCommands.ok(source, "Deleted Iris world '" + dimensionId + "': evacuated, unloaded, chunk/mantle data wiped, and dropped from the startup registry.");
} else {
IrisModdedCommands.ok(source, "Disabled Iris world '" + dimensionId + "': evacuated, unloaded, and dropped from the startup registry.");
IrisModdedCommands.ok(source, "World data on disk is kept; re-enable with /iris world enable " + dimensionId + " <pack> or delete it with /iris world delete " + dimensionId + ".");
}
return 1;
}
@@ -209,7 +329,7 @@ public final class ModdedWorldCommands {
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
loaded++;
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.activePackKey() + "'");
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack '" + generator.activePack() + "' dimension '" + generator.activeDimensionKey() + "'");
}
}
String primary = ModdedModConfig.get().primaryWorld();
@@ -244,6 +364,15 @@ public final class ModdedWorldCommands {
return dimensions;
}
private static String dimensionArgument(CommandContext<CommandSourceStack> context) {
for (ParsedCommandNode<CommandSourceStack> node : context.getNodes()) {
if ("dimension".equals(node.getNode().getName())) {
return node.getRange().get(context.getInput());
}
}
return IdentifierArgument.getId(context, "dimension").toString();
}
private static String normalizeDimensionId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing dimension id.");
@@ -0,0 +1,306 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedLootApplier;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
public final class ModdedChunkUpdateService implements ModdedTickableService {
private static final long PASS_PERIOD_MILLIS = 3_000L;
private static final int PLAYER_CHUNK_RADIUS = 1;
private static final int SILENT_FLAGS = Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_SKIP_ON_PLACE;
private final Set<Long> warmupQueue = ConcurrentHashMap.newKeySet();
private volatile ExecutorService warmupExecutor;
private long lastPassAt;
@Override
public void onEnable() {
if (warmupExecutor != null) {
return;
}
warmupExecutor = Executors.newSingleThreadExecutor((Runnable runnable) -> {
Thread thread = new Thread(runnable, "Iris Mantle Warmup");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
lastPassAt = 0L;
}
@Override
public void onDisable() {
ExecutorService active = warmupExecutor;
warmupExecutor = null;
if (active != null) {
active.shutdown();
}
warmupQueue.clear();
}
@Override
public void onServerTick(MinecraftServer server) {
long now = System.currentTimeMillis();
if (now - lastPassAt < PASS_PERIOD_MILLIS) {
return;
}
lastPassAt = now;
if (!IrisSettings.get().getWorld().isPostLoadBlockUpdates()) {
return;
}
for (ServerLevel level : server.getAllLevels()) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
continue;
}
Engine engine = generator.engineIfBound();
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
continue;
}
if (level.players().isEmpty() || isPregenActive(engine)) {
continue;
}
try {
updateNearPlayers(engine, level);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
private boolean isPregenActive(Engine engine) {
PregeneratorJob job = PregeneratorJob.getInstance();
return job != null && job.targetsWorldName(engine.getWorld().name());
}
private void updateNearPlayers(Engine engine, ServerLevel level) {
for (ServerPlayer player : level.players()) {
int centerX = player.blockPosition().getX() >> 4;
int centerZ = player.blockPosition().getZ() >> 4;
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
updateChunk(engine, level, centerX + dx, centerZ + dz);
}
}
}
}
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
return;
}
}
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunk(mantle, chunkX, chunkZ);
return;
}
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk));
chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk));
});
} finally {
chunk.release();
}
}
private void runTilePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
int minHeight = engine.getWorld().minHeight();
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
int y = yf + minHeight;
if (y < level.getMinY() || y > level.getMaxY()) {
return;
}
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
});
}
private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) {
if (!(tile instanceof ModdedTileData moddedTile)) {
return;
}
String snbt = moddedTile.snbt();
if (snbt == null || snbt.isBlank()) {
return;
}
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
if (!state.hasBlockEntity()) {
return;
}
try {
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
if (restored == null) {
return;
}
level.setBlockEntity(restored);
restored.setChanged();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void warmupMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
ExecutorService active = warmupExecutor;
if (active == null) {
return;
}
long key = Cache.key(chunkX, chunkZ);
if (!warmupQueue.add(key)) {
return;
}
try {
active.execute(() -> {
try {
mantle.getChunk(chunkX, chunkZ);
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
warmupQueue.remove(key);
}
});
} catch (RejectedExecutionException rejected) {
warmupQueue.remove(key);
}
}
private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
int minHeight = engine.getWorld().minHeight();
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
int[][] grid = new int[16][16];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
grid[x][z] = Integer.MIN_VALUE;
}
}
RNG rng = new RNG(Cache.key(chunkX, chunkZ));
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
int y = yf + minHeight;
if (y < level.getMinY() || y > level.getMaxY()) {
return;
}
int lx = x & 15;
int lz = z & 15;
BlockPos pos = new BlockPos(baseX + lx, y, baseZ + lz);
if (!ModdedBlockResolution.isFluid(level.getBlockState(pos))) {
return;
}
boolean exposed = ModdedBlockResolution.isAir(level.getBlockState(pos.below()))
|| ModdedBlockResolution.isAir(level.getBlockState(pos.west()))
|| ModdedBlockResolution.isAir(level.getBlockState(pos.east()))
|| ModdedBlockResolution.isAir(level.getBlockState(pos.south()))
|| ModdedBlockResolution.isAir(level.getBlockState(pos.north()));
if (exposed) {
grid[lx][lz] = Math.max(grid[lx][lz], y);
}
});
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (grid[x][z] == Integer.MIN_VALUE) {
continue;
}
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk, rng);
}
}
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
if (v != null && v.isUpdate()) {
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk, rng);
}
});
chunk.deleteSlices(MatterUpdate.class);
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
}
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
if (y < level.getMinY() || y > level.getMaxY()) {
return;
}
BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
BlockState state = level.getBlockState(pos);
engine.blockUpdatedMetric();
if (ModdedBlockResolution.isStorage(state)) {
if (!ModdedBlockResolution.isStorageChest(state)) {
return;
}
RNG rx = rng.nextParallelRNG(BlockPosition.toLong(x, y, z));
try {
ModdedLootApplier.apply(engine, level, pos, state, chunk, rx, x & 15, z & 15);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
} else {
level.setBlock(pos, Blocks.AIR.defaultBlockState(), SILENT_FLAGS);
level.setBlock(pos, state, Block.UPDATE_ALL);
}
}
}
@@ -0,0 +1,201 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.runtime.GoldenHashEngine;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long TRIM_PERIOD_MILLIS = 2_000L;
private static final long SAVE_PERIOD_MILLIS = 60_000L;
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final Set<Engine> inFlight = ConcurrentHashMap.newKeySet();
private volatile ExecutorService service;
private long lastMaintenanceAt;
private long lastSaveAt;
@Override
public void onEnable() {
if (service != null) {
return;
}
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
IrisSettings.IrisSettingsEngineSVC engineSettings = settings.getEngineSVC();
ThreadFactory factory = (engineSettings.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(engineSettings.getPriority()))
.name("Iris EngineSVC-", 0)
.factory();
service = Executors.newThreadPerTaskExecutor(factory);
tectonicLimit.set(settings.getTectonicPlateSize());
lastMaintenanceAt = 0L;
lastSaveAt = System.currentTimeMillis();
}
@Override
public void onDisable() {
ExecutorService active = service;
service = null;
if (active != null) {
active.shutdown();
}
inFlight.clear();
}
@Override
public void onServerTick(MinecraftServer server) {
ExecutorService active = service;
if (active == null) {
return;
}
long now = System.currentTimeMillis();
if (now - lastMaintenanceAt < TRIM_PERIOD_MILLIS) {
return;
}
lastMaintenanceAt = now;
boolean flush = now - lastSaveAt >= SAVE_PERIOD_MILLIS;
if (flush) {
lastSaveAt = now;
}
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
int share = tectonicLimit.get() / Math.max(engines.size(), 1);
for (Engine engine : engines) {
if (!inFlight.add(engine)) {
continue;
}
try {
active.execute(() -> {
try {
maintain(engine, share, flush);
} finally {
inFlight.remove(engine);
}
});
} catch (RejectedExecutionException rejected) {
inFlight.remove(engine);
}
}
}
private void maintain(Engine engine, int share, boolean flush) {
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
return;
}
if (flush) {
try {
engine.save();
} catch (Throwable e) {
if (isMantleClosed(e)) {
return;
}
IrisLogging.reportError(e);
LOGGER.error("Iris engine save failed for {}", engine.getWorld().name(), e);
}
}
if (!shouldReduce(engine) || shouldSkipForMaintenance(engine) || GoldenHashEngine.isActive()) {
return;
}
try {
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
engine.getMantle().trim(0L, 0);
} else {
engine.getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), activeTectonicLimit(engine, share));
}
long unloadStart = System.currentTimeMillis();
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine, share);
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
if (heapPressure && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
if (count > 0) {
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}", count, System.currentTimeMillis() - unloadStart, engine.getWorld().name());
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
return;
}
IrisLogging.reportError(e);
LOGGER.error("Iris engine maintenance failed for {}", engine.getWorld().name(), e);
}
}
private boolean shouldReduce(Engine engine) {
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return true;
}
return pregenTargets(engine);
}
private boolean shouldSkipForMaintenance(Engine engine) {
if (engine.getWorld() == null || !IrisToolbelt.isWorldMaintenanceActive(engine.getWorld().name())) {
return false;
}
return !pregenTargets(engine);
}
private boolean pregenTargets(Engine engine) {
if (engine.getWorld() == null) {
return false;
}
PregeneratorJob job = PregeneratorJob.getInstance();
return job != null && job.targetsWorldName(engine.getWorld().name());
}
private int activeTectonicLimit(Engine engine, int share) {
if (!pregenTargets(engine)) {
return share;
}
return Math.max(share, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
}
private static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
}
@@ -29,18 +29,30 @@ import org.apache.logging.log4j.message.Message;
import java.util.List;
public final class ModdedLogFilterService implements ModdedService, Filter {
private static final String VANILLA_LOGGER_PREFIX = "net.minecraft";
private static final List<String> FILTERS = List.of(
"Ignoring heightmap data for chunk",
"Could not save data net.minecraft.world.entity.raid.PersistentRaid",
"UUID of added entity already exists");
private boolean installed = false;
@Override
public void onEnable() {
if (installed) {
return;
}
((Logger) LogManager.getRootLogger()).addFilter(this);
installed = true;
}
@Override
public void onDisable() {
if (!installed) {
return;
}
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
installed = false;
}
@Override
@@ -82,76 +94,76 @@ public final class ModdedLogFilterService implements ModdedService, Filter {
@Override
public Result filter(LogEvent event) {
return check(event.getMessage().getFormattedMessage());
return check(event.getLoggerName(), event.getMessage().getFormattedMessage());
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return check(String.valueOf(msg));
return check(logger.getName(), String.valueOf(msg));
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return check(msg.getFormattedMessage());
return check(logger.getName(), msg.getFormattedMessage());
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object... params) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) {
return check(message);
return check(logger.getName(), message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) {
return check(message);
return check(logger.getName(), message);
}
private Result check(String message) {
if (message == null) {
private Result check(String loggerName, String message) {
if (loggerName == null || !loggerName.startsWith(VANILLA_LOGGER_PREFIX) || message == null) {
return Result.NEUTRAL;
}
for (String filter : FILTERS) {
@@ -0,0 +1,68 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.spi.IrisPlatforms;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public final class ModdedSettingsHotloadService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long POLL_PERIOD_MILLIS = 3_000L;
private long lastPollAt;
private long lastModified;
@Override
public void onEnable() {
lastPollAt = 0L;
lastModified = settingsFile().lastModified();
}
@Override
public void onDisable() {
}
@Override
public void onServerTick(MinecraftServer server) {
long now = System.currentTimeMillis();
if (now - lastPollAt < POLL_PERIOD_MILLIS) {
return;
}
lastPollAt = now;
long modified = settingsFile().lastModified();
if (modified == lastModified) {
return;
}
if (IrisSettings.settings != null) {
IrisSettings.invalidate();
}
IrisSettings.get();
lastModified = settingsFile().lastModified();
LOGGER.info("Hotloaded settings.json");
}
private static File settingsFile() {
return IrisPlatforms.get().dataFile("settings.json");
}
}
@@ -0,0 +1,184 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.service;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStudioHotloadService implements ModdedTickableService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_";
private static final long POLL_MILLIS = 250L;
private static final long CHECK_LATCH_MILLIS = 1_000L;
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
private volatile ExecutorService executor;
private long lastPollAt;
@Override
public void onEnable() {
if (executor != null) {
return;
}
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
Thread thread = new Thread(task, "Iris Studio Hotload");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
lastPollAt = 0L;
}
@Override
public void onDisable() {
ExecutorService active = executor;
executor = null;
if (active != null) {
active.shutdownNow();
}
watches.clear();
}
@Override
public void onServerTick(MinecraftServer server) {
ExecutorService active = executor;
if (active == null) {
return;
}
long now = System.currentTimeMillis();
if (now - lastPollAt < POLL_MILLIS) {
return;
}
lastPollAt = now;
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
Set<String> seen = null;
for (ModdedDimensionManager.Handle handle : handles) {
String dimensionId = handle.dimensionId();
if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX)) {
continue;
}
IrisModdedChunkGenerator generator = handle.generator();
if (generator == null) {
continue;
}
Engine engine = generator.engineIfBound();
if (engine == null || engine.isClosed()) {
continue;
}
if (seen == null) {
seen = new HashSet<>();
}
seen.add(dimensionId);
Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch());
if (throttled(generator, engine, now)) {
continue;
}
if (!watch.latch.flip()) {
continue;
}
if (!watch.busy.compareAndSet(false, true)) {
continue;
}
try {
active.execute(() -> {
try {
poll(dimensionId, watch, generator, engine);
} finally {
watch.busy.set(false);
}
});
} catch (RejectedExecutionException rejected) {
watch.busy.set(false);
}
}
if (!watches.isEmpty()) {
Set<String> keep = seen == null ? Set.of() : seen;
watches.keySet().removeIf((String key) -> !keep.contains(key));
}
}
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
return true;
}
PregeneratorJob job = PregeneratorJob.getInstance();
return job != null && job.targetsWorldName(engine.getWorld().name());
}
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
try {
if (watch.engine != engine) {
watch.engine = engine;
watch.folder = new ReactiveFolder(
engine.getData().getDataFolder(),
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, generator, engine),
new KList<>(".iob", ".json"),
new KList<>(".iris"),
new KList<>());
return;
}
ReactiveFolder folder = watch.folder;
if (folder != null) {
folder.check();
}
} catch (Throwable e) {
LOGGER.error("Iris studio hotload check failed for {}", dimensionId, e);
}
}
private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) {
if (engine.isClosed()) {
return;
}
long start = System.currentTimeMillis();
try {
engine.hotloadSilently();
generator.onHotload();
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
}
}
private static final class Watch {
private final ChronoLatch latch = new ChronoLatch(CHECK_LATCH_MILLIS, false);
private final AtomicBoolean busy = new AtomicBoolean(false);
private volatile Engine engine;
private volatile ReactiveFolder folder;
}
}
@@ -21,12 +21,7 @@ package art.arcane.iris.neoforge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
@@ -34,15 +29,13 @@ import net.minecraft.server.packs.PackType;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModList;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.VersionInfo;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.registries.DeferredRegister;
@@ -50,20 +43,11 @@ import net.neoforged.neoforge.registries.DeferredRegister;
@Mod("irisworldgen")
public final class IrisNeoForgeBootstrap {
public IrisNeoForgeBootstrap(IEventBus modBus) {
ModdedEngineBootstrap.initialize(new NeoForgeModdedLoader());
String modVersion = ModList.get().getModContainerById("irisworldgen")
.map((ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
VersionInfo versionInfo = FMLLoader.getCurrent().getVersionInfo();
ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + versionInfo.mcVersion() + " (NeoForge " + versionInfo.neoForgeVersion() + ")");
ModdedEngineBootstrap.selfTest(IrisNeoForgeBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind();
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(modBus);
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
ModdedEngineBootstrap.bootCommon(new NeoForgeModdedLoader(), "NeoForge " + FMLLoader.getCurrent().getVersionInfo().neoForgeVersion(), () -> {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(modBus);
});
modBus.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
@@ -71,12 +55,8 @@ public final class IrisNeoForgeBootstrap {
}
});
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
});
NeoForge.EVENT_BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> {
if (ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) {
@@ -93,17 +73,5 @@ public final class IrisNeoForgeBootstrap {
ModdedEngineBootstrap.tick(event.getServer());
ModdedWandService.serverTick(event.getServer());
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity);
}
String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) {
ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule();
}
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.neoforge;
import art.arcane.iris.modded.ModdedLoader;
import net.minecraft.server.MinecraftServer;
import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.FMLPaths;
import net.neoforged.neoforge.server.ServerLifecycleHooks;
@@ -52,6 +53,11 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
return ServerLifecycleHooks.getCurrentServer();
}
@Override
public boolean clientEnvironment() {
return FMLEnvironment.getDist().isClient();
}
@Override
public Path configDir() {
return FMLPaths.CONFIGDIR.get();
@@ -141,6 +141,10 @@ public class IrisSettings {
@Data
public static class IrisSettingsPregen {
private static final int REFERENCE_WORLD_HEIGHT = 384;
private static final int MIN_RESIDENT_TECTONIC_PLATES = 16;
private static final double MANTLE_HEAP_FRACTION = 0.6D;
private static final int REFERENCE_PLATE_MEGABYTES = 48;
public boolean useTicketQueue = true;
public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
@@ -159,6 +163,17 @@ public class IrisSettings {
return Math.max(16, maxResidentTectonicPlates);
}
public int getEffectiveResidentTectonicPlates(int worldHeight) {
int baseCap = getMaxResidentTectonicPlates();
int normalizedHeight = Math.max(1, worldHeight);
int heightScaledCap = (int) Math.round((double) baseCap * REFERENCE_WORLD_HEIGHT / (double) normalizedHeight);
long maxHeapMegabytes = getHardware.getProcessMemory();
double plateMegabytes = (double) REFERENCE_PLATE_MEGABYTES * (double) normalizedHeight / (double) REFERENCE_WORLD_HEIGHT;
int byteBudgetCap = (int) Math.floor(MANTLE_HEAP_FRACTION * (double) maxHeapMegabytes / plateMegabytes);
int effective = Math.min(heightScaledCap, byteBudgetCap);
return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(baseCap, effective));
}
public int getMantleBackpressureWaitMs() {
return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000));
}
@@ -25,10 +25,19 @@ import java.awt.GraphicsEnvironment;
public final class GuiHost {
private static volatile Provider provider = new Provider() {
};
private static volatile boolean desktopSuppressed = false;
private GuiHost() {
}
public static void suppressDesktop(boolean suppress) {
desktopSuppressed = suppress;
}
public static boolean isDesktopSuppressed() {
return desktopSuppressed;
}
public interface Provider {
default Engine findActiveEngine() {
return null;
@@ -56,6 +65,6 @@ public final class GuiHost {
}
public static boolean isAvailable() {
return !GraphicsEnvironment.isHeadless();
return !desktopSuppressed && !GraphicsEnvironment.isHeadless();
}
}
@@ -85,8 +85,11 @@ public final class PregenRenderer extends JPanel implements KeyListener {
}
public void close() {
if (frame != null) {
frame.setVisible(false);
JFrame activeFrame = frame;
if (activeFrame != null) {
frame = null;
activeFrame.setVisible(false);
activeFrame.dispose();
}
}
@@ -33,7 +33,6 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import java.awt.Color;
import java.util.concurrent.ExecutorService;
@@ -63,12 +62,18 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
private final Engine engine;
private final ExecutorService service;
private final Thread worker;
private PregenRenderer renderer;
private Consumer2<Position2, Color> drawFunction;
private int rgc = 0;
private String[] info;
private volatile double lastChunksPerSecond = 0D;
private volatile long lastChunksRemaining = 0L;
private volatile long lastGenerated = 0L;
private volatile long lastTotalChunks = 0L;
private volatile long lastEta = 0L;
private volatile long lastElapsed = 0L;
private volatile String lastMethod = "Void";
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
instance.updateAndGet(old -> {
@@ -84,26 +89,32 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
info = new String[]{"Initializing..."};
this.task = task;
this.pregenerator = new IrisPregenerator(task, method, this);
max = new Position2(0, 0);
max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE);
min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
service = Executors.newVirtualThreadPerTaskExecutor();
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
open();
}
worker = new Thread(() -> {
J.sleep(1000);
computeBounds();
this.pregenerator.start();
}, "Iris Pregenerator");
worker.setPriority(Thread.MIN_PRIORITY);
worker.setDaemon(true);
worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex));
worker.start();
}
private void computeBounds() {
task.iterateAllChunks((xx, zz) -> {
min.setX(Math.min(xx, min.getX()));
min.setZ(Math.min(zz, min.getZ()));
max.setX(Math.max(xx, max.getX()));
max.setZ(Math.max(zz, max.getZ()));
});
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
open();
}
Thread t = new Thread(() -> {
J.sleep(1000);
this.pregenerator.start();
}, "Iris Pregenerator");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
service = Executors.newVirtualThreadPerTaskExecutor();
}
public static boolean shutdownInstance() {
@@ -116,6 +127,23 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return true;
}
public static boolean shutdownAndWait(long timeoutMs) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
inst.pregenerator.close();
inst.worker.interrupt();
try {
inst.worker.join(timeoutMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
instance.compareAndSet(inst, null);
return true;
}
public static PregeneratorJob getInstance() {
return instance.get();
}
@@ -153,13 +181,36 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
}
public boolean targetsWorld(World world) {
if (world == null || engine == null || engine.getWorld() == null) {
return false;
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName) {
}
public static PregenProgress progressSnapshot() {
PregeneratorJob inst = instance.get();
if (inst == null) {
return null;
}
String targetName = engine.getWorld().name();
return targetName != null && targetName.equalsIgnoreCase(world.getName());
double percent = inst.lastTotalChunks <= 0 ? 0D : ((double) inst.lastGenerated / (double) inst.lastTotalChunks) * 100D;
return new PregenProgress(
percent,
inst.lastGenerated,
inst.lastTotalChunks,
Math.max(0D, inst.lastChunksPerSecond),
Math.max(0L, inst.lastChunksRemaining),
inst.lastEta,
inst.lastElapsed,
inst.lastMethod,
inst.paused(),
inst.pregenerator.getFailedChunks(),
inst.worldName());
}
public String worldName() {
if (engine == null || engine.getWorld() == null) {
return null;
}
return engine.getWorld().name();
}
public boolean targetsWorldName(String worldName) {
@@ -252,6 +303,11 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
lastChunksPerSecond = chunksPerSecond;
lastChunksRemaining = chunksRemaining;
lastGenerated = generated;
lastTotalChunks = totalChunks;
lastEta = eta;
lastElapsed = elapsed;
lastMethod = method;
info = new String[]{
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
@@ -1,5 +1,6 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.nms.INMS;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldCreator;
@@ -31,6 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend {
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
INMS.get().ensureServerLevelInjection();
try {
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
@@ -164,6 +164,9 @@ public interface INMSBinding {
throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation.");
}
default void ensureServerLevelInjection() {
}
int countCustomBiomes();
default boolean supportsDataPacks() {
@@ -188,6 +191,13 @@ public interface INMSBinding {
return false;
}
default boolean saveAndUnloadChunk(World world, int x, int z) {
return false;
}
default void flushChunkIO(World world) {
}
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
@@ -0,0 +1,160 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pack;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.misc.WebCache;
import art.arcane.volmlib.util.io.IO;
import org.zeroturnaround.zip.ZipUtil;
import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.function.Consumer;
public final class PackDownloader {
private PackDownloader() {
}
public static String download(File packsFolder, String repo, String branch, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
feedback.accept("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = WebCache.getNonCachedFile("pack-" + repo, url);
File temp = WebCache.getTemp();
File work = new File(temp, "dl-" + UUID.randomUUID());
if (zip == null || !zip.exists()) {
feedback.accept("Failed to find pack at " + url);
feedback.accept("Make sure you specified the correct repo and branch!");
feedback.accept("For example: /iris download overworld branch=stable");
return null;
}
feedback.accept("Unpacking " + repo);
try {
ZipUtil.unpack(zip, work);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
feedback.accept(
"""
Issue when unpacking. Please check/do the following:
1. Do you have a functioning internet connection?
2. Did the download corrupt?
3. Try deleting the */plugins/iris/packs folder and re-download.
4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld
5. Contact support (if all other options do not help)"""
);
}
File dir = null;
File[] zipFiles = work.listFiles();
if (zipFiles == null) {
feedback.accept("No files were extracted from the zip file.");
return null;
}
try {
dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
} catch (NullPointerException e) {
IrisLogging.reportError(e);
feedback.accept("Error when finding home directory. Are there any non-text characters in the file name?");
return null;
}
if (dir == null) {
feedback.accept("Invalid Format. Missing root folder or too many folders!");
return null;
}
IrisData data = IrisData.get(dir);
String[] dimensions = data.getDimensionLoader().getPossibleKeys();
if (dimensions == null || dimensions.length == 0) {
feedback.accept("No dimension file found in the extracted zip file.");
feedback.accept("Check it is there on GitHub and report this to staff!");
return null;
}
if (dimensions.length != 1) {
feedback.accept("Dimensions folder must have 1 file in it");
return null;
}
IrisDimension d = data.getDimensionLoader().load(dimensions[0]);
data.close();
if (d == null) {
feedback.accept("Invalid dimension (folder) in dimensions folder");
return null;
}
String key = d.getLoadKey();
feedback.accept("Importing " + d.getName() + " (" + key + ")");
File packEntry = new File(packsFolder, key);
if (forceOverwrite) {
IO.delete(packEntry);
}
if (IrisData.loadAnyDimension(key, null) != null) {
feedback.accept("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!");
return null;
}
File[] existingEntries = packEntry.listFiles();
if (packEntry.exists() && existingEntries != null && existingEntries.length > 0) {
feedback.accept("Another pack is using the key " + key + ". IMPORT FAILED!");
return null;
}
FileUtils.copyDirectory(dir, packEntry);
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
feedback.accept("Successfully Aquired " + d.getName());
validateDownloaded(packEntry, feedback);
return key;
}
private static void validateDownloaded(File packEntry, Consumer<String> feedback) {
try {
PackValidationResult result = PackValidator.validate(packEntry);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
feedback.accept("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
feedback.accept(" - " + reason);
}
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
feedback.accept("Pack '" + result.getPackName() + "' validated ("
+ result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, "
+ result.getWarnings().size() + " warning(s)).");
} else {
feedback.accept("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
IrisLogging.reportError("Pack validation failed for '" + packEntry.getName() + "'", e);
}
}
}
@@ -1,268 +0,0 @@
package art.arcane.iris.core.pregenerator;
import com.google.gson.Gson;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Data;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldUnloadEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
public class DeepSearchPregenerator extends Thread implements Listener {
@Getter
private static DeepSearchPregenerator instance;
private final DeepSearchJob job;
private final File destination;
private final int maxPosition;
private World world;
private final ChronoLatch latch;
private static AtomicInteger foundChunks;
private final AtomicInteger foundLast;
private final AtomicInteger foundTotalChunks;
private final AtomicLong startTime;
private final RollingSequence chunksPerSecond;
private final RollingSequence chunksPerMinute;
private final AtomicInteger chunkCachePos;
private final AtomicInteger chunkCacheSize;
private int pos;
private final AtomicInteger foundCacheLast;
private final AtomicInteger foundCache;
private LinkedHashMap<Integer, Position2> chunkCache;
private KList<Position2> chunkQueue;
private final ReentrantLock cacheLock;
private static final Map<String, DeepSearchJob> jobs = new HashMap<>();
public DeepSearchPregenerator(DeepSearchJob job, File destination) {
this.job = job;
this.chunkCacheSize = new AtomicInteger(); // todo
this.chunkCachePos = new AtomicInteger(1000);
this.foundCacheLast = new AtomicInteger();
this.foundCache = new AtomicInteger();
this.cacheLock = new ReentrantLock();
this.destination = destination;
this.chunkCache = new LinkedHashMap<>();
this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
}).count();
this.world = Bukkit.getWorld(job.getWorld().getUID());
this.chunkQueue = new KList<>();
this.latch = new ChronoLatch(3000);
this.startTime = new AtomicLong(M.ms());
this.chunksPerSecond = new RollingSequence(10);
this.chunksPerMinute = new RollingSequence(10);
foundChunks = new AtomicInteger(0);
this.foundLast = new AtomicInteger(0);
this.foundTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2)));
this.pos = 0;
jobs.put(job.getWorld().getName(), job);
DeepSearchPregenerator.instance = this;
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(world)) {
interrupt();
}
}
public void run() {
while (!interrupted()) {
tick();
}
try {
saveNow();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void tick() {
DeepSearchJob job = jobs.get(world.getName());
// chunkCache(); //todo finish this
if (latch.flip() && !job.paused) {
if (cacheLock.isLocked()) {
IrisLogging.info("DeepFinder: Caching: " + chunkCachePos.get() + " Of " + chunkCacheSize.get());
} else {
long eta = computeETA();
save();
int secondGenerated = foundChunks.get() - foundLast.get();
foundLast.set(foundChunks.get());
secondGenerated = secondGenerated / 3;
chunksPerSecond.put(secondGenerated);
chunksPerMinute.put(secondGenerated * 60);
IrisLogging.info("DeepFinder: " + C.IRIS + world.getName() + C.RESET + " Searching: " + Form.f(foundChunks.get()) + " of " + Form.f(foundTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2));
}
}
if (foundChunks.get() >= foundTotalChunks.get()) {
IrisLogging.info("Completed DeepSearch!");
interrupt();
}
}
private long computeETA() {
return (long) ((foundTotalChunks.get() - foundChunks.get()) / chunksPerSecond.getAverage()) * 1000;
// todo broken
}
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private void queueSystem(Position2 chunk) {
if (chunkQueue.isEmpty()) {
for (int limit = 512; limit != 0; limit--) {
pos = job.getPosition() + 1;
chunkQueue.add(getChunk(pos));
}
} else {
//MCAUtil.read();
}
}
private void findInChunk(World world, int x, int z) throws IOException {
int xx = x * 16;
int zz = z * 16;
Engine engine = IrisToolbelt.access(world).getEngine();
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
int height = engine.getHeight(xx + i, zz + j);
if (height > 300) {
File found = new File("plugins", "iris" + File.separator + "found.txt");
found.getParentFile().mkdirs();
IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz);
IrisLogging.info("Found at! " + xx + ", " + zz + " Biome ID: " + biome.getName());
try (FileWriter writer = new FileWriter(found, true)) {
writer.write("Biome at: X: " + xx + " Z: " + zz + " Biome ID: " + biome.getName() + "\n");
}
return;
}
}
}
}
public Position2 getChunk(int position) {
int p = -1;
AtomicInteger xx = new AtomicInteger();
AtomicInteger zz = new AtomicInteger();
Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
xx.set(x);
zz.set(z);
});
while (s.hasNext() && p++ < position) {
s.next();
}
return new Position2(xx.get(), zz.get());
}
public void save() {
J.a(() -> {
try {
saveNow();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
public static void setPausedDeep(World world) {
DeepSearchJob job = jobs.get(world.getName());
if (isPausedDeep(world)){
job.paused = false;
} else {
job.paused = true;
}
if ( job.paused) {
IrisLogging.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Paused");
} else {
IrisLogging.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Resumed");
}
}
public static boolean isPausedDeep(World world) {
DeepSearchJob job = jobs.get(world.getName());
return job != null && job.isPaused();
}
public void shutdownInstance(World world) throws IOException {
IrisLogging.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Shutting down..");
DeepSearchJob job = jobs.get(world.getName());
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File deepFile = new File(worldDirectory, "DeepSearch.json");
if (job == null) {
IrisLogging.error("No DeepSearch job found for world: " + world.getName());
return;
}
try {
if (!job.isPaused()) {
job.setPaused(true);
}
save();
jobs.remove(world.getName());
J.a(() -> {
while (deepFile.exists()) {
deepFile.delete();
J.sleep(1000);
}
IrisLogging.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed.");
}, 20);
} catch (Exception e) {
IrisLogging.error("Failed to shutdown DeepSearch for " + world.getName());
e.printStackTrace();
} finally {
saveNow();
interrupt();
}
}
public void saveNow() throws IOException {
IO.writeAll(this.destination, new Gson().toJson(job));
}
@Data
@lombok.Builder
public static class DeepSearchJob {
private World world;
@lombok.Builder.Default
private int radiusBlocks = 5000;
@lombok.Builder.Default
private int position = 0;
@lombok.Builder.Default
boolean paused = false;
}
}
@@ -67,6 +67,8 @@ public class IrisPregenerator {
private final KSet<Position2> net;
private final ChronoLatch cl;
private final ChronoLatch saveLatch;
private final ChronoLatch heapReclaimLatch;
private final AtomicLong failed;
private final IrisPackBenchmarking benchmarking;
public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) {
@@ -74,6 +76,8 @@ public class IrisPregenerator {
this.listener = listenify(listener);
cl = new ChronoLatch(10000);
saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs());
heapReclaimLatch = new ChronoLatch(1000);
failed = new AtomicLong(0);
generatedRegions = new KSet<>();
this.shutdown = new AtomicBoolean(false);
this.paused = new AtomicBoolean(false);
@@ -95,7 +99,6 @@ public class IrisPregenerator {
cachedLast = new AtomicLong(0);
cachedLastMinute = new AtomicLong(0);
totalChunks = new AtomicLong(0);
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
startTime = new AtomicLong(M.ms());
ticker = new Looper() {
@Override
@@ -175,13 +178,27 @@ public class IrisPregenerator {
public void start() {
init();
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
startTime.set(M.ms());
ticker.start();
checkRegions();
PrecisionStopwatch p = PrecisionStopwatch.start();
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
IrisLogging.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
shutdown();
try {
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
long failedCount = failed.get();
if (failedCount > 0) {
IrisLogging.warn("Pregen finished with " + Form.f(failedCount) + " failed chunk(s); failures are not cached, rerun to fill them");
}
IrisLogging.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Pregen aborted after " + Form.duration((long) p.getMilliseconds()) + " due to " + e.getClass().getSimpleName() + ": " + e.getMessage());
} finally {
shutdown();
}
if (benchmarking == null) {
IrisLogging.info(C.IRIS + "Pregen stopped.");
} else {
@@ -266,12 +283,20 @@ public class IrisPregenerator {
hit = true;
listener.onRegionGenerating(x, z);
task.iterateChunks(x, z, (xx, zz) -> {
while (paused.get() && !shutdown.get()) {
while ((paused.get() || MantleHeapPressure.overHighWater()) && !shutdown.get()) {
if (!paused.get()) {
reclaimHeapPressure();
}
J.sleep(50);
}
if (shutdown.get()) {
return;
}
generator.generateChunk(xx, zz, listener);
});
generator.onRegionSubmitted(x, z);
}
if (hit) {
@@ -280,9 +305,13 @@ public class IrisPregenerator {
if (saveLatch.flip()) {
listener.onSaving();
generator.save();
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
try {
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
@@ -291,6 +320,26 @@ public class IrisPregenerator {
}
}
private void reclaimHeapPressure() {
if (!heapReclaimLatch.flip()) {
return;
}
try {
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
mantle.unloadTectonicPlate(0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
}
private void checkRegion(int x, int z) {
if (generatedRegions.contains(new Position2(x, z))) {
return;
@@ -299,6 +348,10 @@ public class IrisPregenerator {
generator.supportsRegions(x, z, listener);
}
public long getFailedChunks() {
return failed.get();
}
public void pause() {
paused.set(true);
}
@@ -326,6 +379,12 @@ public class IrisPregenerator {
if (c) cached.addAndGet(1);
}
@Override
public void onChunkFailed(int x, int z) {
failed.addAndGet(1);
listener.onChunkFailed(x, z);
}
@Override
public void onRegionGenerated(int x, int z) {
listener.onRegionGenerated(x, z);
@@ -0,0 +1,82 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pregenerator;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public final class MantleHeapPressure {
private static final double HIGH_WATER = 0.92D;
private static final double LOW_WATER = 0.82D;
private static final double PANIC_WATER = 0.96D;
private static final long PANIC_GC_INTERVAL_MS = 30_000L;
private static final AtomicBoolean engaged = new AtomicBoolean(false);
private static final AtomicLong lastPanicGcAt = new AtomicLong(0L);
private MantleHeapPressure() {
}
public static double usedFraction() {
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
long max = heap.getMax();
if (max <= 0L) {
Runtime runtime = Runtime.getRuntime();
long runtimeMax = runtime.maxMemory();
if (runtimeMax <= 0L) {
return 0.0D;
}
long used = runtime.totalMemory() - runtime.freeMemory();
return (double) used / (double) runtimeMax;
}
return (double) heap.getUsed() / (double) max;
}
public static boolean overHighWater() {
double fraction = usedFraction();
if (engaged.get()) {
if (fraction <= LOW_WATER) {
engaged.set(false);
return false;
}
return true;
}
if (fraction >= HIGH_WATER) {
engaged.set(true);
return true;
}
return false;
}
public static boolean overPanicWater() {
return usedFraction() >= PANIC_WATER;
}
public static void requestPanicReclaim() {
long now = System.currentTimeMillis();
long last = lastPanicGcAt.get();
if (now - last < PANIC_GC_INTERVAL_MS) {
return;
}
if (lastPanicGcAt.compareAndSet(last, now)) {
System.gc();
}
}
}
@@ -29,6 +29,9 @@ public interface PregenListener {
void onChunkGenerated(int x, int z, boolean cached);
default void onChunkFailed(int x, int z) {
}
void onRegionGenerated(int x, int z);
void onRegionGenerating(int x, int z);
@@ -0,0 +1,148 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import java.util.function.Supplier;
public final class PregenMantleBackpressure {
private final Supplier<Mantle> mantleSupplier;
private final int maxResidentTectonicPlates;
private final int waitMs;
private final long timeoutMs;
private final Runnable onBudgetTimeout;
private final Supplier<String> diagnostics;
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics) {
this.mantleSupplier = mantleSupplier;
this.maxResidentTectonicPlates = maxResidentTectonicPlates;
this.waitMs = waitMs;
this.timeoutMs = timeoutMs;
this.onBudgetTimeout = onBudgetTimeout;
this.diagnostics = diagnostics;
}
public void apply() {
enforceMantleBudget();
awaitHeapHeadroom();
}
public void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
int freed;
int resident;
try {
mantle.trim(0L, 0);
freed = mantle.unloadTectonicPlate(0);
resident = mantle.getLoadedRegionCount();
} catch (Throwable e) {
IrisLogging.reportError(e);
break;
}
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs) {
IrisLogging.warn("Pregen mantle backpressure exceeded " + timeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + diagnostics.get());
onBudgetTimeout.run();
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(waitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
public void awaitHeapHeadroom() {
Mantle mantle = resolveMantle();
long lastLog = 0L;
while (MantleHeapPressure.overHighWater()) {
try {
if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) {
mantle.trim(0L, 0);
mantle.unloadTectonicPlate(0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen heap pressure: pausing generation at "
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; evicting tectonic plates and waiting for headroom"
+ (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + ".");
}
try {
Thread.sleep(waitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private Mantle resolveMantle() {
try {
return mantleSupplier.get();
} catch (Throwable ignored) {
return null;
}
}
}
@@ -0,0 +1,64 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import java.util.concurrent.atomic.AtomicBoolean;
public final class PregenPerformanceProfile {
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
private PregenPerformanceProfile() {
}
public static boolean apply() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void apply(Engine engine) {
boolean changed = apply();
if (changed && engine != null) {
engine.hotloadComplex();
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
}
@@ -19,7 +19,6 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.Spiraled;
@@ -29,12 +28,20 @@ import lombok.Data;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Builder
@Data
public class PregenTask {
private static final KMap<Long, int[]> ORDERS = new KMap<>();
private static final int MAX_CACHED_ORDERS = 512;
private static final LinkedHashMap<Long, int[]> ORDERS = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, int[]> eldest) {
return size() > MAX_CACHED_ORDERS;
}
};
@Builder.Default
private final boolean gui = false;
@@ -71,7 +78,18 @@ public class PregenTask {
private static int[] orderForPull(int pullX, int pullZ) {
long key = orderKey(pullX, pullZ);
return ORDERS.computeIfAbsent(key, PregenTask::computeOrder);
synchronized (ORDERS) {
int[] cached = ORDERS.get(key);
if (cached != null) {
return cached;
}
}
int[] computed = computeOrder(key);
synchronized (ORDERS) {
ORDERS.put(key, computed);
}
return computed;
}
private static int[] computeOrder(long key) {
@@ -119,6 +137,11 @@ public class PregenTask {
}));
}
public int[] regionBounds() {
Bound bound = bounds.region();
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
}
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
@@ -80,5 +80,11 @@ public interface PregeneratorMethod {
*/
void generateChunk(int x, int z, PregenListener listener);
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
}
default void onRegionSubmitted(int regionX, int regionZ) {
}
Mantle getMantle();
}
@@ -196,6 +196,16 @@ public class PregenCacheImpl implements PregenCache {
boolean test(Region region);
}
private enum CacheResult {
NOOP,
SET,
COMPLETED
}
private interface RegionCacheOp {
CacheResult apply(Region region);
}
private static class Plate {
private final int x;
private final int z;
@@ -216,7 +226,7 @@ public class PregenCacheImpl implements PregenCache {
this.lastAccess = System.currentTimeMillis();
}
private boolean cache(int x, int z, RegionPredicate predicate) {
private boolean cache(int x, int z, RegionCacheOp op) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return false;
@@ -229,7 +239,13 @@ public class PregenCacheImpl implements PregenCache {
regions[index] = region;
}
if (!predicate.test(region)) {
CacheResult result = op.apply(region);
if (result == CacheResult.NOOP) {
return false;
}
dirty = true;
if (result != CacheResult.COMPLETED) {
return false;
}
@@ -237,7 +253,6 @@ public class PregenCacheImpl implements PregenCache {
if (count == SIZE) {
regions = null;
}
dirty = true;
return true;
}
@@ -282,23 +297,23 @@ public class PregenCacheImpl implements PregenCache {
this.words = words;
}
private boolean cache() {
private CacheResult cache() {
if (count == SIZE) {
return false;
return CacheResult.NOOP;
}
count = SIZE;
words = null;
return true;
return CacheResult.COMPLETED;
}
private boolean cache(int x, int z) {
private CacheResult cache(int x, int z) {
if (count == SIZE) {
return false;
return CacheResult.NOOP;
}
long[] value = words;
if (value == null) {
return false;
return CacheResult.NOOP;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
@@ -306,17 +321,17 @@ public class PregenCacheImpl implements PregenCache {
long bit = 1L << (index & 63);
boolean current = (value[wordIndex] & bit) != 0L;
if (current) {
return false;
return CacheResult.NOOP;
}
count++;
if (count == SIZE) {
words = null;
return true;
return CacheResult.COMPLETED;
}
value[wordIndex] = value[wordIndex] | bit;
return false;
return CacheResult.SET;
}
private boolean isCached() {
@@ -70,6 +70,16 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
method.generateChunk(x, z, listener);
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
}
@Override
public Mantle getMantle() {
return method.getMantle();
@@ -23,9 +23,13 @@ import art.arcane.iris.core.IrisPaperLikeBackendMode;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenMantleBackpressure;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.parallel.MultiBurst;
@@ -37,15 +41,16 @@ import org.bukkit.World;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -53,8 +58,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CHUNK_CLEANUP_INTERVAL_MS = 10_000L;
private static final long CHUNK_CLEANUP_MIN_AGE_MS = 5_000L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -73,12 +76,20 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final boolean urgent;
private final Map<Chunk, Long> lastUse;
private final ConcurrentLinkedQueue<ChunkUse> chunkUseQueue;
private final ConcurrentHashMap<Long, AtomicInteger> regionPending;
private final ConcurrentHashMap<Long, Queue<Chunk>> regionChunks;
private final KSet<Long> drainedRegions;
private final KSet<Long> evictedRegions;
private volatile int evictionWindowRegions;
private volatile int boundsMinRegionX;
private volatile int boundsMinRegionZ;
private volatile int boundsMaxRegionX;
private volatile int boundsMaxRegionZ;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicLong lastFailedReleaseLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
@@ -86,14 +97,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms());
private final AtomicBoolean chunkCleanupRunning = new AtomicBoolean(false);
private final Object permitMonitor = new Object();
private volatile Engine metricsEngine;
private volatile Mantle cachedMantle;
private final int maxResidentTectonicPlates;
private final int mantleBackpressureWaitMs;
private final long mantleBackpressureTimeoutMs;
private final PregenMantleBackpressure backpressure;
public AsyncPregenMethod(World world, int unusedThreads) {
if (!PaperLib.isPaper()) {
@@ -140,13 +147,25 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.urgent = false;
this.lastUse = new ConcurrentHashMap<>();
this.chunkUseQueue = new ConcurrentLinkedQueue<>();
this.regionPending = new ConcurrentHashMap<>();
this.regionChunks = new ConcurrentHashMap<>();
this.drainedRegions = new KSet<>();
this.evictedRegions = new KSet<>();
this.evictionWindowRegions = -1;
this.boundsMinRegionX = Integer.MIN_VALUE;
this.boundsMinRegionZ = Integer.MIN_VALUE;
this.boundsMaxRegionX = Integer.MAX_VALUE;
this.boundsMaxRegionZ = Integer.MAX_VALUE;
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
int pregenWorldHeight = world.getMaxHeight() - world.getMinHeight();
this.backpressure = new PregenMantleBackpressure(
this::resolveMantle,
pregen.getEffectiveResidentTectonicPlates(pregenWorldHeight),
pregen.getMantleBackpressureWaitMs(),
pregen.getMantleBackpressureTimeoutMs(),
this::lowerAdaptiveInFlightLimit,
this::metricsSnapshot);
}
private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) {
@@ -173,124 +192,242 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return -1;
}
private void unloadAndSaveAllChunks() {
if (foliaRuntime) {
lastUse.clear();
chunkUseQueue.clear();
private static long rkey(int rx, int rz) {
return (((long) rx) << 32) | (rz & 0xFFFFFFFFL);
}
private int evictionWindow() {
int cached = evictionWindowRegions;
if (cached > 0) {
return cached;
}
Engine engine = resolveMetricsEngine();
if (engine == null) {
return 2;
}
try {
int radius = engine.getMantle().getRadius();
int resolved = radius > 0 ? Math.max(1, (int) Math.ceil(radius / 32.0)) : 2;
evictionWindowRegions = resolved;
return resolved;
} catch (Throwable ignored) {
return 2;
}
}
private void onChunkCompleted(int x, int z, Chunk chunk) {
if (chunk == null) {
return;
}
if (lastUse.isEmpty()) {
try {
long rk = rkey(x >> 5, z >> 5);
regionChunks.computeIfAbsent(rk, k -> new ConcurrentLinkedQueue<>()).add(chunk);
AtomicInteger pending = regionPending.get(rk);
if (pending != null && pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void onChunkFailedToLoad(int x, int z) {
try {
long rk = rkey(x >> 5, z >> 5);
AtomicInteger pending = regionPending.get(rk);
if (pending != null && pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
long now = M.ms();
long last = lastFailedReleaseLogAt.get();
if (now - last >= timeoutWarnIntervalMs && lastFailedReleaseLogAt.compareAndSet(last, now)) {
IrisLogging.warn("Released region slot for failed or timed out chunk at " + x + "," + z + ". " + metricsSnapshot());
}
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
boundsMinRegionX = minRegionX;
boundsMinRegionZ = minRegionZ;
boundsMaxRegionX = maxRegionX;
boundsMaxRegionZ = maxRegionZ;
}
private boolean inBounds(int rx, int rz) {
return rx >= boundsMinRegionX && rx <= boundsMaxRegionX && rz >= boundsMinRegionZ && rz <= boundsMaxRegionZ;
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
try {
long rk = rkey(regionX, regionZ);
AtomicInteger pending = regionPending.get(rk);
if (pending == null || pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void onRegionDrained(long rk) {
if (!drainedRegions.add(rk)) {
return;
}
int w = evictionWindow();
int rx = (int) (rk >> 32);
int rz = (int) rk;
for (int dx = -w; dx <= w; dx++) {
for (int dz = -w; dz <= w; dz++) {
int cx = rx + dx;
int cz = rz + dz;
long candidate = rkey(cx, cz);
if (evictedRegions.contains(candidate)) {
continue;
}
if (!drainedRegions.contains(candidate)) {
continue;
}
if (allNeighborsDrained(cx, cz, w)) {
evictRegion(candidate);
}
}
}
}
private boolean allNeighborsDrained(int rx, int rz, int w) {
for (int dx = -w; dx <= w; dx++) {
for (int dz = -w; dz <= w; dz++) {
int nx = rx + dx;
int nz = rz + dz;
if (!inBounds(nx, nz)) {
continue;
}
if (!drainedRegions.contains(rkey(nx, nz))) {
return false;
}
}
}
return true;
}
private void evictRegion(long c) {
if (!evictedRegions.add(c)) {
return;
}
regionPending.remove(c);
Queue<Chunk> chunks = regionChunks.remove(c);
if (chunks == null || chunks.isEmpty()) {
return;
}
try {
if (foliaRuntime) {
Chunk anchor = null;
for (Chunk chunk : chunks) {
if (chunk == null) {
continue;
}
if (anchor == null) {
anchor = chunk;
}
int cx = chunk.getX();
int cz = chunk.getZ();
if (!J.runRegion(world, cx, cz, () -> unloadChunkSafely(cx, cz))) {
unloadChunkSafely(cx, cz);
}
}
if (anchor != null) {
int ax = anchor.getX();
int az = anchor.getZ();
if (!J.runRegion(world, ax, az, () -> INMS.get().flushChunkIO(world))) {
INMS.get().flushChunkIO(world);
}
}
return;
}
J.s(() -> {
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
INMS.get().flushChunkIO(world);
});
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void unloadChunkSafely(int cx, int cz) {
try {
world.removePluginChunkTicket(cx, cz, BukkitPlatform.plugin());
} catch (Throwable ignored) {
}
try {
if (!INMS.get().saveAndUnloadChunk(world, cx, cz)) {
world.unloadChunk(cx, cz, true);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void flushAllRemainingChunks() {
List<Long> keys = new ArrayList<>(regionChunks.keySet());
if (foliaRuntime) {
for (Long rk : keys) {
evictRegion(rk);
}
return;
}
try {
J.sfut(() -> {
if (world == null) {
IrisLogging.warn("World was null somehow...");
return;
for (Long rk : keys) {
if (!evictedRegions.add(rk)) {
continue;
}
regionPending.remove(rk);
Queue<Chunk> chunks = regionChunks.remove(rk);
if (chunks == null) {
continue;
}
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
}
long minTime = M.ms() - 10_000;
AtomicBoolean unloaded = new AtomicBoolean(false);
lastUse.entrySet().removeIf(i -> {
final Chunk chunk = i.getKey();
final Long lastUseTime = i.getValue();
if (!chunk.isLoaded() || lastUseTime == null)
return true;
if (lastUseTime < minTime) {
chunk.unload();
unloaded.set(true);
return true;
}
return false;
});
if (unloaded.get()) {
world.save();
}
if (lastUse.isEmpty()) {
chunkUseQueue.clear();
}
world.save();
INMS.get().flushChunkIO(world);
}).get();
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
private void periodicChunkCleanup() {
long now = M.ms();
long lastCleanup = lastChunkCleanup.get();
if (now - lastCleanup < CHUNK_CLEANUP_INTERVAL_MS) {
return;
}
if (!lastChunkCleanup.compareAndSet(lastCleanup, now)) {
return;
}
if (foliaRuntime) {
int sizeBefore = lastUse.size();
if (sizeBefore > 0) {
lastUse.clear();
chunkUseQueue.clear();
IrisLogging.info("Periodic chunk cleanup: cleared " + sizeBefore + " Folia chunk references");
}
return;
}
int sizeBefore = lastUse.size();
if (sizeBefore == 0) {
return;
}
if (!chunkCleanupRunning.compareAndSet(false, true)) {
return;
}
long minTime = now - CHUNK_CLEANUP_MIN_AGE_MS;
J.a(() -> {
try {
int removedCount = cleanupQueuedChunkUses(minTime);
if (removedCount > 0) {
IrisLogging.info("Periodic chunk cleanup: removed " + removedCount + "/" + sizeBefore + " stale chunk references");
}
} finally {
chunkCleanupRunning.set(false);
}
});
}
private int cleanupQueuedChunkUses(long minTime) {
int removed = 0;
while (true) {
ChunkUse chunkUse = chunkUseQueue.peek();
if (chunkUse == null) {
return removed;
}
Long latestUse = lastUse.get(chunkUse.chunk());
if (latestUse != null && latestUse > chunkUse.lastUseTime()) {
chunkUseQueue.poll();
continue;
}
if (latestUse != null && latestUse >= minTime) {
return removed;
}
chunkUseQueue.poll();
if (latestUse != null && lastUse.remove(chunkUse.chunk(), latestUse)) {
removed++;
}
}
}
private void recordChunkUse(Chunk chunk) {
long now = M.ms();
lastUse.put(chunk, now);
chunkUseQueue.offer(new ChunkUse(chunk, now));
}
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
Throwable root = throwable;
while (root.getCause() != null) {
@@ -527,56 +664,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return resolved;
}
private void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
mantle.trim(0L, 0);
int freed = mantle.unloadTectonicPlate(0);
int resident = mantle.getLoadedRegionCount();
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= mantleBackpressureTimeoutMs) {
IrisLogging.warn("Pregen mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(mantleBackpressureWaitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
@Override
public void init() {
IrisLogging.info("Async pregen init: world=" + world.getName()
@@ -591,7 +678,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
+ ", urgent=" + urgent
+ ", timeout=" + timeoutSeconds + "s");
unloadAndSaveAllChunks();
if (workerPoolThreads > 0) {
increaseWorkerThreads();
}
@@ -610,14 +696,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
unloadAndSaveAllChunks();
flushAllRemainingChunks();
executor.shutdown();
resetWorkerThreads();
}
@Override
public void save() {
unloadAndSaveAllChunks();
}
@Override
@@ -633,8 +718,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
periodicChunkCleanup();
enforceMantleBudget();
backpressure.enforceMantleBudget();
backpressure.awaitHeapHeadroom();
try {
long waitStart = M.ms();
synchronized (permitMonitor) {
@@ -659,6 +744,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return;
}
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
markSubmitted();
executor.generate(x, z, listener);
}
@@ -833,6 +919,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) {
markFinished(false);
semaphore.release();
listener.onChunkFailed(x, z);
IrisLogging.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot());
}
}
@@ -842,17 +929,21 @@ public class AsyncPregenMethod implements PregeneratorMethod {
try {
if (throwable != null) {
onChunkFutureFailure(x, z, throwable);
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
if (chunk == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(chunk);
onChunkCompleted(x, z, chunk);
success = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -877,13 +968,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
.get();
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(i);
onChunkCompleted(x, z, i);
success = true;
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
@@ -913,13 +1006,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
boolean success = false;
try {
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(i);
onChunkCompleted(x, z, i);
success = true;
} finally {
markFinished(success);
@@ -929,9 +1024,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private record ChunkUse(Chunk chunk, long lastUseTime) {
}
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
}
}
@@ -1,27 +1,22 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class CachedPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod method;
private final PregenCache cache;
private final PregenTask task;
private volatile PregenListener wrappedSource;
private volatile PregenListener wrappedListener;
public CachedPregenMethod(PregeneratorMethod method, String worldName) {
public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) {
this.method = method;
PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName);
if (cache == null) {
IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback");
cache = GlobalCacheSVC.createDefault(worldName);
}
this.cache = cache;
this.cache = cache.sync();
this.task = task;
}
@Override
@@ -31,6 +26,7 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void close() {
cache.write();
method.close();
cache.write();
}
@@ -48,6 +44,9 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public String getMethod(int x, int z) {
if (cache.isRegionCached(x, z)) {
return "Cached";
}
return method.getMethod(x, z);
}
@@ -55,14 +54,10 @@ public class CachedPregenMethod implements PregeneratorMethod {
public void generateRegion(int x, int z, PregenListener listener) {
if (cache.isRegionCached(x, z)) {
listener.onRegionGenerated(x, z);
int rX = x << 5, rZ = z << 5;
for (int cX = 0; cX < 32; cX++) {
for (int cZ = 0; cZ < 32; cZ++) {
listener.onChunkGenerated(rX + cX, rZ + cZ, true);
listener.onChunkCleaned(rX + cX, rZ + cZ);
}
}
task.iterateChunks(x, z, (cX, cZ) -> {
listener.onChunkGenerated(cX, cZ, true);
listener.onChunkCleaned(cX, cZ);
});
return;
}
method.generateRegion(x, z, listener);
@@ -76,8 +71,113 @@ public class CachedPregenMethod implements PregeneratorMethod {
listener.onChunkCleaned(x, z);
return;
}
method.generateChunk(x, z, listener);
cache.cacheChunk(x, z);
method.generateChunk(x, z, cachingListener(listener));
}
private PregenListener cachingListener(PregenListener listener) {
PregenListener source = wrappedSource;
PregenListener wrapped = wrappedListener;
if (source == listener && wrapped != null) {
return wrapped;
}
PregenListener created = new PregenListener() {
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached);
}
@Override
public void onChunkGenerating(int x, int z) {
listener.onChunkGenerating(x, z);
}
@Override
public void onChunkGenerated(int x, int z, boolean cachedChunk) {
if (!cachedChunk) {
cache.cacheChunk(x, z);
}
listener.onChunkGenerated(x, z, cachedChunk);
}
@Override
public void onChunkFailed(int x, int z) {
listener.onChunkFailed(x, z);
}
@Override
public void onRegionGenerated(int x, int z) {
listener.onRegionGenerated(x, z);
}
@Override
public void onRegionGenerating(int x, int z) {
listener.onRegionGenerating(x, z);
}
@Override
public void onChunkCleaned(int x, int z) {
listener.onChunkCleaned(x, z);
}
@Override
public void onRegionSkipped(int x, int z) {
listener.onRegionSkipped(x, z);
}
@Override
public void onNetworkStarted(int x, int z) {
listener.onNetworkStarted(x, z);
}
@Override
public void onNetworkFailed(int x, int z) {
listener.onNetworkFailed(x, z);
}
@Override
public void onNetworkReclaim(int revert) {
listener.onNetworkReclaim(revert);
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
listener.onNetworkGeneratedChunk(x, z);
}
@Override
public void onNetworkDownloaded(int x, int z) {
listener.onNetworkDownloaded(x, z);
}
@Override
public void onClose() {
listener.onClose();
}
@Override
public void onSaving() {
listener.onSaving();
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
listener.onChunkExistsInRegionGen(x, z);
}
};
wrappedSource = listener;
wrappedListener = created;
return created;
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
}
@Override
@@ -67,6 +67,16 @@ public class HybridPregenMethod implements PregeneratorMethod {
inWorld.generateChunk(x, z, listener);
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
inWorld.onRegionSubmitted(regionX, regionZ);
}
@Override
public Mantle getMantle() {
return inWorld.getMantle();
@@ -0,0 +1,69 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.project;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.stream.Stream;
public final class IrisProjectCopier {
private IrisProjectCopier() {
}
public static void copyProject(File sourcePack, File targetPack, String sourceKey, String targetKey) throws IOException {
Path source = sourcePack.toPath();
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.sorted(Comparator.naturalOrder()).toList()) {
String relative = source.relativize(path).toString();
if (relative.isEmpty() || relative.equals(".git") || relative.startsWith(".git" + File.separator) || relative.endsWith(".code-workspace")) {
continue;
}
Path destination = targetPack.toPath().resolve(relative);
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
File oldDimension = new File(targetPack, "dimensions/" + sourceKey + ".json");
File newDimension = new File(targetPack, "dimensions/" + targetKey + ".json");
if (oldDimension.isFile() && !oldDimension.equals(newDimension)) {
Files.copy(oldDimension.toPath(), newDimension.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.delete(oldDimension.toPath());
}
if (newDimension.isFile()) {
JSONObject json = new JSONObject(IO.readAll(newDimension));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(targetKey.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimension, json.toString(4));
}
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.core.runtime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
@@ -29,8 +30,6 @@ import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -61,19 +60,7 @@ public final class ChunkJobReporter {
}
public static List<int[]> orderedTargets(int centerChunkX, int centerChunkZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerChunkX + dx, centerChunkZ + dz});
}
}
targets.sort(Comparator.comparingInt(t -> {
int ox = t[0] - centerChunkX;
int oz = t[1] - centerChunkZ;
return ox * ox + oz * oz;
}));
return targets;
return ChunkSpiral.centerOut(centerChunkX, centerChunkZ, radius);
}
public void start() {
@@ -0,0 +1,484 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.runtime;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class GoldenHashEngine {
public enum Mode {
AUTO,
CAPTURE,
VERIFY
}
public interface ChunkSnapshot {
int minY();
int maxY();
PlatformBlockState block(int x, int y, int z);
PlatformBiome biome(int x, int y, int z);
}
public interface ChunkSource {
ChunkSnapshot generate(int chunkX, int chunkZ) throws Exception;
}
public interface Feedback {
void ok(String message);
void warn(String message);
void fail(String message);
}
public interface Progress {
default void stage(String stage) {
}
default void total(int total) {
}
default void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
}
default void chunkFailed(int chunkX, int chunkZ, Throwable error) {
}
}
public record Request(String worldName, long seed, String mcVersion, int minY, int maxY, int centerChunkX,
int centerChunkZ, int radius, int threads, Mode mode, boolean resetMantle, boolean deep,
String nullBiomeKey) {
}
private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0);
private static final String FORMAT = "iris-goldenhash v1";
private static final int BIOME_STEP = 4;
private static final int MAX_REPORTED_MISMATCHES = 10;
private final Engine engine;
private final Request request;
private final ChunkSource source;
private final Feedback feedback;
private final Progress progress;
private final int radius;
private final int threads;
private final File goldenFile;
public GoldenHashEngine(Engine engine, Request request, File goldenDir, ChunkSource source, Feedback feedback, Progress progress) {
this.engine = engine;
this.request = request;
this.source = source;
this.feedback = feedback;
this.progress = progress;
this.radius = Math.max(0, request.radius());
this.threads = Math.max(1, request.threads());
goldenDir.mkdirs();
this.goldenFile = new File(goldenDir, engine.getDimension().getLoadKey()
+ "-s" + request.seed()
+ "-c" + request.centerChunkX() + "x" + request.centerChunkZ()
+ "-r" + this.radius + ".hashes");
}
public static boolean isActive() {
return ACTIVE_SCANS.get() > 0;
}
public File getGoldenFile() {
return goldenFile;
}
public boolean run() {
ACTIVE_SCANS.incrementAndGet();
try {
boolean exists = goldenFile.exists();
if (request.mode() == Mode.VERIFY && !exists) {
feedback.fail("No golden capture at " + goldenFile.getAbsolutePath() + "; run a capture first.");
return false;
}
if (request.resetMantle()) {
progress.stage("Resetting mantle");
resetMantleFull();
}
List<int[]> targets = ChunkSpiral.centerOut(request.centerChunkX(), request.centerChunkZ(), radius);
progress.total(targets.size());
progress.stage("Generating");
Map<Long, String> lines = scan(targets);
if (lines.size() != targets.size()) {
feedback.fail("GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
return false;
}
progress.stage("Comparing");
if (request.mode() == Mode.CAPTURE || (request.mode() == Mode.AUTO && !exists)) {
capture(lines);
return true;
}
return verify(lines);
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.fail("GoldenHash failed: " + e);
return false;
} finally {
ACTIVE_SCANS.decrementAndGet();
}
}
private void resetMantleFull() {
try {
Mantle mantle = engine.getMantle().getMantle();
mantle.saveAll();
File folder = mantle.getDataFolder();
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
file.delete();
}
}
}
feedback.ok("Mantle reset (" + folder.getAbsolutePath() + ")");
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.warn("Mantle reset failed (" + e.getClass().getSimpleName() + "); continuing with existing mantle state.");
}
}
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
ok = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
progress.chunkFailed(chunkX, chunkZ, e);
} finally {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
inFlight.release();
done.countDown();
}
});
}
done.await();
return lines;
}
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
int minY = snapshot.minY();
int maxY = snapshot.maxY();
IdentityHashMap<PlatformBlockState, byte[]> blockCache = new IdentityHashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
byte[] nullBiome = (request.nullBiomeKey() + "\n").getBytes(StandardCharsets.UTF_8);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
PlatformBlockState data = snapshot.block(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(data, (PlatformBlockState d) -> (d.key() + "\n").getBytes(StandardCharsets.UTF_8));
blockDigest.update(bytes);
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = minY; y < maxY; y += BIOME_STEP) {
PlatformBiome biome = snapshot.biome(x, y, z);
byte[] bytes = biome == null
? nullBiome
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8));
biomeDigest.update(bytes);
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private void capture(Map<Long, String> lines) throws IOException {
List<String> body = orderedBody(lines);
String combined = combinedHash(body);
List<String> out = new ArrayList<>();
out.add("#" + FORMAT);
out.add("#world=" + request.worldName());
out.add("#dim=" + engine.getDimension().getLoadKey());
out.add("#seed=" + request.seed());
out.add("#mc=" + request.mcVersion());
out.add("#minY=" + request.minY() + " maxY=" + request.maxY());
out.add("#center=" + request.centerChunkX() + "," + request.centerChunkZ());
out.add("#radius=" + radius);
out.addAll(body);
out.add("#combined=" + combined);
Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
feedback.ok("Golden captured: " + body.size() + " chunks combined=" + shortHash(combined));
feedback.ok(goldenFile.getAbsolutePath());
IrisLogging.info("goldenhash captured: " + goldenFile.getAbsolutePath() + " combined=" + combined);
}
private boolean verify(Map<Long, String> lines) throws IOException {
List<String> existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
Map<String, String> meta = new HashMap<>();
Map<String, String> goldenChunks = new HashMap<>();
for (String line : existing) {
if (line.startsWith("#")) {
int eq = line.indexOf('=');
if (eq > 0) {
meta.put(line.substring(1, eq), line.substring(eq + 1));
}
} else if (!line.isBlank()) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
goldenChunks.put(line.substring(0, second), line);
}
}
String expectedSeed = String.valueOf(request.seed());
String expectedDim = engine.getDimension().getLoadKey();
if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
feedback.fail("Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
return false;
}
if (!request.mcVersion().equals(meta.get("mc"))) {
feedback.warn("Golden was captured on mc=" + meta.get("mc") + ", running mc=" + request.mcVersion() + ". Diffs may be version-induced.");
}
List<String> body = orderedBody(lines);
List<String> mismatches = new ArrayList<>();
for (String line : body) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
String key = line.substring(0, second);
String golden = goldenChunks.get(key);
if (!line.equals(golden)) {
mismatches.add(key + (golden == null ? " (missing in golden)" : ""));
}
}
String combined = combinedHash(body);
if (mismatches.isEmpty()) {
feedback.ok("GOLDEN MATCH: " + body.size() + "/" + goldenChunks.size() + " chunks, combined=" + shortHash(combined));
IrisLogging.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined);
return true;
}
feedback.fail("GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
feedback.fail(" chunk " + mismatches.get(i));
}
if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
feedback.fail(" ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more");
}
File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new");
List<String> out = new ArrayList<>(body);
out.add("#combined=" + combined);
Files.write(current.toPath(), out, StandardCharsets.UTF_8);
feedback.warn("Current hashes written to " + current.getName());
IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
progress.stage("Diagnosing");
diagnose(mismatches.getFirst());
return false;
}
private void diagnose(String mismatchKey) {
try {
String[] parts = mismatchKey.trim().split(" ");
int chunkX = Integer.parseInt(parts[0]);
int chunkZ = Integer.parseInt(parts[1]);
ChunkSnapshot first = source.generate(chunkX, chunkZ);
ChunkSnapshot second = source.generate(chunkX, chunkZ);
int minY = first.minY();
int maxY = first.maxY();
List<String> diffs = new ArrayList<>();
for (int x = 0; x < 16 && diffs.size() < 50; x++) {
for (int z = 0; z < 16 && diffs.size() < 50; z++) {
for (int y = minY; y < maxY && diffs.size() < 50; y++) {
String a = first.block(x, y, z).key();
String b = second.block(x, y, z).key();
if (!a.equals(b)) {
diffs.add(x + " " + y + " " + z + " | " + a + " | " + b);
}
}
}
}
List<String> mantleDiffs = new ArrayList<>();
String mantleStatus;
try {
EngineMantle engineMantle = engine.getMantle();
int margin = Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
for (int dx = -margin; dx <= margin; dx++) {
for (int dz = -margin; dz <= margin; dz++) {
engineMantle.getMantle().deleteChunk(chunkX + dx, chunkZ + dz);
}
}
ChunkSnapshot reset = source.generate(chunkX, chunkZ);
for (int x = 0; x < 16 && mantleDiffs.size() < 80; x++) {
for (int z = 0; z < 16 && mantleDiffs.size() < 80; z++) {
for (int y = minY; y < maxY && mantleDiffs.size() < 80; y++) {
String a = first.block(x, y, z).key();
String b = reset.block(x, y, z).key();
if (!a.equals(b)) {
mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b);
}
}
}
}
mantleStatus = mantleDiffs.isEmpty() ? "STABLE (mantle rebuild reproduces scan output)" : "DIVERGED (" + mantleDiffs.size() + "+ diffs - mantle build is state/order dependent)";
} catch (Throwable t) {
mantleDiffs.clear();
mantleStatus = "SKIPPED (" + t.getClass().getSimpleName() + ")";
}
List<String> report = new ArrayList<>();
report.add("#goldenhash diagnosis chunk=" + chunkX + "," + chunkZ);
report.add("#repeat-generation: " + (diffs.isEmpty() ? "STABLE (nondeterminism is order/state-dependent, not per-call)" : "UNSTABLE (" + diffs.size() + "+ diffs between two back-to-back generations)"));
report.addAll(diffs);
report.add("#mantle-reset regeneration: " + mantleStatus);
report.addAll(mantleDiffs);
report.add("#full non-air dump of generation 1 follows (x y z state)");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
String state = first.block(x, y, z).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
report.add(x + " " + y + " " + z + " " + state);
}
}
}
}
File diag = new File(goldenFile.getParentFile(), goldenFile.getName() + ".diag-c" + chunkX + "x" + chunkZ + ".txt");
Files.write(diag.toPath(), report, StandardCharsets.UTF_8);
String repeatPart = diffs.isEmpty() ? "Repeat-gen STABLE" : "Repeat-gen UNSTABLE (" + diffs.size() + "+ block diffs)";
String mantlePart = "mantle-reset " + mantleStatus;
if (diffs.isEmpty() && mantleDiffs.isEmpty() && mantleStatus.startsWith("STABLE")) {
feedback.warn(repeatPart + ", " + mantlePart + " -> " + diag.getName());
} else {
feedback.fail(repeatPart + ", " + mantlePart + " -> " + diag.getName());
}
IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.fail("Diagnosis failed: " + e.getMessage());
}
}
private void writeDeepDump(int chunkX, int chunkZ, ChunkSnapshot snapshot) throws IOException {
File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep"));
dir.mkdirs();
int minY = snapshot.minY();
int maxY = snapshot.maxY();
List<String> out = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
String state = snapshot.block(x, y, z).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
out.add(x + " " + y + " " + z + " " + state);
}
}
}
}
Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8);
}
private List<String> orderedBody(Map<Long, String> lines) {
Map<Long, String> sorted = new TreeMap<>(lines);
return new ArrayList<>(sorted.values());
}
private String combinedHash(List<String> body) {
MessageDigest digest = sha256();
for (String line : body) {
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
}
private static String shortHash(String hex) {
return hex.substring(0, 12);
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -18,76 +18,47 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class GoldenHashScanner {
private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0);
private static final String FORMAT = "iris-goldenhash v1";
public static boolean isScanActive() {
return ACTIVE_SCANS.get() > 0;
}
private static final int BIOME_STEP = 4;
private static final int MAX_REPORTED_MISMATCHES = 10;
private static final byte[] NULL_BIOME = "null\n".getBytes(StandardCharsets.UTF_8);
private final World world;
private final Engine engine;
private final VolmitSender sender;
private final int centerChunkX;
private final int centerChunkZ;
private final int radius;
private final boolean resetMantle;
private final int threads;
private final boolean deep;
private final File goldenFile;
private final ChunkJobReporter reporter;
private final GoldenHashEngine hashEngine;
public GoldenHashScanner(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius, boolean resetMantle, int threads, boolean deep) {
this.world = world;
this.engine = engine;
this.sender = sender;
this.centerChunkX = centerChunkX;
this.centerChunkZ = centerChunkZ;
this.radius = Math.max(0, radius);
this.resetMantle = resetMantle;
this.threads = Math.max(1, threads);
this.deep = deep;
this.goldenFile = IrisPlatforms.get().dataFile("golden", engine.getDimension().getLoadKey()
+ "-s" + world.getSeed()
+ "-c" + centerChunkX + "x" + centerChunkZ
+ "-r" + this.radius + ".hashes");
this.reporter = new ChunkJobReporter(sender, "GoldenHash", world);
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
world.getName(),
world.getSeed(),
Bukkit.getBukkitVersion(),
world.getMinHeight(),
world.getMaxHeight(),
centerChunkX,
centerChunkZ,
radius,
threads,
GoldenHashEngine.Mode.AUTO,
resetMantle,
deep,
"null");
this.hashEngine = new GoldenHashEngine(engine, request, IrisPlatforms.get().dataFolder("golden"), this::snapshot, feedback(), progress());
}
public static boolean isScanActive() {
return GoldenHashEngine.isActive();
}
public void start() {
@@ -98,334 +69,75 @@ public final class GoldenHashScanner {
}
private void run() {
boolean error = false;
ACTIVE_SCANS.incrementAndGet();
boolean ok = false;
try {
if (resetMantle) {
reporter.setStage("Resetting mantle");
resetMantleFull();
}
List<int[]> targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius);
reporter.setTotal(targets.size());
reporter.setStage("Generating");
Map<Long, String> lines = scan(targets);
if (lines.size() != targets.size()) {
sender.sendMessage(C.RED + "GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
error = true;
return;
}
reporter.setStage("Comparing");
if (goldenFile.exists()) {
error = !verify(lines);
} else {
capture(lines);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
error = true;
ok = hashEngine.run();
} finally {
ACTIVE_SCANS.decrementAndGet();
reporter.finish(error);
reporter.finish(!ok);
}
}
private void resetMantleFull() {
Mantle mantle = engine.getMantle().getMantle();
mantle.saveAll();
File folder = mantle.getDataFolder();
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
file.delete();
}
}
}
}
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
TerrainChunk buffer = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, buffer));
if (deep) {
writeDeepDump(chunkX, chunkZ, buffer);
}
ok = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
reporter.countApplied(ok);
inFlight.release();
done.countDown();
}
});
}
done.await();
return lines;
}
private String hashChunk(int chunkX, int chunkZ, TerrainChunk buffer) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
int minY = buffer.getMinHeight();
int maxY = buffer.getMaxHeight();
IdentityHashMap<PlatformBlockState, byte[]> blockCache = new IdentityHashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
PlatformBlockState data = buffer.getBlockData(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(data, (PlatformBlockState d) -> (d.key() + "\n").getBytes(StandardCharsets.UTF_8));
blockDigest.update(bytes);
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = minY; y < maxY; y += BIOME_STEP) {
PlatformBiome biome = buffer.getBiome(x, y, z);
byte[] bytes = biome == null
? NULL_BIOME
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8));
biomeDigest.update(bytes);
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private void capture(Map<Long, String> lines) throws IOException {
List<String> body = orderedBody(lines);
String combined = combinedHash(body);
List<String> out = new ArrayList<>();
out.add("#" + FORMAT);
out.add("#world=" + world.getName());
out.add("#dim=" + engine.getDimension().getLoadKey());
out.add("#seed=" + world.getSeed());
out.add("#mc=" + Bukkit.getBukkitVersion());
out.add("#minY=" + world.getMinHeight() + " maxY=" + world.getMaxHeight());
out.add("#center=" + centerChunkX + "," + centerChunkZ);
out.add("#radius=" + radius);
out.addAll(body);
out.add("#combined=" + combined);
Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
sender.sendMessage(C.GREEN + "Golden captured: " + C.GOLD + body.size() + " chunks" + C.GREEN + " combined=" + C.GOLD + shortHash(combined));
sender.sendMessage(C.GRAY + goldenFile.getAbsolutePath());
IrisLogging.info("goldenhash captured: " + goldenFile.getAbsolutePath() + " combined=" + combined);
}
private boolean verify(Map<Long, String> lines) throws IOException {
List<String> existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
Map<String, String> meta = new HashMap<>();
Map<String, String> goldenChunks = new HashMap<>();
for (String line : existing) {
if (line.startsWith("#")) {
int eq = line.indexOf('=');
if (eq > 0) {
meta.put(line.substring(1, eq), line.substring(eq + 1));
}
} else if (!line.isBlank()) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
goldenChunks.put(line.substring(0, second), line);
}
}
String expectedSeed = String.valueOf(world.getSeed());
String expectedDim = engine.getDimension().getLoadKey();
if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
sender.sendMessage(C.RED + "Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
return false;
}
String mc = Bukkit.getBukkitVersion();
if (!mc.equals(meta.get("mc"))) {
sender.sendMessage(C.YELLOW + "Golden was captured on mc=" + meta.get("mc") + ", running mc=" + mc + ". Diffs may be version-induced.");
}
List<String> body = orderedBody(lines);
List<String> mismatches = new ArrayList<>();
for (String line : body) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
String key = line.substring(0, second);
String golden = goldenChunks.get(key);
if (!line.equals(golden)) {
mismatches.add(key + (golden == null ? " (missing in golden)" : ""));
}
}
String combined = combinedHash(body);
if (mismatches.isEmpty()) {
sender.sendMessage(C.GREEN + "GOLDEN MATCH: " + C.GOLD + body.size() + "/" + goldenChunks.size() + C.GREEN
+ " chunks, combined=" + C.GOLD + shortHash(combined));
IrisLogging.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined);
return true;
}
sender.sendMessage(C.RED + "GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
sender.sendMessage(C.RED + " chunk " + mismatches.get(i));
}
if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
sender.sendMessage(C.RED + " ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more");
}
File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new");
List<String> out = new ArrayList<>(body);
out.add("#combined=" + combined);
Files.write(current.toPath(), out, StandardCharsets.UTF_8);
sender.sendMessage(C.YELLOW + "Current hashes written to " + current.getName());
IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
reporter.setStage("Diagnosing");
diagnose(mismatches.getFirst());
return false;
}
private void diagnose(String mismatchKey) {
try {
String[] parts = mismatchKey.trim().split(" ");
int chunkX = Integer.parseInt(parts[0]);
int chunkZ = Integer.parseInt(parts[1]);
TerrainChunk first = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, first, false);
TerrainChunk second = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, second, false);
int minY = first.getMinHeight();
int maxY = first.getMaxHeight();
List<String> diffs = new ArrayList<>();
for (int x = 0; x < 16 && diffs.size() < 50; x++) {
for (int z = 0; z < 16 && diffs.size() < 50; z++) {
for (int y = minY; y < maxY && diffs.size() < 50; y++) {
String a = first.getBlockData(x, y, z).key();
String b = second.getBlockData(x, y, z).key();
if (!a.equals(b)) {
diffs.add(x + " " + y + " " + z + " | " + a + " | " + b);
}
}
}
private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception {
TerrainChunk buffer = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
return new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
return buffer.getMinHeight();
}
EngineMantle engineMantle = engine.getMantle();
int margin = Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
for (int dx = -margin; dx <= margin; dx++) {
for (int dz = -margin; dz <= margin; dz++) {
engineMantle.getMantle().deleteChunk(chunkX + dx, chunkZ + dz);
}
}
TerrainChunk reset = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, reset, false);
List<String> mantleDiffs = new ArrayList<>();
for (int x = 0; x < 16 && mantleDiffs.size() < 80; x++) {
for (int z = 0; z < 16 && mantleDiffs.size() < 80; z++) {
for (int y = minY; y < maxY && mantleDiffs.size() < 80; y++) {
String a = first.getBlockData(x, y, z).key();
String b = reset.getBlockData(x, y, z).key();
if (!a.equals(b)) {
mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b);
}
}
}
@Override
public int maxY() {
return buffer.getMaxHeight();
}
List<String> report = new ArrayList<>();
report.add("#goldenhash diagnosis chunk=" + chunkX + "," + chunkZ);
report.add("#repeat-generation: " + (diffs.isEmpty() ? "STABLE (nondeterminism is order/state-dependent, not per-call)" : "UNSTABLE (" + diffs.size() + "+ diffs between two back-to-back generations)"));
report.addAll(diffs);
report.add("#mantle-reset regeneration: " + (mantleDiffs.isEmpty() ? "STABLE (mantle rebuild reproduces scan output)" : "DIVERGED (" + mantleDiffs.size() + "+ diffs - mantle build is state/order dependent)"));
report.addAll(mantleDiffs);
report.add("#full non-air dump of generation 1 follows (x y z state)");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
String state = first.getBlockData(x, y, z).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
report.add(x + " " + y + " " + z + " " + state);
}
}
}
@Override
public PlatformBlockState block(int x, int y, int z) {
return buffer.getBlockData(x, y, z);
}
File diag = new File(goldenFile.getParentFile(), goldenFile.getName() + ".diag-c" + chunkX + "x" + chunkZ + ".txt");
Files.write(diag.toPath(), report, StandardCharsets.UTF_8);
sender.sendMessage((diffs.isEmpty() ? C.YELLOW + "Repeat-gen STABLE" : C.RED + "Repeat-gen UNSTABLE (" + diffs.size() + "+ block diffs)")
+ C.GRAY + ", " + (mantleDiffs.isEmpty() ? C.YELLOW + "mantle-reset STABLE" : C.RED + "mantle-reset DIVERGED (" + mantleDiffs.size() + "+ diffs)")
+ C.GRAY + " -> " + diag.getName());
IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
} catch (Throwable e) {
IrisLogging.reportError(e);
sender.sendMessage(C.RED + "Diagnosis failed: " + e.getMessage());
}
}
private List<String> orderedBody(Map<Long, String> lines) {
Map<Long, String> sorted = new TreeMap<>(lines);
return new ArrayList<>(sorted.values());
}
private String combinedHash(List<String> body) {
MessageDigest digest = sha256();
for (String line : body) {
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
}
private void writeDeepDump(int chunkX, int chunkZ, TerrainChunk buffer) throws IOException {
File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep"));
dir.mkdirs();
int minY = buffer.getMinHeight();
int maxY = buffer.getMaxHeight();
List<String> out = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
String state = buffer.getBlockData(x, y, z).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
out.add(x + " " + y + " " + z + " " + state);
}
}
@Override
public PlatformBiome biome(int x, int y, int z) {
return buffer.getBiome(x, y, z);
}
}
Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8);
};
}
private static String shortHash(String hex) {
return hex.substring(0, 12);
private GoldenHashEngine.Feedback feedback() {
return new GoldenHashEngine.Feedback() {
@Override
public void ok(String message) {
sender.sendMessage(C.GREEN + message);
}
@Override
public void warn(String message) {
sender.sendMessage(C.YELLOW + message);
}
@Override
public void fail(String message) {
sender.sendMessage(C.RED + message);
}
};
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private GoldenHashEngine.Progress progress() {
return new GoldenHashEngine.Progress() {
@Override
public void stage(String stage) {
reporter.setStage(stage);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
@Override
public void total(int total) {
reporter.setTotal(total);
}
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
reporter.countApplied(ok);
}
};
}
}
@@ -1,16 +1,13 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.splash.IrisSplashRenderer;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public enum Mode {
@@ -54,36 +51,44 @@ public enum Mode {
}
public void splash() {
String padd = Form.repeat(" ", 4);
String padd2 = Form.repeat(" ", 4);
String version = BukkitPlatform.plugin().getDescription().getVersion();
String releaseTrain = getReleaseTrain(version);
String serverVersion = getServerVersion();
String startupDate = getStartupDate();
int javaVersion = getJavaVersion();
String[] splash = IrisSplashRenderer.render(this::splashTone);
String[] info = IrisSplashComposer.composeInfo(version, getServerVersion(), infoStyle());
IrisLogging.info(IrisSplashComposer.compose(splash, info));
}
String[] info = new String[]{
"",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1.1.6]",
padd2 + C.GRAY + " Version: " + color + version,
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
padd2 + C.GRAY + " Server: " + color + serverVersion,
padd2 + C.GRAY + " Java: " + color + javaVersion + C.GRAY + " | Date: " + color + startupDate,
padd2 + C.GRAY + " Commit: " + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT,
"",
"",
"",
""
private IrisSplashComposer.InfoStyle infoStyle() {
return new IrisSplashComposer.InfoStyle() {
@Override
public String linePrefix() {
return " ".repeat(4);
}
@Override
public String title(String text) {
return color + text;
}
@Override
public String subtitle(String text) {
return C.AQUA + text;
}
@Override
public String tag(String text) {
return C.RED + text;
}
@Override
public String label(String text) {
return C.GRAY + text;
}
@Override
public String value(String text) {
return color + text;
}
};
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(padd).append(splash[i]).append(info[i]).append("\n");
}
IrisLogging.info(builder.toString());
}
private String splashTone(char glyph) {
@@ -108,34 +113,4 @@ public enum Mode {
}
return version;
}
private int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
private String getStartupDate() {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
private String getReleaseTrain(String version) {
String value = version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
}
@@ -26,9 +26,11 @@ import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.IrisPack;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
@@ -36,7 +38,6 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONException;
import art.arcane.volmlib.util.json.JSONObject;
@@ -52,7 +53,6 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -259,99 +259,12 @@ public class StudioSVC implements IrisService {
}
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = art.arcane.iris.util.common.misc.WebCache.getNonCachedFile("pack-" + repo, url);
File temp = art.arcane.iris.util.common.misc.WebCache.getTemp();
File work = new File(temp, "dl-" + UUID.randomUUID());
File packs = getWorkspaceFolder();
String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, sender::sendMessage);
if (zip == null || !zip.exists()) {
sender.sendMessage("Failed to find pack at " + url);
sender.sendMessage("Make sure you specified the correct repo and branch!");
sender.sendMessage("For example: /iris download overworld branch=stable");
return;
}
sender.sendMessage("Unpacking " + repo);
try {
ZipUtil.unpack(zip, work);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
sender.sendMessage(
"""
Issue when unpacking. Please check/do the following:
1. Do you have a functioning internet connection?
2. Did the download corrupt?
3. Try deleting the */plugins/iris/packs folder and re-download.
4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld
5. Contact support (if all other options do not help)"""
);
}
File dir = null;
File[] zipFiles = work.listFiles();
if (zipFiles == null) {
sender.sendMessage("No files were extracted from the zip file.");
if (key == null) {
return;
}
try {
dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
} catch (NullPointerException e) {
IrisLogging.reportError(e);
sender.sendMessage("Error when finding home directory. Are there any non-text characters in the file name?");
return;
}
if (dir == null) {
sender.sendMessage("Invalid Format. Missing root folder or too many folders!");
return;
}
IrisData data = IrisData.get(dir);
String[] dimensions = data.getDimensionLoader().getPossibleKeys();
if (dimensions == null || dimensions.length == 0) {
sender.sendMessage("No dimension file found in the extracted zip file.");
sender.sendMessage("Check it is there on GitHub and report this to staff!");
} else if (dimensions.length != 1) {
sender.sendMessage("Dimensions folder must have 1 file in it");
return;
}
IrisDimension d = data.getDimensionLoader().load(dimensions[0]);
data.close();
if (d == null) {
sender.sendMessage("Invalid dimension (folder) in dimensions folder");
return;
}
String key = d.getLoadKey();
sender.sendMessage("Importing " + d.getName() + " (" + key + ")");
File packEntry = new File(packs, key);
if (forceOverwrite) {
IO.delete(packEntry);
}
if (IrisData.loadAnyDimension(key, null) != null) {
sender.sendMessage("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!");
return;
}
if (packEntry.exists() && packEntry.listFiles().length > 0) {
sender.sendMessage("Another pack is using the key " + key + ". IMPORT FAILED!");
return;
}
FileUtils.copyDirectory(dir, packEntry);
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
sender.sendMessage("Successfully Aquired " + d.getName());
ServerConfigurator.installDataPacks(true);
}
@@ -557,32 +470,7 @@ public class StudioSVC implements IrisService {
}
try {
FileUtils.copyDirectory(importPack, newPack, pathname -> !pathname.getAbsolutePath().contains(".git"), false);
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
new File(importPack, existingPack + ".code-workspace").delete();
File dimFile = new File(importPack, "dimensions/" + existingPack + ".json");
File newDimFile = new File(newPack, "dimensions/" + newName + ".json");
try {
FileUtils.copyFile(dimFile, newDimFile);
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
new File(newPack, "dimensions/" + existingPack + ".json").delete();
try {
JSONObject json = new JSONObject(IO.readAll(newDimFile));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(newName.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimFile, json.toString(4));
}
IrisProjectCopier.copyProject(importPack, newPack, existingPack, newName);
} catch (JSONException | IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
@@ -0,0 +1,116 @@
package art.arcane.iris.core.splash;
import art.arcane.iris.BuildConstants;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public final class IrisSplashComposer {
public static final String RELEASE_TAG = "RC.1.1.6";
private static final String SPLASH_PADDING = " ".repeat(4);
private IrisSplashComposer() {
}
public static String[] composeInfo(String version, String serverLine, InfoStyle style) {
String releaseTrain = releaseTrain(version);
String prefix = style.linePrefix();
return new String[]{
"",
prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + " " + RELEASE_TAG + "]"),
prefix + style.label(" Version: ") + style.value(version),
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
prefix + style.label(" Server: ") + style.value(serverLine),
prefix + style.label(" Java: ") + style.value(String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()),
prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT),
"",
"",
"",
""
};
}
public static String compose(String[] splash, String[] info) {
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(SPLASH_PADDING).append(splash[i]).append(info[i]).append('\n');
}
return builder.toString();
}
public static List<String> composePackLines(File packFolder, IrisSplashPackScanner.SplashPackErrorReporter reporter) {
List<IrisSplashPackScanner.SplashPackMetadata> packs = IrisSplashPackScanner.collect(packFolder, reporter);
if (packs.isEmpty()) {
return List.of();
}
List<String> lines = new ArrayList<>(packs.size() + 1);
lines.add("Custom Dimensions: " + packs.size());
for (IrisSplashPackScanner.SplashPackMetadata pack : packs) {
lines.add(" " + pack.name() + " v" + pack.version());
}
return lines;
}
public static int javaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
public static String releaseTrain(String version) {
String value = version == null ? "unknown" : version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
private static String startupDate() {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
public interface InfoStyle {
InfoStyle PLAIN = new InfoStyle() {
};
default String linePrefix() {
return "";
}
default String title(String text) {
return text;
}
default String subtitle(String text) {
return text;
}
default String tag(String text) {
return text;
}
default String label(String text) {
return text;
}
default String value(String text) {
return text;
}
}
}
@@ -26,11 +26,14 @@ import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
@@ -53,7 +56,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -66,7 +68,6 @@ public class IrisToolbelt {
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
private static final AtomicBoolean PREGEN_PROFILE_JVM_HINT_LOGGED = new AtomicBoolean(false);
/**
* Will find / download / search for the dimension or return null
@@ -247,39 +248,24 @@ public class IrisToolbelt {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine);
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld().name()), task) : method, engine);
}
private static PregenCache resolvePregenCache(String worldName) {
PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName);
if (cache == null) {
IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback");
cache = GlobalCacheSVC.createDefault(worldName);
}
return cache;
}
public static boolean applyPregenPerformanceProfile() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
return PregenPerformanceProfile.apply();
}
public static void applyPregenPerformanceProfile(Engine engine) {
boolean changed = applyPregenPerformanceProfile();
if (changed && engine != null) {
engine.hotloadComplex();
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
PregenPerformanceProfile.apply(engine);
}
/**
@@ -432,15 +418,26 @@ public class IrisToolbelt {
return;
}
String name = world.getName();
int depth = worldMaintenanceDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
beginWorldMaintenance(world.getName(), reason, bypassMantleStages);
}
public static void beginWorldMaintenance(String worldName, String reason) {
beginWorldMaintenance(worldName, reason, false);
}
public static void beginWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) {
if (worldName == null) {
return;
}
int depth = worldMaintenanceDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
if (bypassMantleStages) {
worldMaintenanceMantleBypassDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
worldMaintenanceMantleBypassDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
}
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
IrisLogging.info("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
} else {
IrisLogging.debug("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
IrisLogging.debug("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
}
}
@@ -449,50 +446,65 @@ public class IrisToolbelt {
return;
}
String name = world.getName();
AtomicInteger depthCounter = worldMaintenanceDepth.get(name);
endWorldMaintenance(world.getName(), reason);
}
public static void endWorldMaintenance(String worldName, String reason) {
if (worldName == null) {
return;
}
AtomicInteger depthCounter = worldMaintenanceDepth.get(worldName);
if (depthCounter == null) {
return;
}
int depth = depthCounter.decrementAndGet();
if (depth <= 0) {
worldMaintenanceDepth.remove(name, depthCounter);
worldMaintenanceDepth.remove(worldName, depthCounter);
depth = 0;
}
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(name);
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(worldName);
int bypassDepth = 0;
if (bypassCounter != null) {
bypassDepth = bypassCounter.decrementAndGet();
if (bypassDepth <= 0) {
worldMaintenanceMantleBypassDepth.remove(name, bypassCounter);
worldMaintenanceMantleBypassDepth.remove(worldName, bypassCounter);
bypassDepth = 0;
}
}
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
IrisLogging.info("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
} else {
IrisLogging.debug("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
IrisLogging.debug("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
}
}
public static boolean isWorldMaintenanceActive(World world) {
if (world == null) {
return world != null && isWorldMaintenanceActive(world.getName());
}
public static boolean isWorldMaintenanceActive(String worldName) {
if (worldName == null) {
return false;
}
AtomicInteger counter = worldMaintenanceDepth.get(world.getName());
AtomicInteger counter = worldMaintenanceDepth.get(worldName);
return counter != null && counter.get() > 0;
}
public static boolean isWorldMaintenanceBypassingMantleStages(World world) {
if (world == null) {
return world != null && isWorldMaintenanceBypassingMantleStages(world.getName());
}
public static boolean isWorldMaintenanceBypassingMantleStages(String worldName) {
if (worldName == null) {
return false;
}
AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(world.getName());
AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(worldName);
return counter != null && counter.get() > 0;
}
@@ -218,7 +218,9 @@ public class IrisEngine implements Engine {
currentMode.close();
}
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
if (getWorld().hasRealWorld()) {
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
}
}
private void setupEngine() {
@@ -406,11 +408,13 @@ public class IrisEngine implements Engine {
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
prehotload();
setupEngine();
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
});
if (getWorld().hasRealWorld()) {
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
});
}
}
@Override
@@ -608,7 +612,7 @@ public class IrisEngine implements Engine {
private boolean isPregeneratorActiveForThisWorld() {
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && getWorld().hasRealWorld() && pregeneratorJob.targetsWorld(getWorld().realWorld());
return pregeneratorJob != null && pregeneratorJob.targetsWorldName(getWorld().name());
}
private void savePrefetchOnce() {
@@ -458,7 +458,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return false;
}
return job.targetsWorld(world);
return job.targetsWorldName(world.getName());
}
private Chunk[] getLoadedChunksSnapshot(World world) {
@@ -105,11 +105,13 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
output, biome, height, getEngine().getHeight());
}
getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height);
if (height >= 0 && height < output.getHeight()) {
getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height);
}
if (cave != null && cave.getDecorators().isNotEmpty()) {
for (int k = height; k > 0; k--) {
for (int k = Math.min(height, output.getHeight() - 1); k > 0; k--) {
solid = PREDICATE_SOLID.test(output.get(i, k, j));
if (solid) {
@@ -109,7 +109,7 @@ final class DecoratorCore {
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
} catch (Throwable e) {
@@ -118,7 +118,7 @@ final class DecoratorCore {
bd = bd.withProperty("half", bottomHalfValue(half));
}
if (B.isAir(data.get(x, height + 1, z))) {
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
@@ -135,6 +135,10 @@ final class DecoratorCore {
int x, int z, int realX, int height, int realZ,
Hunk<PlatformBlockState> data, RNG rng, IrisData irisData,
boolean underwater, boolean caveSkipFluid, EngineMantle mantle) {
if (height < 0 || height >= data.getHeight()) {
return;
}
PlatformBlockState bdx = data.get(x, height, z);
PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ);
@@ -166,7 +170,7 @@ final class DecoratorCore {
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
} catch (Throwable e) {
@@ -175,7 +179,7 @@ final class DecoratorCore {
bd = bd.withProperty("half", bottomHalfValue(half));
}
if (B.isAir(data.get(x, height + 1, z))) {
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
@@ -196,6 +200,10 @@ final class DecoratorCore {
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int max, Hunk<PlatformBlockState> data,
RNG rng, IrisData irisData, PlaceOpts opts) {
if (height < 0 || height >= data.getHeight()) {
return;
}
int effectiveMax = max;
if (opts.underwater && height < opts.fluidHeight) {
effectiveMax = opts.fluidHeight;
@@ -232,6 +240,10 @@ final class DecoratorCore {
break;
}
if (height + 1 + i >= data.getHeight()) {
break;
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
@@ -58,7 +58,6 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
@@ -660,15 +659,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
}
default void cleanupMantleChunk(int x, int z) {
World world = getWorld().realWorld();
if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) {
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return;
}
if (shouldSkipMaintenanceCleanup()) {
return;
}
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
getMantle().cleanupChunk(x, z);
}
}
private boolean shouldSkipMaintenanceCleanup() {
IrisWorld world = getWorld();
if (world == null) {
return false;
}
String worldName = world.name();
if (!IrisToolbelt.isWorldMaintenanceActive(worldName)) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob == null || !pregeneratorJob.targetsWorldName(worldName);
}
}
@@ -98,7 +98,7 @@ public interface EngineMode extends Staged {
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(getEngine().getWorld().realWorld());
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldName(getEngine().getWorld().name());
return shouldDisableContextCacheForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
@@ -57,9 +57,11 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public void set(int x, int y, int z, PlatformBlockState state) {
BlockData d = (BlockData) state.nativeHandle();
Block block = world.getBlockAt(x, y + world.getMinHeight(), z);
int worldY = y + world.getMinHeight();
if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return;
Block block = world.getBlockAt(x, worldY, z);
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
if (block.getType() == Material.BEDROCK) return;
InventorySlotType slot = null;
if (BukkitBlockResolution.isStorageChest(d)) {
slot = InventorySlotType.STORAGE;
@@ -128,7 +130,9 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
tile.toBukkitTry(world.getBlockAt(xx, yy + world.getMinHeight(), zz));
int worldY = yy + world.getMinHeight();
if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return;
tile.toBukkitTry(world.getBlockAt(xx, worldY, zz));
}
@Override
@@ -19,12 +19,14 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.attribute.Attributable;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
@@ -33,7 +35,9 @@ import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Snippet("attribute-modifier")
@Accessors(chain = true)
@@ -42,17 +46,19 @@ import java.util.UUID;
@Desc("Represents an attribute modifier for an item or an entity. This allows you to create modifications to basic game attributes such as MAX_HEALTH or ARMOR_VALUE.")
@Data
public class IrisAttributeModifier {
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
@Required
@Desc("The Attribute type. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.")
private Attribute attribute = null;
@Desc("The Attribute type key such as max_health or minecraft:armor. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.")
private String attribute = null;
@MinNumber(2)
@Required
@Desc("The Attribute Name is used internally only for the game. This value should be unique to all other attributes applied to this item/entity. It is not shown in game.")
private String name = "";
@Desc("The application operation (add number is default). Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.")
private Operation operation = Operation.ADD_NUMBER;
@Desc("The application operation: ADD_NUMBER (default), ADD_SCALAR or MULTIPLY_SCALAR_1. Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.")
private String operation = "ADD_NUMBER";
@Desc("Minimum amount for this modifier. Iris randomly chooses an amount, this is the minimum it can choose randomly for this attribute.")
private double minAmount = 1;
@@ -67,13 +73,21 @@ public class IrisAttributeModifier {
public void apply(RNG rng, ItemMeta meta) {
if (rng.nextDouble() < getChance()) {
meta.addAttributeModifier(getAttribute(), createModifier(rng));
Attribute resolved = resolveAttribute();
if (resolved == null) {
return;
}
meta.addAttributeModifier(resolved, createModifier(rng));
}
}
public void apply(RNG rng, Attributable meta) {
if (rng.nextDouble() < getChance()) {
meta.getAttribute(getAttribute()).addModifier(createModifier(rng));
Attribute resolved = resolveAttribute();
if (resolved == null) {
return;
}
meta.getAttribute(resolved).addModifier(createModifier(rng));
}
}
@@ -81,9 +95,44 @@ public class IrisAttributeModifier {
return rng.d(getMinAmount(), getMaxAmount());
}
private Attribute resolveAttribute() {
if (attribute == null || attribute.isBlank()) {
return null;
}
String value = attribute.trim().toLowerCase(Locale.ROOT);
Attribute resolved;
if (value.contains(":")) {
NamespacedKey key = NamespacedKey.fromString(value);
resolved = key == null ? null : Registry.ATTRIBUTE.get(key);
} else {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value));
if (resolved == null && value.startsWith("generic_")) {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic_".length())));
}
if (resolved == null && value.startsWith("generic.")) {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic.".length())));
}
}
if (resolved == null && WARNED_ATTRIBUTES.add(value)) {
IrisLogging.warn("Unknown attribute '" + attribute + "' in attribute-modifier '" + name + "'");
}
return resolved;
}
private Operation resolveOperation() {
if (operation == null || operation.isBlank()) {
return Operation.ADD_NUMBER;
}
try {
return Operation.valueOf(operation.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
return Operation.ADD_NUMBER;
}
}
private AttributeModifier createModifier(RNG rng) {
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey());
return new AttributeModifier(key, getAmount(rng), getOperation(), EquipmentSlotGroup.ANY);
return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY);
}
private String generateModifierKey() {
@@ -293,6 +293,19 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return resolved == null ? getDerivative() : resolved;
}
public String getVanillaDerivativeKey() {
String resolved = namespacedBiomeKey(vanillaDerivative);
return resolved == null ? namespacedBiomeKey(derivative) : resolved;
}
private static String namespacedBiomeKey(String key) {
if (key == null || key.isBlank()) {
return null;
}
String trimmed = key.trim();
return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed;
}
private KList<Biome> getBiomeScatterResolved() {
return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter));
}
@@ -38,8 +38,8 @@ import org.bukkit.TreeType;
public class IrisTree {
@Required
@Desc("The types of trees overwritten by this object")
@ArrayType(min = 1, type = TreeType.class)
private KList<TreeType> treeTypes;
@ArrayType(min = 1, type = String.class)
private KList<String> treeTypes = new KList<>();
@Desc("If enabled, overrides any TreeType")
private boolean anyTree = false;
@@ -71,6 +71,17 @@ public class IrisTree {
}
private boolean matchesType(TreeType type) {
return getTreeTypes().contains(type);
if (type == null) {
return false;
}
String name = type.name();
for (String i : getTreeTypes()) {
if (i != null && i.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
}
@@ -503,7 +503,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
World realWorld = this.world.realWorld();
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorld(realWorld);
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldName(realWorld.getName());
}
@Override
@@ -1,55 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.platform.bukkit;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.spi.ChunkWriteTarget;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
/**
* Bukkit adapter for the hot-path chunk write surface backed by TerrainChunk.
*/
public final class BukkitChunkWriteTarget implements ChunkWriteTarget {
private final TerrainChunk terrain;
public BukkitChunkWriteTarget(TerrainChunk terrain) {
this.terrain = terrain;
}
@Override
public void setBlock(int x, int y, int z, PlatformBlockState state) {
terrain.setBlock(x, y, z, state);
}
@Override
public void setBiome(int x, int y, int z, PlatformBiome biome) {
terrain.setBiome(x, y, z, biome);
}
@Override
public int minHeight() {
return terrain.getMinHeight();
}
@Override
public int maxHeight() {
return terrain.getMaxHeight();
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformCapabilities;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
@@ -32,7 +31,6 @@ import art.arcane.iris.spi.PlatformWorld;
import art.arcane.iris.util.common.misc.Bindings;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Bukkit;
@@ -45,7 +43,6 @@ import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
@@ -90,7 +87,6 @@ public final class BukkitPlatform implements IrisPlatform {
private final BukkitRegistries registries = new BukkitRegistries();
private final BukkitScheduler scheduler = new BukkitScheduler();
private final PlatformCapabilities capabilities = new BukkitCapabilities();
private final BukkitStructureHooks structureHooks = new BukkitStructureHooks();
private final BukkitBiomeWriter biomeWriter = new BukkitBiomeWriter();
@@ -244,11 +240,6 @@ public final class BukkitPlatform implements IrisPlatform {
return scheduler;
}
@Override
public PlatformCapabilities capabilities() {
return capabilities;
}
@Override
public PlatformStructureHooks structureHooks() {
return structureHooks;
@@ -311,18 +302,6 @@ public final class BukkitPlatform implements IrisPlatform {
return entity != null;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
if (!(player instanceof Player bukkitPlayer) || itemKey == null || amount <= 0) {
return false;
}
Material material = Material.matchMaterial(itemKey);
if (material == null) {
return false;
}
return bukkitPlayer.getInventory().addItem(new ItemStack(material, amount)).isEmpty();
}
@Override
public void log(LogLevel level, String message) {
bridge().logSink().accept(level, message);
@@ -337,26 +316,4 @@ public final class BukkitPlatform implements IrisPlatform {
public void reportError(Throwable error) {
bridge().errorSink().accept(error);
}
private static final class BukkitCapabilities implements PlatformCapabilities {
@Override
public boolean customBiomes() {
return INMS.get().supportsCustomBiomes();
}
@Override
public boolean dataPacks() {
return INMS.get().supportsDataPacks();
}
@Override
public boolean structurePlacement() {
return true;
}
@Override
public boolean regionizedThreading() {
return J.isFolia();
}
}
}
@@ -16,25 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.spi;
package art.arcane.iris.util.common.math;
/**
* Capability flags describing which optional platform features an adapter supports; all default to unsupported.
*/
public interface PlatformCapabilities {
default boolean customBiomes() {
return false;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class ChunkSpiral {
private ChunkSpiral() {
}
default boolean dataPacks() {
return false;
}
public static List<int[]> centerOut(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
default boolean structurePlacement() {
return false;
}
default boolean regionizedThreading() {
return false;
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.util.common.scheduling;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.engine.framework.PreservationRegistry;
@@ -121,6 +122,13 @@ public class J {
}
public static void aBukkit(Runnable a) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().async(a);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -328,6 +336,13 @@ public class J {
}
public static void s(Runnable r) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().global(r);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -355,7 +370,7 @@ public class J {
public static CompletableFuture sfut(Runnable r) {
CompletableFuture f = new CompletableFuture();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -370,7 +385,7 @@ public class J {
public static <T> CompletableFuture<T> sfut(Supplier<T> r) {
CompletableFuture<T> f = new CompletableFuture<>();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -388,7 +403,7 @@ public class J {
public static CompletableFuture sfut(Runnable r, int delay) {
CompletableFuture f = new CompletableFuture();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -410,6 +425,13 @@ public class J {
}
public static void s(Runnable r, int delay) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().laterGlobal(r, delay);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -451,7 +473,7 @@ public class J {
}
public static int sr(Runnable r, int interval) {
if (!isPluginEnabled()) {
if (!canSchedule()) {
return -1;
}
@@ -461,13 +483,13 @@ public class J {
Runnable[] loop = new Runnable[1];
loop[0] = () -> {
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
r.run();
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
@@ -496,6 +518,17 @@ public class J {
}
public static void a(Runnable r, int delay) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
if (delay <= 0) {
IrisPlatforms.get().scheduler().async(r);
} else {
IrisPlatforms.get().scheduler().laterGlobal(() -> IrisPlatforms.get().scheduler().async(r), delay);
}
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -521,7 +554,7 @@ public class J {
}
public static int ar(Runnable r, int interval) {
if (!isPluginEnabled()) {
if (!canSchedule()) {
return -1;
}
@@ -531,13 +564,13 @@ public class J {
Runnable[] loop = new Runnable[1];
loop[0] = () -> {
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
r.run();
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
@@ -582,6 +615,10 @@ public class J {
return BUKKIT_PRESENT && BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin());
}
private static boolean canSchedule() {
return BUKKIT_PRESENT ? isPluginEnabled() : IrisPlatforms.isBound();
}
private static long ticksToMilliseconds(int ticks) {
return Math.max(0L, ticks) * TICK_MS;
}
@@ -55,6 +55,10 @@ public class ChunkDataHunkHolder extends AtomicHunk<PlatformBlockState> {
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= getHeight()) {
return States.AIR;
}
PlatformBlockState b = super.getRaw(x, y, z);
return b != null ? b : States.AIR;
@@ -255,6 +255,14 @@ public class FastNoiseDouble {
m_seed = seed;
}
public double getFrequency() {
return m_frequency;
}
public double getFractalBounding() {
return m_fractalBounding;
}
// Sets frequency for all noise types
// Default: 0.01
public void setFrequency(double frequency) {
@@ -0,0 +1,8 @@
package art.arcane.iris.util.simd;
public interface NoiseKernels2D {
String describe();
void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length);
}
@@ -0,0 +1,99 @@
package art.arcane.iris.util.simd;
public final class ScalarNoiseKernels2D implements NoiseKernels2D {
static final double F2 = 0.5D * (Math.sqrt(3.0D) - 1.0D);
static final double G2 = (3.0D - Math.sqrt(3.0D)) / 6.0D;
static final long X_PRIME = 1619L;
static final long Y_PRIME = 31337L;
static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D};
@Override
public String describe() {
return "scalar";
}
static long fastFloor(double f) {
return f >= 0D ? (long) f : (long) f - 1L;
}
static double gradCoord2D(long seed, long x, long y, double xd, double yd) {
long hash = seed;
hash ^= X_PRIME * x;
hash ^= Y_PRIME * y;
hash = hash * hash * hash * 60493L;
hash = (hash >> 13) ^ hash;
int gradientIndex = ((int) hash & 7) << 1;
return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]);
}
static double singleSimplex(long seed, double x, double y) {
double t = (x + y) * F2;
long i = fastFloor(x + t);
long j = fastFloor(y + t);
t = (i + j) * G2;
double x0 = x - (i - t);
double y0 = y - (j - t);
long i1;
long j1;
if (x0 > y0) {
i1 = 1L;
j1 = 0L;
} else {
i1 = 0L;
j1 = 1L;
}
double x1 = x0 - i1 + G2;
double y1 = y0 - j1 + G2;
double x2 = x0 - 1D + (2D * G2);
double y2 = y0 - 1D + (2D * G2);
double n0;
double n1;
double n2;
double a = 0.5D - x0 * x0 - y0 * y0;
if (a < 0D) {
n0 = 0D;
} else {
a *= a;
n0 = a * a * gradCoord2D(seed, i, j, x0, y0);
}
double b = 0.5D - x1 * x1 - y1 * y1;
if (b < 0D) {
n1 = 0D;
} else {
b *= b;
n1 = b * b * gradCoord2D(seed, i + i1, j + j1, x1, y1);
}
double c = 0.5D - x2 * x2 - y2 * y2;
if (c < 0D) {
n2 = 0D;
} else {
c *= c;
n2 = c * c * gradCoord2D(seed, i + 1L, j + 1L, x2, y2);
}
return 50D * (n0 + n1 + n2);
}
static double simplexFractalFBMScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double xIn, double yIn) {
double x = xIn * frequency;
double y = yIn * frequency;
long s = seed;
double sum = singleSimplex(s, x, y);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x *= lacunarity;
y *= lacunarity;
amp *= gain;
sum += singleSimplex(++s, x, y) * amp;
}
return sum * fractalBounding;
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
for (int k = 0; k < length; k++) {
out[k] = simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs[k], zs[k]);
}
}
}
@@ -54,6 +54,36 @@ public final class SimdSupport {
return vector == null ? new ScalarSimdKernels() : vector;
}
private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D();
public static NoiseKernels2D noiseKernels2D() {
return NOISE_KERNELS_2D;
}
public static NoiseKernels2D createVectorNoiseKernels2D() {
if (!MODULE_PRESENT) {
return null;
}
try {
Class<?> cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D");
boolean profitable = (boolean) cls.getMethod("profitable").invoke(null);
if (!profitable) {
return null;
}
return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance();
} catch (Throwable e) {
return null;
}
}
private static NoiseKernels2D selectNoiseKernels2D() {
if (!simdEnabledInSettings()) {
return new ScalarNoiseKernels2D();
}
NoiseKernels2D vector = createVectorNoiseKernels2D();
return vector == null ? new ScalarNoiseKernels2D() : vector;
}
private static boolean simdEnabledInSettings() {
try {
return IrisSettings.get().getPerformance().isSimdKernels();
@@ -0,0 +1,129 @@
package art.arcane.iris.util.simd;
import jdk.incubator.vector.DoubleVector;
import jdk.incubator.vector.LongVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
public final class VectorNoiseKernels2D implements NoiseKernels2D {
private static final VectorSpecies<Double> DS = DoubleVector.SPECIES_PREFERRED;
private static final VectorSpecies<Long> LS = LongVector.SPECIES_PREFERRED;
private static final boolean ALIGNED = DS.length() == LS.length();
private static final int MIN_PROFITABLE_LANES = 4;
private static final boolean PROFITABLE = ALIGNED && DS.length() >= MIN_PROFITABLE_LANES;
private static final double[] GRAD_X = {-1D, 1D, -1D, 1D, 0D, -1D, 0D, 1D};
private static final double[] GRAD_Y = {-1D, -1D, 1D, 1D, -1D, 0D, 1D, 0D};
private static final double F2 = ScalarNoiseKernels2D.F2;
private static final double G2 = ScalarNoiseKernels2D.G2;
private static final long X_PRIME = ScalarNoiseKernels2D.X_PRIME;
private static final long Y_PRIME = ScalarNoiseKernels2D.Y_PRIME;
private static final ThreadLocal<long[]> LONG_SCRATCH = ThreadLocal.withInitial(() -> new long[LS.length()]);
public static boolean lanesAligned() {
return ALIGNED;
}
public static boolean profitable() {
return PROFITABLE;
}
@Override
public String describe() {
return DS.length() + "x64 lanes, " + DS.vectorShape();
}
private static LongVector floorToLong(DoubleVector f) {
LongVector truncated = (LongVector) f.convertShape(VectorOperators.D2L, LS, 0);
VectorMask<Long> negative = f.compare(VectorOperators.LT, 0D).cast(LS);
return truncated.sub(1L, negative);
}
private static DoubleVector toDouble(LongVector v) {
return (DoubleVector) v.convertShape(VectorOperators.L2D, DS, 0);
}
private static DoubleVector gradCoord(long seed, LongVector i, LongVector j, DoubleVector xd, DoubleVector yd,
int[] idxScratch) {
LongVector hash = LongVector.broadcast(LS, seed)
.lanewise(VectorOperators.XOR, i.mul(X_PRIME))
.lanewise(VectorOperators.XOR, j.mul(Y_PRIME));
hash = hash.mul(hash).mul(hash).mul(60493L);
LongVector shifted = hash.lanewise(VectorOperators.ASHR, 13);
hash = shifted.lanewise(VectorOperators.XOR, hash);
LongVector idx = hash.lanewise(VectorOperators.AND, 7L);
long[] tmp = LONG_SCRATCH.get();
idx.intoArray(tmp, 0);
for (int l = 0; l < idxScratch.length; l++) {
idxScratch[l] = (int) tmp[l];
}
DoubleVector gx = DoubleVector.fromArray(DS, GRAD_X, 0, idxScratch, 0);
DoubleVector gy = DoubleVector.fromArray(DS, GRAD_Y, 0, idxScratch, 0);
return xd.mul(gx).add(yd.mul(gy));
}
private static DoubleVector corner(DoubleVector xk, DoubleVector yk, DoubleVector grad) {
DoubleVector t = DoubleVector.broadcast(DS, 0.5D).sub(xk.mul(xk)).sub(yk.mul(yk));
VectorMask<Double> negative = t.compare(VectorOperators.LT, 0D);
DoubleVector t2 = t.mul(t);
DoubleVector t4 = t2.mul(t2);
return t4.mul(grad).blend(0D, negative);
}
private static DoubleVector singleSimplexVector(long seed, DoubleVector x, DoubleVector y, int[] idxScratch) {
DoubleVector t = x.add(y).mul(F2);
LongVector i = floorToLong(x.add(t));
LongVector j = floorToLong(y.add(t));
DoubleVector skew = toDouble(i.add(j)).mul(G2);
DoubleVector x0 = x.sub(toDouble(i).sub(skew));
DoubleVector y0 = y.sub(toDouble(j).sub(skew));
VectorMask<Double> xGreater = x0.compare(VectorOperators.GT, y0);
VectorMask<Long> xGreaterL = xGreater.cast(LS);
LongVector i1 = LongVector.zero(LS).blend(1L, xGreaterL);
LongVector j1 = LongVector.broadcast(LS, 1L).blend(0L, xGreaterL);
DoubleVector x1 = x0.sub(toDouble(i1)).add(G2);
DoubleVector y1 = y0.sub(toDouble(j1)).add(G2);
DoubleVector x2 = x0.sub(1D).add(2D * G2);
DoubleVector y2 = y0.sub(1D).add(2D * G2);
DoubleVector n0 = corner(x0, y0, gradCoord(seed, i, j, x0, y0, idxScratch));
DoubleVector n1 = corner(x1, y1, gradCoord(seed, i.add(i1), j.add(j1), x1, y1, idxScratch));
DoubleVector n2 = corner(x2, y2, gradCoord(seed, i.add(1L), j.add(1L), x2, y2, idxScratch));
return n0.add(n1).add(n2).mul(50D);
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
if (!ALIGNED) {
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, 0, length);
return;
}
int lanes = DS.length();
int bound = DS.loopBound(length);
int[] idxScratch = new int[lanes];
int k = 0;
for (; k < bound; k += lanes) {
DoubleVector x = DoubleVector.fromArray(DS, xs, k).mul(frequency);
DoubleVector y = DoubleVector.fromArray(DS, zs, k).mul(frequency);
long s = seed;
DoubleVector sum = singleSimplexVector(s, x, y, idxScratch);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x = x.mul(lacunarity);
y = y.mul(lacunarity);
amp *= gain;
sum = sum.add(singleSimplexVector(++s, x, y, idxScratch).mul(amp));
}
sum.mul(fractalBounding).intoArray(out, k);
}
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, k, length);
}
private static void tailScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int from, int length) {
for (int k = from; k < length; k++) {
out[k] = ScalarNoiseKernels2D.simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain,
fractalBounding, xs[k], zs[k]);
}
}
}
@@ -0,0 +1,70 @@
package art.arcane.iris.core.pregenerator.cache;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenCacheImplPersistenceTest {
private File directory;
@Before
public void setUp() throws Exception {
directory = Files.createTempDirectory("iris-pregen-cache-persist").toFile();
}
@Test
public void partialRegionChunkPersistsAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheChunk(5, 7);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isChunkCached(5, 7));
assertFalse(reloaded.isChunkCached(5, 8));
assertFalse(reloaded.isRegionCached(0, 0));
}
@Test
public void partialRegionNegativeCoordinatesPersistAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheChunk(-3, -1);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isChunkCached(-3, -1));
assertFalse(reloaded.isChunkCached(-3, -2));
}
@Test
public void fullRegionPersistsAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheRegion(2, 3);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isRegionCached(2, 3));
assertTrue(reloaded.isChunkCached((2 << 5) + 11, (3 << 5) + 19));
assertFalse(reloaded.isRegionCached(2, 4));
}
@Test
public void regionCompletedChunkByChunkPersistsAsRegion() {
PregenCache cache = PregenCache.create(directory);
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
cache.cacheChunk(x, z);
}
}
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isRegionCached(0, 0));
assertTrue(reloaded.isChunkCached(31, 31));
assertFalse(reloaded.isChunkCached(32, 0));
}
}
@@ -0,0 +1,213 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CachedPregenMethodCompletionTest {
private File directory;
private PregenCache cache;
private CapturingMethod underlying;
private PregenTask task;
private CachedPregenMethod method;
private RecordingListener listener;
@Before
public void setUp() throws Exception {
directory = Files.createTempDirectory("iris-pregen-cache-test").toFile();
cache = PregenCache.create(directory);
underlying = new CapturingMethod();
task = PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(256)
.radiusZ(256)
.build();
method = new CachedPregenMethod(underlying, cache, task);
listener = new RecordingListener();
}
@Test
public void generateChunkDoesNotCacheOnSubmitOnlyOnCompletion() {
method.generateChunk(3, 4, listener);
assertEquals(1, underlying.generateChunkCalls.get());
assertFalse(cache.isChunkCached(3, 4));
underlying.capturedListener.get().onChunkGenerated(3, 4, false);
assertTrue(cache.isChunkCached(3, 4));
assertEquals(1, listener.generated.get());
}
@Test
public void generateChunkFailureIsNotCached() {
method.generateChunk(6, 9, listener);
underlying.capturedListener.get().onChunkFailed(6, 9);
assertFalse(cache.isChunkCached(6, 9));
assertEquals(0, listener.generated.get());
assertEquals(1, listener.failed.get());
}
@Test
public void generateChunkSkipsUnderlyingMethodWhenCached() {
method.generateChunk(1, 2, listener);
underlying.capturedListener.get().onChunkGenerated(1, 2, false);
assertEquals(1, underlying.generateChunkCalls.get());
method.generateChunk(1, 2, listener);
assertEquals(1, underlying.generateChunkCalls.get());
assertEquals(2, listener.generated.get());
assertEquals(1, listener.generatedCached.get());
}
@Test
public void generateRegionReplayRespectsTaskBounds() {
cache.cacheRegion(0, 0);
List<long[]> expected = new ArrayList<>();
task.iterateChunks(0, 0, (x, z) -> expected.add(new long[]{x, z}));
assertTrue(expected.size() < 1024);
method.generateRegion(0, 0, listener);
assertEquals(0, underlying.generateRegionCalls.get());
assertEquals(expected.size(), listener.generated.get());
assertEquals(expected.size(), listener.generatedCached.get());
}
private static final class CapturingMethod implements PregeneratorMethod {
private final AtomicInteger generateChunkCalls = new AtomicInteger();
private final AtomicInteger generateRegionCalls = new AtomicInteger();
private final AtomicReference<PregenListener> capturedListener = new AtomicReference<>();
@Override
public void init() {
}
@Override
public void close() {
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "capture";
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
generateRegionCalls.incrementAndGet();
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
generateChunkCalls.incrementAndGet();
capturedListener.set(listener);
}
@Override
public Mantle getMantle() {
return null;
}
}
private static final class RecordingListener implements PregenListener {
private final AtomicInteger generated = new AtomicInteger();
private final AtomicInteger generatedCached = new AtomicInteger();
private final AtomicInteger failed = new AtomicInteger();
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
}
@Override
public void onChunkGenerating(int x, int z) {
}
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
generated.incrementAndGet();
if (cached) {
generatedCached.incrementAndGet();
}
}
@Override
public void onChunkFailed(int x, int z) {
failed.incrementAndGet();
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
}
}
}
@@ -0,0 +1,214 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GoldenHashEngineTest {
private static final int MIN_Y = 0;
private static final int MAX_Y = 8;
private IrisSettings previousSettings;
private File goldenDir;
private Engine engine;
private PlatformBlockState stone;
private PlatformBlockState dirt;
private PlatformBiome plains;
@Before
public void setUp() throws Exception {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
goldenDir = Files.createTempDirectory("iris-goldenhash-test").toFile();
engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("testdim");
when(engine.getDimension()).thenReturn(dimension);
stone = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
dirt = mock(PlatformBlockState.class);
when(dirt.key()).thenReturn("minecraft:dirt");
plains = mock(PlatformBiome.class);
when(plains.key()).thenReturn("minecraft:plains");
}
@After
public void tearDown() {
IrisSettings.settings = previousSettings;
}
@Test
public void capturePersistsGoldenFileWithStableFormat() throws Exception {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), feedback, progress());
assertTrue(hashEngine.run());
File goldenFile = new File(goldenDir, "testdim-s1234-c0x0-r0.hashes");
assertTrue(goldenFile.exists());
List<String> lines = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
assertEquals("#iris-goldenhash v1", lines.get(0));
assertEquals("#world=testworld", lines.get(1));
assertEquals("#dim=testdim", lines.get(2));
assertEquals("#seed=1234", lines.get(3));
assertEquals("#mc=test-1.0", lines.get(4));
assertEquals("#minY=0 maxY=8", lines.get(5));
assertEquals("#center=0,0", lines.get(6));
assertEquals("#radius=0", lines.get(7));
String expectedBody = referenceLine(0, 0, 3, 2, 1);
assertEquals(expectedBody, lines.get(8));
assertEquals("#combined=" + referenceCombined(expectedBody), lines.get(9));
}
@Test
public void verifyMatchesGoldenCapture() {
RecordingFeedback captureFeedback = new RecordingFeedback();
GoldenHashEngine capture = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), captureFeedback, progress());
assertTrue(capture.run());
RecordingFeedback verifyFeedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), verifyFeedback, progress());
assertTrue(verify.run());
assertTrue(verifyFeedback.ok.stream().anyMatch((String line) -> line.startsWith("GOLDEN MATCH: 1/1")));
assertTrue(verifyFeedback.fail.isEmpty());
}
@Test
public void verifyFlagsMismatchAndWritesDiagnostics() {
RecordingFeedback captureFeedback = new RecordingFeedback();
GoldenHashEngine capture = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), captureFeedback, progress());
assertTrue(capture.run());
RecordingFeedback verifyFeedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.VERIFY), goldenDir, source(dirt, 7, 4, 9), verifyFeedback, progress());
assertFalse(verify.run());
assertTrue(verifyFeedback.fail.stream().anyMatch((String line) -> line.startsWith("GOLDEN MISMATCH: 1/1")));
File goldenFile = new File(goldenDir, "testdim-s1234-c0x0-r0.hashes");
assertTrue(new File(goldenDir, goldenFile.getName() + ".new").exists());
assertTrue(new File(goldenDir, goldenFile.getName() + ".diag-c0x0.txt").exists());
}
@Test
public void verifyModeWithoutCaptureFails() {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.VERIFY), goldenDir, source(dirt, 3, 2, 1), feedback, progress());
assertFalse(verify.run());
assertTrue(feedback.fail.stream().anyMatch((String line) -> line.startsWith("No golden capture at ")));
}
@Test
public void abortsWithoutWritingWhenChunkGenerationFails() {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine.ChunkSource broken = (int chunkX, int chunkZ) -> {
throw new IllegalStateException("boom");
};
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, broken, feedback, progress());
assertFalse(hashEngine.run());
assertFalse(new File(goldenDir, "testdim-s1234-c0x0-r0.hashes").exists());
assertTrue(feedback.fail.stream().anyMatch((String line) -> line.startsWith("GoldenHash aborted: 1 chunk(s)")));
}
private GoldenHashEngine.Request request(GoldenHashEngine.Mode mode) {
return new GoldenHashEngine.Request("testworld", 1234L, "test-1.0", MIN_Y, MAX_Y, 0, 0, 0, 1, mode, false, false, "null");
}
private GoldenHashEngine.Progress progress() {
return new GoldenHashEngine.Progress() {
};
}
private GoldenHashEngine.ChunkSource source(PlatformBlockState special, int sx, int sy, int sz) {
return (int chunkX, int chunkZ) -> new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
return MIN_Y;
}
@Override
public int maxY() {
return MAX_Y;
}
@Override
public PlatformBlockState block(int x, int y, int z) {
return x == sx && y == sy && z == sz ? special : stone;
}
@Override
public PlatformBiome biome(int x, int y, int z) {
return plains;
}
};
}
private String referenceLine(int chunkX, int chunkZ, int sx, int sy, int sz) throws Exception {
MessageDigest blockDigest = MessageDigest.getInstance("SHA-256");
MessageDigest biomeDigest = MessageDigest.getInstance("SHA-256");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = MIN_Y; y < MAX_Y; y++) {
String key = x == sx && y == sy && z == sz ? "minecraft:dirt" : "minecraft:stone";
blockDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
for (int x = 0; x < 16; x += 4) {
for (int z = 0; z < 16; z += 4) {
for (int y = MIN_Y; y < MAX_Y; y += 4) {
biomeDigest.update("minecraft:plains\n".getBytes(StandardCharsets.UTF_8));
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private String referenceCombined(String bodyLine) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update((bodyLine + "\n").getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest.digest());
}
private static final class RecordingFeedback implements GoldenHashEngine.Feedback {
private final List<String> ok = new ArrayList<>();
private final List<String> warn = new ArrayList<>();
private final List<String> fail = new ArrayList<>();
@Override
public void ok(String message) {
ok.add(message);
}
@Override
public void warn(String message) {
warn.add(message);
}
@Override
public void fail(String message) {
fail.add(message);
}
}
}
@@ -0,0 +1,55 @@
package art.arcane.iris.util.common.math;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class ChunkSpiralTest {
@Test
public void centerOutMatchesLegacyOrdering() {
int centerX = 5;
int centerZ = -3;
int radius = 3;
List<int[]> expected = legacyOrderedTargets(centerX, centerZ, radius);
List<int[]> actual = ChunkSpiral.centerOut(centerX, centerZ, radius);
assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
assertArrayEquals(expected.get(i), actual.get(i));
}
}
@Test
public void centerOutStartsAtCenterAndCoversSquare() {
List<int[]> targets = ChunkSpiral.centerOut(10, 20, 2);
assertEquals(25, targets.size());
assertArrayEquals(new int[]{10, 20}, targets.get(0));
}
@Test
public void centerOutRadiusZeroIsSingleChunk() {
List<int[]> targets = ChunkSpiral.centerOut(-7, 9, 0);
assertEquals(1, targets.size());
assertArrayEquals(new int[]{-7, 9}, targets.get(0));
}
private static List<int[]> legacyOrderedTargets(int centerChunkX, int centerChunkZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerChunkX + dx, centerChunkZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerChunkX;
int oz = t[1] - centerChunkZ;
return ox * ox + oz * oz;
}));
return targets;
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.util.simd;
import art.arcane.iris.util.project.noise.FastNoiseDouble;
import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType;
import art.arcane.iris.util.project.noise.FastNoiseDouble.NoiseType;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class NoiseKernels2DParityTest {
private static FastNoiseDouble oracle(long seed, int octaves) {
FastNoiseDouble n = new FastNoiseDouble(seed);
n.setNoiseType(NoiseType.SimplexFractal);
n.setFractalType(FractalType.FBM);
n.setFractalOctaves(octaves);
return n;
}
private static void assertKernelMatchesOracle(NoiseKernels2D kernel) {
RNG rng = new RNG(99L);
for (int octaves = 1; octaves <= 8; octaves++) {
FastNoiseDouble n = oracle(1337L + octaves, octaves);
int length = 256;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 1_000_000D;
double z = (rng.nextDouble() - 0.5D) * 1_000_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
kernel.simplexFractalFBM(1337L + octaves, octaves, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("octaves=" + octaves + " idx=" + k, expected[k], out[k], 0D);
}
}
}
@Test
public void scalarKernelIsBitExactToOracle() {
assertKernelMatchesOracle(new ScalarNoiseKernels2D());
}
@Test
public void vectorKernelSingleOctaveIsBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
FastNoiseDouble n = oracle(202020L, 1);
RNG rng = new RNG(7L);
int length = 333;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 800_000D;
double z = (rng.nextDouble() - 0.5D) * 800_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
new VectorNoiseKernels2D().simplexFractalFBM(202020L, 1, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("idx=" + k, expected[k], out[k], 0D);
}
}
@Test
public void vectorKernelAllOctavesAreBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
assertKernelMatchesOracle(new VectorNoiseKernels2D());
}
@Test
public void simdSupportReturnsBitExactNoiseKernel() {
NoiseKernels2D kernel = SimdSupport.noiseKernels2D();
org.junit.Assert.assertNotNull(kernel);
assertKernelMatchesOracle(kernel);
}
@Test
public void simdSupportNeverSelectsVectorBelowProfitableLanes() {
if (VectorNoiseKernels2D.profitable()) {
org.junit.Assert.assertTrue(SimdSupport.createVectorNoiseKernels2D() instanceof VectorNoiseKernels2D);
} else {
org.junit.Assert.assertTrue(SimdSupport.noiseKernels2D() instanceof ScalarNoiseKernels2D);
}
}
@Test
public void vectorMatchesScalarAcrossEdgeCases() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
NoiseKernels2D scalar = new ScalarNoiseKernels2D();
NoiseKernels2D vector = new VectorNoiseKernels2D();
RNG rng = new RNG(31337L);
int[] lengths = {1, 3, 7, 8, 9, 255, 256, 257};
double[] edgeCoords = {-4.0D, -3.0D, -1.0D, -0.0D, 0.0D, 1.0D, 4.0D, 1_000_000.0D, -1_000_000.0D};
for (int octaves = 1; octaves <= 8; octaves++) {
for (int length : lengths) {
double[] xs = new double[length];
double[] zs = new double[length];
double[] a = new double[length];
double[] b = new double[length];
for (int k = 0; k < length; k++) {
boolean edge = (k % 3) == 0;
xs[k] = edge ? edgeCoords[k % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
zs[k] = edge ? edgeCoords[(k + 1) % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
}
scalar.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, a, length);
vector.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, b, length);
for (int k = 0; k < length; k++) {
assertEquals("oct=" + octaves + " len=" + length + " idx=" + k, a[k], b[k], 0D);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More