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