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
+90
View File
@@ -0,0 +1,90 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (core:check + spi:build)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run core checks and build spi
run: ./gradlew :core:check :spi:build :probe:deserializationProbe --console=plain --stacktrace
build-artifacts:
name: Build ${{ matrix.platform }}
needs: verify
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- platform: bukkit
task: buildBukkit
- platform: fabric
task: buildFabric
- platform: forge
task: buildForge
- platform: neoforge
task: buildNeoforge
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build ${{ matrix.platform }} artifact
run: ./gradlew ${{ matrix.task }} --console=plain --stacktrace
- name: Upload ${{ matrix.platform }} jar
uses: actions/upload-artifact@v4
with:
name: iris-${{ matrix.platform }}
path: dist/*.jar
retention-days: 14
if-no-files-found: error
release-bundle:
name: Release bundle (buildAll)
if: startsWith(github.ref, 'refs/tags/v')
needs: verify
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build all platforms
run: ./gradlew buildAll --console=plain --stacktrace
- name: Upload release bundle
uses: actions/upload-artifact@v4
with:
name: iris-release-bundle
path: dist/*.jar
retention-days: 90
if-no-files-found: error
+2 -1
View File
@@ -10,7 +10,8 @@ libs/
collection/
/core/src/main/java/art/arcane/iris/util/uniques/
out/
tools/simd-bench/out/
DataPackExamples/
+108 -54
View File
@@ -1,75 +1,129 @@
# Iris
The master branch is for the latest version of minecraft.
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
or NeoForge server mod, and produces bit-identical terrain on every platform for the same pack and
seed. The master branch targets the current Minecraft version (26.2).
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
# Building
Consider supporting development by buying Iris on Spigot.
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.[README.md](README.md)
## Platforms
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.2 | Full feature set |
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.0+ |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2+ |
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
Java 25 is required on every platform.
### Command Line Builds
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
workspaces with schema autocomplete over the server's live registries, entity spawning parity
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
the goldenhash determinism gate, which is interchangeable across all four platforms.
1. Install [Java JDK 25](https://adoptium.net/temurin/releases/?version=25)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-25` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 25
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
3. Once the project has setup, run `gradlew iris`
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
## Install
### IDE Builds (for development)
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
the server. On first boot Iris downloads the default `overworld` pack automatically.
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
prefer.
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris
downloads the default `overworld` pack before the worldgen datapack is written, so the default
pack is fully active immediately. Packs installed later register their custom dimension types
(height ranges) and custom biomes through the forced datapack at server start — restart once after
adding a pack so worlds get its full heights and biomes; worlds created before that restart run
with fallback heights.
# Iris Toolbelt
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
Create New World screen; the integrated server runs the same engine.
Everyone needs a tool-belt.
## The client mod
```java
package art.arcane.iris.core.tools;
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
// Get IrisDataManager from a world
IrisToolbelt.access(anyWorld).getCompound().getData();
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
mod is inert.
// Get Default Engine from world
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
## Quickstart
// Get the engine at the given height
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
Create and enter an Iris world.
// IS THIS THING ON?
boolean yes=IrisToolbelt.isIrisWorld(world);
Plugin (optional arguments are keyed):
// GTFO for worlds (moves players to any other world, just not this one)
IrisToolbelt.evacuate(world);
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
.name("myWorld") // The world name
.dimension("terrifyinghands")
.seed(69133742) // The world seed
.pregen(PregenTask // Define a pregen job to run
.builder()
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
.build())
.create();
```
/iris create myworld type=overworld seed=1337
/iris load myworld
```
Mod (positional arguments):
```
/iris create myworld overworld 1337
```
`/iris pregen start` pregenerates around spawn; players with the client mod see the native HUD,
everyone else gets a boss bar (modded) or console/status output. `/iris pregen status` reports
progress on the plugin.
## Studio and VSCode workspace
The studio is the pack authoring environment, available on all platforms. Studio worlds are
transient — they are deleted on close and purged at startup.
```
/iris studio create <name> [template] scaffold a new pack (default template: example)
/iris studio open <pack> [seed] open a temporary studio world for live editing
/iris studio vscode [pack] write a .code-workspace with JSON schemas
/iris studio update [pack] regenerate the workspace schemas
/iris studio close close and discard the studio world
```
As in the quickstart, optional arguments are keyed on the plugin (`seed=1234`) and positional on
the mod.
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
files hotloads the changes and regenerates the schemas.
## Building from source
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAll
```
builds every platform artifact into `dist/`:
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
Discord asking for free copies or a compile tutorial.
## Maintainer docs
- [Minecraft version bump checklist](docs/mc-version-bump.md)
- [Release checklist](docs/release-checklist.md)
@@ -1,5 +1,34 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
@@ -7,22 +36,18 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import net.minecraft.core.*;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.*;
import net.minecraft.world.level.biome.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.*;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
@@ -42,7 +67,6 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class IrisChunkGenerator extends CustomChunkGenerator {
@@ -29,7 +29,15 @@ import art.arcane.volmlib.util.math.Vector3d;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
import art.arcane.volmlib.util.nbt.mca.palette.*;
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAChunkBiomeContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAGlobalPalette;
import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMap;
import art.arcane.volmlib.util.nbt.mca.palette.MCAIdMapper;
import art.arcane.volmlib.util.nbt.mca.palette.MCAPalette;
import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
import art.arcane.volmlib.util.nbt.mca.palette.MCAPalettedContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.iris.util.common.scheduling.J;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -38,11 +46,25 @@ import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.core.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.Registry;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CollectionTag;
import net.minecraft.nbt.DoubleTag;
import net.minecraft.nbt.EndTag;
import net.minecraft.nbt.FloatTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.NumericTag;
import net.minecraft.nbt.ShortTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.TagParser;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
@@ -82,7 +104,12 @@ import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.TreeFeature;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.CraftChunk;
@@ -109,7 +136,12 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
+1 -1
View File
@@ -1,4 +1,4 @@
def apiVersion = '26.2'
String apiVersion = providers.gradleProperty('minecraftVersion').get()
def mainClass = 'art.arcane.iris.Iris'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
@@ -85,12 +85,18 @@ import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.scheduling.Queue;
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
import lombok.NonNull;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.*;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.IllegalPluginAccessException;
@@ -98,10 +104,24 @@ import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -972,6 +992,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
super.onDisable();
J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll);
IrisPlatforms.unbind();
}
@Override
@@ -51,13 +51,17 @@ import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
@@ -20,14 +20,17 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import java.awt.*;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
@@ -49,7 +49,10 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -32,7 +32,14 @@ import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.StudioMode;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.data.Cuboid;
@@ -50,7 +57,14 @@ import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
@@ -60,7 +74,16 @@ import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation")
public class CommandObject implements DirectorExecutor {
@@ -61,7 +61,7 @@ public class CommandPregen implements DirectorExecutor {
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
sender().sendMessage(C.RED + "Failed to start pregeneration. See console for details.");
Iris.reportError(e);
e.printStackTrace();
}
@@ -31,7 +31,19 @@ import art.arcane.iris.core.structure.FeatureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisNoiseGenerator;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
@@ -60,7 +72,12 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
@@ -30,7 +30,10 @@ import org.bukkit.event.world.WorldUnloadEvent;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.concurrent.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -0,0 +1,208 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisServerTransport;
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.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.messaging.Messenger;
import org.bukkit.plugin.messaging.PluginMessageListener;
import java.util.UUID;
public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport {
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
private static final int DIMENSION_SYNC_RETRY_TICKS = 20;
private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15;
private IrisSessionRegistry registry;
private IrisProtocolServer protocolServer;
@Override
public void onEnable() {
Messenger messenger = Bukkit.getMessenger();
messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
registry = new IrisSessionRegistry();
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
protocolServer.setEngineResolver(engineResolver);
protocolServer.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, registry));
IrisServices.register(IrisProtocolServer.class, protocolServer);
for (Player player : Bukkit.getOnlinePlayers()) {
registry.register(new IrisSession(player.getUniqueId().toString(), this));
}
}
@Override
public void onDisable() {
IrisServices.remove(IrisProtocolServer.class);
Messenger messenger = Bukkit.getMessenger();
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
IrisSessionRegistry current = registry;
if (current != null) {
for (IrisSession session : current.all()) {
current.unregister(session.id());
}
}
registry = null;
protocolServer = null;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this));
syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS);
}
@EventHandler
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
if (registry == null) {
return;
}
syncDimension(event.getPlayer(), 1);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
IrisSessionRegistry current = registry;
if (current == null) {
return;
}
current.unregister(event.getPlayer().getUniqueId().toString());
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
IrisProtocolServer current = protocolServer;
if (current == null || !IrisProtocol.CHANNEL.equals(channel)) {
return;
}
current.onClientFrame(player.getUniqueId().toString(), message);
}
@Override
public void sendToClient(String sessionId, byte[] frame) {
if (sessionId == null || frame == null) {
return;
}
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return;
}
J.runEntity(player, () -> deliver(player, frame));
}
private void deliver(Player player, byte[] frame) {
if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) {
return;
}
player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame);
}
private void syncDimension(Player player, int attemptsLeft) {
IrisProtocolServer protocol = protocolServer;
IrisSessionRegistry current = registry;
if (protocol == null || current == null || !player.isOnline()) {
return;
}
String sessionId = player.getUniqueId().toString();
IrisSession session = current.get(sessionId);
if (session == null) {
return;
}
if (!session.isReady()) {
if (attemptsLeft > 1) {
J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS);
}
return;
}
World world = player.getWorld();
Engine engine = engineFor(world);
if (engine != null) {
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
return;
}
protocol.sendDimensionStatus(sessionId, world.getName(), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false);
}
private static Engine resolveEngine(String sessionId) {
UUID playerId = parseUuid(sessionId);
if (playerId == null) {
return null;
}
Player player = Bukkit.getPlayer(playerId);
if (player == null || !player.isOnline()) {
return null;
}
return engineFor(player.getWorld());
}
private static Engine engineFor(World world) {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) {
return null;
}
Engine engine = access.getEngine();
if (engine == null || engine.isClosed()) {
return null;
}
return engine;
}
private static UUID parseUuid(String sessionId) {
try {
return UUID.fromString(sessionId);
} catch (IllegalArgumentException invalid) {
return null;
}
}
private static String brand() {
return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)";
}
}
@@ -18,6 +18,12 @@
package art.arcane.iris.core.service;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.edit.DustRevealer;
@@ -35,7 +41,6 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.SR;
import art.arcane.iris.util.common.scheduling.jobs.Job;
import org.bukkit.*;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -25,7 +25,7 @@ import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.awt.*;
import java.awt.Color;
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
@@ -0,0 +1,6 @@
package art.arcane.iris.client;
@FunctionalInterface
public interface ClientPacketSink {
void send(byte[] frame);
}
@@ -0,0 +1,161 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.spi.protocol.ProtocolException;
import net.minecraft.resources.Identifier;
public final class IrisClient {
public static final Identifier HUD_ELEMENT_ID = Identifier.fromNamespaceAndPath("irisworldgen", "pregen_hud");
public static final Identifier KEYBIND_CATEGORY_ID = Identifier.fromNamespaceAndPath("irisworldgen", "iris");
public static final String KEYBIND_TOGGLE_HUD = "key.irisworldgen.toggle_pregen_hud";
public static final String KEYBIND_OPEN_MAP = "key.irisworldgen.open_vision_map";
public static final String KEYBIND_TOGGLE_WHAT = "key.irisworldgen.toggle_what_overlay";
private static final IrisClientSession SESSION = new IrisClientSession();
private static final IrisClientPregenState PREGEN = new IrisClientPregenState();
private static final IrisClientDimension DIMENSION = new IrisClientDimension();
private static final IrisClientTileCache TILES = new IrisClientTileCache(IrisClient::sendFrame, System::currentTimeMillis);
private static final IrisClientCursor CURSOR = new IrisClientCursor(IrisClient::sendFrame, System::currentTimeMillis);
private static final IrisClientMarkers MARKERS = new IrisClientMarkers();
private static final IrisClientRegionMap REGIONS = new IrisClientRegionMap();
private static final IrisClientToasts TOASTS = new IrisClientToasts();
private static volatile ClientPacketSink sink;
private static volatile boolean hudVisible = true;
private static volatile boolean whatVisible = false;
private IrisClient() {
}
public static IrisClientSession session() {
return SESSION;
}
public static IrisClientPregenState pregen() {
return PREGEN;
}
public static IrisClientDimension dimension() {
return DIMENSION;
}
public static IrisClientTileCache tiles() {
return TILES;
}
public static IrisClientCursor cursor() {
return CURSOR;
}
public static IrisClientMarkers markers() {
return MARKERS;
}
public static IrisClientRegionMap regions() {
return REGIONS;
}
public static IrisClientToasts toasts() {
return TOASTS;
}
public static boolean hudVisible() {
return hudVisible;
}
public static void toggleHud() {
hudVisible = !hudVisible;
}
public static boolean whatVisible() {
return whatVisible;
}
public static void toggleWhat() {
whatVisible = !whatVisible;
}
public static boolean visionAvailable() {
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_VISION) != 0L;
}
public static boolean cursorAvailable() {
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_CURSOR) != 0L;
}
public static void bindSender(ClientPacketSink boundSink) {
sink = boundSink;
SESSION.bind(boundSink);
}
public static void sendFrame(byte[] frame) {
ClientPacketSink activeSink = sink;
if (activeSink != null) {
activeSink.send(frame);
}
}
public static void onWorldJoin() {
SESSION.sendHello();
}
public static void onDisconnect() {
SESSION.reset();
PREGEN.clear();
DIMENSION.clear();
TILES.clear();
CURSOR.clear();
MARKERS.clear();
REGIONS.clear();
TOASTS.clear();
}
public static void onInbound(byte[] frame) {
if (frame == null) {
return;
}
IrisMessage message;
try {
message = IrisMessageCodec.decode(frame);
} catch (ProtocolException rejected) {
return;
}
if (message == null) {
return;
}
route(message);
}
private static void route(IrisMessage message) {
switch (message) {
case IrisMessage.ServerHello serverHello -> SESSION.onServerHello(serverHello);
case IrisMessage.PregenProgress progress -> PREGEN.onProgress(progress);
case IrisMessage.PregenEnd end -> onPregenEnd(end.jobId());
case IrisMessage.DimensionStatus status -> onDimensionStatus(status);
case IrisMessage.CursorInfo cursorInfo -> CURSOR.onCursorInfo(cursorInfo);
case IrisMessage.VisionTile visionTile -> TILES.onVisionTile(visionTile);
case IrisMessage.VisionMarkers visionMarkers -> MARKERS.onMarkers(visionMarkers);
case IrisMessage.PregenRegionDelta delta -> REGIONS.onDelta(delta, PREGEN.activeJobId());
case IrisMessage.StudioHotload hotload -> TOASTS.enqueueHotload(hotload.packKey(), hotload.changedFiles(), hotload.failed(), hotload.message());
case IrisMessage.Toast toast -> TOASTS.enqueue(toast.kind(), toast.title(), toast.body());
default -> {
}
}
}
private static void onPregenEnd(long jobId) {
PREGEN.onEnd(jobId);
REGIONS.onEnd(jobId);
}
private static void onDimensionStatus(IrisMessage.DimensionStatus status) {
boolean changed = DIMENSION.onDimensionStatus(status);
if (changed) {
TILES.clear();
MARKERS.clear();
CURSOR.clear();
REGIONS.clear();
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import java.util.function.LongSupplier;
public final class IrisClientCursor {
private static final long MIN_REQUEST_INTERVAL_MILLIS = 500L;
private final ClientPacketSink sink;
private final LongSupplier clock;
private volatile IrisMessage.CursorInfo latest;
private int lastRequestedX;
private int lastRequestedZ;
private boolean requested;
private long lastRequestMillis;
public IrisClientCursor(ClientPacketSink sink, LongSupplier clock) {
this.sink = sink;
this.clock = clock;
this.latest = null;
this.lastRequestedX = 0;
this.lastRequestedZ = 0;
this.requested = false;
this.lastRequestMillis = 0L;
}
public synchronized void requestFor(int blockX, int blockZ) {
if (requested && blockX == lastRequestedX && blockZ == lastRequestedZ) {
return;
}
long now = clock.getAsLong();
if (now - lastRequestMillis < MIN_REQUEST_INTERVAL_MILLIS) {
return;
}
sink.send(IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(blockX, blockZ)));
lastRequestedX = blockX;
lastRequestedZ = blockZ;
requested = true;
lastRequestMillis = now;
}
public void onCursorInfo(IrisMessage.CursorInfo info) {
latest = info;
}
public IrisMessage.CursorInfo latest() {
return latest;
}
public synchronized void clear() {
latest = null;
requested = false;
lastRequestMillis = 0L;
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
public final class IrisClientDimension {
private volatile IrisMessage.DimensionStatus status;
public boolean onDimensionStatus(IrisMessage.DimensionStatus incoming) {
IrisMessage.DimensionStatus previous = status;
status = incoming;
if (previous == null) {
return true;
}
return previous.irisWorld() != incoming.irisWorld() || !previous.dimensionKey().equals(incoming.dimensionKey());
}
public IrisMessage.DimensionStatus status() {
return status;
}
public boolean irisWorld() {
IrisMessage.DimensionStatus current = status;
return current != null && current.irisWorld();
}
public void clear() {
status = null;
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.client;
import net.minecraft.client.gui.GuiGraphicsExtractor;
public final class IrisClientHud {
private IrisClientHud() {
}
public static void render(GuiGraphicsExtractor graphics) {
IrisToastPresenter.pump();
IrisPregenHud.render(graphics);
IrisWhatOverlay.render(graphics);
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.client;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
public final class IrisClientKeybinds {
private static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(IrisClient.KEYBIND_CATEGORY_ID);
public static final KeyMapping TOGGLE_HUD = new KeyMapping(IrisClient.KEYBIND_TOGGLE_HUD, GLFW.GLFW_KEY_H, CATEGORY);
public static final KeyMapping OPEN_MAP = new KeyMapping(IrisClient.KEYBIND_OPEN_MAP, GLFW.GLFW_KEY_M, CATEGORY);
public static final KeyMapping TOGGLE_WHAT = new KeyMapping(IrisClient.KEYBIND_TOGGLE_WHAT, GLFW.GLFW_KEY_J, CATEGORY);
private IrisClientKeybinds() {
}
public static KeyMapping.Category category() {
return CATEGORY;
}
public static void pollToggle() {
while (TOGGLE_HUD.consumeClick()) {
IrisClient.toggleHud();
}
while (TOGGLE_WHAT.consumeClick()) {
IrisClient.toggleWhat();
}
while (OPEN_MAP.consumeClick()) {
openVisionScreen();
}
}
private static void openVisionScreen() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.level == null || minecraft.player == null) {
return;
}
minecraft.setScreenAndShow(new IrisVisionScreen());
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientMarkers {
private final Map<IrisTileKey, List<IrisMessage.VisionMarkers.Marker>> byTile;
public IrisClientMarkers() {
this.byTile = new ConcurrentHashMap<>();
}
public void onMarkers(IrisMessage.VisionMarkers markers) {
byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers()));
}
public List<IrisMessage.VisionMarkers.Marker> forTile(IrisTileKey key) {
return byTile.get(key);
}
public void clear() {
byTile.clear();
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientPregenState {
private final ConcurrentHashMap<Long, IrisMessage.PregenProgress> jobs;
private volatile Long activeJobId;
public IrisClientPregenState() {
this.jobs = new ConcurrentHashMap<>();
this.activeJobId = null;
}
public void onProgress(IrisMessage.PregenProgress progress) {
jobs.put(progress.jobId(), progress);
activeJobId = progress.jobId();
}
public void onEnd(long jobId) {
jobs.remove(jobId);
Long current = activeJobId;
if (current != null && current == jobId) {
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
}
}
public IrisMessage.PregenProgress active() {
Long current = activeJobId;
if (current == null) {
return null;
}
return jobs.get(current);
}
public Long activeJobId() {
return activeJobId;
}
public void clear() {
jobs.clear();
activeJobId = null;
}
}
@@ -0,0 +1,118 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientRegionMap {
private static final int MAX_REGIONS = 4096;
private final ConcurrentHashMap<Long, Integer> states;
private volatile boolean tracking;
private volatile long jobId;
private volatile Bounds bounds;
public IrisClientRegionMap() {
this.states = new ConcurrentHashMap<>();
this.tracking = false;
this.jobId = 0L;
this.bounds = null;
}
public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) {
if (activeJobId == null) {
return;
}
long active = activeJobId;
if (delta.jobId() != active) {
return;
}
if (!tracking || jobId != delta.jobId()) {
resetTo(delta.jobId());
}
record(delta.regionX(), delta.regionZ(), delta.state());
}
public void onEnd(long endedJobId) {
if (tracking && jobId == endedJobId) {
clear();
}
}
public boolean hasData() {
return tracking && !states.isEmpty();
}
public Bounds bounds() {
return bounds;
}
public void forEachCell(CellConsumer consumer) {
for (Map.Entry<Long, Integer> entry : states.entrySet()) {
long packed = entry.getKey();
consumer.accept(regionX(packed), regionZ(packed), entry.getValue());
}
}
public void clear() {
states.clear();
bounds = null;
tracking = false;
jobId = 0L;
}
private void resetTo(long newJobId) {
states.clear();
bounds = null;
jobId = newJobId;
tracking = true;
}
private void record(int regionX, int regionZ, int state) {
long packed = pack(regionX, regionZ);
if (!states.containsKey(packed)) {
if (states.size() >= MAX_REGIONS) {
return;
}
Bounds current = bounds;
bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ);
}
states.put(packed, state);
}
private static long pack(int regionX, int regionZ) {
return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL);
}
private static int regionX(long packed) {
return (int) (packed >> 32);
}
private static int regionZ(long packed) {
return (int) packed;
}
@FunctionalInterface
public interface CellConsumer {
void accept(int regionX, int regionZ, int state);
}
public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
public Bounds union(int regionX, int regionZ) {
return new Bounds(
Math.min(minRegionX, regionX),
Math.min(minRegionZ, regionZ),
Math.max(maxRegionX, regionX),
Math.max(maxRegionZ, regionZ));
}
public int regionsWide() {
return maxRegionX - minRegionX + 1;
}
public int regionsTall() {
return maxRegionZ - minRegionZ + 1;
}
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
public final class IrisClientSession {
private static final long CLIENT_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR | IrisProtocol.CAPABILITY_STUDIO;
private volatile State state;
private volatile long serverCapabilities;
private volatile boolean irisActive;
private volatile String serverBrand;
private volatile ClientPacketSink sink;
public IrisClientSession() {
this.state = State.IDLE;
this.serverCapabilities = 0L;
this.irisActive = false;
this.serverBrand = "";
this.sink = null;
}
public void bind(ClientPacketSink boundSink) {
this.sink = boundSink;
}
public State state() {
return state;
}
public boolean isReady() {
return state == State.READY;
}
public boolean irisActive() {
return irisActive;
}
public long serverCapabilities() {
return serverCapabilities;
}
public String serverBrand() {
return serverBrand;
}
public void sendHello() {
ClientPacketSink activeSink = sink;
if (activeSink == null) {
return;
}
byte[] frame = IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, CLIENT_CAPABILITIES));
state = State.AWAITING_HELLO;
activeSink.send(frame);
}
public void onServerHello(IrisMessage.ServerHello hello) {
this.serverCapabilities = hello.capabilities();
this.irisActive = hello.irisActive();
this.serverBrand = hello.serverBrand();
this.state = State.READY;
}
public void reset() {
this.state = State.IDLE;
this.serverCapabilities = 0L;
this.irisActive = false;
this.serverBrand = "";
}
public enum State {
IDLE,
AWAITING_HELLO,
READY
}
}
@@ -0,0 +1,113 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.LongSupplier;
public final class IrisClientTileCache {
private static final int MAX_CACHED_TILES = 256;
private static final long REQUEST_RETRY_MILLIS = 3000L;
private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND;
private final ClientPacketSink sink;
private final LongSupplier clock;
private final IrisTileAssembler assembler;
private final LinkedHashMap<IrisTileKey, IrisTileImage> cache;
private final Map<IrisTileKey, Long> pending;
private final Deque<IrisTileKey> queue;
private final Set<IrisTileKey> queued;
private long windowStartMillis;
private int sentInWindow;
public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) {
this.sink = sink;
this.clock = clock;
this.assembler = new IrisTileAssembler();
this.cache = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, IrisTileImage> eldest) {
return size() > MAX_CACHED_TILES;
}
};
this.pending = new HashMap<>();
this.queue = new ArrayDeque<>();
this.queued = new HashSet<>();
this.windowStartMillis = 0L;
this.sentInWindow = 0;
}
public synchronized void onVisionTile(IrisMessage.VisionTile tile) {
IrisTileImage image = assembler.add(tile);
if (image == null) {
return;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
cache.put(key, image);
pending.remove(key);
queued.remove(key);
}
public synchronized IrisTileImage get(IrisTileKey key) {
return cache.get(key);
}
public synchronized void resetRequestQueue() {
queue.clear();
queued.clear();
}
public synchronized void request(IrisTileKey key) {
if (cache.containsKey(key)) {
return;
}
long now = clock.getAsLong();
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
return;
}
if (queued.add(key)) {
queue.addLast(key);
}
}
public synchronized void pump() {
long now = clock.getAsLong();
if (now - windowStartMillis >= 1000L) {
windowStartMillis = now;
sentInWindow = 0;
}
while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) {
IrisTileKey key = queue.pollFirst();
queued.remove(key);
if (cache.containsKey(key)) {
continue;
}
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
continue;
}
sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom())));
pending.put(key, now);
sentInWindow++;
}
}
public synchronized void clear() {
assembler.clear();
cache.clear();
pending.clear();
queue.clear();
queued.clear();
sentInWindow = 0;
windowStartMillis = 0L;
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.ArrayDeque;
public final class IrisClientToasts {
private static final int MAX_PENDING = 4;
private final ArrayDeque<Pending> pending;
public IrisClientToasts() {
this.pending = new ArrayDeque<>();
}
public void enqueue(int kind, String title, String body) {
Pending entry = new Pending(kind, normalize(title), normalize(body));
synchronized (pending) {
while (pending.size() >= MAX_PENDING) {
pending.pollFirst();
}
pending.addLast(entry);
}
}
public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) {
int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS;
enqueue(kind, "Studio Hotload", hotloadBody(packKey, changedFiles, failed, message));
}
public Pending poll() {
synchronized (pending) {
return pending.pollFirst();
}
}
public void clear() {
synchronized (pending) {
pending.clear();
}
}
private static String normalize(String value) {
return value == null ? "" : value;
}
private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) {
StringBuilder builder = new StringBuilder();
String pack = normalize(packKey);
if (!pack.isEmpty()) {
builder.append(pack);
}
if (changedFiles != 0) {
append(builder, changedFiles + (changedFiles == 1 ? " file" : " files"));
}
String text = normalize(message);
if (!text.isEmpty()) {
append(builder, text);
}
if (builder.isEmpty()) {
return failed ? "reload failed" : "reloaded";
}
return builder.toString();
}
private static void append(StringBuilder builder, String part) {
if (!builder.isEmpty()) {
builder.append(" ");
}
builder.append(part);
}
public record Pending(int kind, String title, String body) {
}
}
@@ -0,0 +1,153 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
public final class IrisPregenHud {
private static final int PANEL_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFD7D7D7;
private static final int BAR_BACK_COLOR = 0xFF2B2B2B;
private static final int BAR_RUNNING_COLOR = 0xFF66BB6A;
private static final int PAUSED_COLOR = 0xFFFFD54F;
private static final int GRID_BACK_COLOR = 0xFF161616;
private static final int CELL_PENDING_COLOR = 0xFF3A3A3A;
private static final int CELL_GENERATING_COLOR = 0xFFFFD54F;
private static final int CELL_DONE_COLOR = 0xFF66BB6A;
private static final int ORIGIN_X = 6;
private static final int ORIGIN_Y = 6;
private static final int PADDING = 4;
private static final int ROW_GAP = 2;
private static final int BAR_HEIGHT = 6;
private static final int MIN_WIDTH = 130;
private static final int MINIMAP_MAX_PX = 64;
private static final int MINIMAP_MAX_CELL = 6;
private static final int MINIMAP_GAP = 4;
private IrisPregenHud() {
}
public static void render(GuiGraphicsExtractor graphics) {
if (!IrisClient.hudVisible()) {
return;
}
IrisMessage.PregenProgress progress = IrisClient.pregen().active();
if (progress == null) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.player == null) {
return;
}
Font font = minecraft.font;
boolean paused = progress.state() == IrisMessage.PregenProgress.STATE_PAUSED;
double percent = progress.chunksTotal() > 0L
? clampPercent((double) progress.chunksDone() / (double) progress.chunksTotal() * 100.0D)
: 0.0D;
String title = "Iris Pregen";
String stats = String.format("%,d / %,d (%.1f%%)", progress.chunksDone(), progress.chunksTotal(), percent);
String tail = paused ? "PAUSED" : rateAndEta(progress);
int accent = paused ? PAUSED_COLOR : BAR_RUNNING_COLOR;
int lineHeight = font.lineHeight;
int contentWidth = Math.max(MIN_WIDTH, Math.max(font.width(title), Math.max(font.width(stats), font.width(tail))));
int contentHeight = lineHeight * 3 + ROW_GAP * 3 + BAR_HEIGHT;
IrisClientRegionMap regionMap = IrisClient.regions();
IrisClientRegionMap.Bounds bounds = regionMap.hasData() ? regionMap.bounds() : null;
boolean showMap = bounds != null;
int cellPx = 0;
int gridWidth = 0;
int gridHeight = 0;
if (showMap) {
int span = Math.max(bounds.regionsWide(), bounds.regionsTall());
cellPx = Math.max(1, Math.min(MINIMAP_MAX_CELL, MINIMAP_MAX_PX / span));
gridWidth = Math.min(MINIMAP_MAX_PX, bounds.regionsWide() * cellPx);
gridHeight = Math.min(MINIMAP_MAX_PX, bounds.regionsTall() * cellPx);
}
int panelWidth = showMap ? Math.max(contentWidth, gridWidth) : contentWidth;
int panelHeight = showMap ? contentHeight + MINIMAP_GAP + gridHeight : contentHeight;
graphics.fill(ORIGIN_X - PADDING, ORIGIN_Y - PADDING, ORIGIN_X + panelWidth + PADDING, ORIGIN_Y + panelHeight + PADDING, PANEL_COLOR);
int cursorY = ORIGIN_Y;
graphics.text(font, title, ORIGIN_X, cursorY, TITLE_COLOR);
cursorY += lineHeight + ROW_GAP;
graphics.text(font, stats, ORIGIN_X, cursorY, TEXT_COLOR);
cursorY += lineHeight + ROW_GAP;
int fillWidth = (int) Math.round(contentWidth * (percent / 100.0D));
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + contentWidth, cursorY + BAR_HEIGHT, BAR_BACK_COLOR);
if (fillWidth > 0) {
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + fillWidth, cursorY + BAR_HEIGHT, accent);
}
cursorY += BAR_HEIGHT + ROW_GAP;
graphics.text(font, tail, ORIGIN_X, cursorY, paused ? PAUSED_COLOR : MUTED_COLOR);
if (showMap) {
renderMinimap(graphics, regionMap, bounds, cellPx, gridWidth, gridHeight, ORIGIN_Y + contentHeight + MINIMAP_GAP);
}
}
private static void renderMinimap(GuiGraphicsExtractor graphics, IrisClientRegionMap regionMap, IrisClientRegionMap.Bounds bounds, int cellPx, int gridWidth, int gridHeight, int gridTop) {
int mapLeft = ORIGIN_X;
int mapRight = mapLeft + gridWidth;
int mapBottom = gridTop + gridHeight;
graphics.fill(mapLeft, gridTop, mapRight, mapBottom, GRID_BACK_COLOR);
int minRegionX = bounds.minRegionX();
int minRegionZ = bounds.minRegionZ();
regionMap.forEachCell((regionX, regionZ, state) -> {
int px = mapLeft + (regionX - minRegionX) * cellPx;
int py = gridTop + (regionZ - minRegionZ) * cellPx;
if (px + cellPx > mapRight || py + cellPx > mapBottom) {
return;
}
graphics.fill(px, py, px + cellPx, py + cellPx, cellColor(state));
});
}
private static int cellColor(int state) {
return switch (state) {
case IrisMessage.PregenRegionDelta.STATE_DONE -> CELL_DONE_COLOR;
case IrisMessage.PregenRegionDelta.STATE_GENERATING -> CELL_GENERATING_COLOR;
default -> CELL_PENDING_COLOR;
};
}
private static String rateAndEta(IrisMessage.PregenProgress progress) {
String rate = String.format("%,.0f/s", progress.chunksPerSecond());
if (progress.etaMillis() > 0L) {
return rate + " ETA " + formatDuration(progress.etaMillis());
}
return rate;
}
private static String formatDuration(long etaMillis) {
long totalSeconds = etaMillis / 1000L;
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
if (hours > 0L) {
return hours + "h " + minutes + "m";
}
if (minutes > 0L) {
return minutes + "m " + seconds + "s";
}
return seconds + "s";
}
private static double clampPercent(double value) {
if (value < 0.0D) {
return 0.0D;
}
if (value > 100.0D) {
return 100.0D;
}
return value;
}
}
@@ -0,0 +1,86 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.HashMap;
import java.util.Map;
final class IrisTileAssembler {
private final Map<IrisTileKey, Partial> partials;
IrisTileAssembler() {
this.partials = new HashMap<>();
}
IrisTileImage add(IrisMessage.VisionTile tile) {
int chunkCount = tile.chunkCount();
int chunkIndex = tile.chunkIndex();
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
return null;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
Partial partial = partials.get(key);
if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) {
if (partial != null && tile.sequence() < partial.sequence()) {
return null;
}
partial = new Partial(tile.sequence(), chunkCount);
partials.put(key, partial);
} else if (tile.sequence() < partial.sequence()) {
return null;
}
if (!partial.accept(chunkIndex, tile.data())) {
return null;
}
partials.remove(key);
return IrisTileCodec.decode(partial.concat());
}
void clear() {
partials.clear();
}
private static final class Partial {
private final int sequence;
private final int chunkCount;
private final byte[][] chunks;
private int received;
Partial(int sequence, int chunkCount) {
this.sequence = sequence;
this.chunkCount = chunkCount;
this.chunks = new byte[chunkCount][];
this.received = 0;
}
int sequence() {
return sequence;
}
int chunkCount() {
return chunkCount;
}
boolean accept(int chunkIndex, byte[] data) {
if (chunks[chunkIndex] == null) {
chunks[chunkIndex] = data;
received++;
}
return received >= chunkCount;
}
byte[] concat() {
int total = 0;
for (byte[] chunk : chunks) {
total += chunk.length;
}
byte[] blob = new byte[total];
int position = 0;
for (byte[] chunk : chunks) {
System.arraycopy(chunk, 0, blob, position, chunk.length);
position += chunk.length;
}
return blob;
}
}
}
@@ -0,0 +1,99 @@
package art.arcane.iris.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
public final class IrisTileCodec {
public static final int MODE_RAW_RGB = 0;
public static final int MODE_PALETTE = 1;
private static final int MAX_DIMENSION = 512;
private static final int OPAQUE = 0xFF000000;
private IrisTileCodec() {
}
public static IrisTileImage decode(byte[] deflatedBlob) {
if (deflatedBlob == null || deflatedBlob.length == 0) {
return null;
}
byte[] raw = inflate(deflatedBlob);
if (raw == null) {
return null;
}
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw))) {
int width = in.readInt();
int height = in.readInt();
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
return null;
}
int mode = in.readUnsignedByte();
int[] argb = new int[width * height];
return switch (mode) {
case MODE_PALETTE -> decodePalette(in, width, height, argb);
case MODE_RAW_RGB -> decodeRaw(in, width, height, argb);
default -> null;
};
} catch (IOException failure) {
return null;
}
}
private static IrisTileImage decodePalette(DataInputStream in, int width, int height, int[] argb) throws IOException {
int paletteSize = in.readInt();
if (paletteSize <= 0 || paletteSize > 256) {
return null;
}
int[] palette = new int[paletteSize];
for (int index = 0; index < paletteSize; index++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
palette[index] = OPAQUE | red << 16 | green << 8 | blue;
}
for (int pixel = 0; pixel < argb.length; pixel++) {
int paletteIndex = in.readUnsignedByte();
if (paletteIndex >= paletteSize) {
return null;
}
argb[pixel] = palette[paletteIndex];
}
return new IrisTileImage(width, height, argb);
}
private static IrisTileImage decodeRaw(DataInputStream in, int width, int height, int[] argb) throws IOException {
for (int pixel = 0; pixel < argb.length; pixel++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
argb[pixel] = OPAQUE | red << 16 | green << 8 | blue;
}
return new IrisTileImage(width, height, argb);
}
private static byte[] inflate(byte[] input) {
Inflater inflater = new Inflater();
inflater.setInput(input);
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2));
byte[] buffer = new byte[8192];
try {
while (!inflater.finished()) {
int produced = inflater.inflate(buffer);
if (produced == 0) {
if (inflater.finished() || inflater.needsInput() || inflater.needsDictionary()) {
break;
}
}
out.write(buffer, 0, produced);
}
} catch (DataFormatException malformed) {
return null;
} finally {
inflater.end();
}
return out.toByteArray();
}
}
@@ -0,0 +1,4 @@
package art.arcane.iris.client;
public record IrisTileImage(int width, int height, int[] argb) {
}
@@ -0,0 +1,4 @@
package art.arcane.iris.client;
public record IrisTileKey(int tileX, int tileZ, int zoom) {
}
@@ -0,0 +1,35 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastManager;
import net.minecraft.network.chat.Component;
public final class IrisToastPresenter {
private IrisToastPresenter() {
}
public static void pump() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.gui == null) {
return;
}
ToastManager manager = minecraft.gui.toastManager();
IrisClientToasts toasts = IrisClient.toasts();
IrisClientToasts.Pending next = toasts.poll();
while (next != null) {
SystemToast.addOrUpdate(manager, tokenFor(next.kind()), Component.literal(next.title()), Component.literal(next.body()));
next = toasts.poll();
}
}
private static SystemToast.SystemToastId tokenFor(int kind) {
return switch (kind) {
case IrisMessage.Toast.KIND_SUCCESS -> SystemToast.SystemToastId.WORLD_BACKUP;
case IrisMessage.Toast.KIND_WARNING -> SystemToast.SystemToastId.UNSECURE_SERVER_WARNING;
case IrisMessage.Toast.KIND_ERROR -> SystemToast.SystemToastId.PACK_LOAD_FAILURE;
default -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION;
};
}
}
@@ -0,0 +1,328 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class IrisVisionScreen extends Screen {
private static final int TILE_PIXELS = 128;
private static final int MIN_ZOOM = 0;
private static final int MAX_ZOOM = 8;
private static final int DEFAULT_ZOOM = 2;
private static final int MAX_TEXTURES = 220;
private static final int BACKGROUND_COLOR = 0xF00B0E14;
private static final int PLACEHOLDER_COLOR = 0xFF161A22;
private static final int GRID_COLOR = 0x33FFFFFF;
private static final int HEADER_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFB9B9B9;
private static final int PLAYER_FILL_COLOR = 0xFFFFDD33;
private static final int PLAYER_BORDER_COLOR = 0xFF101010;
private static final int MARKER_COLOR = 0xFF4FC3F7;
private static final int MARKER_LABEL_BG = 0xE0101010;
private final Map<IrisTileKey, TileTexture> textures;
private double centerBlockX;
private double centerBlockZ;
private int zoom;
private boolean initialized;
private String renderedDimensionKey;
public IrisVisionScreen() {
super(Component.literal("Iris Vision"));
this.textures = new LinkedHashMap<>(64, 0.75f, true);
this.centerBlockX = 0.0D;
this.centerBlockZ = 0.0D;
this.zoom = DEFAULT_ZOOM;
this.initialized = false;
this.renderedDimensionKey = null;
}
@Override
protected void init() {
if (!initialized) {
centerOnPlayer();
initialized = true;
}
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
graphics.fill(0, 0, width, height, BACKGROUND_COLOR);
IrisClientSession session = IrisClient.session();
if (!session.isReady()) {
drawCentered(graphics, "Connecting to Iris server...", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", "not connected");
return;
}
IrisMessage.DimensionStatus status = IrisClient.dimension().status();
if (!IrisClient.visionAvailable() || status == null) {
drawCentered(graphics, "Not an Iris world", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", status == null ? "no dimension data" : label(status));
return;
}
syncWorld(status);
renderTiles(graphics, mouseX, mouseY, status);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
return true;
}
@Override
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
int blocksPerPixel = 1 << zoom;
centerBlockX -= dragX * blocksPerPixel;
centerBlockZ -= dragY * blocksPerPixel;
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (scrollY == 0.0D) {
return false;
}
int previousBlocksPerPixel = 1 << zoom;
double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel;
double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel;
int updated = zoom + (scrollY > 0.0D ? -1 : 1);
zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM));
int newBlocksPerPixel = 1 << zoom;
centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel;
centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel;
return true;
}
@Override
public void removed() {
releaseTextures();
super.removed();
}
private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) {
int blocksPerPixel = 1 << zoom;
int tileSpanBlocks = TILE_PIXELS * blocksPerPixel;
int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel);
int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel);
int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1;
int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1;
int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1;
int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1;
int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks);
int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks);
IrisClientTileCache cache = IrisClient.tiles();
cache.resetRequestQueue();
List<IrisTileKey> missing = new ArrayList<>();
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
int screenX = originX + tileX * TILE_PIXELS;
int screenY = originY + tileZ * TILE_PIXELS;
IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom);
IrisTileImage image = cache.get(key);
if (image == null) {
graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR);
missing.add(key);
continue;
}
Identifier texture = ensureTexture(key, image);
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS);
}
}
requestMissing(cache, missing, centerTileX, centerTileZ);
cache.pump();
evictTextures();
drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel);
drawPlayer(graphics, blocksPerPixel);
drawHeader(graphics, "Iris Vision", label(status) + " zoom " + zoom + " x" + (long) centerBlockX + " z" + (long) centerBlockZ);
drawFooter(graphics, "Drag to pan Scroll to zoom Esc to close");
}
private void requestMissing(IrisClientTileCache cache, List<IrisTileKey> missing, int centerTileX, int centerTileZ) {
missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ)));
for (IrisTileKey key : missing) {
cache.request(key);
}
}
private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) {
IrisClientMarkers markers = IrisClient.markers();
String hoverLabel = null;
int hoverX = 0;
int hoverY = 0;
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
List<IrisMessage.VisionMarkers.Marker> tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom));
if (tileMarkers == null) {
continue;
}
for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) {
int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel);
int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel);
graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR);
if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) {
hoverLabel = marker.label();
hoverX = screenX;
hoverY = screenY;
}
}
}
}
if (hoverLabel != null) {
int labelWidth = font.width(hoverLabel);
graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG);
graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR);
}
}
private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel);
int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel);
graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR);
graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR);
}
private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) {
int lineHeight = font.lineHeight;
int headerHeight = lineHeight + 8;
graphics.fill(0, 0, width, headerHeight, HEADER_COLOR);
graphics.text(font, title, 8, 4, TITLE_COLOR);
graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR);
}
private void drawFooter(GuiGraphicsExtractor graphics, String hint) {
int lineHeight = font.lineHeight;
int footerTop = height - lineHeight - 8;
graphics.fill(0, footerTop, width, height, HEADER_COLOR);
graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR);
}
private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) {
graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color);
}
private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) {
TileTexture existing = textures.get(key);
if (existing != null) {
return existing.id();
}
int tileWidth = image.width();
int tileHeight = image.height();
int[] argb = image.argb();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false);
for (int y = 0; y < tileHeight; y++) {
int row = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x]));
}
}
DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage);
Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key));
Minecraft.getInstance().getTextureManager().register(id, texture);
textures.put(key, new TileTexture(id, texture));
return id;
}
private void evictTextures() {
if (textures.size() <= MAX_TEXTURES) {
return;
}
TextureManager manager = Minecraft.getInstance().getTextureManager();
Iterator<Map.Entry<IrisTileKey, TileTexture>> iterator = textures.entrySet().iterator();
while (textures.size() > MAX_TEXTURES && iterator.hasNext()) {
Map.Entry<IrisTileKey, TileTexture> entry = iterator.next();
manager.release(entry.getValue().id());
iterator.remove();
}
}
private void releaseTextures() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft != null) {
TextureManager manager = minecraft.getTextureManager();
for (TileTexture texture : textures.values()) {
manager.release(texture.id());
}
}
textures.clear();
}
private void syncWorld(IrisMessage.DimensionStatus status) {
if (renderedDimensionKey == null) {
renderedDimensionKey = status.dimensionKey();
return;
}
if (!renderedDimensionKey.equals(status.dimensionKey())) {
renderedDimensionKey = status.dimensionKey();
releaseTextures();
centerOnPlayer();
}
}
private void centerOnPlayer() {
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
centerBlockX = player.getBlockX();
centerBlockZ = player.getBlockZ();
}
}
private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) {
long deltaX = (long) key.tileX() - centerTileX;
long deltaZ = (long) key.tileZ() - centerTileZ;
return deltaX * deltaX + deltaZ * deltaZ;
}
private static int toAbgr(int argb) {
int alpha = argb >>> 24 & 0xFF;
int red = argb >> 16 & 0xFF;
int green = argb >> 8 & 0xFF;
int blue = argb & 0xFF;
return alpha << 24 | blue << 16 | green << 8 | red;
}
private static String texturePath(IrisTileKey key) {
return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom();
}
private static String safe(int value) {
return value < 0 ? "m" + (-(long) value) : Integer.toString(value);
}
private static String label(IrisMessage.DimensionStatus status) {
String pack = status.packKey() == null || status.packKey().isEmpty() ? "" : " pack " + status.packKey();
return status.dimensionKey() + pack;
}
private record TileTexture(Identifier id, DynamicTexture texture) {
}
}
@@ -0,0 +1,85 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.ArrayList;
import java.util.List;
public final class IrisWhatOverlay {
private static final int PANEL_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFC7C7C7;
private static final int PADDING = 4;
private static final int ROW_GAP = 2;
private static final int CURSOR_OFFSET = 12;
private IrisWhatOverlay() {
}
public static void render(GuiGraphicsExtractor graphics) {
if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.player == null) {
return;
}
LocalPlayer player = minecraft.player;
int blockX = player.getBlockX();
int blockZ = player.getBlockZ();
HitResult hit = minecraft.hitResult;
if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) {
BlockPos pos = blockHit.getBlockPos();
blockX = pos.getX();
blockZ = pos.getZ();
}
IrisClient.cursor().requestFor(blockX, blockZ);
IrisMessage.CursorInfo info = IrisClient.cursor().latest();
List<String> lines = new ArrayList<>();
if (info == null) {
lines.add("querying " + blockX + ", " + blockZ + "...");
} else {
lines.add("Biome: " + display(info.biomeKey()));
lines.add("Region: " + display(info.regionKey()));
if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) {
lines.add("Cave: " + display(info.caveBiomeKey()));
}
lines.add("Height: " + info.height() + " (" + info.blockX() + ", " + info.blockZ() + ")");
}
draw(graphics, minecraft.font, lines);
}
private static void draw(GuiGraphicsExtractor graphics, Font font, List<String> lines) {
int lineHeight = font.lineHeight;
int contentWidth = font.width("Iris What");
for (String line : lines) {
contentWidth = Math.max(contentWidth, font.width(line));
}
int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET;
int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET;
int rows = lines.size() + 1;
int contentHeight = lineHeight * rows + ROW_GAP * rows;
graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR);
int cursorY = originY;
graphics.text(font, "Iris What", originX, cursorY, TITLE_COLOR);
cursorY += lineHeight + ROW_GAP;
for (String line : lines) {
graphics.text(font, line, originX, cursorY, line.startsWith("Height") ? MUTED_COLOR : TEXT_COLOR);
cursorY += lineHeight + ROW_GAP;
}
}
private static String display(String key) {
return key == null || key.isEmpty() ? "-" : key;
}
}
+16 -12
View File
@@ -48,6 +48,7 @@ sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
@@ -100,18 +101,21 @@ configurations.create('jij') {
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
jij('net.fabricmc.fabric-api:fabric-api-base:2.0.4+ece063239e')
jij('net.fabricmc.fabric-api:fabric-registry-sync-v0:7.1.0+c7bd5b8e9e')
jij('net.fabricmc.fabric-api:fabric-resource-loader-v1:2.0.13+9edec1269e')
jij('net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.3+4575b05f9e')
jij('net.fabricmc.fabric-api:fabric-command-api-v2:3.1.0+00cb03469e')
jij('net.fabricmc.fabric-api:fabric-events-interaction-v0:5.2.6+8f57f7ee9e')
implementation('net.fabricmc.fabric-api:fabric-api-base:2.0.4+ece063239e')
implementation('net.fabricmc.fabric-api:fabric-registry-sync-v0:7.1.0+c7bd5b8e9e')
implementation('net.fabricmc.fabric-api:fabric-resource-loader-v1:2.0.13+9edec1269e')
implementation('net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.3+4575b05f9e')
implementation('net.fabricmc.fabric-api:fabric-command-api-v2:3.1.0+00cb03469e')
implementation('net.fabricmc.fabric-api:fabric-events-interaction-v0:5.2.6+8f57f7ee9e')
List<Object> fabricApi = [
libs.fabricApi.base,
libs.fabricApi.registrySync,
libs.fabricApi.resourceLoader,
libs.fabricApi.lifecycleEvents,
libs.fabricApi.commandApi,
libs.fabricApi.eventsInteraction,
libs.fabricApi.networking,
libs.fabricApi.rendering,
libs.fabricApi.keyMapping
]
fabricApi.each { Object notation ->
add('jij', notation)
add('implementation', notation)
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
@@ -0,0 +1,54 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
public final class FabricProtocolNetworking {
private FabricProtocolNetworking() {
}
public static void install() {
PayloadTypeRegistry.clientboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
PayloadTypeRegistry.serverboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
ServerPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
(payload, context) -> ModdedProtocolHandler.onInbound(context.player(), payload.data()));
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ModdedProtocolHandler.onPlayerJoin(handler.player));
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> ModdedProtocolHandler.onPlayerDisconnect(handler.player));
ModdedProtocolHandler.bindChannel(new FabricProtocolChannel());
}
private static final class FabricProtocolChannel implements ModdedProtocolChannel {
@Override
public boolean canReceive(ServerPlayer player) {
return ServerPlayNetworking.canSend(player, ModdedIrisPayload.TYPE);
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
ServerPlayNetworking.send(player, payload);
}
}
}
@@ -49,6 +49,7 @@ public final class IrisFabricBootstrap implements ModInitializer {
public void onInitialize() {
ModdedEngineBootstrap.bootCommon(new FabricModdedLoader(), "Fabric",
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
FabricProtocolNetworking.install();
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
@@ -0,0 +1,50 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.client.IrisClient;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.iris.client.IrisClientKeybinds;
import art.arcane.iris.modded.ModdedIrisPayload;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
public final class IrisFabricClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
(payload, context) -> IrisClient.onInbound(payload.data()));
IrisClient.bindSender(frame -> {
if (ClientPlayNetworking.canSend(ModdedIrisPayload.TYPE)) {
ClientPlayNetworking.send(new ModdedIrisPayload(frame));
}
});
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> IrisClient.onWorldJoin());
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> IrisClient.onDisconnect());
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_HUD);
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.OPEN_MAP);
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_WHAT);
HudElementRegistry.addLast(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics));
ClientTickEvents.END_CLIENT_TICK.register(client -> IrisClientKeybinds.pollToggle());
}
}
@@ -0,0 +1,35 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.modded.ModdedDeathLoot;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntity.class)
public class LivingEntityMixin {
@Inject(method = "die", at = @At("TAIL"))
private void iris$dropIrisDeathLoot(DamageSource damageSource, CallbackInfo info) {
ModdedDeathLoot.handle((LivingEntity) (Object) this);
}
}
@@ -13,7 +13,8 @@
"accessWidener": "irisworldgen.accesswidener",
"mixins": ["irisworldgen.mixins.json"],
"entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"]
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
"client": ["art.arcane.iris.fabric.IrisFabricClient"]
},
"jars": [
{ "file": "META-INF/jars/fabric-api-base.jar" },
@@ -22,10 +23,12 @@
{ "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" },
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
{ "file": "META-INF/jars/fabric-command-api-v2.jar" },
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" }
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" },
{ "file": "META-INF/jars/fabric-rendering-v1.jar" },
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }
],
"depends": {
"fabricloader": ">=0.19.0",
"fabricloader": ">=0.19.3",
"minecraft": "~${minecraftVersion}",
"java": ">=25"
}
@@ -4,6 +4,7 @@
"package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"LivingEntityMixin",
"ServerPacksSourceMixin"
],
"injectors": {
+1
View File
@@ -56,6 +56,7 @@ sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
@@ -0,0 +1,31 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.client.IrisClient;
import net.minecraftforge.event.network.CustomPayloadEvent;
public final class ForgeClientProtocol {
private ForgeClientProtocol() {
}
public static void handleInbound(CustomPayloadEvent.Context context, byte[] data) {
context.enqueueWork(() -> IrisClient.onInbound(data));
}
}
@@ -0,0 +1,86 @@
/*
* 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.forge;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import art.arcane.iris.spi.protocol.IrisProtocol;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.event.network.CustomPayloadEvent;
import net.minecraftforge.network.Channel;
import net.minecraftforge.network.ChannelBuilder;
import net.minecraftforge.network.NetworkProtocol;
import net.minecraftforge.network.PacketDistributor;
public final class ForgeProtocolNetworking {
private static volatile Channel<CustomPacketPayload> channel;
private ForgeProtocolNetworking() {
}
public static void register() {
Channel<CustomPacketPayload> boundChannel = ChannelBuilder.named(Identifier.parse(IrisProtocol.CHANNEL))
.optional()
.payloadChannel()
.protocol(NetworkProtocol.PLAY)
.bidirectional()
.add(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, ForgeProtocolNetworking::onPayload)
.build();
channel = boundChannel;
ModdedProtocolHandler.bindChannel(new ForgeProtocolChannel(boundChannel));
}
public static Channel<CustomPacketPayload> channel() {
return channel;
}
private static void onPayload(ModdedIrisPayload payload, CustomPayloadEvent.Context context) {
context.setPacketHandled(true);
if (context.isClientSide()) {
ForgeClientProtocol.handleInbound(context, payload.data());
return;
}
ServerPlayer player = context.getSender();
if (player == null) {
return;
}
ModdedProtocolHandler.onInbound(player, payload.data());
}
private static final class ForgeProtocolChannel implements ModdedProtocolChannel {
private final Channel<CustomPacketPayload> channel;
private ForgeProtocolChannel(Channel<CustomPacketPayload> channel) {
this.channel = channel;
}
@Override
public boolean canReceive(ServerPlayer player) {
return true;
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
channel.send(payload, PacketDistributor.PLAYER.with(player));
}
}
}
@@ -19,22 +19,29 @@
package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDeathLoot;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedProtocolHandler;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.event.AddPackFindersEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.registries.DeferredRegister;
@@ -49,14 +56,31 @@ public final class IrisForgeBootstrap {
chunkGenerators.register(context.getModBusGroup());
});
ForgeProtocolNetworking.register();
if (FMLEnvironment.dist == Dist.CLIENT) {
IrisForgeClient.init();
}
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerJoin(player);
}
});
PlayerEvent.PlayerLoggedOutEvent.BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerDisconnect(player);
}
});
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
if (event.getPackType() == PackType.SERVER_DATA) {
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
}
});
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
LivingDropsEvent.BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity()));
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
@@ -0,0 +1,58 @@
/*
* 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.forge;
import art.arcane.iris.client.IrisClient;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.iris.client.IrisClientKeybinds;
import art.arcane.iris.modded.ModdedIrisPayload;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.network.Channel;
import net.minecraftforge.network.PacketDistributor;
public final class IrisForgeClient {
private IrisForgeClient() {
}
public static void init() {
IrisClient.bindSender(IrisForgeClient::sendToServer);
AddGuiOverlayLayersEvent.BUS.addListener((AddGuiOverlayLayersEvent event) ->
event.getLayeredDraw().add(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics)));
RegisterKeyMappingsEvent.BUS.addListener((RegisterKeyMappingsEvent event) -> {
event.register(IrisClientKeybinds.TOGGLE_HUD);
event.register(IrisClientKeybinds.OPEN_MAP);
event.register(IrisClientKeybinds.TOGGLE_WHAT);
});
ClientPlayerNetworkEvent.LoggingIn.BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin());
ClientPlayerNetworkEvent.LoggingOut.BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect());
InputEvent.Key.BUS.addListener((InputEvent.Key event) -> IrisClientKeybinds.pollToggle());
}
private static void sendToServer(byte[] frame) {
Channel<CustomPacketPayload> channel = ForgeProtocolNetworking.channel();
if (channel == null) {
return;
}
channel.send(new ModdedIrisPayload(frame), PacketDistributor.SERVER.noArg());
}
}
@@ -1,5 +1,5 @@
modLoader = "javafml"
loaderVersion = "[64,)"
loaderVersion = "[65,)"
license = "GPL-3.0"
[[mods]]
@@ -12,7 +12,7 @@ authors = "Arcane Arts (Volmit Software)"
[[dependencies.irisworldgen]]
modId = "forge"
mandatory = true
versionRange = "[64,)"
versionRange = "[65,)"
ordering = "NONE"
side = "BOTH"
@@ -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"
}
+1
View File
@@ -48,6 +48,7 @@ sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
@@ -19,6 +19,7 @@
package art.arcane.iris.neoforge;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDeathLoot;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.command.IrisModdedCommands;
@@ -28,12 +29,15 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.living.LivingDropsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
@@ -55,9 +59,16 @@ public final class IrisNeoForgeBootstrap {
}
});
NeoForgeProtocolNetworking.register(modBus);
if (FMLEnvironment.getDist() == Dist.CLIENT) {
IrisNeoForgeClient.init(modBus);
}
NeoForge.EVENT_BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity()));
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> {
if (ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) {
event.setCanceled(true);
@@ -0,0 +1,56 @@
/*
* 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.neoforge;
import art.arcane.iris.client.IrisClient;
import art.arcane.iris.client.IrisClientHud;
import art.arcane.iris.client.IrisClientKeybinds;
import art.arcane.iris.modded.ModdedIrisPayload;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent;
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent;
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
public final class IrisNeoForgeClient {
private IrisNeoForgeClient() {
}
public static void registerClientbound(PayloadRegistrar registrar) {
registrar.playToClient(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC,
(payload, context) -> context.enqueueWork(() -> IrisClient.onInbound(payload.data())));
}
public static void init(IEventBus modBus) {
IrisClient.bindSender(frame -> ClientPacketDistributor.sendToServer(new ModdedIrisPayload(frame)));
modBus.addListener((RegisterGuiLayersEvent event) ->
event.registerAboveAll(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics)));
modBus.addListener((RegisterKeyMappingsEvent event) -> {
event.register(IrisClientKeybinds.TOGGLE_HUD);
event.register(IrisClientKeybinds.OPEN_MAP);
event.register(IrisClientKeybinds.TOGGLE_WHAT);
});
NeoForge.EVENT_BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin());
NeoForge.EVENT_BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect());
NeoForge.EVENT_BUS.addListener((ClientTickEvent.Post event) -> IrisClientKeybinds.pollToggle());
}
}
@@ -0,0 +1,82 @@
/*
* 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.neoforge;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.common.extensions.ICommonPacketListener;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
public final class NeoForgeProtocolNetworking {
private NeoForgeProtocolNetworking() {
}
public static void register(IEventBus modBus) {
modBus.addListener((RegisterPayloadHandlersEvent event) -> onRegisterPayloads(event));
NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerJoin(player);
}
});
NeoForge.EVENT_BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> {
if (event.getEntity() instanceof ServerPlayer player) {
ModdedProtocolHandler.onPlayerDisconnect(player);
}
});
ModdedProtocolHandler.bindChannel(new NeoForgeProtocolChannel());
}
private static void onRegisterPayloads(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("1").optional();
if (FMLEnvironment.getDist() == Dist.CLIENT) {
IrisNeoForgeClient.registerClientbound(registrar);
} else {
registrar.playToClient(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
}
registrar.playToServer(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, NeoForgeProtocolNetworking::onInbound);
}
private static void onInbound(ModdedIrisPayload payload, IPayloadContext context) {
if (context.player() instanceof ServerPlayer player) {
ModdedProtocolHandler.onInbound(player, payload.data());
}
}
private static final class NeoForgeProtocolChannel implements ModdedProtocolChannel {
@Override
public boolean canReceive(ServerPlayer player) {
return ((ICommonPacketListener) player.connection).hasChannel(ModdedIrisPayload.TYPE);
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
PacketDistributor.sendToPlayer(player, payload);
}
}
}
@@ -1,5 +1,5 @@
modLoader = "javafml"
loaderVersion = "[1,)"
loaderVersion = "[3,)"
license = "GPL-3.0"
[[mods]]
@@ -12,7 +12,7 @@ authors = "Arcane Arts (Volmit Software)"
[[dependencies.irisworldgen]]
modId = "neoforge"
type = "required"
versionRange = "[1,)"
versionRange = "[26.2,)"
ordering = "NONE"
side = "BOTH"
+1
View File
@@ -201,6 +201,7 @@ def generateTemplates = tasks.register('generateTemplates', Copy) {
logger.error('Git commit hash not found', failure)
return 'unknown'
},
minecraftVersion: providers.gradleProperty('minecraftVersion'),
])
from(templateSource)
-9
View File
@@ -7,7 +7,6 @@ art/arcane/iris/core/edit/DustRevealer.java
art/arcane/iris/core/events/IrisEngineEvent.java
art/arcane/iris/core/events/IrisEngineHotloadEvent.java
art/arcane/iris/core/events/IrisLootEvent.java
art/arcane/iris/core/gui/PregeneratorJob.java
art/arcane/iris/core/lifecycle/BukkitPublicBackend.java
art/arcane/iris/core/lifecycle/CapabilitySnapshot.java
art/arcane/iris/core/lifecycle/PaperLibBootstrap.java
@@ -36,16 +35,13 @@ art/arcane/iris/core/link/data/NexoDataProvider.java
art/arcane/iris/core/nms/INMS.java
art/arcane/iris/core/nms/INMSBinding.java
art/arcane/iris/core/nms/MinecraftVersion.java
art/arcane/iris/core/nms/datapack/v1206/DataFixerV1206.java
art/arcane/iris/core/nms/v1X/NMSBinding1X.java
art/arcane/iris/core/pack/IrisPack.java
art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java
art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java
art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java
art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java
art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java
art/arcane/iris/core/project/IrisProject.java
art/arcane/iris/core/project/SchemaBuilder.java
art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java
art/arcane/iris/core/runtime/ChunkClearer.java
art/arcane/iris/core/runtime/ChunkJobReporter.java
@@ -78,25 +74,20 @@ art/arcane/iris/core/tools/IrisWorldCreator.java
art/arcane/iris/core/tools/TreePlausibilizer.java
art/arcane/iris/engine/IrisEngineEffects.java
art/arcane/iris/engine/IrisWorldManager.java
art/arcane/iris/engine/actuator/IrisBiomeActuator.java
art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java
art/arcane/iris/engine/data/chunk/TerrainChunk.java
art/arcane/iris/engine/decorator/DecoratorCore.java
art/arcane/iris/engine/framework/Engine.java
art/arcane/iris/engine/framework/EngineAssignedWorldManager.java
art/arcane/iris/engine/framework/EnginePlayer.java
art/arcane/iris/engine/framework/EngineWorldManager.java
art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java
art/arcane/iris/engine/mantle/MantleWriter.java
art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java
art/arcane/iris/engine/object/BlockDataMergeSupport.java
art/arcane/iris/engine/object/IPostBlockAccess.java
art/arcane/iris/engine/object/IrisAttributeModifier.java
art/arcane/iris/engine/object/IrisBiome.java
art/arcane/iris/engine/object/IrisBiomeCustom.java
art/arcane/iris/engine/object/IrisBiomeCustomParticle.java
art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java
art/arcane/iris/engine/object/IrisBlockData.java
art/arcane/iris/engine/object/IrisBlockDrops.java
art/arcane/iris/engine/object/IrisCommandRegistry.java
art/arcane/iris/engine/object/IrisCompat.java
@@ -73,6 +73,7 @@ public class IrisSettings {
try {
String ss = IO.readAll(s);
settings = new Gson().fromJson(ss, IrisSettings.class);
migrateLegacyKeys(ss);
try {
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
} catch (IOException e) {
@@ -87,6 +88,17 @@ public class IrisSettings {
return settings;
}
private static void migrateLegacyKeys(String rawJson) {
JSONObject root = new JSONObject(rawJson);
JSONObject worldObject = root.optJSONObject("world");
if (worldObject == null || !worldObject.has("anbientEntitySpawningSystem")) {
return;
}
settings.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", settings.getWorld().isAmbientEntitySpawningSystem()));
IrisLogging.info("Migrated legacy settings key world.anbientEntitySpawningSystem -> world.ambientEntitySpawningSystem");
}
public static void invalidate() {
synchronized (settings) {
settings = null;
@@ -115,7 +127,7 @@ public class IrisSettings {
public static class IrisSettingsWorld {
public boolean postLoadBlockUpdates = true;
public boolean forcePersistEntities = true;
public boolean anbientEntitySpawningSystem = true;
public boolean ambientEntitySpawningSystem = true;
public long asyncTickIntervalMS = 700;
public double targetSpawnEntitiesPerChunk = 0.95;
public boolean markerEntitySpawningSystem = true;
@@ -8,7 +8,10 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Getter;
import org.bukkit.*;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@@ -19,7 +19,10 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.pregenerator.IrisPregenerator;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
@@ -345,6 +348,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
public void onRegionGenerated(int x, int z) {
shouldGc();
rgc++;
broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_DONE);
}
private void shouldGc() {
@@ -353,9 +357,17 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
}
}
private void broadcastRegionDelta(int regionX, int regionZ, int state) {
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer == null) {
return;
}
protocolServer.broadcastPregenRegionDelta(pregenerator.getJobId(), regionX, regionZ, state);
}
@Override
public void onRegionGenerating(int x, int z) {
broadcastRegionDelta(x, z, IrisMessage.PregenRegionDelta.STATE_GENERATING);
}
@Override
@@ -21,6 +21,8 @@ package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
@@ -154,6 +156,11 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
});
public VisionGUI(JFrame frame) {
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null) {
preservation.register(e);
preservation.register(eh);
}
m.set(8);
rs.put(1);
setBackground(BG);
@@ -22,7 +22,11 @@ import org.bukkit.event.EventHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
@@ -18,7 +18,13 @@
package art.arcane.iris.core.loader;
import com.google.gson.*;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
@@ -27,7 +33,22 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisImage;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisMod;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.engine.object.matter.IrisMatterObject;
import art.arcane.volmlib.util.collection.KList;
@@ -48,7 +69,11 @@ import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@Data
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@@ -25,7 +25,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.awt.*;
import java.awt.Desktop;
import java.io.File;
@Data
@@ -46,13 +46,19 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.io.*;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@@ -74,6 +80,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return thread;
});
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
private static final AtomicBoolean schemaBuildExecutorRegistered = new AtomicBoolean();
protected final AtomicCache<KList<File>> folderCache;
protected KSet<String> firstAccess;
protected File root;
@@ -101,6 +108,10 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getResourceLoaderCacheSize());
IrisLogging.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath());
IrisServices.get(PreservationRegistry.class).registerCache(this);
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null && schemaBuildExecutorRegistered.compareAndSet(false, true)) {
preservation.register(schemaBuildExecutor);
}
}
public JSONObject buildSchema() {
@@ -4,7 +4,13 @@ import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
public class BlockProperty {
@@ -1,5 +1,7 @@
package art.arcane.iris.core.nms.datapack.v1192;
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.FALSE;
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
import art.arcane.volmlib.util.json.JSONObject;
@@ -7,7 +9,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.Map;
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.*;
public class DataFixerV1192 implements IDataFixer {
private static final Map<Dimension, IrisDimensionTypeOptions> OPTIONS = Map.of(
@@ -32,7 +32,11 @@ import art.arcane.volmlib.util.math.Vector3d;
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import org.bukkit.*;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
@@ -0,0 +1,224 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pack;
import art.arcane.iris.spi.PlatformRegistries;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class ContentKeyValidator {
private static final int MAX_SUGGESTION_SCANS = 4096;
private static final String DEFAULT_NAMESPACE = "minecraft";
private ContentKeyValidator() {
}
public enum ContentRegistry {
BLOCK,
ITEM,
ENTITY;
public String label() {
return name().toLowerCase(Locale.ROOT);
}
}
public record ContentKeyError(String key, ContentRegistry registry, boolean namespaceLoaded, String suggestion) {
public String message() {
StringBuilder sb = new StringBuilder(64);
sb.append("Unknown ").append(registry.label()).append(" key '").append(key).append('\'');
sb.append(" (missing from the ").append(registry.label()).append(" registry");
if (!namespaceLoaded) {
sb.append("; namespace '").append(namespaceOf(key)).append("' is not loaded on this server");
}
sb.append(')');
if (suggestion != null) {
sb.append(" - did you mean '").append(suggestion).append("'?");
}
return sb.toString();
}
}
public static List<ContentKeyError> validate(PlatformRegistries registries,
Collection<String> referencedBlocks,
Collection<String> referencedItems,
Collection<String> referencedEntities) {
if (registries == null) {
return List.of();
}
List<String> blockKeys = normalizeKeyList(registries.blockKeys());
List<String> itemKeys = normalizeKeyList(registries.itemKeys());
List<String> entityKeys = normalizeKeyList(registries.entityKeys());
Set<String> knownBlocks = new HashSet<>(blockKeys);
Set<String> knownItems = new HashSet<>(itemKeys);
Set<String> knownEntities = new HashSet<>(entityKeys);
Set<String> loadedNamespaces = new HashSet<>();
addNamespaces(blockKeys, loadedNamespaces);
addNamespaces(itemKeys, loadedNamespaces);
addNamespaces(entityKeys, loadedNamespaces);
Map<String, ContentKeyError> errors = new LinkedHashMap<>();
validateCategory(referencedBlocks, ContentRegistry.BLOCK, knownBlocks, blockKeys, loadedNamespaces, errors);
validateCategory(referencedItems, ContentRegistry.ITEM, knownItems, itemKeys, loadedNamespaces, errors);
validateCategory(referencedEntities, ContentRegistry.ENTITY, knownEntities, entityKeys, loadedNamespaces, errors);
return List.copyOf(errors.values());
}
static String namespaceOf(String key) {
int colon = key.indexOf(':');
return colon < 0 ? DEFAULT_NAMESPACE : key.substring(0, colon);
}
private static void validateCategory(Collection<String> referenced,
ContentRegistry registry,
Set<String> known,
List<String> candidatePool,
Set<String> loadedNamespaces,
Map<String, ContentKeyError> errors) {
if (referenced == null || referenced.isEmpty()) {
return;
}
for (String raw : referenced) {
String normalized = normalizeKey(raw);
if (normalized == null || known.contains(normalized)) {
continue;
}
String dedupKey = registry.name() + '|' + normalized;
if (errors.containsKey(dedupKey)) {
continue;
}
boolean namespaceLoaded = loadedNamespaces.contains(namespaceOf(normalized));
String suggestion = nearestKey(normalized, candidatePool);
errors.put(dedupKey, new ContentKeyError(normalized, registry, namespaceLoaded, suggestion));
}
}
private static List<String> normalizeKeyList(List<String> keys) {
if (keys == null || keys.isEmpty()) {
return List.of();
}
List<String> out = new ArrayList<>(keys.size());
for (String key : keys) {
if (key == null || key.isEmpty()) {
continue;
}
String value = key.trim().toLowerCase(Locale.ROOT);
if (!value.isEmpty()) {
out.add(value.indexOf(':') < 0 ? DEFAULT_NAMESPACE + ':' + value : value);
}
}
return out;
}
private static void addNamespaces(List<String> keys, Set<String> namespaces) {
for (String key : keys) {
namespaces.add(namespaceOf(key));
}
}
private static String normalizeKey(String raw) {
if (raw == null) {
return null;
}
String value = raw.trim().toLowerCase(Locale.ROOT);
int bracket = value.indexOf('[');
if (bracket >= 0) {
value = value.substring(0, bracket).trim();
}
if (value.isEmpty()) {
return null;
}
return value.indexOf(':') < 0 ? DEFAULT_NAMESPACE + ':' + value : value;
}
private static String nearestKey(String key, List<String> candidatePool) {
String path = pathOf(key);
int maxDistance = Math.max(2, path.length() / 3);
int bestDistance = Integer.MAX_VALUE;
String best = null;
int scans = 0;
for (String candidate : candidatePool) {
String candidatePath = pathOf(candidate);
if (Math.abs(candidatePath.length() - path.length()) > maxDistance) {
continue;
}
if (scans++ >= MAX_SUGGESTION_SCANS) {
break;
}
int distance = boundedLevenshtein(path, candidatePath, maxDistance);
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
if (distance <= 1) {
break;
}
}
}
return bestDistance <= maxDistance ? best : null;
}
private static String pathOf(String key) {
int colon = key.indexOf(':');
return colon < 0 ? key : key.substring(colon + 1);
}
private static int boundedLevenshtein(String a, String b, int max) {
int la = a.length();
int lb = b.length();
if (Math.abs(la - lb) > max) {
return max + 1;
}
int[] prev = new int[lb + 1];
int[] curr = new int[lb + 1];
for (int j = 0; j <= lb; j++) {
prev[j] = j;
}
for (int i = 1; i <= la; i++) {
curr[0] = i;
int rowMin = curr[0];
char ca = a.charAt(i - 1);
for (int j = 1; j <= lb; j++) {
int cost = ca == b.charAt(j - 1) ? 0 : 1;
curr[j] = Math.min(Math.min(prev[j] + 1, curr[j - 1] + 1), prev[j - 1] + cost);
if (curr[j] < rowMin) {
rowMin = curr[j];
}
}
if (rowMin > max) {
return max + 1;
}
int[] tmp = prev;
prev = curr;
curr = tmp;
}
return prev[lb];
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
@@ -35,6 +37,7 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Stream;
@@ -98,9 +101,126 @@ public final class PackValidator {
warnings.add("Unused-resource GC pass failed: " + e.getMessage());
}
runContentKeyValidation(packFolder, warnings);
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
private static void runContentKeyValidation(File packFolder, List<String> warnings) {
try {
if (!IrisPlatforms.isBound()) {
return;
}
PlatformRegistries registries = IrisPlatforms.get().registries();
if (registries == null) {
return;
}
List<String> blockKeys = registries.blockKeys();
List<String> itemKeys = registries.itemKeys();
List<String> entityKeys = registries.entityKeys();
if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) {
return;
}
ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder);
List<ContentKeyValidator.ContentKeyError> errors = ContentKeyValidator.validate(
registries, referenced.blocks(), referenced.items(), referenced.entities());
for (ContentKeyValidator.ContentKeyError error : errors) {
warnings.add(error.message());
}
} catch (Throwable e) {
IrisLogging.reportError("PackValidator content-key validation failed for pack '" + packFolder.getName() + "'", e);
}
}
private static ReferencedContentKeys collectReferencedContentKeys(File packFolder) {
Set<String> blocks = new HashSet<>();
Set<String> items = new HashSet<>();
Set<String> entities = new HashSet<>();
Set<String> customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks"));
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
List<Path> files = stream.filter(Files::isRegularFile)
.filter(PackValidator::isScannableJsonPath)
.toList();
for (Path path : files) {
String relative = packFolder.toPath().relativize(path).toString().replace(File.separatorChar, '/');
boolean inLoot = relative.startsWith("loot/");
boolean inEntities = relative.startsWith("entities/");
JSONObject json;
try {
json = new JSONObject(Files.readString(path, StandardCharsets.UTF_8));
} catch (Throwable ignored) {
continue;
}
collectFromNode(json, blocks, inLoot ? items : null, inEntities ? entities : null, customBlocks);
}
} catch (Throwable e) {
IrisLogging.reportError("PackValidator failed to walk pack for content-key extraction", e);
}
return new ReferencedContentKeys(blocks, items, entities);
}
private static void collectFromNode(Object node, Set<String> blocks, Set<String> items, Set<String> entities, Set<String> customBlocks) {
if (node instanceof JSONObject obj) {
for (String key : obj.keySet()) {
Object value = obj.get(key);
if (value instanceof String str) {
if ("block".equals(key)) {
addBlockRef(str, blocks, customBlocks);
} else if (items != null && "type".equals(key)) {
addSimpleRef(str, items);
} else if (entities != null && "type".equals(key)) {
addSimpleRef(str, entities);
}
} else {
collectFromNode(value, blocks, items, entities, customBlocks);
}
}
} else if (node instanceof JSONArray arr) {
for (int i = 0; i < arr.length(); i++) {
collectFromNode(arr.get(i), blocks, items, entities, customBlocks);
}
}
}
private static void addBlockRef(String raw, Set<String> blocks, Set<String> customBlocks) {
String value = raw.trim().toLowerCase(Locale.ROOT);
int bracket = value.indexOf('[');
if (bracket >= 0) {
value = value.substring(0, bracket).trim();
}
if (value.isEmpty() || customBlocks.contains(value)) {
return;
}
blocks.add(value);
}
private static void addSimpleRef(String raw, Set<String> target) {
String value = raw.trim().toLowerCase(Locale.ROOT);
if (!value.isEmpty()) {
target.add(value);
}
}
private static Set<String> deriveRegistrantKeys(File folder) {
Set<String> keys = new HashSet<>();
if (!folder.isDirectory()) {
return keys;
}
for (File file : listJsonRecursive(folder)) {
String key = deriveKey(folder, file);
if (key != null && !key.isBlank()) {
keys.add(key.toLowerCase(Locale.ROOT));
}
}
return keys;
}
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
}
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes");
@@ -19,6 +19,9 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.tools.IrisPackBenchmarking;
import art.arcane.volmlib.util.collection.KList;
@@ -41,6 +44,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class IrisPregenerator {
private static final double INVALID = 9223372036854775807d;
private static final AtomicLong JOB_SEQUENCE = new AtomicLong(0L);
private final long jobId;
private final PregenTask task;
private final PregeneratorMethod generator;
private final PregenListener listener;
@@ -72,6 +77,7 @@ public class IrisPregenerator {
private final IrisPackBenchmarking benchmarking;
public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) {
this.jobId = JOB_SEQUENCE.incrementAndGet();
benchmarking = IrisPackBenchmarking.getInstance();
this.listener = listenify(listener);
cl = new ChronoLatch(10000);
@@ -138,6 +144,17 @@ public class IrisPregenerator {
totalChunks.get() - generated.get(), eta, M.ms() - startTime.get(), currentGeneratorMethod.get(),
cached);
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer != null) {
protocolServer.broadcastPregenProgress(
jobId,
generated.get(),
totalChunks.get(),
cached ? cachedPerSecond.getAverage() : chunksPerSecond.getAverage(),
eta,
paused.get() ? IrisMessage.PregenProgress.STATE_PAUSED : IrisMessage.PregenProgress.STATE_RUNNING);
}
if (cl.flip()) {
double percentage = ((double) generated.get() / (double) totalChunks.get()) * 100;
@@ -172,6 +189,10 @@ public class IrisPregenerator {
}
public long getJobId() {
return jobId;
}
public void close() {
shutdown.set(true);
}
@@ -219,6 +240,11 @@ public class IrisPregenerator {
generator.close();
ticker.interrupt();
listener.onClose();
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
if (protocolServer != null) {
long total = totalChunks.get();
protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total);
}
Mantle mantle = getMantle();
if (mantle != null) {
reclaimTectonicPlates(mantle);
@@ -1,6 +1,8 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.data.Varint;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.documentation.RegionCoordinates;
@@ -23,9 +25,11 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class PregenCacheImpl implements PregenCache {
private static final ExecutorService DISPATCHER = Executors.newFixedThreadPool(4);
private static final AtomicBoolean DISPATCHER_REGISTERED = new AtomicBoolean();
private static final short SIZE = 1024;
private final File directory;
@@ -35,6 +39,10 @@ public class PregenCacheImpl implements PregenCache {
public PregenCacheImpl(File directory, int maxSize) {
this.directory = directory;
this.maxSize = maxSize;
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null && DISPATCHER_REGISTERED.compareAndSet(false, true)) {
preservation.register(DISPATCHER);
}
}
@Override
@@ -23,7 +23,9 @@ import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -63,6 +65,10 @@ public class MedievalPregenMethod implements PregeneratorMethod {
this.prefetchDisabled = new AtomicBoolean(J.isFolia());
int prefetchThreads = J.isFolia() ? 0 : Math.max(2, Math.min(16, configuredThreads));
this.prefetchPool = prefetchThreads > 0 ? newPrefetchPool(prefetchThreads) : null;
PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class);
if (preservation != null && prefetchPool != null) {
preservation.register(prefetchPool);
}
}
private static ExecutorService newPrefetchPool(int threads) {
@@ -27,7 +27,16 @@ import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
@@ -56,7 +65,8 @@ import org.dom4j.Document;
import org.dom4j.Element;
import org.zeroturnaround.zip.ZipUtil;
import java.awt.*;
import java.awt.Desktop;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
@@ -18,10 +18,11 @@
package art.arcane.iris.core.project;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformNumericRange;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.link.data.DataType;
import art.arcane.iris.core.loader.IrisData;
@@ -29,23 +30,37 @@ import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.structure.StructureSchemaKeys;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.framework.ListFunction;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
import art.arcane.iris.engine.object.annotations.RegistryListFont;
import art.arcane.iris.engine.object.annotations.RegistryListFunction;
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity;
import art.arcane.iris.engine.object.annotations.RegistryListStructure;
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructureSet;
import art.arcane.iris.engine.object.annotations.RegistryMapBlockState;
import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.util.common.reflect.KeyedType;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.GraphicsEnvironment;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -55,13 +70,14 @@ import java.util.function.Function;
public class SchemaBuilder {
private static final String SYMBOL_LIMIT__N = "*";
private static final String SYMBOL_TYPE__N = "";
private static final JSONArray POTION_TYPES = getPotionTypes();
private static final JSONArray ENCHANT_TYPES = getEnchantTypes();
private static final String MINECRAFT_NAMESPACE = "minecraft:";
private static final JSONArray FONT_TYPES = new JSONArray(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
private final KMap<String, JSONObject> definitions;
private final Class<?> root;
private final KList<String> warnings;
private final IrisData data;
private JSONArray potionTypes;
private JSONArray enchantTypes;
public SchemaBuilder(Class<?> root, IrisData data) {
this.data = data;
@@ -70,30 +86,6 @@ public class SchemaBuilder {
this.root = root;
}
private static JSONArray getPotionTypes() {
JSONArray a = new JSONArray();
for (PotionEffectType gg : Registry.EFFECT) {
NamespacedKey key = KeyedType.getKey(gg);
if (key != null) {
a.put(key.getKey().toUpperCase(Locale.ROOT).replaceAll("\\Q \\E", "_"));
}
}
return a;
}
private static JSONArray getEnchantTypes() {
JSONArray array = new JSONArray();
for (Enchantment e : Registry.ENCHANTMENT) {
NamespacedKey key = KeyedType.getKey(e);
if (key != null) {
array.put(key.getKey());
}
}
return array;
}
public JSONObject construct() {
JSONObject schema = new JSONObject();
schema.put("$schema", "http://json-schema.org/draft-07/schema#");
@@ -122,6 +114,52 @@ public class SchemaBuilder {
return schema;
}
private JSONArray potionTypes() {
if (potionTypes == null) {
JSONArray a = new JSONArray();
for (String key : IrisPlatforms.get().registries().potionEffectKeys()) {
a.put(stripNamespace(key).toUpperCase(Locale.ROOT).replace(" ", "_"));
}
potionTypes = a;
}
return potionTypes;
}
private JSONArray enchantTypes() {
if (enchantTypes == null) {
JSONArray a = new JSONArray();
for (String key : IrisPlatforms.get().registries().enchantmentKeys()) {
a.put(stripNamespace(key));
}
enchantTypes = a;
}
return enchantTypes;
}
private JSONArray itemTypes() {
JSONArray a = new JSONArray();
for (String key : IrisPlatforms.get().registries().itemKeys()) {
a.put(key.startsWith(MINECRAFT_NAMESPACE) ? key.substring(MINECRAFT_NAMESPACE.length()) : key);
}
return a;
}
private JSONArray blockTypes() {
JSONArray a = new JSONArray();
for (String i : data.getBlockLoader().getPossibleKeys()) {
a.put(i);
}
for (String i : IrisPlatforms.get().registries().blockTypeKeys()) {
a.put(i);
}
return a;
}
private static String stripNamespace(String key) {
int idx = key.indexOf(':');
return idx < 0 ? key : key.substring(idx + 1);
}
private JSONObject buildProperties(Class<?> c) {
JSONObject o = new JSONObject();
JSONObject properties = new JSONObject();
@@ -132,7 +170,7 @@ public class SchemaBuilder {
JSONArray required = new JSONArray();
JSONArray extended = new JSONArray();
var parent = c.getSuperclass();
Class<?> parent = c.getSuperclass();
while (parent != null && IrisRegistrant.class.isAssignableFrom(parent)) {
buildProperties(properties, required, extended, parent);
parent = parent.getSuperclass();
@@ -255,7 +293,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
INMS.get().getStructureKeys(),
IrisPlatforms.get().registries().structureKeys(),
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
definitions.put(key, j);
@@ -270,17 +308,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : data.getBlockLoader().getPossibleKeys()) {
ja.put(i);
}
for (String i : BukkitBlockResolution.getBlockTypes()) {
ja.put(i);
}
j.put("enum", ja);
j.put("enum", blockTypes());
definitions.put(key, j);
}
@@ -295,7 +323,7 @@ public class SchemaBuilder {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : INMS.get().getStructureKeys()) {
for (String i : IrisPlatforms.get().registries().structureKeys()) {
ja.put(i);
}
@@ -314,7 +342,7 @@ public class SchemaBuilder {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : INMS.get().getStructureSetKeys()) {
for (String i : IrisPlatforms.get().structureHooks().structureSetKeys()) {
ja.put(i);
}
@@ -331,7 +359,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", BukkitBlockResolution.getItemTypes());
j.put("enum", itemTypes());
definitions.put(key, j);
}
@@ -339,6 +367,25 @@ public class SchemaBuilder {
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListEntityType.class)) {
String key = "enum-entity-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : IrisPlatforms.get().registries().entityKeys()) {
ja.put(i);
}
j.put("enum", ja);
definitions.put(key, j);
}
fancyType = "Entity Type";
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListSpecialEntity.class)) {
String key = "enum-reg-specialentity";
@@ -374,7 +421,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", ENCHANT_TYPES);
j.put("enum", enchantTypes());
definitions.put(key, j);
}
@@ -386,7 +433,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
j.put("enum", potionTypes());
definitions.put(key, j);
}
@@ -394,9 +441,9 @@ public class SchemaBuilder {
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
Class<? extends ListFunction<KList<String>>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
try {
var instance = functionClass.getDeclaredConstructor().newInstance();
ListFunction<KList<String>> instance = functionClass.getDeclaredConstructor().newInstance();
String key = instance.key();
fancyType = instance.fancyName();
@@ -411,27 +458,13 @@ public class SchemaBuilder {
} catch (Throwable e) {
IrisLogging.error("Could not execute apply method in " + functionClass.getName());
}
} else if (k.getType().equals(PotionEffectType.class)) {
String key = "enum-potion-effect-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
definitions.put(key, j);
}
fancyType = "Potion Effect Type";
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (KeyedType.isKeyed(k.getType())) {
fancyType = addEnum(k.getType(), prop, description, KeyedType.values(k.getType()), Function.identity());
} else if (SchemaKeyedTypes.isKeyed(k.getType())) {
fancyType = addEnum(k.getType(), prop, description, SchemaKeyedTypes.values(k.getType()), Function.identity());
} else if (k.getType().isEnum()) {
fancyType = addEnum(k.getType(), prop, description, k.getType().getEnumConstants(), o -> ((Enum<?>) o).name());
}
}
case "object" -> {
//TODO add back descriptions
if (k.isAnnotationPresent(RegistryMapBlockState.class)) {
String blockType = k.getDeclaredAnnotation(RegistryMapBlockState.class).value();
fancyType = "Block State";
@@ -439,10 +472,13 @@ public class SchemaBuilder {
JSONArray any = new JSONArray();
prop.put("anyOf", any);
BukkitBlockResolution.getBlockStates().forEach((blocks, properties) -> {
if (blocks.isEmpty()) return;
for (BlockStateGroup group : reconstructBlockStateGroups()) {
List<String> blocks = group.blocks();
if (blocks.isEmpty()) {
continue;
}
String raw = blocks.getFirst().replace(':', '_');
String raw = blocks.get(0).replace(':', '_');
String enumKey = "enum-block-state-" + raw;
String propertiesKey = "obj-block-state-" + raw;
@@ -470,15 +506,15 @@ public class SchemaBuilder {
if (!definitions.containsKey(propertiesKey)) {
JSONObject props = new JSONObject();
properties.forEach(property -> {
props.put(property.name(), property.buildJson());
});
for (PlatformBlockProperty property : group.properties()) {
props.put(property.name(), buildBlockPropertyJson(property));
}
definitions.put(propertiesKey, new JSONObject()
.put("type", "object")
.put("properties", props));
}
});
}
} else {
fancyType = k.getType().getSimpleName().replaceAll("\\QIris\\E", "") + " (Object)";
String key = "obj-" + k.getType().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
@@ -549,7 +585,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
INMS.get().getStructureKeys(),
IrisPlatforms.get().registries().structureKeys(),
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
definitions.put(key, j);
@@ -565,17 +601,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : data.getBlockLoader().getPossibleKeys()) {
ja.put(i);
}
for (String i : BukkitBlockResolution.getBlockTypes()) {
ja.put(i);
}
j.put("enum", ja);
j.put("enum", blockTypes());
definitions.put(key, j);
}
@@ -589,7 +615,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", BukkitBlockResolution.getItemTypes());
j.put("enum", itemTypes());
definitions.put(key, j);
}
@@ -597,6 +623,26 @@ public class SchemaBuilder {
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListEntityType.class)) {
fancyType = "List of Entity Types";
String key = "enum-entity-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
JSONArray ja = new JSONArray();
for (String i : IrisPlatforms.get().registries().entityKeys()) {
ja.put(i);
}
j.put("enum", ja);
definitions.put(key, j);
}
JSONObject items = new JSONObject();
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListFont.class)) {
String key = "enum-font";
fancyType = "List of Font Families";
@@ -617,7 +663,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", ENCHANT_TYPES);
j.put("enum", enchantTypes());
definitions.put(key, j);
}
@@ -631,7 +677,7 @@ public class SchemaBuilder {
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
j.put("enum", potionTypes());
definitions.put(key, j);
}
@@ -640,9 +686,9 @@ public class SchemaBuilder {
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
Class<? extends ListFunction<KList<String>>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
try {
var instance = functionClass.getDeclaredConstructor().newInstance();
ListFunction<KList<String>> instance = functionClass.getDeclaredConstructor().newInstance();
String key = instance.key();
fancyType = instance.fancyName();
@@ -659,22 +705,8 @@ public class SchemaBuilder {
} catch (Throwable e) {
IrisLogging.error("Could not execute apply method in " + functionClass.getName());
}
} else if (t.type().equals(PotionEffectType.class)) {
fancyType = "List of Potion Effect Types";
String key = "enum-potion-effect-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
definitions.put(key, j);
}
JSONObject items = new JSONObject();
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (KeyedType.isKeyed(t.type())) {
fancyType = addEnumList(prop, description, t, KeyedType.values(t.type()), Function.identity());
} else if (SchemaKeyedTypes.isKeyed(t.type())) {
fancyType = addEnumList(prop, description, t, SchemaKeyedTypes.values(t.type()), Function.identity());
} else if (t.type().isEnum()) {
fancyType = addEnumList(prop, description, t, t.type().getEnumConstants(), o -> ((Enum<?>) o).name());
}
@@ -719,7 +751,7 @@ public class SchemaBuilder {
if (present) d.add(" ");
if (value instanceof List) {
d.add(SYMBOL_LIMIT__N + " Default Value is an empty list");
} else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum<?>) && !KeyedType.isKeyed(k.getType())) {
} else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum<?>) && !SchemaKeyedTypes.isKeyed(k.getType())) {
d.add(SYMBOL_LIMIT__N + " Default Value is a default object (create this object to see default properties)");
} else {
d.add(SYMBOL_LIMIT__N + " Default Value is " + value);
@@ -783,7 +815,7 @@ public class SchemaBuilder {
@NotNull
private <T> String addEnumList(JSONObject prop, KList<String> description, ArrayType t, T[] values, Function<T, String> function) {
JSONObject items = new JSONObject();
var s = addEnum(t.type(), items, description, values, function);
String s = addEnum(t.type(), items, description, values, function);
prop.put("items", items);
return "List of " + s + "s";
@@ -826,6 +858,47 @@ public class SchemaBuilder {
return type.getSimpleName().replaceAll("\\QIris\\E", "");
}
private JSONObject buildBlockPropertyJson(PlatformBlockProperty property) {
JSONObject json = new JSONObject();
json.put("type", property.jsonType());
json.put("default", property.defaultValue());
List<Object> allowed = property.allowedValues();
if (!allowed.isEmpty()) {
json.put("enum", new JSONArray(allowed));
}
if (property.hasNumericRange()) {
PlatformNumericRange range = property.numericRange();
if ("integer".equals(property.jsonType())) {
json.put("minimum", (long) range.minimum());
json.put("maximum", (long) range.maximum());
} else {
json.put("minimum", range.minimum());
json.put("maximum", range.maximum());
}
json.put("exclusiveMinimum", range.exclusiveMinimum());
json.put("exclusiveMaximum", range.exclusiveMaximum());
}
return json;
}
private List<BlockStateGroup> reconstructBlockStateGroups() {
List<BlockStateGroup> groups = new ArrayList<>();
List<PlatformBlockProperty> currentProperties = null;
List<String> currentBlocks = null;
for (Map.Entry<String, List<PlatformBlockProperty>> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) {
List<PlatformBlockProperty> value = entry.getValue();
if (currentProperties != null && value == currentProperties) {
currentBlocks.add(entry.getKey());
} else {
currentBlocks = new ArrayList<>();
currentBlocks.add(entry.getKey());
currentProperties = value;
groups.add(new BlockStateGroup(currentBlocks, value));
}
}
return groups;
}
private String getType(Class<?> c) {
if (c.equals(int.class) || c.equals(Integer.class) || c.equals(long.class) || c.equals(Long.class)) {
return "integer";
@@ -839,7 +912,7 @@ public class SchemaBuilder {
return "boolean";
}
if (c.equals(String.class) || c.isEnum() || KeyedType.isKeyed(c)) {
if (c.equals(String.class) || c.isEnum() || SchemaKeyedTypes.isKeyed(c)) {
return "string";
}
@@ -864,11 +937,6 @@ public class SchemaBuilder {
return r.getDeclaredAnnotation(Desc.class).value();
}
// suppress warnings on bukkit classes
if (r.getDeclaringClass().getName().startsWith("org.bukkit.")) {
return "Bukkit package classes and enums have no descriptions";
}
warnings.addIfMissing("Missing @Desc on field " + r.getName() + " (" + r.getType() + ") in " + r.getDeclaringClass().getCanonicalName());
return "No Field Description";
}
@@ -883,4 +951,7 @@ public class SchemaBuilder {
}
return "";
}
private record BlockStateGroup(List<String> blocks, List<PlatformBlockProperty> properties) {
}
}

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