This commit is contained in:
Brian Neumann-Fopiano
2026-07-09 13:54:59 -04:00
parent d1fec88769
commit 0a7a531646
259 changed files with 11055 additions and 884 deletions
@@ -84,7 +84,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static final ExecutorService GEN_POOL = createGenPool();
private static volatile ExecutorService genPool = createGenPool();
public static void startGenPool() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
}
public static void shutdownGenPool() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
@@ -269,7 +283,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState),
GEN_POOL);
genPool);
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
@@ -294,6 +308,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
writeBlocks(chunk, blocks, dimMinY, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
return chunk;
}
@@ -21,6 +21,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 art.arcane.volmlib.util.data.UnresolvedKeyLog;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
@@ -83,7 +84,7 @@ public final class ModdedBlockResolution {
private static final Set<Block> PLACE_ONTO_LEAVES = blockSet(
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static long lastWarnMs;
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
private ModdedBlockResolution() {
}
@@ -150,13 +151,14 @@ public final class ModdedBlockResolution {
return map;
}
private static boolean shouldWarn() {
long now = System.currentTimeMillis();
if (now - lastWarnMs >= 1000) {
lastWarnMs = now;
return true;
private static void warnUnresolved(String key, String message) {
if (UNRESOLVED.firstOccurrence(key)) {
IrisLogging.warn(message);
}
String summary = UNRESOLVED.pollSummary();
if (summary != null) {
IrisLogging.warn(summary);
}
return false;
}
public static BlockState airState() {
@@ -192,7 +194,7 @@ public final class ModdedBlockResolution {
return parsed;
}
IrisLogging.error("Can't find block data for " + bdxf);
return resolveNoCompat("STONE");
return new Parsed(AIR, null);
}
static Parsed resolveNoCompat(String bdxf) {
@@ -222,8 +224,8 @@ public final class ModdedBlockResolution {
if (provided != null) {
return new Parsed(provided, null);
}
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data '" + bd + "'");
if (warn) {
warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
}
return new Parsed(AIR, null);
}
@@ -231,8 +233,8 @@ public final class ModdedBlockResolution {
return bdx;
} catch (Throwable e) {
e.printStackTrace();
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data '" + bdxf + "'");
if (warn) {
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
}
}
@@ -297,8 +299,8 @@ public final class ModdedBlockResolution {
}
if (bx == null) {
if (warn && shouldWarn()) {
IrisLogging.warn("Unknown Block Data: " + ix);
if (warn) {
warnUnresolved(ix, "Unknown Block Data: " + ix);
}
return null;
}
@@ -0,0 +1,130 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.ChunkGenerator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedDeathLoot {
private static final String TAG_PREFIX = "iris_loot|";
private static final int LOOT_RNG_SALT = 345911;
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
private ModdedDeathLoot() {
}
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
if (entity == null || tableKeys == null || tableKeys.isEmpty()) {
return;
}
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys));
}
public static void handle(LivingEntity entity) {
if (entity == null || !(entity.level() instanceof ServerLevel level)) {
return;
}
String encoded = findTag(entity);
if (encoded == null) {
return;
}
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
if (parts.length < 5) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return;
}
long seed;
int spawnX;
int spawnY;
int spawnZ;
try {
seed = Long.parseLong(parts[0]);
spawnX = Integer.parseInt(parts[1]);
spawnY = Integer.parseInt(parts[2]);
spawnZ = Integer.parseInt(parts[3]);
} catch (NumberFormatException error) {
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
return;
}
Engine engine = engineFor(level);
if (engine == null) {
return;
}
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT);
KList<ItemStack> drops = new KList<>();
for (String key : parts[4].split(",")) {
String trimmed = key.trim();
if (trimmed.isEmpty()) {
continue;
}
IrisLootTable table = engine.getData().getLootLoader().load(trimmed);
if (table == null) {
if (WARNED_TABLES.add(trimmed)) {
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'");
}
continue;
}
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ));
}
emit(level, entity, drops);
}
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
double x = entity.getX();
double y = entity.getY() + 0.5D;
double z = entity.getZ();
for (ItemStack stack : drops) {
if (stack == null || stack.isEmpty()) {
continue;
}
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack);
itemEntity.setDefaultPickUpDelay();
level.addFreshEntity(itemEntity);
}
}
private static String findTag(LivingEntity entity) {
for (String tag : entity.entityTags()) {
if (tag.startsWith(TAG_PREFIX)) {
return tag;
}
}
return null;
}
private static Engine engineFor(ServerLevel level) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (generator instanceof IrisModdedChunkGenerator irisGenerator) {
return irisGenerator.engineIfBound();
}
return null;
}
}
@@ -21,7 +21,6 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
@@ -36,6 +35,7 @@ import art.arcane.iris.modded.command.ModdedStudioCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
@@ -43,10 +43,6 @@ import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -81,6 +77,7 @@ public final class ModdedEngineBootstrap {
ModdedPrimaryWorldRouter.tick(server);
SERVICE_MANAGER.tick(server);
ModdedPregenBossBar.tick(server);
ModdedProtocolHandler.tickDimensionSync(server);
}
public static void start(MinecraftServer server) {
@@ -91,10 +88,14 @@ public final class ModdedEngineBootstrap {
if (scheduler != null) {
scheduler.reset();
}
IrisModdedChunkGenerator.startGenPool();
SERVICE_MANAGER.enableAll();
ModdedProtocolHandler.start(server);
ModdedSentry.start(loader());
}
public static void stop() {
ModdedProtocolHandler.stop();
ModdedPregenJob.shutdown();
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
@@ -105,8 +106,10 @@ public final class ModdedEngineBootstrap {
ModdedDimensionManager.clear();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
scheduler.shutdown();
}
IrisModdedChunkGenerator.shutdownGenPool();
ModdedSentry.flush();
ModdedStartup.reset();
currentServer = null;
}
@@ -197,8 +200,9 @@ public final class ModdedEngineBootstrap {
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
@@ -209,53 +213,4 @@ public final class ModdedEngineBootstrap {
return created;
}
}
private static final class InertWorldManager implements EngineWorldManager {
@Override
public void close() {
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent e) {
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
}
@Override
public void onChunkLoad(Chunk e, boolean generated) {
}
@Override
public void onChunkUnload(Chunk e) {
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
}
}
}
@@ -0,0 +1,337 @@
/*
* 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.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisCommand;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.AgeableMob;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.animal.panda.Panda;
import net.minecraft.world.entity.monster.zombie.Zombie;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.item.ItemStack;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedEntitySpawner {
private static final int PASSENGER_RNG_BASE = 234858;
private static final int LEASH_RNG_SEED = 234548;
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
private static final Set<String> WARNED_TYPES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
private static boolean warnedSpawnEffect = false;
private ModdedEntitySpawner() {
}
public static Entity spawn(Engine engine, IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
if (engine == null || irisEntity == null || level == null) {
return null;
}
double x = blockX + 0.5;
double y = blockY + 0.5;
double z = blockZ + 0.5;
Entity created = create(irisEntity, level, x, y, z);
if (created == null) {
return null;
}
if (irisEntity.isSpecialType() && !irisEntity.isApplySettingsToCustomMobAnyways()) {
return created;
}
applyConfig(engine, irisEntity, created, level, blockX, blockY, blockZ, rng);
return created;
}
private static Entity create(IrisEntity irisEntity, ServerLevel level, double x, double y, double z) {
if (irisEntity.isSpecialType()) {
return ModdedCustomContentRegistry.spawnMob(level, x, y, z, irisEntity.getSpecialType());
}
EntityType<?> type = resolveType(irisEntity.getType());
if (type == null) {
return null;
}
return type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
}
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
String customName = irisEntity.getCustomName();
if (customName != null && !customName.isBlank()) {
entity.setCustomName(Component.literal(colorize(customName)));
}
entity.setCustomNameVisible(irisEntity.isCustomNameVisible());
entity.setGlowingTag(irisEntity.isGlowing());
entity.setNoGravity(!irisEntity.isGravity());
entity.setInvulnerable(irisEntity.isInvulnerable());
entity.setSilent(irisEntity.isSilent());
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
if (persistent && entity instanceof Mob persistentMob) {
persistentMob.setPersistenceRequired();
}
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
if (entity instanceof LivingEntity living) {
applyAttributes(irisEntity, living, level, rng);
}
if (entity instanceof LivingEntity lootHolder && !irisEntity.getLoot().getTables().isEmpty()) {
ModdedDeathLoot.tag(lootHolder, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng);
}
if (entity instanceof Mob mob) {
mob.setNoAi(!irisEntity.isAi());
mob.setCanPickUpLoot(irisEntity.isPickupItems());
if (!irisEntity.isRemovable()) {
mob.setPersistenceRequired();
}
if (irisEntity.getLeashHolder() != null) {
Entity holder = spawn(engine, irisEntity.getLeashHolder(), level, blockX, blockY, blockZ, rng.nextParallelRNG(LEASH_RNG_SEED));
if (holder != null) {
mob.setLeashedTo(holder, true);
}
}
}
if (entity instanceof LivingEntity equippable) {
applyEquipment(irisEntity, equippable, level, rng);
}
if (irisEntity.isBaby()) {
applyBaby(entity);
}
if (entity instanceof Panda panda) {
panda.setMainGene(gene(irisEntity.getPandaMainGene()));
panda.setHiddenGene(gene(irisEntity.getPandaHiddenGene()));
}
if (entity instanceof Villager villager) {
villager.setPersistenceRequired();
}
if (irisEntity.getSpawnEffect() != null || irisEntity.isSpawnEffectRiseOutOfGround()) {
noteSpawnEffectGap();
}
applyRawCommands(irisEntity, level, blockX, blockY, blockZ);
}
private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
int index = 0;
for (IrisEntity passengerEntity : irisEntity.getPassengers()) {
Entity passenger = spawn(engine, passengerEntity, level, blockX, blockY, blockZ, rng.nextParallelRNG(PASSENGER_RNG_BASE + index++));
if (passenger != null) {
passenger.startRiding(entity);
}
}
}
private static void applyAttributes(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) {
KList<IrisAttributeModifier> modifiers = irisEntity.getAttributes();
if (modifiers.isEmpty()) {
return;
}
Registry<Attribute> registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE);
int index = 0;
for (IrisAttributeModifier modifier : modifiers) {
index++;
if (rng.nextDouble() >= modifier.getChance()) {
continue;
}
Holder<Attribute> holder = ModdedItemTranslator.resolveAttribute(registry, modifier.getAttribute());
if (holder == null) {
if (WARNED_ATTRIBUTES.add(modifier.getAttribute())) {
IrisLogging.warn("Iris entity: unknown attribute '" + modifier.getAttribute() + "'");
}
continue;
}
AttributeInstance instance = living.getAttributes().getInstance(holder);
if (instance == null) {
continue;
}
Identifier id = Identifier.tryParse("iris:" + normalizeName(modifier.getName()) + "_" + index);
if (id == null) {
continue;
}
instance.addOrReplacePermanentModifier(new AttributeModifier(id, modifier.getAmount(rng), ModdedItemTranslator.operationFor(modifier.getOperation())));
}
}
private static void applyEquipment(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) {
setSlot(living, EquipmentSlot.HEAD, irisEntity.getHelmet(), level, rng);
setSlot(living, EquipmentSlot.CHEST, irisEntity.getChestplate(), level, rng);
setSlot(living, EquipmentSlot.LEGS, irisEntity.getLeggings(), level, rng);
setSlot(living, EquipmentSlot.FEET, irisEntity.getBoots(), level, rng);
setSlot(living, EquipmentSlot.MAINHAND, irisEntity.getMainHand(), level, rng);
setSlot(living, EquipmentSlot.OFFHAND, irisEntity.getOffHand(), level, rng);
}
private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) {
if (loot == null || rng.i(1, loot.getRarity()) != 1) {
return;
}
ItemStack stack = ModdedItemTranslator.stack(loot, rng, level);
if (stack != null && !stack.isEmpty()) {
living.setItemSlot(slot, stack);
}
}
private static void applyBaby(Entity entity) {
if (entity instanceof AgeableMob ageable) {
ageable.setBaby(true);
return;
}
if (entity instanceof Zombie zombie) {
zombie.setBaby(true);
}
}
private static void applyRawCommands(IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ) {
KList<IrisCommand> rawCommands = irisEntity.getRawCommands();
if (rawCommands.isEmpty()) {
return;
}
MinecraftServer server = level.getServer();
if (server == null) {
return;
}
for (IrisCommand command : rawCommands) {
for (String raw : command.getCommands()) {
if (raw == null || raw.isBlank()) {
continue;
}
String prepared = (raw.startsWith("/") ? raw.substring(1) : raw)
.replace("{x}", String.valueOf(blockX))
.replace("{y}", String.valueOf(blockY))
.replace("{z}", String.valueOf(blockZ));
ModdedServerCommands.dispatch(server, prepared);
}
}
}
private static EntityType<?> resolveType(String key) {
if (key == null || key.isBlank()) {
return null;
}
String normalized = key.trim().toLowerCase(Locale.ROOT);
Identifier id = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) {
if (WARNED_TYPES.add(normalized)) {
IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn");
}
return null;
}
return BuiltInRegistries.ENTITY_TYPE.getValue(id);
}
private static EntitySpawnReason reasonFor(String reason) {
if (reason == null || reason.isBlank()) {
return EntitySpawnReason.NATURAL;
}
String value = reason.trim().toUpperCase(Locale.ROOT);
String mapped = switch (value) {
case "CHUNK_GEN" -> "CHUNK_GENERATION";
case "SPAWNER_EGG" -> "SPAWN_EGG";
case "DISPENSE_EGG" -> "DISPENSER";
case "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_WITHER", "CUSTOM", "DEFAULT" -> "MOB_SUMMONED";
default -> value;
};
try {
return EntitySpawnReason.valueOf(mapped);
} catch (IllegalArgumentException ignored) {
return EntitySpawnReason.NATURAL;
}
}
private static Panda.Gene gene(String value) {
if (value == null || value.isBlank()) {
return Panda.Gene.NORMAL;
}
try {
return Panda.Gene.valueOf(value.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ignored) {
return Panda.Gene.NORMAL;
}
}
private static boolean forcePersist() {
return IrisSettings.get().getWorld().isForcePersistEntities();
}
private static void noteSpawnEffectGap() {
if (!warnedSpawnEffect) {
warnedSpawnEffect = true;
IrisLogging.debug("Iris entity spawnEffect / spawnEffectRiseOutOfGround are Bukkit-only visual paths and are skipped on modded.");
}
}
private static String normalizeName(String name) {
String source = name == null || name.isBlank() ? "modifier" : name;
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
return normalized.isBlank() ? "modifier" : normalized;
}
private static String colorize(String text) {
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) {
chars[i] = '§';
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
}
}
return new String(chars);
}
}
@@ -0,0 +1,43 @@
/*
* 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.spi.protocol.IrisProtocol;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
public record ModdedIrisPayload(byte[] data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<ModdedIrisPayload> TYPE = new CustomPacketPayload.Type<>(Identifier.parse(IrisProtocol.CHANNEL));
public static final StreamCodec<RegistryFriendlyByteBuf, ModdedIrisPayload> STREAM_CODEC = CustomPacketPayload.codec(ModdedIrisPayload::write, ModdedIrisPayload::read);
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
private static ModdedIrisPayload read(RegistryFriendlyByteBuf buffer) {
return new ModdedIrisPayload(buffer.readByteArray(IrisProtocol.MAX_FRAME_BYTES));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeByteArray(data);
}
}
@@ -43,13 +43,19 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Unit;
import net.minecraft.world.entity.EquipmentSlotGroup;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.CustomModelData;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.item.component.ItemAttributeModifiers;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.ItemEnchantments;
@@ -96,6 +102,21 @@ public final class ModdedItemTranslator {
return out;
}
public static ItemStack stack(IrisLoot loot, RNG rng, ServerLevel level) {
try {
ItemStack stack = baseStack(loot, rng);
if (stack == null || stack.isEmpty()) {
return null;
}
applyComponents(loot, stack, rng, level);
applyCustomNbt(stack, loot.getCustomNbt());
return stack;
} catch (Throwable e) {
IrisLogging.reportError(e);
return null;
}
}
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
return null;
@@ -134,14 +155,14 @@ public final class ModdedItemTranslator {
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
applyEnchantments(loot, stack, rng, level);
consumeAttributeDraws(loot, rng);
applyAttributes(loot, stack, rng, level);
if (loot.isUnbreakable()) {
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
}
if (loot.getItemFlags().isNotEmpty()) {
warnOnce("itemFlags", "Iris loot: itemFlags are not supported on modded; skipping flags");
applyItemFlags(loot, stack);
}
if (loot.getCustomModel() != null) {
@@ -165,7 +186,7 @@ public final class ModdedItemTranslator {
String dye = readString(DYE_COLOR_FIELD, loot);
if (dye != null) {
warnOnce("dyeColor", "Iris loot: dyeColor is not supported on modded; skipping");
applyDyeColor(stack, dye);
}
if (loot.getDisplayName() != null) {
@@ -208,16 +229,128 @@ public final class ModdedItemTranslator {
}
}
private static void consumeAttributeDraws(IrisLoot loot, RNG rng) {
private static void applyAttributes(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
if (loot.getAttributes().isEmpty()) {
return;
}
warnOnce("attributes", "Iris loot: attribute modifiers are not supported on modded; skipping");
Registry<Attribute> registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE);
ItemAttributeModifiers.Builder builder = ItemAttributeModifiers.builder();
boolean changed = false;
for (IrisAttributeModifier attribute : loot.getAttributes()) {
if (rng.nextDouble() < attribute.getChance()) {
attribute.getAmount(rng);
if (rng.nextDouble() >= attribute.getChance()) {
continue;
}
double amount = attribute.getAmount(rng);
Holder<Attribute> holder = resolveAttribute(registry, attribute.getAttribute());
if (holder == null) {
warnOnce("attribute:" + attribute.getAttribute(), "Iris loot: unknown attribute '" + attribute.getAttribute() + "'");
continue;
}
Identifier modifierId = attributeModifierId(attribute.getName());
if (modifierId == null) {
continue;
}
AttributeModifier modifier = new AttributeModifier(modifierId, amount, operationFor(attribute.getOperation()));
builder.add(holder, modifier, EquipmentSlotGroup.ANY);
changed = true;
}
if (changed) {
stack.set(DataComponents.ATTRIBUTE_MODIFIERS, builder.build());
}
}
public static Holder<Attribute> resolveAttribute(Registry<Attribute> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
String value = key.trim().toLowerCase(Locale.ROOT);
Holder<Attribute> direct = lookupAttribute(registry, Identifier.tryParse(value.indexOf(':') >= 0 ? value : "minecraft:" + value));
if (direct != null) {
return direct;
}
if (value.startsWith("generic_")) {
return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic_".length())));
}
if (value.startsWith("generic.")) {
return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic.".length())));
}
return null;
}
private static Holder<Attribute> lookupAttribute(Registry<Attribute> registry, Identifier id) {
if (id == null) {
return null;
}
Optional<Holder.Reference<Attribute>> holder = registry.get(id);
return holder.isPresent() ? holder.get() : null;
}
public static AttributeModifier.Operation operationFor(String operation) {
if (operation == null || operation.isBlank()) {
return AttributeModifier.Operation.ADD_VALUE;
}
return switch (operation.trim().toUpperCase(Locale.ROOT)) {
case "ADD_SCALAR", "ADD_MULTIPLIED_BASE" -> AttributeModifier.Operation.ADD_MULTIPLIED_BASE;
case "MULTIPLY_SCALAR_1", "ADD_MULTIPLIED_TOTAL" -> AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL;
default -> AttributeModifier.Operation.ADD_VALUE;
};
}
private static Identifier attributeModifierId(String name) {
String source = name == null || name.isBlank() ? "modifier" : name;
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
if (normalized.isBlank()) {
normalized = "modifier";
}
return Identifier.tryParse("iris:" + normalized);
}
private static void applyItemFlags(IrisLoot loot, ItemStack stack) {
TooltipDisplay display = stack.getOrDefault(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.DEFAULT);
boolean changed = false;
for (String flag : loot.getItemFlags()) {
if (flag == null || flag.isBlank()) {
continue;
}
DataComponentType<?> hidden = tooltipComponentFor(flag.trim().toUpperCase(Locale.ROOT));
if (hidden == null) {
warnOnce("itemFlag:" + flag, "Iris loot: item flag '" + flag + "' has no modded tooltip equivalent; skipping");
continue;
}
display = display.withHidden(hidden, true);
changed = true;
}
if (changed) {
stack.set(DataComponents.TOOLTIP_DISPLAY, display);
}
}
private static DataComponentType<?> tooltipComponentFor(String flag) {
return switch (flag) {
case "HIDE_ENCHANTS" -> DataComponents.ENCHANTMENTS;
case "HIDE_STORED_ENCHANTS" -> DataComponents.STORED_ENCHANTMENTS;
case "HIDE_ATTRIBUTES" -> DataComponents.ATTRIBUTE_MODIFIERS;
case "HIDE_UNBREAKABLE" -> DataComponents.UNBREAKABLE;
case "HIDE_DESTROYS" -> DataComponents.CAN_BREAK;
case "HIDE_PLACED_ON" -> DataComponents.CAN_PLACE_ON;
case "HIDE_DYE" -> DataComponents.DYED_COLOR;
case "HIDE_ARMOR_TRIM" -> DataComponents.TRIM;
default -> null;
};
}
private static void applyDyeColor(ItemStack stack, String dye) {
DyeColor color = DyeColor.byName(dye.trim().toLowerCase(Locale.ROOT), null);
if (color == null) {
warnOnce("dyeColor:" + dye, "Iris loot: unknown dyeColor '" + dye + "'");
return;
}
stack.set(DataComponents.BASE_COLOR, color);
}
private static void applyLore(IrisLoot loot, ItemStack stack) {
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiomeWriter;
@@ -37,6 +38,7 @@ import java.util.function.Consumer;
public final class ModdedPlatform implements IrisPlatform {
private static volatile Consumer<Throwable> ERROR_SINK = null;
private static volatile Consumer<Throwable> CAPTURE_SINK = null;
private final ModdedLoader loader;
private final ModdedRegistries registries;
@@ -56,6 +58,10 @@ public final class ModdedPlatform implements IrisPlatform {
ERROR_SINK = sink;
}
public static void captureSink(Consumer<Throwable> sink) {
CAPTURE_SINK = sink;
}
public MinecraftServer server() {
return loader.currentServer();
}
@@ -116,12 +122,13 @@ public final class ModdedPlatform implements IrisPlatform {
@Override
public int irisVersionNumber() {
return 0;
return parseVersion(loader.modVersion());
}
@Override
public int minecraftVersionNumber() {
return 0;
int fromLoader = parseVersion(loader.minecraftVersion());
return fromLoader > 0 ? fromLoader : parseVersion(BuildConstants.MINECRAFT_VERSION);
}
@Override
@@ -130,6 +137,7 @@ public final class ModdedPlatform implements IrisPlatform {
@Override
public void dispatchConsoleCommand(String command) {
ModdedServerCommands.dispatch(loader.currentServer(), command);
}
@Override
@@ -159,13 +167,48 @@ public final class ModdedPlatform implements IrisPlatform {
@Override
public void reportError(Throwable error) {
if (error == null) {
return;
}
Consumer<Throwable> sink = ERROR_SINK;
if (sink != null && error != null) {
if (sink != null) {
sink.accept(error);
return;
}
if (error != null) {
ModdedIrisLog.error("Iris reported error", error);
ModdedIrisLog.error("Iris reported error", error);
Consumer<Throwable> capture = CAPTURE_SINK;
if (capture != null) {
try {
capture.accept(error);
} catch (Throwable captureFailure) {
ModdedIrisLog.error("Iris error-reporting sink failed", captureFailure);
}
}
}
private static int parseVersion(String raw) {
if (raw == null || raw.isBlank()) {
return -1;
}
int hyphen = raw.indexOf('-');
String head = hyphen >= 0 ? raw.substring(0, hyphen) : raw;
StringBuilder digits = new StringBuilder(head.length());
for (int i = 0; i < head.length(); i++) {
char ch = head.charAt(i);
if (ch >= '0' && ch <= '9') {
digits.append(ch);
} else if (ch != '.') {
break;
}
}
if (digits.isEmpty()) {
return -1;
}
try {
long value = Long.parseLong(digits.toString());
return value > Integer.MAX_VALUE ? -1 : (int) value;
} catch (NumberFormatException error) {
return -1;
}
}
}
@@ -0,0 +1,27 @@
/*
* 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.level.ServerPlayer;
public interface ModdedProtocolChannel {
boolean canReceive(ServerPlayer player);
void send(ServerPlayer player, ModdedIrisPayload payload);
}
@@ -0,0 +1,193 @@
/*
* 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.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisSession;
import art.arcane.iris.core.protocol.IrisSessionRegistry;
import art.arcane.iris.core.protocol.IrisVisionRequestService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedProtocolHandler {
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
private static final int DIMENSION_SYNC_INTERVAL_TICKS = 5;
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final ConcurrentHashMap<String, Engine> SESSION_ENGINES = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, String> SESSION_LEVELS = new ConcurrentHashMap<>();
private static volatile ModdedProtocolChannel channel;
private static volatile IrisSessionRegistry registry;
private static volatile IrisProtocolServer protocolServer;
private static volatile ModdedProtocolTransport transport;
private static int dimensionSyncTicks;
private ModdedProtocolHandler() {
}
public static void bindChannel(ModdedProtocolChannel boundChannel) {
channel = Objects.requireNonNull(boundChannel, "protocol channel");
}
public static void start(MinecraftServer server) {
ModdedProtocolChannel boundChannel = channel;
if (server == null || boundChannel == null) {
return;
}
SESSION_ENGINES.clear();
SESSION_LEVELS.clear();
IrisSessionRegistry sessionRegistry = new IrisSessionRegistry();
ModdedProtocolTransport serverTransport = new ModdedProtocolTransport(server, boundChannel);
IrisProtocolServer protocol = new IrisProtocolServer(sessionRegistry, SERVER_CAPABILITIES, brand(), true);
EngineResolver engineResolver = (String sessionId) -> {
Engine engine = SESSION_ENGINES.get(sessionId);
return engine == null || engine.isClosed() ? null : engine;
};
protocol.setEngineResolver(engineResolver);
protocol.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, sessionRegistry));
registry = sessionRegistry;
transport = serverTransport;
protocolServer = protocol;
IrisServices.register(IrisProtocolServer.class, protocol);
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
sessionRegistry.register(new IrisSession(player.getUUID().toString(), serverTransport));
}
}
public static void stop() {
IrisServices.remove(IrisProtocolServer.class);
IrisSessionRegistry current = registry;
if (current != null) {
for (IrisSession session : current.all()) {
current.unregister(session.id());
}
}
SESSION_ENGINES.clear();
SESSION_LEVELS.clear();
registry = null;
protocolServer = null;
transport = null;
}
public static void onPlayerJoin(ServerPlayer player) {
IrisSessionRegistry current = registry;
ModdedProtocolTransport currentTransport = transport;
if (player == null || current == null || currentTransport == null) {
return;
}
current.register(new IrisSession(player.getUUID().toString(), currentTransport));
}
public static void onPlayerDisconnect(ServerPlayer player) {
IrisSessionRegistry current = registry;
if (player == null || current == null) {
return;
}
String sessionId = player.getUUID().toString();
current.unregister(sessionId);
SESSION_ENGINES.remove(sessionId);
SESSION_LEVELS.remove(sessionId);
}
public static void onInbound(ServerPlayer player, byte[] frame) {
IrisProtocolServer current = protocolServer;
if (player == null || frame == null || current == null) {
return;
}
String sessionId = player.getUUID().toString();
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
current.onClientFrame(sessionId, frame);
return;
}
scheduler.global(() -> current.onClientFrame(sessionId, frame));
}
public static void tickDimensionSync(MinecraftServer server) {
IrisSessionRegistry current = registry;
IrisProtocolServer protocol = protocolServer;
if (server == null || current == null || protocol == null) {
return;
}
dimensionSyncTicks++;
if (dimensionSyncTicks < DIMENSION_SYNC_INTERVAL_TICKS) {
return;
}
dimensionSyncTicks = 0;
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
String sessionId = player.getUUID().toString();
IrisSession session = current.get(sessionId);
if (session == null || !session.isReady()) {
continue;
}
ServerLevel level = player.level();
String levelId = level.dimension().identifier().toString();
Engine cached = SESSION_ENGINES.get(sessionId);
if (levelId.equals(SESSION_LEVELS.get(sessionId)) && (cached == null || !cached.isClosed())) {
continue;
}
if (syncDimension(protocol, sessionId, level, levelId)) {
SESSION_LEVELS.put(sessionId, levelId);
}
}
}
private static boolean syncDimension(IrisProtocolServer protocol, String sessionId, ServerLevel level, String levelId) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
Engine engine = generator instanceof IrisModdedChunkGenerator irisGenerator ? resolveEngine(level, irisGenerator) : null;
if (generator instanceof IrisModdedChunkGenerator && engine == null) {
return false;
}
long seed = level.getSeed();
if (engine != null) {
SESSION_ENGINES.put(sessionId, engine);
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
seed, engine.getMinHeight(), engine.getMaxHeight(), true);
return true;
}
SESSION_ENGINES.remove(sessionId);
protocol.sendDimensionStatus(sessionId, levelId, "", seed, level.getMinY(), level.getMinY() + level.getHeight(), false);
return true;
}
private static Engine resolveEngine(ServerLevel level, IrisModdedChunkGenerator generator) {
try {
return generator.commandEngine();
} catch (Throwable failure) {
LOGGER.error("Iris dimension status engine lookup failed for {}", level.dimension().identifier(), failure);
return null;
}
}
private static String brand() {
return "Iris " + ModdedEngineBootstrap.loader().modVersion() + " (" + ModdedEngineBootstrap.loader().platformName() + ")";
}
}
@@ -0,0 +1,69 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.protocol.IrisServerTransport;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import java.util.Objects;
import java.util.UUID;
public final class ModdedProtocolTransport implements IrisServerTransport {
private final MinecraftServer server;
private final ModdedProtocolChannel channel;
public ModdedProtocolTransport(MinecraftServer server, ModdedProtocolChannel channel) {
this.server = Objects.requireNonNull(server, "server");
this.channel = Objects.requireNonNull(channel, "protocol channel");
}
@Override
public void sendToClient(String sessionId, byte[] frame) {
if (sessionId == null || frame == null) {
return;
}
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return;
}
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
return;
}
ModdedIrisPayload payload = new ModdedIrisPayload(frame);
scheduler.global(() -> deliver(playerId, payload));
}
private void deliver(UUID playerId, ModdedIrisPayload payload) {
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
if (player == null || !channel.canReceive(player)) {
return;
}
channel.send(player, payload);
}
private static UUID parseUuid(String sessionId) {
try {
return UUID.fromString(sessionId);
} catch (IllegalArgumentException invalid) {
return null;
}
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
@@ -30,12 +31,17 @@ import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;
public final class ModdedRegistries implements PlatformRegistries {
@@ -88,10 +94,7 @@ public final class ModdedRegistries implements PlatformRegistries {
@Override
public PlatformItem item(String key) {
String normalized = key.trim().toLowerCase(Locale.ROOT).replace(' ', '_');
if (normalized.startsWith("minecraft:")) {
normalized = normalized.substring("minecraft:".length());
}
Identifier identifier = Identifier.tryParse("minecraft:" + normalized);
Identifier identifier = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
if (identifier == null || !BuiltInRegistries.ITEM.containsKey(identifier)) {
return null;
}
@@ -144,6 +147,69 @@ public final class ModdedRegistries implements PlatformRegistries {
return keys;
}
@Override
public List<String> itemKeys() {
List<String> keys = new ArrayList<>();
for (Identifier identifier : BuiltInRegistries.ITEM.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> entityKeys() {
List<String> keys = new ArrayList<>();
for (Identifier identifier : BuiltInRegistries.ENTITY_TYPE.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> blockTypeKeys() {
List<String> keys = new ArrayList<>();
for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> enchantmentKeys() {
List<String> keys = new ArrayList<>();
Registry<Enchantment> registry = enchantmentRegistry();
if (registry == null) {
return keys;
}
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public List<String> potionEffectKeys() {
List<String> keys = new ArrayList<>();
for (Identifier identifier : BuiltInRegistries.MOB_EFFECT.keySet()) {
keys.add(identifier.toString());
}
return keys;
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
Map<String, List<PlatformBlockProperty>> properties = new LinkedHashMap<>();
for (Block block : BuiltInRegistries.BLOCK) {
BlockState defaultState = block.defaultBlockState();
List<PlatformBlockProperty> converted = new ArrayList<>();
for (Property<?> property : block.getStateDefinition().getProperties()) {
converted.add(convertProperty(property, defaultState));
}
properties.put(BuiltInRegistries.BLOCK.getKey(block).toString(), List.copyOf(converted));
}
return properties;
}
private Registry<Biome> biomeRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
@@ -151,4 +217,29 @@ public final class ModdedRegistries implements PlatformRegistries {
}
return instance.registryAccess().lookupOrThrow(Registries.BIOME);
}
private Registry<Enchantment> enchantmentRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
return null;
}
return instance.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
}
private static <T extends Comparable<T>> PlatformBlockProperty convertProperty(Property<T> property, BlockState defaultState) {
T defaultValue = defaultState.getValue(property);
Class<T> valueClass = property.getValueClass();
List<Object> allowedValues = new ArrayList<>();
if (valueClass == Boolean.class || valueClass == Integer.class) {
for (T value : property.getPossibleValues()) {
allowedValues.add(value);
}
String jsonType = valueClass == Boolean.class ? "boolean" : "integer";
return new PlatformBlockProperty(property.getName(), jsonType, defaultValue, List.copyOf(allowedValues), null);
}
for (T value : property.getPossibleValues()) {
allowedValues.add(property.getName(value));
}
return new PlatformBlockProperty(property.getName(), "string", property.getName(defaultValue), List.copyOf(allowedValues), null);
}
}
@@ -43,13 +43,17 @@ public final class ModdedScheduler implements PlatformScheduler {
private static volatile Thread mainThread;
private final ThreadPoolExecutor asyncExecutor;
private volatile ThreadPoolExecutor asyncExecutor;
private final ConcurrentLinkedQueue<Runnable> mainQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<DelayedTask> delayedQueue = new ConcurrentLinkedQueue<>();
public ModdedScheduler() {
this.asyncExecutor = createAsyncExecutor();
}
private static ThreadPoolExecutor createAsyncExecutor() {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(ASYNC_QUEUE_CAPACITY);
this.asyncExecutor = new ThreadPoolExecutor(
ThreadPoolExecutor executor = new ThreadPoolExecutor(
ASYNC_CORE_THREADS,
ASYNC_MAX_THREADS,
ASYNC_KEEP_ALIVE_SECONDS,
@@ -57,7 +61,8 @@ public final class ModdedScheduler implements PlatformScheduler {
workQueue,
new AsyncThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
this.asyncExecutor.allowCoreThreadTimeOut(true);
executor.allowCoreThreadTimeOut(true);
return executor;
}
public static void tick(MinecraftServer server) {
@@ -115,7 +120,18 @@ public final class ModdedScheduler implements PlatformScheduler {
}
public void reset() {
asyncExecutor.getQueue().clear();
if (asyncExecutor.isShutdown()) {
asyncExecutor = createAsyncExecutor();
} else {
asyncExecutor.getQueue().clear();
}
mainQueue.clear();
delayedQueue.clear();
mainThread = null;
}
public void shutdown() {
asyncExecutor.shutdownNow();
mainQueue.clear();
delayedQueue.clear();
mainThread = null;
@@ -0,0 +1,81 @@
/*
* 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.BuildConstants;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.json.JSONException;
import com.google.gson.JsonSyntaxException;
import io.sentry.Sentry;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedSentry {
private static final String DSN = "http://4cdbb9ac953306529947f4ca1e8e6b26@sentry.volmit.com:8080/2";
private static final long FLUSH_TIMEOUT_MS = 2000L;
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private ModdedSentry() {
}
public static void start(ModdedLoader loader) {
IrisSettings.IrisSettingsSentry settings = IrisSettings.get().getSentry();
if (settings.disableAutoReporting || Sentry.isEnabled() || Boolean.getBoolean("iris.suppressReporting")) {
return;
}
if (!STARTED.compareAndSet(false, true)) {
return;
}
IrisLogging.info("Enabling Sentry for anonymous error reporting. You can disable this in the settings.");
Sentry.init(options -> {
options.setDsn(DSN);
if (settings.debug) {
options.setDebug(true);
}
options.setAttachServerName(false);
options.setEnableUncaughtExceptionHandler(false);
options.setRelease(loader.modVersion());
options.setEnvironment(BuildConstants.ENVIRONMENT);
options.setBeforeSend((event, hint) -> suppress(event.getThrowable()) ? null : event);
});
Sentry.configureScope(scope -> {
scope.setTag("server", loader.platformName());
scope.setTag("server.mc", loader.minecraftVersion());
scope.setTag("iris.loader", loader.platformName());
scope.setTag("iris.commit", BuildConstants.COMMIT);
});
ModdedPlatform.captureSink(Sentry::captureException);
}
public static void flush() {
if (Sentry.isEnabled()) {
Sentry.flush(FLUSH_TIMEOUT_MS);
Sentry.close();
}
ModdedPlatform.captureSink(null);
STARTED.set(false);
}
private static boolean suppress(Throwable error) {
return (error instanceof IllegalStateException state && "zip file closed".equals(state.getMessage()))
|| error instanceof JSONException
|| error instanceof JsonSyntaxException;
}
}
@@ -0,0 +1,41 @@
/*
* 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.spi.IrisLogging;
import net.minecraft.server.MinecraftServer;
public final class ModdedServerCommands {
private ModdedServerCommands() {
}
public static boolean dispatch(MinecraftServer server, String command) {
if (server == null || command == null || command.isBlank()) {
return false;
}
String prepared = command.startsWith("/") ? command.substring(1) : command;
try {
server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), prepared);
return true;
} catch (Throwable error) {
IrisLogging.reportError(error);
return false;
}
}
}
@@ -0,0 +1,119 @@
/*
* 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.loader.ResourceLoader;
import art.arcane.iris.core.project.SchemaBuilder;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
public final class ModdedWorkspaceGenerator {
private ModdedWorkspaceGenerator() {
}
public static File writeWorkspace(IrisData data, File folder) throws IOException {
JSONObject workspaceConfig = buildWorkspace(data);
File workspace = new File(folder, folder.getName() + ".code-workspace");
IO.writeAll(workspace, workspaceConfig.toString(4));
return workspace;
}
public static JSONObject buildWorkspace(IrisData data) {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject json = new JSONObject();
json.put("editor.autoIndent", "brackets");
json.put("editor.acceptSuggestionOnEnter", "smart");
json.put("editor.cursorSmoothCaretAnimation", true);
json.put("editor.dragAndDrop", false);
json.put("files.trimTrailingWhitespace", true);
json.put("diffEditor.ignoreTrimWhitespace", true);
json.put("files.trimFinalNewlines", true);
json.put("editor.suggest.showKeywords", false);
json.put("editor.suggest.showSnippets", false);
json.put("editor.suggest.showWords", false);
JSONObject quick = new JSONObject();
quick.put("strings", true);
json.put("editor.quickSuggestions", quick);
json.put("editor.suggest.insertMode", "replace");
settings.put("[json]", json);
settings.put("json.maxItemsComputed", 30000);
settings.put("json.schemas", buildSchemas(data));
ws.put("settings", settings);
return ws;
}
private static JSONArray buildSchemas(IrisData data) {
JSONArray schemas = new JSONArray();
for (ResourceLoader<?> loader : data.getLoaders().v()) {
if (loader.supportsSchemas()) {
schemas.put(loader.buildSchema());
}
}
for (Class<?> snippetClass : data.resolveSnippets()) {
try {
String snipType = snippetClass.getDeclaredAnnotation(Snippet.class).value();
JSONObject entry = new JSONObject();
KList<String> fileMatch = new KList<>();
for (int depth = 1; depth < 8; depth++) {
fileMatch.add("/snippet/" + snipType + Form.repeat("/*", depth) + ".json");
}
entry.put("fileMatch", new JSONArray(fileMatch.toArray()));
entry.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
schemas.put(entry);
File schemaFile = new File(data.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
J.attemptAsync(() -> {
try {
IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, data).construct().toString(4));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
});
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
return schemas;
}
}
@@ -0,0 +1,624 @@
/*
* 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.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisRate;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawnGroup;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisSurface;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterMarker;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public final class ModdedWorldManager implements EngineWorldManager {
private static final ConcurrentHashMap<Engine, Queue<Long>> INITIAL_QUEUES = new ConcurrentHashMap<>();
private static final int MAX_INITIAL_QUEUE = 8192;
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
private static final long COUNT_INTERVAL_MS = 3_000L;
private static final int ENTITY_SCAN_RADIUS = 64;
private static final int PLAYER_CHUNK_RADIUS = 4;
private static final int MIN_TICK_INTERVAL_MS = 1_000;
private final Engine engine;
private long lastAmbientAt;
private long lastCountAt;
private volatile int cachedEntityCount;
private volatile int cachedConsideredChunks;
private volatile double cachedSaturation;
public ModdedWorldManager(Engine engine) {
this.engine = engine;
INITIAL_QUEUES.putIfAbsent(engine, new ConcurrentLinkedQueue<>());
}
public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) {
if (engine == null) {
return;
}
Queue<Long> queue = INITIAL_QUEUES.get(engine);
if (queue == null || queue.size() >= MAX_INITIAL_QUEUE) {
return;
}
queue.add(pack(chunkX, chunkZ));
}
public void serverTick(ServerLevel level) {
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
return;
}
if (isPregenActive()) {
return;
}
drainInitialSpawns(level);
ambientTick(level);
}
private void drainInitialSpawns(ServerLevel level) {
Queue<Long> queue = INITIAL_QUEUES.get(engine);
if (queue == null || queue.isEmpty()) {
return;
}
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
queue.clear();
return;
}
int budget = MAX_INITIAL_DRAIN_PER_TICK;
while (budget-- > 0) {
Long key = queue.poll();
if (key == null) {
return;
}
int chunkX = unpackX(key);
int chunkZ = unpackZ(key);
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
continue;
}
try {
initialSpawnChunk(level, chunkX, chunkZ);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
private void initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isChunkLoaded(chunkX, chunkZ) || mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
chunk.raiseFlagUnchecked(MantleFlag.INITIAL_SPAWNED, () -> {
if (markerSystemEnabled()) {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, true);
}
});
} finally {
chunk.release();
}
}
private void ambientTick(ServerLevel level) {
long now = System.currentTimeMillis();
long interval = Math.max((long) MIN_TICK_INTERVAL_MS, IrisSettings.get().getWorld().getAsyncTickIntervalMS());
if (now - lastAmbientAt < interval) {
return;
}
lastAmbientAt = now;
if (level.players().isEmpty()) {
return;
}
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
return;
}
refreshEntityCount(level, now);
if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
return;
}
long[] candidates = loadedChunksNearPlayers(level);
if (candidates.length == 0) {
return;
}
int spawnBuffer = RNG.r.i(2, 12);
while (spawnBuffer-- > 0) {
long key = candidates[RNG.r.nextInt(candidates.length)];
try {
ambientSpawnChunk(level, unpackX(key), unpackZ(key));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
if (markerSystemEnabled()) {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, false);
}
} finally {
chunk.release();
}
}
private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
int minHeight = engine.getWorld().minHeight();
chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> {
String tag = marker.getTag();
if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) {
return;
}
IrisMarker resolved = engine.getData().getMarkerLoader().load(tag);
if (resolved == null) {
return;
}
int worldX = (chunkX << 4) + (x & 15);
int worldZ = (chunkZ << 4) + (z & 15);
int worldY = yf + minHeight;
if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) {
return;
}
KList<IrisSpawner> spawners = resolveMarkerSpawners(resolved);
if (spawners.isEmpty()) {
return;
}
IrisSpawner chosen = spawners.getRandom();
if (chosen == null) {
return;
}
spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial);
});
}
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) {
KList<IrisSpawner> spawners = new KList<>();
for (String key : marker.getSpawners()) {
IrisSpawner spawner = engine.getData().getSpawnerLoader().load(key);
if (spawner == null) {
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
continue;
}
spawner.setReferenceMarker(marker);
spawners.add(spawner);
}
return spawners;
}
private void spawnFromSpawner(ServerLevel level, IrisPosition position, IrisSpawner spawner, boolean initial) {
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
if (spawns.isEmpty()) {
return;
}
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
}
IrisEntitySpawn chosen = rarityPick(spawns);
if (chosen == null) {
return;
}
int chunkX = position.getX() >> 4;
int chunkZ = position.getZ() >> 4;
if (!canSpawn(spawner, chunkX, chunkZ)) {
return;
}
int spawned = spawnEntryAt(level, chosen, spawner, position);
if (spawned > 0) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void spawnAmbient(ServerLevel level, int chunkX, int chunkZ, boolean initial) {
IrisComplex complex = engine.getComplex();
if (complex == null) {
return;
}
int blockX = chunkX << 4;
int blockZ = chunkZ << 4;
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
IrisRegion region = engine.getRegion(blockX, blockZ);
int chunkMobs = countChunkMobs(level, chunkX, chunkZ);
KList<IrisEntitySpawn> pool = new KList<>();
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(region.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
if (pool.isEmpty()) {
return;
}
IrisEntitySpawn chosen = rarityPick(pool);
if (chosen == null || chosen.getReferenceSpawner() == null) {
return;
}
IrisSpawner spawner = chosen.getReferenceSpawner();
if (!canSpawn(spawner, chunkX, chunkZ)) {
return;
}
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ);
if (spawned > 0) {
spawner.spawn(engine, chunkX, chunkZ);
}
}
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) {
for (IrisSpawner spawner : spawners) {
if (spawner == null) {
continue;
}
if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) {
continue;
}
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
continue;
}
if (!canSpawn(spawner, chunkX, chunkZ)) {
continue;
}
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
for (IrisEntitySpawn entry : spawns) {
entry.setReferenceSpawner(spawner);
entry.setReferenceMarker(spawner.getReferenceMarker());
pool.add(entry);
}
}
}
private int spawnEntry(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
}
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
if (count <= 0) {
return 0;
}
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
IrisSpawnGroup group = spawner.getGroup();
int spawned = 0;
for (int i = 0; i < count; i++) {
int worldX = (chunkX << 4) + RNG.r.i(15);
int worldZ = (chunkZ << 4) + RNG.r.i(15);
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
int worldY = switch (group) {
case NORMAL -> surfaceY + 1;
case CAVE -> solidY + 1;
case UNDERWATER, BEACH -> surfaceY > solidY + 1 ? RNG.r.i(solidY + 1, surfaceY) : surfaceY;
};
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
continue;
}
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
continue;
}
if (!clearForSpawn(level, worldX, worldY, worldZ)) {
continue;
}
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
spawned++;
}
}
return spawned;
}
private int spawnEntryAt(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, IrisPosition position) {
IrisEntity irisEntity = entry.getRealEntity(engine);
if (irisEntity == null) {
return 0;
}
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
if (count <= 0) {
return 0;
}
exhaustMarker(spawner, position);
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
int worldX = position.getX();
int worldY = position.getY() + 1;
int worldZ = position.getZ();
int spawned = 0;
for (int i = 0; i < count; i++) {
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
spawned++;
}
}
return spawned;
}
private void exhaustMarker(IrisSpawner spawner, IrisPosition position) {
IrisMarker marker = spawner.getReferenceMarker();
if (marker == null || !marker.shouldExhaust()) {
return;
}
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
}
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
IrisRate rate = spawner.getMaximumRate();
if (!rate.isInfinite() && !engine.getEngineData().getCooldown(spawner).canSpawn(rate)) {
return false;
}
IrisRate chunkRate = spawner.getMaximumRatePerChunk();
return chunkRate.isInfinite() || engine.getEngineData().getChunk(chunkX, chunkZ).getCooldown(spawner).canSpawn(chunkRate);
}
private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) {
IrisRange range = spawner.getAllowedLightLevels();
if (range.getMin() > 0 || range.getMax() < 15) {
return range.contains(level.getMaxLocalRawBrightness(new BlockPos(worldX, worldY, worldZ)));
}
return true;
}
private boolean surfaceMatches(IrisSurface surface, ServerLevel level, int worldX, int worldY, int worldZ) {
BlockState below = level.getBlockState(new BlockPos(worldX, worldY - 1, worldZ));
return matchesSurface(surface, below);
}
private static boolean matchesSurface(IrisSurface surface, BlockState below) {
if (ModdedBlockResolution.isSolid(below)) {
return surface == IrisSurface.LAND || surface == IrisSurface.OVERWORLD
|| (surface == IrisSurface.ANIMAL && isAnimalGround(below));
}
if (below.is(Blocks.LAVA)) {
return surface == IrisSurface.LAVA;
}
if (ModdedBlockResolution.isWater(below) || ModdedBlockResolution.isWaterLogged(below) || isAquaticFoliage(below)) {
return surface == IrisSurface.WATER || surface == IrisSurface.OVERWORLD;
}
return false;
}
private static boolean isAnimalGround(BlockState state) {
return state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT) || state.is(Blocks.DIRT_PATH)
|| state.is(Blocks.COARSE_DIRT) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL)
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.SNOW_BLOCK);
}
private static boolean isAquaticFoliage(BlockState state) {
return state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS)
|| state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT);
}
private boolean clearForSpawn(ServerLevel level, int worldX, int worldY, int worldZ) {
BlockState at = level.getBlockState(new BlockPos(worldX, worldY, worldZ));
BlockState above = level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ));
return !ModdedBlockResolution.isSolid(at) && !ModdedBlockResolution.isSolid(above);
}
private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) {
return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ)))
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ)));
}
private int countChunkMobs(ServerLevel level, int chunkX, int chunkZ) {
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16);
return level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
}
private void refreshEntityCount(ServerLevel level, long now) {
if (now - lastCountAt < COUNT_INTERVAL_MS) {
return;
}
lastCountAt = now;
int mobs = 0;
int chunks = 0;
for (ServerPlayer player : level.players()) {
int px = player.blockPosition().getX();
int pz = player.blockPosition().getZ();
AABB box = new AABB(px - ENTITY_SCAN_RADIUS, level.getMinY(), pz - ENTITY_SCAN_RADIUS,
px + ENTITY_SCAN_RADIUS, level.getMaxY(), pz + ENTITY_SCAN_RADIUS);
mobs += level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
chunks += (PLAYER_CHUNK_RADIUS * 2 + 1) * (PLAYER_CHUNK_RADIUS * 2 + 1);
}
cachedEntityCount = mobs;
cachedConsideredChunks = chunks;
cachedSaturation = mobs / (chunks + 1.0) * 1.28;
}
private long[] loadedChunksNearPlayers(ServerLevel level) {
Set<Long> keys = new HashSet<>();
for (ServerPlayer player : level.players()) {
int centerX = player.blockPosition().getX() >> 4;
int centerZ = player.blockPosition().getZ() >> 4;
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
int chunkX = centerX + dx;
int chunkZ = centerZ + dz;
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) != null) {
keys.add(pack(chunkX, chunkZ));
}
}
}
}
long[] result = new long[keys.size()];
int index = 0;
for (long key : keys) {
result[index++] = key;
}
return result;
}
private boolean isPregenActive() {
PregeneratorJob job = PregeneratorJob.getInstance();
return job != null && job.targetsWorldName(engine.getWorld().name());
}
private static boolean markerSystemEnabled() {
return IrisSettings.get().getWorld().isMarkerEntitySpawningSystem();
}
private static boolean ambientSystemEnabled() {
return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
}
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
int totalRarity = 0;
for (IrisEntitySpawn entry : entries) {
totalRarity += IRare.get(entry);
}
if (totalRarity <= 0) {
return entries.getRandom();
}
KList<IrisEntitySpawn> weighted = new KList<>();
for (IrisEntitySpawn entry : entries) {
weighted.addMultiple(entry, totalRarity / IRare.get(entry));
}
return weighted.getRandom();
}
private static long pack(int x, int z) {
return (((long) x) & 0xFFFFFFFFL) | ((((long) z) & 0xFFFFFFFFL) << 32);
}
private static int unpackX(long key) {
return (int) (key & 0xFFFFFFFFL);
}
private static int unpackZ(long key) {
return (int) ((key >> 32) & 0xFFFFFFFFL);
}
@Override
public void close() {
INITIAL_QUEUES.remove(engine);
}
@Override
public int getEntityCount() {
return cachedEntityCount;
}
@Override
public int getChunkCount() {
return cachedConsideredChunks;
}
@Override
public double getEntitySaturation() {
return cachedSaturation;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
engine.getMantle().save();
}
@Override
public void onBlockBreak(BlockBreakEvent e) {
}
@Override
public void onBlockPlace(BlockPlaceEvent e) {
}
@Override
public void onChunkLoad(Chunk e, boolean generated) {
}
@Override
public void onChunkUnload(Chunk e) {
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
}
}
@@ -194,6 +194,8 @@ public final class IrisModdedCommands {
root.then(ModdedStructureCommands.tree("structure"));
root.then(ModdedStructureCommands.tree("struct"));
root.then(ModdedStructureCommands.tree("str"));
root.then(ModdedDeveloperCommands.tree("developer"));
root.then(ModdedDeveloperCommands.tree("dev"));
return root;
}
@@ -72,7 +72,8 @@ final class ModdedCommandHelp {
Entry.group("world", "Runtime Iris dimension creation, removal and status", "w"),
Entry.group("datapack", "World datapack install and status helpers", "datapacks", "dp"),
Entry.group("structure", "Iris structure index, info and placement tools", "struct", "str"),
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold")
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold"),
Entry.group("developer", "Developer diagnostics: Sentry test, network interfaces, region-file scan", "dev")
));
SECTIONS.put("find", List.of(
Entry.command("biome", "<key>", "Find an Iris biome"),
@@ -162,6 +163,11 @@ final class ModdedCommandHelp {
));
SECTIONS.put("struct", SECTIONS.get("structure"));
SECTIONS.put("str", SECTIONS.get("structure"));
SECTIONS.put("developer", List.of(
Entry.command("sentry", "", "Send a test exception to the Iris error reporter"),
Entry.command("network", "", "List network interfaces and their addresses", "ip")
));
SECTIONS.put("dev", SECTIONS.get("developer"));
}
private ModdedCommandHelp() {
@@ -0,0 +1,76 @@
/*
* 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.spi.IrisPlatforms;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.function.Predicate;
final class ModdedDeveloperCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private ModdedDeveloperCommands() {
}
static LiteralArgumentBuilder<CommandSourceStack> tree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "developer"))
.then(Commands.literal("sentry")
.executes((CommandContext<CommandSourceStack> context) -> sentry(context.getSource())))
.then(Commands.literal("network")
.executes((CommandContext<CommandSourceStack> context) -> network(context.getSource())))
.then(Commands.literal("ip")
.executes((CommandContext<CommandSourceStack> context) -> network(context.getSource())));
}
private static int sentry(CommandSourceStack source) {
IrisPlatforms.get().reportError(new Exception("This is an Iris Sentry test exception"));
ModdedCommandFeedback.ok(source, "Dispatched a test exception to the Iris error reporter (Sentry if enabled).");
return 1;
}
private static int network(CommandSourceStack source) {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface networkInterface : Collections.list(interfaces)) {
ModdedCommandFeedback.ok(source, networkInterface.getDisplayName());
for (InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
ModdedCommandFeedback.ok(source, " " + address.getHostAddress());
}
}
return 1;
} catch (SocketException error) {
LOGGER.error("Iris developer network dump failed", error);
ModdedCommandFeedback.fail(source, "Network scan failed: " + error.getClass().getSimpleName());
return 0;
}
}
}
@@ -1,6 +1,10 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisSession;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.volmlib.util.format.Form;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
@@ -27,6 +31,9 @@ public final class ModdedPregenBossBar {
if (player == null) {
return;
}
if (hasClientPregenHud(player)) {
return;
}
viewer = player.getUUID();
bar = new ServerBossEvent(BAR_ID, Component.literal("Iris Pregen starting..."), BossEvent.BossBarColor.GREEN, BossEvent.BossBarOverlay.PROGRESS);
bar.setProgress(0.0F);
@@ -84,6 +91,15 @@ public final class ModdedPregenBossBar {
return name;
}
private static boolean hasClientPregenHud(ServerPlayer player) {
IrisProtocolServer protocol = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocol == null) {
return false;
}
IrisSession session = protocol.registry().get(player.getUUID().toString());
return session != null && session.isReady() && session.hasCapability(IrisProtocol.CAPABILITY_PREGEN);
}
private static double clamp01(double value) {
if (value < 0.0D) {
return 0.0D;
@@ -36,13 +36,13 @@ 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.modded.ModdedWorkspaceGenerator;
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.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
@@ -262,15 +262,15 @@ public final class ModdedStudioCommands {
if (folder == null) {
return 0;
}
File workspace = new File(folder, folder.getName() + ".code-workspace");
File workspace;
try {
IO.writeAll(workspace, workspaceConfig().toString(4));
workspace = ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(folder), folder);
} catch (IOException e) {
LOGGER.error("Iris workspace write failed for {}", workspace, e);
IrisModdedCommands.fail(source, "Failed to write " + workspace.getAbsolutePath() + ": " + e.getMessage());
LOGGER.error("Iris workspace write failed for {}", folder, e);
IrisModdedCommands.fail(source, "Failed to write workspace for " + folder.getAbsolutePath() + ": " + e.getMessage());
return 0;
}
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " (JSON schemas require the Bukkit studio toolchain).");
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " with JSON schemas for autocomplete.");
if (!open) {
return 1;
}
@@ -289,41 +289,6 @@ public final class ModdedStudioCommands {
return 1;
}
private static JSONObject workspaceConfig() {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject json = new JSONObject();
json.put("editor.autoIndent", "brackets");
json.put("editor.acceptSuggestionOnEnter", "smart");
json.put("editor.cursorSmoothCaretAnimation", true);
json.put("editor.dragAndDrop", false);
json.put("files.trimTrailingWhitespace", true);
json.put("diffEditor.ignoreTrimWhitespace", true);
json.put("files.trimFinalNewlines", true);
json.put("editor.suggest.showKeywords", false);
json.put("editor.suggest.showSnippets", false);
json.put("editor.suggest.showWords", false);
JSONObject quick = new JSONObject();
quick.put("strings", true);
json.put("editor.quickSuggestions", quick);
json.put("editor.suggest.insertMode", "replace");
settings.put("[json]", json);
settings.put("json.maxItemsComputed", 30000);
settings.put("json.schemas", new JSONArray());
ws.put("settings", settings);
return ws;
}
private static String guiUnavailableMessage() {
if (GuiHost.isDesktopSuppressed()) {
return "Iris desktop GUIs are disabled in singleplayer/client to avoid crashing the game client; use the in-game map and chat output instead.";
@@ -611,9 +576,14 @@ public final class ModdedStudioCommands {
}
}
IrisProjectCopier.copyProject(templateFolder, target, template, name);
try {
ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(target), target);
} catch (IOException e) {
LOGGER.error("Iris studio create workspace generation failed for {}", name, e);
}
server.execute(() -> {
IrisModdedCommands.ok(source, "Created project '" + name + "' at " + target.getAbsolutePath());
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack, then create a world with it. VSCode workspaces are generated by the Bukkit studio toolchain only.");
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack. A VSCode workspace with JSON schema autocomplete was generated; open it or rerun /iris studio vscode.");
});
} catch (Throwable e) {
LOGGER.error("Iris studio create failed for {}", name, e);
@@ -0,0 +1,59 @@
/*
* 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.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedWorldManager;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
public final class ModdedEntitySpawnService implements ModdedTickableService {
@Override
public void onEnable() {
}
@Override
public void onDisable() {
}
@Override
public void onServerTick(MinecraftServer server) {
for (ServerLevel level : server.getAllLevels()) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
continue;
}
Engine engine = generator.engineIfBound();
if (engine == null || engine.isClosed()) {
continue;
}
EngineWorldManager worldManager = engine.getWorldManager();
if (!(worldManager instanceof ModdedWorldManager moddedWorldManager)) {
continue;
}
try {
moddedWorldManager.serverTick(level);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
}
}
@@ -19,9 +19,11 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
@@ -169,12 +171,22 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
try {
engine.hotloadSilently();
generator.onHotload();
regenerateSchemas(dimensionId, engine);
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
}
}
private void regenerateSchemas(String dimensionId, Engine engine) {
try {
IrisData data = engine.getData();
ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder());
} catch (Throwable e) {
LOGGER.error("Iris studio schema regeneration failed for {}", dimensionId, e);
}
}
private static final class Watch {
private final ChronoLatch latch = new ChronoLatch(CHECK_LATCH_MILLIS, false);
private final AtomicBoolean busy = new AtomicBoolean(false);
@@ -0,0 +1,4 @@
{
"key.categories.irisworldgen.iris": "Iris",
"key.irisworldgen.toggle_pregen_hud": "Toggle Pregen HUD"
}