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
@@ -859,88 +859,6 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
try {
art.arcane.iris.util.nbt.common.mca.MCAFile mca = nbtWorld.getMCA(chunkX >> 5, chunkZ >> 5);
if (mca == null) {
return false;
}
art.arcane.iris.util.nbt.common.mca.Chunk chunk = art.arcane.iris.util.nbt.common.mca.Chunk.newChunk();
art.arcane.iris.util.nbt.common.mca.Chunk.injectIrisData(chunk);
int blockHeight = blocks.getHeight();
java.util.IdentityHashMap<BlockData, CompoundTag> encodeCache = new java.util.IdentityHashMap<>(64);
for (int y = 0; y < blockHeight; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
PlatformBlockState platformState = blocks.getRaw(x, y, z);
if (platformState == null) {
continue;
}
Object nativeHandle = platformState.nativeHandle();
if (!(nativeHandle instanceof BlockData blockData)) {
continue;
}
if (blockData instanceof IrisCustomData customData) {
blockData = customData.getBase();
}
if (blockData.getMaterial().isAir()) {
continue;
}
CompoundTag nbtState = encodeCache.get(blockData);
if (nbtState == null) {
nbtState = NBTWorld.getCompound(blockData);
if (nbtState == null) {
continue;
}
encodeCache.put(blockData, nbtState);
}
chunk.setBlockStateAt(x, y, z, nbtState, false);
}
}
}
int biomeHeight = biomes.getHeight();
for (int by = 0; by < biomeHeight; by += 4) {
int quartY = by >> 2;
for (int bz = 0; bz < 16; bz += 4) {
int quartZ = bz >> 2;
for (int bx = 0; bx < 16; bx += 4) {
int quartX = bx >> 2;
PlatformBiome platformBiome = biomes.getRaw(bx, by, bz);
if (platformBiome == null) {
continue;
}
String biomeKey = platformBiome.key();
if (biomeKey == null || biomeKey.isEmpty()) {
continue;
}
int biomeId = getBiomeBaseIdForKey(biomeKey);
if (biomeId < 0) {
continue;
}
chunk.setBiomeAt(quartX, quartY, quartZ, biomeId);
}
}
}
chunk.cleanupPalettesAndBlockStates();
synchronized (mca) {
mca.setChunk(chunkX & 31, chunkZ & 31, chunk);
}
return true;
} catch (Throwable e) {
IrisLogging.warn("writeChunkNbtDirect failed at " + chunkX + "," + chunkZ + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
e.printStackTrace(System.err);
IrisLogging.reportError(e);
return false;
}
}
@Override
public boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
try {
@@ -39,6 +39,7 @@ import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.EditSVC;
import art.arcane.iris.core.service.PreservationSVC;
@@ -947,6 +948,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void onEnable() {
IrisPlatforms.bind(new BukkitPlatform());
enable();
BukkitGuiHost.install();
super.onEnable();
Bukkit.getPluginManager().registerEvents(this, this);
}
@@ -323,7 +323,7 @@ public class CommandStudio implements DirectorExecutor {
return;
}
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
sender().sendMessage(C.GREEN + "Opening map!");
}
@@ -0,0 +1,91 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider {
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
public static void install() {
GuiHost.set(new BukkitGuiHost());
}
@Override
public Engine findActiveEngine() {
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return null;
}
@Override
public void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
hotloadHooks.put(onHotload, listener);
Iris.instance.registerListener(listener);
}
@Override
public void unregisterHotloadHook(Runnable onHotload) {
Listener listener = hotloadHooks.remove(onHotload);
if (listener != null) {
Iris.instance.unregisterListener(listener);
}
}
@Override
public GuiOverlay overlayFor(Engine engine) {
return engine == null ? null : new BukkitVisionOverlay(engine);
}
private static final class HotloadListener implements Listener {
private final Runnable onHotload;
private HotloadListener(Runnable onHotload) {
this.onHotload = onHotload;
}
@EventHandler
public void on(IrisEngineHotloadEvent event) {
onHotload.run();
}
}
}
@@ -0,0 +1,111 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
public BukkitVisionOverlay(Engine engine) {
this.engine = engine;
}
@Override
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : world.getPlayers()) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
entity.getHealth(), maxHealth));
}
sink.accept(markers);
});
}
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasRealWorld()) {
return;
}
J.s(() -> {
List<Player> players = world.getPlayers();
if (players.isEmpty()) {
return;
}
Player player = players.get(0);
int xx = (int) worldX;
int zz = (int) worldZ;
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
});
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
}
+1
View File
@@ -147,6 +147,7 @@ tasks.named('compileJava', JavaCompile).configure {
}
loom {
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
runs {
server {
String parity = providers.gradleProperty('irisParity').getOrNull()
@@ -0,0 +1,48 @@
/*
* 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.fabric;
import art.arcane.iris.modded.ModdedForcedDatapack;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Set;
public final class FabricForcedDatapackSources {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricForcedDatapackSources() {
}
public static void attach(PackRepository repository) {
if (repository == null) {
return;
}
try {
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
repository.sources = merged;
} catch (Throwable e) {
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
}
}
}
@@ -72,13 +72,17 @@ public final class IrisFabricBootstrap implements ModInitializer {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
});
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> ModdedWandService.serverTick(server));
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> {
ModdedEngineBootstrap.tick(server);
ModdedWandService.serverTick(server);
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -0,0 +1,36 @@
/*
* 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.fabric.mixin;
import art.arcane.iris.fabric.FabricForcedDatapackSources;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerPacksSource.class)
public class ServerPacksSourceMixin {
@Inject(method = "createPackRepository(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/server/packs/repository/PackRepository;", at = @At("RETURN"))
private static void iris$addForcedDatapackSource(LevelStorageSource.LevelStorageAccess storage, CallbackInfoReturnable<PackRepository> info) {
FabricForcedDatapackSources.attach(info.getReturnValue());
}
}
@@ -10,6 +10,8 @@
},
"license": "GPL-3.0",
"environment": "*",
"accessWidener": "irisworldgen.accesswidener",
"mixins": ["irisworldgen.mixins.json"],
"entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"]
},
@@ -0,0 +1,6 @@
accessWidener v2 official
accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map;
accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor;
accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;
accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
@@ -0,0 +1,12 @@
{
"required": true,
"minVersion": "0.8",
"package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"ServerPacksSourceMixin"
],
"injectors": {
"defaultRequire": 1
}
}
+11
View File
@@ -170,6 +170,7 @@ dependencies {
}
minecraft {
accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg'))
runs {
register('server') {
workingDir = layout.projectDirectory.dir('run')
@@ -214,6 +215,7 @@ tasks.named('shadowJar', ShadowJar).configure {
}
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
@@ -223,7 +225,16 @@ tasks.named('shadowJar', ShadowJar).configure {
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**')
exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**')
exclude('org/checkerframework/**')
exclude('org/jetbrains/annotations/**')
exclude('org/intellij/lang/**')
}
tasks.named('assemble').configure {
@@ -20,6 +20,7 @@ package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
@@ -29,7 +30,9 @@ import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.event.AddPackFindersEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
@@ -66,13 +69,22 @@ public final class IrisForgeBootstrap {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
});
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
}
});
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.server()));
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> {
ModdedEngineBootstrap.tick(event.server());
ModdedWandService.serverTick(event.server());
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -0,0 +1,3 @@
public net.minecraft.server.MinecraftServer levels
public net.minecraft.server.MinecraftServer executor
public net.minecraft.server.MinecraftServer storageSource
@@ -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);
}
+12
View File
@@ -126,6 +126,8 @@ dependencies {
neoForge {
version = neoForgeVersion
accessTransformers.from('src/main/resources/META-INF/accesstransformer.cfg')
runs {
server {
server()
@@ -176,6 +178,7 @@ tasks.named('shadowJar', ShadowJar).configure {
}
archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}"))
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
@@ -184,7 +187,16 @@ tasks.named('shadowJar', ShadowJar).configure {
exclude('META-INF/*.RSA')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**')
exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**')
exclude('org/checkerframework/**')
exclude('org/jetbrains/annotations/**')
exclude('org/intellij/lang/**')
}
tasks.named('assemble').configure {
@@ -20,6 +20,7 @@ package art.arcane.iris.neoforge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
@@ -29,6 +30,7 @@ import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
@@ -38,6 +40,7 @@ import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.VersionInfo;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
@@ -62,10 +65,17 @@ public final class IrisNeoForgeBootstrap {
chunkGenerators.register(modBus);
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
modBus.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
}
});
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
ModdedEngineBootstrap.stop();
});
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> {
@@ -79,7 +89,10 @@ public final class IrisNeoForgeBootstrap {
event.setCancellationResult(InteractionResult.SUCCESS);
}
});
NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.getServer()));
NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> {
ModdedEngineBootstrap.tick(event.getServer());
ModdedWandService.serverTick(event.getServer());
});
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -0,0 +1,3 @@
public net.minecraft.server.MinecraftServer levels
public net.minecraft.server.MinecraftServer executor
public net.minecraft.server.MinecraftServer storageSource
@@ -150,11 +150,6 @@ public class IrisSettings {
public int maxResidentTectonicPlates = 96;
public int mantleBackpressureWaitMs = 25;
public int mantleBackpressureTimeoutMs = 60_000;
public boolean enableSpigotDirectPregen = false;
public boolean isEnableSpigotDirectPregen() {
return enableSpigotDirectPregen;
}
public int getChunkLoadTimeoutSeconds() {
return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120));
@@ -0,0 +1,61 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.Engine;
import java.awt.GraphicsEnvironment;
public final class GuiHost {
private static volatile Provider provider = new Provider() {
};
private GuiHost() {
}
public interface Provider {
default Engine findActiveEngine() {
return null;
}
default void registerHotloadHook(Runnable onHotload) {
}
default void unregisterHotloadHook(Runnable onHotload) {
}
default GuiOverlay overlayFor(Engine engine) {
return null;
}
}
public static void set(Provider boundProvider) {
if (boundProvider != null) {
provider = boundProvider;
}
}
public static Provider get() {
return provider;
}
public static boolean isAvailable() {
return !GraphicsEnvironment.isHeadless();
}
}
@@ -0,0 +1,29 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
public static GuiMarker player(String name, double worldX, double worldZ) {
return new GuiMarker(name, worldX, 0, worldZ, 0, 0);
}
public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth);
}
}
@@ -0,0 +1,34 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.RenderType;
import java.util.List;
import java.util.function.Consumer;
public interface GuiOverlay {
List<GuiMarker> players();
void requestEntities(Consumer<List<GuiMarker>> sink);
void teleport(double worldX, double worldZ);
String openInEditor(double worldX, double worldZ, RenderType type);
}
@@ -1,6 +1,6 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
* 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
@@ -18,15 +18,11 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
@@ -36,23 +32,44 @@ import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import javax.swing.*;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener {
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(24, 24, 28);
@@ -88,6 +105,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
private final RollingSequence fpsHistory = new RollingSequence(60);
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
private final MultiBurst gx = MultiBurst.burst;
private final Runnable hotloadHook = this::refreshGenerator;
private double scale = 1;
private double animScale = 10;
private double ox = 0;
@@ -107,7 +125,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
private String currentName = "STATIC";
public NoiseExplorerGUI() {
Iris.instance.registerListener(this);
GuiHost.get().registerHotloadHook(hotloadHook);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(new MouseMotionListener() {
@@ -130,7 +148,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
}
public static void launch() {
Engine engine = findActiveEngine();
Engine engine = GuiHost.get().findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
buildFrame("Noise Explorer", nv, engine, null, null);
@@ -138,7 +156,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
}
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
Engine engine = findActiveEngine();
Engine engine = GuiHost.get().findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
nv.loader = gen;
@@ -148,20 +166,6 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
});
}
private static Engine findActiveEngine() {
try {
for (World w : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(w);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {}
}
} catch (Throwable ignored) {}
return null;
}
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JFrame frame = new JFrame(title);
@@ -180,7 +184,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
Iris.instance.unregisterListener(nv);
GuiHost.get().unregisterHotloadHook(nv.hotloadHook);
}
});
return frame;
@@ -364,8 +368,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
}
@EventHandler
public void on(IrisEngineHotloadEvent e) {
private void refreshGenerator() {
if (generator != null && loader != null) {
generator = loader.get();
}
@@ -0,0 +1,31 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.volmlib.util.math.Position2;
public interface PregenRenderSource {
Position2 min();
Position2 max();
String[] progress();
boolean paused();
}
@@ -0,0 +1,158 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.function.Consumer2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.iris.util.common.scheduling.J;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.concurrent.locks.ReentrantLock;
public final class PregenRenderer extends JPanel implements KeyListener {
private static final long serialVersionUID = 2094606939770332040L;
private final KList<Runnable> order = new KList<>();
private final ReentrantLock lock = new ReentrantLock();
private final int res = 512;
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
private final PregenRenderSource source;
private final Runnable onPause;
private Graphics2D bg;
private JFrame frame;
private PregenRenderer(PregenRenderSource source, Runnable onPause) {
this.source = source;
this.onPause = onPause;
}
public static PregenRenderer open(String title, PregenRenderSource source, Runnable onPause) {
PregenRenderer renderer = new PregenRenderer(source, onPause);
JFrame frame = new JFrame(title);
renderer.frame = frame;
frame.addKeyListener(renderer);
frame.add(renderer);
frame.setSize(1000, 1000);
frame.setVisible(true);
return renderer;
}
public Consumer2<Position2, Color> drawFunction() {
return (Position2 c, Color color) -> {
lock.lock();
try {
order.add(() -> draw(c, color, bg));
} finally {
lock.unlock();
}
};
}
public void submit(int x, int z, Color color) {
drawFunction().accept(new Position2(x, z), color);
}
public boolean isVisibleFrame() {
return frame != null && frame.isVisible();
}
public void close() {
if (frame != null) {
frame.setVisible(false);
}
}
@Override
public void paint(Graphics gx) {
Graphics2D g = (Graphics2D) gx;
bg = (Graphics2D) image.getGraphics();
lock.lock();
try {
while (order.isNotEmpty()) {
try {
order.pop().run();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
} finally {
lock.unlock();
}
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
g.setColor(Color.WHITE);
g.setFont(new Font("Hevetica", Font.BOLD, 13));
String[] prog = source.progress();
int h = g.getFontMetrics().getHeight() + 5;
int hh = 20;
if (source.paused()) {
g.drawString("PAUSED", 20, hh += h);
g.drawString("Press P to Resume", 20, hh += h);
} else {
for (String i : prog) {
g.drawString(i, 20, hh += h);
}
g.drawString("Press P to Pause", 20, hh += h);
}
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
repaint();
}
private void draw(Position2 p, Color c, Graphics2D bg) {
double pw = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX());
double ph = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ());
double pwa = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX() + 1);
double pha = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ() + 1);
int x = (int) M.lerp(0, res, pw);
int z = (int) M.lerp(0, res, ph);
int xa = (int) M.lerp(0, res, pwa);
int za = (int) M.lerp(0, res, pha);
bg.setColor(c);
bg.fillRect(x, z, xa - x, za - z);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_P && onPause != null) {
onPause.run();
}
}
}
@@ -30,25 +30,19 @@ import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.format.MemoryMonitor;
import art.arcane.volmlib.util.function.Consumer2;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.awt.Color;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class PregeneratorJob implements PregenListener {
public class PregeneratorJob implements PregenListener, PregenRenderSource {
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
private static final Color COLOR_MANTLE = parseColor("#3c2773");
@@ -69,8 +63,8 @@ public class PregeneratorJob implements PregenListener {
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
private final Engine engine;
private final ExecutorService service;
private JFrame frame;
private PregenRenderer renderer;
private Consumer2<Position2, Color> drawFunction;
private int rgc = 0;
private String[] info;
private volatile double lastChunksPerSecond = 0D;
@@ -212,8 +206,8 @@ public class PregeneratorJob implements PregenListener {
public void draw(int x, int z, Color color) {
try {
if (renderer != null && frame != null && frame.isVisible()) {
renderer.func.accept(new Position2(x, z), color);
if (renderer != null && drawFunction != null && renderer.isVisibleFrame()) {
drawFunction.accept(new Position2(x, z), color);
}
} catch (Throwable ignored) {
IrisLogging.error("Failed to draw pregen");
@@ -232,11 +226,11 @@ public class PregeneratorJob implements PregenListener {
J.a(() -> {
try {
monitor.close();
if (frame == null) {
if (renderer == null) {
return;
}
J.sleep(3000);
frame.setVisible(false);
renderer.close();
} catch (Throwable ignored) {
IrisLogging.error("Error closing pregen gui");
}
@@ -246,20 +240,8 @@ public class PregeneratorJob implements PregenListener {
public void open() {
J.a(() -> {
try {
frame = new JFrame("Pregen View");
renderer = new PregenRenderer();
frame.addKeyListener(renderer);
renderer.l = new ReentrantLock();
renderer.frame = frame;
renderer.job = this;
renderer.func = (c, b) -> {
renderer.l.lock();
renderer.order.add(() -> renderer.draw(c, b, renderer.bg));
renderer.l.unlock();
};
frame.add(renderer);
frame.setSize(1000, 1000);
frame.setVisible(true);
renderer = PregenRenderer.open("Pregen View", this, PregeneratorJob::pauseResume);
drawFunction = renderer.drawFunction();
} catch (Throwable ignored) {
IrisLogging.error("Error opening pregen gui");
}
@@ -292,7 +274,7 @@ public class PregeneratorJob implements PregenListener {
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
if (renderer == null || frame == null || !frame.isVisible()) return;
if (renderer == null || !renderer.isVisibleFrame()) return;
service.submit(() -> {
if (engine != null) {
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
@@ -378,112 +360,23 @@ public class PregeneratorJob implements PregenListener {
draw(x, z, COLOR_EXISTS);
}
private Position2 getMax() {
@Override
public Position2 max() {
return max;
}
private Position2 getMin() {
@Override
public Position2 min() {
return min;
}
private boolean paused() {
@Override
public boolean paused() {
return pregenerator.paused();
}
private String[] getProgress() {
@Override
public String[] progress() {
return info;
}
public static class PregenRenderer extends JPanel implements KeyListener {
private static final long serialVersionUID = 2094606939770332040L;
private final KList<Runnable> order = new KList<>();
private final int res = 512;
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
Graphics2D bg;
private PregeneratorJob job;
private ReentrantLock l;
private Consumer2<Position2, Color> func;
private JFrame frame;
public PregenRenderer() {
}
public void paint(int x, int z, Color c) {
func.accept(new Position2(x, z), c);
}
@Override
public void paint(Graphics gx) {
Graphics2D g = (Graphics2D) gx;
bg = (Graphics2D) image.getGraphics();
l.lock();
while (order.isNotEmpty()) {
try {
order.pop().run();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
l.unlock();
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
g.setColor(Color.WHITE);
g.setFont(new Font("Hevetica", Font.BOLD, 13));
String[] prog = job.getProgress();
int h = g.getFontMetrics().getHeight() + 5;
int hh = 20;
if (job.paused()) {
g.drawString("PAUSED", 20, hh += h);
g.drawString("Press P to Resume", 20, hh += h);
} else {
for (String i : prog) {
g.drawString(i, 20, hh += h);
}
g.drawString("Press P to Pause", 20, hh += h);
}
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
repaint();
}
private void draw(Position2 p, Color c, Graphics2D bg) {
double pw = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX());
double ph = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ());
double pwa = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX() + 1);
double pha = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ() + 1);
int x = (int) M.lerp(0, res, pw);
int z = (int) M.lerp(0, res, ph);
int xa = (int) M.lerp(0, res, pwa);
int za = (int) M.lerp(0, res, pha);
bg.setColor(c);
bg.fillRect(x, z, xa - x, za - z);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_P) {
PregeneratorJob.pauseResume();
}
}
public void close() {
frame.setVisible(false);
}
}
}
@@ -1,6 +1,6 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
* 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
@@ -18,15 +18,11 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -38,25 +34,43 @@ import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import javax.swing.*;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.event.MouseInputListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
private static final long serialVersionUID = 2094606939770332040L;
@@ -65,9 +79,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
private static final Color CARD_BORDER = new Color(60, 60, 75, 180);
private static final Color TEXT_PRIMARY = new Color(220, 220, 230);
private static final Color TEXT_SECONDARY = new Color(140, 140, 155);
private static final Color TEXT_DIM = new Color(90, 90, 105);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color ACCENT_DIM = new Color(60, 100, 200, 100);
private static final Color PLAYER_COLOR = new Color(80, 200, 120);
private static final Color MOB_COLOR = new Color(220, 80, 80);
private static final Color STATUS_BG = new Color(24, 24, 30, 240);
@@ -81,7 +93,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
private static final int CARD_PAD = 12;
private static final int STATUS_HEIGHT = 26;
private final KList<LivingEntity> lastEntities = new KList<>();
private final KList<GuiMarker> lastEntities = new KList<>();
private final KMap<String, Long> notifications = new KMap<>();
private final ChronoLatch centities = new ChronoLatch(1000);
private final RollingSequence rs = new RollingSequence(512);
@@ -95,7 +107,6 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
private boolean help = true;
private boolean helpIgnored = false;
private boolean shift = false;
private Player player = null;
private boolean debug = false;
private boolean control = false;
private boolean eco = false;
@@ -103,8 +114,9 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
private boolean follow = false;
private boolean alt = false;
private boolean grid = false;
private GuiMarker followMarker = null;
private IrisRenderer renderer;
private IrisWorld world;
private GuiOverlay overlay;
private double velocity = 0;
private int lowq = 12;
private double scale = 128;
@@ -128,10 +140,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
Thread t = new Thread(r);
t.setName("Iris HD Renderer " + tid);
t.setPriority(Thread.MIN_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> {
Iris.info("Exception encountered in " + et.getName());
ex.printStackTrace();
});
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
return t;
});
@@ -140,10 +149,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
Thread t = new Thread(r);
t.setName("Iris Renderer " + tid);
t.setPriority(Thread.NORM_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> {
Iris.info("Exception encountered in " + et.getName());
ex.printStackTrace();
});
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
return t;
});
@@ -170,11 +176,15 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
});
}
private static void createAndShowGUI(Engine r, int s, IrisWorld world) {
public static void launch(Engine g) {
J.a(() -> createAndShowGUI(g));
}
private static void createAndShowGUI(Engine r) {
JFrame frame = new JFrame("Iris Vision");
VisionGUI nv = new VisionGUI(frame);
nv.world = world;
nv.engine = r;
nv.overlay = GuiHost.get().overlayFor(r);
nv.renderer = new IrisRenderer(r);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
@@ -261,18 +271,17 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
return sep;
}
public static void launch(Engine g, int i) {
J.a(() -> createAndShowGUI(g, i, g.getWorld()));
}
public boolean updateEngine() {
if (engine.isClosed()) {
if (world.hasRealWorld()) {
try {
engine = IrisToolbelt.access(world.realWorld()).getEngine();
return !engine.isClosed();
} catch (Throwable ignored) {
try {
Engine reacquired = GuiHost.get().findActiveEngine();
if (reacquired != null && !reacquired.isClosed()) {
engine = reacquired;
overlay = GuiHost.get().overlayFor(reacquired);
renderer = new IrisRenderer(reacquired);
return true;
}
} catch (Throwable ignored) {
}
}
return false;
@@ -378,8 +387,8 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
void toggleFollow() {
follow = !follow;
if (player != null && follow) {
notify("Following " + player.getName());
if (followMarker != null && follow) {
notify("Following " + followMarker.label());
} else if (follow) {
notify("No player in world");
follow = false;
@@ -478,28 +487,26 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
@Override
public void paint(Graphics gx) {
if (engine.isClosed()) {
if (engine.isClosed() && !updateEngine()) {
EventQueue.invokeLater(() -> {
try { setVisible(false); } catch (Throwable ignored) { }
});
return;
}
if (updateEngine()) {
dump();
}
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
oxp = lerp(oxp, ox, 0.36);
ozp = lerp(ozp, oz, 0.36);
hx = lerp(hx, lx, 0.36);
hz = lerp(hz, lz, 0.36);
if (centities.flip()) {
J.s(() -> {
if (centities.flip() && overlay != null) {
overlay.requestEntities((List<GuiMarker> next) -> {
synchronized (lastEntities) {
lastEntities.clear();
lastEntities.addAll(world.getEntitiesByClass(LivingEntity.class));
if (next != null) {
lastEntities.addAll(next);
}
}
});
}
@@ -588,8 +595,8 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
}
private void handleFollow() {
if (follow && player != null) {
animateTo(player.getLocation().getX(), player.getLocation().getZ());
if (follow && followMarker != null) {
animateTo(followMarker.worldX(), followMarker.worldZ());
}
}
@@ -635,28 +642,30 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
g.drawString(right, w - rw - 8, y + 17);
g.setColor(ACCENT);
int modeW = g.getFontMetrics().stringWidth(" " + modeName(currentType));
g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1);
}
private void renderEntities(Graphics2D g) {
Player b = null;
GuiMarker firstPlayer = null;
for (Player i : world.getPlayers()) {
b = i;
renderPlayerMarker(g, i.getLocation().getX(), i.getLocation().getZ(), i.getName());
List<GuiMarker> players = overlay == null ? List.of() : overlay.players();
if (players != null) {
for (GuiMarker i : players) {
firstPlayer = i;
renderPlayerMarker(g, i.worldX(), i.worldZ(), i.label());
}
}
synchronized (lastEntities) {
double dist = Double.MAX_VALUE;
LivingEntity nearest = null;
GuiMarker nearest = null;
for (LivingEntity i : lastEntities) {
if (i instanceof Player) continue;
renderMobMarker(g, i.getLocation().getX(), i.getLocation().getZ());
for (GuiMarker i : lastEntities) {
renderMobMarker(g, i.worldX(), i.worldZ());
if (shift) {
double d = i.getLocation().distanceSquared(
new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz)));
double dx = i.worldX() - getWorldX(hx);
double dz = i.worldZ() - getWorldZ(hz);
double d = dx * dx + dz * dz;
if (d < dist) {
dist = d;
nearest = i;
@@ -665,22 +674,24 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
}
if (nearest != null && shift) {
double sx = getScreenX(nearest.getLocation().getX());
double sz = getScreenZ(nearest.getLocation().getZ());
double sx = getScreenX(nearest.worldX());
double sz = getScreenZ(nearest.worldZ());
g.setColor(MOB_COLOR);
g.fillOval((int) sx - 6, (int) sz - 6, 12, 12);
g.setColor(new Color(220, 80, 80, 60));
g.fillOval((int) sx - 10, (int) sz - 10, 20, 20);
KList<String> k = new KList<>();
k.add(Form.capitalizeWords(nearest.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")));
k.add("Pos: " + nearest.getLocation().getBlockX() + ", " + nearest.getLocation().getBlockY() + ", " + nearest.getLocation().getBlockZ());
k.add("HP: " + Form.f(nearest.getHealth(), 1) + " / " + Form.f(nearest.getAttribute(MAX_HEALTH).getValue(), 1));
k.add(nearest.label());
k.add("Pos: " + (int) nearest.worldX() + ", " + (int) nearest.worldY() + ", " + (int) nearest.worldZ());
if (nearest.maxHealth() > 0) {
k.add("HP: " + Form.f(nearest.health(), 1) + " / " + Form.f(nearest.maxHealth(), 1));
}
drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k);
}
}
player = b;
followMarker = firstPlayer;
}
private void renderPlayerMarker(Graphics2D g, double x, double z, String name) {
@@ -884,32 +895,23 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
@Override public void mouseExited(MouseEvent e) { }
private void open() {
IrisComplex complex = engine.getComplex();
File r = null;
switch (currentType) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
if (overlay == null) {
return;
}
if (r != null) {
notify("Opened " + r.getName());
String opened = overlay.openInEditor(getWorldX(hx), getWorldZ(hz), currentType);
if (opened != null) {
notify("Opened " + opened);
}
}
private void teleport() {
J.s(() -> {
if (player != null) {
int xx = (int) getWorldX(hx);
int zz = (int) getWorldZ(hz);
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
notify("Teleported to " + xx + ", " + yy + ", " + zz);
} else {
notify("No player in world");
}
});
if (overlay == null) {
notify("No player in world");
return;
}
int xx = (int) getWorldX(hx);
int zz = (int) getWorldZ(hz);
overlay.teleport(xx, zz);
notify("Teleporting to " + xx + ", " + zz);
}
}
@@ -27,9 +27,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -182,10 +180,6 @@ public interface INMSBinding {
return false;
}
default boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
return false;
}
default boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
return false;
}
@@ -5,11 +5,10 @@ import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.EntityType;
import java.util.Locale;
@@ -23,7 +22,7 @@ public class DataFixerV1206 extends DataFixerV1192 {
json.remove("creature_spawn_probability");
}
var spawns = biome.getSpawns();
KList<IrisBiomeCustomSpawn> spawns = biome.getSpawns();
if (spawns != null && spawns.isNotEmpty()) {
JSONObject spawners = new JSONObject();
KMap<IrisBiomeCustomSpawnType, JSONArray> groups = new KMap<>();
@@ -32,20 +31,15 @@ public class DataFixerV1206 extends DataFixerV1192 {
if (i == null) {
continue;
}
EntityType type = i.getType();
if (type == null) {
String key = i.getTypeKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId());
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject();
NamespacedKey key = type.getKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + biome.getId());
continue;
}
o.put("type", key.toString());
o.put("type", key);
o.put("weight", i.getWeight());
o.put("minCount", i.getMinCount());
o.put("maxCount", i.getMaxCount());
@@ -18,13 +18,10 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.World;
public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
@@ -33,36 +30,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
public AsyncOrMedievalPregenMethod(World world, int threads) {
if (PaperLib.isPaper()) {
method = new AsyncPregenMethod(world, threads);
} else if (shouldUseSpigotDirect(world)) {
method = new SpigotDirectPregenMethod(world, threads);
} else {
method = new MedievalPregenMethod(world);
}
}
private static boolean shouldUseSpigotDirect(World world) {
if (!IrisSettings.get().getPregen().isEnableSpigotDirectPregen()) {
return false;
}
if (IrisToolbelt.isIrisStudioWorld(world)) {
return false;
}
if (!world.isAutoSave()) {
return false;
}
try {
String name = Bukkit.getServer().getName();
if (name != null) {
String lower = name.toLowerCase();
if (lower.contains("spigot") || lower.contains("craftbukkit")) {
return true;
}
}
} catch (Throwable ignored) {
}
return false;
}
@Override
public void init() {
method.init();
@@ -1,586 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.storage.AtomicHunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class SpigotDirectPregenMethod implements PregeneratorMethod {
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long PERMIT_WAIT_NOTIFY_MS = 500L;
private final World world;
private final int threads;
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final int maxResidentTectonicPlates;
private final int mantleBackpressureWaitMs;
private final int mantleBackpressureTimeoutMs;
private final Semaphore semaphore;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
private final AtomicLong submitted = new AtomicLong();
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final Object permitMonitor = new Object();
private final Set<Long> liveLoadedChunkKeys;
private final ConcurrentHashMap<Long, Boolean> chunksWrittenDirect;
private final AtomicBoolean writePathDisabled;
private volatile Engine cachedEngine;
private volatile Mantle cachedMantle;
private volatile NBTWorld nbtWorld;
private volatile MedievalPregenMethod fallback;
public SpigotDirectPregenMethod(World world, int threads) {
this.world = world;
int configured = Math.max(1, threads);
this.threads = Math.max(8, Math.min(32, configured));
this.semaphore = new Semaphore(this.threads, true);
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
this.liveLoadedChunkKeys = ConcurrentHashMap.newKeySet();
this.chunksWrittenDirect = new ConcurrentHashMap<>();
this.writePathDisabled = new AtomicBoolean(false);
}
@Override
public void init() {
try {
this.nbtWorld = new NBTWorld(world.getWorldFolder());
} catch (Throwable e) {
IrisLogging.error("SpigotDirect pregen could not open NBTWorld for " + world.getName() + "; disabling direct write path.");
IrisLogging.reportError(e);
writePathDisabled.set(true);
}
snapshotLoadedChunks();
IrisLogging.info("SpigotDirect pregen init: world=" + world.getName()
+ ", threads=" + threads
+ ", adaptiveLimit=" + adaptiveInFlightLimit.get()
+ ", initialLoadedChunks=" + liveLoadedChunkKeys.size()
+ ", maxResidentTectonicPlates=" + maxResidentTectonicPlates
+ ", timeout=" + timeoutSeconds + "s");
}
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
if (nbtWorld != null) {
try {
nbtWorld.flushNow();
nbtWorld.close();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
evictWrittenChunksFromServer();
if (fallback != null) {
try {
fallback.close();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
@Override
public void save() {
if (nbtWorld != null) {
try {
nbtWorld.save();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
if (fallback != null) {
try {
fallback.save();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public String getMethod(int x, int z) {
return "SpigotDirect";
}
@Override
public Mantle getMantle() {
if (IrisToolbelt.isIrisWorld(world)) {
return IrisToolbelt.access(world).getEngine().getMantle().getMantle();
}
return null;
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
enforceMantleBudget();
long key = chunkKey(x, z);
if (writePathDisabled.get() || nbtWorld == null || liveLoadedChunkKeys.contains(key) || world.isChunkLoaded(x, z)) {
ensureFallback().generateChunk(x, z, listener);
return;
}
Engine engine = resolveEngine();
if (engine == null || !engine.getDimension().isUseMantle()) {
ensureFallback().generateChunk(x, z, listener);
return;
}
try {
synchronized (permitMonitor) {
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
permitMonitor.wait(PERMIT_WAIT_NOTIFY_MS);
}
}
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
markSubmitted();
MultiBurst.burst.lazy(() -> runDirect(engine, x, z, listener));
}
private void runDirect(Engine engine, int x, int z, PregenListener listener) {
boolean success = false;
try {
int worldMinY = world.getMinHeight();
int worldMaxY = world.getMaxHeight();
int height = worldMaxY - worldMinY;
PlatformBlockState air = IrisPlatforms.get().registries().air();
Hunk<PlatformBlockState> blocks = new AirDefaultAtomicHunk(16, height, 16, air);
Hunk<PlatformBiome> biomes = Hunk.newSynchronizedArrayHunk(16, height, 16);
try {
engine.generate(x << 4, z << 4, blocks, biomes, false);
} catch (Throwable e) {
handleFailure(x, z, e);
return;
}
boolean wrote;
try {
wrote = INMS.get().writeChunkNbtDirect(nbtWorld, x, z, blocks, biomes);
} catch (Throwable e) {
handleFailure(x, z, e);
return;
}
if (!wrote) {
if (writePathDisabled.compareAndSet(false, true)) {
IrisLogging.warn("SpigotDirect NBT write returned false at chunk " + x + "," + z
+ "; disabling direct path. Subsequent chunks will route through MedievalPregenMethod on the iterator thread.");
}
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
success = true;
return;
}
chunksWrittenDirect.put(chunkKey(x, z), Boolean.TRUE);
try {
cleanupMantleChunk(x, z);
} catch (Throwable ignored) {
}
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
success = true;
} catch (Throwable e) {
handleFailure(x, z, e);
} finally {
markFinished(success);
semaphore.release();
}
}
private void handleFailure(int x, int z, Throwable error) {
IrisLogging.warn("SpigotDirect pregen failure at chunk " + x + "," + z + ". " + metricsSnapshot());
IrisLogging.reportError(error);
if (error != null) {
error.printStackTrace(System.err);
}
if (writePathDisabled.compareAndSet(false, true)) {
IrisLogging.warn("SpigotDirect: disabling direct write path after first failure. Subsequent chunks route through MedievalPregenMethod on the iterator thread.");
}
onTimeout(x, z);
}
private MedievalPregenMethod ensureFallback() {
MedievalPregenMethod existing = fallback;
if (existing != null) {
return existing;
}
synchronized (this) {
existing = fallback;
if (existing == null) {
existing = new MedievalPregenMethod(world);
existing.init();
fallback = existing;
}
}
return existing;
}
private void snapshotLoadedChunks() {
try {
Set<Long> loaded = J.sfut(() -> {
Set<Long> keys = new HashSet<>();
if (world == null) {
return keys;
}
for (Chunk loadedChunk : world.getLoadedChunks()) {
keys.add(chunkKey(loadedChunk.getX(), loadedChunk.getZ()));
}
return keys;
}).get();
if (loaded != null) {
liveLoadedChunkKeys.addAll(loaded);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void evictWrittenChunksFromServer() {
if (chunksWrittenDirect.isEmpty()) {
return;
}
try {
J.sfut(() -> {
int evicted = 0;
for (Long key : chunksWrittenDirect.keySet()) {
int x = keyX(key);
int z = keyZ(key);
if (!world.isChunkLoaded(x, z)) {
continue;
}
if (INMS.get().forceEvictChunk(world, x, z)) {
evicted++;
}
}
if (evicted > 0) {
IrisLogging.info("SpigotDirect: force-evicted " + evicted + " chunks loaded mid-pregen so the server reloads them from disk.");
}
}).get();
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) | (z & 0xffffffffL);
}
private static int keyX(long key) {
return (int) (key >> 32);
}
private static int keyZ(long key) {
return (int) (key & 0xffffffffL);
}
private void onTimeout(int x, int z) {
int streak = timeoutStreak.incrementAndGet();
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
lowerAdaptiveInFlightLimit();
}
long now = M.ms();
long last = lastTimeoutLogAt.get();
if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) {
suppressedTimeoutLogs.incrementAndGet();
return;
}
int suppressed = suppressedTimeoutLogs.getAndSet(0);
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
IrisLogging.warn("SpigotDirect pregen failure cluster at " + x + "," + z
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ suppressedText + " " + metricsSnapshot());
}
private void onSuccess() {
int streak = timeoutStreak.get();
if (streak > 0) {
int newStreak = Math.max(0, streak - 2);
timeoutStreak.compareAndSet(streak, newStreak);
if (newStreak > 0) {
return;
}
}
if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) {
raiseAdaptiveInFlightLimit();
}
}
private void lowerAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current <= adaptiveMinInFlightLimit) {
return;
}
int next = Math.max(adaptiveMinInFlightLimit, current - 1);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("decrease", next);
notifyPermitWaiters();
return;
}
}
}
private void raiseAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current >= threads) {
return;
}
int deficit = threads - current;
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
int next = Math.min(threads, current + step);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("increase", next);
notifyPermitWaiters();
return;
}
}
}
private void logAdaptiveLimit(String mode, int value) {
long now = M.ms();
long last = lastAdaptiveLogAt.get();
if (now - last < 5000L) {
return;
}
if (lastAdaptiveLogAt.compareAndSet(last, now)) {
IrisLogging.info("SpigotDirect pregen adaptive limit " + mode + " -> " + value + " (" + metricsSnapshot() + ")");
}
}
private String metricsSnapshot() {
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
return "world=" + world.getName()
+ " permits=" + semaphore.availablePermits() + "/" + threads
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ " inFlight=" + inFlight.get()
+ " submitted=" + submitted.get()
+ " completed=" + completed.get()
+ " failed=" + failed.get()
+ " stalledForMs=" + stalledFor;
}
private void markSubmitted() {
submitted.incrementAndGet();
inFlight.incrementAndGet();
}
private void markFinished(boolean success) {
if (success) {
completed.incrementAndGet();
onSuccess();
} else {
failed.incrementAndGet();
}
lastProgressAt.set(M.ms());
int after = inFlight.decrementAndGet();
if (after < 0) {
inFlight.compareAndSet(after, 0);
}
notifyPermitWaiters();
}
private void notifyPermitWaiters() {
synchronized (permitMonitor) {
permitMonitor.notifyAll();
}
}
private void cleanupMantleChunk(int x, int z) {
Engine engine = resolveEngine();
if (engine != null) {
try {
engine.getMantle().forceCleanupChunk(x, z);
} catch (Throwable ignored) {
}
}
}
private Engine resolveEngine() {
Engine cached = cachedEngine;
if (cached != null) {
return cached;
}
if (!IrisToolbelt.isIrisWorld(world)) {
return null;
}
try {
Engine resolved = IrisToolbelt.access(world).getEngine();
if (resolved != null) {
cachedEngine = resolved;
}
return resolved;
} catch (Throwable ignored) {
return null;
}
}
private Mantle resolveMantle() {
Mantle cached = cachedMantle;
if (cached != null) {
return cached;
}
Mantle resolved = getMantle();
if (resolved != null) {
cachedMantle = resolved;
}
return resolved;
}
private void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
mantle.trim(0L, 0);
int freed = mantle.unloadTectonicPlate(0);
int resident = mantle.getLoadedRegionCount();
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= mantleBackpressureTimeoutMs) {
IrisLogging.warn("SpigotDirect mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("SpigotDirect mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(mantleBackpressureWaitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private static final class AirDefaultAtomicHunk extends AtomicHunk<PlatformBlockState> {
private final PlatformBlockState airDefault;
AirDefaultAtomicHunk(int w, int h, int d, PlatformBlockState airDefault) {
super(w, h, d);
this.airDefault = airDefault;
}
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
PlatformBlockState v = super.getRaw(x, y, z);
return v != null ? v : airDefault;
}
}
}
@@ -29,8 +29,6 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.EntityType;
import java.awt.*;
import java.util.Locale;
@@ -147,20 +145,15 @@ public class IrisBiomeCustom {
if (i == null) {
continue;
}
EntityType type = i.getType();
if (type == null) {
String key = i.getTypeKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId());
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject();
NamespacedKey key = type.getKey();
if (key == null) {
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + getId());
continue;
}
o.put("type", key.toString());
o.put("type", key);
o.put("weight", i.getWeight());
o.put("minCount", i.getMinCount());
o.put("maxCount", i.getMaxCount());
@@ -50,6 +50,13 @@ public class IrisBiomeCustomSpawn {
});
}
public String getTypeKey() {
if (type == null || type.isBlank()) {
return null;
}
return type.contains(":") ? type : "minecraft:" + type;
}
@MinNumber(1)
@Desc("The min to spawn")
private int minCount = 2;
@@ -38,11 +38,14 @@ import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
@@ -291,6 +294,35 @@ public final class BukkitPlatform implements IrisPlatform {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command);
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
if (!(world instanceof World bukkitWorld) || entityKey == null) {
return false;
}
NamespacedKey namespacedKey = NamespacedKey.fromString(entityKey);
if (namespacedKey == null) {
return false;
}
EntityType type = Registry.ENTITY_TYPE.get(namespacedKey);
if (type == null) {
return false;
}
Entity entity = spawnEntity(new Location(bukkitWorld, x, y, z), type, CreatureSpawnEvent.SpawnReason.COMMAND);
return entity != null;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
if (!(player instanceof Player bukkitPlayer) || itemKey == null || amount <= 0) {
return false;
}
Material material = Material.matchMaterial(itemKey);
if (material == null) {
return false;
}
return bukkitPlayer.getInventory().addItem(new ItemStack(material, amount)).isEmpty();
}
@Override
public void log(LogLevel level, String message) {
bridge().logSink().accept(level, message);
@@ -8,8 +8,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void paperLikeRecommendedCapTracksWorkerThreads() {
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(1));
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
assertEquals(48, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
assertEquals(32, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
assertEquals(96, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(80));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(128));
}
@@ -17,8 +17,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void foliaRecommendedCapTracksWorkerThreads() {
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(1));
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(12));
assertEquals(80, AsyncPregenMethod.computeFoliaRecommendedCap(20));
assertEquals(96, AsyncPregenMethod.computeFoliaRecommendedCap(12));
assertEquals(160, AsyncPregenMethod.computeFoliaRecommendedCap(20));
assertEquals(192, AsyncPregenMethod.computeFoliaRecommendedCap(80));
}
@@ -427,6 +427,16 @@ public final class StubPlatform implements IrisPlatform {
public void dispatchConsoleCommand(String command) {
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
return false;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
return false;
}
@Override
public void log(LogLevel level, String message) {
if (VERBOSE) {
@@ -70,6 +70,10 @@ public interface IrisPlatform {
void dispatchConsoleCommand(String command);
boolean spawnEntity(Object world, String entityKey, double x, double y, double z);
boolean giveItem(Object player, String itemKey, int amount);
void log(LogLevel level, String message);
void msg(String message);