diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..cb1209db1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 0ec004470..841868749 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,8 @@ libs/ collection/ -/core/src/main/java/art/arcane/iris/util/uniques/ +out/ +tools/simd-bench/out/ DataPackExamples/ diff --git a/README.md b/README.md index b88489bdf..80cc6fab9 100644 --- a/README.md +++ b/README.md @@ -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 [template] scaffold a new pack (default template: example) +/iris studio open [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 [CraftBukkit] .jar +Iris v [Fabric] +.jar +Iris v [Forge] +.jar +Iris v [NeoForge] +.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) diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java index 500aaafda..7214c4385 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -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 { diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java index 0ba89cb76..65b49c71a 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java @@ -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; diff --git a/adapters/bukkit/plugin/build.gradle b/adapters/bukkit/plugin/build.gradle index 1ab183594..6e6abddfe 100644 --- a/adapters/bukkit/plugin/build.gradle +++ b/adapters/bukkit/plugin/build.gradle @@ -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') diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 9ecadc364..959a9b114 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -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 diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index 1deb1daf5..8ebd35788 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -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 { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java index 5d0a858b2..bdf4b39ed 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -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") diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java index c9addac65..5991ea535 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -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; diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java index f00d14b6c..7f2dc407d 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -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 { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java index 38293816d..3f1afb564 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -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(); } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 9e6a3a6bc..4e9cc48e1 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -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; diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index 152a79aa6..13cb13cf3 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -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; diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java new file mode 100644 index 000000000..79745ea49 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisProtocolService.java @@ -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 . + */ + +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)"; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java index b147f409a..54ae6b50f 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java @@ -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; diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java index 7d2353de5..48d1aba23 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java @@ -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; diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java b/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java new file mode 100644 index 000000000..826f32383 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/ClientPacketSink.java @@ -0,0 +1,6 @@ +package art.arcane.iris.client; + +@FunctionalInterface +public interface ClientPacketSink { + void send(byte[] frame); +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java new file mode 100644 index 000000000..658cb9c5c --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClient.java @@ -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(); + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java new file mode 100644 index 000000000..4170a8dcf --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientCursor.java @@ -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; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java new file mode 100644 index 000000000..42488151f --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientDimension.java @@ -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; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java new file mode 100644 index 000000000..3832c888b --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientHud.java @@ -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); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java new file mode 100644 index 000000000..5ba3b0f71 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientKeybinds.java @@ -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()); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java new file mode 100644 index 000000000..33de3d60f --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientMarkers.java @@ -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> 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 forTile(IrisTileKey key) { + return byTile.get(key); + } + + public void clear() { + byTile.clear(); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java new file mode 100644 index 000000000..ca6267552 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientPregenState.java @@ -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 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; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java new file mode 100644 index 000000000..980677403 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientRegionMap.java @@ -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 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 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; + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java new file mode 100644 index 000000000..ad92cbfa7 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientSession.java @@ -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 + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java new file mode 100644 index 000000000..5b54d7cc4 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientTileCache.java @@ -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 cache; + private final Map pending; + private final Deque queue; + private final Set 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 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; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java new file mode 100644 index 000000000..2e6b62c59 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java @@ -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; + + 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) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java new file mode 100644 index 000000000..f4e647251 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java @@ -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; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java new file mode 100644 index 000000000..8aee38ea3 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java @@ -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 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; + } + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java new file mode 100644 index 000000000..1461bfdc4 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java @@ -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(); + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java new file mode 100644 index 000000000..e29f5288d --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileImage.java @@ -0,0 +1,4 @@ +package art.arcane.iris.client; + +public record IrisTileImage(int width, int height, int[] argb) { +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java new file mode 100644 index 000000000..d60daa05c --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileKey.java @@ -0,0 +1,4 @@ +package art.arcane.iris.client; + +public record IrisTileKey(int tileX, int tileZ, int zoom) { +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java new file mode 100644 index 000000000..36b63709e --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisToastPresenter.java @@ -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; + }; + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java new file mode 100644 index 000000000..77d82db75 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java @@ -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 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 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 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 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> iterator = textures.entrySet().iterator(); + while (textures.size() > MAX_TEXTURES && iterator.hasNext()) { + Map.Entry 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) { + } +} diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java new file mode 100644 index 000000000..abe590312 --- /dev/null +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java @@ -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 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 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; + } +} diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index d2c9fc3fd..019841bb4 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -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 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 diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java new file mode 100644 index 000000000..c5465c243 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/FabricProtocolNetworking.java @@ -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 . + */ + +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); + } + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java index 75ff8ecdc..ed4a17d3e 100644 --- a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java @@ -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 dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher)); diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java new file mode 100644 index 000000000..94acff154 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricClient.java @@ -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 . + */ + +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()); + } +} diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/LivingEntityMixin.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/LivingEntityMixin.java new file mode 100644 index 000000000..75eed5747 --- /dev/null +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/LivingEntityMixin.java @@ -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 . + */ + +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); + } +} diff --git a/adapters/fabric/src/main/resources/fabric.mod.json b/adapters/fabric/src/main/resources/fabric.mod.json index 48c3f1883..829b2af3a 100644 --- a/adapters/fabric/src/main/resources/fabric.mod.json +++ b/adapters/fabric/src/main/resources/fabric.mod.json @@ -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" } diff --git a/adapters/fabric/src/main/resources/irisworldgen.mixins.json b/adapters/fabric/src/main/resources/irisworldgen.mixins.json index 14a174a8b..86bc9e136 100644 --- a/adapters/fabric/src/main/resources/irisworldgen.mixins.json +++ b/adapters/fabric/src/main/resources/irisworldgen.mixins.json @@ -4,6 +4,7 @@ "package": "art.arcane.iris.fabric.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ + "LivingEntityMixin", "ServerPacksSourceMixin" ], "injectors": { diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index 9d3ad4e15..7c058d406 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -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' diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java new file mode 100644 index 000000000..fdef8f260 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeClientProtocol.java @@ -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 . + */ + +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)); + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java new file mode 100644 index 000000000..e63f14f77 --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/ForgeProtocolNetworking.java @@ -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 . + */ + +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 channel; + + private ForgeProtocolNetworking() { + } + + public static void register() { + Channel 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 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 channel; + + private ForgeProtocolChannel(Channel 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)); + } + } +} diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java index b9fe969d4..b8b742130 100644 --- a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java @@ -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 event) -> ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())); PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate) (PlayerInteractEvent.RightClickBlock event) -> diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java new file mode 100644 index 000000000..3d20e7b1e --- /dev/null +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeClient.java @@ -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 . + */ + +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 channel = ForgeProtocolNetworking.channel(); + if (channel == null) { + return; + } + channel.send(new ModdedIrisPayload(frame), PacketDistributor.SERVER.noArg()); + } +} diff --git a/adapters/forge/src/main/resources/META-INF/mods.toml b/adapters/forge/src/main/resources/META-INF/mods.toml index 71a190e47..67c54b572 100644 --- a/adapters/forge/src/main/resources/META-INF/mods.toml +++ b/adapters/forge/src/main/resources/META-INF/mods.toml @@ -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" diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java index 83aff14d5..2f07188ad 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -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; } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java index 3e42a1dee..ce5cd7471 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java @@ -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 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; } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java new file mode 100644 index 000000000..bf822677e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDeathLoot.java @@ -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 . + */ + +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 WARNED_TABLES = ConcurrentHashMap.newKeySet(); + + private ModdedDeathLoot() { + } + + public static void tag(LivingEntity entity, KList 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 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 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; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java index 0cd06d720..582fab6cb 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java @@ -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) { - } - } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java new file mode 100644 index 000000000..759b2f82c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java @@ -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 . + */ + +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 WARNED_TYPES = ConcurrentHashMap.newKeySet(); + private static final Set 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 modifiers = irisEntity.getAttributes(); + if (modifiers.isEmpty()) { + return; + } + + Registry registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE); + int index = 0; + for (IrisAttributeModifier modifier : modifiers) { + index++; + if (rng.nextDouble() >= modifier.getChance()) { + continue; + } + Holder 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 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); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java new file mode 100644 index 000000000..85c43af8d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisPayload.java @@ -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 . + */ + +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 TYPE = new CustomPacketPayload.Type<>(Identifier.parse(IrisProtocol.CHANNEL)); + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(ModdedIrisPayload::write, ModdedIrisPayload::read); + + @Override + public CustomPacketPayload.Type 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); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java index 9cad4c030..10137165c 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedItemTranslator.java @@ -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 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 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 resolveAttribute(Registry registry, String key) { + if (key == null || key.isBlank()) { + return null; + } + String value = key.trim().toLowerCase(Locale.ROOT); + Holder 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 lookupAttribute(Registry registry, Identifier id) { + if (id == null) { + return null; + } + Optional> 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) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java index 252833b45..433d32d54 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -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 ERROR_SINK = null; + private static volatile Consumer 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 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 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 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; } } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java new file mode 100644 index 000000000..f21db9250 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolChannel.java @@ -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 . + */ + +package art.arcane.iris.modded; + +import net.minecraft.server.level.ServerPlayer; + +public interface ModdedProtocolChannel { + boolean canReceive(ServerPlayer player); + + void send(ServerPlayer player, ModdedIrisPayload payload); +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java new file mode 100644 index 000000000..8cb636852 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolHandler.java @@ -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 . + */ + +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 SESSION_ENGINES = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap 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() + ")"; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java new file mode 100644 index 000000000..a2daa7a5e --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedProtocolTransport.java @@ -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 . + */ + +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; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java index 99f906c76..e81ca9583 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedRegistries.java @@ -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 itemKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.ITEM.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List entityKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.ENTITY_TYPE.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List blockTypeKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List enchantmentKeys() { + List keys = new ArrayList<>(); + Registry registry = enchantmentRegistry(); + if (registry == null) { + return keys; + } + for (Identifier identifier : registry.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public List potionEffectKeys() { + List keys = new ArrayList<>(); + for (Identifier identifier : BuiltInRegistries.MOB_EFFECT.keySet()) { + keys.add(identifier.toString()); + } + return keys; + } + + @Override + public Map> blockStateProperties() { + Map> properties = new LinkedHashMap<>(); + for (Block block : BuiltInRegistries.BLOCK) { + BlockState defaultState = block.defaultBlockState(); + List 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 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 enchantmentRegistry() { + MinecraftServer instance = server.get(); + if (instance == null) { + return null; + } + return instance.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); + } + + private static > PlatformBlockProperty convertProperty(Property property, BlockState defaultState) { + T defaultValue = defaultState.getValue(property); + Class valueClass = property.getValueClass(); + List 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); + } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java index 46db35fc7..e9df43a9b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedScheduler.java @@ -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 mainQueue = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue delayedQueue = new ConcurrentLinkedQueue<>(); public ModdedScheduler() { + this.asyncExecutor = createAsyncExecutor(); + } + + private static ThreadPoolExecutor createAsyncExecutor() { BlockingQueue 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; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java new file mode 100644 index 000000000..fcb1b6aeb --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSentry.java @@ -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 . + */ + +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; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java new file mode 100644 index 000000000..800f4e396 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedServerCommands.java @@ -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 . + */ + +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; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java new file mode 100644 index 000000000..f0b5ee441 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorkspaceGenerator.java @@ -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 . + */ + +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 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; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java new file mode 100644 index 000000000..88e5620c2 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldManager.java @@ -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 . + */ + +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> 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 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 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 mantle = engine.getMantle().getMantle(); + if (!mantle.isChunkLoaded(chunkX, chunkZ) || mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) { + return; + } + + MantleChunk 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 mantle = engine.getMantle().getMantle(); + if (!mantle.isChunkLoaded(chunkX, chunkZ)) { + return; + } + + MantleChunk 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 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 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 resolveMarkerSpawners(IrisMarker marker) { + KList 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 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 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 pool, KList 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 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 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 entries) { + int totalRarity = 0; + for (IrisEntitySpawn entry : entries) { + totalRarity += IRare.get(entry); + } + if (totalRarity <= 0) { + return entries.getRandom(); + } + KList 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) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java index fad8d713e..0220f4ea2 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -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; } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index 02f3a1bd5..e005b99ee 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -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", "", "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() { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java new file mode 100644 index 000000000..86ee99137 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java @@ -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 . + */ + +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 GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + + private ModdedDeveloperCommands() { + } + + static LiteralArgumentBuilder tree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "developer")) + .then(Commands.literal("sentry") + .executes((CommandContext context) -> sentry(context.getSource()))) + .then(Commands.literal("network") + .executes((CommandContext context) -> network(context.getSource()))) + .then(Commands.literal("ip") + .executes((CommandContext 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 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; + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java index bc0261bcd..c0ca26406 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenBossBar.java @@ -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; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java index 6f818ffa2..45c38fa57 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java @@ -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); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java new file mode 100644 index 000000000..4f824e826 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedEntitySpawnService.java @@ -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 . + */ + +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); + } + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java index 2628fd5d3..5f6d5b47b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java @@ -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); diff --git a/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json new file mode 100644 index 000000000..f5898c8f4 --- /dev/null +++ b/adapters/modded-common/src/main/resources/assets/irisworldgen/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "key.categories.irisworldgen.iris": "Iris", + "key.irisworldgen.toggle_pregen_hud": "Toggle Pregen HUD" +} diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index 2fbd503d0..6c1726b57 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -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' diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java index f2a68cebe..9b30fe4a9 100644 --- a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java @@ -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); diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java new file mode 100644 index 000000000..9ddc2a099 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeClient.java @@ -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 . + */ + +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()); + } +} diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java new file mode 100644 index 000000000..37705e226 --- /dev/null +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/NeoForgeProtocolNetworking.java @@ -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 . + */ + +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); + } + } +} diff --git a/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml index dfcc36f8c..c9d115b67 100644 --- a/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -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" diff --git a/core/build.gradle b/core/build.gradle index 6af94040f..172dad0c5 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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) diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt index e19929db5..fe8deeb95 100644 --- a/core/purity-allowlist.txt +++ b/core/purity-allowlist.txt @@ -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 diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index dd2e0fe72..380069819 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.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; diff --git a/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java b/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java index 3249ae125..ece0a171f 100644 --- a/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java +++ b/core/src/main/java/art/arcane/iris/core/events/IrisLootEvent.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index edb094fdb..c87c25ea3 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -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 diff --git a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java index e83229ba9..268cadb06 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java +++ b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java @@ -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); diff --git a/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java b/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java index 14451c226..94df44fc4 100644 --- a/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java +++ b/core/src/main/java/art/arcane/iris/core/link/data/MythicMobsDataProvider.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java index 60d513a2a..df4e47283 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -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 { diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java b/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java index f58c67977..abd061bfa 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisRegistrant.java @@ -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 diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index f01244b17..7b8185c03 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -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 implements MeteredCache { return thread; }); private static final Set schemaBuildQueue = ConcurrentHashMap.newKeySet(); + private static final AtomicBoolean schemaBuildExecutorRegistered = new AtomicBoolean(); protected final AtomicCache> folderCache; protected KSet firstAccess; protected File root; @@ -101,6 +108,10 @@ public class ResourceLoader 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() { diff --git a/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java b/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java index 5fe6f5f22..3a9d01425 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java +++ b/core/src/main/java/art/arcane/iris/core/nms/container/BlockProperty.java @@ -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 { diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java index 26b8b362a..8de733b3e 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1192/DataFixerV1192.java @@ -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 OPTIONS = Map.of( diff --git a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java index 0381ffdb9..785c4e4a7 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java new file mode 100644 index 000000000..92d891717 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java @@ -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 . + */ + +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 validate(PlatformRegistries registries, + Collection referencedBlocks, + Collection referencedItems, + Collection referencedEntities) { + if (registries == null) { + return List.of(); + } + + List blockKeys = normalizeKeyList(registries.blockKeys()); + List itemKeys = normalizeKeyList(registries.itemKeys()); + List entityKeys = normalizeKeyList(registries.entityKeys()); + + Set knownBlocks = new HashSet<>(blockKeys); + Set knownItems = new HashSet<>(itemKeys); + Set knownEntities = new HashSet<>(entityKeys); + + Set loadedNamespaces = new HashSet<>(); + addNamespaces(blockKeys, loadedNamespaces); + addNamespaces(itemKeys, loadedNamespaces); + addNamespaces(entityKeys, loadedNamespaces); + + Map 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 referenced, + ContentRegistry registry, + Set known, + List candidatePool, + Set loadedNamespaces, + Map 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 normalizeKeyList(List keys) { + if (keys == null || keys.isEmpty()) { + return List.of(); + } + List 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 keys, Set 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 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]; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index 4be8fdd32..d8ee38796 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -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 warnings) { + try { + if (!IrisPlatforms.isBound()) { + return; + } + PlatformRegistries registries = IrisPlatforms.get().registries(); + if (registries == null) { + return; + } + List blockKeys = registries.blockKeys(); + List itemKeys = registries.itemKeys(); + List entityKeys = registries.entityKeys(); + if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) { + return; + } + + ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder); + List 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 blocks = new HashSet<>(); + Set items = new HashSet<>(); + Set entities = new HashSet<>(); + Set customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks")); + + try (Stream stream = Files.walk(packFolder.toPath())) { + List 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 blocks, Set items, Set entities, Set 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 blocks, Set 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 target) { + String value = raw.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty()) { + target.add(value); + } + } + + private static Set deriveRegistrantKeys(File folder) { + Set 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 blocks, Set items, Set entities) { + } + private static void validateDimensions(File packFolder, File[] dimensionFiles, List blockingErrors, List warnings) { File regionsFolder = new File(packFolder, "regions"); File biomesFolder = new File(packFolder, "biomes"); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index 56048e1d5..406b33624 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -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); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java index b032a7bdd..ca3c98971 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java @@ -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 diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index 798e496d4..b98fb1a6b 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -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) { diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 7090e7e3a..c613ddf70 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -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; diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java index 9605bea5b..4410f7a90 100644 --- a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java @@ -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 definitions; private final Class root; private final KList 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>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); try { - var instance = functionClass.getDeclaredConstructor().newInstance(); + ListFunction> 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 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>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); try { - var instance = functionClass.getDeclaredConstructor().newInstance(); + ListFunction> 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 String addEnumList(JSONObject prop, KList description, ArrayType t, T[] values, Function 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 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 reconstructBlockStateGroups() { + List groups = new ArrayList<>(); + List currentProperties = null; + List currentBlocks = null; + for (Map.Entry> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) { + List 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 blocks, List properties) { + } } diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java b/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java new file mode 100644 index 000000000..6026c96e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaKeyedTypes.java @@ -0,0 +1,34 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.util.common.reflect.KeyedType; + +final class SchemaKeyedTypes { + private SchemaKeyedTypes() { + } + + static boolean isKeyed(Class type) { + return KeyedType.isKeyed(type); + } + + static String[] values(Class type) { + return KeyedType.values(type); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java b/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java new file mode 100644 index 000000000..0b073c6db --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/EngineResolver.java @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; + +@FunctionalInterface +public interface EngineResolver { + Engine resolve(String sessionId); +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java new file mode 100644 index 000000000..6ecd109d2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisCursorResolver.java @@ -0,0 +1,45 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.spi.protocol.IrisMessage; + +public final class IrisCursorResolver { + private IrisCursorResolver() { + } + + public static IrisMessage.CursorInfo resolve(Engine engine, int blockX, int blockZ) { + IrisBiome surfaceBiome = engine.getSurfaceBiome(blockX, blockZ); + IrisRegion region = engine.getRegion(blockX, blockZ); + IrisBiome caveBiome = engine.getCaveBiome(blockX, blockZ); + int height = engine.getHeight(blockX, blockZ); + String biomeKey = keyOf(surfaceBiome == null ? null : surfaceBiome.getLoadKey()); + String regionKey = keyOf(region == null ? null : region.getLoadKey()); + String caveBiomeKey = keyOf(caveBiome == null ? null : caveBiome.getLoadKey()); + String dimensionKey = keyOf(engine.getDimension().getLoadKey()); + return new IrisMessage.CursorInfo(blockX, blockZ, biomeKey, regionKey, caveBiomeKey, height, dimensionKey); + } + + private static String keyOf(String value) { + return value == null ? "" : value; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java new file mode 100644 index 000000000..77202cfcd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisProtocolServer.java @@ -0,0 +1,348 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +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 java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongSupplier; + +public final class IrisProtocolServer { + private final IrisSessionRegistry registry; + private final long serverCapabilities; + private final String serverBrand; + private final boolean irisActive; + private final LongSupplier clock; + private final AtomicLong droppedBeforeHello; + private final AtomicLong rateLimitedFrames; + private final AtomicLong rejectedFrames; + private final AtomicLong versionMismatches; + private final AtomicLong capabilityRejected; + private final AtomicLong noEngineDrops; + private final AtomicLong cursorInfoServed; + private final AtomicLong visionTileForwarded; + private final AtomicLong visionRateLimited; + private final AtomicLong pregenRegionDeltasBroadcast; + private final AtomicLong studioHotloadsBroadcast; + private final AtomicLong toastsBroadcast; + private final AtomicLong toastsSent; + private volatile EngineResolver engineResolver; + private volatile VisionTileRequestHandler visionTileHandler; + + public IrisProtocolServer(IrisSessionRegistry registry, long serverCapabilities, String serverBrand, boolean irisActive) { + this(registry, serverCapabilities, serverBrand, irisActive, System::currentTimeMillis); + } + + IrisProtocolServer(IrisSessionRegistry registry, long serverCapabilities, String serverBrand, boolean irisActive, LongSupplier clock) { + this.registry = Objects.requireNonNull(registry, "session registry"); + this.serverCapabilities = serverCapabilities; + this.serverBrand = Objects.requireNonNull(serverBrand, "server brand"); + this.irisActive = irisActive; + this.clock = Objects.requireNonNull(clock, "clock"); + this.droppedBeforeHello = new AtomicLong(0L); + this.rateLimitedFrames = new AtomicLong(0L); + this.rejectedFrames = new AtomicLong(0L); + this.versionMismatches = new AtomicLong(0L); + this.capabilityRejected = new AtomicLong(0L); + this.noEngineDrops = new AtomicLong(0L); + this.cursorInfoServed = new AtomicLong(0L); + this.visionTileForwarded = new AtomicLong(0L); + this.visionRateLimited = new AtomicLong(0L); + this.pregenRegionDeltasBroadcast = new AtomicLong(0L); + this.studioHotloadsBroadcast = new AtomicLong(0L); + this.toastsBroadcast = new AtomicLong(0L); + this.toastsSent = new AtomicLong(0L); + } + + public IrisSessionRegistry registry() { + return registry; + } + + public void setEngineResolver(EngineResolver engineResolver) { + this.engineResolver = engineResolver; + } + + public void setVisionTileHandler(VisionTileRequestHandler visionTileHandler) { + this.visionTileHandler = visionTileHandler; + } + + public void onClientFrame(String sessionId, byte[] frame) { + IrisSession session = registry.get(sessionId); + if (session == null) { + return; + } + if (!session.allowInbound(clock.getAsLong())) { + rateLimitedFrames.incrementAndGet(); + return; + } + IrisMessage message; + try { + message = IrisMessageCodec.decode(frame); + } catch (ProtocolException rejected) { + rejectedFrames.incrementAndGet(); + return; + } + if (message == null) { + return; + } + dispatch(session, message); + } + + public void broadcastPregenProgress(long jobId, long chunksDone, long chunksTotal, double chunksPerSecond, long etaMillis, int state) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenProgress(jobId, chunksDone, chunksTotal, chunksPerSecond, etaMillis, state)); + } + session.sendRaw(frame); + } + } + + public void pregenEnd(long jobId, boolean completed) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenEnd(jobId, completed)); + } + session.sendRaw(frame); + } + } + + public void broadcastDimensionStatus(String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady()) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.DimensionStatus(dimensionKey, packKey, seed, minY, maxY, irisWorld)); + } + session.sendRaw(frame); + } + } + + public void sendDimensionStatus(String sessionId, String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) { + IrisSession session = registry.get(sessionId); + if (session == null || !session.isReady()) { + return; + } + session.send(new IrisMessage.DimensionStatus(dimensionKey, packKey, seed, minY, maxY, irisWorld)); + } + + public void broadcastPregenRegionDelta(long jobId, int regionX, int regionZ, int state) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.PregenRegionDelta(jobId, regionX, regionZ, state)); + } + session.sendRaw(frame); + } + if (frame != null) { + pregenRegionDeltasBroadcast.incrementAndGet(); + } + } + + public void broadcastStudioHotload(String packKey, int changedFiles, boolean failed, String message) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.StudioHotload(packKey, changedFiles, failed, message)); + } + session.sendRaw(frame); + } + if (frame != null) { + studioHotloadsBroadcast.incrementAndGet(); + } + } + + public void broadcastToast(int kind, String title, String body) { + if (registry.isEmpty()) { + return; + } + byte[] frame = null; + for (IrisSession session : registry.all()) { + if (!session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + continue; + } + if (frame == null) { + frame = IrisMessageCodec.encode(new IrisMessage.Toast(kind, title, body)); + } + session.sendRaw(frame); + } + if (frame != null) { + toastsBroadcast.incrementAndGet(); + } + } + + public void sendToast(String sessionId, int kind, String title, String body) { + IrisSession session = registry.get(sessionId); + if (session == null || !session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)) { + return; + } + session.send(new IrisMessage.Toast(kind, title, body)); + toastsSent.incrementAndGet(); + } + + public long droppedBeforeHelloCount() { + return droppedBeforeHello.get(); + } + + public long rateLimitedFrameCount() { + return rateLimitedFrames.get(); + } + + public long rejectedFrameCount() { + return rejectedFrames.get(); + } + + public long versionMismatchCount() { + return versionMismatches.get(); + } + + public long capabilityRejectedCount() { + return capabilityRejected.get(); + } + + public long noEngineDropCount() { + return noEngineDrops.get(); + } + + public long cursorInfoServedCount() { + return cursorInfoServed.get(); + } + + public long visionTileForwardedCount() { + return visionTileForwarded.get(); + } + + public long visionRateLimitedCount() { + return visionRateLimited.get(); + } + + public long pregenRegionDeltasBroadcastCount() { + return pregenRegionDeltasBroadcast.get(); + } + + public long studioHotloadsBroadcastCount() { + return studioHotloadsBroadcast.get(); + } + + public long toastsBroadcastCount() { + return toastsBroadcast.get(); + } + + public long toastsSentCount() { + return toastsSent.get(); + } + + private void dispatch(IrisSession session, IrisMessage message) { + if (session.state() == IrisSession.State.AWAITING_HELLO) { + if (message instanceof IrisMessage.ClientHello clientHello) { + onClientHello(session, clientHello); + return; + } + droppedBeforeHello.incrementAndGet(); + return; + } + switch (message) { + case IrisMessage.CursorInfoRequest cursorInfoRequest -> onCursorInfoRequest(session, cursorInfoRequest); + case IrisMessage.VisionTileRequest visionTileRequest -> onVisionTileRequest(session, visionTileRequest); + default -> { + } + } + } + + private void onClientHello(IrisSession session, IrisMessage.ClientHello clientHello) { + if (clientHello.protocolVersion() != IrisProtocol.PROTOCOL_VERSION) { + versionMismatches.incrementAndGet(); + return; + } + session.markReady(clientHello.protocolVersion(), clientHello.capabilities()); + session.send(new IrisMessage.ServerHello(IrisProtocol.PROTOCOL_VERSION, serverCapabilities, serverBrand, irisActive)); + } + + private void onCursorInfoRequest(IrisSession session, IrisMessage.CursorInfoRequest request) { + if (!session.hasCapability(IrisProtocol.CAPABILITY_CURSOR)) { + capabilityRejected.incrementAndGet(); + return; + } + EngineResolver resolver = engineResolver; + if (resolver == null) { + noEngineDrops.incrementAndGet(); + return; + } + Engine engine = resolver.resolve(session.id()); + if (engine == null) { + noEngineDrops.incrementAndGet(); + return; + } + session.send(IrisCursorResolver.resolve(engine, request.blockX(), request.blockZ())); + cursorInfoServed.incrementAndGet(); + } + + private void onVisionTileRequest(IrisSession session, IrisMessage.VisionTileRequest request) { + if (!session.hasCapability(IrisProtocol.CAPABILITY_VISION)) { + capabilityRejected.incrementAndGet(); + return; + } + if (!session.allowVisionTile(clock.getAsLong())) { + visionRateLimited.incrementAndGet(); + return; + } + VisionTileRequestHandler handler = visionTileHandler; + if (handler == null) { + noEngineDrops.incrementAndGet(); + return; + } + handler.handle(session.id(), request.tileX(), request.tileZ(), request.zoomLevel()); + visionTileForwarded.incrementAndGet(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java new file mode 100644 index 000000000..e0c8794c6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisServerTransport.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +public interface IrisServerTransport { + void sendToClient(String sessionId, byte[] frame); +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java new file mode 100644 index 000000000..c219b0617 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisSession.java @@ -0,0 +1,116 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.util.Objects; + +public final class IrisSession { + private final String id; + private final IrisServerTransport transport; + private volatile State state; + private volatile long capabilities; + private volatile int remoteProtocolVersion; + private long inboundWindowStartMillis; + private int inboundFramesInWindow; + private long visionTileWindowStartMillis; + private int visionTileRequestsInWindow; + + public IrisSession(String id, IrisServerTransport transport) { + this.id = Objects.requireNonNull(id, "session id"); + this.transport = Objects.requireNonNull(transport, "session transport"); + this.state = State.AWAITING_HELLO; + this.capabilities = 0L; + this.remoteProtocolVersion = 0; + this.inboundWindowStartMillis = 0L; + this.inboundFramesInWindow = 0; + this.visionTileWindowStartMillis = 0L; + this.visionTileRequestsInWindow = 0; + } + + public String id() { + return id; + } + + public State state() { + return state; + } + + public long capabilities() { + return capabilities; + } + + public int remoteProtocolVersion() { + return remoteProtocolVersion; + } + + public boolean isReady() { + return state == State.READY; + } + + public boolean hasCapability(long capabilityMask) { + return (capabilities & capabilityMask) != 0L; + } + + public void markReady(int protocolVersion, long negotiatedCapabilities) { + this.remoteProtocolVersion = protocolVersion; + this.capabilities = negotiatedCapabilities; + this.state = State.READY; + } + + public synchronized boolean allowInbound(long nowMillis) { + if (nowMillis - inboundWindowStartMillis >= 1000L) { + inboundWindowStartMillis = nowMillis; + inboundFramesInWindow = 0; + } + if (inboundFramesInWindow >= IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND) { + return false; + } + inboundFramesInWindow++; + return true; + } + + public synchronized boolean allowVisionTile(long nowMillis) { + if (nowMillis - visionTileWindowStartMillis >= 1000L) { + visionTileWindowStartMillis = nowMillis; + visionTileRequestsInWindow = 0; + } + if (visionTileRequestsInWindow >= IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND) { + return false; + } + visionTileRequestsInWindow++; + return true; + } + + public void send(IrisMessage message) { + sendRaw(IrisMessageCodec.encode(message)); + } + + public void sendRaw(byte[] frame) { + transport.sendToClient(id, frame); + } + + public enum State { + AWAITING_HELLO, + READY + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java new file mode 100644 index 000000000..07a800f18 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisSessionRegistry.java @@ -0,0 +1,53 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import java.util.concurrent.ConcurrentHashMap; + +public final class IrisSessionRegistry { + private final ConcurrentHashMap sessions; + + public IrisSessionRegistry() { + this.sessions = new ConcurrentHashMap<>(); + } + + public void register(IrisSession session) { + sessions.put(session.id(), session); + } + + public IrisSession unregister(String sessionId) { + return sessions.remove(sessionId); + } + + public IrisSession get(String sessionId) { + return sessions.get(sessionId); + } + + public boolean isEmpty() { + return sessions.isEmpty(); + } + + public int size() { + return sessions.size(); + } + + public Iterable all() { + return sessions.values(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java new file mode 100644 index 000000000..c0c45bdef --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisTileEncoder.java @@ -0,0 +1,138 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.IrisRenderer; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.Deflater; + +public final class IrisTileEncoder { + public static final int TILE_PIXELS = 128; + public static final int MAX_ZOOM_LEVEL = 8; + public static final int MODE_RAW_RGB = 0; + public static final int MODE_PALETTE = 1; + private static final int MAX_PALETTE_COLORS = 256; + private static final int RGB_MASK = 0xFFFFFF; + + private IrisTileEncoder() { + } + + public static List encode(Engine engine, int tileX, int tileZ, int zoomLevel, int sequence) { + int zoom = clampZoom(zoomLevel); + int blocksPerPixel = 1 << zoom; + double size = (double) TILE_PIXELS * blocksPerPixel; + double sx = (double) tileX * size; + double sz = (double) tileZ * size; + BufferedImage image = new IrisRenderer(engine).render(sx, sz, size, TILE_PIXELS, RenderType.BIOME); + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + byte[] blob = encodePixels(pixels, TILE_PIXELS, TILE_PIXELS); + return splitIntoChunks(blob, tileX, tileZ, zoom, sequence); + } + + public static byte[] encodePixels(int[] pixels, int width, int height) { + Map palette = buildPalette(pixels); + ByteArrayOutputStream raw = new ByteArrayOutputStream(pixels.length); + try (DataOutputStream out = new DataOutputStream(raw)) { + out.writeInt(width); + out.writeInt(height); + if (palette != null) { + out.writeByte(MODE_PALETTE); + out.writeInt(palette.size()); + for (Integer color : palette.keySet()) { + out.writeByte(color >> 16 & 0xFF); + out.writeByte(color >> 8 & 0xFF); + out.writeByte(color & 0xFF); + } + for (int pixel : pixels) { + out.writeByte(palette.get(pixel & RGB_MASK)); + } + } else { + out.writeByte(MODE_RAW_RGB); + for (int pixel : pixels) { + out.writeByte(pixel >> 16 & 0xFF); + out.writeByte(pixel >> 8 & 0xFF); + out.writeByte(pixel & 0xFF); + } + } + } catch (IOException failure) { + throw new UncheckedIOException("vision tile pixel encode failed", failure); + } + return deflate(raw.toByteArray()); + } + + public static List splitIntoChunks(byte[] blob, int tileX, int tileZ, int zoomLevel, int sequence) { + int chunkBytes = IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES; + int chunkCount = Math.max(1, (blob.length + chunkBytes - 1) / chunkBytes); + List chunks = new ArrayList<>(chunkCount); + for (int index = 0; index < chunkCount; index++) { + int start = index * chunkBytes; + int end = Math.min(start + chunkBytes, blob.length); + byte[] slice = new byte[end - start]; + System.arraycopy(blob, start, slice, 0, slice.length); + chunks.add(new IrisMessage.VisionTile(tileX, tileZ, zoomLevel, sequence, index, chunkCount, slice)); + } + return chunks; + } + + private static Map buildPalette(int[] pixels) { + LinkedHashMap palette = new LinkedHashMap<>(); + for (int pixel : pixels) { + int color = pixel & RGB_MASK; + if (!palette.containsKey(color)) { + if (palette.size() >= MAX_PALETTE_COLORS) { + return null; + } + palette.put(color, palette.size()); + } + } + return palette; + } + + private static byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(Deflater.BEST_SPEED); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length / 2)); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + int produced = deflater.deflate(buffer); + out.write(buffer, 0, produced); + } + deflater.end(); + return out.toByteArray(); + } + + private static int clampZoom(int zoomLevel) { + return Math.max(0, Math.min(zoomLevel, MAX_ZOOM_LEVEL)); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java b/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java new file mode 100644 index 000000000..ad0adf900 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/IrisVisionRequestService.java @@ -0,0 +1,154 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public final class IrisVisionRequestService implements VisionTileRequestHandler { + private static final int DEFAULT_MAX_PENDING = 64; + + private final EngineResolver engineResolver; + private final IrisSessionRegistry registry; + private final Executor executor; + private final int maxPending; + private final ArrayDeque pending; + private final ConcurrentHashMap sequences; + private final AtomicLong droppedSaturated; + private final AtomicLong droppedNoEngine; + private final AtomicLong droppedNoSession; + private final AtomicLong tilesEncoded; + + IrisVisionRequestService(EngineResolver engineResolver, IrisSessionRegistry registry, Executor executor, int maxPending) { + this.engineResolver = Objects.requireNonNull(engineResolver, "engine resolver"); + this.registry = Objects.requireNonNull(registry, "session registry"); + this.executor = Objects.requireNonNull(executor, "executor"); + this.maxPending = Math.max(1, maxPending); + this.pending = new ArrayDeque<>(); + this.sequences = new ConcurrentHashMap<>(); + this.droppedSaturated = new AtomicLong(0L); + this.droppedNoEngine = new AtomicLong(0L); + this.droppedNoSession = new AtomicLong(0L); + this.tilesEncoded = new AtomicLong(0L); + } + + public static IrisVisionRequestService create(EngineResolver engineResolver, IrisSessionRegistry registry) { + AtomicInteger threadIndex = new AtomicInteger(0); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), runnable -> { + Thread thread = new Thread(runnable); + thread.setName("Iris Vision Tile " + threadIndex.incrementAndGet()); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + executor.allowCoreThreadTimeOut(true); + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null) { + preservation.register(executor); + } + return new IrisVisionRequestService(engineResolver, registry, executor, DEFAULT_MAX_PENDING); + } + + @Override + public void handle(String sessionId, int tileX, int tileZ, int zoomLevel) { + PendingRequest request = new PendingRequest(sessionId, tileX, tileZ, zoomLevel); + synchronized (pending) { + while (pending.size() >= maxPending) { + pending.pollFirst(); + droppedSaturated.incrementAndGet(); + } + pending.addLast(request); + } + executor.execute(this::drainOne); + } + + public long droppedSaturatedCount() { + return droppedSaturated.get(); + } + + public long droppedNoEngineCount() { + return droppedNoEngine.get(); + } + + public long droppedNoSessionCount() { + return droppedNoSession.get(); + } + + public long tilesEncodedCount() { + return tilesEncoded.get(); + } + + public int pendingSize() { + synchronized (pending) { + return pending.size(); + } + } + + private void drainOne() { + PendingRequest request; + synchronized (pending) { + request = pending.pollFirst(); + } + if (request == null) { + return; + } + process(request); + } + + private void process(PendingRequest request) { + IrisSession session = registry.get(request.sessionId()); + if (session == null || !session.isReady() || !session.hasCapability(IrisProtocol.CAPABILITY_VISION)) { + droppedNoSession.incrementAndGet(); + return; + } + Engine engine = engineResolver.resolve(request.sessionId()); + if (engine == null) { + droppedNoEngine.incrementAndGet(); + return; + } + int sequence = nextSequence(request); + List chunks = IrisTileEncoder.encode(engine, request.tileX(), request.tileZ(), request.zoomLevel(), sequence); + for (IrisMessage.VisionTile chunk : chunks) { + session.send(chunk); + } + tilesEncoded.incrementAndGet(); + } + + private int nextSequence(PendingRequest request) { + String key = request.sessionId() + ":" + request.tileX() + ":" + request.tileZ() + ":" + request.zoomLevel(); + return sequences.merge(key, 1, Integer::sum); + } + + private record PendingRequest(String sessionId, int tileX, int tileZ, int zoomLevel) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java b/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java new file mode 100644 index 000000000..ccfdb755b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/protocol/VisionTileRequestHandler.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +@FunctionalInterface +public interface VisionTileRequestHandler { + void handle(String sessionId, int tileX, int tileZ, int zoomLevel); +} diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java index e41eceadb..ab16e2709 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.safeguard.task; +import art.arcane.iris.BuildConstants; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.nms.INMS; @@ -94,7 +95,7 @@ public final class Tasks { } else if (parts.length >= 2) { supportedVersions = parts[1]; } else { - supportedVersions = "26.2"; + supportedVersions = BuildConstants.MINECRAFT_VERSION; } if (!(INMS.get() instanceof NMSBinding1X)) { diff --git a/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java index 903fede82..b69cf899e 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java @@ -21,7 +21,8 @@ package art.arcane.iris.core.service; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.platform.bukkit.BukkitPlatform; -import art.arcane.iris.core.link.*; +import art.arcane.iris.core.link.ExternalDataProvider; +import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.link.data.DataType; import art.arcane.iris.core.nms.container.BlockProperty; import art.arcane.iris.core.nms.container.Pair; @@ -42,7 +43,11 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.inventory.ItemStack; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Optional; import java.util.stream.Collectors; @Data diff --git a/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java index 1c4b41e00..5e85212f7 100644 --- a/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/TreeSVC.java @@ -23,7 +23,15 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisBiome; +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.IrisRegion; +import art.arcane.iris.engine.object.IrisTreeModes; +import art.arcane.iris.engine.object.IrisTreeSize; +import art.arcane.iris.engine.object.TileData; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.platform.bukkit.BukkitBlockState; import art.arcane.iris.spi.PlatformBlockState; @@ -35,7 +43,12 @@ import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.scheduling.J; -import org.bukkit.*; +import org.bukkit.Bukkit; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.TreeType; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.data.BlockData; diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java index 35e52455c..85fa16c31 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java @@ -2,13 +2,17 @@ package art.arcane.iris.core.tools; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IrisObject; import art.arcane.volmlib.util.data.Varint; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.nbt.io.NBTUtil; import art.arcane.volmlib.util.nbt.io.NamedTag; -import art.arcane.volmlib.util.nbt.tag.*; +import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.IntTag; +import art.arcane.volmlib.util.nbt.tag.ShortTag; +import art.arcane.volmlib.util.nbt.tag.Tag; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; @@ -16,8 +20,13 @@ import org.apache.commons.io.FileUtils; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; -import java.io.*; -import java.util.*; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index d5c528ff6..397d19e53 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -44,7 +44,9 @@ import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.FoliaScheduler; import lombok.Data; import lombok.experimental.Accessors; -import org.bukkit.*; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.WorldCreator; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -241,6 +243,7 @@ public class IrisCreator { } catch (Throwable e) { dx.set(true); cancelRepeatingTask(pregenProgressTask); + IrisLogging.reportError(e); e.printStackTrace(); } } @@ -401,6 +404,7 @@ public class IrisCreator { IrisLogging.info("Registered \"" + name + "\" in bukkit.yml"); } catch (IOException e) { IrisLogging.error("Failed to update bukkit.yml!"); + IrisLogging.reportError(e); e.printStackTrace(); } } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java index d3e4d7fa4..42831ff8a 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisWorldCreator.java @@ -19,7 +19,8 @@ package art.arcane.iris.core.tools; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.platform.BukkitChunkGenerator; import org.bukkit.Bukkit; import org.bukkit.World; diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 154cad12c..982d7420f 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -22,7 +22,14 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDecorationPart; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisInterpolator; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBiome; @@ -41,7 +48,13 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import java.io.File; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; @Data @EqualsAndHashCode(exclude = "data") diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 639cea776..03bd6e3bb 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -18,16 +18,40 @@ package art.arcane.iris.engine; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineMetrics; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.EngineWorldManagerProvider; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.GenerationSessionManager; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomePaletteLayer; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionMode; +import art.arcane.iris.engine.object.IrisDimensionModeType; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.spi.IrisLogging; import com.google.common.util.concurrent.AtomicDouble; import com.google.gson.Gson; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.events.IrisEngineHotloadEvent; import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.protocol.IrisProtocolServer; import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.Pair; @@ -35,9 +59,7 @@ import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.structure.StructureIndexService; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.*; import art.arcane.iris.engine.mantle.EngineMantle; -import art.arcane.iris.engine.object.*; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.spi.PlatformBiome; @@ -264,6 +286,7 @@ public class IrisEngine implements Engine { J.a(() -> DatapackIngestService.refreshWorkspace(getData())); } catch (Throwable e) { IrisLogging.error("FAILED TO SETUP ENGINE!"); + IrisLogging.reportError(e); e.printStackTrace(); } @@ -403,20 +426,40 @@ public class IrisEngine implements Engine { } public void hotloadSilently() { - getData().dump(); - getData().clearLists(); - getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); - prehotload(); - setupEngine(); - if (getWorld().hasRealWorld()) { - J.a(() -> { - synchronized (ServerConfigurator.class) { - ServerConfigurator.installDataPacks(false); - } - }); + try { + getData().dump(); + getData().clearLists(); + getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); + prehotload(); + setupEngine(); + if (getWorld().hasRealWorld()) { + J.a(() -> { + synchronized (ServerConfigurator.class) { + ServerConfigurator.installDataPacks(false); + } + }); + } + broadcastStudioHotload(false, ""); + } catch (RuntimeException | Error e) { + broadcastStudioHotload(true, e.getClass().getSimpleName() + ": " + e.getMessage()); + throw e; } } + private void broadcastStudioHotload(boolean failed, String message) { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer == null) { + return; + } + IrisDimension dimension = getDimension(); + String packKey = dimension == null ? "" : dimension.getLoadKey(); + protocolServer.broadcastStudioHotload(packKey, 0, failed, message); + protocolServer.broadcastToast( + failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS, + "Studio Hotload", + failed ? packKey + " failed" : packKey); + } + @Override public IrisEngineData getEngineData() { return engineData.aquire(() -> { @@ -431,6 +474,7 @@ public class IrisEngine implements Engine { IrisLogging.error("Failed to read Engine Data! Corrupted File? recreating..."); } } catch (IOException e) { + IrisLogging.reportError(e); e.printStackTrace(); } } @@ -448,6 +492,7 @@ public class IrisEngine implements Engine { try { IO.writeAll(f, new Gson().toJson(data)); } catch (IOException e) { + IrisLogging.reportError(e); e.printStackTrace(); } } else { @@ -724,6 +769,7 @@ public class IrisEngine implements Engine { IrisLogging.debug("Saved Engine Data"); } catch (IOException e) { IrisLogging.error("Failed to save Engine Data"); + IrisLogging.reportError(e); e.printStackTrace(); } } @@ -755,6 +801,7 @@ public class IrisEngine implements Engine { public void fail(String error, Throwable e) { failing = true; IrisLogging.error(error); + IrisLogging.reportError(e); e.printStackTrace(); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java index 1326a9a49..72719b512 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -54,7 +54,11 @@ import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.util.common.parallel.HyperLock; import art.arcane.iris.util.common.parallel.MultiBurst; -import lombok.*; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; import java.io.File; import java.io.IOException; diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 73f725fcb..b44f0bee4 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -26,7 +26,14 @@ import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedWorldManager; import art.arcane.iris.engine.platform.EngineBukkitOps; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockDrops; +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.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawner; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -63,7 +70,14 @@ import org.bukkit.inventory.ItemStack; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -145,7 +159,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { engine.getEngineData().cleanup(getEngine()); } - if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAnbientEntitySpawningSystem()) { + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { return 3000; } @@ -576,7 +590,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { }); } - if (!IrisSettings.get().getWorld().isAnbientEntitySpawningSystem()) { + if (!IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { return; } @@ -1018,6 +1032,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager { public void close() { super.close(); looper.interrupt(); + if (cleanupService != null) { + cleanupService.shutdownNow(); + } } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java index acf5c7e4b..fa8b6d6b0 100644 --- a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -2,7 +2,13 @@ package art.arcane.iris.engine; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisInterpolator; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.DataProvider; @@ -13,7 +19,12 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.interpolation.Interpolated; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; public class UpperDimensionContext implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java index 48e05a65a..d220058af 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.actuator; -import art.arcane.iris.engine.decorator.*; +import art.arcane.iris.engine.decorator.IrisCeilingDecorator; +import art.arcane.iris.engine.decorator.IrisSeaFloorDecorator; +import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; +import art.arcane.iris.engine.decorator.IrisShoreLineDecorator; +import art.arcane.iris.engine.decorator.IrisSurfaceDecorator; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.framework.EngineDecorator; diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java index 275a1dd5c..3a29978aa 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java +++ b/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java @@ -18,7 +18,12 @@ package art.arcane.iris.engine.data.io; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; public interface Deserializer { diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java index 9928bacdb..9ea42a380 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java +++ b/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java @@ -18,7 +18,12 @@ package art.arcane.iris.engine.data.io; -import java.io.*; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; public interface Serializer { diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java index 822644f68..043a447b0 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java +++ b/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java @@ -18,7 +18,13 @@ package art.arcane.iris.engine.data.io; -import java.io.*; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; public interface StringDeserializer extends Deserializer { diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java index fbfdb84e0..eb5608b29 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java +++ b/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java @@ -18,7 +18,13 @@ package art.arcane.iris.engine.data.io; -import java.io.*; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.Writer; public interface StringSerializer extends Serializer { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index 557cc3cb2..2aed5ece2 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -32,7 +32,19 @@ import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.data.chunk.TerrainChunk; import art.arcane.iris.engine.mantle.EngineMantle; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisColor; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionCarvingEntry; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.engine.object.IrisLootMode; +import art.arcane.iris.engine.object.IrisLootReference; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java index 7f54ac33c..25f7233a4 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java @@ -23,7 +23,7 @@ import art.arcane.iris.core.events.IrisEngineHotloadEvent; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; -import org.bukkit.*; +import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -60,32 +60,6 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent } } -// @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) -// public void on(PlayerTeleportEvent e) { -// if(ignoreTP.get()) { -// System.out.println("IgTP1"); -// return; -// } -// -// if(!PaperLib.isPaper() || e.getTo() == null) { -// System.out.println("IgTP2"); -// -//// return; -// } -// -//// try { -//// System.out.println("IgTP3"); -//// -//// if(e.getTo().getWorld().equals(getTarget().getWorld().realWorld())) { -//// System.out.println("IgTP4"); -//// -//// getEngine().getWorldManager().teleportAsync(e); -//// } -//// } catch(Throwable ex) { -//// -//// } -// } - @EventHandler public void on(WorldSaveEvent e) { if (e.getWorld().equals(getTarget().getWorld().realWorld())) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java index 2d8299db2..2319d965f 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/PreservationRegistry.java @@ -18,9 +18,13 @@ package art.arcane.iris.engine.framework; +import java.util.concurrent.ExecutorService; + public interface PreservationRegistry { void register(Thread thread); + void register(ExecutorService service); + void registerCache(MeteredCache cache); void dereference(); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java index a903d7394..2ba61eacf 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java @@ -23,7 +23,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.util.function.BiFunction; diff --git a/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java b/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java index 5d21b8883..676bea97b 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/Renderer.java @@ -18,7 +18,7 @@ package art.arcane.iris.engine.framework.render; -import java.awt.*; +import java.awt.Color; @FunctionalInterface public interface Renderer { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 1b0454390..1ff81fb3d 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -51,7 +51,10 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; import org.bukkit.util.Vector; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static art.arcane.iris.engine.mantle.EngineMantle.AIR; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 7ebb02bec..34a57028a 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -30,7 +30,25 @@ import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.CarvingMode; +import art.arcane.iris.engine.object.DecayControlPlacer; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.IrisAxisRotationClamp; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisCaveAnchorMode; +import art.arcane.iris.engine.object.IrisCaveProfile; +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.IrisObjectRotation; +import art.arcane.iris.engine.object.IrisObjectScale; +import art.arcane.iris.engine.object.IrisObjectTranslate; +import art.arcane.iris.engine.object.IrisObjectVacuum; +import art.arcane.iris.engine.object.IrisProceduralObjects; +import art.arcane.iris.engine.object.IrisProceduralPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.engine.object.TileData; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java index cf07c0504..c761aeaac 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java @@ -26,7 +26,12 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EngineStage; import art.arcane.iris.engine.framework.IrisEngineMode; -import art.arcane.iris.engine.modifier.*; +import art.arcane.iris.engine.modifier.IrisCarveModifier; +import art.arcane.iris.engine.modifier.IrisCustomModifier; +import art.arcane.iris.engine.modifier.IrisDepositModifier; +import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier; +import art.arcane.iris.engine.modifier.IrisPerfectionModifier; +import art.arcane.iris.engine.modifier.IrisPostModifier; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.scheduling.J; diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java index e132f7a14..4151ed491 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -20,7 +20,13 @@ package art.arcane.iris.engine.modifier; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedModifier; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositVariant; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisProceduralBlocks; +import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.data.HeightMap; import art.arcane.iris.util.project.hunk.Hunk; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java b/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java index 6e8ef0a4a..9d9baa007 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisAttributeModifier.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java b/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java index 3ba2f1841..7a7c00d65 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisAxisRotationClamp.java @@ -18,7 +18,12 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.math.M; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 6bc43644e..114e4d9e4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -24,7 +24,13 @@ import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -46,7 +52,7 @@ import art.arcane.iris.spi.PlatformBlockState; import org.bukkit.NamespacedKey; import org.bukkit.block.Biome; -import java.awt.*; +import java.awt.Color; import java.util.EnumMap; @Accessors(chain = true) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java index 9a3bac1f0..8e9f4d4c9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java @@ -19,7 +19,13 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.nms.datapack.IDataFixer; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -30,7 +36,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import java.awt.*; +import java.awt.Color; import java.util.Locale; @Snippet("custom-biome") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java index d84f422eb..61cc3b037 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomParticle.java @@ -19,7 +19,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.data.registry.RegistryUtil; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java index 0f5e05fbc..b62c9b2dc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java @@ -19,7 +19,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.data.registry.RegistryUtil; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java index 2624f143d..ed27ff39b 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java @@ -19,10 +19,18 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.data.DataProvider; import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; @Snippet("generator-layer") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java index 0262bf44e..db3e455b3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java @@ -20,7 +20,13 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java index ac603f146..3acae87b5 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBlockData.java @@ -23,7 +23,12 @@ import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryMapBlockState; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.volmlib.util.collection.KList; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java index bd58d59b1..34279a511 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java @@ -2,7 +2,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.util.common.math.IrisBlockVector; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.math.M; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java b/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java index 8d8394da1..3a8b255be 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisColor.java @@ -28,7 +28,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import java.awt.*; +import java.awt.Color; @Snippet("color") @Accessors(chain = true) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java index d43023c96..b850e3ec6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCommandRegistry.java @@ -18,7 +18,13 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.platform.bukkit.BukkitWorld; import art.arcane.iris.spi.PlatformWorld; import art.arcane.volmlib.util.collection.KList; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java index 998ae56b0..66cb4a39e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java @@ -20,7 +20,13 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java index fb5a0090e..f7dcb4ca4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java @@ -21,7 +21,12 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.math.Vector3i; import art.arcane.volmlib.util.collection.KList; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index b385070de..4c18cf7f9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -28,7 +28,13 @@ import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.nms.datapack.IDataFixer; import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListFunction; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KSet; @@ -48,7 +54,8 @@ import lombok.experimental.Accessors; import org.bukkit.World.Environment; import org.bukkit.block.Biome; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java index e84244a8d..0a287df32 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionType.java @@ -3,10 +3,17 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.nms.datapack.IDataFixer; import art.arcane.volmlib.util.data.Varint; import art.arcane.volmlib.util.io.IO; -import lombok.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; import lombok.experimental.Accessors; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; @Getter @ToString diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java index 328a2d989..41394e19e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionTypeOptions.java @@ -1,11 +1,16 @@ package art.arcane.iris.engine.object; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.DEFAULT; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.FALSE; +import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE; 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.volmlib.util.data.Varint; import art.arcane.volmlib.util.json.JSONObject; -import lombok.*; import lombok.experimental.Accessors; import org.jetbrains.annotations.Nullable; @@ -14,7 +19,6 @@ import java.io.DataOutput; import java.io.IOException; import java.util.Arrays; -import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.*; @Data @NoArgsConstructor diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java index 2639da840..5a48f40cc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java @@ -20,7 +20,13 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.DependsOn; +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.RegistryListPotionEffect; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.scheduling.ChronoLatch; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java index 5bd58568c..c7cabef49 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEnchantment.java @@ -18,7 +18,12 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListEnchantment; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java index d36c13098..708272719 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java @@ -25,8 +25,13 @@ import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.EngineBukkitOps; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListEntityType; +import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.data.registry.RegistryUtil; import art.arcane.iris.platform.bukkit.BukkitWorld; import art.arcane.iris.spi.PlatformWorld; import art.arcane.volmlib.util.collection.KList; @@ -42,9 +47,18 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import org.bukkit.*; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; import org.bukkit.attribute.Attributable; -import org.bukkit.entity.*; +import org.bukkit.entity.Ageable; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Mob; +import org.bukkit.entity.Panda; +import org.bukkit.entity.Villager; import org.bukkit.entity.Panda.Gene; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.inventory.Inventory; @@ -55,6 +69,7 @@ import org.bukkit.loot.Lootable; import org.bukkit.util.Vector; import java.util.Collection; +import java.util.Locale; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; @@ -71,11 +86,12 @@ import static art.arcane.iris.util.common.data.registry.Particles.ITEM; @EqualsAndHashCode(callSuper = false) public class IrisEntity extends IrisRegistrant { @Required - @Desc("The type of entity to spawn. To spawn a mythic mob, set this type to unknown and define mythic type.") - private EntityType type = null; + @RegistryListEntityType + @Desc("The namespaced key of the entity type to spawn, such as minecraft:zombie or alexsmobs:grizzly_bear. To spawn a mob from another plugin or provider, set this type to unknown and define the special type.") + private String type = null; - @Desc("The SpawnReason to spawn the entity with.") - private CreatureSpawnEvent.SpawnReason reason = null; + @Desc("The SpawnReason name to spawn the entity with, such as NATURAL or COMMAND.") + private String reason = null; @Desc("The custom name of this entity") private String customName = ""; @@ -145,11 +161,11 @@ public class IrisEntity extends IrisRegistrant { @Desc("Simply moves the entity from below the surface slowly out of the ground as a spawn-in effect") private boolean spawnEffectRiseOutOfGround = false; - @Desc("The main gene for a panda if the entity type is a panda") - private Gene pandaMainGene = null; + @Desc("The main gene for a panda if the entity type is a panda. One of NORMAL, LAZY, WORRIED, PLAYFUL, BROWN, WEAK or AGGRESSIVE.") + private String pandaMainGene = null; - @Desc("The hidden gene for a panda if the entity type is a panda") - private Gene pandaHiddenGene = null; + @Desc("The hidden gene for a panda if the entity type is a panda. One of NORMAL, LAZY, WORRIED, PLAYFUL, BROWN, WEAK or AGGRESSIVE.") + private String pandaHiddenGene = null; @Desc("The this entity is ageable, set it's baby status") private boolean baby = false; @@ -171,20 +187,51 @@ public class IrisEntity extends IrisRegistrant { @Desc("Run raw commands when this entity is spawned. Use {x}, {y}, and {z} for location. /summon pig {x} {y} {z}") private KList rawCommands = new KList<>(); - public EntityType getType() { - return type == null ? EntityType.UNKNOWN : type; + public EntityType getBukkitType() { + if (type == null || type.isBlank()) { + return EntityType.UNKNOWN; + } + + NamespacedKey key = NamespacedKey.fromString(type.trim().toLowerCase(Locale.ROOT)); + + if (key == null) { + return EntityType.UNKNOWN; + } + + EntityType entityType = RegistryUtil.lookup(EntityType.class).get(key); + return entityType == null ? EntityType.UNKNOWN : entityType; } - public CreatureSpawnEvent.SpawnReason getReason() { - return reason == null ? CreatureSpawnEvent.SpawnReason.NATURAL : reason; + public CreatureSpawnEvent.SpawnReason getBukkitReason() { + if (reason == null || reason.isBlank()) { + return CreatureSpawnEvent.SpawnReason.NATURAL; + } + + try { + return CreatureSpawnEvent.SpawnReason.valueOf(reason.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return CreatureSpawnEvent.SpawnReason.NATURAL; + } } - public Gene getPandaMainGene() { - return pandaMainGene == null ? Gene.NORMAL : pandaMainGene; + public Gene getBukkitPandaMainGene() { + return resolveGene(pandaMainGene); } - public Gene getPandaHiddenGene() { - return pandaHiddenGene == null ? Gene.NORMAL : pandaHiddenGene; + public Gene getBukkitPandaHiddenGene() { + return resolveGene(pandaHiddenGene); + } + + private static Gene resolveGene(String gene) { + if (gene == null || gene.isBlank()) { + return Gene.NORMAL; + } + + try { + return Gene.valueOf(gene.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return Gene.NORMAL; + } } public Entity spawn(Engine gen, Location at) { @@ -307,8 +354,8 @@ public class IrisEntity extends IrisRegistrant { } if (e instanceof Panda) { - ((Panda) e).setMainGene(getPandaMainGene()); - ((Panda) e).setMainGene(getPandaHiddenGene()); + ((Panda) e).setMainGene(getBukkitPandaMainGene()); + ((Panda) e).setHiddenGene(getBukkitPandaHiddenGene()); } if (e instanceof Villager) { @@ -401,7 +448,7 @@ public class IrisEntity extends IrisRegistrant { return null; } - if (EntityType.UNKNOWN.equals(getType()) && !isSpecialType()) { + if (EntityType.UNKNOWN.equals(getBukkitType()) && !isSpecialType()) { return null; } @@ -435,7 +482,7 @@ public class IrisEntity extends IrisRegistrant { } - return BukkitPlatform.spawnEntity(at, getType(), getReason()); + return BukkitPlatform.spawnEntity(at, getBukkitType(), getBukkitReason()); } public boolean isSpecialType() { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java index 7e97a95a9..316e3a4f0 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java @@ -21,7 +21,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.math.RNG; @@ -177,7 +181,7 @@ public class IrisEntitySpawn implements IRare { return null; } - Vector3d boundingBox = BukkitPlatform.entityBoundingBox(irisEntity.getType()); + Vector3d boundingBox = BukkitPlatform.entityBoundingBox(irisEntity.getBukkitType()); if (!ignoreSurfaces && boundingBox != null) { boolean isClearForSpawn = isAreaClearForSpawn(at, boundingBox); if (!isClearForSpawn) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java index 1038792c0..240fcabda 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionFunction.java @@ -10,7 +10,14 @@ import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.math.RNG; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; import lombok.experimental.Accessors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java index 86bb26d77..8357d4aee 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExpressionLoad.java @@ -27,7 +27,13 @@ import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.stream.ProceduralStream; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.experimental.Accessors; @Snippet("expression-load") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java index 23a063d76..f1f72f0c9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java @@ -20,7 +20,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.Required; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.interpolation.IrisInterpolation; import art.arcane.volmlib.util.json.JSONObject; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java index ed9a63178..d82d929b2 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java @@ -20,7 +20,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java index 965f60e60..000fd2e5a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java @@ -24,7 +24,7 @@ import art.arcane.volmlib.util.json.JSONObject; import art.arcane.iris.util.common.plugin.VolmitSender; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java index 7abf4ef46..1838dc3eb 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.function.NoiseProvider3; import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; import art.arcane.iris.util.project.interpolation.IrisInterpolation; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLake.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLake.java index dcf7ac027..5adc5ccef 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLake.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLake.java @@ -21,7 +21,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java index ef9b126a8..82d4c17f4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java @@ -24,7 +24,13 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.service.ExternalDataSVC; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListItemType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; @@ -49,7 +55,7 @@ import org.bukkit.inventory.meta.LeatherArmorMeta; import org.bukkit.inventory.meta.components.CustomModelDataComponent; import org.bukkit.material.Colorable; -import java.awt.*; +import java.awt.Color; import java.util.List; import java.util.Optional; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java index 5878cee2d..e79c16dcc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java @@ -19,7 +19,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.DataProvider; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java index f09a12622..d31215127 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java @@ -20,7 +20,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +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.math.RNG; import art.arcane.iris.util.project.noise.CNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java index fa31bbd46..9232a0e84 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMod.java @@ -19,7 +19,12 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisRegistrant; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.iris.util.common.plugin.VolmitSender; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java index 72a1f04b3..2129c460b 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeInjector.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java index 248bd76c3..4d6323a15 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModBiomeReplacer.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java index 7a9ba35ce..978cf3297 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModNoiseStyleReplacer.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java index 7f3acf16e..707bf82f0 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementBiomeInjector.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java index fce4720ac..09ee0f121 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectPlacementRegionInjector.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java index 989be20df..470603546 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModObjectReplacer.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java index c22db119f..67ac742fe 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisModRegionReplacer.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java index 1224c582c..1c4d1b459 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java @@ -20,7 +20,12 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.interpolation.IrisInterpolation; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 75053687f..958da404d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -39,7 +39,10 @@ import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.common.math.*; +import art.arcane.iris.util.common.math.AxisAlignedBB; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.common.math.Vector3i; import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.iris.util.common.parallel.BurstExecutor; import art.arcane.iris.util.common.parallel.MultiBurst; @@ -56,8 +59,19 @@ import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; -import java.io.*; -import java.util.*; +import java.io.BufferedInputStream; +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.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -502,7 +516,10 @@ public class IrisObject extends IrisRegistrant { try { latch.await(); - } catch (InterruptedException ignored) {} + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while writing object", interrupted); + } if (ref.get() != null) throw ref.get(); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java index c96bb0e47..4c186b130 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLoot.java @@ -20,7 +20,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java index 6842826fd..d89c2d7db 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectMarker.java @@ -20,7 +20,13 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java index c6e93bf4a..2a9ba1325 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -23,7 +23,13 @@ import art.arcane.iris.platform.bukkit.BukkitBlockResolution; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.volmlib.util.collection.KList; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java index 85075adc7..38c621d5f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectReplace.java @@ -20,7 +20,12 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java index 6ce9d77f0..2b9a67795 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.common.math.IrisBlockVector; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java index 65c651d04..7130f2bb2 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVanillaLoot.java @@ -2,7 +2,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.RegistryListFunction; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.annotations.functions.LootTableKeyFunction; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java index a1af9fa2a..f5cc2f3f1 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java @@ -19,7 +19,12 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListPotionEffect; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java index b830c6501..c58c95b0f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 7cd3cce36..333cfa0e7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -24,7 +24,12 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; @@ -40,7 +45,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import java.awt.*; +import java.awt.Color; import java.util.Random; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiver.java index 361063205..6803fb068 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRiver.java @@ -21,7 +21,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java index c755d6f53..9cc66ac5f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java @@ -19,7 +19,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.annotations.*; +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.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.math.RNG; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerOverride.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerOverride.java index 6b6bbe036..d3e8cc9af 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerOverride.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerOverride.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.DependsOn; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerTrade.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerTrade.java index dc0cba68e..e0fae3011 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerTrade.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVillagerTrade.java @@ -19,7 +19,12 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.*; +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.RegistryListItemType; +import art.arcane.iris.engine.object.annotations.Required; +import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java index 370515381..a94ec3862 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisWorld.java @@ -21,7 +21,11 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; -import lombok.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; import lombok.experimental.Accessors; import org.bukkit.Bukkit; import org.bukkit.Location; diff --git a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java index 0ccae46e3..e11b55cea 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/LegacyTileData.java @@ -1,5 +1,16 @@ package art.arcane.iris.engine.object; +import org.bukkit.Bukkit; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.block.Banner; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.Sign; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.volmlib.util.collection.KList; @@ -11,8 +22,6 @@ import lombok.EqualsAndHashCode; import lombok.NonNull; import lombok.ToString; import org.apache.commons.io.function.IOFunction; -import org.bukkit.*; -import org.bukkit.block.*; import org.bukkit.block.banner.Pattern; import org.bukkit.block.banner.PatternType; import org.bukkit.block.sign.Side; diff --git a/core/src/main/java/art/arcane/iris/engine/object/TileData.java b/core/src/main/java/art/arcane/iris/engine/object/TileData.java index ce266b396..a6e0b6e6e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/TileData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/TileData.java @@ -26,7 +26,12 @@ import com.google.gson.Strictness; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.util.common.reflect.KeyedType; import art.arcane.volmlib.util.collection.KMap; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.block.Block; diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java index 6f87fd418..8c07ecbe1 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/ArrayType.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java index 637952f97..20f7887e0 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/Desc.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java index e1c18632e..23a01a9df 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java index f1b907fd6..9c698782d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBlockType.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java index c4dbd8907..918fe7343 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEnchantment.java @@ -3,7 +3,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java new file mode 100644 index 000000000..74473f2c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListEntityType.java @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListEntityType { + +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java index 90844d846..83a8dc660 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFont.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java index 8f5e3d172..d1157b77a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListFunction.java @@ -6,7 +6,9 @@ import art.arcane.volmlib.util.collection.KList; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java index 7c3167656..b1ff189dd 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListItemType.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java index f2c1b2aa4..9b8493c8f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java @@ -3,7 +3,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java index 45688f3b8..7f8e40b19 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListResource.java @@ -23,7 +23,9 @@ import art.arcane.iris.core.loader.IrisRegistrant; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java index 5821b4f03..528c33c47 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListSpecialEntity.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java index 052eec8da..8f985907c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListStructure.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java index 8d0a2e8f4..138eeb11c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructure.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java index fb46765df..e877e6ff5 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListVanillaStructureSet.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java index 57e8c04ec..a449f470c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/Required.java @@ -21,7 +21,9 @@ package art.arcane.iris.engine.object.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) diff --git a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java index 64eee3069..febc5e515 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java @@ -22,7 +22,12 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.object.IRare; import art.arcane.iris.engine.object.IrisStyledRange; -import art.arcane.iris.engine.object.annotations.*; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; +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.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.RNG; diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index e3a66d57d..3948e2350 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -18,6 +18,14 @@ package art.arcane.iris.engine.platform; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.HeightMap; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisSettings; @@ -54,7 +62,6 @@ import io.papermc.lib.PaperLib; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Setter; -import org.bukkit.*; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -71,7 +78,6 @@ import java.io.PrintWriter; import java.util.Date; import java.util.List; import java.util.Random; -import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java index bd8112cec..ecdfa503d 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitRegistries.java @@ -18,21 +18,36 @@ package art.arcane.iris.platform.bukkit; +import art.arcane.iris.core.link.Identifier; +import art.arcane.iris.core.link.data.DataType; import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.nms.container.BlockProperty; +import art.arcane.iris.core.service.ExternalDataSVC; +import art.arcane.iris.spi.IrisServices; 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; +import art.arcane.iris.spi.PlatformNumericRange; import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.reflect.KeyedType; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; +import org.bukkit.potion.PotionEffectType; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Bukkit adapter resolving namespaced keys against the live Bukkit registries. @@ -116,4 +131,98 @@ public final class BukkitRegistries implements PlatformRegistries { public List structureKeys() { return new ArrayList<>(INMS.get().getStructureKeys()); } + + @Override + public List itemKeys() { + List keys = new ArrayList<>(); + for (Material material : Registry.MATERIAL) { + if (material.isItem()) { + keys.add(material.getKey().toString()); + } + } + ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class); + if (external != null) { + for (Identifier identifier : external.getAllIdentifiers(DataType.ITEM)) { + keys.add(identifier.toString()); + } + } + return keys; + } + + @Override + public List entityKeys() { + List keys = new ArrayList<>(); + for (EntityType type : Registry.ENTITY_TYPE) { + NamespacedKey key = KeyedType.getKey(type); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public List blockTypeKeys() { + return new ArrayList<>(Arrays.asList(BukkitBlockResolution.getBlockTypes())); + } + + @Override + public List enchantmentKeys() { + List keys = new ArrayList<>(); + for (Enchantment enchantment : Registry.ENCHANTMENT) { + NamespacedKey key = KeyedType.getKey(enchantment); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public List potionEffectKeys() { + List keys = new ArrayList<>(); + for (PotionEffectType effect : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(effect); + if (key != null) { + keys.add(key.toString()); + } + } + return keys; + } + + @Override + public Map> blockStateProperties() { + Map> properties = new LinkedHashMap<>(); + BukkitBlockResolution.getBlockStates().forEach((blocks, blockProperties) -> { + if (blocks.isEmpty()) { + return; + } + List converted = new ArrayList<>(blockProperties.size()); + for (BlockProperty blockProperty : blockProperties) { + converted.add(toPlatformProperty(blockProperty)); + } + List shared = List.copyOf(converted); + for (String block : blocks) { + properties.put(block, shared); + } + }); + return properties; + } + + private static PlatformBlockProperty toPlatformProperty(BlockProperty property) { + JSONObject json = property.buildJson(); + Object defaultValue = json.has("default") ? json.get("default") : null; + List allowedValues = new ArrayList<>(); + if (json.has("enum")) { + JSONArray values = json.getJSONArray("enum"); + for (int index = 0; index < values.length(); index++) { + allowedValues.add(values.get(index)); + } + } + PlatformNumericRange range = null; + if (json.has("minimum")) { + range = new PlatformNumericRange(json.getDouble("minimum"), json.getDouble("maximum"), json.getBoolean("exclusiveMinimum"), json.getBoolean("exclusiveMaximum")); + } + return new PlatformBlockProperty(property.name(), json.getString("type"), defaultValue, List.copyOf(allowedValues), range); + } } diff --git a/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java b/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java index 15ebc41d2..67f13c6ef 100644 --- a/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/IrisCustomData.java @@ -8,7 +8,9 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Proxy; -import java.util.*; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; public interface IrisCustomData extends BlockData { @NonNull BlockData getBase(); diff --git a/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java index 64874211c..c6543f258 100644 --- a/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/KeyedRegistry.java @@ -8,7 +8,9 @@ import org.bukkit.Registry; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; diff --git a/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java index dee15b228..3b1eb553b 100644 --- a/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/registry/RegistryUtil.java @@ -15,7 +15,9 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.*; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import static art.arcane.iris.util.common.data.registry.KeyedRegistry.wrapped; diff --git a/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java b/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java index 2b1938bbc..552288c54 100644 --- a/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java +++ b/core/src/main/java/art/arcane/iris/util/common/misc/getHardware.java @@ -5,7 +5,12 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import oshi.SystemInfo; -import oshi.hardware.*; +import oshi.hardware.CentralProcessor; +import oshi.hardware.Display; +import oshi.hardware.GraphicsCard; +import oshi.hardware.HWDiskStore; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.PowerSource; import oshi.software.os.OSFileStore; import oshi.software.os.OperatingSystem; diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java index 97ed8507f..e5ae9f477 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java @@ -27,7 +27,10 @@ import art.arcane.volmlib.util.reflect.V; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; import org.bukkit.command.Command; -import org.bukkit.command.*; +import org.bukkit.command.CommandMap; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.SimpleCommandMap; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.event.HandlerList; diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java index 70b1eb020..496efaa43 100644 --- a/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java @@ -21,15 +21,32 @@ package art.arcane.iris.util.project.hunk; import art.arcane.iris.engine.data.chunk.TerrainChunk; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.function.*; +import art.arcane.volmlib.util.function.Consumer2; +import art.arcane.volmlib.util.function.Consumer3; +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.function.Consumer4IO; +import art.arcane.volmlib.util.function.Consumer5; +import art.arcane.volmlib.util.function.Consumer6; +import art.arcane.volmlib.util.function.Consumer8; +import art.arcane.volmlib.util.function.Function3; +import art.arcane.volmlib.util.function.Function4; +import art.arcane.volmlib.util.function.Supplier3R; import art.arcane.volmlib.util.hunk.HunkComputeSupport; import art.arcane.volmlib.util.hunk.HunkCoreSupport; import art.arcane.volmlib.util.hunk.HunkInterpolationSupport; import art.arcane.volmlib.util.hunk.HunkMutationSupport; import art.arcane.volmlib.util.hunk.HunkSectionSupport; import art.arcane.volmlib.util.hunk.HunkLike; -import art.arcane.iris.util.project.hunk.storage.*; -import art.arcane.iris.util.project.hunk.view.*; +import art.arcane.iris.util.project.hunk.storage.ArrayHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicDoubleHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicIntegerHunk; +import art.arcane.iris.util.project.hunk.storage.AtomicLongHunk; +import art.arcane.iris.util.project.hunk.storage.SynchronizedArrayHunk; +import art.arcane.iris.util.project.hunk.view.ChunkBiomeHunkView; +import art.arcane.iris.util.project.hunk.view.ChunkDataHunkView; +import art.arcane.iris.util.project.hunk.view.ChunkHunkView; +import art.arcane.iris.util.project.hunk.view.TerrainChunkBiomeHunkView; import art.arcane.iris.util.project.interpolation.InterpolationMethod; import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; import art.arcane.iris.util.project.interpolation.IrisInterpolation; diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java index bbbd82c86..6960a7a5c 100644 --- a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java @@ -21,6 +21,7 @@ package art.arcane.iris.util.project.interpolation; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.volmlib.util.interpolation.Starcast; import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.function.Consumer2; import art.arcane.volmlib.util.function.NoiseProvider; @@ -324,20 +325,20 @@ public class IrisInterpolation { p.end(); - System.out.println(m + ": " + Form.duration(p.getMilliseconds(), 8)); + IrisLogging.info("%s", m + ": " + Form.duration(p.getMilliseconds(), 8)); } public static void printOptimizedSrc(boolean arrays) { - System.out.println(generateOptimizedStarcast(3, arrays)); - System.out.println(generateOptimizedStarcast(5, arrays)); - System.out.println(generateOptimizedStarcast(6, arrays)); - System.out.println(generateOptimizedStarcast(7, arrays)); - System.out.println(generateOptimizedStarcast(9, arrays)); - System.out.println(generateOptimizedStarcast(12, arrays)); - System.out.println(generateOptimizedStarcast(24, arrays)); - System.out.println(generateOptimizedStarcast(32, arrays)); - System.out.println(generateOptimizedStarcast(48, arrays)); - System.out.println(generateOptimizedStarcast(64, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(3, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(5, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(6, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(7, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(9, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(12, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(24, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(32, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(48, arrays)); + IrisLogging.info("%s", generateOptimizedStarcast(64, arrays)); } public static String generateOptimizedStarcast(double checks, boolean array) { diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java index d933f590a..161156306 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java @@ -35,11 +35,41 @@ import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.common.parallel.BurstExecutor; import art.arcane.iris.util.common.parallel.MultiBurst; -import art.arcane.iris.util.project.stream.arithmetic.*; -import art.arcane.iris.util.project.stream.convert.*; +import art.arcane.iris.util.project.stream.arithmetic.AddingStream; +import art.arcane.iris.util.project.stream.arithmetic.ClampedStream; +import art.arcane.iris.util.project.stream.arithmetic.CoordinateBitShiftLeftStream; +import art.arcane.iris.util.project.stream.arithmetic.CoordinateBitShiftRightStream; +import art.arcane.iris.util.project.stream.arithmetic.DividingStream; +import art.arcane.iris.util.project.stream.arithmetic.FittedStream; +import art.arcane.iris.util.project.stream.arithmetic.MaxingStream; +import art.arcane.iris.util.project.stream.arithmetic.MinningStream; +import art.arcane.iris.util.project.stream.arithmetic.ModuloStream; +import art.arcane.iris.util.project.stream.arithmetic.MultiplyingStream; +import art.arcane.iris.util.project.stream.arithmetic.OffsetStream; +import art.arcane.iris.util.project.stream.arithmetic.RadialStream; +import art.arcane.iris.util.project.stream.arithmetic.RoundingDoubleStream; +import art.arcane.iris.util.project.stream.arithmetic.SlopeStream; +import art.arcane.iris.util.project.stream.arithmetic.SubtractingStream; +import art.arcane.iris.util.project.stream.arithmetic.ZoomStream; +import art.arcane.iris.util.project.stream.convert.AwareConversionStream2D; +import art.arcane.iris.util.project.stream.convert.AwareConversionStream3D; +import art.arcane.iris.util.project.stream.convert.CachedConversionStream; +import art.arcane.iris.util.project.stream.convert.ConversionStream; +import art.arcane.iris.util.project.stream.convert.ForceDoubleStream; +import art.arcane.iris.util.project.stream.convert.RoundingStream; +import art.arcane.iris.util.project.stream.convert.SelectionStream; +import art.arcane.iris.util.project.stream.convert.SignificanceStream; +import art.arcane.iris.util.project.stream.convert.To3DStream; import art.arcane.iris.util.project.stream.interpolation.Interpolated; import art.arcane.iris.util.project.stream.sources.FunctionStream; -import art.arcane.iris.util.project.stream.utility.*; +import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D; +import art.arcane.iris.util.project.stream.utility.CachedStream2D; +import art.arcane.iris.util.project.stream.utility.CachedStream3D; +import art.arcane.iris.util.project.stream.utility.ContextInjectingStream; +import art.arcane.iris.util.project.stream.utility.NullSafeStream; +import art.arcane.iris.util.project.stream.utility.ProfiledStream; +import art.arcane.iris.util.project.stream.utility.SemaphoreStream; +import art.arcane.iris.util.project.stream.utility.SynchronizedStream; import java.util.ArrayList; import java.util.List; diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/UBufferedImage.java b/core/src/main/java/art/arcane/iris/util/project/uniques/UBufferedImage.java index 0c2346069..8f5fc0c5b 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/UBufferedImage.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/UBufferedImage.java @@ -1,6 +1,6 @@ package art.arcane.iris.util.project.uniques; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.WritableRaster; diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/UFeature.java b/core/src/main/java/art/arcane/iris/util/project/uniques/UFeature.java index da349b881..981366424 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/UFeature.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/UFeature.java @@ -4,7 +4,7 @@ import art.arcane.volmlib.util.function.NoiseInjector; import art.arcane.iris.util.project.interpolation.InterpolationMethod; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; -import java.awt.*; +import java.awt.Color; import java.util.List; import java.util.function.Consumer; diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/UImage.java b/core/src/main/java/art/arcane/iris/util/project/uniques/UImage.java index 7da458d6c..30dc4d39d 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/UImage.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/UImage.java @@ -1,6 +1,6 @@ package art.arcane.iris.util.project.uniques; -import java.awt.*; +import java.awt.Color; public interface UImage { int getWidth(); diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/UniqueRenderer.java b/core/src/main/java/art/arcane/iris/util/project/uniques/UniqueRenderer.java index 121ee7d9b..97a230a56 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/UniqueRenderer.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/UniqueRenderer.java @@ -1,289 +1,302 @@ -package art.arcane.iris.util.project.uniques; - -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.function.NoiseProvider; -import art.arcane.iris.util.project.interpolation.InterpolationMethod; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.noise.CNG; -import art.arcane.iris.util.common.parallel.BurstExecutor; -import art.arcane.volmlib.util.scheduling.ChronoLatch; -import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.iris.util.project.uniques.features.*; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; - -public class UniqueRenderer { - static final List backgrounds = List.of(new UFWarpedBackground()); - static final List interpolators = List.of(new UFInterpolator(), new UFNOOP()); - static final List features = List.of(new UFWarpedLines(), new UFWarpedDisc(), new UFWarpedDots(), new UFWarpedCircle()); - static UniqueRenderer renderer; - private final String seed; - private final ProceduralStream spatialSeed; - private final int width; - private final int height; - private final KMap writing = new KMap<>(); - private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); - private final KList sortedStyles = new KList(); - private final KList sortedInterpolators = new KList(); - int cores = Runtime.getRuntime().availableProcessors(); - - public UniqueRenderer(String seed, int width, int height) { - renderer = this; - computeNoiseStyles(3000, 2); - computeInterpolationMethods(3000, 2); - this.seed = seed; - this.width = width; - this.height = height; - spatialSeed = NoiseStyle.FRACTAL_WATER.stream(new RNG(seed)).convert((d) -> new RNG(Math.round(seed.hashCode() + (d * 934321234D)))); - new Thread(() -> { - while (true) { - J.sleep(5000); - - if (!writing.isEmpty()) { - System.out.println(Form.repeat("\n", 60)); - System.out.println(Form.memSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), 2) + " of " + Form.memSize(Runtime.getRuntime().totalMemory(), 2)); - KMap c = writing.copy(); - - for (String i : writing.k().sort()) { - String prog = ""; - String f = writing.get(i); - - if (f.contains("%")) { - String v = f.split("\\Q%\\E")[0]; - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 2))) / 100D, 30); - } catch (Throwable e) { - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 1))) / 100D, 30); - } catch (Throwable ee) { - try { - prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 3))) / 100D, 30); - } catch (Throwable eee) { - - } - } - } - } - - System.out.println(prog + " " + i + " => " + f); - } - } - } - }).start(); - } - - public UMeta renderFrameBuffer(long id, double t) { - UMeta meta = new UMeta(); - meta.setId(id); - meta.setTime(t); - RNG rng = spatialSeed.get(id, id + ((id * id) % (id / 3D))); - RNG rngbg = spatialSeed.get(id, -id + ((id * id) % (id / 4D))); - BufferedImage buf = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - BufferedImage bufFG = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - UImage image = new UBufferedImage(buf); - UImage imageFG = new UBufferedImage(bufFG); - ChronoLatch cl = new ChronoLatch(250); - UFeature background = rng.pick(backgrounds); - UFeature interpolator = rng.pick(interpolators); - UFeature foreground = rng.pick(features); - UFeature foregroundInterpolator = rng.pick(interpolators); - UFeatureMeta backgroundMeta = new UFeatureMeta(); - UFeatureMeta foregroundMeta = new UFeatureMeta(); - UFeatureMeta backgroundInterpolatorMeta = new UFeatureMeta(); - UFeatureMeta foregroundInterpolatorMeta = new UFeatureMeta(); - background.render(image, rngbg, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(p / 4D) + " [" + background.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, backgroundMeta); - backgroundMeta.setFeature(background.getClass().getSimpleName()); - meta.registerFeature("background", backgroundMeta); - interpolator.render(image, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.25 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, backgroundInterpolatorMeta); - backgroundInterpolatorMeta.setFeature(interpolator.getClass().getSimpleName()); - meta.registerFeature("backgroundInterpolator", backgroundInterpolatorMeta); - foreground.render(imageFG, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.5 + (p / 4d)) + " [" + foreground.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, foregroundMeta); - foregroundMeta.setFeature(foreground.getClass().getSimpleName()); - meta.registerFeature("foreground", foregroundMeta); - overlay(imageFG, bufFG, image); - foregroundInterpolator.render(image, rng, t, (p) -> { - if (cl.flip()) { - writing.put("#" + id + ":" + t, Form.pc(0.75 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); - } - }, foregroundInterpolatorMeta); - foregroundInterpolatorMeta.setFeature(foregroundInterpolator.getClass().getSimpleName()); - meta.registerFeature("foregroundInterpolator", foregroundInterpolatorMeta); - overlay(imageFG, bufFG, image); - meta.setImage(buf); - writing.remove("#" + id + ":" + t); - return meta; - } - - private void overlay(UImage layer, BufferedImage layerBuf, UImage onto) { - for (int i = 0; i < onto.getWidth(); i++) { - for (int j = 0; j < onto.getHeight(); j++) { - if (layerBuf.getRGB(i, j) != 0) { - onto.set(i, j, layer.get(i, j)); - } - } - } - } - - private String drawProgress(double progress, int len) { - int max = len; - int in = (int) Math.round(progress * max); - max -= in; - - return "[" + Form.repeat("=", in) + Form.repeat(" ", max) + "]"; - } - - private void computeNoiseStyles(double time, double scope) { - List allowedStyles = new KList<>(NoiseStyle.values()); - allowedStyles.remove(NoiseStyle.FLAT); - KMap speeds = new KMap<>(); - double allocateMS = time; - double maxTestDuration = allocateMS / allowedStyles.size(); - System.out.println("Running Noise Style Benchmark for " + Form.duration(allocateMS, 0) + "."); - System.out.println("Benchmarking " + allowedStyles.size() + " + Noise Styles for " + Form.duration(maxTestDuration, 1) + " each."); - System.out.println(); - - for (NoiseStyle i : allowedStyles) { - int score = 0; - CNG cng = i.create(new RNG("renderspeedtest")); - PrecisionStopwatch p = PrecisionStopwatch.start(); - double g = 0; - while (p.getMilliseconds() < maxTestDuration) { - cng.noise(g, -g * 2); - g += 0.1; - g *= 1.25; - score++; - } - - speeds.put(i, score); - } - - for (NoiseStyle i : speeds.sortKNumber()) { - System.out.println(Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); - } - System.out.println(); - int takeUpTo = (int) Math.max(1, scope * speeds.size()); - System.out.println("Choosing the fastest " + Form.pc(scope) + " styles (" + takeUpTo + ")"); - - for (NoiseStyle i : speeds.sortKNumber().reverse()) { - if (takeUpTo-- <= 0) { - break; - } - - sortedStyles.add(i); - System.out.println("- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); - } - } - - private void computeInterpolationMethods(double time, double scope) { - List allowedStyles = new KList<>(InterpolationMethod.values()); - allowedStyles.remove(InterpolationMethod.NONE); - KMap speeds = new KMap<>(); - double allocateMS = time; - double maxTestDuration = allocateMS / allowedStyles.size(); - System.out.println("Running Interpolation Method Benchmark for " + Form.duration(allocateMS, 0) + "."); - System.out.println("Benchmarking " + allowedStyles.size() + " + Interpolation Methods for " + Form.duration(maxTestDuration, 1) + " each."); - System.out.println(); - - RNG r = new RNG("renderspeedtestinterpolation"); - CNG cng = NoiseStyle.SIMPLEX.create(r); - NoiseProvider np = (x, z) -> cng.noise(x, z); - - for (InterpolationMethod i : allowedStyles) { - int score = 0; - - PrecisionStopwatch p = PrecisionStopwatch.start(); - double g = 0; - while (p.getMilliseconds() < maxTestDuration) { - IrisInterpolation.getNoise(i, (int) g, (int) (-g * 2.225), r.d(4, 64), np); - cng.noise(g, -g * 2); - g += 1.1; - g *= 1.25; - score++; - } - - speeds.put(i, score); - } - - for (InterpolationMethod i : speeds.sortKNumber()) { - System.out.println(Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); - } - System.out.println(); - int takeUpTo = (int) Math.max(1, scope * speeds.size()); - System.out.println("Choosing the fastest " + Form.pc(scope) + " interpolators (" + takeUpTo + ")"); - - for (InterpolationMethod i : speeds.sortKNumber().reverse()) { - if (takeUpTo-- <= 0) { - break; - } - - sortedInterpolators.add(i); - System.out.println("- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); - } - } - - public void writeCollectionFrames(File folder, int fromId, int toId) { - folder.mkdirs(); - BurstExecutor burst = new BurstExecutor(executor, Math.min(toId - fromId, 1000)); - burst.setMulticore(true); - AtomicInteger ai = new AtomicInteger(0); - int max = toId - fromId; - - for (int i = fromId; i <= toId; i++) { - int ii = i; - burst.queue(() -> { - - writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); - writeFrame(new File(folder, ii + ".png"), ii, 0); - ai.incrementAndGet(); - writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); - }); - } - - burst.complete(); - writing.remove("!#[" + fromId + "-" + toId + "] Collection"); - } - - public void writeFrame(File destination, long id, double t) { - try { - renderFrameBuffer(id, t).export(destination); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - public void report(String s) { - System.out.println(s); - } - - public KList getStyles() { - return sortedStyles; - } - - public List getInterpolators() { - return sortedInterpolators; - } -} +package art.arcane.iris.util.project.uniques; + +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.uniques.features.UFInterpolator; +import art.arcane.iris.util.project.uniques.features.UFNOOP; +import art.arcane.iris.util.project.uniques.features.UFWarpedBackground; +import art.arcane.iris.util.project.uniques.features.UFWarpedCircle; +import art.arcane.iris.util.project.uniques.features.UFWarpedDisc; +import art.arcane.iris.util.project.uniques.features.UFWarpedDots; +import art.arcane.iris.util.project.uniques.features.UFWarpedLines; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +public class UniqueRenderer { + static final List backgrounds = List.of(new UFWarpedBackground()); + static final List interpolators = List.of(new UFInterpolator(), new UFNOOP()); + static final List features = List.of(new UFWarpedLines(), new UFWarpedDisc(), new UFWarpedDots(), new UFWarpedCircle()); + static UniqueRenderer renderer; + private final String seed; + private final ProceduralStream spatialSeed; + private final int width; + private final int height; + private final KMap writing = new KMap<>(); + private final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); + private final KList sortedStyles = new KList(); + private final KList sortedInterpolators = new KList(); + int cores = Runtime.getRuntime().availableProcessors(); + + public UniqueRenderer(String seed, int width, int height) { + renderer = this; + PreservationRegistry preservation = IrisServices.getOrNull(PreservationRegistry.class); + if (preservation != null) { + preservation.register(executor); + } + computeNoiseStyles(3000, 2); + computeInterpolationMethods(3000, 2); + this.seed = seed; + this.width = width; + this.height = height; + spatialSeed = NoiseStyle.FRACTAL_WATER.stream(new RNG(seed)).convert((d) -> new RNG(Math.round(seed.hashCode() + (d * 934321234D)))); + new Thread(() -> { + while (true) { + J.sleep(5000); + + if (!writing.isEmpty()) { + IrisLogging.info("%s", Form.repeat("\n", 60)); + IrisLogging.info("%s", Form.memSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), 2) + " of " + Form.memSize(Runtime.getRuntime().totalMemory(), 2)); + KMap c = writing.copy(); + + for (String i : writing.k().sort()) { + String prog = ""; + String f = writing.get(i); + + if (f.contains("%")) { + String v = f.split("\\Q%\\E")[0]; + try { + prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 2))) / 100D, 30); + } catch (Throwable e) { + try { + prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 1))) / 100D, 30); + } catch (Throwable ee) { + try { + prog = drawProgress(Double.valueOf(Integer.parseInt(v.substring(v.length() - 3))) / 100D, 30); + } catch (Throwable eee) { + + } + } + } + } + + IrisLogging.info("%s", prog + " " + i + " => " + f); + } + } + } + }).start(); + } + + public UMeta renderFrameBuffer(long id, double t) { + UMeta meta = new UMeta(); + meta.setId(id); + meta.setTime(t); + RNG rng = spatialSeed.get(id, id + ((id * id) % (id / 3D))); + RNG rngbg = spatialSeed.get(id, -id + ((id * id) % (id / 4D))); + BufferedImage buf = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + BufferedImage bufFG = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + UImage image = new UBufferedImage(buf); + UImage imageFG = new UBufferedImage(bufFG); + ChronoLatch cl = new ChronoLatch(250); + UFeature background = rng.pick(backgrounds); + UFeature interpolator = rng.pick(interpolators); + UFeature foreground = rng.pick(features); + UFeature foregroundInterpolator = rng.pick(interpolators); + UFeatureMeta backgroundMeta = new UFeatureMeta(); + UFeatureMeta foregroundMeta = new UFeatureMeta(); + UFeatureMeta backgroundInterpolatorMeta = new UFeatureMeta(); + UFeatureMeta foregroundInterpolatorMeta = new UFeatureMeta(); + background.render(image, rngbg, t, (p) -> { + if (cl.flip()) { + writing.put("#" + id + ":" + t, Form.pc(p / 4D) + " [" + background.getClass().getSimpleName() + " " + Form.pc(p) + "]"); + } + }, backgroundMeta); + backgroundMeta.setFeature(background.getClass().getSimpleName()); + meta.registerFeature("background", backgroundMeta); + interpolator.render(image, rng, t, (p) -> { + if (cl.flip()) { + writing.put("#" + id + ":" + t, Form.pc(0.25 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); + } + }, backgroundInterpolatorMeta); + backgroundInterpolatorMeta.setFeature(interpolator.getClass().getSimpleName()); + meta.registerFeature("backgroundInterpolator", backgroundInterpolatorMeta); + foreground.render(imageFG, rng, t, (p) -> { + if (cl.flip()) { + writing.put("#" + id + ":" + t, Form.pc(0.5 + (p / 4d)) + " [" + foreground.getClass().getSimpleName() + " " + Form.pc(p) + "]"); + } + }, foregroundMeta); + foregroundMeta.setFeature(foreground.getClass().getSimpleName()); + meta.registerFeature("foreground", foregroundMeta); + overlay(imageFG, bufFG, image); + foregroundInterpolator.render(image, rng, t, (p) -> { + if (cl.flip()) { + writing.put("#" + id + ":" + t, Form.pc(0.75 + (p / 4d)) + " [" + interpolator.getClass().getSimpleName() + " " + Form.pc(p) + "]"); + } + }, foregroundInterpolatorMeta); + foregroundInterpolatorMeta.setFeature(foregroundInterpolator.getClass().getSimpleName()); + meta.registerFeature("foregroundInterpolator", foregroundInterpolatorMeta); + overlay(imageFG, bufFG, image); + meta.setImage(buf); + writing.remove("#" + id + ":" + t); + return meta; + } + + private void overlay(UImage layer, BufferedImage layerBuf, UImage onto) { + for (int i = 0; i < onto.getWidth(); i++) { + for (int j = 0; j < onto.getHeight(); j++) { + if (layerBuf.getRGB(i, j) != 0) { + onto.set(i, j, layer.get(i, j)); + } + } + } + } + + private String drawProgress(double progress, int len) { + int max = len; + int in = (int) Math.round(progress * max); + max -= in; + + return "[" + Form.repeat("=", in) + Form.repeat(" ", max) + "]"; + } + + private void computeNoiseStyles(double time, double scope) { + List allowedStyles = new KList<>(NoiseStyle.values()); + allowedStyles.remove(NoiseStyle.FLAT); + KMap speeds = new KMap<>(); + double allocateMS = time; + double maxTestDuration = allocateMS / allowedStyles.size(); + IrisLogging.info("%s", "Running Noise Style Benchmark for " + Form.duration(allocateMS, 0) + "."); + IrisLogging.info("%s", "Benchmarking " + allowedStyles.size() + " + Noise Styles for " + Form.duration(maxTestDuration, 1) + " each."); + IrisLogging.info(""); + + for (NoiseStyle i : allowedStyles) { + int score = 0; + CNG cng = i.create(new RNG("renderspeedtest")); + PrecisionStopwatch p = PrecisionStopwatch.start(); + double g = 0; + while (p.getMilliseconds() < maxTestDuration) { + cng.noise(g, -g * 2); + g += 0.1; + g *= 1.25; + score++; + } + + speeds.put(i, score); + } + + for (NoiseStyle i : speeds.sortKNumber()) { + IrisLogging.info("%s", Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); + } + IrisLogging.info(""); + int takeUpTo = (int) Math.max(1, scope * speeds.size()); + IrisLogging.info("%s", "Choosing the fastest " + Form.pc(scope) + " styles (" + takeUpTo + ")"); + + for (NoiseStyle i : speeds.sortKNumber().reverse()) { + if (takeUpTo-- <= 0) { + break; + } + + sortedStyles.add(i); + IrisLogging.info("%s", "- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); + } + } + + private void computeInterpolationMethods(double time, double scope) { + List allowedStyles = new KList<>(InterpolationMethod.values()); + allowedStyles.remove(InterpolationMethod.NONE); + KMap speeds = new KMap<>(); + double allocateMS = time; + double maxTestDuration = allocateMS / allowedStyles.size(); + IrisLogging.info("%s", "Running Interpolation Method Benchmark for " + Form.duration(allocateMS, 0) + "."); + IrisLogging.info("%s", "Benchmarking " + allowedStyles.size() + " + Interpolation Methods for " + Form.duration(maxTestDuration, 1) + " each."); + IrisLogging.info(""); + + RNG r = new RNG("renderspeedtestinterpolation"); + CNG cng = NoiseStyle.SIMPLEX.create(r); + NoiseProvider np = (x, z) -> cng.noise(x, z); + + for (InterpolationMethod i : allowedStyles) { + int score = 0; + + PrecisionStopwatch p = PrecisionStopwatch.start(); + double g = 0; + while (p.getMilliseconds() < maxTestDuration) { + IrisInterpolation.getNoise(i, (int) g, (int) (-g * 2.225), r.d(4, 64), np); + cng.noise(g, -g * 2); + g += 1.1; + g *= 1.25; + score++; + } + + speeds.put(i, score); + } + + for (InterpolationMethod i : speeds.sortKNumber()) { + IrisLogging.info("%s", Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + " => " + Form.f(speeds.get(i))); + } + IrisLogging.info(""); + int takeUpTo = (int) Math.max(1, scope * speeds.size()); + IrisLogging.info("%s", "Choosing the fastest " + Form.pc(scope) + " interpolators (" + takeUpTo + ")"); + + for (InterpolationMethod i : speeds.sortKNumber().reverse()) { + if (takeUpTo-- <= 0) { + break; + } + + sortedInterpolators.add(i); + IrisLogging.info("%s", "- " + Form.capitalizeWords(i.name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); + } + } + + public void writeCollectionFrames(File folder, int fromId, int toId) { + folder.mkdirs(); + BurstExecutor burst = new BurstExecutor(executor, Math.min(toId - fromId, 1000)); + burst.setMulticore(true); + AtomicInteger ai = new AtomicInteger(0); + int max = toId - fromId; + + for (int i = fromId; i <= toId; i++) { + int ii = i; + burst.queue(() -> { + + writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); + writeFrame(new File(folder, ii + ".png"), ii, 0); + ai.incrementAndGet(); + writing.put("!#[" + fromId + "-" + toId + "] Collection", ai.get() + " of " + max + " (" + Form.pc(ai.get() / (double) max, 0) + ")"); + }); + } + + burst.complete(); + writing.remove("!#[" + fromId + "-" + toId + "] Collection"); + } + + public void writeFrame(File destination, long id, double t) { + try { + renderFrameBuffer(id, t).export(destination); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public void report(String s) { + IrisLogging.info("%s", s); + } + + public KList getStyles() { + return sortedStyles; + } + + public List getInterpolators() { + return sortedInterpolators; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFInterpolator.java b/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFInterpolator.java index b0db91df5..4b3a38e1e 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFInterpolator.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFInterpolator.java @@ -8,7 +8,7 @@ import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.uniques.UFeature; import art.arcane.iris.util.project.uniques.UFeatureMeta; import art.arcane.iris.util.project.uniques.UImage; -import java.awt.*; +import java.awt.Color; import java.util.function.Consumer; public class UFInterpolator implements UFeature { diff --git a/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFWarpedDots.java b/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFWarpedDots.java index 4d6fd2a49..df79d6ca6 100644 --- a/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFWarpedDots.java +++ b/core/src/main/java/art/arcane/iris/util/project/uniques/features/UFWarpedDots.java @@ -5,7 +5,7 @@ import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.uniques.UFeature; import art.arcane.iris.util.project.uniques.UFeatureMeta; import art.arcane.iris.util.project.uniques.UImage; -import java.awt.*; +import java.awt.Color; import java.util.function.Consumer; public class UFWarpedDots implements UFeature { diff --git a/core/src/main/templates/BuildConstants.java b/core/src/main/templates/BuildConstants.java index 23f77cea3..928cb279d 100644 --- a/core/src/main/templates/BuildConstants.java +++ b/core/src/main/templates/BuildConstants.java @@ -4,4 +4,5 @@ package art.arcane.iris; public interface BuildConstants { String ENVIRONMENT = "${environment}"; String COMMIT = "${commit}"; + String MINECRAFT_VERSION = "${minecraftVersion}"; } \ No newline at end of file diff --git a/core/src/test/java/art/arcane/iris/core/pack/ContentKeyValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/ContentKeyValidatorTest.java new file mode 100644 index 000000000..9ebaaf823 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pack/ContentKeyValidatorTest.java @@ -0,0 +1,178 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.pack.ContentKeyValidator.ContentKeyError; +import art.arcane.iris.core.pack.ContentKeyValidator.ContentRegistry; +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; +import art.arcane.iris.spi.PlatformRegistries; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ContentKeyValidatorTest { + private static PlatformRegistries registries() { + return new FakeRegistries( + List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:oak_log", "minecraft:grass_block"), + List.of("minecraft:diamond", "minecraft:wooden_pickaxe", "minecraft:stone_pickaxe"), + List.of("minecraft:zombie", "minecraft:creeper")); + } + + @Test + public void validateFlagsUnknownBlockKeyWithSingleError() { + List errors = ContentKeyValidator.validate(registries(), + List.of("minecraft:stone", "minecraft:not_a_real_block"), List.of(), List.of()); + assertEquals(1, errors.size()); + ContentKeyError error = errors.get(0); + assertEquals("minecraft:not_a_real_block", error.key()); + assertEquals(ContentRegistry.BLOCK, error.registry()); + assertTrue(error.namespaceLoaded()); + } + + @Test + public void validateFlagsUnloadedNamespaceWhenNamespaceMissing() { + List errors = ContentKeyValidator.validate(registries(), + List.of("create:cogwheel"), List.of(), List.of()); + assertEquals(1, errors.size()); + assertFalse(errors.get(0).namespaceLoaded()); + assertEquals("create", ContentKeyValidator.namespaceOf(errors.get(0).key())); + } + + @Test + public void validateSuggestsNearestKeyForTypo() { + List errors = ContentKeyValidator.validate(registries(), + List.of("minecraft:cobblstone"), List.of(), List.of()); + assertEquals(1, errors.size()); + assertEquals("minecraft:cobblestone", errors.get(0).suggestion()); + } + + @Test + public void validateDedupsRepeatedKeyIntoOneError() { + List errors = ContentKeyValidator.validate(registries(), + List.of("minecraft:ghostblock", "minecraft:ghostblock", "ghostblock"), List.of(), List.of()); + assertEquals(1, errors.size()); + assertEquals("minecraft:ghostblock", errors.get(0).key()); + } + + @Test + public void validateDefaultsBareKeyToMinecraftNamespace() { + List errors = ContentKeyValidator.validate(registries(), + List.of("stone", "oak_log"), List.of(), List.of()); + assertTrue(errors.isEmpty()); + } + + @Test + public void validateStripsBlockStateBeforeLookup() { + List errors = ContentKeyValidator.validate(registries(), + List.of("minecraft:oak_log[axis=y]"), List.of(), List.of()); + assertTrue(errors.isEmpty()); + } + + @Test + public void validateReportsUnknownItemAndEntityPerRegistry() { + List errors = ContentKeyValidator.validate(registries(), + List.of(), List.of("minecraft:not_an_item"), List.of("minecraft:not_an_entity")); + assertEquals(2, errors.size()); + assertTrue(errors.stream().anyMatch(e -> e.registry() == ContentRegistry.ITEM && e.key().equals("minecraft:not_an_item"))); + assertTrue(errors.stream().anyMatch(e -> e.registry() == ContentRegistry.ENTITY && e.key().equals("minecraft:not_an_entity"))); + } + + @Test + public void validateReturnsEmptyWhenRegistriesNull() { + assertTrue(ContentKeyValidator.validate(null, List.of("minecraft:whatever"), List.of(), List.of()).isEmpty()); + } + + private record FakeRegistries(List blocks, List items, List entities) implements PlatformRegistries { + @Override + public PlatformBlockState block(String key) { + return null; + } + + @Override + public PlatformBlockState blockOrNull(String key) { + return null; + } + + @Override + public PlatformBlockState blockOrNull(String key, boolean warn) { + return null; + } + + @Override + public PlatformBlockState air() { + return null; + } + + @Override + public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) { + return null; + } + + @Override + public PlatformBiome biome(String key) { + return null; + } + + @Override + public PlatformItem item(String key) { + return null; + } + + @Override + public PlatformEntityType entity(String key) { + return null; + } + + @Override + public List blockKeys() { + return blocks; + } + + @Override + public List biomeKeys() { + return List.of(); + } + + @Override + public List structureKeys() { + return List.of(); + } + + @Override + public List itemKeys() { + return items; + } + + @Override + public List entityKeys() { + return entities; + } + + @Override + public List blockTypeKeys() { + return List.of(); + } + + @Override + public List enchantmentKeys() { + return List.of(); + } + + @Override + public List potionEffectKeys() { + return List.of(); + } + + @Override + public Map> blockStateProperties() { + return Map.of(); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheExecutorRegistrationTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheExecutorRegistrationTest.java new file mode 100644 index 000000000..8a7560047 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheExecutorRegistrationTest.java @@ -0,0 +1,58 @@ +package art.arcane.iris.core.pregenerator.cache; + +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisServices; +import org.junit.After; +import org.junit.Test; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PregenCacheExecutorRegistrationTest { + private static final class RecordingPreservation implements PreservationRegistry { + private final List executors = new CopyOnWriteArrayList<>(); + + @Override + public void register(Thread thread) { + } + + @Override + public void register(ExecutorService service) { + executors.add(service); + } + + @Override + public void registerCache(MeteredCache cache) { + } + + @Override + public void dereference() { + } + } + + @After + public void clearServices() { + IrisServices.clear(); + } + + @Test + public void dispatcherExecutorIsRegisteredForShutdown() throws Exception { + RecordingPreservation preservation = new RecordingPreservation(); + IrisServices.register(PreservationRegistry.class, preservation); + + File directory = Files.createTempDirectory("iris-pregen-exec-reg").toFile(); + PregenCache.create(directory); + + assertFalse("PregenCacheImpl must register its dispatcher executor for shutdown", preservation.executors.isEmpty()); + for (ExecutorService service : preservation.executors) { + assertFalse("registered executor must be live so preservation can shut it down later", service.isShutdown()); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java new file mode 100644 index 000000000..47e5045d4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java @@ -0,0 +1,332 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.loader.IrisData; +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.RegistryListEnchantment; +import art.arcane.iris.engine.object.annotations.RegistryListEntityType; +import art.arcane.iris.engine.object.annotations.RegistryListItemType; +import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.LogLevel; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBiomeWriter; +import art.arcane.iris.spi.PlatformBlockProperty; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformItem; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.spi.PlatformScheduler; +import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class SchemaBuilderParityTest { + private static final List POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost"); + private static final List ENCHANT_KEYS = List.of("minecraft:sharpness", "cool_mod:vorpal"); + private static final List ITEM_KEYS = List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby"); + private static final List ENTITY_KEYS = List.of("minecraft:zombie", "cool_mod:grizzly_bear"); + + private static final List EXPECTED_POTIONS = List.of("SPEED", "SLOW_FALLING", "MEGA_BOOST"); + private static final List EXPECTED_ENCHANTS = List.of("sharpness", "vorpal"); + private static final List EXPECTED_ITEMS = List.of("stone", "diamond_sword", "cool_mod:ruby"); + private static final List EXPECTED_ENTITIES = List.of("minecraft:zombie", "cool_mod:grizzly_bear"); + + private static final String EXPECTED_COUNT_DESCRIPTION = "count\nThe count.\n\nInteger\n* Default Value is 5\n* Minimum allowed is 2\n* Maximum allowed is 9"; + private static final String EXPECTED_COUNT_HTML_DESCRIPTION = "count
The count.

Integer
* Default Value is 5
* Minimum allowed is 2
* Maximum allowed is 9"; + + private IrisPlatform previous; + + @Before + public void bindFakePlatform() { + previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + if (previous != null) { + IrisPlatforms.unbind(); + } + IrisPlatforms.bind(new FakePlatform()); + } + + @After + public void restorePlatform() { + IrisPlatforms.unbind(); + if (previous != null) { + IrisPlatforms.bind(previous); + } + } + + @Test + public void registryDependentEnumsMatchLegacyTransforms() { + JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions"); + assertEquals(EXPECTED_POTIONS, enumValues(definitions, "enum-potion-effect-type")); + assertEquals(EXPECTED_ENCHANTS, enumValues(definitions, "enum-enchantment")); + assertEquals(EXPECTED_ITEMS, enumValues(definitions, "enum-item-type")); + assertEquals(EXPECTED_ENTITIES, enumValues(definitions, "enum-entity-type")); + } + + @Test + public void registryIndependentPathsAreByteIdentical() { + JSONObject schema = new SchemaBuilder(IndependentModel.class, (IrisData) null).construct(); + JSONObject count = schema.getJSONObject("properties").getJSONObject("count"); + assertEquals("integer", count.getString("type")); + assertEquals(2, count.getInt("minimum")); + assertEquals(9, count.getInt("maximum")); + assertEquals(EXPECTED_COUNT_DESCRIPTION, count.getString("description")); + assertEquals(EXPECTED_COUNT_HTML_DESCRIPTION, count.getString("x-intellij-html-description")); + + JSONObject flavor = schema.getJSONObject("properties").getJSONObject("flavor"); + assertEquals("string", flavor.getString("type")); + assertEquals(List.of("ALPHA", "BETA"), enumValues(schema.getJSONObject("definitions"), flavorDefinitionKey())); + } + + private static String flavorDefinitionKey() { + return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase(); + } + + private static List enumValues(JSONObject definitions, String key) { + JSONArray array = definitions.getJSONObject(key).getJSONArray("enum"); + List values = new ArrayList<>(array.length()); + for (int index = 0; index < array.length(); index++) { + values.add(array.getString(index)); + } + return values; + } + + @Desc("Registry dependent model.") + public static class RegistryModel { + @Desc("Potion field.") + @RegistryListPotionEffect + private String potion = ""; + + @Desc("Enchant field.") + @RegistryListEnchantment + private String enchant = ""; + + @Desc("Item field.") + @RegistryListItemType + private String item = ""; + + @Desc("Entity field.") + @RegistryListEntityType + private String entity = ""; + } + + @Desc("Independent model.") + public static class IndependentModel { + @Desc("The count.") + @MinNumber(2) + @MaxNumber(9) + private int count = 5; + + @Desc("A flag.") + private boolean flag = false; + + @Desc("A flavor.") + private Flavor flavor = Flavor.ALPHA; + } + + public enum Flavor { + ALPHA, + BETA + } + + private static final class FakeRegistries implements PlatformRegistries { + @Override + public PlatformBlockState block(String key) { + return null; + } + + @Override + public PlatformBlockState blockOrNull(String key) { + return null; + } + + @Override + public PlatformBlockState blockOrNull(String key, boolean warn) { + return null; + } + + @Override + public PlatformBlockState air() { + return null; + } + + @Override + public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) { + return null; + } + + @Override + public PlatformBiome biome(String key) { + return null; + } + + @Override + public PlatformItem item(String key) { + return null; + } + + @Override + public PlatformEntityType entity(String key) { + return null; + } + + @Override + public List blockKeys() { + return List.of(); + } + + @Override + public List biomeKeys() { + return List.of(); + } + + @Override + public List structureKeys() { + return List.of(); + } + + @Override + public List itemKeys() { + return ITEM_KEYS; + } + + @Override + public List entityKeys() { + return ENTITY_KEYS; + } + + @Override + public List blockTypeKeys() { + return List.of(); + } + + @Override + public List enchantmentKeys() { + return ENCHANT_KEYS; + } + + @Override + public List potionEffectKeys() { + return POTION_KEYS; + } + + @Override + public Map> blockStateProperties() { + return Map.of(); + } + } + + private static final class FakePlatform implements IrisPlatform { + private final PlatformRegistries registries = new FakeRegistries(); + + @Override + public String platformName() { + return "fake"; + } + + @Override + public String minecraftVersion() { + return "0.0.0"; + } + + @Override + public PlatformRegistries registries() { + return registries; + } + + @Override + public PlatformScheduler scheduler() { + return null; + } + + @Override + public PlatformStructureHooks structureHooks() { + return null; + } + + @Override + public PlatformBiomeWriter biomeWriter() { + return null; + } + + @Override + public File dataFolder() { + return new File("."); + } + + @Override + public File dataFile(String... path) { + return new File("."); + } + + @Override + public File pluginJar() { + return new File("."); + } + + @Override + public int irisVersionNumber() { + return 0; + } + + @Override + public int minecraftVersionNumber() { + return 0; + } + + @Override + public void callEvent(Object event) { + } + + @Override + public void dispatchConsoleCommand(String command) { + } + + @Override + public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) { + return false; + } + + @Override + public void log(LogLevel level, String message) { + } + + @Override + public void msg(String message) { + } + + @Override + public void reportError(Throwable error) { + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/protocol/IrisCursorResolverTest.java b/core/src/test/java/art/arcane/iris/core/protocol/IrisCursorResolverTest.java new file mode 100644 index 000000000..f7f23a228 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/protocol/IrisCursorResolverTest.java @@ -0,0 +1,96 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +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.spi.protocol.IrisMessage; +import org.junit.Test; + +import java.lang.reflect.Proxy; + +import static org.junit.Assert.assertEquals; + +public class IrisCursorResolverTest { + @Test + public void resolvesBiomeRegionCaveHeightAndDimensionKeys() { + IrisBiome biome = new IrisBiome(); + biome.setLoadKey("iris:plains"); + IrisRegion region = new IrisRegion(); + region.setLoadKey("iris:temperate"); + IrisBiome cave = new IrisBiome(); + cave.setLoadKey("iris:lush_cave"); + IrisDimension dimension = new IrisDimension(); + dimension.setLoadKey("overworld"); + Engine engine = engine(biome, region, cave, 72, dimension); + + IrisMessage.CursorInfo info = IrisCursorResolver.resolve(engine, 100, -200); + + assertEquals(100, info.blockX()); + assertEquals(-200, info.blockZ()); + assertEquals("iris:plains", info.biomeKey()); + assertEquals("iris:temperate", info.regionKey()); + assertEquals("iris:lush_cave", info.caveBiomeKey()); + assertEquals(72, info.height()); + assertEquals("overworld", info.dimensionKey()); + } + + @Test + public void missingCaveAndNullKeysCollapseToEmptyStrings() { + IrisDimension dimension = new IrisDimension(); + dimension.setLoadKey("overworld"); + Engine engine = engine(null, null, null, 0, dimension); + + IrisMessage.CursorInfo info = IrisCursorResolver.resolve(engine, 0, 0); + + assertEquals("", info.biomeKey()); + assertEquals("", info.regionKey()); + assertEquals("", info.caveBiomeKey()); + assertEquals("overworld", info.dimensionKey()); + } + + private static Engine engine(IrisBiome biome, IrisRegion region, IrisBiome cave, int height, IrisDimension dimension) { + return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class[]{Engine.class}, (proxy, method, args) -> switch (method.getName()) { + case "getSurfaceBiome" -> biome; + case "getRegion" -> region; + case "getCaveBiome" -> cave; + case "getHeight" -> height; + case "getDimension" -> dimension; + case "toString" -> "proxyEngine"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> defaultReturn(method.getReturnType()); + }); + } + + private static Object defaultReturn(Class returnType) { + if (returnType == int.class) { + return 0; + } + if (returnType == long.class) { + return 0L; + } + if (returnType == boolean.class) { + return false; + } + return null; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/protocol/IrisMessageCodecTest.java b/core/src/test/java/art/arcane/iris/core/protocol/IrisMessageCodecTest.java new file mode 100644 index 000000000..24765d9de --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/protocol/IrisMessageCodecTest.java @@ -0,0 +1,327 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +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.IrisWireReader; +import art.arcane.iris.spi.protocol.IrisWireWriter; +import art.arcane.iris.spi.protocol.ProtocolException; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IrisMessageCodecTest { + @Test + public void clientHelloRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.ClientHello original = new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void serverHelloRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.ServerHello original = new IrisMessage.ServerHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN, "Paper", true); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void pregenProgressRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.PregenProgress original = new IrisMessage.PregenProgress(42L, 128L, 4096L, 512.25D, 60000L, IrisMessage.PregenProgress.STATE_RUNNING); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void pregenEndRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.PregenEnd completed = new IrisMessage.PregenEnd(7L, true); + IrisMessage.PregenEnd aborted = new IrisMessage.PregenEnd(9L, false); + assertEquals(completed, IrisMessageCodec.decode(IrisMessageCodec.encode(completed))); + assertEquals(aborted, IrisMessageCodec.decode(IrisMessageCodec.encode(aborted))); + } + + @Test + public void negativeAndExtremePrimitivesRoundTrip() throws ProtocolException { + IrisMessage.ClientHello hello = new IrisMessage.ClientHello(Integer.MIN_VALUE, Long.MIN_VALUE); + IrisMessage.PregenProgress progress = new IrisMessage.PregenProgress(Long.MAX_VALUE, Long.MIN_VALUE, -1L, -12.5D, -1L, Integer.MIN_VALUE); + assertEquals(hello, IrisMessageCodec.decode(IrisMessageCodec.encode(hello))); + assertEquals(progress, IrisMessageCodec.decode(IrisMessageCodec.encode(progress))); + } + + @Test + public void emptyAndUnicodeStringsRoundTrip() throws ProtocolException { + IrisMessage.ServerHello empty = new IrisMessage.ServerHello(1, 0L, "", false); + IrisMessage.ServerHello unicode = new IrisMessage.ServerHello(1, 0L, "Paper🌍日本語", true); + assertEquals(empty, IrisMessageCodec.decode(IrisMessageCodec.encode(empty))); + assertEquals(unicode, IrisMessageCodec.decode(IrisMessageCodec.encode(unicode))); + } + + @Test + public void varintRoundTripsBoundaryValues() throws ProtocolException { + int[] values = {0, 1, 127, 128, 16383, 16384, Integer.MAX_VALUE, -1, Integer.MIN_VALUE}; + for (int value : values) { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(value); + IrisWireReader reader = new IrisWireReader(writer.toByteArray()); + assertEquals(value, reader.readVarInt()); + } + } + + @Test + public void maxWidthVarintEncodesToFiveBytes() { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(-1); + assertEquals(5, writer.toByteArray().length); + } + + @Test + public void everyPrefixOfAValidFrameRejectsCleanly() { + byte[][] frames = { + IrisMessageCodec.encode(new IrisMessage.ClientHello(1, IrisProtocol.CAPABILITY_PREGEN)), + IrisMessageCodec.encode(new IrisMessage.ServerHello(1, 0L, "Paper🌍", true)), + IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 2L, 3L, 4.5D, 6L, 1)), + IrisMessageCodec.encode(new IrisMessage.PregenEnd(1L, true)) + }; + for (byte[] frame : frames) { + for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) { + byte[] prefix = Arrays.copyOf(frame, prefixLength); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix)); + } + } + } + + @Test + public void encodingBeyondFrameCapThrowsIllegalState() { + String oversizedBrand = "a".repeat(IrisProtocol.MAX_FRAME_BYTES); + IrisMessage.ServerHello oversized = new IrisMessage.ServerHello(1, 0L, oversizedBrand, true); + assertThrows(IllegalStateException.class, () -> IrisMessageCodec.encode(oversized)); + } + + @Test + public void decodingFrameBeyondCapRejects() { + byte[] oversized = new byte[IrisProtocol.MAX_FRAME_BYTES + 1]; + oversized[0] = (byte) IrisProtocol.TYPE_CLIENT_HELLO; + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(oversized)); + } + + @Test + public void poisonedStringLengthRejectsWithoutAllocatingClaimedSize() { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(IrisProtocol.TYPE_SERVER_HELLO); + writer.writeInt(1); + writer.writeLong(0L); + writer.writeVarInt(Integer.MAX_VALUE); + byte[] poisoned = writer.toByteArray(); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned)); + } + + @Test + public void unknownMessageTypeDecodesToIgnorableNull() throws ProtocolException { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(9999); + writer.writeLong(123L); + assertNull(IrisMessageCodec.decode(writer.toByteArray())); + } + + @Test + public void dimensionStatusRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.DimensionStatus original = new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true); + IrisMessage.DimensionStatus disabled = new IrisMessage.DimensionStatus("", "", Long.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, false); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + assertEquals(disabled, IrisMessageCodec.decode(IrisMessageCodec.encode(disabled))); + } + + @Test + public void cursorInfoRequestRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.CursorInfoRequest original = new IrisMessage.CursorInfoRequest(1024, -2048); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void cursorInfoRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.CursorInfo original = new IrisMessage.CursorInfo(12, -34, "iris:plains", "iris:temperate", "", 72, "overworld"); + IrisMessage.CursorInfo withCave = new IrisMessage.CursorInfo(Integer.MIN_VALUE, Integer.MAX_VALUE, "iris:desert", "iris:arid", "iris:lush_cave", -1, "minecraft:the_nether"); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + assertEquals(withCave, IrisMessageCodec.decode(IrisMessageCodec.encode(withCave))); + } + + @Test + public void visionTileRequestRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.VisionTileRequest original = new IrisMessage.VisionTileRequest(-9, 15, 3); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void visionTileRoundTripsThroughCodec() throws ProtocolException { + byte[] payload = {1, 2, 3, 4, 5, -1, -2, 127, 0}; + IrisMessage.VisionTile original = new IrisMessage.VisionTile(3, -4, 2, 7, 1, 5, payload); + IrisMessage.VisionTile decoded = (IrisMessage.VisionTile) IrisMessageCodec.decode(IrisMessageCodec.encode(original)); + assertEquals(original.tileX(), decoded.tileX()); + assertEquals(original.tileZ(), decoded.tileZ()); + assertEquals(original.zoomLevel(), decoded.zoomLevel()); + assertEquals(original.sequence(), decoded.sequence()); + assertEquals(original.chunkIndex(), decoded.chunkIndex()); + assertEquals(original.chunkCount(), decoded.chunkCount()); + assertArrayEquals(payload, decoded.data()); + } + + @Test + public void visionTileEmptyDataRoundTrips() throws ProtocolException { + IrisMessage.VisionTile original = new IrisMessage.VisionTile(0, 0, 0, 0, 0, 1, new byte[0]); + IrisMessage.VisionTile decoded = (IrisMessage.VisionTile) IrisMessageCodec.decode(IrisMessageCodec.encode(original)); + assertArrayEquals(new byte[0], decoded.data()); + } + + @Test + public void visionMarkersRoundTripsThroughCodec() throws ProtocolException { + List markers = List.of( + new IrisMessage.VisionMarkers.Marker(10, 20, 1, "spawn"), + new IrisMessage.VisionMarkers.Marker(-5, 7, 2, ""), + new IrisMessage.VisionMarkers.Marker(Integer.MIN_VALUE, Integer.MAX_VALUE, 3, "日本語")); + IrisMessage.VisionMarkers original = new IrisMessage.VisionMarkers(4, -8, 1, markers); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void visionMarkersEmptyListRoundTrips() throws ProtocolException { + IrisMessage.VisionMarkers original = new IrisMessage.VisionMarkers(0, 0, 0, List.of()); + assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original))); + } + + @Test + public void everyPrefixOfANewFrameRejectsCleanly() { + byte[][] frames = { + IrisMessageCodec.encode(new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true)), + IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(100, -200)), + IrisMessageCodec.encode(new IrisMessage.CursorInfo(1, 2, "plains", "region", "", 72, "overworld")), + IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(3, 4, 2)), + IrisMessageCodec.encode(new IrisMessage.VisionTile(1, 2, 0, 0, 0, 1, new byte[]{9, 8, 7})), + IrisMessageCodec.encode(new IrisMessage.VisionMarkers(1, 2, 0, List.of(new IrisMessage.VisionMarkers.Marker(1, 2, 0, "a")))) + }; + for (byte[] frame : frames) { + for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) { + byte[] prefix = Arrays.copyOf(frame, prefixLength); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix)); + } + } + } + + @Test + public void visionTileChunkAtCapFitsUnderFrameCap() { + byte[] data = new byte[IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES]; + IrisMessage.VisionTile tile = new IrisMessage.VisionTile(Integer.MAX_VALUE, Integer.MIN_VALUE, IrisProtocol.MAX_FRAME_BYTES, Integer.MAX_VALUE, 4095, 4096, data); + byte[] frame = IrisMessageCodec.encode(tile); + assertTrue("vision tile chunk frame must fit under cap", frame.length <= IrisProtocol.MAX_FRAME_BYTES); + assertTrue("header math must reserve room for the max chunk", IrisProtocol.VISION_TILE_HEADER_BYTES + 3 + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES <= IrisProtocol.MAX_FRAME_BYTES); + } + + @Test + public void visionMarkersAtCapFitUnderFrameCap() { + List markers = new ArrayList<>(IrisProtocol.MAX_VISION_MARKERS); + for (int index = 0; index < IrisProtocol.MAX_VISION_MARKERS; index++) { + markers.add(new IrisMessage.VisionMarkers.Marker(index, -index, index & 7, "m" + index)); + } + byte[] frame = IrisMessageCodec.encode(new IrisMessage.VisionMarkers(1, 2, 0, markers)); + assertTrue("marker frame at cap must fit under cap", frame.length <= IrisProtocol.MAX_FRAME_BYTES); + } + + @Test + public void poisonedMarkerCountRejectsWithoutOverread() { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(IrisProtocol.TYPE_VISION_MARKERS); + writer.writeInt(0); + writer.writeInt(0); + writer.writeInt(0); + writer.writeVarInt(Integer.MAX_VALUE); + byte[] poisoned = writer.toByteArray(); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned)); + } + + @Test + public void pregenRegionDeltaRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.PregenRegionDelta generating = new IrisMessage.PregenRegionDelta(42L, 3, -7, IrisMessage.PregenRegionDelta.STATE_GENERATING); + IrisMessage.PregenRegionDelta done = new IrisMessage.PregenRegionDelta(Long.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, IrisMessage.PregenRegionDelta.STATE_DONE); + assertEquals(generating, IrisMessageCodec.decode(IrisMessageCodec.encode(generating))); + assertEquals(done, IrisMessageCodec.decode(IrisMessageCodec.encode(done))); + } + + @Test + public void studioHotloadRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.StudioHotload ok = new IrisMessage.StudioHotload("overworld", 12, false, ""); + IrisMessage.StudioHotload failed = new IrisMessage.StudioHotload("iris:desert", Integer.MIN_VALUE, true, "IllegalStateException: bad pack🌍"); + assertEquals(ok, IrisMessageCodec.decode(IrisMessageCodec.encode(ok))); + assertEquals(failed, IrisMessageCodec.decode(IrisMessageCodec.encode(failed))); + } + + @Test + public void toastRoundTripsThroughCodec() throws ProtocolException { + IrisMessage.Toast info = new IrisMessage.Toast(IrisMessage.Toast.KIND_INFO, "Title", "Body"); + IrisMessage.Toast error = new IrisMessage.Toast(IrisMessage.Toast.KIND_ERROR, "", "日本語"); + assertEquals(info, IrisMessageCodec.decode(IrisMessageCodec.encode(info))); + assertEquals(error, IrisMessageCodec.decode(IrisMessageCodec.encode(error))); + } + + @Test + public void everyPrefixOfAStudioComfortFrameRejectsCleanly() { + byte[][] frames = { + IrisMessageCodec.encode(new IrisMessage.PregenRegionDelta(9L, 4, -5, IrisMessage.PregenRegionDelta.STATE_GENERATING)), + IrisMessageCodec.encode(new IrisMessage.StudioHotload("overworld", 3, true, "boom")), + IrisMessageCodec.encode(new IrisMessage.Toast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld")) + }; + for (byte[] frame : frames) { + for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) { + byte[] prefix = Arrays.copyOf(frame, prefixLength); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix)); + } + } + } + + @Test + public void studioHotloadOversizePackKeyThrowsIllegalState() { + String oversized = "a".repeat(IrisProtocol.MAX_FRAME_BYTES); + IrisMessage.StudioHotload hotload = new IrisMessage.StudioHotload(oversized, 0, false, ""); + assertThrows(IllegalStateException.class, () -> IrisMessageCodec.encode(hotload)); + } + + @Test + public void poisonedStudioHotloadPackKeyLengthRejectsWithoutAllocatingClaimedSize() { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(IrisProtocol.TYPE_STUDIO_HOTLOAD); + writer.writeVarInt(Integer.MAX_VALUE); + byte[] poisoned = writer.toByteArray(); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned)); + } + + @Test + public void poisonedToastTitleLengthRejectsWithoutAllocatingClaimedSize() { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(IrisProtocol.TYPE_TOAST); + writer.writeInt(IrisMessage.Toast.KIND_INFO); + writer.writeVarInt(Integer.MAX_VALUE); + byte[] poisoned = writer.toByteArray(); + assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned)); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/protocol/IrisProtocolServerTest.java b/core/src/test/java/art/arcane/iris/core/protocol/IrisProtocolServerTest.java new file mode 100644 index 000000000..8ab247abe --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/protocol/IrisProtocolServerTest.java @@ -0,0 +1,531 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.engine.framework.Engine; +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.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 org.junit.Test; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisProtocolServerTest { + private static final String BRAND = "TestBrand"; + private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN; + + @Test + public void frameBeforeHelloIsDroppedAndCounted() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING))); + + assertEquals(IrisSession.State.AWAITING_HELLO, session.state()); + assertEquals(1L, server.droppedBeforeHelloCount()); + assertEquals(0, transport.sent.size()); + } + + @Test + public void wrongProtocolVersionNeverReachesReady() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION + 999, IrisProtocol.CAPABILITY_PREGEN))); + + assertEquals(IrisSession.State.AWAITING_HELLO, session.state()); + assertEquals(1L, server.versionMismatchCount()); + assertEquals(0, transport.sent.size()); + } + + @Test + public void matchingHelloReachesReadyAndAnswersServerHello() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + + assertEquals(IrisSession.State.READY, session.state()); + assertEquals(IrisProtocol.CAPABILITY_PREGEN, session.capabilities()); + assertEquals(1, transport.sent.size()); + IrisMessage.ServerHello answer = (IrisMessage.ServerHello) transport.sent.get(0); + assertEquals(IrisProtocol.PROTOCOL_VERSION, answer.protocolVersion()); + assertEquals(SERVER_CAPABILITIES, answer.capabilities()); + assertEquals(BRAND, answer.serverBrand()); + assertTrue(answer.irisActive()); + } + + @Test + public void readySessionReceivesProgressAndEndBroadcasts() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + + server.broadcastPregenProgress(5L, 10L, 100L, 3.0D, 2000L, IrisMessage.PregenProgress.STATE_PAUSED); + server.pregenEnd(5L, true); + + assertEquals(3, transport.sent.size()); + IrisMessage.PregenProgress progress = (IrisMessage.PregenProgress) transport.sent.get(1); + assertEquals(new IrisMessage.PregenProgress(5L, 10L, 100L, 3.0D, 2000L, IrisMessage.PregenProgress.STATE_PAUSED), progress); + assertEquals(new IrisMessage.PregenEnd(5L, true), transport.sent.get(2)); + } + + @Test + public void unregisteringStopsBroadcasts() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + int afterHandshake = transport.sent.size(); + + registry.unregister("s1"); + server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING); + server.pregenEnd(1L, true); + + assertEquals(afterHandshake, transport.sent.size()); + assertTrue(registry.isEmpty()); + } + + @Test + public void broadcastToEmptyRegistryDoesNothing() { + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + + server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING); + server.pregenEnd(1L, false); + + assertTrue(registry.isEmpty()); + } + + @Test + public void awaitingSessionDoesNotReceiveBroadcasts() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING); + + assertEquals(0, transport.sent.size()); + } + + @Test + public void readySessionWithoutPregenCapabilityIsNotBroadcastTo() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, 0L))); + + server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING); + + assertFalse(session.hasCapability(IrisProtocol.CAPABILITY_PREGEN)); + assertEquals(1, transport.sent.size()); + } + + @Test + public void burstBeyondRateLimitDropsWithoutDispatching() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true, () -> 1000L); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + int overflow = 5; + int total = IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND + overflow; + byte[] preHelloFrame = IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING)); + for (int index = 0; index < total; index++) { + server.onClientFrame("s1", preHelloFrame); + } + + assertEquals(IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND, server.droppedBeforeHelloCount()); + assertEquals(overflow, server.rateLimitedFrameCount()); + } + + @Test + public void cursorInfoRequestServedForCapableReadySession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR))); + server.setEngineResolver(sessionId -> cursorEngine("iris:plains", "iris:temperate", "iris:lush_cave", 88, "overworld")); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(10, -20))); + + assertEquals(1L, server.cursorInfoServedCount()); + IrisMessage.CursorInfo info = (IrisMessage.CursorInfo) transport.sent.get(1); + assertEquals(10, info.blockX()); + assertEquals(-20, info.blockZ()); + assertEquals("iris:plains", info.biomeKey()); + assertEquals("iris:temperate", info.regionKey()); + assertEquals("iris:lush_cave", info.caveBiomeKey()); + assertEquals(88, info.height()); + assertEquals("overworld", info.dimensionKey()); + } + + @Test + public void cursorInfoRequestWithoutCapabilityRejectedWithoutResolvingEngine() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, 0L))); + server.setEngineResolver(sessionId -> { + throw new AssertionError("engine must not be resolved without cursor capability"); + }); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(1, 2))); + + assertEquals(1L, server.capabilityRejectedCount()); + assertEquals(1, transport.sent.size()); + } + + @Test + public void cursorInfoRequestWithNullEngineCountsDrop() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR))); + server.setEngineResolver(sessionId -> null); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(1, 2))); + + assertEquals(1L, server.noEngineDropCount()); + assertEquals(0L, server.cursorInfoServedCount()); + assertEquals(1, transport.sent.size()); + } + + @Test + public void visionTileRequestForwardedForCapableReadySession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION))); + RecordingVisionHandler handler = new RecordingVisionHandler(); + server.setVisionTileHandler(handler); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(7, -9, 3))); + + assertEquals(1, handler.count); + assertEquals(7, handler.lastTileX); + assertEquals(-9, handler.lastTileZ); + assertEquals(3, handler.lastZoom); + assertEquals(1L, server.visionTileForwardedCount()); + } + + @Test + public void visionTileRequestBudgetDropsExcessWithinWindow() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true, () -> 1000L); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION))); + RecordingVisionHandler handler = new RecordingVisionHandler(); + server.setVisionTileHandler(handler); + + int overflow = 3; + int total = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND + overflow; + byte[] request = IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0)); + for (int index = 0; index < total; index++) { + server.onClientFrame("s1", request); + } + + assertEquals(IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND, handler.count); + assertEquals(IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND, server.visionTileForwardedCount()); + assertEquals(overflow, server.visionRateLimitedCount()); + } + + @Test + public void visionTileRequestWithoutCapabilityRejected() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR))); + RecordingVisionHandler handler = new RecordingVisionHandler(); + server.setVisionTileHandler(handler); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0))); + + assertEquals(0, handler.count); + assertEquals(1L, server.capabilityRejectedCount()); + } + + @Test + public void visionTileRequestWithoutHandlerCountsDrop() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION))); + + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0))); + + assertEquals(1L, server.noEngineDropCount()); + } + + @Test + public void readySessionReceivesDimensionStatusBroadcast() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION))); + + server.broadcastDimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true); + + assertEquals(new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true), transport.sent.get(1)); + } + + @Test + public void awaitingSessionDoesNotReceiveDimensionStatusBroadcast() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.broadcastDimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true); + + assertEquals(0, transport.sent.size()); + } + + @Test + public void sendDimensionStatusTargetsSingleReadySession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR))); + + server.sendDimensionStatus("s1", "minecraft:the_nether", "nether", 42L, 0, 256, true); + + assertEquals(new IrisMessage.DimensionStatus("minecraft:the_nether", "nether", 42L, 0, 256, true), transport.sent.get(1)); + } + + @Test + public void pregenRegionDeltaReachesPregenCapableSession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + + server.broadcastPregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_GENERATING); + + assertEquals(new IrisMessage.PregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_GENERATING), transport.sent.get(1)); + assertEquals(1L, server.pregenRegionDeltasBroadcastCount()); + } + + @Test + public void pregenRegionDeltaNotSentToSessionWithoutPregenCapability() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO))); + + server.broadcastPregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_DONE); + + assertEquals(1, transport.sent.size()); + assertEquals(0L, server.pregenRegionDeltasBroadcastCount()); + } + + @Test + public void studioHotloadReachesStudioCapableSession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO))); + + server.broadcastStudioHotload("overworld", 5, false, ""); + + assertEquals(new IrisMessage.StudioHotload("overworld", 5, false, ""), transport.sent.get(1)); + assertEquals(1L, server.studioHotloadsBroadcastCount()); + } + + @Test + public void readySessionWithoutStudioCapabilityReceivesNoStudioMessages() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + + server.broadcastStudioHotload("overworld", 5, true, "boom"); + server.broadcastToast(IrisMessage.Toast.KIND_ERROR, "Studio Hotload", "overworld failed"); + + assertFalse(session.hasCapability(IrisProtocol.CAPABILITY_STUDIO)); + assertEquals(1, transport.sent.size()); + assertEquals(0L, server.studioHotloadsBroadcastCount()); + assertEquals(0L, server.toastsBroadcastCount()); + } + + @Test + public void broadcastToastReachesStudioCapableSession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO))); + + server.broadcastToast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld"); + + assertEquals(new IrisMessage.Toast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld"), transport.sent.get(1)); + assertEquals(1L, server.toastsBroadcastCount()); + } + + @Test + public void sendToastTargetsStudioCapableSession() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO))); + + server.sendToast("s1", IrisMessage.Toast.KIND_INFO, "Hello", "World"); + + assertEquals(new IrisMessage.Toast(IrisMessage.Toast.KIND_INFO, "Hello", "World"), transport.sent.get(1)); + assertEquals(1L, server.toastsSentCount()); + } + + @Test + public void sendToastWithoutStudioCapabilityDropsWithoutCounting() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN))); + + server.sendToast("s1", IrisMessage.Toast.KIND_INFO, "Hello", "World"); + + assertEquals(1, transport.sent.size()); + assertEquals(0L, server.toastsSentCount()); + } + + @Test + public void awaitingSessionDoesNotReceiveStudioHotloadBroadcast() { + RecordingTransport transport = new RecordingTransport(); + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true); + IrisSession session = new IrisSession("s1", transport); + registry.register(session); + + server.broadcastStudioHotload("overworld", 1, false, ""); + + assertEquals(0, transport.sent.size()); + assertEquals(0L, server.studioHotloadsBroadcastCount()); + } + + private static Engine cursorEngine(String biomeKey, String regionKey, String caveKey, int height, String dimensionKey) { + IrisBiome biome = new IrisBiome(); + biome.setLoadKey(biomeKey); + IrisRegion region = new IrisRegion(); + region.setLoadKey(regionKey); + IrisBiome cave = new IrisBiome(); + cave.setLoadKey(caveKey); + IrisDimension dimension = new IrisDimension(); + dimension.setLoadKey(dimensionKey); + return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class[]{Engine.class}, (proxy, method, args) -> switch (method.getName()) { + case "getSurfaceBiome" -> biome; + case "getRegion" -> region; + case "getCaveBiome" -> cave; + case "getHeight" -> height; + case "getDimension" -> dimension; + case "toString" -> "proxyEngine"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> null; + }); + } + + private static final class RecordingVisionHandler implements VisionTileRequestHandler { + private int count; + private int lastTileX; + private int lastTileZ; + private int lastZoom; + + @Override + public void handle(String sessionId, int tileX, int tileZ, int zoomLevel) { + count++; + lastTileX = tileX; + lastTileZ = tileZ; + lastZoom = zoomLevel; + } + } + + private static final class RecordingTransport implements IrisServerTransport { + private final List sent = new ArrayList<>(); + + @Override + public void sendToClient(String sessionId, byte[] frame) { + try { + sent.add(IrisMessageCodec.decode(frame)); + } catch (ProtocolException rejected) { + throw new AssertionError(rejected); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/protocol/IrisTileEncoderTest.java b/core/src/test/java/art/arcane/iris/core/protocol/IrisTileEncoderTest.java new file mode 100644 index 000000000..398a850d3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/protocol/IrisTileEncoderTest.java @@ -0,0 +1,187 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisMessageCodec; +import art.arcane.iris.spi.protocol.IrisProtocol; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IrisTileEncoderTest { + private record DecodedBlob(int width, int height, int mode, int[] pixels) { + } + + @Test + public void paletteModeRoundTripsToIdenticalPixels() throws Exception { + int[] pixels = new int[256]; + for (int index = 0; index < pixels.length; index++) { + pixels[index] = (index << 16) | (index << 8) | index; + } + byte[] blob = IrisTileEncoder.encodePixels(pixels, 16, 16); + DecodedBlob decoded = decodeBlob(blob); + assertEquals(IrisTileEncoder.MODE_PALETTE, decoded.mode()); + assertEquals(16, decoded.width()); + assertEquals(16, decoded.height()); + assertArrayEquals(pixels, decoded.pixels()); + } + + @Test + public void rawModeRoundTripsToIdenticalPixels() throws Exception { + int[] pixels = new int[1024]; + for (int index = 0; index < pixels.length; index++) { + pixels[index] = index; + } + byte[] blob = IrisTileEncoder.encodePixels(pixels, 32, 32); + DecodedBlob decoded = decodeBlob(blob); + assertEquals(IrisTileEncoder.MODE_RAW_RGB, decoded.mode()); + assertArrayEquals(pixels, decoded.pixels()); + } + + @Test + public void alphaBitsAreStrippedBeforeEncoding() throws Exception { + int[] pixels = new int[4]; + pixels[0] = 0xFF123456; + pixels[1] = 0x00123456; + pixels[2] = 0xFFABCDEF; + pixels[3] = 0x77ABCDEF; + byte[] blob = IrisTileEncoder.encodePixels(pixels, 2, 2); + DecodedBlob decoded = decodeBlob(blob); + assertEquals(IrisTileEncoder.MODE_PALETTE, decoded.mode()); + assertArrayEquals(new int[]{0x123456, 0x123456, 0xABCDEF, 0xABCDEF}, decoded.pixels()); + } + + @Test + public void chunkSplitAndReassembleReproducesBlob() { + byte[] blob = new byte[60000]; + for (int index = 0; index < blob.length; index++) { + blob[index] = (byte) (index * 31); + } + List chunks = IrisTileEncoder.splitIntoChunks(blob, 5, -3, 2, 9); + int expectedCount = (blob.length + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES - 1) / IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES; + assertEquals(expectedCount, chunks.size()); + assertTrue("large blob must span multiple chunks", chunks.size() >= 2); + for (int index = 0; index < chunks.size(); index++) { + IrisMessage.VisionTile chunk = chunks.get(index); + assertEquals(5, chunk.tileX()); + assertEquals(-3, chunk.tileZ()); + assertEquals(2, chunk.zoomLevel()); + assertEquals(9, chunk.sequence()); + assertEquals(index, chunk.chunkIndex()); + assertEquals(expectedCount, chunk.chunkCount()); + assertTrue(chunk.data().length <= IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES); + assertTrue("each chunk frame must fit under cap", IrisMessageCodec.encode(chunk).length <= IrisProtocol.MAX_FRAME_BYTES); + } + assertArrayEquals(blob, reassemble(chunks)); + } + + @Test + public void emptyBlobProducesSingleEmptyChunk() { + List chunks = IrisTileEncoder.splitIntoChunks(new byte[0], 0, 0, 0, 0); + assertEquals(1, chunks.size()); + assertEquals(1, chunks.get(0).chunkCount()); + assertEquals(0, chunks.get(0).data().length); + } + + @Test + public void fullPipelineReassemblesToIdenticalPixels() throws Exception { + int[] pixels = new int[4096]; + for (int index = 0; index < pixels.length; index++) { + pixels[index] = (index * 2654435761L & 0xFFFFFFL) == 0 ? 1 : (int) (index * 2654435761L & 0xFFFFFFL); + } + byte[] blob = IrisTileEncoder.encodePixels(pixels, 64, 64); + List chunks = IrisTileEncoder.splitIntoChunks(blob, 1, 2, 0, 3); + byte[] reassembled = reassemble(chunks); + assertArrayEquals(blob, reassembled); + DecodedBlob decoded = decodeBlob(reassembled); + assertArrayEquals(pixels, decoded.pixels()); + for (IrisMessage.VisionTile chunk : chunks) { + assertTrue(IrisMessageCodec.encode(chunk).length <= IrisProtocol.MAX_FRAME_BYTES); + } + } + + private static byte[] reassemble(List chunks) { + IrisMessage.VisionTile[] ordered = new IrisMessage.VisionTile[chunks.get(0).chunkCount()]; + for (IrisMessage.VisionTile chunk : chunks) { + ordered[chunk.chunkIndex()] = chunk; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (IrisMessage.VisionTile chunk : ordered) { + out.writeBytes(chunk.data()); + } + return out.toByteArray(); + } + + private static DecodedBlob decodeBlob(byte[] blob) throws Exception { + byte[] raw = inflate(blob); + DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw)); + int width = in.readInt(); + int height = in.readInt(); + int mode = in.readUnsignedByte(); + int count = width * height; + int[] pixels = new int[count]; + if (mode == IrisTileEncoder.MODE_PALETTE) { + int paletteSize = in.readInt(); + 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] = (red << 16) | (green << 8) | blue; + } + for (int index = 0; index < count; index++) { + pixels[index] = palette[in.readUnsignedByte()]; + } + } else { + for (int index = 0; index < count; index++) { + int red = in.readUnsignedByte(); + int green = in.readUnsignedByte(); + int blue = in.readUnsignedByte(); + pixels[index] = (red << 16) | (green << 8) | blue; + } + } + return new DecodedBlob(width, height, mode, pixels); + } + + private static byte[] inflate(byte[] input) throws DataFormatException { + Inflater inflater = new Inflater(); + inflater.setInput(input); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2)); + byte[] buffer = new byte[8192]; + while (!inflater.finished()) { + int produced = inflater.inflate(buffer); + if (produced == 0 && inflater.needsInput()) { + break; + } + out.write(buffer, 0, produced); + } + inflater.end(); + return out.toByteArray(); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/protocol/IrisVisionRequestServiceTest.java b/core/src/test/java/art/arcane/iris/core/protocol/IrisVisionRequestServiceTest.java new file mode 100644 index 000000000..4b2e25bf3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/protocol/IrisVisionRequestServiceTest.java @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +package art.arcane.iris.core.protocol; + +import art.arcane.iris.spi.protocol.IrisProtocol; +import org.junit.Test; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +public class IrisVisionRequestServiceTest { + private static final Executor DIRECT = Runnable::run; + private static final Executor DISABLED = runnable -> { + }; + + @Test + public void saturationDropsOldestRequestsAndCounts() { + IrisSessionRegistry registry = new IrisSessionRegistry(); + registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION); + EngineResolver resolver = sessionId -> { + throw new AssertionError("disabled executor must not process requests"); + }; + int maxPending = 4; + int overflow = 6; + IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DISABLED, maxPending); + + for (int index = 0; index < maxPending + overflow; index++) { + service.handle("s1", index, 0, 0); + } + + assertEquals(overflow, service.droppedSaturatedCount()); + assertEquals(maxPending, service.pendingSize()); + } + + @Test + public void nullEngineDropsAndCounts() { + IrisSessionRegistry registry = new IrisSessionRegistry(); + CountingTransport transport = registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION); + EngineResolver resolver = sessionId -> null; + IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DIRECT, 8); + + service.handle("s1", 0, 0, 0); + + assertEquals(1L, service.droppedNoEngineCount()); + assertEquals(0L, service.tilesEncodedCount()); + assertEquals(0, transport.sent.get()); + } + + @Test + public void missingReadyOrCapableSessionDropsWithoutResolvingEngine() { + IrisSessionRegistry registry = new IrisSessionRegistry(); + IrisSession awaiting = new IrisSession("awaiting", new CountingTransport()); + registry.register(awaiting); + IrisSession noCapability = new IrisSession("nocap", new CountingTransport()); + noCapability.markReady(IrisProtocol.PROTOCOL_VERSION, 0L); + registry.register(noCapability); + EngineResolver resolver = sessionId -> { + throw new AssertionError("engine must not be resolved for an ineligible session"); + }; + IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DIRECT, 8); + + service.handle("unregistered", 0, 0, 0); + service.handle("awaiting", 0, 0, 0); + service.handle("nocap", 0, 0, 0); + + assertEquals(3L, service.droppedNoSessionCount()); + assertEquals(0L, service.droppedNoEngineCount()); + assertEquals(0L, service.tilesEncodedCount()); + } + + private static CountingTransport registerReady(IrisSessionRegistry registry, String sessionId, long capabilities) { + CountingTransport transport = new CountingTransport(); + IrisSession session = new IrisSession(sessionId, transport); + session.markReady(IrisProtocol.PROTOCOL_VERSION, capabilities); + registry.register(session); + return transport; + } + + private static final class CountingTransport implements IrisServerTransport { + private final AtomicInteger sent = new AtomicInteger(0); + + @Override + public void sendToClient(String sessionId, byte[] frame) { + sent.incrementAndGet(); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java index 8cd00b202..e79b471c2 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java @@ -7,9 +7,17 @@ import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.volmlib.util.math.RNG; import org.junit.Test; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class DecoratorCoreTest { diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java index 1bcd441f1..24d1f6fae 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java @@ -20,16 +20,23 @@ package art.arcane.iris.platform.bukkit; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockProperty; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.block.data.BlockData; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; +import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; import java.util.logging.Logger; import static org.junit.Assert.assertEquals; @@ -38,6 +45,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -62,9 +70,28 @@ public class BukkitSpiConformanceTest { private static BlockData blockData(String asString) { BlockData data = mock(BlockData.class); doReturn(asString).when(data).getAsString(); + doReturn(asString).when(data).getAsString(anyBoolean()); return data; } + private static PlatformRegistries registries() { + return new BukkitRegistries(); + } + + private static void assertNamespacedRegistryList(Supplier> supplier) { + List keys; + try { + keys = supplier.get(); + } catch (Throwable unavailable) { + Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable); + return; + } + assertFalse(keys.isEmpty()); + for (String key : keys) { + assertTrue("expected namespaced key but was '" + key + "'", key.contains(":")); + } + } + @Test public void blockStateInterningReturnsSameInstanceForSameKey() { BlockData first = blockData("iristest:intern_block[axis=y]"); @@ -129,4 +156,62 @@ public class BukkitSpiConformanceTest { } assertSame(first, IrisPlatforms.get()); } + + @Test + public void itemKeysAreNamespacedAndNonEmpty() { + assertNamespacedRegistryList(() -> registries().itemKeys()); + } + + @Test + public void entityKeysAreNamespacedAndNonEmpty() { + assertNamespacedRegistryList(() -> registries().entityKeys()); + } + + @Test + public void enchantmentKeysAreNamespacedAndNonEmpty() { + assertNamespacedRegistryList(() -> registries().enchantmentKeys()); + } + + @Test + public void potionEffectKeysAreNamespacedAndNonEmpty() { + assertNamespacedRegistryList(() -> registries().potionEffectKeys()); + } + + @Test + public void blockTypeKeysAreNamespacedAndNonEmpty() { + assertNamespacedRegistryList(() -> registries().blockTypeKeys()); + } + + @Test + public void blockTypeKeysMatchAuthorableBlockTypeSource() { + List keys; + List legacy; + try { + keys = registries().blockTypeKeys(); + legacy = Arrays.asList(BukkitBlockResolution.getBlockTypes()); + } catch (Throwable unavailable) { + Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable); + return; + } + assertEquals(legacy, keys); + } + + @Test + public void blockStatePropertiesAreKeyedByNamespacedBlock() { + Map> states; + try { + states = registries().blockStateProperties(); + } catch (Throwable unavailable) { + Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable); + return; + } + assertFalse(states.isEmpty()); + for (Map.Entry> entry : states.entrySet()) { + assertTrue("expected namespaced block key but was '" + entry.getKey() + "'", entry.getKey().contains(":")); + for (PlatformBlockProperty property : entry.getValue()) { + assertFalse(property.name().isEmpty()); + assertFalse(property.jsonType().isEmpty()); + } + } + } } diff --git a/core/src/test/java/art/arcane/iris/spi/IrisPlatformsBindLifecycleTest.java b/core/src/test/java/art/arcane/iris/spi/IrisPlatformsBindLifecycleTest.java new file mode 100644 index 000000000..3daf3fdc5 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/spi/IrisPlatformsBindLifecycleTest.java @@ -0,0 +1,46 @@ +package art.arcane.iris.spi; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class IrisPlatformsBindLifecycleTest { + @Before + public void resetBinding() { + IrisPlatforms.unbind(); + } + + @After + public void clearBinding() { + IrisPlatforms.unbind(); + } + + @Test + public void rebindsAfterUnbind() { + IrisPlatform first = mock(IrisPlatform.class); + IrisPlatforms.bind(first); + assertTrue(IrisPlatforms.isBound()); + assertSame(first, IrisPlatforms.get()); + + IrisPlatforms.unbind(); + assertFalse(IrisPlatforms.isBound()); + + IrisPlatform second = mock(IrisPlatform.class); + IrisPlatforms.bind(second); + assertTrue(IrisPlatforms.isBound()); + assertSame(second, IrisPlatforms.get()); + } + + @Test + public void unbindIsIdempotentWhenUnbound() { + IrisPlatforms.unbind(); + assertFalse(IrisPlatforms.isBound()); + IrisPlatforms.unbind(); + assertFalse(IrisPlatforms.isBound()); + } +} diff --git a/core/src/test/java/art/arcane/iris/spi/IrisServicesTest.java b/core/src/test/java/art/arcane/iris/spi/IrisServicesTest.java new file mode 100644 index 000000000..735e24eba --- /dev/null +++ b/core/src/test/java/art/arcane/iris/spi/IrisServicesTest.java @@ -0,0 +1,53 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi; + +import org.junit.Test; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class IrisServicesTest { + @Test + public void removeThenReregisterRebindsService() { + Sample first = new Sample(); + Sample second = new Sample(); + + IrisServices.register(Sample.class, first); + assertSame(first, IrisServices.get(Sample.class)); + + IrisServices.remove(Sample.class); + assertNull(IrisServices.getOrNull(Sample.class)); + + IrisServices.register(Sample.class, second); + assertSame(second, IrisServices.get(Sample.class)); + + IrisServices.remove(Sample.class); + assertNull(IrisServices.getOrNull(Sample.class)); + } + + @Test + public void removeMissingServiceIsNoOp() { + IrisServices.remove(Sample.class); + assertNull(IrisServices.getOrNull(Sample.class)); + } + + private static final class Sample { + } +} diff --git a/docs/mc-version-bump.md b/docs/mc-version-bump.md new file mode 100644 index 000000000..be114c3b9 --- /dev/null +++ b/docs/mc-version-bump.md @@ -0,0 +1,80 @@ +# Minecraft Version Bump Checklist + +`gradle.properties` `minecraftVersion` is the single source of truth for the target Minecraft +version. Most build outputs derive from it. This document lists every edit required to move Iris +to a new Minecraft version, in order. + +## Source of truth + +`gradle.properties`: + +- `minecraftVersion` — target MC version (e.g. `26.2`). Drives the Bukkit plugin `api-version`, + `BuildConstants.MINECRAFT_VERSION`, the `com.mojang:minecraft` coordinate, all mod-metadata + minecraft ranges, and every dist/jar artifact name. +- `fabricLoaderVersion` — Fabric Loader version. +- `forgeVersion` — Forge version (`-`). +- `neoForgeVersion` — NeoForge version. +- `irisVersion` — bump the trailing `-` suffix to match (e.g. `4.0.0-26.2` -> `4.0.0-27.0`). + +## Ordered steps + +1. Edit `gradle.properties`: update `minecraftVersion`, `fabricLoaderVersion`, `forgeVersion`, + `neoForgeVersion`, and the `irisVersion` suffix. + +2. Edit `gradle/libs.versions.toml`: + - `spigot` — the Spigot/Paper API pin used to compile against (`-R0.1-SNAPSHOT`). + - `fabricApi-*` — the six Fabric API module versions, if the new MC requires different + Fabric API builds. Each module is versioned independently (`+`). + +3. Edit `core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java` (manual, structural): + - Append a new enum constant `V_("", , ::new)`. + - `packFormat` comes from https://minecraft.wiki/w/Pack_format. + - `getLatest()` returns the last enum constant, so append; do not reorder. + - Add a matching `IDataFixer` implementation under `core/src/main/java/art/arcane/iris/core/nms/datapack/` + if the datapack format changed. + +4. Register the new Bukkit NMS binding module: + - `settings.gradle` — add `include(':adapters:bukkit:nms:v__R')`. + - `build.gradle` — add the binding to the `nmsBindings` map: + `v__R: ''` (e.g. `'26.2.build.25-alpha'`). + - Create the binding sources under `adapters/bukkit/nms/v__R/`. + +5. Update loader version-range metadata (manual floors/ranges only; the `minecraft` ranges are + templated from `minecraftVersion` and need no edit): + - `adapters/fabric/src/main/resources/fabric.mod.json` — `minecraft` is `~${minecraftVersion}` + (auto). Update the `fabricloader` floor (`>=0.19.0`) if the loader minimum changes, and the + `jars` list if the bundled Fabric API modules change. + - `adapters/forge/src/main/resources/META-INF/mods.toml` — `minecraft` versionRange is + `[${minecraftVersion}]` (auto). Update `loaderVersion` (`[64,)`) and the `forge` dependency + versionRange for the new Forge line. + - `adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml` — `minecraft` versionRange + is `[${minecraftVersion}]` (auto). Update `loaderVersion` and the `neoforge` dependency range + if the loader minimum changes. + +6. Build and verify: + - `./gradlew :core:check` + - `./gradlew buildBukkit` + - `./gradlew buildFabric` + - `./gradlew buildForge` + - `./gradlew buildNeoforge` + +## Derived automatically (do not hand-edit on a version bump) + +- Bukkit plugin `api-version` — `adapters/bukkit/plugin/build.gradle` reads `minecraftVersion`. +- `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in + `core/build.gradle` from `minecraftVersion`; consumed by `Tasks.supportedVersions`. +- Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`. +- Dist/jar artifact names and the `com.mojang:minecraft` coordinate — composed from + `minecraftVersion` in the build scripts. + +## Notes + +- `build.gradle`, the adapter `build.gradle` files, and `settings.gradle` carry `.getOrElse('26.2')` + defensive defaults for the version properties. `gradle.properties` always overrides them, so a + bump does not require touching those fallbacks; refresh them only if the checked-in default should + track the current release. +- The Java literal `"26.2"` intentionally remains in `DataVersion.java` (structural enum constant), + `core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java`, and + `core/src/test/java/art/arcane/iris/core/lifecycle/PaperLibBootstrapTest.java`. The test files use + MC version strings as parser fixtures, not as a version source; update them only when the version + string formats they exercise change. diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 000000000..44917ad9b --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,78 @@ +# Iris Release Checklist + +Manual release procedure. There is no release automation by design: every step below is run by +a person and verified by eye. Work top to bottom; do not skip the verify gates. + +Reference values below assume the current `gradle.properties`: `irisVersion=4.0.0-26.2`, +`minecraftVersion=26.2`, `fabricLoaderVersion=0.19.3`, `forgeVersion=26.2-65.0.0`, +`neoForgeVersion=26.2.0.6-beta`. For a Minecraft version bump, do `docs/mc-version-bump.md` first, +then start this checklist. + +## a. Preflight + +- [ ] Working tree clean on the exact commit you intend to tag (`git status` shows nothing to commit). +- [ ] CI is green on that commit. The `verify` job (`.github/workflows/ci.yml`) runs + `./gradlew :core:check :spi:build :probe:deserializationProbe` on JDK 25; the `build-artifacts` + matrix builds all four platform jars. Do not release on a red or stale run. +- [ ] `MasterChangelog.MD` Iris section is coherent: one consolidated entry set, deduplicated, no + date-sliced headers, and it describes the current shipped state (not superseded intermediate work). +- [ ] Version fields correct in `gradle.properties`: `irisVersion` is the release version and its + trailing `-` suffix matches `minecraftVersion`. For a Minecraft bump, confirm every step in + `docs/mc-version-bump.md` is done (loader ranges, `DataVersion`, NMS binding). +- [ ] JDK 25 is the active toolchain locally (`java -version` reports 25). + +## b. Build + +- [ ] From the Iris project root: `./gradlew buildAll` +- [ ] `dist/` contains the four platform jars (exact names for this release): + - [ ] `Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar` (Bukkit/Paper/Purpur/Spigot/Folia plugin) + - [ ] `Iris v4.0.0-26.2 [Fabric] 26.2+0.19.3.jar` + - [ ] `Iris v4.0.0-26.2 [Forge] 26.2+65.0.0.jar` + - [ ] `Iris v4.0.0-26.2 [NeoForge] 26.2+26.2.0.6-beta.jar` + - Naming pattern: `Iris v [] [+].jar`. +- [ ] The developer SPI jar is built by the same run at `spi/build/libs/iris-spi-4.0.0-26.2.jar`. + It is the platform-API artifact for downstream developers and is not copied into `dist/`; it is + not uploaded to the mod portals (see publish). +- [ ] Each mod jar bundles core + spi + shaded libs; each plugin/mod jar is self-contained. + +## c. Verify (release gates) + +- [ ] `:core:check` and `:probe:deserializationProbe` passed in CI on the tag commit (a. covers this). +- [ ] Golden-hash determinism VERIFY passes on all four platforms and matches the same hash: + - [ ] Bukkit plugin: `/iris goldenhash verify ` + - [ ] Fabric mod: `/iris goldenhash verify ` + - [ ] Forge mod: `/iris goldenhash verify ` + - [ ] NeoForge mod: `/iris goldenhash verify ` + - The hash is interchangeable across platforms: all four MUST report identical output for the same + pack and seed. Any mismatch blocks the release. +- [ ] Live modded content-mod gate: on each loader, boot the mod jar alongside a real content mod + (e.g. Create) and generate an Iris world. Confirm no load-time rejection, no class-loader crash, + and that modded blocks/items/entities author and generate. + - [ ] Fabric + content mod + - [ ] Forge + content mod + - [ ] NeoForge + content mod +- [ ] Client-mod matrix: install the mod on the client (keybind `H` toggles the pregen HUD) and confirm: + - [ ] Modded server + modded client: HUD receives pregen progress over `irisworldgen:main`. + - [ ] Modded server + vanilla client: server generates normally; vanilla client is unaffected. + - [ ] Paper (Bukkit) server + modded client: HUD receives pregen progress over vanilla plugin messaging. + - [ ] Folia smoke: plugin loads and an Iris world generates on Folia. + - [ ] Non-Iris server + modded client: client is inert, no errors. + +## d. Publish (all manual, no automation) + +- [ ] Modrinth: upload the three mod jars and the plugin jar. Tag loaders `fabric` / `forge` / + `neoforge` on the mod files; mark the environment server + client; set game version 26.2. +- [ ] CurseForge: upload the three mod jars with the matching loader tags and game version 26.2. +- [ ] Existing plugin distribution channels: publish the plugin jar + (`Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar`) where the plugin already ships. +- [ ] Sentry: add a release note / mark the release so incoming reports map to this version + (the mod version string is the Sentry release tag). +- [ ] Storepage / `listing.json` staleness review: check the listing copy for pre-4.0 content + (Bukkit-only framing, old feature lists, screenshots). Flag anything stale for update before or + right after launch. (Review only; this checklist does not change store copy.) + +## e. Post + +- [ ] Tag the release commit (`v`) and push the tag. The `release-bundle` CI job + (tag-gated, `refs/tags/v*`) runs `buildAll` and uploads the `dist/` bundle for the record. +- [ ] Announce the release on the community channels once the portals show the new files live. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9fff231b6..1437bf293 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,17 @@ kgenerators = "7.3" # https://repo.codemc.io/repository/maven-public/me/kryniowe multiverseCore = "5.1.0" craftengine = "0.0.67" # https://github.com/Xiao-MoMi/craft-engine/releases +# Fabric API (net.fabricmc.fabric-api) - each module is independently versioned +fabricApi-base = "2.0.4+ece063239e" +fabricApi-registrySync = "7.1.0+c7bd5b8e9e" +fabricApi-resourceLoader = "2.0.13+9edec1269e" +fabricApi-lifecycleEvents = "4.1.3+4575b05f9e" +fabricApi-commandApi = "3.1.0+00cb03469e" +fabricApi-eventsInteraction = "5.2.6+8f57f7ee9e" +fabricApi-networking = "6.3.3+72073ef09e" +fabricApi-rendering = "25.3.1+6988455e9e" +fabricApi-keyMapping = "2.0.5+e2bdee789e" + [libraries] # Core Libraries lombok = { module = "org.projectlombok:lombok", version.ref ="lombok" } @@ -108,6 +119,17 @@ multiverseCore = { module = "org.mvplugins.multiverse.core:multiverse-core", ver craftengine-core = { module = "net.momirealms:craft-engine-core", version.ref = "craftengine" } craftengine-bukkit = { module = "net.momirealms:craft-engine-bukkit", version.ref = "craftengine" } +# Fabric API +fabricApi-base = { module = "net.fabricmc.fabric-api:fabric-api-base", version.ref = "fabricApi-base" } +fabricApi-registrySync = { module = "net.fabricmc.fabric-api:fabric-registry-sync-v0", version.ref = "fabricApi-registrySync" } +fabricApi-resourceLoader = { module = "net.fabricmc.fabric-api:fabric-resource-loader-v1", version.ref = "fabricApi-resourceLoader" } +fabricApi-lifecycleEvents = { module = "net.fabricmc.fabric-api:fabric-lifecycle-events-v1", version.ref = "fabricApi-lifecycleEvents" } +fabricApi-commandApi = { module = "net.fabricmc.fabric-api:fabric-command-api-v2", version.ref = "fabricApi-commandApi" } +fabricApi-eventsInteraction = { module = "net.fabricmc.fabric-api:fabric-events-interaction-v0", version.ref = "fabricApi-eventsInteraction" } +fabricApi-networking = { module = "net.fabricmc.fabric-api:fabric-networking-api-v1", version.ref = "fabricApi-networking" } +fabricApi-rendering = { module = "net.fabricmc.fabric-api:fabric-rendering-v1", version.ref = "fabricApi-rendering" } +fabricApi-keyMapping = { module = "net.fabricmc.fabric-api:fabric-key-mapping-api-v1", version.ref = "fabricApi-keyMapping" } + [plugins] shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } slimjar = { id = "de.crazydev22.slimjar", version.ref = "slimjar" } diff --git a/probe/build.gradle b/probe/build.gradle index 83295a496..9f30e3339 100644 --- a/probe/build.gradle +++ b/probe/build.gradle @@ -59,3 +59,17 @@ tasks.register('genProbe', JavaExec) { args(probePack, probeRadius) dependsOn(':core:compileJava') } + +String deserializationPack = providers.gradleProperty('deserializationPack') + .orElse(project.file('src/main/resources/deserialization-pack').absolutePath) + .get() + +tasks.register('deserializationProbe', JavaExec) { + group = 'verification' + description = 'Deserializes entity, spawner and loot pack content through the real Iris Gson loaders on a Bukkit-free JVM to keep Bukkit-typed pack-model fields dead.' + mainClass = 'art.arcane.iris.probe.DeserializationProbe' + classpath = sourceSets.main.runtimeClasspath + jvmArgs('--add-modules', 'jdk.incubator.vector') + args(deserializationPack) + dependsOn(':core:compileJava') +} diff --git a/probe/src/main/java/art/arcane/iris/probe/DeserializationProbe.java b/probe/src/main/java/art/arcane/iris/probe/DeserializationProbe.java new file mode 100644 index 000000000..165eaac50 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/probe/DeserializationProbe.java @@ -0,0 +1,269 @@ +/* + * 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 . + */ + +package art.arcane.iris.probe; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.MeteredCache; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.volmlib.util.collection.KList; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class DeserializationProbe { + private static final String FIXTURE_RESOURCE = "deserialization-pack"; + private static final String ENTITY_KEY = "probe_entity"; + private static final String SPAWNER_KEY = "probe_spawner"; + private static final String LOOT_KEY = "probe_loot"; + private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); + + @FunctionalInterface + private interface ProbeStep { + void run() throws Exception; + } + + private static final class InertPreservation implements PreservationRegistry { + @Override + public void register(Thread thread) { + } + + @Override + public void register(ExecutorService service) { + } + + @Override + public void registerCache(MeteredCache cache) { + } + + @Override + public void dereference() { + } + } + + public static void main(String[] args) throws Exception { + IrisPlatforms.bind(new StubPlatform()); + StubPlatform.verbose(false); + StubPlatform.errorSink(REPORTED::add); + IrisServices.register(PreservationRegistry.class, new InertPreservation()); + + boolean bukkitPresent = bukkitPresent(); + System.out.println("[deserprobe] org.bukkit on classpath: " + bukkitPresent); + if (bukkitPresent) { + System.out.println("[deserprobe] FAIL: org.bukkit is on the probe runtime classpath; this gate only proves the Bukkit-free deserialization contract when Bukkit is absent."); + System.exit(1); + return; + } + + File fixtureSource = resolveFixture(args); + if (fixtureSource == null || !fixtureSource.isDirectory()) { + System.out.println("[deserprobe] FAIL: fixture pack folder not found" + + (fixtureSource == null ? "" : ": " + fixtureSource.getAbsolutePath())); + System.exit(1); + return; + } + + File workRoot = Files.createTempDirectory("iris-deserprobe").toFile(); + File pack = clonePack(fixtureSource, workRoot); + System.out.println("[deserprobe] fixture: " + fixtureSource.getAbsolutePath()); + System.out.println("[deserprobe] work copy: " + pack.getAbsolutePath()); + + IrisData data; + try { + data = IrisData.get(pack); + } catch (Throwable e) { + System.out.println("[deserprobe] FAIL: IrisData loader construction threw before any deserialization"); + e.printStackTrace(System.out); + printReported(drain()); + System.exit(1); + return; + } + + int failures = 0; + failures += run("entity<" + ENTITY_KEY + ">", () -> assertEntity(data)); + failures += run("spawner<" + SPAWNER_KEY + ">", () -> assertSpawner(data)); + failures += run("loot<" + LOOT_KEY + ">", () -> assertLoot(data)); + + List residual = drain(); + if (!residual.isEmpty()) { + System.out.println("[deserprobe] FAIL: " + residual.size() + " residual error(s) reported after deserialization:"); + printReported(residual); + failures += residual.size(); + } + + System.out.println("[deserprobe] RESULT: " + (failures == 0 ? "PASS" : "FAIL")); + System.exit(failures == 0 ? 0 : 1); + } + + private static void assertEntity(IrisData data) { + IrisEntity entity = data.getEntityLoader().load(ENTITY_KEY); + require(entity != null, "entity deserialized"); + require("minecraft:panda".equals(entity.getType()), "type round-trips as authored namespaced string"); + require("COMMAND".equals(entity.getReason()), "reason retained as string"); + require("BROWN".equals(entity.getPandaMainGene()), "pandaMainGene retained as string"); + require("LAZY".equals(entity.getPandaHiddenGene()), "pandaHiddenGene retained as string"); + require(entity.isBaby(), "baby flag retained"); + require(entity.isGlowing(), "glowing flag retained"); + + IrisLoot helmet = entity.getHelmet(); + require(helmet != null, "helmet equipment loot deserialized"); + require(helmet.isUnbreakable(), "helmet unbreakable flag retained"); + require(helmet.getItemFlags().contains("HIDE_ATTRIBUTES"), "helmet item flags retained as strings"); + KList helmetEnchants = helmet.getEnchantments(); + require(!helmetEnchants.isEmpty(), "helmet enchantments deserialized"); + require(entity.getMainHand() != null, "mainHand equipment loot deserialized"); + + List passengers = entity.getPassengers(); + require(passengers.size() == 1, "single passenger entity deserialized"); + require("minecraft:zombie".equals(passengers.get(0).getType()), "nested passenger type round-trips"); + + require(entity.getAttributes().size() == 1, "attribute modifier deserialized"); + require("generic.max_health".equals(entity.getAttributes().get(0).getAttribute()), "attribute key retained as string"); + + require(entity.getLoot() != null, "loot reference deserialized"); + require(entity.getLoot().getTables().contains(LOOT_KEY), "loot reference table retained"); + } + + private static void assertSpawner(IrisData data) { + IrisSpawner spawner = data.getSpawnerLoader().load(SPAWNER_KEY); + require(spawner != null, "spawner deserialized"); + List spawns = spawner.getSpawns(); + require(spawns.size() == 1, "single entity spawn deserialized"); + require(ENTITY_KEY.equals(spawns.get(0).getEntity()), "spawn references the probe entity by key"); + require("NORMAL".equals(spawner.getGroup().name()), "spawn group enum deserialized"); + require(spawner.getMaxEntitiesPerChunk() == 4, "spawner numeric field retained"); + } + + private static void assertLoot(IrisData data) { + IrisLootTable table = data.getLootLoader().load(LOOT_KEY); + require(table != null, "loot table deserialized"); + require(LOOT_KEY.equals(table.getName()), "loot table name retained"); + require(table.getRarity() == 4, "loot table rarity retained"); + + KList items = table.getLoot(); + require(!items.isEmpty(), "loot items deserialized"); + IrisLoot item = items.get(0); + require(item.getMaxAmount() == 3, "loot item amount retained"); + require("STORAGE".equals(item.getSlotTypes().name()), "loot item slot type enum deserialized"); + require(item.getItemFlags().contains("HIDE_ENCHANTS"), "loot item flags retained as strings"); + require(!item.getEnchantments().isEmpty(), "loot item enchantments deserialized"); + require("unbreaking".equals(item.getEnchantments().get(0).getEnchantment()), "loot item enchantment key retained as string"); + } + + private static void require(boolean condition, String description) { + if (!condition) { + throw new IllegalStateException("assertion failed: " + description); + } + } + + private static int run(String label, ProbeStep step) { + drain(); + Throwable thrown = null; + try { + step.run(); + } catch (Throwable e) { + thrown = e; + } + List reported = drain(); + int failures = 0; + if (thrown != null) { + System.out.println("[deserprobe] " + label + " FAILED: " + thrown.getClass().getName() + ": " + thrown.getMessage()); + thrown.printStackTrace(System.out); + failures++; + } + if (!reported.isEmpty()) { + System.out.println("[deserprobe] " + label + " FAILED: " + reported.size() + " error(s) reported (swallowed) during deserialization:"); + printReported(reported); + failures += reported.size(); + } + if (failures == 0) { + System.out.println("[deserprobe] " + label + " OK"); + } + return failures; + } + + private static boolean bukkitPresent() { + try { + Class.forName("org.bukkit.Bukkit", false, DeserializationProbe.class.getClassLoader()); + return true; + } catch (ClassNotFoundException absent) { + return false; + } + } + + private static File resolveFixture(String[] args) throws Exception { + if (args.length > 0 && !args[0].isBlank()) { + return new File(args[0]); + } + URL resource = DeserializationProbe.class.getClassLoader().getResource(FIXTURE_RESOURCE); + if (resource == null) { + return null; + } + return new File(resource.toURI()); + } + + private static File clonePack(File source, File workRoot) throws IOException { + Path sourcePath = source.toPath(); + Path destinationPath = workRoot.toPath().resolve(source.getName()); + List paths; + try (Stream walk = Files.walk(sourcePath)) { + paths = walk.collect(Collectors.toList()); + } + for (Path path : paths) { + Path target = destinationPath.resolve(sourcePath.relativize(path).toString()); + if (Files.isDirectory(path)) { + Files.createDirectories(target); + } else { + Files.createDirectories(target.getParent()); + Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING); + } + } + return destinationPath.toFile(); + } + + private static List drain() { + synchronized (REPORTED) { + List drained = new ArrayList<>(REPORTED); + REPORTED.clear(); + return drained; + } + } + + private static void printReported(List errors) { + for (Throwable error : errors) { + error.printStackTrace(System.out); + } + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index 3da84db28..7153ff4b1 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -49,6 +49,7 @@ import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; public final class GenerationProbe { private static final String DIMENSION_KEY = "overworld"; @@ -61,6 +62,10 @@ public final class GenerationProbe { public void register(Thread thread) { } + @Override + public void register(ExecutorService service) { + } + @Override public void registerCache(MeteredCache cache) { } diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java index beb786f33..46d18bbe2 100644 --- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java +++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java @@ -22,6 +22,7 @@ import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.LogLevel; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiomeWriter; +import art.arcane.iris.spi.PlatformBlockProperty; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformEntityType; import art.arcane.iris.spi.PlatformItem; @@ -32,6 +33,7 @@ import art.arcane.iris.spi.PlatformWorld; import java.io.File; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -354,6 +356,36 @@ public final class StubPlatform implements IrisPlatform { public List structureKeys() { return List.of(); } + + @Override + public List itemKeys() { + return List.of(); + } + + @Override + public List entityKeys() { + return List.of(); + } + + @Override + public List blockTypeKeys() { + return List.of(); + } + + @Override + public List enchantmentKeys() { + return List.of(); + } + + @Override + public List potionEffectKeys() { + return List.of(); + } + + @Override + public Map> blockStateProperties() { + return Map.of(); + } } @Override diff --git a/probe/src/main/resources/deserialization-pack/entities/probe_entity.json b/probe/src/main/resources/deserialization-pack/entities/probe_entity.json new file mode 100644 index 000000000..cb821b533 --- /dev/null +++ b/probe/src/main/resources/deserialization-pack/entities/probe_entity.json @@ -0,0 +1,67 @@ +{ + "type": "minecraft:panda", + "reason": "COMMAND", + "customName": "&aProbe Panda", + "customNameVisible": true, + "glowing": true, + "baby": true, + "pandaMainGene": "BROWN", + "pandaHiddenGene": "LAZY", + "helmet": { + "type": "minecraft:diamond_helmet", + "slotTypes": "STORAGE", + "rarity": 2, + "minAmount": 1, + "maxAmount": 1, + "unbreakable": true, + "itemFlags": [ + "HIDE_ATTRIBUTES" + ], + "enchantments": [ + { + "enchantment": "protection", + "minLevel": 3, + "maxLevel": 4, + "chance": 1.0 + } + ], + "lore": [ + "&7Probe helmet" + ] + }, + "mainHand": { + "type": "minecraft:diamond_sword", + "rarity": 1, + "enchantments": [ + { + "enchantment": "sharpness", + "minLevel": 5, + "maxLevel": 5 + } + ] + }, + "passengers": [ + { + "type": "minecraft:zombie", + "reason": "NATURAL", + "baby": false + } + ], + "attributes": [ + { + "attribute": "generic.max_health", + "name": "probe_health", + "operation": "ADD_NUMBER", + "minAmount": 20, + "maxAmount": 20, + "chance": 1.0 + } + ], + "loot": { + "mode": "ADD", + "tables": [ + "probe_loot" + ], + "multiplier": 1.5 + } +} diff --git a/probe/src/main/resources/deserialization-pack/loot/probe_loot.json b/probe/src/main/resources/deserialization-pack/loot/probe_loot.json new file mode 100644 index 000000000..bd50cb9a4 --- /dev/null +++ b/probe/src/main/resources/deserialization-pack/loot/probe_loot.json @@ -0,0 +1,31 @@ +{ + "name": "probe_loot", + "rarity": 4, + "maxPicked": 3, + "minPicked": 1, + "maxTries": 8, + "loot": [ + { + "type": "minecraft:diamond", + "slotTypes": "STORAGE", + "rarity": 2, + "minAmount": 1, + "maxAmount": 3, + "unbreakable": false, + "itemFlags": [ + "HIDE_ENCHANTS" + ], + "enchantments": [ + { + "enchantment": "unbreaking", + "minLevel": 3, + "maxLevel": 3, + "chance": 1.0 + } + ], + "lore": [ + "&bShiny" + ] + } + ] +} diff --git a/probe/src/main/resources/deserialization-pack/spawners/probe_spawner.json b/probe/src/main/resources/deserialization-pack/spawners/probe_spawner.json new file mode 100644 index 000000000..815e838bb --- /dev/null +++ b/probe/src/main/resources/deserialization-pack/spawners/probe_spawner.json @@ -0,0 +1,23 @@ +{ + "spawns": [ + { + "entity": "probe_entity", + "rarity": 1, + "minSpawns": 1, + "maxSpawns": 2 + } + ], + "maxEntitiesPerChunk": 4, + "group": "NORMAL", + "weather": "ANY", + "maximumRate": { + "amount": 1, + "per": { + "seconds": 30 + } + }, + "allowedLightLevels": { + "min": 0, + "max": 7 + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java b/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java index b36ef62e5..0c92f5d65 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java @@ -34,6 +34,10 @@ public final class IrisPlatforms { platform = p; } + public static synchronized void unbind() { + platform = null; + } + public static IrisPlatform get() { IrisPlatform bound = platform; if (bound == null) { diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisServices.java b/spi/src/main/java/art/arcane/iris/spi/IrisServices.java index 80bd36321..0da0a3040 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisServices.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisServices.java @@ -38,6 +38,15 @@ public final class IrisServices { return type.cast(implementation); } + public static T getOrNull(Class type) { + Object implementation = SERVICES.get(type); + return implementation == null ? null : type.cast(implementation); + } + + public static void remove(Class type) { + SERVICES.remove(type); + } + public static void clear() { SERVICES.clear(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java new file mode 100644 index 000000000..f8b878a9f --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java @@ -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 . + */ + +package art.arcane.iris.spi; + +import java.util.List; + +public record PlatformBlockProperty(String name, String jsonType, Object defaultValue, List allowedValues, PlatformNumericRange numericRange) { + public boolean hasNumericRange() { + return numericRange != null; + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java b/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java new file mode 100644 index 000000000..b48ec1116 --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi; + +public record PlatformNumericRange(double minimum, double maximum, boolean exclusiveMinimum, boolean exclusiveMaximum) { +} diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java b/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java index 783081269..d155451eb 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java @@ -19,6 +19,7 @@ package art.arcane.iris.spi; import java.util.List; +import java.util.Map; /** * Resolves namespaced string keys against the platform's live registries into interned neutral handles. @@ -45,4 +46,16 @@ public interface PlatformRegistries { List biomeKeys(); List structureKeys(); + + List itemKeys(); + + List entityKeys(); + + List blockTypeKeys(); + + List enchantmentKeys(); + + List potionEffectKeys(); + + Map> blockStateProperties(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java new file mode 100644 index 000000000..37970bedf --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java @@ -0,0 +1,131 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +import java.util.List; + +public sealed interface IrisMessage { + int messageTypeId(); + + record ClientHello(int protocolVersion, long capabilities) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_CLIENT_HELLO; + } + } + + record ServerHello(int protocolVersion, long capabilities, String serverBrand, boolean irisActive) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_SERVER_HELLO; + } + } + + record PregenProgress(long jobId, long chunksDone, long chunksTotal, double chunksPerSecond, long etaMillis, int state) implements IrisMessage { + public static final int STATE_RUNNING = 0; + public static final int STATE_PAUSED = 1; + + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_PREGEN_PROGRESS; + } + } + + record PregenEnd(long jobId, boolean completed) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_PREGEN_END; + } + } + + record DimensionStatus(String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_DIMENSION_STATUS; + } + } + + record CursorInfoRequest(int blockX, int blockZ) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_CURSOR_INFO_REQUEST; + } + } + + record CursorInfo(int blockX, int blockZ, String biomeKey, String regionKey, String caveBiomeKey, int height, String dimensionKey) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_CURSOR_INFO; + } + } + + record VisionTileRequest(int tileX, int tileZ, int zoomLevel) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_VISION_TILE_REQUEST; + } + } + + record VisionTile(int tileX, int tileZ, int zoomLevel, int sequence, int chunkIndex, int chunkCount, byte[] data) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_VISION_TILE; + } + } + + record VisionMarkers(int tileX, int tileZ, int zoomLevel, List markers) implements IrisMessage { + public record Marker(int blockX, int blockZ, int kind, String label) { + } + + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_VISION_MARKERS; + } + } + + record PregenRegionDelta(long jobId, int regionX, int regionZ, int state) implements IrisMessage { + public static final int STATE_PENDING = 0; + public static final int STATE_GENERATING = 1; + public static final int STATE_DONE = 2; + + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_PREGEN_REGION_DELTA; + } + } + + record StudioHotload(String packKey, int changedFiles, boolean failed, String message) implements IrisMessage { + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_STUDIO_HOTLOAD; + } + } + + record Toast(int kind, String title, String body) implements IrisMessage { + public static final int KIND_INFO = 0; + public static final int KIND_SUCCESS = 1; + public static final int KIND_WARNING = 2; + public static final int KIND_ERROR = 3; + + @Override + public int messageTypeId() { + return IrisProtocol.TYPE_TOAST; + } + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java new file mode 100644 index 000000000..b4f507c77 --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java @@ -0,0 +1,164 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +import java.util.ArrayList; +import java.util.List; + +public final class IrisMessageCodec { + private IrisMessageCodec() { + } + + public static byte[] encode(IrisMessage message) { + IrisWireWriter writer = new IrisWireWriter(); + writer.writeVarInt(message.messageTypeId()); + switch (message) { + case IrisMessage.ClientHello clientHello -> { + writer.writeInt(clientHello.protocolVersion()); + writer.writeLong(clientHello.capabilities()); + } + case IrisMessage.ServerHello serverHello -> { + writer.writeInt(serverHello.protocolVersion()); + writer.writeLong(serverHello.capabilities()); + writer.writeString(serverHello.serverBrand()); + writer.writeBoolean(serverHello.irisActive()); + } + case IrisMessage.PregenProgress pregenProgress -> { + writer.writeLong(pregenProgress.jobId()); + writer.writeLong(pregenProgress.chunksDone()); + writer.writeLong(pregenProgress.chunksTotal()); + writer.writeDouble(pregenProgress.chunksPerSecond()); + writer.writeLong(pregenProgress.etaMillis()); + writer.writeInt(pregenProgress.state()); + } + case IrisMessage.PregenEnd pregenEnd -> { + writer.writeLong(pregenEnd.jobId()); + writer.writeBoolean(pregenEnd.completed()); + } + case IrisMessage.DimensionStatus dimensionStatus -> { + writer.writeString(dimensionStatus.dimensionKey()); + writer.writeString(dimensionStatus.packKey()); + writer.writeLong(dimensionStatus.seed()); + writer.writeInt(dimensionStatus.minY()); + writer.writeInt(dimensionStatus.maxY()); + writer.writeBoolean(dimensionStatus.irisWorld()); + } + case IrisMessage.CursorInfoRequest cursorInfoRequest -> { + writer.writeInt(cursorInfoRequest.blockX()); + writer.writeInt(cursorInfoRequest.blockZ()); + } + case IrisMessage.CursorInfo cursorInfo -> { + writer.writeInt(cursorInfo.blockX()); + writer.writeInt(cursorInfo.blockZ()); + writer.writeString(cursorInfo.biomeKey()); + writer.writeString(cursorInfo.regionKey()); + writer.writeString(cursorInfo.caveBiomeKey()); + writer.writeInt(cursorInfo.height()); + writer.writeString(cursorInfo.dimensionKey()); + } + case IrisMessage.VisionTileRequest visionTileRequest -> { + writer.writeInt(visionTileRequest.tileX()); + writer.writeInt(visionTileRequest.tileZ()); + writer.writeInt(visionTileRequest.zoomLevel()); + } + case IrisMessage.VisionTile visionTile -> { + writer.writeInt(visionTile.tileX()); + writer.writeInt(visionTile.tileZ()); + writer.writeInt(visionTile.zoomLevel()); + writer.writeInt(visionTile.sequence()); + writer.writeInt(visionTile.chunkIndex()); + writer.writeInt(visionTile.chunkCount()); + writer.writeBytes(visionTile.data()); + } + case IrisMessage.VisionMarkers visionMarkers -> { + writer.writeInt(visionMarkers.tileX()); + writer.writeInt(visionMarkers.tileZ()); + writer.writeInt(visionMarkers.zoomLevel()); + List markers = visionMarkers.markers(); + writer.writeVarInt(markers.size()); + for (IrisMessage.VisionMarkers.Marker marker : markers) { + writer.writeInt(marker.blockX()); + writer.writeInt(marker.blockZ()); + writer.writeInt(marker.kind()); + writer.writeString(marker.label()); + } + } + case IrisMessage.PregenRegionDelta pregenRegionDelta -> { + writer.writeLong(pregenRegionDelta.jobId()); + writer.writeInt(pregenRegionDelta.regionX()); + writer.writeInt(pregenRegionDelta.regionZ()); + writer.writeInt(pregenRegionDelta.state()); + } + case IrisMessage.StudioHotload studioHotload -> { + writer.writeString(studioHotload.packKey()); + writer.writeInt(studioHotload.changedFiles()); + writer.writeBoolean(studioHotload.failed()); + writer.writeString(studioHotload.message()); + } + case IrisMessage.Toast toast -> { + writer.writeInt(toast.kind()); + writer.writeString(toast.title()); + writer.writeString(toast.body()); + } + } + return writer.toByteArray(); + } + + public static IrisMessage decode(byte[] frame) throws ProtocolException { + if (frame == null) { + throw new ProtocolException("null frame"); + } + if (frame.length > IrisProtocol.MAX_FRAME_BYTES) { + throw new ProtocolException("frame exceeds " + IrisProtocol.MAX_FRAME_BYTES + " byte cap"); + } + IrisWireReader reader = new IrisWireReader(frame); + int messageTypeId = reader.readVarInt(); + return switch (messageTypeId) { + case IrisProtocol.TYPE_CLIENT_HELLO -> new IrisMessage.ClientHello(reader.readInt(), reader.readLong()); + case IrisProtocol.TYPE_SERVER_HELLO -> new IrisMessage.ServerHello(reader.readInt(), reader.readLong(), reader.readString(), reader.readBoolean()); + case IrisProtocol.TYPE_PREGEN_PROGRESS -> new IrisMessage.PregenProgress(reader.readLong(), reader.readLong(), reader.readLong(), reader.readDouble(), reader.readLong(), reader.readInt()); + case IrisProtocol.TYPE_PREGEN_END -> new IrisMessage.PregenEnd(reader.readLong(), reader.readBoolean()); + case IrisProtocol.TYPE_DIMENSION_STATUS -> new IrisMessage.DimensionStatus(reader.readString(), reader.readString(), reader.readLong(), reader.readInt(), reader.readInt(), reader.readBoolean()); + case IrisProtocol.TYPE_CURSOR_INFO_REQUEST -> new IrisMessage.CursorInfoRequest(reader.readInt(), reader.readInt()); + case IrisProtocol.TYPE_CURSOR_INFO -> new IrisMessage.CursorInfo(reader.readInt(), reader.readInt(), reader.readString(), reader.readString(), reader.readString(), reader.readInt(), reader.readString()); + case IrisProtocol.TYPE_VISION_TILE_REQUEST -> new IrisMessage.VisionTileRequest(reader.readInt(), reader.readInt(), reader.readInt()); + case IrisProtocol.TYPE_VISION_TILE -> new IrisMessage.VisionTile(reader.readInt(), reader.readInt(), reader.readInt(), reader.readInt(), reader.readInt(), reader.readInt(), reader.readBytes()); + case IrisProtocol.TYPE_VISION_MARKERS -> decodeVisionMarkers(reader); + case IrisProtocol.TYPE_PREGEN_REGION_DELTA -> new IrisMessage.PregenRegionDelta(reader.readLong(), reader.readInt(), reader.readInt(), reader.readInt()); + case IrisProtocol.TYPE_STUDIO_HOTLOAD -> new IrisMessage.StudioHotload(reader.readString(), reader.readInt(), reader.readBoolean(), reader.readString()); + case IrisProtocol.TYPE_TOAST -> new IrisMessage.Toast(reader.readInt(), reader.readString(), reader.readString()); + default -> null; + }; + } + + private static IrisMessage.VisionMarkers decodeVisionMarkers(IrisWireReader reader) throws ProtocolException { + int tileX = reader.readInt(); + int tileZ = reader.readInt(); + int zoomLevel = reader.readInt(); + int count = reader.readVarInt(); + if (count < 0) { + throw new ProtocolException("negative marker count"); + } + List markers = new ArrayList<>(Math.min(count, IrisProtocol.MAX_VISION_MARKERS)); + for (int index = 0; index < count; index++) { + markers.add(new IrisMessage.VisionMarkers.Marker(reader.readInt(), reader.readInt(), reader.readInt(), reader.readString())); + } + return new IrisMessage.VisionMarkers(tileX, tileZ, zoomLevel, markers); + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java new file mode 100644 index 000000000..80e3966d5 --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +public final class IrisProtocol { + public static final int PROTOCOL_VERSION = 1; + public static final String CHANNEL = "irisworldgen:main"; + public static final int MAX_FRAME_BYTES = 24576; + public static final int MAX_INBOUND_FRAMES_PER_SECOND = 32; + public static final int MAX_VISION_TILE_REQUESTS_PER_SECOND = 8; + public static final int VISION_TILE_HEADER_BYTES = 25; + public static final int VISION_TILE_MAX_CHUNK_BYTES = 24000; + public static final int MAX_VISION_MARKERS = 256; + public static final long CAPABILITY_PREGEN = 1L << 0; + public static final long CAPABILITY_VISION = 1L << 1; + public static final long CAPABILITY_CURSOR = 1L << 2; + public static final long CAPABILITY_STUDIO = 1L << 3; + public static final int TYPE_CLIENT_HELLO = 1; + public static final int TYPE_SERVER_HELLO = 2; + public static final int TYPE_PREGEN_PROGRESS = 3; + public static final int TYPE_PREGEN_END = 4; + public static final int TYPE_DIMENSION_STATUS = 5; + public static final int TYPE_CURSOR_INFO_REQUEST = 6; + public static final int TYPE_CURSOR_INFO = 7; + public static final int TYPE_VISION_TILE_REQUEST = 8; + public static final int TYPE_VISION_TILE = 9; + public static final int TYPE_VISION_MARKERS = 10; + public static final int TYPE_PREGEN_REGION_DELTA = 11; + public static final int TYPE_STUDIO_HOTLOAD = 12; + public static final int TYPE_TOAST = 13; + + private IrisProtocol() { + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java new file mode 100644 index 000000000..e14a6ec49 --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java @@ -0,0 +1,118 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +import java.nio.charset.StandardCharsets; + +public final class IrisWireReader { + private final byte[] frame; + private final int limit; + private int position; + + public IrisWireReader(byte[] frame) { + this.frame = frame; + this.limit = frame.length; + this.position = 0; + } + + public int readVarInt() throws ProtocolException { + int result = 0; + int shift = 0; + for (int index = 0; index < 5; index++) { + requireRemaining(1); + int current = frame[position++] & 0xFF; + result |= (current & 0x7F) << shift; + if ((current & 0x80) == 0) { + return result; + } + shift += 7; + } + throw new ProtocolException("varint exceeds 5 bytes"); + } + + public int readInt() throws ProtocolException { + requireRemaining(4); + int value = ((frame[position] & 0xFF) << 24) + | ((frame[position + 1] & 0xFF) << 16) + | ((frame[position + 2] & 0xFF) << 8) + | (frame[position + 3] & 0xFF); + position += 4; + return value; + } + + public long readLong() throws ProtocolException { + requireRemaining(8); + long value = ((long) (frame[position] & 0xFF) << 56) + | ((long) (frame[position + 1] & 0xFF) << 48) + | ((long) (frame[position + 2] & 0xFF) << 40) + | ((long) (frame[position + 3] & 0xFF) << 32) + | ((long) (frame[position + 4] & 0xFF) << 24) + | ((long) (frame[position + 5] & 0xFF) << 16) + | ((long) (frame[position + 6] & 0xFF) << 8) + | (frame[position + 7] & 0xFF); + position += 8; + return value; + } + + public double readDouble() throws ProtocolException { + return Double.longBitsToDouble(readLong()); + } + + public boolean readBoolean() throws ProtocolException { + requireRemaining(1); + return frame[position++] != 0; + } + + public String readString() throws ProtocolException { + int declaredLength = readVarInt(); + if (declaredLength < 0) { + throw new ProtocolException("negative string length"); + } + if (declaredLength > remaining()) { + throw new ProtocolException("string length exceeds remaining frame bytes"); + } + String value = new String(frame, position, declaredLength, StandardCharsets.UTF_8); + position += declaredLength; + return value; + } + + public byte[] readBytes() throws ProtocolException { + int declaredLength = readVarInt(); + if (declaredLength < 0) { + throw new ProtocolException("negative byte array length"); + } + if (declaredLength > remaining()) { + throw new ProtocolException("byte array length exceeds remaining frame bytes"); + } + byte[] value = new byte[declaredLength]; + System.arraycopy(frame, position, value, 0, declaredLength); + position += declaredLength; + return value; + } + + private int remaining() { + return limit - position; + } + + private void requireRemaining(int count) throws ProtocolException { + if (position + count > limit) { + throw new ProtocolException("truncated frame: needed " + count + " bytes, had " + remaining()); + } + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java new file mode 100644 index 000000000..a56263b2a --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java @@ -0,0 +1,116 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public final class IrisWireWriter { + private byte[] buffer; + private int length; + + public IrisWireWriter() { + this.buffer = new byte[64]; + this.length = 0; + } + + public void writeVarInt(int value) { + int remaining = value; + while (true) { + int chunk = remaining & 0x7F; + remaining >>>= 7; + if (remaining != 0) { + writeByte(chunk | 0x80); + } else { + writeByte(chunk); + return; + } + } + } + + public void writeInt(int value) { + ensure(4); + buffer[length++] = (byte) (value >>> 24); + buffer[length++] = (byte) (value >>> 16); + buffer[length++] = (byte) (value >>> 8); + buffer[length++] = (byte) value; + } + + public void writeLong(long value) { + ensure(8); + buffer[length++] = (byte) (value >>> 56); + buffer[length++] = (byte) (value >>> 48); + buffer[length++] = (byte) (value >>> 40); + buffer[length++] = (byte) (value >>> 32); + buffer[length++] = (byte) (value >>> 24); + buffer[length++] = (byte) (value >>> 16); + buffer[length++] = (byte) (value >>> 8); + buffer[length++] = (byte) value; + } + + public void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + + public void writeBoolean(boolean value) { + writeByte(value ? 1 : 0); + } + + public void writeString(String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + writeVarInt(encoded.length); + ensure(encoded.length); + System.arraycopy(encoded, 0, buffer, length, encoded.length); + length += encoded.length; + } + + public void writeBytes(byte[] value) { + writeVarInt(value.length); + ensure(value.length); + System.arraycopy(value, 0, buffer, length, value.length); + length += value.length; + } + + public byte[] toByteArray() { + return Arrays.copyOf(buffer, length); + } + + private void writeByte(int value) { + ensure(1); + buffer[length++] = (byte) value; + } + + private void ensure(int extra) { + if ((long) length + extra > IrisProtocol.MAX_FRAME_BYTES) { + throw new IllegalStateException("Iris protocol frame exceeds " + IrisProtocol.MAX_FRAME_BYTES + " byte cap"); + } + int required = length + extra; + if (required <= buffer.length) { + return; + } + int grown = buffer.length; + while (grown < required) { + grown <<= 1; + } + if (grown > IrisProtocol.MAX_FRAME_BYTES) { + grown = IrisProtocol.MAX_FRAME_BYTES; + } + buffer = Arrays.copyOf(buffer, grown); + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java b/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java new file mode 100644 index 000000000..84c6857d6 --- /dev/null +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +package art.arcane.iris.spi.protocol; + +public class ProtocolException extends Exception { + public ProtocolException(String message) { + super(message); + } +}