few more changes

This commit is contained in:
Brian Neumann-Fopiano
2026-06-17 19:18:44 -04:00
parent 8fb627f9c1
commit f777ea6b18
77 changed files with 4270 additions and 1369 deletions
@@ -79,10 +79,43 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile Engine engine;
private volatile String activePackKey;
private volatile long seedOverride = Long.MIN_VALUE;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
super(biomeSource);
this.dimensionKey = dimensionKey;
this.activePackKey = dimensionKey;
}
public synchronized void repoint(String packKey, long seed) {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
this.activePackKey = packKey;
this.seedOverride = seed;
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
this.engine = null;
this.announced.set(false);
this.biomeHolders.clear();
}
public synchronized void resetToDefault() {
repoint(dimensionKey, Long.MIN_VALUE);
}
public String activePackKey() {
return activePackKey;
}
@Override
@@ -116,7 +149,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, dimensionKey);
Engine created = ModdedWorldEngines.get(level, activePackKey, seedOverride);
engine = created;
return created;
}
@@ -18,6 +18,9 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import net.minecraft.core.Registry;
@@ -25,12 +28,17 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public final class ModdedBiomeWriter implements PlatformBiomeWriter {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String VANILLA_FALLBACK_KEY = "minecraft:plains";
private final Supplier<MinecraftServer> server;
public ModdedBiomeWriter(Supplier<MinecraftServer> server) {
@@ -40,12 +48,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
@Override
public int biomeIdFor(String key) {
Registry<Biome> registry = biomeRegistry();
Identifier identifier = Identifier.tryParse(key);
if (registry == null || identifier == null) {
if (registry == null) {
return 0;
}
Biome biome = registry.getValue(identifier);
return biome == null ? 0 : registry.getId(biome);
int direct = idForKey(registry, key);
if (direct >= 0) {
return direct;
}
int derivative = idForDerivative(registry, key);
if (derivative >= 0) {
return derivative;
}
return fallbackId(registry);
}
@Override
@@ -64,6 +78,60 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return biomes;
}
private int idForKey(Registry<Biome> registry, String key) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return -1;
}
Biome biome = registry.getValue(identifier);
return biome == null ? -1 : registry.getId(biome);
}
private int idForDerivative(Registry<Biome> registry, String key) {
int colon = key.indexOf(':');
if (colon <= 0 || colon >= key.length() - 1) {
return -1;
}
String dimensionLoadKey = key.substring(0, colon);
String customBiomeId = key.substring(colon + 1);
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
if (owner == null) {
return -1;
}
org.bukkit.block.Biome derivative = owner.getVanillaDerivative();
if (derivative == null || derivative.getKey() == null) {
return -1;
}
return idForKey(registry, derivative.getKey().toString());
}
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) {
continue;
}
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
continue;
}
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (customBiomeId.equals(custom.getId())) {
return biome;
}
}
}
}
return null;
}
private int fallbackId(Registry<Biome> registry) {
int plains = idForKey(registry, VANILLA_FALLBACK_KEY);
return plains >= 0 ? plains : 0;
}
private Registry<Biome> biomeRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.spi.IrisLogging;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@@ -217,6 +218,10 @@ public final class ModdedBlockResolution {
Parsed bdx = parseBlockData(bd, warn);
if (bdx == null) {
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd);
if (provided != null) {
return new Parsed(provided, null);
}
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data '" + bd + "'");
}
@@ -0,0 +1,265 @@
/*
* 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 net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
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.world.entity.Relative;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.storage.DerivedLevelData;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.ServerLevelData;
import net.minecraft.world.level.storage.WorldData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
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 volatile ModdedServerAccess access;
private ModdedDimensionManager() {
}
public static void bindAccess(ModdedServerAccess serverAccess) {
access = serverAccess;
}
public static Handle handle(String dimensionId) {
return HANDLES.get(dimensionId);
}
public static List<Handle> handles() {
return new ArrayList<>(HANDLES.values());
}
public static ServerLevel level(MinecraftServer server, String dimensionId) {
Handle handle = HANDLES.get(dimensionId);
if (handle != null && handle.level().getServer() == server) {
return handle.level();
}
ResourceKey<Level> key = levelKey(dimensionId);
for (ServerLevel level : server.getAllLevels()) {
if (level.dimension().equals(key)) {
return level;
}
}
return null;
}
public static Engine engine(MinecraftServer server, String dimensionId) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return null;
}
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
return null;
}
return generator.commandEngine();
}
public static Handle create(MinecraftServer server, String dimensionId, String packKey, 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;
}
if (serverAccess.hasLevel(server, key)) {
ServerLevel present = level(server, dimensionId);
if (present == null || !(present.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
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);
HANDLES.put(dimensionId, handle);
return handle;
}
try {
Handle handle = inject(server, serverAccess, dimensionId, key, packKey, seed);
HANDLES.put(dimensionId, handle);
LOGGER.info("Iris injected runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed);
return handle;
} catch (Throwable e) {
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, 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));
return handle;
}
public static boolean removePersistent(MinecraftServer server, String dimensionId) {
boolean removed = remove(server, dimensionId);
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) {
ResourceKey<Level> key = levelKey(dimensionId);
ServerLevel level = level(server, dimensionId);
if (level == null) {
HANDLES.remove(dimensionId);
return false;
}
try {
evacuate(server, level);
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
generator.unbindEngine();
}
ModdedWorldEngines.evict(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key);
level.close();
HANDLES.remove(dimensionId);
if (wipeStorage) {
ModdedDimensionStorage.wipe(server, key);
}
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
return true;
} catch (Throwable e) {
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
}
}
}
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return false;
}
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
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) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
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) {
RegistryAccess registryAccess = server.registryAccess();
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess);
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);
LevelStem stem = new LevelStem(dimensionType, generator);
WorldData worldData = server.getWorldData();
ServerLevelData overworldData = worldData.overworldData();
DerivedLevelData derivedLevelData = new DerivedLevelData(worldData, overworldData);
Executor executor = serverAccess.levelExecutor(server);
LevelStorageSource.LevelStorageAccess storage = serverAccess.levelStorage(server);
long obfuscatedSeed = BiomeManager.obfuscateSeed(seed);
ServerLevel level = new ServerLevel(
server,
executor,
storage,
derivedLevelData,
key,
stem,
false,
obfuscatedSeed,
List.of(),
false);
serverAccess.putLevel(server, key, level);
server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, packKey, seed, level, generator);
}
private static void evacuate(MinecraftServer server, ServerLevel from) {
ServerLevel fallback = server.overworld();
if (fallback == from) {
return;
}
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);
}
}
private static ResourceKey<Level> levelKey(String dimensionId) {
Identifier identifier = Identifier.parse(dimensionId);
return ResourceKey.create(Registries.DIMENSION, identifier);
}
private static ModdedServerAccess requireAccess() {
ModdedServerAccess bound = access;
if (bound == null) {
throw new IllegalStateException("Iris modded server access is not bound; the loader bootstrap must bind ModdedServerAccess before runtime dimension injection");
}
return bound;
}
public record Handle(String dimensionId, String packKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
}
}
@@ -0,0 +1,123 @@
/*
* 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.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ModdedDimensionRegistryStore {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String FILE_NAME = "iris-dimensions.json";
private ModdedDimensionRegistryStore() {
}
public static List<PersistentDimension> load(MinecraftServer server) {
Path file = storeFile(server);
if (!Files.isRegularFile(file)) {
return new ArrayList<>();
}
try {
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
JSONArray entries = root.optJSONArray("dimensions");
if (entries == null) {
return new ArrayList<>();
}
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
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) {
continue;
}
long seed = entry.optLong("seed", 0L);
deduplicated.put(id, new PersistentDimension(id, packKey, seed));
}
return new ArrayList<>(deduplicated.values());
} catch (RuntimeException | IOException e) {
LOGGER.error("Iris persistent dimension registry at {} is invalid; ignoring it", file, e);
return new ArrayList<>();
}
}
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
Map<String, PersistentDimension> current = index(load(server));
current.put(dimension.id(), dimension);
write(server, new ArrayList<>(current.values()));
}
public static synchronized void remove(MinecraftServer server, String id) {
Map<String, PersistentDimension> current = index(load(server));
if (current.remove(id) != null) {
write(server, new ArrayList<>(current.values()));
}
}
private static Map<String, PersistentDimension> index(List<PersistentDimension> dimensions) {
Map<String, PersistentDimension> map = new LinkedHashMap<>();
for (PersistentDimension dimension : dimensions) {
map.put(dimension.id(), dimension);
}
return map;
}
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
Path file = storeFile(server);
JSONArray entries = new JSONArray();
for (PersistentDimension dimension : dimensions) {
JSONObject entry = new JSONObject();
entry.put("id", dimension.id());
entry.put("packKey", dimension.packKey());
entry.put("seed", dimension.seed());
entries.put(entry);
}
JSONObject root = new JSONObject();
root.put("dimensions", entries);
try {
Files.createDirectories(file.getParent());
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
Files.writeString(temp, root.toString(2), StandardCharsets.UTF_8);
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
LOGGER.error("Iris failed to write persistent dimension registry at {}", file, e);
}
}
private static Path storeFile(MinecraftServer server) {
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
}
public record PersistentDimension(String id, String packKey, long seed) {
}
}
@@ -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;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public final class ModdedDimensionStorage {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
private ModdedDimensionStorage() {
}
public static File storageFolder(MinecraftServer server, ResourceKey<Level> dimension) {
return DimensionType.getStorageFolder(dimension, server.getWorldPath(LevelResource.ROOT)).toFile();
}
public static void wipe(MinecraftServer server, ResourceKey<Level> dimension) {
File storageFolder = storageFolder(server, dimension);
for (String folder : CHUNK_DATA_FOLDERS) {
deleteRecursively(new File(storageFolder, folder).toPath());
}
LOGGER.info("Iris wiped dimension storage at {}", storageFolder.getAbsolutePath());
}
private static void deleteRecursively(Path root) {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.deleteIfExists(path);
}
} catch (IOException e) {
LOGGER.error("Iris failed to wipe dimension storage at {}", root, e);
}
}
}
@@ -22,11 +22,14 @@ import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
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.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.server.MinecraftServer;
@@ -45,12 +48,38 @@ public final class ModdedEngineBootstrap {
"art.arcane.iris.core.loader.IrisData"
};
private static final Object LOCK = new Object();
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform;
private ModdedEngineBootstrap() {
}
public static ModdedServiceManager services() {
return SERVICE_MANAGER;
}
public static ModdedScheduler schedulerOrNull() {
ModdedPlatform bound = platform;
return bound == null ? null : bound.moddedScheduler();
}
public static void tick(MinecraftServer server) {
ModdedScheduler.tick(server);
ModdedStartup.runOnce(server);
ModdedPrimaryWorldRouter.tick(server);
SERVICE_MANAGER.tick(server);
}
public static void stop() {
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.shutdown();
}
}
public static void initialize(ModdedLoader moddedLoader) {
loader = moddedLoader;
}
@@ -97,33 +126,25 @@ public final class ModdedEngineBootstrap {
ModdedLoader boundLoader = loader();
ModdedPlatform created = new ModdedPlatform(boundLoader);
IrisPlatforms.bind(created);
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
ModdedGuiHost.install();
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
IrisServices.register(PreservationRegistry.class, new InertPreservation());
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
ModdedIrisSplash.print(boundLoader);
return created;
}
}
private static final class InertPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class InertWorldManager implements EngineWorldManager {
@Override
public void close() {
@@ -0,0 +1,221 @@
/*
* 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.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import net.minecraft.network.chat.Component;
import net.minecraft.server.packs.PackLocationInfo;
import net.minecraft.server.packs.PackSelectionConfig;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.PathPackResources;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackSource;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
public final class ModdedForcedDatapack {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String PACK_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris";
private static final String STUDIO_POOL_TYPE_KEY = "studio_pool";
private static final String STUDIO_POOL_TYPE_RESOURCE = "/data/irisworldgen/dimension_type/overworld.json";
private static final int STUDIO_POOL_MIN_Y = -256;
private static final int STUDIO_POOL_MAX_Y = 512;
private static final Object LOCK = new Object();
private ModdedForcedDatapack() {
}
public static RepositorySource repositorySource() {
return (Consumer<Pack> consumer) -> {
Pack pack = buildPack();
if (pack != null) {
consumer.accept(pack);
}
};
}
public static Path datapackRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack");
}
private static Path packDirectory() {
return datapackRoot().resolve(PACK_FOLDER);
}
private static Pack buildPack() {
Path directory = generate();
if (directory == null) {
return null;
}
PackLocationInfo location = new PackLocationInfo(
PACK_ID,
Component.literal("Iris World Generation"),
PackSource.BUILT_IN,
Optional.empty());
PackSelectionConfig selection = new PackSelectionConfig(true, Pack.Position.TOP, true);
PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory);
Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection);
if (pack == null) {
LOGGER.error("Iris forced datapack at {} produced no readable pack metadata", directory);
}
return pack;
}
private static Path generate() {
synchronized (LOCK) {
try {
return write();
} catch (IOException e) {
LOGGER.error("Iris failed to generate the forced startup datapack", e);
return null;
}
}
}
private static Path write() throws IOException {
Path packDirectory = packDirectory();
clean(packDirectory);
Files.createDirectories(packDirectory);
File packFolder = packDirectory.toFile();
KList<File> folders = new KList<>();
folders.add(packFolder.getParentFile());
KSet<String> seenBiomes = new KSet<>();
IDataFixer fixer = DataVersion.getLatest().get();
int packCount = 0;
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
if (packs != null) {
for (File pack : packs) {
if (installPack(pack, fixer, folders, seenBiomes)) {
packCount++;
}
}
}
writePackMeta(packDirectory);
writeStudioPoolType(packDirectory);
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} custom biome(s) at {}", packCount, 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()) {
return false;
}
try {
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(packKey);
if (dimension == null) {
return false;
}
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;
}
}
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
if (fitsStudioPool(dimension)) {
return;
}
IrisDimensionType type = dimension.getDimensionType();
String json = type.toJson(fixer);
String typeKey = dimension.getDimensionTypeKey();
for (File parent : folders) {
Path output = parent.toPath().resolve(PACK_FOLDER).resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(typeKey + ".json");
Files.createDirectories(output.getParent());
Files.writeString(output, json, StandardCharsets.UTF_8);
}
}
private static boolean fitsStudioPool(IrisDimension dimension) {
return dimension.getMinHeight() >= STUDIO_POOL_MIN_Y && dimension.getMaxHeight() <= STUDIO_POOL_MAX_Y;
}
private static void writeStudioPoolType(Path packDirectory) throws IOException {
Path output = packDirectory.resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(STUDIO_POOL_TYPE_KEY + ".json");
Files.createDirectories(output.getParent());
Files.writeString(output, readStudioPoolType(), StandardCharsets.UTF_8);
}
private static String readStudioPoolType() throws IOException {
try (InputStream stream = ModdedForcedDatapack.class.getResourceAsStream(STUDIO_POOL_TYPE_RESOURCE)) {
if (stream == null) {
throw new IOException("Bundled studio pool dimension type resource is missing: " + STUDIO_POOL_TYPE_RESOURCE);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private static void writePackMeta(Path packDirectory) throws IOException {
int packFormat = DataVersion.getLatest().getPackFormat();
String json = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
+ " \"pack_format\": " + packFormat + ",\n"
+ " \"min_format\": " + packFormat + ",\n"
+ " \"max_format\": " + packFormat + "\n"
+ " }\n"
+ "}\n";
Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8);
}
private static void clean(Path packDirectory) throws IOException {
if (!Files.exists(packDirectory)) {
return;
}
List<Path> entries = new ArrayList<>();
try (Stream<Path> walk = Files.walk(packDirectory)) {
walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
}
for (Path entry : entries) {
Files.deleteIfExists(entry);
}
}
private static Path packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
}
}
@@ -0,0 +1,123 @@
/*
* 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.volmlib.util.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ModdedModConfig {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Object LOCK = new Object();
private static volatile ModdedModConfig instance;
private final String defaultPack;
private final boolean autoDownloadDefaultPack;
private final String primaryWorld;
private final boolean routePlayersToPrimaryWorld;
private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld) {
this.defaultPack = defaultPack;
this.autoDownloadDefaultPack = autoDownloadDefaultPack;
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
}
public static ModdedModConfig get() {
ModdedModConfig bound = instance;
if (bound != null) {
return bound;
}
synchronized (LOCK) {
if (instance != null) {
return instance;
}
instance = load();
return instance;
}
}
public static void setPrimaryWorld(String dimensionId) {
synchronized (LOCK) {
ModdedModConfig current = get();
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld);
instance = updated;
write(configFile(), updated);
}
}
public String defaultPack() {
return defaultPack;
}
public boolean autoDownloadDefaultPack() {
return autoDownloadDefaultPack;
}
public String primaryWorld() {
return primaryWorld;
}
public boolean routePlayersToPrimaryWorld() {
return routePlayersToPrimaryWorld;
}
private static Path configFile() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("modded.json");
}
private static ModdedModConfig load() {
Path file = configFile();
ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true);
if (!Files.isRegularFile(file)) {
write(file, defaults);
return defaults;
}
try {
JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
return new ModdedModConfig(
json.optString("defaultPack", defaults.defaultPack),
json.optBoolean("autoDownloadDefaultPack", defaults.autoDownloadDefaultPack),
json.optString("primaryWorld", defaults.primaryWorld),
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld));
} catch (RuntimeException | IOException e) {
LOGGER.error("Iris modded config at {} is invalid; using defaults", file, e);
return defaults;
}
}
private static void write(Path file, ModdedModConfig config) {
JSONObject json = new JSONObject();
json.put("defaultPack", config.defaultPack);
json.put("autoDownloadDefaultPack", config.autoDownloadDefaultPack);
json.put("primaryWorld", config.primaryWorld);
json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld);
try {
Files.createDirectories(file.getParent());
Files.writeString(file, json.toString(4), StandardCharsets.UTF_8);
} catch (IOException e) {
LOGGER.error("Iris failed to write modded config at {}", file, e);
}
}
}
@@ -22,10 +22,20 @@ 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;
@@ -57,6 +67,10 @@ public final class ModdedPlatform implements IrisPlatform {
return loader.currentServer();
}
public ModdedScheduler moddedScheduler() {
return scheduler;
}
@Override
public String platformName() {
return loader.platformName();
@@ -130,6 +144,35 @@ public final class ModdedPlatform implements IrisPlatform {
public void dispatchConsoleCommand(String command) {
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
if (!(world instanceof ServerLevel level) || entityKey == null) {
return false;
}
PlatformEntityType resolved = registries.entity(entityKey);
if (resolved == null) {
return false;
}
EntityType<?> type = (EntityType<?>) resolved.nativeHandle();
BlockPos pos = BlockPos.containing(x, y, z);
Entity entity = type.spawn(level, pos, EntitySpawnReason.COMMAND);
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);
@@ -0,0 +1,93 @@
/*
* 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 net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedPrimaryWorldRouter {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int TICK_INTERVAL = 20;
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
private static int tickCounter = 0;
private ModdedPrimaryWorldRouter() {
}
public static void clear() {
routed.clear();
}
public static void tick(MinecraftServer server) {
if (server == null) {
return;
}
tickCounter++;
if (tickCounter < TICK_INTERVAL) {
return;
}
tickCounter = 0;
ModdedModConfig config = ModdedModConfig.get();
if (!config.routePlayersToPrimaryWorld()) {
return;
}
String primary = config.primaryWorld();
if (primary.isBlank()) {
return;
}
ServerLevel target = ModdedDimensionManager.level(server, primary);
if (target == null) {
return;
}
ServerLevel overworld = server.overworld();
if (target == overworld) {
return;
}
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
for (ServerPlayer player : players) {
UUID id = player.getUUID();
if (routed.contains(id)) {
continue;
}
if (player.level() != overworld) {
routed.add(id);
continue;
}
try {
ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ());
routed.add(id);
} catch (Throwable e) {
LOGGER.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
}
}
}
}
@@ -20,28 +20,170 @@ package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public final class ModdedScheduler implements PlatformScheduler {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int ASYNC_CORE_THREADS = 2;
private static final int ASYNC_MAX_THREADS = Math.max(4, Runtime.getRuntime().availableProcessors());
private static final int ASYNC_QUEUE_CAPACITY = 4096;
private static final long ASYNC_KEEP_ALIVE_SECONDS = 30L;
private static volatile Thread mainThread;
private final ThreadPoolExecutor asyncExecutor;
private final ConcurrentLinkedQueue<Runnable> mainQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<DelayedTask> delayedQueue = new ConcurrentLinkedQueue<>();
public ModdedScheduler() {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(ASYNC_QUEUE_CAPACITY);
this.asyncExecutor = new ThreadPoolExecutor(
ASYNC_CORE_THREADS,
ASYNC_MAX_THREADS,
ASYNC_KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
workQueue,
new AsyncThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
this.asyncExecutor.allowCoreThreadTimeOut(true);
}
public static void tick(MinecraftServer server) {
if (server == null) {
return;
}
mainThread = server.getRunningThread();
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
return;
}
scheduler.drain();
}
@Override
public void global(Runnable task) {
task.run();
if (task == null) {
return;
}
if (onMainThread()) {
runGuarded(task);
return;
}
mainQueue.add(task);
}
@Override
public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) {
task.run();
global(task);
}
@Override
public void async(Runnable task) {
task.run();
if (task == null) {
return;
}
asyncExecutor.execute(() -> runGuarded(task));
}
@Override
public void laterGlobal(Runnable task, int ticks) {
if (task == null) {
return;
}
if (ticks <= 0) {
global(task);
return;
}
delayedQueue.add(new DelayedTask(task, ticks));
}
@Override
public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) {
laterGlobal(task, ticks);
}
public void shutdown() {
asyncExecutor.shutdownNow();
mainQueue.clear();
delayedQueue.clear();
}
private void drain() {
promoteDelayed();
Runnable task;
while ((task = mainQueue.poll()) != null) {
runGuarded(task);
}
}
private void promoteDelayed() {
if (delayedQueue.isEmpty()) {
return;
}
List<DelayedTask> retained = new ArrayList<>();
DelayedTask delayed;
while ((delayed = delayedQueue.poll()) != null) {
if (delayed.tick()) {
mainQueue.add(delayed.task());
} else {
retained.add(delayed);
}
}
delayedQueue.addAll(retained);
}
private boolean onMainThread() {
Thread main = mainThread;
return main != null && Thread.currentThread() == main;
}
private void runGuarded(Runnable task) {
try {
task.run();
} catch (Throwable error) {
LOGGER.error("Iris scheduled task failed", error);
}
}
private static final class DelayedTask {
private final Runnable task;
private int remaining;
private DelayedTask(Runnable task, int remaining) {
this.task = task;
this.remaining = remaining;
}
private boolean tick() {
remaining--;
return remaining <= 0;
}
private Runnable task() {
return task;
}
}
private static final class AsyncThreadFactory implements ThreadFactory {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "iris-modded-async-" + counter.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
}
@@ -0,0 +1,39 @@
/*
* 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 net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.LevelStorageSource;
import java.util.concurrent.Executor;
public interface ModdedServerAccess {
Executor levelExecutor(MinecraftServer server);
LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server);
ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key);
boolean hasLevel(MinecraftServer server, ResourceKey<Level> key);
}
@@ -0,0 +1,54 @@
/*
* 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 net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.LevelStorageSource;
import java.util.concurrent.Executor;
public final class ModdedServerLevels implements ModdedServerAccess {
@Override
public Executor levelExecutor(MinecraftServer server) {
return server.executor;
}
@Override
public LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server) {
return server.storageSource;
}
@Override
public ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
return server.levels.put(key, level);
}
@Override
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
return server.levels.remove(key);
}
@Override
public boolean hasLevel(MinecraftServer server, ResourceKey<Level> key) {
return server.levels.containsKey(key);
}
}
@@ -0,0 +1,113 @@
/*
* 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.modded.service.ModdedService;
import art.arcane.iris.modded.service.ModdedTickableService;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
public final class ModdedServiceManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private final Map<Class<? extends ModdedService>, ModdedService> services = new LinkedHashMap<>();
private boolean enabled = false;
public synchronized <T extends ModdedService> T register(Class<T> type, T service) {
if (services.containsKey(type)) {
return type.cast(services.get(type));
}
services.put(type, service);
if (enabled) {
enableService(service);
}
return service;
}
public synchronized <T extends ModdedService> T service(Class<T> type) {
ModdedService service = services.get(type);
return service == null ? null : type.cast(service);
}
public synchronized void enableAll() {
if (enabled) {
return;
}
enabled = true;
for (ModdedService service : services.values()) {
enableService(service);
}
}
public synchronized void tick(MinecraftServer server) {
if (!enabled) {
return;
}
for (ModdedService service : services.values()) {
if (service instanceof ModdedTickableService tickable) {
tickService(tickable, server);
}
}
}
public synchronized void disableAll() {
if (!enabled) {
return;
}
enabled = false;
forEachReversed(this::disableService);
services.clear();
}
private void enableService(ModdedService service) {
try {
service.onEnable();
} catch (Throwable error) {
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), error);
}
}
private void disableService(ModdedService service) {
try {
service.onDisable();
} catch (Throwable error) {
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), error);
}
}
private void tickService(ModdedTickableService service, MinecraftServer server) {
try {
service.onServerTick(server);
} catch (Throwable error) {
LOGGER.error("Iris service tick failed for {}", service.getClass().getName(), error);
}
}
private void forEachReversed(Consumer<ModdedService> action) {
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
action.accept(ordered[i]);
}
}
}
@@ -0,0 +1,85 @@
/*
* 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 net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
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 ModdedStartup() {
}
public static void runOnce(MinecraftServer server) {
if (server == null || !STARTED.compareAndSet(false, true)) {
return;
}
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
ensureDefaultPack();
return;
}
scheduler.async(ModdedStartup::ensureDefaultPack);
}
private static void reinjectPersistentDimensions(MinecraftServer server) {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = ModdedDimensionRegistryStore.load(server);
if (dimensions.isEmpty()) {
return;
}
int injected = 0;
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
try {
ModdedDimensionManager.create(server, dimension.id(), dimension.packKey(), 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.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);
}
}
}
@@ -20,11 +20,18 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.object.TileData;
import art.arcane.volmlib.util.collection.KMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public final class ModdedTileData extends TileData {
public static final String NBT_PROPERTY = "nbt";
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private final byte[] raw;
private final KMap<String, Object> tileProperties;
@@ -34,6 +41,22 @@ public final class ModdedTileData extends TileData {
this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties;
}
public static ModdedTileData capture(String blockKey, String snbt) throws IOException {
KMap<String, Object> properties = new KMap<>();
properties.put(NBT_PROPERTY, snbt);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) {
out.writeUTF(blockKey);
out.writeUTF(GSON.toJson(properties));
}
return new ModdedTileData(bytes.toByteArray(), properties);
}
public String snbt() {
Object value = tileProperties.get(NBT_PROPERTY);
return value == null ? null : value.toString();
}
@Override
public KMap<String, Object> getProperties() {
return tileProperties;
@@ -31,6 +31,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -41,15 +43,34 @@ public final class ModdedWorldEngines {
private ModdedWorldEngines() {
}
public static Engine get(ServerLevel level, String dimensionKey) {
public static Engine get(ServerLevel level, String dimensionKey, long seedOverride) {
Engine existing = ENGINES.get(level);
if (existing != null) {
return existing;
}
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey));
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey, seedOverride));
}
private static Engine create(ServerLevel level, String dimensionKey) {
public static Collection<Engine> activeEngines() {
return new ArrayList<>(ENGINES.values());
}
public static void evict(ServerLevel level) {
Engine removed = ENGINES.remove(level);
if (removed == null) {
return;
}
try {
if (!removed.isClosed()) {
removed.close();
}
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
} catch (Throwable e) {
LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e);
}
}
private static Engine create(ServerLevel level, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
File pack = resolvePack(dimensionKey);
IrisData data = IrisData.get(pack);
@@ -60,7 +81,7 @@ public final class ModdedWorldEngines {
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + pack.getAbsolutePath());
}
long seed = level.getSeed();
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
IrisWorld world = IrisWorld.builder()
.name(level.dimension().identifier().toString().replace(':', '_'))
@@ -0,0 +1,144 @@
/*
* 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.api;
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() {
}
public static boolean isIrisLevel(ServerLevel level) {
if (level == null) {
return false;
}
return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator;
}
public static boolean isStudioLevel(ServerLevel level) {
Engine engine = getEngine(level);
return engine != null && engine.isStudio();
}
public static Engine getEngine(ServerLevel level) {
if (level == null) {
return null;
}
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
return null;
}
try {
return irisGenerator.commandEngine();
} catch (Throwable error) {
return null;
}
}
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
return pregenerate(level, radiusBlocks, 0, 0, ModdedPregenMode.ASYNC, false);
}
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, ModdedPregenMode mode, boolean cached) {
Engine engine = getEngine(level);
if (engine == null) {
return false;
}
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, mode, cached);
}
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
return null;
}
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
}
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
Engine engine = getEngine(level);
if (engine == null || data == null) {
return;
}
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
}
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
return;
}
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
}
public static void retainMantleDataForSlice(Class<?> sliceType) {
if (sliceType == null) {
return;
}
IrisToolbelt.retainMantleDataForSlice(sliceType.getCanonicalName());
}
public static void registerProvider(ModdedDataProvider provider) {
ModdedCustomContentRegistry.register(provider);
}
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;
}
}
@@ -0,0 +1,182 @@
/*
* 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.api;
import art.arcane.iris.modded.ModdedBlockResolution;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public final class ModdedCustomContentRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
private static final Map<String, BlockState> CUSTOM_BLOCKS = new ConcurrentHashMap<>();
private static volatile boolean scanned = false;
private ModdedCustomContentRegistry() {
}
public static void registerCustomBlockData(String namespace, String key, String state) {
if (namespace == null || key == null || state == null) {
return;
}
Identifier identifier = Identifier.tryParse(namespace + ":" + key);
if (identifier == null) {
LOGGER.warn("Iris custom block data registration rejected invalid id {}:{}", namespace, key);
return;
}
BlockState parsed;
try {
parsed = ModdedBlockResolution.strictParse(state).handle();
} catch (Throwable error) {
LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error);
return;
}
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
}
public static void register(ModdedDataProvider provider) {
if (provider == null) {
return;
}
for (ModdedDataProvider existing : PROVIDERS) {
if (existing.modId().equals(provider.modId())) {
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId());
return;
}
}
PROVIDERS.add(provider);
try {
provider.init();
} catch (Throwable error) {
LOGGER.error("Iris custom content provider '{}' failed to initialize", provider.modId(), error);
}
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
public static void discover() {
if (scanned) {
return;
}
scanned = true;
ServiceLoader<ModdedDataProvider> loader = ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader());
for (ModdedDataProvider provider : loader) {
register(provider);
}
}
public static boolean hasProviders() {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
}
public static BlockState resolveBlock(String key) {
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
return null;
}
Identifier base = parseIdentifier(key);
if (base == null) {
return null;
}
BlockState custom = CUSTOM_BLOCKS.get(base.toString());
if (custom != null) {
return custom;
}
if (PROVIDERS.isEmpty()) {
return null;
}
Map<String, String> state = parseState(key);
for (ModdedDataProvider provider : PROVIDERS) {
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) {
continue;
}
try {
BlockState resolved = provider.getBlockData(base, state);
if (resolved != null) {
return resolved;
}
} catch (Throwable error) {
LOGGER.error("Iris custom content provider '{}' failed resolving block {}", provider.modId(), key, error);
}
}
return null;
}
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
if (PROVIDERS.isEmpty() || level == null || key == null) {
return null;
}
Identifier base = parseIdentifier(key);
if (base == null) {
return null;
}
for (ModdedDataProvider provider : PROVIDERS) {
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.ENTITY)) {
continue;
}
try {
Entity entity = provider.spawnMob(level, x, y, z, base);
if (entity != null) {
return entity;
}
} catch (Throwable error) {
LOGGER.error("Iris custom content provider '{}' failed spawning mob {}", provider.modId(), key, error);
}
}
return null;
}
private static Identifier parseIdentifier(String key) {
String trimmed = key.trim();
int bracket = trimmed.indexOf('[');
String head = bracket < 0 ? trimmed : trimmed.substring(0, bracket);
return Identifier.tryParse(head);
}
private static Map<String, String> parseState(String key) {
Map<String, String> state = new LinkedHashMap<>();
int open = key.indexOf('[');
int close = key.indexOf(']');
if (open < 0 || close < open) {
return state;
}
String body = key.substring(open + 1, close);
if (body.isEmpty()) {
return state;
}
for (String pair : body.split(",")) {
int eq = pair.indexOf('=');
if (eq <= 0) {
continue;
}
state.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim());
}
return state;
}
}
@@ -0,0 +1,50 @@
/*
* 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.api;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Collection;
import java.util.Map;
public interface ModdedDataProvider {
String modId();
default boolean isReady() {
return true;
}
Collection<Identifier> getTypes(ModdedDataType type);
boolean isValidProvider(Identifier id, ModdedDataType type);
default BlockState getBlockData(Identifier blockId, Map<String, String> state) {
return null;
}
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
return null;
}
default void init() {
}
}
@@ -0,0 +1,25 @@
/*
* 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.api;
public enum ModdedDataType {
BLOCK,
ITEM,
ENTITY
}
@@ -23,16 +23,21 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
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.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.matter.MatterMarker;
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.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
@@ -45,6 +50,7 @@ import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
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.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -53,6 +59,8 @@ import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -76,6 +84,8 @@ public final class IrisModdedCommands {
private static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
private static final SuggestionProvider<CommandSourceStack> MARKER_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("cave_floor", "cave_ceiling", "object"), builder);
private static final DustParticleOptions MARKER_DUST = new DustParticleOptions(0x5A8CFF, 1.2F);
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
private static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
@@ -104,7 +114,12 @@ public final class IrisModdedCommands {
.executes((CommandContext<CommandSourceStack> context) -> info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(Commands.literal("what").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource())));
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource()))
.then(Commands.literal("block")
.executes((CommandContext<CommandSourceStack> context) -> whatBlock(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(gotoTree("goto"));
root.then(gotoTree("find"));
@@ -133,6 +148,8 @@ public final class IrisModdedCommands {
root.then(ModdedObjectCommands.tree("o"));
root.then(editTree());
root.then(createTree());
root.then(ModdedStudioCommands.tree("studio"));
root.then(ModdedStudioCommands.tree("std"));
root.then(ModdedStudioCommands.tree("s"));
@@ -150,6 +167,21 @@ public final class IrisModdedCommands {
return root;
}
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)
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
1337L))
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
LongArgumentType.getLong(context, "seed"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> helpTree() {
return Commands.literal("help")
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""))
@@ -202,13 +234,19 @@ public final class IrisModdedCommands {
.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))
.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")))))))
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.literal("stop")
.executes((CommandContext<CommandSourceStack> context) -> pregenStop(context.getSource())))
.then(Commands.literal("x")
@@ -221,6 +259,14 @@ 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 LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
@@ -288,19 +334,29 @@ public final class IrisModdedCommands {
return 1;
}
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ) {
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
if (!ModdedPregenJob.start(level, engine, radius, centerX, centerZ)) {
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, mode, cached)) {
fail(source, "A pregeneration task is already running. Stop it first with /iris pregen stop.");
return 0;
}
String guiNote;
if (!gui) {
guiNote = "";
} else if (showGui) {
guiNote = " A progress map window is opening on the server display.";
} else {
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
}
String modeNote = mode == ModdedPregenMode.SYNC ? " Mode: sync" + (cached ? " (checkpoint cache resumable)." : ".") : "";
ok(source, "Pregen started in " + level.dimension().identifier() + " of " + (radius * 2) + " by " + (radius * 2)
+ " blocks from " + centerX + "," + centerZ + ". Progress logs to console; see /iris pregen status.");
+ " blocks from " + centerX + "," + centerZ + "." + modeNote + " Progress logs to console; see /iris pregen status." + guiNote);
return 1;
}
@@ -418,6 +474,108 @@ public final class IrisModdedCommands {
return 1;
}
private static int whatBlock(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players (it inspects the block you are looking at).");
return 0;
}
HitResult hit = player.pick(128.0D, 1.0F, false);
if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) {
fail(source, "Look at a block, not the sky.");
return 0;
}
ServerLevel level = source.getLevel();
BlockPos pos = blockHit.getBlockPos();
BlockState state = level.getBlockState(pos);
PlatformBlockState platform = ModdedBlockState.of(state, null);
ok(source, "Block: " + platform.key() + " (y=" + pos.getY() + ")");
List<String> flags = new ArrayList<>();
if (platform.isSolid()) {
flags.add("solid");
}
if (platform.isFluid()) {
flags.add("fluid");
}
if (platform.isWater()) {
flags.add("water");
}
if (platform.isWaterLogged()) {
flags.add("waterlogged");
}
if (platform.isStorage()) {
flags.add("storage (loot capable)");
}
if (platform.isLit()) {
flags.add("lit");
}
if (platform.isFoliage()) {
flags.add("foliage");
}
if (platform.isFoliagePlantable()) {
flags.add("plantable foliage");
}
if (platform.isDecorant()) {
flags.add("decorant");
}
if (platform.isOre()) {
flags.add("ore");
}
if (platform.hasTileEntity()) {
flags.add("tile entity");
}
ok(source, flags.isEmpty() ? "Properties: (none)" : "Properties: " + String.join(", ", flags));
return 1;
}
private static int whatMarkers(CommandSourceStack source, String markerRaw) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players (markers render as particles around you).");
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
String marker = markerRaw.trim();
BlockPos origin = player.blockPosition();
int chunkX = origin.getX() >> 4;
int chunkZ = origin.getZ() >> 4;
MinecraftServer server = source.getServer();
ok(source, "Scanning for '" + marker + "' markers around you...");
Thread thread = new Thread(() -> {
List<int[]> hits = new ArrayList<>();
MatterMarker matterMarker = new MatterMarker(marker);
try {
for (int cx = chunkX - 4; cx <= chunkX + 4; cx++) {
for (int cz = chunkZ - 4; cz <= chunkZ + 4; cz++) {
for (IrisPosition position : engine.getMantle().findMarkers(cx, cz, matterMarker)) {
hits.add(new int[]{position.getX(), position.getY(), position.getZ()});
}
}
}
} catch (Throwable e) {
LOGGER.error("Iris marker scan failed for {}", marker, e);
server.execute(() -> fail(source, "Marker scan failed: " + e.getClass().getSimpleName()));
return;
}
server.execute(() -> {
for (int[] hit : hits) {
level.sendParticles(player, MARKER_DUST, true, true,
hit[0] + 0.5D, hit[1] + 1.0D, hit[2] + 0.5D,
3, 0.2D, 0.2D, 0.2D, 0.0D);
}
ok(source, "Found " + hits.size() + " nearby marker(s) (" + marker + ")");
});
}, "Iris Marker Scan");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int gotoBiome(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
@@ -19,6 +19,7 @@
package art.arcane.iris.modded.command;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
@@ -161,6 +162,9 @@ final class ModdedCommandHelp {
ModdedCommandFeedback.clear(source);
sendHeader(source, normalized);
if (!Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(source)) {
ModdedCommandFeedback.send(source, opNotice());
}
if (!normalized.isEmpty()) {
ModdedCommandFeedback.send(source, backButton(normalized));
}
@@ -293,6 +297,17 @@ final class ModdedCommandHelp {
return hover;
}
private static MutableComponent opNotice() {
MutableComponent notice = Component.empty();
notice.append(text("", REQUIRED));
notice.append(text("Iris commands need operator permission (level 2). ", REQUIRED_TEXT));
notice.append(text("Run ", DESCRIPTION));
notice.append(text("/op <you>", PARAMETER_ALT));
notice.append(text(" from the console (or enable cheats in singleplayer); ", DESCRIPTION));
notice.append(text("until then these commands will not run or tab-complete.", USAGE));
return notice;
}
private static MutableComponent footer() {
return ModdedCommandFeedback.footer();
}
@@ -48,7 +48,7 @@ import java.util.function.Predicate;
public final class ModdedDatapackCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final String WORLD_PACK_NAME = ModdedWorldDatapackWriter.WORLD_PACK_NAME;
private static final String WORLD_PACK_NAME = "iris";
private ModdedDatapackCommands() {
}
@@ -0,0 +1,87 @@
/*
* 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;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.gui.GuiOverlay;
import art.arcane.iris.engine.framework.Engine;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import java.util.Map;
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 volatile Engine active;
private volatile MinecraftServer server;
private ModdedGuiHost() {
}
public static void install() {
GuiHost.set(INSTANCE);
}
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine) {
INSTANCE.server = server;
INSTANCE.active = engine;
INSTANCE.levels.put(engine, level);
}
public static boolean isGuiLaunchable() {
return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis();
}
public static String guiUnavailableReason() {
if (!GuiHost.isAvailable()) {
return "headless JVM (no display)";
}
return "gui.useServerLaunchedGuis=false in Iris settings";
}
@Override
public Engine findActiveEngine() {
Engine current = active;
if (current != null && !current.isClosed()) {
return current;
}
for (Map.Entry<Engine, ServerLevel> entry : levels.entrySet()) {
if (!entry.getKey().isClosed()) {
return entry.getKey();
}
}
return null;
}
@Override
public GuiOverlay overlayFor(Engine engine) {
if (engine == null) {
return null;
}
ServerLevel level = levels.get(engine);
if (level == null || server == null) {
return null;
}
return new ModdedVisionOverlay(server, level, engine);
}
}
@@ -23,7 +23,9 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.brigadier.arguments.IntegerArgumentType;
@@ -37,9 +39,14 @@ import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
@@ -272,7 +279,8 @@ public final class ModdedObjectCommands {
return 0;
}
int[] tilesSkipped = {0};
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped);
int[] tilesSaved = {0};
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
@@ -284,15 +292,25 @@ public final class ModdedObjectCommands {
IrisModdedCommands.fail(source, "Failed to save object: " + e.getMessage());
return 0;
}
String tileNote = tilesSkipped[0] > 0 ? " (" + tilesSkipped[0] + " tile state(s) not captured; tile capture is Bukkit-only for now)" : "";
StringBuilder tileNote = new StringBuilder();
if (tilesSaved[0] > 0) {
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
if (tilesSkipped[0] > 0) {
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
}
tileNote.append(")");
} else if (tilesSkipped[0] > 0) {
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
}
IrisModdedCommands.ok(source, "Saved " + engine.getData().getDataFolder().getName() + "/objects/" + name + ".iob: "
+ w + "x" + h + "x" + d + ", " + object.getBlocks().size() + " block(s)" + tileNote);
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSkipped[0], file.getAbsolutePath());
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
return 1;
}
private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped) {
private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped, int[] tilesSaved) {
IrisObject object = new IrisObject(w, h, d);
HolderLookup.Provider provider = level.registryAccess();
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -301,16 +319,41 @@ public final class ModdedObjectCommands {
if (state.is(Blocks.AIR)) {
continue;
}
int ox = x - min.getX();
int oy = y - min.getY();
int oz = z - min.getZ();
object.setUnsigned(ox, oy, oz, ModdedBlockState.of(state, null));
if (state.hasBlockEntity()) {
tilesSkipped[0]++;
TileData tile = captureTile(level, provider, cursor.immutable(), state);
if (tile != null) {
object.setUnsignedTile(ox, oy, oz, tile);
tilesSaved[0]++;
} else {
tilesSkipped[0]++;
}
}
object.setUnsigned(x - min.getX(), y - min.getY(), z - min.getZ(), ModdedBlockState.of(state, null));
}
}
}
return object;
}
private static TileData captureTile(ServerLevel level, HolderLookup.Provider provider, BlockPos pos, BlockState state) {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity == null) {
return null;
}
try {
CompoundTag tag = blockEntity.saveWithFullMetadata(provider);
String snbt = NbtUtils.structureToSnbt(tag);
String blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
return ModdedTileData.capture(blockKey, snbt);
} catch (Throwable e) {
LOGGER.error("Iris tile capture failed at {} {} {}", pos.getX(), pos.getY(), pos.getZ(), e);
return null;
}
}
private static int paste(CommandSourceStack source, String keyRaw, int rotation, BlockPos at) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
@@ -354,11 +397,11 @@ public final class ModdedObjectCommands {
}
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
String tileNote = tileNote(placer);
IrisModdedCommands.ok(source, "Placed " + key + " at " + target.getX() + " " + target.getY() + " " + target.getZ()
+ " rot=" + rotation + " (" + placer.writes() + " write(s), " + placer.nonAirWrites() + " non-air" + tileNote + ")");
LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesSkipped={}",
key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.skippedTiles());
LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesRestored={} tilesSkipped={}",
key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.restoredTiles(), placer.skippedTiles());
return placer.writes() > 0 ? 1 : 0;
}
@@ -619,4 +662,15 @@ public final class ModdedObjectCommands {
return "(" + first.getX() + "," + first.getY() + "," + first.getZ() + ") -> ("
+ second.getX() + "," + second.getY() + "," + second.getZ() + ")";
}
static String tileNote(ModdedObjectPlacer placer) {
StringBuilder note = new StringBuilder();
if (placer.restoredTiles() > 0) {
note.append(", ").append(placer.restoredTiles()).append(" tile entity state(s) restored");
}
if (placer.skippedTiles() > 0) {
note.append(", ").append(placer.skippedTiles()).append(" tile state(s) skipped");
}
return note.toString();
}
}
@@ -24,23 +24,32 @@ import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel;
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 net.minecraft.world.level.levelgen.Heightmap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
final class ModdedObjectPlacer implements IObjectPlacer {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private final ServerLevel level;
private final Map<BlockPos, BlockState> undo = new HashMap<>();
private int writes = 0;
private int nonAirWrites = 0;
private int skippedTiles = 0;
private int restoredTiles = 0;
ModdedObjectPlacer(ServerLevel level) {
this.level = level;
@@ -62,6 +71,10 @@ final class ModdedObjectPlacer implements IObjectPlacer {
return skippedTiles;
}
int restoredTiles() {
return restoredTiles;
}
@Override
public int getHighest(int x, int z, IrisData data) {
return level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
@@ -128,7 +141,35 @@ final class ModdedObjectPlacer implements IObjectPlacer {
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
skippedTiles++;
if (!(tile instanceof ModdedTileData moddedTile)) {
skippedTiles++;
return;
}
String snbt = moddedTile.snbt();
if (snbt == null || snbt.isBlank()) {
skippedTiles++;
return;
}
BlockPos pos = new BlockPos(xx, yy, zz);
BlockState state = level.getBlockState(pos);
if (!state.hasBlockEntity()) {
skippedTiles++;
return;
}
try {
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
if (restored == null) {
skippedTiles++;
return;
}
level.setBlockEntity(restored);
restored.setChanged();
restoredTiles++;
} catch (Throwable e) {
LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e);
skippedTiles++;
}
}
@Override
@@ -18,26 +18,42 @@
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.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
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 {
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);
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;
@@ -45,12 +61,46 @@ public final class ModdedPregenJob implements PregenListener {
private volatile long elapsed;
private volatile String method = "Modded";
private ModdedPregenJob(ServerLevel level, Engine engine, PregenTask task) {
private ModdedPregenJob(MinecraftServer server, ServerLevel level, Engine engine, PregenTask task, boolean gui, ModdedPregenMode mode, boolean cached) {
this.dimension = level.dimension().identifier().toString();
this.pregenerator = new IrisPregenerator(task, new ModdedPregenMethod(level, engine), this);
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;
if (cached) {
PregenCache cache = PregenCache.create(cacheDirectory(level)).sync();
resolvedMethod = new CachedPregenMethod(baseMethod, cache);
}
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;
}
public static boolean start(ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ) {
private static File cacheDirectory(ServerLevel level) {
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
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;
}
@@ -60,8 +110,9 @@ public final class ModdedPregenJob implements PregenListener {
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
ModdedPregenJob job = new ModdedPregenJob(level, engine, task);
ModdedPregenJob job = new ModdedPregenJob(server, level, engine, task, gui, mode, cached);
if (!ACTIVE.compareAndSet(null, job)) {
job.closeRenderer();
return false;
}
Thread thread = new Thread(() -> {
@@ -70,6 +121,7 @@ public final class ModdedPregenJob implements PregenListener {
} catch (Throwable e) {
LOGGER.error("Iris pregen failed for {}", job.dimension, e);
} finally {
job.closeRenderer();
ACTIVE.compareAndSet(job, null);
}
}, "Iris Pregen");
@@ -78,6 +130,27 @@ public final class ModdedPregenJob implements PregenListener {
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) {
@@ -176,10 +249,12 @@ public final class ModdedPregenJob implements PregenListener {
@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
@@ -228,5 +303,34 @@ public final class ModdedPregenJob implements PregenListener {
@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();
}
}
@@ -31,8 +31,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class ModdedPregenMethod implements PregeneratorMethod {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
@@ -40,13 +42,19 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private final ServerLevel level;
private final Engine engine;
private final ModdedPregenMode mode;
private final Semaphore semaphore;
private final int permits;
private final int timeoutSeconds;
public ModdedPregenMethod(ServerLevel level, Engine engine) {
this(level, engine, ModdedPregenMode.ASYNC);
}
public ModdedPregenMethod(ServerLevel level, Engine engine, ModdedPregenMode mode) {
this.level = level;
this.engine = engine;
this.mode = mode;
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());
@@ -54,13 +62,15 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public void init() {
LOGGER.info("Iris modded pregen init: dim={} inFlightCap={} timeout={}s",
level.dimension().identifier(), permits, timeoutSeconds);
LOGGER.info("Iris modded pregen init: dim={} mode={} inFlightCap={} timeout={}s",
level.dimension().identifier(), mode, mode == ModdedPregenMode.ASYNC ? permits : 1, timeoutSeconds);
}
@Override
public void close() {
semaphore.acquireUninterruptibly(permits);
if (mode == ModdedPregenMode.ASYNC) {
semaphore.acquireUninterruptibly(permits);
}
saveLevel();
}
@@ -85,7 +95,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public boolean isAsyncChunkMode() {
return true;
return mode == ModdedPregenMode.ASYNC;
}
@Override
@@ -95,6 +105,38 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override
public void generateChunk(int x, int z, PregenListener listener) {
if (mode == ModdedPregenMode.SYNC) {
generateChunkSync(x, z, listener);
return;
}
generateChunkAsync(x, z, listener);
}
private void generateChunkSync(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
ChunkPos pos = new ChunkPos(x, z);
CompletableFuture<?> loadFuture = CompletableFuture
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
.thenCompose((CompletableFuture<?> inner) -> inner);
try {
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());
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
} finally {
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
}
}
private void generateChunkAsync(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
try {
semaphore.acquire();
@@ -0,0 +1,24 @@
/*
* 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
}
@@ -201,7 +201,7 @@ public final class ModdedStructureCommands {
return 0;
}
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
String tileNote = ModdedObjectCommands.tileNote(placer);
IrisModdedCommands.ok(source, "Placed '" + key + "' (" + pieces.size() + " pieces, " + placer.writes() + " write(s)" + tileNote + ") at your location. /iris object undo to revert.");
return 1;
}
@@ -18,36 +18,47 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.IrisSettings;
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.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
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.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Spiraler;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
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.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.ZipUtil;
@@ -57,11 +68,17 @@ 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;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
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;
@@ -69,7 +86,22 @@ public final class ModdedStudioCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
private static final Pattern STUDIO_ID_SANITIZER = Pattern.compile("[^a-z0-9_-]");
private static final String STUDIO_NAMESPACE = "irisworldgen";
private static final String STUDIO_PREFIX = "studio_";
private static final String DEFAULT_TEMPLATE = "example";
private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>();
private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
};
private ModdedStudioCommands() {
}
@@ -107,20 +139,26 @@ public final class ModdedStudioCommands {
root.then(openTree("open"));
root.then(openTree("o"));
root.then(message("close", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
root.then(message("x", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
root.then(message("tpstudio", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
root.then(message("stp", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
root.then(Commands.literal("close")
.executes((CommandContext<CommandSourceStack> context) -> close(context.getSource())));
root.then(Commands.literal("x")
.executes((CommandContext<CommandSourceStack> context) -> close(context.getSource())));
root.then(Commands.literal("tpstudio")
.executes((CommandContext<CommandSourceStack> context) -> tpStudio(context.getSource())));
root.then(Commands.literal("stp")
.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(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."));
root.then(message("noise", "The noise explorer is a desktop GUI launched from the Bukkit plugin."));
root.then(message("nmap", "The noise explorer is a desktop GUI launched from the Bukkit plugin."));
root.then(message("map", "The world map renderer is a desktop GUI launched from the Bukkit plugin."));
root.then(message("render", "The world map renderer is a desktop GUI launched from the Bukkit plugin."));
root.then(noiseTree("noise"));
root.then(noiseTree("nmap"));
root.then(mapTree("map"));
root.then(mapTree("render"));
root.then(message("loot", "Loot simulation opens a Bukkit chest inventory GUI; it is not available on modded servers."));
root.then(message("profile", "Pack performance profiling is part of the Bukkit studio toolchain and is not ported to modded servers."));
root.then(message("spawn", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers."));
@@ -145,6 +183,75 @@ public final class ModdedStudioCommands {
return 0;
}
private static LiteralArgumentBuilder<CommandSourceStack> noiseTree(String name) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), null, 12345L))
.then(Commands.argument("generator", StringArgumentType.word()).suggests(GENERATOR_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), 12345L))
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), LongArgumentType.getLong(context, "seed")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> mapTree(String name) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> map(context.getSource()));
}
private static int noise(CommandSourceStack source, String generatorKey, long seed) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
IrisModdedCommands.fail(source, guiUnavailableMessage());
return 0;
}
if (engine != null) {
ModdedGuiHost.bindContext(source.getServer(), level, engine);
}
if (generatorKey == null || generatorKey.isBlank()) {
NoiseExplorerGUI.launch();
IrisModdedCommands.ok(source, "Opening the Noise Explorer on the server display.");
return 1;
}
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; run /iris studio noise from an Iris dimension to resolve generators, or omit the generator name.");
return 0;
}
IrisGenerator generator = engine.getData().getGeneratorLoader().load(generatorKey.trim());
if (generator == null) {
IrisModdedCommands.fail(source, "Unknown generator '" + generatorKey + "' in pack " + engine.getDimension().getLoadKey() + ".");
return 0;
}
long mixedSeed = new RNG(seed).nextParallelRNG(3245).lmax();
Supplier<Function2<Double, Double, Double>> supplier = () -> (Double x, Double z) -> generator.getHeight(x, z, mixedSeed);
NoiseExplorerGUI.launch(supplier, generatorKey.trim());
IrisModdedCommands.ok(source, "Opening the Noise Explorer for generator '" + generatorKey.trim() + "' (seed " + seed + ").");
return 1;
}
private static int map(CommandSourceStack source) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; stand in an Iris (or studio) dimension and run /iris studio map.");
return 0;
}
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
IrisModdedCommands.fail(source, guiUnavailableMessage());
return 0;
}
ModdedGuiHost.bindContext(source.getServer(), level, engine);
VisionGUI.launch(engine);
IrisModdedCommands.ok(source, "Opening the Vision map for " + level.dimension().identifier() + " on the server display.");
return 1;
}
private static String guiUnavailableMessage() {
if (!GuiHost.isAvailable()) {
return "This server has no display (headless JVM); the Iris desktop GUIs need an AWT-capable session.";
}
return "Server-launched GUIs are disabled (gui.useServerLaunchedGuis=false in Iris settings).";
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> {
@@ -176,50 +283,177 @@ public final class ModdedStudioCommands {
return folder;
}
private static String studioDimensionId(ServerPlayer player) {
String base = STUDIO_ID_SANITIZER.matcher(player.getScoreboardName().toLowerCase(Locale.ROOT)).replaceAll("_");
if (base.isBlank()) {
base = player.getUUID().toString().replace("-", "");
}
return STUDIO_NAMESPACE + ":" + STUDIO_PREFIX + base;
}
private static int open(CommandSourceStack source, String pack, long seed) {
File folder = resolvePack(source, pack);
if (folder == null) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players (a studio teleports you into the dimension).");
return 0;
}
IrisData data = IrisData.get(folder);
IrisDimension dimension = data.getDimensionLoader().load(folder.getName());
if (dimension == null) {
IrisModdedCommands.fail(source, "Pack '" + folder.getName() + "' has no dimensions/" + folder.getName() + ".json");
if (pack == null || pack.isBlank()) {
IrisModdedCommands.fail(source, "Provide a dimension pack: /iris studio open <pack> [seed]");
return 0;
}
IrisModdedCommands.ok(source, studioOpenComponent(dimension, folder, seed));
MinecraftServer server = source.getServer();
UUID owner = player.getUUID();
String dimensionId = studioDimensionId(player);
IrisModdedCommands.ok(source, "Opening studio for '" + pack + "' (seed " + seed + ")...");
Thread thread = new Thread(() -> openAsync(source, server, owner, dimensionId, pack, seed), "Iris Studio Open");
thread.setDaemon(true);
thread.start();
return 1;
}
private static MutableComponent studioOpenComponent(IrisDimension dimension, File folder, long seed) {
String dimensionKey = dimension.getLoadKey() == null ? folder.getName() : dimension.getLoadKey();
MutableComponent message = Component.empty();
message.append(ModdedCommandFeedback.header("Iris Studio"));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Opening studio for the \"", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(dimension.getName(), ModdedCommandFeedback.PARAMETER_ALT));
message.append(ModdedCommandFeedback.text("\" pack", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(" (seed: " + seed + ")", ModdedCommandFeedback.VALUE));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Pack ", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(dimensionKey, ModdedCommandFeedback.PARAMETER));
message.append(ModdedCommandFeedback.text(" is ready at ", ModdedCommandFeedback.DESCRIPTION));
message.append(ModdedCommandFeedback.text(folder.getAbsolutePath(), ModdedCommandFeedback.VALUE));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Modded servers cannot create Bukkit's temporary studio world at runtime; edit this pack directly, then use the matching world/datapack workflow or ", ModdedCommandFeedback.DESCRIPTION));
message.append(ModdedCommandFeedback.button("/iris regen", "/iris regen", "Regenerate nearby chunks after editing this pack", false));
message.append(ModdedCommandFeedback.text(" in an Iris dimension.", ModdedCommandFeedback.DESCRIPTION));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.button("Validate", "/iris pack validate " + dimensionKey, "Validate this pack before loading it", true));
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
message.append(ModdedCommandFeedback.button("World Help", "/iris world", "Open Iris world command help", true));
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
message.append(ModdedCommandFeedback.button("Package", "/iris studio package " + dimensionKey, "Package this dimension", false));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.footer());
return message;
private static void openAsync(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
try {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, "Pack '" + pack + "' missing; downloading IrisDimensions/" + pack + "..."));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master",
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack + "' could not be downloaded; check the name and try /iris download " + pack + "."));
return;
}
}
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(pack);
if (dimension == null) {
server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack + "' has no dimensions/" + pack + ".json"));
return;
}
server.execute(() -> injectAndTeleport(source, server, owner, dimensionId, pack, seed));
} catch (Throwable e) {
LOGGER.error("Iris studio open failed for {}", pack, e);
server.execute(() -> IrisModdedCommands.fail(source, "Studio open failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage())));
}
}
private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
ServerPlayer player = server.getPlayerList().getPlayer(owner);
if (player == null) {
return;
}
ModdedDimensionManager.Handle handle;
try {
handle = ModdedDimensionManager.create(server, dimensionId, 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()));
return;
}
STUDIOS.put(owner, dimensionId);
ServerLevel studio = handle.level();
int surface = studio.getMaxY();
try {
Engine engine = IrisModdedCommands.engineFor(studio);
if (engine != null) {
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
}
} catch (Throwable e) {
LOGGER.error("Iris studio surface probe failed for {}", dimensionId, e);
}
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
IrisModdedCommands.ok(source, "Studio open: " + dimensionId + " now runs '" + pack + "' seed " + seed + ". Use /iris studio close when done.");
}
private static int close(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players.");
return 0;
}
MinecraftServer server = source.getServer();
UUID owner = player.getUUID();
String dimensionId = STUDIOS.remove(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, "You do not have an open studio. Use /iris studio open <pack> first.");
return 0;
}
try {
ModdedDimensionManager.remove(server, dimensionId, true);
} catch (Throwable e) {
LOGGER.error("Iris studio close failed for {}", dimensionId, e);
IrisModdedCommands.fail(source, "Studio close failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
return 0;
}
IrisModdedCommands.ok(source, "Studio closed: " + dimensionId + " was evacuated, unloaded and its region data deleted.");
return 1;
}
private static int status(CommandSourceStack source) {
MinecraftServer server = source.getServer();
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
List<ModdedDimensionManager.Handle> studios = new ArrayList<>();
for (ModdedDimensionManager.Handle handle : handles) {
if (handle.dimensionId().startsWith(STUDIO_NAMESPACE + ":" + STUDIO_PREFIX) && ModdedDimensionManager.level(server, handle.dimensionId()) != null) {
studios.add(handle);
}
}
if (studios.isEmpty()) {
IrisModdedCommands.ok(source, "No studio dimensions are currently open.");
return 1;
}
IrisModdedCommands.ok(source, "Active studio dimension(s): " + studios.size());
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);
}
return 1;
}
private static UUID ownerOf(String dimensionId) {
for (Map.Entry<UUID, String> entry : STUDIOS.entrySet()) {
if (entry.getValue().equals(dimensionId)) {
return entry.getKey();
}
}
return null;
}
private static String ownerName(MinecraftServer server, UUID owner) {
ServerPlayer player = server.getPlayerList().getPlayer(owner);
return player == null ? owner.toString() : player.getScoreboardName();
}
private static int tpStudio(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players.");
return 0;
}
MinecraftServer server = source.getServer();
String dimensionId = STUDIOS.get(player.getUUID());
if (dimensionId == null) {
IrisModdedCommands.fail(source, "You do not have an open studio. Use /iris studio open <pack> first.");
return 0;
}
ServerLevel studio = ModdedDimensionManager.level(server, dimensionId);
if (studio == null) {
STUDIOS.remove(player.getUUID());
IrisModdedCommands.fail(source, "Your studio dimension is no longer loaded. Use /iris studio open <pack> again.");
return 0;
}
int surface = studio.getMaxY();
try {
Engine engine = IrisModdedCommands.engineFor(studio);
if (engine != null) {
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
}
} catch (Throwable e) {
LOGGER.error("Iris tpstudio surface probe failed", e);
}
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
IrisModdedCommands.ok(source, "Teleported to your studio (" + dimensionId + ").");
return 1;
}
private static int version(CommandSourceStack source, String pack) {
@@ -0,0 +1,94 @@
/*
* 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;
import art.arcane.iris.core.gui.GuiMarker;
import art.arcane.iris.core.gui.GuiOverlay;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
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.LivingEntity;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public final class ModdedVisionOverlay implements GuiOverlay {
private final MinecraftServer server;
private final ServerLevel level;
private final Engine engine;
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine) {
this.server = server;
this.level = level;
this.engine = engine;
}
@Override
public List<GuiMarker> players() {
List<GuiMarker> markers = new ArrayList<>();
for (ServerPlayer player : level.players()) {
Vec3 position = player.position();
markers.add(GuiMarker.player(player.getScoreboardName(), position.x(), position.z()));
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
server.execute(() -> {
List<GuiMarker> markers = new ArrayList<>();
for (Entity entity : level.getAllEntities()) {
if (entity instanceof ServerPlayer || !(entity instanceof LivingEntity living)) {
continue;
}
Vec3 position = living.position();
markers.add(GuiMarker.entity(living.getType().toShortString(), position.x(), position.y(), position.z(),
living.getHealth(), living.getMaxHealth()));
}
sink.accept(markers);
});
}
@Override
public void teleport(double worldX, double worldZ) {
int blockX = (int) worldX;
int blockZ = (int) worldZ;
server.execute(() -> {
List<ServerPlayer> players = level.players();
if (players.isEmpty()) {
return;
}
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);
});
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
return null;
}
}
@@ -18,12 +18,16 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedModConfig;
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.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
@@ -34,15 +38,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.Locale;
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 SuggestionProvider<CommandSourceStack> ENABLED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestEnabledDimensions(context, builder);
private static final String DEFAULT_NAMESPACE = "irisworldgen";
private static final SuggestionProvider<CommandSourceStack> LOADED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(loadedIrisDimensions(context.getSource().getServer()), builder);
private ModdedWorldCommands() {
}
@@ -63,15 +68,9 @@ public final class ModdedWorldCommands {
root.then(enableTree("create"));
root.then(replaceOverworldTree());
root.then(Commands.literal("disable")
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(Commands.literal("remove")
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(Commands.literal("rm")
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(disableTree("disable"));
root.then(disableTree("remove"));
root.then(disableTree("rm"));
return root;
}
@@ -83,105 +82,183 @@ public final class ModdedWorldCommands {
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
StringArgumentType.getString(context, "dimension"),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "pack")))
StringArgumentType.getString(context, "pack"),
1337L))
.then(Commands.argument("packDimension", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
StringArgumentType.getString(context, "dimension"),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "packDimension"))))));
StringArgumentType.getString(context, "packDimension"),
1337L)))));
}
private static LiteralArgumentBuilder<CommandSourceStack> disableTree(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"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> replaceOverworldTree() {
return Commands.literal("replace-overworld")
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
"minecraft:overworld",
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "pack")))
.then(Commands.argument("packDimension", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
"minecraft:overworld",
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "packDimension")))));
}
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension) {
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
return enable(source, name, pack, pack, seed);
}
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension, long seed) {
MinecraftServer server = source.getServer();
String dimensionId;
try {
ModdedWorldDatapackWriter.WriteResult result = ModdedWorldDatapackWriter.enable(source.getServer(), targetDimension, packName, packDimension);
IrisModdedCommands.ok(source, "Enabled Iris world " + result.targetDimension() + " using pack '" + result.packName() + "' dimension '" + result.packDimension() + "'.");
IrisModdedCommands.ok(source, "Wrote " + result.dimensionFile().getPath());
IrisModdedCommands.ok(source, "Wrote " + result.typeFile().getPath());
IrisModdedCommands.ok(source, "Wrote " + result.packMetaFile().getPath());
IrisModdedCommands.ok(source, "Restart the server, then use /execute in " + result.targetDimension() + " run tp <player> 0 100 0 or a portal/modded dimension tool.");
if ("minecraft:overworld".equals(result.targetDimension())) {
IrisModdedCommands.ok(source, "minecraft:overworld replacement was explicitly requested.");
}
return 1;
} catch (IOException e) {
LOGGER.error("Iris world datapack write failed for {}", targetDimension, e);
IrisModdedCommands.fail(source, "Failed to write Iris world datapack: " + e.getMessage());
return 0;
dimensionId = normalizeDimensionId(targetDimension);
} catch (IllegalArgumentException e) {
IrisModdedCommands.fail(source, e.getMessage());
return 0;
}
if (!loadPackDimension(source, packName, packDimension)) {
return 0;
}
try {
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, seed);
} catch (Throwable e) {
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, packName, 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, "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) {
MinecraftServer server = source.getServer();
String dimensionId = DEFAULT_NAMESPACE + ":primary";
if (!loadPackDimension(source, packName, packDimension)) {
return 0;
}
try {
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, 1337L);
} catch (Throwable e) {
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, packName, 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, "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);
if (!packFolder.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + packName + "' 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");
return false;
}
return true;
}
private static int disable(CommandSourceStack source, String targetDimension) {
MinecraftServer server = source.getServer();
String dimensionId;
try {
File removed = ModdedWorldDatapackWriter.disable(source.getServer(), targetDimension);
IrisModdedCommands.ok(source, "Removed " + removed.getPath());
IrisModdedCommands.ok(source, "Restart the server for the dimension removal to apply.");
return 1;
} catch (IOException e) {
LOGGER.error("Iris world datapack removal failed for {}", targetDimension, e);
IrisModdedCommands.fail(source, "Failed to remove Iris world datapack entry: " + e.getMessage());
return 0;
dimensionId = normalizeDimensionId(targetDimension);
} catch (IllegalArgumentException e) {
IrisModdedCommands.fail(source, e.getMessage());
return 0;
}
boolean removed;
try {
removed = ModdedDimensionManager.removePersistent(server, dimensionId);
} 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()));
return 0;
}
if (dimensionId.equals(ModdedModConfig.get().primaryWorld())) {
ModdedModConfig.setPrimaryWorld("");
ModdedPrimaryWorldRouter.clear();
}
if (!removed) {
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry.");
return 1;
}
IrisModdedCommands.ok(source, "Removed Iris world '" + dimensionId + "': evacuated, unloaded, region data deleted, and dropped from the startup registry.");
return 1;
}
private static int status(CommandSourceStack source) {
list(source);
int loaded = 0;
MinecraftServer server = source.getServer();
int loaded = 0;
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.dimensionKey() + "'");
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.activePackKey() + "'");
}
}
String primary = ModdedModConfig.get().primaryWorld();
if (!primary.isBlank()) {
IrisModdedCommands.ok(source, "Primary world: " + primary + (ModdedModConfig.get().routePlayersToPrimaryWorld() ? " (players routed there)" : " (routing disabled)"));
}
if (loaded == 0) {
IrisModdedCommands.fail(source, "No Iris dimensions are currently loaded. Enabled dimensions require a server restart before Minecraft loads them.");
IrisModdedCommands.fail(source, "No Iris dimensions are currently loaded. Create one with /iris world create <name> <pack>.");
}
return loaded > 0 ? 1 : 0;
}
private static int list(CommandSourceStack source) {
try {
List<String> dimensions = ModdedWorldDatapackWriter.enabledDimensions(source.getServer());
IrisModdedCommands.ok(source, "Enabled Iris world datapack dimensions: " + dimensions.size());
for (String dimension : dimensions) {
IrisModdedCommands.ok(source, " - " + dimension);
}
if (dimensions.isEmpty()) {
IrisModdedCommands.ok(source, "Use /iris world enable <dimension> <pack> to create one without replacing the main world.");
}
return 1;
} catch (IOException e) {
LOGGER.error("Iris world datapack listing failed", e);
IrisModdedCommands.fail(source, "Failed to list Iris world datapack dimensions: " + e.getMessage());
return 0;
List<String> dimensions = loadedIrisDimensions(source.getServer());
IrisModdedCommands.ok(source, "Loaded Iris dimensions: " + dimensions.size());
for (String dimension : dimensions) {
IrisModdedCommands.ok(source, " - " + dimension);
}
if (dimensions.isEmpty()) {
IrisModdedCommands.ok(source, "Use /iris world create <name> <pack> to inject one without restarting.");
}
return 1;
}
private static CompletableFuture<Suggestions> suggestEnabledDimensions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
return SharedSuggestionProvider.suggest(ModdedWorldDatapackWriter.enabledDimensions(context.getSource().getServer()), builder);
} catch (IOException e) {
return Suggestions.empty();
private static List<String> loadedIrisDimensions(MinecraftServer server) {
List<String> dimensions = new ArrayList<>();
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
dimensions.add(level.dimension().identifier().toString());
}
}
return dimensions;
}
private static String normalizeDimensionId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing dimension id.");
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
String namespace = DEFAULT_NAMESPACE;
String path = normalized;
int colon = normalized.indexOf(':');
if (colon >= 0) {
namespace = normalized.substring(0, colon);
path = normalized.substring(colon + 1);
}
if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) {
throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path.");
}
return namespace + ":" + path;
}
}
@@ -1,217 +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;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.LevelResource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
final class ModdedWorldDatapackWriter {
static final String WORLD_PACK_NAME = "iris";
private static final String DEFAULT_NAMESPACE = "irisworldgen";
private static final String TYPE_NAMESPACE = "irisworldgen";
private ModdedWorldDatapackWriter() {
}
static WriteResult enable(MinecraftServer server, String targetDimension, String packName, String packDimension) throws IOException {
ResourceTarget target = ResourceTarget.parse(targetDimension);
String cleanPackName = cleanPackSegment(packName, "pack");
String cleanPackDimension = cleanPackSegment(packDimension, "pack dimension");
File packFolder = new File(ModdedPackCommands.packsRoot(), cleanPackName);
if (!packFolder.isDirectory()) {
throw new IllegalArgumentException("Pack '" + cleanPackName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
}
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(cleanPackDimension);
if (dimension == null) {
throw new IllegalArgumentException("Pack '" + cleanPackName + "' does not contain dimensions/" + cleanPackDimension + ".json");
}
String typeKey = IrisDimension.sanitizeDimensionTypeKeyValue(cleanPackDimension);
File typeFile = dimensionTypeFile(server, typeKey);
File dimensionFile = dimensionFile(server, target);
File mcmeta = packMetaFile(server);
writeParented(typeFile, dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
writeParented(dimensionFile, dimensionJson(cleanPackDimension, typeKey));
writePackMeta(mcmeta);
return new WriteResult(target.id(), cleanPackName, cleanPackDimension, dimensionFile, typeFile, mcmeta);
}
static File disable(MinecraftServer server, String targetDimension) throws IOException {
ResourceTarget target = ResourceTarget.parse(targetDimension);
File dimensionFile = dimensionFile(server, target);
if (!dimensionFile.isFile()) {
throw new IllegalArgumentException("No Iris world datapack dimension exists for " + target.id());
}
Files.delete(dimensionFile.toPath());
return dimensionFile;
}
static List<String> enabledDimensions(MinecraftServer server) throws IOException {
File dataRoot = new File(irisPackFolder(server), "data");
if (!dataRoot.isDirectory()) {
return List.of();
}
List<String> dimensions = new ArrayList<>();
File[] namespaces = dataRoot.listFiles(File::isDirectory);
if (namespaces == null) {
return List.of();
}
for (File namespace : namespaces) {
File dimensionRoot = new File(namespace, "dimension");
if (!dimensionRoot.isDirectory()) {
continue;
}
collectDimensionFiles(namespace.getName(), dimensionRoot, dimensions);
}
Collections.sort(dimensions);
return dimensions;
}
static File packMetaFile(MinecraftServer server) {
return new File(irisPackFolder(server), "pack.mcmeta");
}
private static void collectDimensionFiles(String namespace, File dimensionRoot, List<String> dimensions) throws IOException {
Path root = dimensionRoot.toPath();
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(Files::isRegularFile)
.filter((Path path) -> path.getFileName().toString().endsWith(".json"))
.forEach((Path path) -> {
String relative = root.relativize(path).toString().replace(File.separatorChar, '/');
String idPath = relative.substring(0, relative.length() - ".json".length());
dimensions.add(namespace + ":" + idPath);
});
}
}
private static File worldDatapacksFolder(MinecraftServer server) {
return server.getWorldPath(LevelResource.DATAPACK_DIR).toFile();
}
private static File irisPackFolder(MinecraftServer server) {
return new File(worldDatapacksFolder(server), WORLD_PACK_NAME);
}
private static File dimensionTypeFile(MinecraftServer server, String typeKey) {
return new File(irisPackFolder(server), "data/" + TYPE_NAMESPACE + "/dimension_type/" + typeKey + ".json");
}
private static File dimensionFile(MinecraftServer server, ResourceTarget target) {
return new File(new File(irisPackFolder(server), "data/" + target.namespace() + "/dimension"), target.path() + ".json");
}
private static void writePackMeta(File output) throws IOException {
int packFormat = DataVersion.getLatest().getPackFormat();
String json = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris world and dimension type definitions.\",\n"
+ " \"pack_format\": " + packFormat + ",\n"
+ " \"min_format\": " + packFormat + ",\n"
+ " \"max_format\": " + packFormat + "\n"
+ " }\n"
+ "}\n";
writeParented(output, json);
}
private static void writeParented(File output, String text) throws IOException {
File parent = output.getParentFile();
if (parent != null) {
parent.mkdirs();
}
Files.writeString(output.toPath(), text, StandardCharsets.UTF_8);
}
private static String dimensionJson(String packDimension, String typeKey) {
return "{\n"
+ " \"type\": \"" + TYPE_NAMESPACE + ":" + typeKey + "\",\n"
+ " \"generator\": {\n"
+ " \"type\": \"irisworldgen:iris\",\n"
+ " \"dimension\": \"" + escape(packDimension) + "\",\n"
+ " \"biome_source\": {\n"
+ " \"type\": \"minecraft:fixed\",\n"
+ " \"biome\": \"minecraft:plains\"\n"
+ " }\n"
+ " }\n"
+ "}\n";
}
private static String cleanPackSegment(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing " + label + ".");
}
String clean = value.trim();
if (clean.contains("/") || clean.contains("..") || clean.contains("\\") || clean.contains(":")) {
throw new IllegalArgumentException("Invalid " + label + " '" + value + "'.");
}
return clean;
}
private static String escape(String value) {
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
record WriteResult(String targetDimension, String packName, String packDimension, File dimensionFile, File typeFile, File packMetaFile) {
}
private record ResourceTarget(String namespace, String path) {
static ResourceTarget parse(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing dimension id.");
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
String namespace = DEFAULT_NAMESPACE;
String path = normalized;
int colon = normalized.indexOf(':');
if (colon >= 0) {
namespace = normalized.substring(0, colon);
path = normalized.substring(colon + 1);
}
if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) {
throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path.");
}
return new ResourceTarget(namespace, path);
}
String id() {
return namespace + ":" + path;
}
}
}
@@ -0,0 +1,164 @@
/*
* 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 org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.message.Message;
import java.util.List;
public final class ModdedLogFilterService implements ModdedService, Filter {
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");
@Override
public void onEnable() {
((Logger) LogManager.getRootLogger()).addFilter(this);
}
@Override
public void onDisable() {
}
@Override
public void initialize() {
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public boolean isStarted() {
return true;
}
@Override
public boolean isStopped() {
return false;
}
@Override
public State getState() {
return State.STARTED;
}
@Override
public Filter.Result getOnMatch() {
return Result.NEUTRAL;
}
@Override
public Filter.Result getOnMismatch() {
return Result.NEUTRAL;
}
@Override
public Result filter(LogEvent event) {
return check(event.getMessage().getFormattedMessage());
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return check(String.valueOf(msg));
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return check(msg.getFormattedMessage());
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object... params) {
return check(message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0) {
return check(message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1) {
return check(message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2) {
return check(message);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) {
return check(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);
}
@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);
}
@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);
}
@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);
}
@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);
}
@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);
}
private Result check(String message) {
if (message == null) {
return Result.NEUTRAL;
}
for (String filter : FILTERS) {
if (message.contains(filter)) {
return Result.DENY;
}
}
return Result.NEUTRAL;
}
}
@@ -0,0 +1,122 @@
/*
* 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.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedPreservationService implements ModdedService, PreservationRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long DEREFERENCE_INTERVAL_MILLIS = 60000L;
private final List<Thread> threads = new CopyOnWriteArrayList<>();
private final List<ExecutorService> services = new CopyOnWriteArrayList<>();
private final List<WeakReference<MeteredCache>> caches = new CopyOnWriteArrayList<>();
private final AtomicBoolean running = new AtomicBoolean(false);
private Thread dereferencer;
@Override
public void register(Thread thread) {
threads.add(thread);
}
public void register(ExecutorService service) {
services.add(service);
}
@Override
public void registerCache(MeteredCache cache) {
caches.add(new WeakReference<>(cache));
}
@Override
public void dereference() {
threads.removeIf((Thread thread) -> !thread.isAlive());
services.removeIf(ExecutorService::isShutdown);
caches.removeIf((WeakReference<MeteredCache> ref) -> {
MeteredCache cache = ref.get();
return cache == null || cache.isClosed();
});
}
@Override
public void onEnable() {
if (!running.compareAndSet(false, true)) {
return;
}
dereferencer = new Thread(this::loop, "iris-modded-preservation");
dereferencer.setDaemon(true);
dereferencer.start();
}
@Override
public void onDisable() {
if (!running.compareAndSet(true, false)) {
return;
}
if (dereferencer != null) {
dereferencer.interrupt();
dereferencer = null;
}
dereference();
shutdownTrackedResources();
}
private void loop() {
while (running.get()) {
try {
Thread.sleep(DEREFERENCE_INTERVAL_MILLIS);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
dereference();
}
}
private void shutdownTrackedResources() {
for (Thread thread : threads) {
if (!thread.isAlive()) {
continue;
}
try {
thread.interrupt();
LOGGER.info("Iris preservation interrupted thread {}", thread.getName());
} catch (Throwable error) {
LOGGER.error("Iris preservation failed to interrupt thread {}", thread.getName(), error);
}
}
for (ExecutorService service : services) {
try {
service.shutdownNow();
LOGGER.info("Iris preservation shut down executor {}", service);
} catch (Throwable error) {
LOGGER.error("Iris preservation failed to shut down executor {}", service, error);
}
}
}
}
@@ -0,0 +1,25 @@
/*
* 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;
public interface ModdedService {
void onEnable();
void onDisable();
}
@@ -0,0 +1,25 @@
/*
* 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 net.minecraft.server.MinecraftServer;
public interface ModdedTickableService extends ModdedService {
void onServerTick(MinecraftServer server);
}