Compare commits

..

19 Commits

Author SHA1 Message Date
Julian Krings eff598d005 update headless dev command 2025-04-02 16:42:39 +02:00
Julian Krings d86ec7b1cd Merge remote-tracking branch 'origin/dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
#	core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java
#	core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
#	nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java
#	nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java
#	nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java
2025-04-02 16:33:51 +02:00
Julian Krings 0d9a45dfd9 disable headless pregen on world creation for benchmark worlds 2025-03-18 16:12:09 +01:00
Julian Krings 8a55bbfd20 Merge branch 'dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
2025-03-07 16:17:23 +01:00
Julian Krings 5934c43b70 Merge branch 'dev' into feat/headless 2025-02-18 17:10:23 +01:00
Julian Krings 11567b13d3 potentially fix chunk position bug 2025-02-18 17:08:08 +01:00
Julian Krings 2087ba88b1 fix adding chunks to region while being saved 2025-02-09 12:33:20 +01:00
Julian Krings e9d1b9f18e prevent Biome.CUSTOM from being resolved on <1.21.3 2025-02-08 21:45:51 +01:00
Julian Krings 6e84d38680 set chunk status to full AFTER generation 2025-02-08 20:59:20 +01:00
Julian Krings 22622f6e8a set chunk status to full on creation 2025-02-08 20:22:18 +01:00
Julian Krings 735203aa95 exclude asm from shadowJar 2025-02-08 12:20:27 +01:00
Julian Krings 013bc365a9 implement headless on all supported versions 2025-02-08 12:07:13 +01:00
Julian Krings c2dfbac641 refactor headless to decrease duplicate code in nms bindings 2025-02-07 21:51:23 +01:00
Julian Krings 7d472c0b13 Merge branch 'dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
2025-02-06 23:29:22 +01:00
Julian Krings 5b4ab0a3c1 add headless pregen dev command 2025-02-04 10:55:39 +01:00
Julian Krings e8dd81b014 add headless to world creation 2025-01-29 16:36:04 +01:00
Julian Krings d32cc281e3 fix compile after merge 2025-01-29 14:32:04 +01:00
Julian Krings 2ff6b59271 Merge branch 'dev' into feat/headless 2025-01-29 14:30:17 +01:00
Julian Krings 3ff87566f5 implement headless for 1.21.4 2025-01-26 14:28:59 +01:00
1851 changed files with 105951 additions and 138940 deletions
-31
View File
@@ -1,31 +0,0 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (core, plugin, spi, probes)
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 verification gates
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
+1 -30
View File
@@ -10,33 +10,4 @@ libs/
collection/ collection/
out/ /core/src/main/java/com/volmit/iris/util/uniques/
tools/simd-bench/out/
tools/simd-bench/simd-bench.jar
DataPackExamples/
TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
adapters/*/run/
.codegraph/
plans/
.env
.env.*
!.env.example
local.properties
*.jks
*.p12
*.pfx
*.pem
*.key
*.hprof
credentials.json
service-account*.json
docs/

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+54 -115
View File
@@ -1,136 +1,75 @@
# Iris # Iris
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain, The master branch is for the latest version of minecraft.
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) # [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
Consider supporting development by buying Iris on Spigot. # Building
## Platforms Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.
| Platform | Artifact | Minecraft | Notes | Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|---|---|---|---|
| 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.4+ |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2.0.12-beta+ |
Java 25 is required on every platform. ## 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.
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen ### Command Line Builds
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.
## Install 1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
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-21.0.1` (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 21
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!
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start ### IDE Builds (for development)
the server. On first boot Iris downloads the default `overworld` pack automatically.
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is * Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris * Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
downloads the default `overworld` pack before the worldgen datapack is written, so the default prefer.
pack is fully active immediately. Packs installed later register their custom dimension types * Resync the project & run your newly created task (under the development folder in gradle tasks!)
(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.
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the # Iris Toolbelt
Create New World screen; the integrated server runs the same engine.
## The client mod Everyone needs a tool-belt.
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a ```java
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the package com.volmit.iris.core.tools;
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the // Get IrisDataManager from a world
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla IrisToolbelt.access(anyWorld).getCompound().getData();
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
mod is inert.
## Quickstart // Get Default Engine from world
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
Create and enter an Iris world. // Get the engine at the given height
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
Plugin (optional arguments are keyed): // IS THIS THING ON?
boolean yes=IrisToolbelt.isIrisWorld(world);
// 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
```
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
servers they are positional and composable:
```
/iris pregen start 352 world=myworld center=0,0 gui=false
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
```
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
console/status output. `/iris pregen status` reports progress on the plugin.
## Studio and VSCode workspace
The studio is the pack authoring environment, available on all platforms. Studio worlds are
transient — they are deleted on close and purged at startup.
```
/iris studio create <name> [template] scaffold a new pack (default template: example)
/iris studio open <pack> [seed] open a temporary studio world for live editing
/iris studio vscode [pack] write a .code-workspace with JSON schemas
/iris studio update [pack] regenerate the workspace schemas
/iris studio close close and discard the studio world
```
As in the quickstart, optional arguments are keyed on the plugin (`seed=1234`) and positional on
the mod.
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
files hotloads the changes and regenerates the schemas.
## Building from source
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
```
./gradlew buildAllToOut
```
builds every platform artifact into `dist/`:
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
Discord asking for free copies or a compile tutorial.
## Maintainer docs
- [Minecraft version bump checklist](docs/mc-version-bump.md)
- [Release checklist](docs/release-checklist.md)
@@ -1,556 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.CraftWorld;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
private static final int NOISE_BIOME_CACHE_MAX = 262144;
private final long seed;
private final Engine engine;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
this.seed = seed;
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
this.cacheDimension = engine.getDimension();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
boolean resolutionFailed = false;
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) {
biomes.add(vanillaHolder);
} else if (!i.isCustom()) {
resolutionFailed = true;
}
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) {
biomes.add(customHolder);
} else {
resolutionFailed = true;
}
}
}
}
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
biomes.add(fallback);
}
return new ArrayList<>(biomes);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
if (o != null) {
return o;
}
return invokeFor(type, source);
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
}
private static Object invokeFor(Class<?> returns, Object in) {
for (Method i : in.getClass().getMethods()) {
if (i.getReturnType().equals(returns)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
for (Field i : sourceType.getDeclaredFields()) {
if (i.getType().equals(returnType)) {
i.setAccessible(true);
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleStructureBiomes().stream();
}
Set<Holder<Biome>> possibleStructureBiomes() {
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
KMap<String, Holder<Biome>> m = new KMap<>();
if (customRegistry == null) {
return m;
}
for (IrisBiome i : engine.getAllBiomes()) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (holder == null) {
if (fallback != null) {
m.put(j.getId(), fallback);
}
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
continue;
}
m.put(j.getId(), holder);
}
}
}
return m;
}
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
if (customRegistry == null || registry == null) {
return Collections.unmodifiableMap(spawnBiomes);
}
for (IrisBiome irisBiome : engine.getAllBiomes()) {
if (!irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
return Collections.unmodifiableMap(spawnBiomes);
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
ensureCachesCurrent();
if (biome == null) {
return null;
}
return vanillaSpawnBiomes.get(biome.value());
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
}
}
return biomes;
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
return internalY > caveSwitchInternalY;
}
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
return getFallbackBiome();
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
return holder == null ? getFallbackBiome() : holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private void ensureCachesCurrent() {
IrisDimension dimension = engine.getDimension();
if (cacheDimension == dimension) {
return;
}
synchronized (this) {
if (cacheDimension == dimension) {
return;
}
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
}
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
if (holder != null) {
return holder;
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
return getFallbackBiome();
}
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
org.bukkit.block.Biome vanillaBiome = resolution.underground
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
if (holder != null) {
return holder;
}
return getFallbackBiome();
}
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
if (holder != null) {
return holder;
}
}
return getFallbackBiome();
}
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
if (engine == null || engine.isClosed()) {
return null;
}
if (engine.getComplex() == null) {
return null;
}
int blockX = x << 2;
int blockZ = z << 2;
int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean underground = false;
IrisBiome irisBiome;
if (deepUnderground) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null) {
return null;
}
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
}
private Holder<Biome> getFallbackBiome() {
if (fallbackBiome != null) {
return fallbackBiome;
}
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
if (holder != null) {
return holder;
}
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static long packColumnKey(int x, int z) {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
return null;
}
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
customBiomeId.toLowerCase(java.util.Locale.ROOT)
);
Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) {
return null;
}
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
return null;
}
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
if (optionalReferenceHolder.isEmpty()) {
return null;
}
return optionalReferenceHolder.get();
}
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
if (plains != null) {
return plains;
}
Holder<Biome> vanilla = firstHolder(registry);
if (vanilla != null) {
return vanilla;
}
return firstHolder(customRegistry);
}
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
for (Biome biome : registry) {
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
if (optionalBiomeKey.isEmpty()) {
continue;
}
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
if (optionalHolder.isPresent()) {
return optionalHolder.get();
}
}
return null;
}
private static final class BiomeResolution {
private final IrisBiome irisBiome;
private final boolean underground;
private final int blockX;
private final int blockY;
private final int blockZ;
private final RNG rng;
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
this.irisBiome = irisBiome;
this.underground = underground;
this.blockX = blockX;
this.blockY = blockY;
this.blockZ = blockZ;
this.rng = rng;
}
}
}
@@ -1,619 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
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.core.registries.Registries;
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.Weighted;
import net.minecraft.util.random.WeightedList;
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 net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
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.blending.Blender;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
import org.bukkit.block.data.BlockData;
import org.spigotmc.SpigotWorldConfig;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
private final Engine engine;
private final CustomBiomeSource customBiomeSource;
private final int runtimeMinY;
private final int runtimeHeight;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile ReachableStructureCache reachableStructureCache;
private volatile StructureStepCache structureStepCache;
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
}
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator());
this.delegate = delegate;
this.engine = engine;
this.customBiomeSource = customBiomeSource;
ServerLevel level = ((CraftWorld) world).getHandle();
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
}
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
if (best != null) {
return Pair.of(best, bestHolder);
}
} catch (Throwable e) {
IrisLogging.reportError("Iris-placed structure lookup failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
}
if (!importedControl().active()) {
return null;
}
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
if (reachable == null || reachable.size() == 0) {
return null;
}
try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
IrisLogging.reportError("Vanilla structure locate failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
return null;
}
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeys(level);
IrisImportedStructureControl control = importedControl();
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
continue;
}
String key = id.toString();
IrisNativeStructureDecision decision = control.resolve(
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
continue;
}
kept.add(holder);
}
if (kept.size() == holders.size()) {
return holders;
}
return HolderSet.direct(kept);
}
private Set<String> reachableStructureKeys(ServerLevel level) {
IrisDimension dimension = engine.getDimension();
ReachableStructureCache cached = reachableStructureCache;
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
synchronized (this) {
cached = reachableStructureCache;
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
Set<String> reachable;
try {
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
} catch (Throwable error) {
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
+ "native locate is disabled until the next hotload.", error);
reachable = Set.of();
}
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
return reachable;
}
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(null);
}
@Override
public ChunkGenerator getDelegate() {
if (delegate instanceof CustomChunkGenerator chunkGenerator)
return chunkGenerator.getDelegate();
return delegate;
}
@Override
public int getMinY() {
return runtimeMinY;
}
@Override
public int getSeaLevel() {
return runtimeMinY + engine.getDimension().getFluidHeight();
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
try {
NativeStructurePostProcessor.applyVerticalShift(
start,
decision.yShift(),
access.getMinY(),
access.getMinY() + access.getHeight());
} catch (RuntimeException error) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
+ access.getPos().x() + "," + access.getPos().z()
+ " because its vertical bounds are invalid.", error);
continue;
}
}
}
private IrisImportedStructureControl importedControl() {
return engine.getDimension().getImportedStructures();
}
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
return delegate.createState(holderlookup, randomstate, i, conf);
}
@Override
public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess);
}
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
}
@Override
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
}
@Override
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
}
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
}
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype);
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = delegate.getMobsAt(
vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition);
if (resolvedSpawns != vanillaSpawns) {
return resolvedSpawns;
}
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype);
if (explicitSpawns.isEmpty()) {
return vanillaSpawns;
}
if (vanillaSpawns.isEmpty()) {
return explicitSpawns;
}
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
}
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) {
applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true);
}
@Override
public void addDebugScreenInfo(List<String> list, RandomState randomstate, BlockPos blockposition) {
delegate.addDebugScreenInfo(list, randomstate, blockposition);
}
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
if (importedControl().active()) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
}
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<List<Structure>> byStep = structuresByStep(registry);
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
try {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
if (!starts.isEmpty()) {
List<StructureStart> resolvedStarts = List.copyOf(starts);
placementGroups.add(new NativePlacementGroup(
structureId, decision, index, step, resolvedStarts));
for (StructureStart start : resolvedStarts) {
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, decision.clearVegetation()));
}
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
}
index++;
}
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
try {
for (StructureStart start : group.starts()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
}
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolveStiltBlock);
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
synchronized (this) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
List<List<Structure>> grouped = new ArrayList<>(steps);
for (int step = 0; step < steps; step++) {
grouped.add(new ArrayList<>());
}
for (Structure structure : registry) {
grouped.get(structure.step().ordinal()).add(structure);
}
for (int step = 0; step < steps; step++) {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
return resolved;
}
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
if (settings.getPalette() == null) {
return Blocks.COBBLESTONE.defaultBlockState();
}
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
return Blocks.COBBLESTONE.defaultBlockState();
}
if (blockData instanceof IrisCustomData customData) {
blockData = customData.getBase();
}
if (blockData instanceof CraftBlockData craftBlockData) {
return craftBlockData.getState();
}
return Blocks.COBBLESTONE.defaultBlockState();
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos cp = chunk.getPos();
int i = cp.getMinBlockX();
int j = cp.getMinBlockZ();
int minY = chunk.getMinY();
int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
}
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int wX = x + blockPos.getX();
int wZ = z + blockPos.getZ();
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
SET_HEIGHT.invoke(surface, x, z, terrainTop);
SET_HEIGHT.invoke(motion, x, z, terrainTop);
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
}
}
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
}
@Override
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
delegate.spawnOriginalMobs(regionlimitedworldaccess);
}
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
Set<EntityType<?>> explicitTypes = new HashSet<>();
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
explicitTypes.add(entry.value().type());
}
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
if (!explicitTypes.contains(entry.value().type())) {
entries.add(entry);
}
}
entries.addAll(explicitSpawns.unwrap());
return WeightedList.of(entries);
}
@Override
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
return delegate.getSpawnHeight(levelheightaccessor);
}
@Override
public int getGenDepth() {
return runtimeHeight;
}
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
}
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
else column[k] = Blocks.AIR.defaultBlockState();
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
}
@Override
public Optional<Identifier> getTypeNameForDataFixer() {
return delegate.getTypeNameForDataFixer();
}
@Override
public void validate() {
delegate.validate();
}
static {
Field biomeSource = null;
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
if (!field.getType().equals(BiomeSource.class))
continue;
biomeSource = field;
break;
}
if (biomeSource == null)
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
Method setHeight = null;
for (Method method : Heightmap.class.getDeclaredMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class))
continue;
setHeight = method;
break;
}
if (setHeight == null)
throw new RuntimeException("Could not find setHeight method in Heightmap!");
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
}
private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) {
try {
BIOME_SOURCE.set(generator, source);
if (generator instanceof CustomChunkGenerator custom)
BIOME_SOURCE.set(custom.getDelegate(), source);
return generator;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
int featureIndex, int step, List<StructureStart> starts) {
}
}
@@ -1,80 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class VanillaStructureBiomes {
private VanillaStructureBiomes() {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
}
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
? customBiomeSource.possibleStructureBiomes()
: source.possibleBiomes();
for (Holder<Biome> holder : possibleBiomes) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
Set<String> keys = new LinkedHashSet<>();
if (access == null || structureKey == null || structureKey.isEmpty()) {
return keys;
}
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
return keys;
}
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
Set<String> reachable = new LinkedHashSet<>();
if (level == null || source == null) {
return reachable;
}
Set<String> possible = possibleBiomeKeys(source);
if (possible.isEmpty()) {
return reachable;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
for (Holder<Biome> holder : entry.getValue().biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
reachable.add(entry.getKey().identifier().toString());
break;
}
}
}
return reachable;
}
}
@@ -1,71 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
assertTrue(filterStart > findStart);
String findMethod = source.substring(findStart, filterStart);
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = findMethod.indexOf("continue;", searchLimit);
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
assertTrue(irisLocate >= 0);
assertTrue(searchLimit > irisLocate);
assertTrue(limitSkip > searchLimit);
assertTrue(nativeFilter > limitSkip);
assertTrue(delegateLocate > nativeFilter);
assertTrue(monumentReject >= 0);
assertTrue(rejectContinue > monumentReject);
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
}
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(occupancyCheck > stiltPlacement);
assertFalse(source.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
}
@Test
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int method = source.indexOf("public static int applyVerticalShift");
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
assertTrue(method >= 0);
assertTrue(clamp > method);
assertTrue(pieceMove > clamp);
assertTrue(junctionMove > pieceMove);
assertTrue(boundsMove > junctionMove);
}
}
@@ -1,38 +0,0 @@
package art.arcane.iris.nativegen;
import net.minecraft.world.level.levelgen.GenerationStep;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
@Test
public void surfaceStructuresClearTreeColumnsAutomatically() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
}
@Test
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
}
@Test
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
}
-49
View File
@@ -1,49 +0,0 @@
String apiVersion = providers.gradleProperty('minecraftVersion').get()
def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed')
.get()
dependencies {
compileOnly(project(':core'))
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
compileOnly(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.paper.api)
testImplementation('junit:junit:4.13.2')
testImplementation(libs.paper.api)
testImplementation(libs.bstats)
testImplementation(libs.sentry)
testImplementation(project(':core'))
testImplementation(volmLibCoordinate) {
transitive = false
}
compileOnly(libs.placeholderApi)
}
tasks.named('processResources').configure {
def pluginProperties = [
name : rootProject.name,
version : rootProject.version,
apiVersion: apiVersion,
main : mainClass,
bootstrapper: bootstrapperClass,
]
inputs.properties(pluginProperties)
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
expand(pluginProperties)
}
}
tasks.named('jar', Jar).configure {
archiveBaseName.set('iris-bukkit-plugin')
}
tasks.named('test').configure {
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
}
File diff suppressed because it is too large Load Diff
@@ -1,30 +0,0 @@
package art.arcane.iris;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import java.io.IOException;
import java.nio.file.Path;
@SuppressWarnings("UnstableApiUsage")
public final class IrisBootstrap implements PluginBootstrap {
@Override
public void bootstrap(BootstrapContext context) {
ProvisionResult provisioned = provision(context);
Path datapackRoot = provisioned.datapackRoot();
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
}
private static ProvisionResult provision(BootstrapContext context) {
try {
return DefaultPackBootstrapProvisioner.provision(
context.getDataDirectory(),
message -> context.getLogger().info(message)
);
} catch (IOException e) {
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
}
}
}
@@ -1,35 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
final class BukkitNativeStructureLocatePolicy {
private static final String MONUMENT_KEY = "minecraft:monument";
private static final String UNAVAILABLE_MESSAGE = "Native monument locating is unavailable on Paper, Purpur, and Folia because a cold search can stall the server thread. The monument lookup was skipped; monument generation is unaffected.";
private BukkitNativeStructureLocatePolicy() {
}
static boolean isUnavailable(String structureKey) {
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
}
static String unavailableMessage() {
return UNAVAILABLE_MESSAGE;
}
}
@@ -1,72 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
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 art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.util.List;
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)")
public class CommandDatapack implements DirectorExecutor {
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"})
public void ingest(
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
boolean restart
) {
VolmitSender sender = sender();
sender.sendMessage(C.GRAY + "Starting datapack ingest...");
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
}
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"})
public void list() {
VolmitSender sender = sender();
KList<String> configured = DatapackIngestService.collectConfiguredImports();
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
sender.sendMessage(C.GREEN + "Configured datapack imports: " + C.WHITE + configured.size());
for (String url : configured) {
sender.sendMessage(C.GRAY + " - " + C.WHITE + url);
}
sender.sendMessage(C.GREEN + "Installed datapacks: " + C.WHITE + installed.size());
for (DatapackIngestService.Entry entry : installed) {
sender.sendMessage(C.GRAY + " - " + C.WHITE + entry.id + C.GRAY + " " + (entry.versionNumber == null ? "?" : entry.versionNumber));
}
if (configured.isEmpty()) {
sender.sendMessage(C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest.");
}
}
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"})
public void remove(
@Param(description = "The datapack id (folder name) shown by /iris datapack list")
String id
) {
DatapackIngestService.remove(sender(), id);
}
}
@@ -1,422 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import com.google.gson.JsonObject;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.runtime.ChunkClearer;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisPackBenchmarking;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.matter.IrisMatterContext;
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 art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
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.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 {
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
public void EngineStatus() {
Iris.service(IrisEngineSVC.class)
.engineStatus(sender());
}
@Director(description = "Send a test exception to sentry")
public void Sentry() {
Engine engine = engine();
Exception testException = new Exception("This is a test");
if (engine == null) {
Iris.reportError(testException);
return;
}
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
Iris.reportError(testException);
}
}
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", origin = DirectorOrigin.BOTH)
public void genhash(
@Param(description = "The world to hash", contextual = true)
World world,
@Param(description = "Radius in chunks around the center", defaultValue = "4")
int radius,
@Param(description = "Center chunk X", defaultValue = "0")
int centerX,
@Param(description = "Center chunk Z", defaultValue = "0")
int centerZ) {
if (world == null) {
sender().sendMessage(C.RED + "World is null.");
return;
}
VolmitSender sender = sender();
sender.sendMessage(C.GREEN + "genhash started: " + ((radius * 2 + 1) * (radius * 2 + 1)) + " chunks...");
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
}
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
long startMs = M.ms();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
long globalHash = 0L;
long solidBlocks = 0L;
Map<String, Long> histogram = new TreeMap<>();
JsonObject chunkHashes = new JsonObject();
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
org.bukkit.ChunkSnapshot snapshot;
try {
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
snapshot = loaded.getChunkSnapshot(false, false, false);
} catch (Throwable e) {
Iris.reportError(e);
sender.sendMessage(C.RED + "genhash failed at chunk " + rx + "," + rz + ": " + e.getMessage());
return;
}
long chunkHash = 0L;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int worldX = (rx << 4) + x;
int worldZ = (rz << 4) + z;
for (int y = minY; y < maxY; y++) {
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
^ ((long) worldZ * 0x165667B19E3779F9L);
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
chunkHash ^= blockHash;
if (material != org.bukkit.Material.AIR
&& material != org.bukkit.Material.CAVE_AIR
&& material != org.bukkit.Material.VOID_AIR) {
histogram.merge(material.name(), 1L, Long::sum);
solidBlocks++;
}
}
}
}
globalHash ^= chunkHash;
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
}
}
int side = radius * 2 + 1;
JsonObject result = new JsonObject();
result.addProperty("global", Long.toHexString(globalHash));
result.addProperty("chunks", side * side);
result.addProperty("solidBlocks", solidBlocks);
result.addProperty("minY", minY);
result.addProperty("maxY", maxY);
JsonObject hist = new JsonObject();
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
hist.addProperty(entry.getKey(), entry.getValue());
}
result.add("histogram", hist);
result.add("chunkHashes", chunkHashes);
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
try {
Files.writeString(out.toPath(), result.toString());
} catch (IOException e) {
Iris.reportError(e);
}
sender.sendMessage(C.GREEN + "genhash global=" + C.GOLD + Long.toHexString(globalHash)
+ C.GREEN + " chunks=" + (side * side) + " solid=" + solidBlocks
+ " in " + Form.duration((long) (M.ms() - startMs), 1));
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
}
private static long genHashMix(long z) {
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", contextual = true)
World world,
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
boolean confirm,
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
boolean freshDownload
) {
if (!confirm) {
sender().sendMessage(new String[]{
C.RED + "You should always make a backup before using this",
C.YELLOW + "Issues caused by this can be, but are not limited to:",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
C.YELLOW + " - Structures not spawning again when regenerating",
C.YELLOW + " - Caves not lining up",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
C.RED + "/iris developer update-world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
});
return;
}
File folder = world.getWorldFolder();
folder.mkdirs();
if (freshDownload) {
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
}
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack, folder);
}
@Director(description = "Test")
public void mantle(
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", defaultValue = "false")
boolean plate,
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", defaultValue = "21474836474")
String name
) throws Throwable {
Engine activeEngine = engine();
if (activeEngine == null) {
sender().sendMessage(C.RED + "Target an Iris world before reading a mantle dump.");
return;
}
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
if (plate) {
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
} catch (Throwable e) {
e.printStackTrace();
}
} else {
Matter.read(section);
}
}
if (!TectonicPlate.hasError()) {
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
}
}
@Director(description = "Test")
public void packBenchmark(
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
IrisDimension dimension,
@Param(description = "Radius in regions", defaultValue = "2048")
int radius,
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
boolean gui
) {
new IrisPackBenchmarking(dimension, radius, gui);
}
@Director(description = "Upgrade to another Minecraft version")
public void upgrade(
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
ServerConfigurator.installDataPacks(version.get(), false);
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
}
@Director(description = "test")
public void mca (
@Param(description = "The world folder to scan for .mca region files") String world) {
try {
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
for (File mca : McaFiles) {
MCAFile MCARegion = MCAUtil.read(mca);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", defaultValue = "0")
int radius
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world.");
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null.");
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "Delete started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ ". Clearing blocks to air.");
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
}
@Director(description = "Test", aliases = {"ip"})
public void network() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
Iris.info("Display Name: %s", ni.getDisplayName());
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
for (InetAddress ia : Collections.list(inetAddresses)) {
Iris.info("IP: %s", ia.getHostAddress());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// --- Regen ---
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", origin = DirectorOrigin.PLAYER, sync = true)
public void regen(
@Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5")
int radius
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "You must be in an Iris world to use regen.");
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
if (engine == null) {
sender().sendMessage(C.RED + "The engine access for this world is null. Generate nearby chunks first.");
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "Regen started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ ". Deleting and regenerating in place.");
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH)
public void goldenhash(
@Param(description = "The world to scan", contextual = true)
World world,
@Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8")
int radius,
@Param(name = "center-x", description = "Center chunk X", defaultValue = "0")
int centerX,
@Param(name = "center-z", description = "Center chunk Z", defaultValue = "0")
int centerZ,
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true")
boolean resetMantle,
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8")
int threads,
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false")
boolean deep
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
return;
}
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "Target must be an Iris world.");
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null.");
return;
}
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ " in buffers (world untouched).");
Iris.info("goldenhash start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
}
}
@@ -1,119 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
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.Desktop;
import java.awt.GraphicsEnvironment;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
public class CommandEdit implements DirectorExecutor {
private boolean noStudio() {
if (!sender().isPlayer()) {
sender().sendMessage(C.RED + "Players only!");
return true;
}
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world is open!");
return true;
}
if (!engine().isStudio()) {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
if (GraphicsEnvironment.isHeadless()) {
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
return true;
}
if (!Desktop.isDesktopSupported()) {
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
return true;
}
return false;
}
@Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
if (noStudio()) {
return;
}
try {
if (biome == null || biome.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(biome.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
if (noStudio()) {
return;
}
try {
if (region == null || region.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(region.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
if (noStudio()) {
return;
}
try {
if (dimension == null || dimension.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(dimension.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
}
@@ -1,284 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
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 art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import org.bukkit.util.StructureSearchResult;
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome")
public void biome(
@Param(description = "The biome to look for")
IrisBiome biome,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
}
@Director(description = "Find a region")
public void region(
@Param(description = "The region to look for")
IrisRegion region,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
}
@Director(description = "Find a point of interest.")
public void poi(
@Param(description = "The type of PoI to look for.")
String type,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", sync = true)
public void structure(
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
String structure
) {
VolmitSender commandSender = sender();
if (commandSender == null) {
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
return;
}
Engine e = engine();
if (e == null) {
commandSender.sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
if (IrisStructureLocator.isPlaced(e, structure)) {
locateIrisStructure(e, structure, commandSender);
return;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
return;
}
Player target = player();
if (target == null) {
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
return;
}
World targetWorld = target.getWorld();
Location origin = target.getLocation();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.s(() -> {
try {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
Structure match = null;
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structure)) {
match = candidate;
break;
}
}
if (match == null) {
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
return;
}
if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
sendStructureMessage(target, commandSender,
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
if (result == null || result.getLocation() == null) {
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
return;
}
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structure + "'.", t);
}
});
}
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
Player target = player();
if (target == null) {
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
return;
}
World targetWorld = target.getWorld();
Location origin = target.getLocation();
int blockX = origin.getBlockX();
int blockZ = origin.getBlockZ();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
return;
}
if (!result.found()) {
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
return;
}
Location destination = new Location(
targetWorld, result.originX(), result.baseY(), result.originZ());
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
} catch (Throwable t) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
}
});
}
@Director(description = "Find an object")
public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
String object,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
Player studioPlayer = player();
if (studioPlayer != null) {
try {
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
return;
}
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (e.hasObjectPlacement(object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
}
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
}
});
}
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
try {
int y = useLocatedY
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
J.runEntity(target, () -> {
BukkitPlatform.teleportAsync(target, destination);
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ "
+ at.getBlockX() + ", " + y + ", " + at.getBlockZ());
});
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
}
}
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
J.runEntity(target, () -> commandSender.sendMessage(message));
}
}
@@ -1,642 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
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.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
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.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import static art.arcane.iris.core.service.EditSVC.deletingWorld;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
public class CommandIris implements DirectorExecutor {
private CommandStudio studio;
private CommandPregen pregen;
private CommandObject object;
private CommandStructure structure;
private CommandWhat what;
private CommandEdit edit;
private CommandDeveloper developer;
private CommandPack pack;
private CommandFind find;
private CommandDatapack datapack;
public static boolean worldCreation = false;
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
String WorldEngine;
String worldNameToCheck = "YourWorldName";
VolmitSender sender = Iris.getSender();
@Director(description = "Create a new world", aliases = {"c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create")
String name,
@Param(
aliases = {"dimension", "pack"},
description = "The dimension/pack to create the world with",
defaultValue = "default",
customHandler = PackDimensionTypeHandler.class
)
String type,
@Param(description = "The seed to generate the world with", defaultValue = "1337")
long seed,
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
boolean main
) {
if (name.equalsIgnoreCase("iris")) {
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (name.equalsIgnoreCase("benchmark")) {
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (IrisWorldStorage.dimensionRoot(name).exists()) {
sender().sendMessage(C.RED + "That folder already exists!");
return;
}
String resolvedType = type.equalsIgnoreCase("default")
? IrisSettings.get().getGenerator().getDefaultWorldType()
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
sender().sendMessage(C.YELLOW + "Or download manually: /iris download " + resolvedType);
return;
}
if (J.isFolia()) {
if (stageFoliaWorldCreation(name, dimension, seed, main)) {
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
}
return;
}
try {
worldCreation = true;
IrisToolbelt.createWorld()
.dimension(resolvedType)
.name(name)
.seed(seed)
.sender(sender())
.studio(false)
.create();
if (main) {
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
return new Thread(() -> updateMainWorld(name));
}));
}
} catch (Throwable e) {
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
worldCreation = false;
return;
}
worldCreation = false;
sender().sendMessage(C.GREEN + "Successfully created your world!");
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
}
private boolean updateMainWorld(String newName) {
try {
File oldLevelRoot = IrisWorldStorage.levelRoot();
File worldContainer = oldLevelRoot.getParentFile();
Properties data = ServerProperties.DATA;
try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
data.load(in);
}
File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromLegacyName(newName));
if (!sourceDimensionRoot.isDirectory()) {
throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath());
}
File newLevelRoot = new File(worldContainer, newName);
if (!newLevelRoot.exists() && !newLevelRoot.mkdirs()) {
throw new IllegalStateException("Could not create target level folder: " + newLevelRoot.getAbsolutePath());
}
for (String sub : List.of("data", "datapacks", "players")) {
File source = new File(oldLevelRoot, sub);
if (!source.exists()) {
continue;
}
IO.copyDirectory(source.toPath(), new File(newLevelRoot, sub).toPath());
}
File targetDimensionRoot = IrisWorldStorage.dimensionRoot(newLevelRoot, NamespacedKey.minecraft("overworld"));
IO.copyDirectory(sourceDimensionRoot.toPath(), targetDimensionRoot.toPath());
World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(newName)).orElse(null);
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName);
if (sourceWorld == null && stagedSeed == null) {
throw new IllegalStateException("Cannot determine the promoted world's seed.");
}
long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed();
data.setProperty("level-name", newName);
data.setProperty("level-seed", Long.toString(promotedSeed));
try (FileOutputStream out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
data.store(out, null);
}
return true;
} catch (Throwable e) {
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
Iris.reportError(e);
return false;
}
}
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
File worldFolder = IrisWorldStorage.dimensionRoot(name);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
if (installed == null) {
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
return false;
}
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
return false;
}
if (main) {
if (updateMainWorld(name)) {
sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\".");
} else {
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
return false;
}
}
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
if (main) {
sender().sendMessage(C.GREEN + "This world is now configured as main for next restart.");
}
return true;
}
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
if (worlds == null) {
worlds = yml.createSection("worlds");
}
ConfigurationSection worldSection = worlds.getConfigurationSection(worldName);
if (worldSection == null) {
worldSection = worlds.createSection(worldName);
}
String generator = "Iris:" + dimension;
worldSection.set("generator", generator);
if (seed != null) {
worldSection.set("seed", seed);
}
try {
yml.save(BUKKIT_YML);
Iris.info("Registered \"" + worldName + "\" in bukkit.yml");
return true;
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage());
Iris.error("Failed to update bukkit.yml!");
Iris.reportError(e);
return false;
}
}
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
public void teleport(
@Param(description = "World to teleport to")
World world,
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
Player player
) {
if (player == null && sender().isPlayer())
player = sender().player();
final Player target = player;
if (target == null) {
sender().sendMessage(C.RED + "The specified player does not exist.");
return;
}
final Location spawn = world.getSpawnLocation();
final Runnable teleportTask = () -> {
BukkitPlatform.teleportAsync(target, spawn);
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
};
if (!J.runEntity(target, teleportTask)) {
teleportTask.run();
}
}
@Director(description = "Print version information")
public void version() {
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
}
/*
/todo
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Director(description = "Print world height information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()));
} else {
World mainWorld = getServer().getWorlds().get(0);
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
}
}
@Director(description = "Check access of all worlds.", aliases = {"accesslist"})
public void worlds() {
KList<World> IrisWorlds = new KList<>();
KList<World> BukkitWorlds = new KList<>();
for (World w : Bukkit.getServer().getWorlds()) {
try {
Engine engine = IrisToolbelt.access(w).getEngine();
if (engine != null) {
IrisWorlds.add(w);
}
} catch (Exception e) {
BukkitWorlds.add(w);
}
}
if (sender().isPlayer()) {
sender().sendMessage(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
}
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName());
}
} else {
Iris.info(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
Iris.info(C.IRIS + "- " +IrisWorld.getName());
}
Iris.info(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
}
}
}
@Director(description = "Remove an Iris world", aliases = {"rm"}, sync = true)
public void remove(
@Param(description = "The world to remove")
World world,
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
boolean delete
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
if (!IrisToolbelt.evacuate(world)) {
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
return;
}
if (!WorldLifecycleService.get().unload(world, false)) {
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
return;
}
try {
if (IrisToolbelt.removeWorld(world)) {
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
} else {
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
}
IrisToolbelt.evacuate(world, "Deleting world");
deletingWorld = true;
if (!delete) {
deletingWorld = false;
return;
}
VolmitSender sender = sender();
J.a(() -> {
int retries = 12;
if (deleteDirectory(world.getWorldFolder())) {
sender.sendMessage(C.GREEN + "Successfully removed world folder");
} else {
while(true){
if (deleteDirectory(world.getWorldFolder())){
sender.sendMessage(C.GREEN + "Successfully removed world folder");
break;
}
retries--;
if (retries == 0){
sender.sendMessage(C.RED + "Failed to remove world folder");
break;
}
J.sleep(3000);
}
}
deletingWorld = false;
});
}
public static boolean deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDirectory(children[i]);
if (!success) {
return false;
}
}
}
return dir.delete();
}
@Director(description = "Toggle debug")
public void debug() {
boolean to = !IrisSettings.get().getGeneral().isDebug();
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(C.GREEN + "Set debug to: " + to);
}
@Director(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
if (PackDownloader.isDefaultOverworld(pack)) {
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + " (beta release)" + (overwrite ? " overwriting" : ""));
Iris.service(StudioSVC.class).downloadDefaultOverworld(sender(), overwrite);
} else {
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
}
ServerConfigurator.installDataPacksIfChanged(true);
}
@Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER)
public void metrics() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world");
return;
}
sender().sendMessage(C.GREEN + "Sending metrics...");
engine().printMetrics(sender());
}
@Director(description = "Reload configuration file (this is also done automatically)")
public void reload() {
IrisSettings.invalidate();
IrisSettings.get();
sender().sendMessage(C.GREEN + "Hotloaded settings");
}
@Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true)
public void unloadWorld(
@Param(description = "The world to unload")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
try {
IrisToolbelt.evacuate(world);
boolean unloaded = WorldLifecycleService.get().unload(world, false);
if (unloaded) {
sender().sendMessage(C.GREEN + "World unloaded successfully.");
} else {
sender().sendMessage(C.RED + "Failed to unload the world.");
}
} catch (Exception e) {
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
}
}
@Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
public void loadWorld(
@Param(description = "The name of the world to load")
String world
) {
worldNameToCheck = world;
boolean worldExists = doesWorldExist(worldNameToCheck);
WorldEngine = world;
if (!worldExists) {
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
return;
}
File directory = new File(IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(world)), "dimensions");
String dimension = null;
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String fileName = file.getName();
if (fileName.endsWith(".json")) {
dimension = fileName.substring(0, fileName.length() - 5);
sender().sendMessage(C.BLUE + "Generator: " + dimension);
}
}
}
}
} else {
sender().sendMessage(C.GOLD + world + " is not an iris world.");
return;
}
if (dimension == null) {
sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + ".");
return;
}
sender().sendMessage(C.GREEN + "Loading world: " + world);
if (!registerWorldInBukkitYml(world, dimension, null)) {
return;
}
if (J.isFolia()) {
sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\".");
return;
}
Iris.instance.checkForBukkitWorlds(world::equals);
sender().sendMessage(C.GREEN + world + " loaded successfully.");
}
@Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true)
public void evacuate(
@Param(description = "Evacuate the world")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
IrisToolbelt.evacuate(world);
}
boolean doesWorldExist(String worldName) {
File worldDirectory = IrisWorldStorage.dimensionRoot(worldName);
return worldDirectory.exists() && worldDirectory.isDirectory();
}
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> options = new LinkedHashSet<>();
options.add("default");
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles();
if (packs != null) {
for (File pack : packs) {
if (pack == null || !pack.isDirectory()) {
continue;
}
options.add(pack.getName());
try {
IrisData data = IrisData.get(pack);
for (String key : data.getDimensionLoader().getPossibleKeys()) {
options.add(key);
options.add(pack.getName() + ":" + key);
}
} catch (Throwable ex) {
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
pack.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
}
return new KList<>(options);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.trim().isEmpty()) {
throw new DirectorParsingException("World type cannot be empty");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -1,259 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackResourceCleanup;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.File;
import java.util.List;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
public class CommandPack implements DirectorExecutor {
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"})
public void validate(
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
String pack
) {
VolmitSender s = sender();
File packsRoot = Iris.instance.getDataFolder("packs");
if (!packsRoot.isDirectory()) {
s.sendMessage(C.RED + "packs/ folder not found.");
return;
}
if (pack == null || pack.isBlank()) {
File[] dirs = packsRoot.listFiles(File::isDirectory);
if (dirs == null || dirs.length == 0) {
s.sendMessage(C.YELLOW + "No packs to validate.");
return;
}
int broken = 0;
for (File dir : dirs) {
PackValidationResult result = runValidate(s, dir);
if (result != null && !result.isLoadable()) {
broken++;
}
}
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
return;
}
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
if (target == null) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
runValidate(s, target);
}
@Director(description = "Preview or apply unused-resource cleanup", aliases = {"c"})
public void cleanup(
@Param(description = "The pack folder name to clean")
String pack,
@Param(description = "preview or apply", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
if (!result.success()) {
s.sendMessage(C.RED + result.error());
reportPaths(s, result.quarantinedPaths(), "still quarantined");
return;
}
if (!result.changed()) {
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Quarantined " + result.quarantinedPaths().size()
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
reportPaths(s, result.quarantinedPaths(), "quarantined");
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(C.RED + "Cleanup mode must be preview or apply.");
return;
}
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
if (!preview.success()) {
s.sendMessage(C.RED + preview.error());
return;
}
if (!preview.hasCandidates()) {
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
return;
}
s.sendMessage(C.YELLOW + "Cleanup preview for pack '" + pack + "': "
+ preview.candidatePaths().size() + " candidate(s). No files were changed.");
reportPaths(s, preview.candidatePaths(), "candidate");
s.sendMessage(C.GRAY + "Run /iris pack cleanup " + pack + " mode=apply to quarantine these candidates after a fresh scan.");
}
@Director(description = "Preview or apply restoration of the latest quarantine", aliases = {"r"})
public void restore(
@Param(description = "The pack folder name to restore")
String pack,
@Param(description = "preview or apply", defaultValue = "preview")
String mode
) {
VolmitSender s = sender();
File packFolder = findPack(s, pack);
if (packFolder == null) {
return;
}
if ("apply".equalsIgnoreCase(mode)) {
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
if (!result.conflicts().isEmpty()) {
s.sendMessage(C.RED + "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
reportPaths(s, result.conflicts(), "conflict");
return;
}
if (!result.success()) {
s.sendMessage(C.RED + result.error());
return;
}
if (!result.changed()) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Restored " + result.restoredPaths().size()
+ " file(s) from " + result.dumpPath() + ".");
reportPaths(s, result.restoredPaths(), "restored");
return;
}
if (!"preview".equalsIgnoreCase(mode)) {
s.sendMessage(C.RED + "Restore mode must be preview or apply.");
return;
}
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
if (!preview.success()) {
s.sendMessage(C.RED + preview.error());
return;
}
if (!preview.hasFiles()) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.YELLOW + "Restore preview for " + preview.dumpPath() + ": "
+ preview.filePaths().size() + " file(s). No files were changed.");
reportPaths(s, preview.filePaths(), "file");
if (!preview.conflicts().isEmpty()) {
s.sendMessage(C.RED + "Restore is blocked by " + preview.conflicts().size() + " existing destination(s).");
reportPaths(s, preview.conflicts(), "conflict");
return;
}
s.sendMessage(C.GRAY + "Run /iris pack restore " + pack + " mode=apply to restore after a fresh conflict check.");
}
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
public void status(
@Param(description = "The pack folder name", defaultValue = "")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
return;
}
PackValidationRegistry.snapshot().forEach((name, result) -> {
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
s.sendMessage(tag + C.RESET + " " + name
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
+ ", warnings=" + result.getWarnings().size() + ")");
});
return;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
return;
}
reportResult(s, result);
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
return null;
}
}
private void reportResult(VolmitSender s, PackValidationResult result) {
if (result.isLoadable()) {
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
+ C.GRAY + " (warnings=" + result.getWarnings().size() + ")");
} else {
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
for (String reason : result.getBlockingErrors()) {
s.sendMessage(C.RED + " - " + reason);
}
}
int wMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < wMax; i++) {
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
}
if (result.getWarnings().size() > wMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
}
}
private File findPack(VolmitSender sender, String pack) {
if (pack == null || pack.isBlank()) {
sender.sendMessage(C.RED + "You must specify a pack name.");
return null;
}
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
if (packFolder == null) {
sender.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return null;
}
return packFolder;
}
private void reportPaths(VolmitSender sender, List<String> paths, String label) {
int max = Math.min(10, paths.size());
for (int i = 0; i < max; i++) {
sender.sendMessage(C.GRAY + " - " + label + ": " + paths.get(i));
}
if (paths.size() > max) {
sender.sendMessage(C.GRAY + " ... and " + (paths.size() - max) + " more.");
}
}
}
@@ -1,123 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
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 art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import org.bukkit.World;
import org.bukkit.util.Vector;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
public class CommandPregen implements DirectorExecutor {
@Director(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center,
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
boolean gui,
@Param(name = "serial", description = "Generate only one chunk at a time", defaultValue = "false")
boolean serial
) {
if (radius <= 0) {
sender().sendMessage(C.RED + "Pregen radius must be greater than zero blocks.");
return;
}
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
sender().sendMessage(C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server.");
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PregenTask task = PregenTask
.builder()
.center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(gui)
.radiusX(radius)
.radiusZ(radius)
.build();
if (serial) {
IrisToolbelt.pregenerateSerial(task, world);
} else {
IrisToolbelt.pregenerate(task, world);
}
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Failed to start pregeneration. See console for details.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Director(description = "Stop the active pregeneration task", aliases = "x")
public void stop() {
if (PregeneratorJob.shutdownInstance()) {
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
sender().sendMessage(message);
Iris.info(message);
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"})
public void pause() {
if (PregeneratorJob.pauseResume()) {
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
}
}
@Director(description = "Show the active pregeneration status")
public void status() {
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
if (progress == null) {
sender().sendMessage(C.YELLOW + "No active pregeneration task.");
return;
}
String world = progress.worldName() == null ? "?" : progress.worldName();
sender().sendMessage(C.GREEN + "Pregen " + C.GOLD + world + C.GREEN + ": " + C.GOLD + Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks())
+ C.GREEN + " (" + C.GOLD + String.format("%.1f", progress.percent()) + "%" + C.GREEN + ")"
+ (progress.paused() ? C.YELLOW + " PAUSED" : ""));
sender().sendMessage(C.GREEN + "Speed: " + C.GOLD + Form.f((int) progress.chunksPerSecond()) + "/s" + C.GREEN
+ " ETA: " + C.GOLD + Form.duration(progress.eta(), 2) + C.GREEN
+ " Elapsed: " + C.GOLD + Form.duration(progress.elapsed(), 2) + C.GREEN
+ " Method: " + C.GOLD + progress.method()
+ (progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""));
}
}
@@ -1,352 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureCaptureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
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.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.generator.structure.Structure;
import java.io.File;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)")
public class CommandStructure implements DirectorExecutor {
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
public void list(
@Param(description = "The dimension whose pack to index", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
File file = StructureIndexService.write(data);
sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath());
}
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
public void importAll(
@Param(description = "The dimension whose pack to import into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
StructureCaptureImporter.Report captured = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed.");
sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect.");
}
@Director(name = "capture", description = "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import.", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
public void capture(
@Param(description = "The dimension whose pack to capture into", aliases = "dim")
IrisDimension dimension
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
}
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
public void verify(
@Param(description = "The dimension to verify", aliases = "dim")
IrisDimension dimension,
@Param(description = "Search radius in chunks around the origin (larger is much slower)", defaultValue = "48")
int radius
) {
World world = resolveIrisWorld(dimension);
if (world == null) {
sender().sendMessage(C.RED + "No loaded Iris world found for " + dimension.getLoadKey() + ". Join or create one first (the search runs against a live world).");
return;
}
boolean senderIsPlayer = sender() != null && sender().isPlayer();
Location center = senderIsPlayer && player().getWorld() == world
? player().getLocation()
: world.getSpawnLocation();
int searchRadius = Math.max(1, Math.min(radius, 1000));
PlatformChunkGenerator access = IrisToolbelt.access(world);
Engine engine = access == null ? null : access.getEngine();
if (engine == null) {
sender().sendMessage(C.RED + "The selected Iris world has no active generator engine.");
return;
}
KList<String> structureKeys = new KList<>();
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure structure : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(structure);
if (key != null) {
structureKeys.add(key.toString());
}
}
VolmitSender commandSender = sender();
Player target = senderIsPlayer ? player() : null;
commandSender.sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName()
+ C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ()
+ " within " + searchRadius + " chunks...");
int centerX = center.getBlockX();
int centerZ = center.getBlockZ();
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
}
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
int searchRadius, VolmitSender commandSender, Player target) {
KList<String> messages = new KList<>();
Set<String> reachable;
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
}
int located = 0;
int nativeEligible = 0;
int disabled = 0;
int unreachable = 0;
int unavailable = 0;
int searchLimited = 0;
int errors = 0;
for (String keyName : structureKeys) {
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
if (irisPlaced) {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
searchLimited++;
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
+ ": unable to complete the density search before its safety limit was reached");
continue;
}
if (!result.found()) {
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
continue;
}
located++;
messages.add(C.AQUA + "[iris] " + C.WHITE + keyName + C.GREEN + " @ "
+ result.originX() + "," + result.baseY() + "," + result.originZ());
} catch (Throwable error) {
errors++;
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
+ error.getClass().getSimpleName());
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
}
continue;
}
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
}
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
unreachable++;
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
continue;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
unavailable++;
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
continue;
}
nativeEligible++;
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
}
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
sendVerificationMessages(commandSender, target, messages);
}
private void sendVerificationMessages(VolmitSender commandSender, Player target,
KList<String> messages) {
Runnable send = () -> {
for (String message : messages) {
commandSender.sendMessage(message);
}
};
if (target != null) {
J.runEntity(target, send);
} else {
J.s(send);
}
}
private World resolveIrisWorld(IrisDimension dimension) {
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
return player().getWorld();
}
}
for (World w : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(w)) {
continue;
}
PlatformChunkGenerator gen = IrisToolbelt.access(w);
if (matchesDimension(gen, dimension.getLoadKey())) {
return w;
}
}
return null;
}
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
return dimensionKey != null
&& generator != null
&& generator.getEngine() != null
&& generator.getEngine().getDimension() != null
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
}
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
public void info(
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to inspect")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0);
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
return;
}
int minX = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (PlacedStructurePiece p : pieces) {
minX = Math.min(minX, p.getMinX());
minZ = Math.min(minZ, p.getMinZ());
maxX = Math.max(maxX, p.getMaxX());
maxZ = Math.max(maxZ, p.getMaxZ());
}
sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)");
}
@Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
public void place(
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
IrisDimension dimension,
@Param(description = "The iris structure key to place")
String structure
) {
IrisData data = dimension.getLoader();
if (data == null) {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
Location loc = player().getLocation();
StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces");
return;
}
Map<Block, BlockData> future = new HashMap<>();
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
for (PlacedStructurePiece p : pieces) {
IrisObjectPlacement config = new IrisObjectPlacement();
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
config.setRotation(p.getRotation());
config.getPlace().add(p.getObject().getLoadKey());
if (!s.getEdit().isEmpty()) {
config.setEdit(s.getEdit());
}
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
}
sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location.");
}
}
@@ -1,91 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider {
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
public static void install() {
GuiHost.set(new BukkitGuiHost());
}
@Override
public Engine findActiveEngine() {
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return null;
}
@Override
public void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
hotloadHooks.put(onHotload, listener);
Iris.instance.registerListener(listener);
}
@Override
public void unregisterHotloadHook(Runnable onHotload) {
Listener listener = hotloadHooks.remove(onHotload);
if (listener != null) {
Iris.instance.unregisterListener(listener);
}
}
@Override
public GuiOverlay overlayFor(Engine engine) {
return engine == null ? null : new BukkitVisionOverlay(engine);
}
private static final class HotloadListener implements Listener {
private final Runnable onHotload;
private HotloadListener(Runnable onHotload) {
this.onHotload = onHotload;
}
@EventHandler
public void on(IrisEngineHotloadEvent event) {
onHotload.run();
}
}
}
@@ -1,112 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
public BukkitVisionOverlay(Engine engine) {
this.engine = engine;
}
@Override
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(world)) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
entity.getHealth(), maxHealth));
}
sink.accept(markers);
});
}
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasPlatformWorld()) {
return;
}
J.s(() -> {
List<Player> players = BukkitWorldBinding.players(world);
if (players.isEmpty()) {
return;
}
Player player = players.get(0);
int xx = (int) worldX;
int zz = (int) worldZ;
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
});
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
}
@@ -1,122 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void refreshWorkspace(Engine engine) {
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
DatapackIngestService.refreshWorkspace(engine.getData());
}
@Override
public void reloadDatapacks(Engine engine) {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
}
@Override
public void fireHotloadEvent(Engine engine) {
IrisPlatforms.get().callEvent(new IrisEngineHotloadEvent(engine));
}
@Override
public void validateDimensionHotload(Engine engine, IrisDimension replacement) {
IrisDimensionRuntimeContract.requireHotloadCompatible(
"Bukkit Studio world '" + engine.getWorld().name() + "'",
engine.getDimension(),
replacement,
"iris");
}
@Override
public boolean isPregeneratorActive(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(world.identity());
}
@Override
public void shutdownPregenerator(Engine engine) {
PregeneratorJob.shutdownInstance();
}
@Override
public boolean shouldDisableChunkContextCache(Engine engine) {
IrisWorld world = engine.getWorld();
if (!J.isFolia() || world == null || !world.hasPlatformWorld()) {
return false;
}
boolean maintenanceActive = WorldMaintenance.isWorldMaintenanceActive(world.identity());
return EngineMode.shouldDisableContextCacheForMaintenance(maintenanceActive, isPregeneratorActive(engine));
}
@Override
public boolean shouldSkipMantleCleanup(Engine engine) {
IrisWorld world = engine.getWorld();
return world != null
&& WorldMaintenance.isWorldMaintenanceActive(world.identity())
&& !isPregeneratorActive(engine);
}
@Override
public boolean shouldSkipMantleMarkerRead(Engine engine, int chunkX, int chunkZ) {
IrisWorld irisWorld = engine.getWorld();
if (!J.isFolia() || irisWorld == null || !irisWorld.hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(irisWorld);
return world != null && J.isOwnedByCurrentRegion(world, chunkX, chunkZ);
}
@Override
public boolean shouldBypassMantleStages(Engine engine) {
if (!J.isFolia() || !engine.getWorld().hasPlatformWorld()) {
return false;
}
World world = BukkitWorldBinding.world(engine.getWorld());
return world != null && IrisToolbelt.isWorldMaintenanceBypassingMantleStages(world);
}
}
@@ -1,310 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorSystem;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
import art.arcane.volmlib.util.director.runtime.DirectorSender;
import art.arcane.volmlib.util.director.visual.DirectorVisualCommand;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
private static final String ROOT_COMMAND = "iris";
private static final String ROOT_PERMISSION = "iris.all";
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient AtomicCache<DirectorVisualCommand> helpCache = new AtomicCache<>();
@Override
public void onEnable() {
PluginCommand command = findBukkitCommand();
if (command != null) {
command.setExecutor(this);
command.setTabCompleter(this);
} else {
registerPaperCommand();
}
J.a(this::getDirector);
}
@Override
public void onDisable() {
}
public DirectorRuntimeEngine getDirector() {
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
new CommandIris(),
null,
buildDirectorContexts(),
this::dispatchDirector,
this,
DirectorSystem.handlers
));
}
private DirectorContextRegistry buildDirectorContexts() {
DirectorContextRegistry contexts = new DirectorContextRegistry();
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
registerContextHandler(contexts, entry.getKey(), entry.getValue());
}
return contexts;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
contexts.register((Class) type, (invocation, map) -> {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
}
return null;
});
}
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
if (mode == DirectorExecutionMode.SYNC) {
J.s(runnable);
} else {
runnable.run();
}
}
@Override
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
DirectorContext.touch(new VolmitSender(sender.sender()));
}
}
@Override
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
DirectorContext.remove();
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return List.of();
}
return tabCompleteRoot(sender, alias, args);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return false;
}
executeRoot(sender, label, args);
return true;
}
void executeRoot(CommandSender sender, String label, String[] args) {
if (!sender.hasPermission(ROOT_PERMISSION)) {
sender.sendMessage("You lack the Permission '" + ROOT_PERMISSION + "'");
return;
}
J.aBukkit(() -> executeCommand(sender, label, args));
}
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
List<String> suggestions = runDirectorTab(sender, alias, args);
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
}
return suggestions;
}
private PluginCommand findBukkitCommand() {
try {
return Iris.instance.getCommand(ROOT_COMMAND);
} catch (UnsupportedOperationException ignored) {
return null;
}
}
private void registerPaperCommand() {
try {
Class<?> registrarType = Class.forName(
"art.arcane.iris.core.service.PaperCommandRegistrar",
true,
getClass().getClassLoader()
);
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
register.invoke(null, Iris.instance, this);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new IllegalStateException("Paper command registration failed", cause);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Paper command registrar is unavailable", e);
} catch (LinkageError e) {
throw new IllegalStateException("Paper command APIs are unavailable", e);
}
}
private void executeCommand(CommandSender sender, String label, String[] args) {
if (sendHelpIfRequested(sender, args)) {
playSuccessSound(sender);
return;
}
DirectorExecutionResult result = runDirector(sender, label, args);
if (result.isSuccess()) {
playSuccessSound(sender);
return;
}
playFailureSound(sender);
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
new VolmitSender(sender).sendMessage(C.RED + "Unknown Iris Command");
}
}
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
Optional<DirectorVisualCommand.HelpRequest> request = DirectorVisualCommand.resolveHelp(getHelpRoot(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
VolmitSender volmitSender = new VolmitSender(sender);
volmitSender.sendDirectorHelp(request.get().command(), request.get().page());
return true;
}
private DirectorVisualCommand getHelpRoot() {
return helpCache.aquireNastyPrint(() -> DirectorVisualCommand.createRoot(getDirector()));
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
try {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
}
}
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
DirectorContext.touch(new VolmitSender(sender));
try {
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return List.of();
} finally {
DirectorContext.remove();
}
}
private void playFailureSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
}
}
private void playSuccessSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
}
}
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
try {
player.playSound(player.getLocation(), a, av, ap);
player.playSound(player.getLocation(), b, bv, bp);
} catch (Throwable e) {
if (SOUND_LOGGED.compareAndSet(false, true)) {
try {
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
} catch (Throwable ignored) {
}
}
}
}
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
@Override
public String getName() {
return sender.getName();
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
@Override
public void sendMessage(String message) {
if (message != null && !message.trim().isEmpty()) {
sender.sendMessage(message);
}
}
}
}
@@ -1,455 +0,0 @@
package art.arcane.iris.core.service;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.Looper;
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.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import lombok.Synchronized;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
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;
public class IrisEngineSVC implements IrisService {
private static final int TRIM_PERIOD = 2_000;
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final AtomicInteger tectonicPlates = new AtomicInteger();
private final AtomicInteger queuedTectonicPlates = new AtomicInteger();
private final AtomicInteger trimmerAlive = new AtomicInteger();
private final AtomicInteger unloaderAlive = new AtomicInteger();
private final AtomicInteger totalWorlds = new AtomicInteger();
private final AtomicDouble maxIdleDuration = new AtomicDouble();
private final AtomicDouble minIdleDuration = new AtomicDouble();
private final AtomicLong loadedChunks = new AtomicLong();
private final KMap<World, Registered> worlds = new KMap<>();
private ScheduledExecutorService service;
private Looper updateTicker;
@Override
public void onEnable() {
var settings = IrisSettings.get().getPerformance();
var engine = settings.getEngineSVC();
service = Executors.newScheduledThreadPool(0,
(engine.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(engine.getPriority()))
.name("Iris EngineSVC-", 0)
.factory());
tectonicLimit.set(settings.getTectonicPlateSize());
Bukkit.getWorlds().forEach(this::add);
setup();
}
@Override
public void onDisable() {
for (World world : worlds.keySet()) {
PlatformChunkGenerator gen = IrisToolbelt.access(world);
if (gen == null) continue;
try {
gen.close();
} catch (Throwable t) {
IrisLogging.reportError(t);
}
}
if (service != null) {
service.shutdown();
}
if (updateTicker != null) {
updateTicker.interrupt();
}
worlds.keySet().forEach(this::remove);
worlds.clear();
}
public int getQueuedTectonicPlateCount() {
return queuedTectonicPlates.get();
}
public double getAverageIdleDuration() {
double min = minIdleDuration.get();
double max = maxIdleDuration.get();
if (!Double.isFinite(min) || !Double.isFinite(max) || min < 0D || max < 0D) {
return 0D;
}
if (max < min) {
return max;
}
return (min + max) / 2D;
}
public double getBiomeCacheUsageRatio() {
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
if (preservation == null) {
return 0D;
}
double total = 0D;
int count = 0;
for (var cache : preservation.getCaches()) {
if (!(cache instanceof CachedStream2D<?>) && !(cache instanceof CachedDoubleStream2D)) {
continue;
}
double usage = cache.getUsage();
if (!Double.isFinite(usage)) {
continue;
}
total += Math.max(0D, Math.min(1D, usage));
count++;
}
if (count <= 0) {
return 0D;
}
return total / count;
}
public void engineStatus(VolmitSender sender) {
long[] sizes = new long[4];
long[] count = new long[4];
for (var cache : IrisServices.get(PreservationSVC.class).getCaches()) {
var type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
count[type]++;
}
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
sender.sendMessage(C.DARK_PURPLE + "Status:");
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (service.isShutdown() ? "Shutdown" : "Running"));
sender.sendMessage(C.DARK_PURPLE + "- Updater: " + C.LIGHT_PURPLE + (updateTicker.isAlive() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Period: " + C.LIGHT_PURPLE + Form.duration(TRIM_PERIOD));
sender.sendMessage(C.DARK_PURPLE + "- Trimmers: " + C.LIGHT_PURPLE + trimmerAlive.get());
sender.sendMessage(C.DARK_PURPLE + "- Unloaders: " + C.LIGHT_PURPLE + unloaderAlive.get());
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
sender.sendMessage(C.DARK_PURPLE + "- Limit: " + C.LIGHT_PURPLE + tectonicLimit.get());
sender.sendMessage(C.DARK_PURPLE + "- Total: " + C.LIGHT_PURPLE + tectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + queuedTectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(maxIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(minIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "Caches:");
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + sizes[0] + " (" + count[0] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + sizes[1] + " (" + count[1] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + sizes[2] + " (" + count[2] + ")");
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + sizes[3] + " (" + count[3] + ")");
sender.sendMessage(C.DARK_PURPLE + "Other:");
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + totalWorlds.get());
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + loadedChunks.get());
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
}
@EventHandler
public void onWorldUnload(WorldUnloadEvent event) {
remove(event.getWorld());
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
add(event.getWorld());
}
private void remove(World world) {
var entry = worlds.remove(world);
if (entry == null) return;
entry.close();
}
private void add(World world) {
if (world == null || worlds.containsKey(world)) return;
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null) return;
worlds.put(world, new Registered(world.getName(), access));
}
private synchronized void setup() {
if (updateTicker != null && updateTicker.isAlive())
return;
updateTicker = new Looper() {
@Override
protected long loop() {
try {
for (World world : Bukkit.getWorlds()) {
add(world);
}
int queuedPlates = 0;
int totalPlates = 0;
long chunks = 0;
int unloaders = 0;
int trimmers = 0;
int iris = 0;
double maxDuration = Long.MIN_VALUE;
double minDuration = Long.MAX_VALUE;
for (var entry : worlds.entrySet()) {
var registered = entry.getValue();
if (registered.closed) continue;
iris++;
if (registered.unloaderAlive()) unloaders++;
if (registered.trimmerAlive()) trimmers++;
var engine = registered.getEngine();
if (engine == null) continue;
queuedPlates += engine.getMantle().getUnloadRegionCount();
totalPlates += engine.getMantle().getLoadedRegionCount();
chunks += entry.getKey().getLoadedChunks().length;
double duration = engine.getMantle().getAdjustedIdleDuration();
if (duration > maxDuration) maxDuration = duration;
if (duration < minDuration) minDuration = duration;
}
trimmerAlive.set(trimmers);
unloaderAlive.set(unloaders);
tectonicPlates.set(totalPlates);
queuedTectonicPlates.set(queuedPlates);
maxIdleDuration.set(maxDuration);
minIdleDuration.set(minDuration);
loadedChunks.set(chunks);
totalWorlds.set(iris);
worlds.values().forEach(Registered::update);
} catch (Throwable e) {
e.printStackTrace();
}
return 1000;
}
};
updateTicker.start();
}
private static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
static boolean shouldSkipMantleReductionForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
return maintenanceActive && !pregeneratorTargetsWorld;
}
private final class Registered {
private final String name;
private final PlatformChunkGenerator access;
private final int offset = RNG.r.nextInt(TRIM_PERIOD);
private transient ScheduledFuture<?> trimmer;
private transient ScheduledFuture<?> unloader;
private transient boolean closed;
private Registered(String name, PlatformChunkGenerator access) {
this.name = name;
this.access = access;
update();
}
private boolean unloaderAlive() {
return unloader != null && !unloader.isDone() && !unloader.isCancelled();
}
private boolean trimmerAlive() {
return trimmer != null && !trimmer.isDone() && !trimmer.isCancelled();
}
@Synchronized
private void update() {
if (closed || service == null || service.isShutdown())
return;
if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) {
trimmer = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
if (GoldenHashScanner.isScanActive()) {
return;
}
try {
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
engine.getMantle().trim(0L, 0);
} else {
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
IrisLogging.reportError(e);
IrisLogging.error("EngineSVC: Failed to trim for " + name);
e.printStackTrace();
}
}, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
if (unloader == null || unloader.isDone() || unloader.isCancelled()) {
unloader = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = BukkitWorldBinding.world(engine.getWorld());
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
if (GoldenHashScanner.isScanActive()) {
return;
}
try {
long unloadStart = System.currentTimeMillis();
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine);
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
if (heapPressure && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
if (count > 0) {
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
IrisLogging.reportError(e);
IrisLogging.error("EngineSVC: Failed to unload for " + name);
e.printStackTrace();
}
}, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
}
private int tectonicLimit() {
return tectonicLimit.get() / Math.max(worlds.size(), 1);
}
private boolean pregenTargets(Engine engine) {
if (engine == null || engine.getWorld() == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
}
private int activeTectonicLimit(Engine engine) {
int limit = tectonicLimit();
if (!pregenTargets(engine)) {
return limit;
}
return Math.max(limit, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
}
private long activeIdleDuration(Engine engine) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
@Synchronized
private void close() {
if (closed) return;
closed = true;
if (trimmer != null) {
trimmer.cancel(false);
trimmer = null;
}
if (unloader != null) {
unloader.cancel(false);
unloader = null;
}
}
@Nullable
private Engine getEngine() {
if (closed) return null;
return access.getEngine();
}
private boolean shouldReduce(Engine engine) {
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return true;
}
return pregenTargets(engine);
}
private boolean shouldSkipForMaintenance(@Nullable World world) {
if (world == null) {
return false;
}
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
if (!maintenanceActive) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(world));
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
}
@@ -1,210 +0,0 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1)
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"iris-engine-metrics"
);
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
@Override
public void onEnable() {
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
Iris.verbose("Integration provider registered for Iris");
}
@Override
public void onDisable() {
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
}
@Override
public String pluginId() {
return "iris";
}
@Override
public String pluginVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public Set<IntegrationProtocolVersion> supportedProtocols() {
return SUPPORTED_PROTOCOLS;
}
@Override
public Set<String> capabilities() {
return CAPABILITIES;
}
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
return IntegrationMetricSchema.descriptors().stream()
.filter(descriptor -> descriptor.key().startsWith("iris."))
.collect(java.util.stream.Collectors.toSet());
}
@Override
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
long now = System.currentTimeMillis();
if (request == null) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"missing request",
now
);
}
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
SUPPORTED_PROTOCOLS,
request.supportedProtocols()
);
if (negotiated.isEmpty()) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"no-common-protocol",
now
);
}
negotiatedProtocol = negotiated.get();
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiatedProtocol,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
now
);
}
@Override
public IntegrationHeartbeat heartbeat() {
long now = System.currentTimeMillis();
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: metricKeys;
long now = System.currentTimeMillis();
Map<String, IntegrationMetricSample> out = new HashMap<>();
for (String key : requested) {
switch (key) {
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
default -> out.put(key, IntegrationMetricSample.unavailable(
IntegrationMetricSchema.descriptor(key),
"unsupported-key",
now
));
}
}
return out;
}
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
double idle = engineService.getAverageIdleDuration();
if (idle > 0D && Double.isFinite(idle)) {
return IntegrationMetricSample.available(descriptor, idle, now);
}
}
return IntegrationMetricSample.available(descriptor, 0D, now);
}
private IntegrationMetricSample samplePregenQueueMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
long totalQueue = 0L;
boolean hasAnySource = false;
long pregenRemaining = PregeneratorJob.chunksRemaining();
if (pregenRemaining >= 0L) {
totalQueue += pregenRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
hasAnySource = true;
}
if (!hasAnySource) {
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
}
return IntegrationMetricSample.available(descriptor, totalQueue, now);
}
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService == null) {
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
}
double ratio = engineService.getBiomeCacheUsageRatio();
if (!Double.isFinite(ratio)) {
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
}
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
}
}
@@ -1,209 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisServerTransport;
import art.arcane.iris.core.protocol.IrisSession;
import art.arcane.iris.core.protocol.IrisSessionRegistry;
import art.arcane.iris.core.protocol.IrisVisionRequestService;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
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, WorldIdentity.serialize(world), "", 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)";
}
}
@@ -1,49 +0,0 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import org.bukkit.command.CommandSender;
import java.util.Collection;
import java.util.List;
final class PaperCommandRegistrar {
private static final String ROOT_COMMAND = "iris";
private PaperCommandRegistrar() {
}
static void register(Iris plugin, CommandSVC commandService) {
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
event.registrar().register(
ROOT_COMMAND,
"Iris world generation command.",
List.of("ir", "irs"),
command(commandService)
)
);
}
static BasicCommand command(CommandSVC commandService) {
return new PaperCommand(commandService);
}
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
@Override
public void execute(CommandSourceStack source, String[] args) {
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public Collection<String> suggest(CommandSourceStack source, String[] args) {
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
}
@Override
public boolean canUse(CommandSender sender) {
return true;
}
}
}
@@ -1,72 +0,0 @@
name: ${name}
version: ${version}
main: ${main}
bootstrapper: ${bootstrapper}
folia-supported: true
api-version: '${apiVersion}'
load: STARTUP
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
dependencies:
server:
PlaceholderAPI:
load: BEFORE
required: false
join-classpath: true
CraftEngine:
load: BEFORE
required: false
join-classpath: true
Nexo:
load: BEFORE
required: false
join-classpath: true
ItemsAdder:
load: BEFORE
required: false
join-classpath: true
SCore:
load: BEFORE
required: false
join-classpath: true
ExecutableItems:
load: BEFORE
required: false
join-classpath: false
MythicLib:
load: BEFORE
required: false
join-classpath: true
MMOItems:
load: BEFORE
required: false
join-classpath: true
eco:
load: BEFORE
required: false
join-classpath: true
EcoItems:
load: BEFORE
required: false
join-classpath: true
MythicMobs:
load: BEFORE
required: false
join-classpath: true
MythicCrucible:
load: BEFORE
required: false
join-classpath: true
KGenerators:
load: BEFORE
required: false
join-classpath: true
Multiverse-Core:
load: AFTER
required: false
join-classpath: true
WorldEdit:
load: BEFORE
required: false
join-classpath: true
@@ -1,28 +0,0 @@
name: ${name}
version: ${version}
main: ${main}
folia-supported: true
load: STARTUP
softdepend:
- PlaceholderAPI
- CraftEngine
- Nexo
- ItemsAdder
- SCore
- ExecutableItems
- MythicLib
- MMOItems
- eco
- EcoItems
- MythicMobs
- MythicCrucible
- KGenerators
- WorldEdit
loadbefore: [ Multiverse-Core ]
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
website: volmit.com
description: More than a Dimension!
commands:
iris:
aliases: [ ir, irs ]
api-version: '${apiVersion}'
@@ -1,86 +0,0 @@
package art.arcane.iris;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import art.arcane.iris.core.service.CommandSVC;
import art.arcane.iris.util.common.plugin.IrisService;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisDiagnosticsTest {
@Test
public void serviceInitializationSkipsPackageHelperClasses() throws Exception {
Class<?> registrar = Class.forName("art.arcane.iris.core.service.PaperCommandRegistrar");
assertFalse(Iris.isConcreteImplementation(registrar, IrisService.class));
assertTrue(Iris.isConcreteImplementation(CommandSVC.class, IrisService.class));
}
@Test
public void reportErrorWithContextPrintsFullStacktrace() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
try {
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
} finally {
System.setErr(originalErr);
}
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Runtime world creation failed."));
assertTrue(text.contains("IllegalStateException"));
assertTrue(text.contains("IllegalArgumentException"));
assertTrue(text.contains("inner"));
}
@Test
public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception {
Path root = Files.createTempDirectory("iris-splash");
try {
Path validPack = root.resolve("overworld");
Files.createDirectories(validPack.resolve("dimensions"));
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
Files.createDirectories(root.resolve("datapack-imports"));
Path brokenPack = root.resolve("broken");
Files.createDirectories(brokenPack.resolve("dimensions"));
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
List<IrisSplashPackScanner.SplashPackMetadata> packs;
try {
packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError);
} finally {
System.setErr(originalErr);
}
assertEquals(1, packs.size());
assertEquals("overworld", packs.get(0).name());
assertEquals("4000", packs.get(0).version());
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
assertTrue(text.contains("Json"));
} finally {
Files.walk(root)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}
@@ -1,97 +0,0 @@
package art.arcane.iris;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoadOrder;
import org.junit.Test;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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.assertNotNull;
import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest {
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
"PlaceholderAPI",
"CraftEngine",
"Nexo",
"ItemsAdder",
"SCore",
"ExecutableItems",
"MythicLib",
"MMOItems",
"eco",
"EcoItems",
"MythicMobs",
"MythicCrucible",
"KGenerators",
"Multiverse-Core",
"WorldEdit"
);
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
.toList();
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
.toList();
@Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
String metadata;
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/paper-plugin.yml")) {
assertNotNull(stream);
metadata = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
for (String pluginId : JOINED_PLUGIN_IDS) {
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "BEFORE", true)));
}
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
}
@Test
public void bukkitMetadataRetainsStartupCommandAndAliases() throws Exception {
PluginDescriptionFile metadata;
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/plugin.yml")) {
assertNotNull(stream);
metadata = new PluginDescriptionFile(stream);
}
assertEquals(Iris.class.getName(), metadata.getMain());
assertEquals(PluginLoadOrder.STARTUP, metadata.getLoad());
Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
}
@Test
public void processedResourcesContainBothMetadataFormats() throws Exception {
URL paperMetadata = PaperPluginMetadataTest.class.getResource("/paper-plugin.yml");
assertNotNull(paperMetadata);
assertEquals("file", paperMetadata.getProtocol());
Path bukkitMetadata = Path.of(paperMetadata.toURI()).resolveSibling("plugin.yml");
assertTrue(Files.isRegularFile(bukkitMetadata));
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
}
private static String optionalDependencyBlock(String pluginId, String loadOrder, boolean joinClasspath) {
return " " + pluginId + ":\n"
+ " load: " + loadOrder + "\n"
+ " required: false\n"
+ " join-classpath: " + joinClasspath + "\n";
}
}
@@ -1,47 +0,0 @@
package art.arcane.iris.core;
import art.arcane.iris.core.commands.CommandStudio;
import art.arcane.iris.core.tools.IrisCreator;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
public class StudioRuntimeCleanupTest {
@Test
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
assertFalse(found);
}
@Test
public void studioCommandNoLongerExposesProfilecache() {
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("profilecache"));
assertFalse(found);
}
@Test
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
.map(method -> method.getName().toLowerCase())
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
assertFalse(found);
}
@Test
public void noisemapPrebakePipelineClassIsRemoved() {
try {
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
} catch (ClassNotFoundException ignored) {
return;
}
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
}
}
@@ -1,32 +0,0 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitNativeStructureLocatePolicyTest {
@Test
public void nativeMonumentLocateIsUnavailable() {
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
}
@Test
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
}
@Test
public void rejectionExplainsSafetyAndGenerationBehavior() {
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
assertTrue(message.contains("unavailable"));
assertTrue(message.contains("stall the server thread"));
assertTrue(message.contains("generation is unaffected"));
}
}
@@ -1,34 +0,0 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisStructureLocateCommandContractTest {
@Test
public void findReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(source.contains("the density search safety limit was reached"));
assertTrue(source.contains("result.originX(), result.baseY(), result.originZ()"));
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
assertTrue(source.contains("[iris-search-limit]"));
assertTrue(source.contains("density searches safety-limited"));
assertTrue(source.contains("result.originX() + \",\" + result.baseY() + \",\" + result.originZ()"));
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
}
@@ -1,24 +0,0 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisEngineSVCTest {
@Test
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
}
@Test
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
}
@Test
public void noMaintenanceNeverSkipsReduction() {
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
}
}
@@ -1,115 +0,0 @@
package art.arcane.iris.core.service;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class PaperCommandRegistrarTest {
@Test
public void paperCommandDelegatesExecutionAndSuggestions() {
RecordingCommandService service = new RecordingCommandService();
BasicCommand command = PaperCommandRegistrar.command(service);
CommandSender sender = sender();
CommandSourceStack source = new TestCommandSourceStack(sender);
String[] arguments = {"create", "world"};
command.execute(source, arguments);
assertSame(sender, service.sender);
assertEquals("iris", service.label);
assertArrayEquals(arguments, service.arguments);
assertEquals(List.of("first", "second"), command.suggest(source, new String[]{"cr"}));
assertTrue(command.canUse(sender));
}
private static CommandSender sender() {
return (CommandSender) Proxy.newProxyInstance(
CommandSender.class.getClassLoader(),
new Class<?>[]{CommandSender.class},
(proxy, method, arguments) -> primitiveDefault(method.getReturnType())
);
}
private static Object primitiveDefault(Class<?> type) {
if (type == boolean.class) {
return false;
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0F;
}
if (type == double.class) {
return 0D;
}
if (type == char.class) {
return '\0';
}
return null;
}
private static final class RecordingCommandService extends CommandSVC {
private CommandSender sender;
private String label;
private String[] arguments;
@Override
void executeRoot(CommandSender sender, String label, String[] args) {
this.sender = sender;
this.label = label;
this.arguments = args;
}
@Override
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
return List.of("first", "second");
}
}
private record TestCommandSourceStack(CommandSender sender) implements CommandSourceStack {
@Override
public Location getLocation() {
return null;
}
@Override
public CommandSender getSender() {
return sender;
}
@Override
public Entity getExecutor() {
return null;
}
@Override
public CommandSourceStack withLocation(Location location) {
return this;
}
@Override
public CommandSourceStack withExecutor(Entity executor) {
return this;
}
}
}
@@ -1,6 +0,0 @@
package art.arcane.iris.client;
@FunctionalInterface
public interface ClientPacketSink {
void send(byte[] frame);
}
@@ -1,161 +0,0 @@
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();
}
}
}
@@ -1,57 +0,0 @@
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;
}
}
@@ -1,29 +0,0 @@
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;
}
}
@@ -1,14 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
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());
}
}
@@ -1,27 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientMarkers {
private final Map<IrisTileKey, List<IrisMessage.VisionMarkers.Marker>> byTile;
public IrisClientMarkers() {
this.byTile = new ConcurrentHashMap<>();
}
public void onMarkers(IrisMessage.VisionMarkers markers) {
byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers()));
}
public List<IrisMessage.VisionMarkers.Marker> forTile(IrisTileKey key) {
return byTile.get(key);
}
public void clear() {
byTile.clear();
}
}
@@ -1,45 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientPregenState {
private final ConcurrentHashMap<Long, IrisMessage.PregenProgress> jobs;
private volatile Long activeJobId;
public IrisClientPregenState() {
this.jobs = new ConcurrentHashMap<>();
this.activeJobId = null;
}
public void onProgress(IrisMessage.PregenProgress progress) {
jobs.put(progress.jobId(), progress);
activeJobId = progress.jobId();
}
public void onEnd(long jobId) {
jobs.remove(jobId);
Long current = activeJobId;
if (current != null && current == jobId) {
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
}
}
public IrisMessage.PregenProgress active() {
Long current = activeJobId;
if (current == null) {
return null;
}
return jobs.get(current);
}
public Long activeJobId() {
return activeJobId;
}
public void clear() {
jobs.clear();
activeJobId = null;
}
}
@@ -1,118 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisClientRegionMap {
private static final int MAX_REGIONS = 4096;
private final ConcurrentHashMap<Long, Integer> states;
private volatile boolean tracking;
private volatile long jobId;
private volatile Bounds bounds;
public IrisClientRegionMap() {
this.states = new ConcurrentHashMap<>();
this.tracking = false;
this.jobId = 0L;
this.bounds = null;
}
public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) {
if (activeJobId == null) {
return;
}
long active = activeJobId;
if (delta.jobId() != active) {
return;
}
if (!tracking || jobId != delta.jobId()) {
resetTo(delta.jobId());
}
record(delta.regionX(), delta.regionZ(), delta.state());
}
public void onEnd(long endedJobId) {
if (tracking && jobId == endedJobId) {
clear();
}
}
public boolean hasData() {
return tracking && !states.isEmpty();
}
public Bounds bounds() {
return bounds;
}
public void forEachCell(CellConsumer consumer) {
for (Map.Entry<Long, Integer> entry : states.entrySet()) {
long packed = entry.getKey();
consumer.accept(regionX(packed), regionZ(packed), entry.getValue());
}
}
public void clear() {
states.clear();
bounds = null;
tracking = false;
jobId = 0L;
}
private void resetTo(long newJobId) {
states.clear();
bounds = null;
jobId = newJobId;
tracking = true;
}
private void record(int regionX, int regionZ, int state) {
long packed = pack(regionX, regionZ);
if (!states.containsKey(packed)) {
if (states.size() >= MAX_REGIONS) {
return;
}
Bounds current = bounds;
bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ);
}
states.put(packed, state);
}
private static long pack(int regionX, int regionZ) {
return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL);
}
private static int regionX(long packed) {
return (int) (packed >> 32);
}
private static int regionZ(long packed) {
return (int) packed;
}
@FunctionalInterface
public interface CellConsumer {
void accept(int regionX, int regionZ, int state);
}
public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
public Bounds union(int regionX, int regionZ) {
return new Bounds(
Math.min(minRegionX, regionX),
Math.min(minRegionZ, regionZ),
Math.max(maxRegionX, regionX),
Math.max(maxRegionZ, regionZ));
}
public int regionsWide() {
return maxRegionX - minRegionX + 1;
}
public int regionsTall() {
return maxRegionZ - minRegionZ + 1;
}
}
}
@@ -1,77 +0,0 @@
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
}
}
@@ -1,113 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.LongSupplier;
public final class IrisClientTileCache {
private static final int MAX_CACHED_TILES = 256;
private static final long REQUEST_RETRY_MILLIS = 3000L;
private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND;
private final ClientPacketSink sink;
private final LongSupplier clock;
private final IrisTileAssembler assembler;
private final LinkedHashMap<IrisTileKey, IrisTileImage> cache;
private final Map<IrisTileKey, Long> pending;
private final Deque<IrisTileKey> queue;
private final Set<IrisTileKey> queued;
private long windowStartMillis;
private int sentInWindow;
public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) {
this.sink = sink;
this.clock = clock;
this.assembler = new IrisTileAssembler();
this.cache = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, IrisTileImage> eldest) {
return size() > MAX_CACHED_TILES;
}
};
this.pending = new HashMap<>();
this.queue = new ArrayDeque<>();
this.queued = new HashSet<>();
this.windowStartMillis = 0L;
this.sentInWindow = 0;
}
public synchronized void onVisionTile(IrisMessage.VisionTile tile) {
IrisTileImage image = assembler.add(tile);
if (image == null) {
return;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
cache.put(key, image);
pending.remove(key);
queued.remove(key);
}
public synchronized IrisTileImage get(IrisTileKey key) {
return cache.get(key);
}
public synchronized void resetRequestQueue() {
queue.clear();
queued.clear();
}
public synchronized void request(IrisTileKey key) {
if (cache.containsKey(key)) {
return;
}
long now = clock.getAsLong();
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
return;
}
if (queued.add(key)) {
queue.addLast(key);
}
}
public synchronized void pump() {
long now = clock.getAsLong();
if (now - windowStartMillis >= 1000L) {
windowStartMillis = now;
sentInWindow = 0;
}
while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) {
IrisTileKey key = queue.pollFirst();
queued.remove(key);
if (cache.containsKey(key)) {
continue;
}
Long lastRequest = pending.get(key);
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
continue;
}
sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom())));
pending.put(key, now);
sentInWindow++;
}
}
public synchronized void clear() {
assembler.clear();
cache.clear();
pending.clear();
queue.clear();
queued.clear();
sentInWindow = 0;
windowStartMillis = 0L;
}
}
@@ -1,75 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.ArrayDeque;
public final class IrisClientToasts {
private static final int MAX_PENDING = 4;
private final ArrayDeque<Pending> pending;
public IrisClientToasts() {
this.pending = new ArrayDeque<>();
}
public void enqueue(int kind, String title, String body) {
Pending entry = new Pending(kind, normalize(title), normalize(body));
synchronized (pending) {
while (pending.size() >= MAX_PENDING) {
pending.pollFirst();
}
pending.addLast(entry);
}
}
public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) {
int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS;
enqueue(kind, "Studio Hotload", hotloadBody(packKey, changedFiles, failed, message));
}
public Pending poll() {
synchronized (pending) {
return pending.pollFirst();
}
}
public void clear() {
synchronized (pending) {
pending.clear();
}
}
private static String normalize(String value) {
return value == null ? "" : value;
}
private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) {
StringBuilder builder = new StringBuilder();
String pack = normalize(packKey);
if (!pack.isEmpty()) {
builder.append(pack);
}
if (changedFiles != 0) {
append(builder, changedFiles + (changedFiles == 1 ? " file" : " files"));
}
String text = normalize(message);
if (!text.isEmpty()) {
append(builder, text);
}
if (builder.isEmpty()) {
return failed ? "reload failed" : "reloaded";
}
return builder.toString();
}
private static void append(StringBuilder builder, String part) {
if (!builder.isEmpty()) {
builder.append(" ");
}
builder.append(part);
}
public record Pending(int kind, String title, String body) {
}
}
@@ -1,153 +0,0 @@
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;
}
}
@@ -1,86 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import java.util.HashMap;
import java.util.Map;
final class IrisTileAssembler {
private final Map<IrisTileKey, Partial> partials;
IrisTileAssembler() {
this.partials = new HashMap<>();
}
IrisTileImage add(IrisMessage.VisionTile tile) {
int chunkCount = tile.chunkCount();
int chunkIndex = tile.chunkIndex();
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
return null;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
Partial partial = partials.get(key);
if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) {
if (partial != null && tile.sequence() < partial.sequence()) {
return null;
}
partial = new Partial(tile.sequence(), chunkCount);
partials.put(key, partial);
} else if (tile.sequence() < partial.sequence()) {
return null;
}
if (!partial.accept(chunkIndex, tile.data())) {
return null;
}
partials.remove(key);
return IrisTileCodec.decode(partial.concat());
}
void clear() {
partials.clear();
}
private static final class Partial {
private final int sequence;
private final int chunkCount;
private final byte[][] chunks;
private int received;
Partial(int sequence, int chunkCount) {
this.sequence = sequence;
this.chunkCount = chunkCount;
this.chunks = new byte[chunkCount][];
this.received = 0;
}
int sequence() {
return sequence;
}
int chunkCount() {
return chunkCount;
}
boolean accept(int chunkIndex, byte[] data) {
if (chunks[chunkIndex] == null) {
chunks[chunkIndex] = data;
received++;
}
return received >= chunkCount;
}
byte[] concat() {
int total = 0;
for (byte[] chunk : chunks) {
total += chunk.length;
}
byte[] blob = new byte[total];
int position = 0;
for (byte[] chunk : chunks) {
System.arraycopy(chunk, 0, blob, position, chunk.length);
position += chunk.length;
}
return blob;
}
}
}
@@ -1,99 +0,0 @@
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();
}
}
@@ -1,4 +0,0 @@
package art.arcane.iris.client;
public record IrisTileImage(int width, int height, int[] argb) {
}
@@ -1,4 +0,0 @@
package art.arcane.iris.client;
public record IrisTileKey(int tileX, int tileZ, int zoom) {
}
@@ -1,35 +0,0 @@
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;
};
}
}
@@ -1,328 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class IrisVisionScreen extends Screen {
private static final int TILE_PIXELS = 128;
private static final int MIN_ZOOM = 0;
private static final int MAX_ZOOM = 8;
private static final int DEFAULT_ZOOM = 2;
private static final int MAX_TEXTURES = 220;
private static final int BACKGROUND_COLOR = 0xF00B0E14;
private static final int PLACEHOLDER_COLOR = 0xFF161A22;
private static final int GRID_COLOR = 0x33FFFFFF;
private static final int HEADER_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFB9B9B9;
private static final int PLAYER_FILL_COLOR = 0xFFFFDD33;
private static final int PLAYER_BORDER_COLOR = 0xFF101010;
private static final int MARKER_COLOR = 0xFF4FC3F7;
private static final int MARKER_LABEL_BG = 0xE0101010;
private final Map<IrisTileKey, TileTexture> textures;
private double centerBlockX;
private double centerBlockZ;
private int zoom;
private boolean initialized;
private String renderedDimensionKey;
public IrisVisionScreen() {
super(Component.literal("Iris Vision"));
this.textures = new LinkedHashMap<>(64, 0.75f, true);
this.centerBlockX = 0.0D;
this.centerBlockZ = 0.0D;
this.zoom = DEFAULT_ZOOM;
this.initialized = false;
this.renderedDimensionKey = null;
}
@Override
protected void init() {
if (!initialized) {
centerOnPlayer();
initialized = true;
}
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
graphics.fill(0, 0, width, height, BACKGROUND_COLOR);
IrisClientSession session = IrisClient.session();
if (!session.isReady()) {
drawCentered(graphics, "Connecting to Iris server...", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", "not connected");
return;
}
IrisMessage.DimensionStatus status = IrisClient.dimension().status();
if (!IrisClient.visionAvailable() || status == null) {
drawCentered(graphics, "Not an Iris world", MUTED_COLOR);
drawHeader(graphics, "Iris Vision", status == null ? "no dimension data" : label(status));
return;
}
syncWorld(status);
renderTiles(graphics, mouseX, mouseY, status);
}
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
return true;
}
@Override
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
int blocksPerPixel = 1 << zoom;
centerBlockX -= dragX * blocksPerPixel;
centerBlockZ -= dragY * blocksPerPixel;
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (scrollY == 0.0D) {
return false;
}
int previousBlocksPerPixel = 1 << zoom;
double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel;
double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel;
int updated = zoom + (scrollY > 0.0D ? -1 : 1);
zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM));
int newBlocksPerPixel = 1 << zoom;
centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel;
centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel;
return true;
}
@Override
public void removed() {
releaseTextures();
super.removed();
}
private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) {
int blocksPerPixel = 1 << zoom;
int tileSpanBlocks = TILE_PIXELS * blocksPerPixel;
int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel);
int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel);
int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1;
int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1;
int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1;
int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1;
int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks);
int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks);
IrisClientTileCache cache = IrisClient.tiles();
cache.resetRequestQueue();
List<IrisTileKey> missing = new ArrayList<>();
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
int screenX = originX + tileX * TILE_PIXELS;
int screenY = originY + tileZ * TILE_PIXELS;
IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom);
IrisTileImage image = cache.get(key);
if (image == null) {
graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR);
missing.add(key);
continue;
}
Identifier texture = ensureTexture(key, image);
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS);
}
}
requestMissing(cache, missing, centerTileX, centerTileZ);
cache.pump();
evictTextures();
drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel);
drawPlayer(graphics, blocksPerPixel);
drawHeader(graphics, "Iris Vision", label(status) + " zoom " + zoom + " x" + (long) centerBlockX + " z" + (long) centerBlockZ);
drawFooter(graphics, "Drag to pan Scroll to zoom Esc to close");
}
private void requestMissing(IrisClientTileCache cache, List<IrisTileKey> missing, int centerTileX, int centerTileZ) {
missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ)));
for (IrisTileKey key : missing) {
cache.request(key);
}
}
private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) {
IrisClientMarkers markers = IrisClient.markers();
String hoverLabel = null;
int hoverX = 0;
int hoverY = 0;
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
List<IrisMessage.VisionMarkers.Marker> tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom));
if (tileMarkers == null) {
continue;
}
for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) {
int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel);
int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel);
graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR);
if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) {
hoverLabel = marker.label();
hoverX = screenX;
hoverY = screenY;
}
}
}
}
if (hoverLabel != null) {
int labelWidth = font.width(hoverLabel);
graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG);
graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR);
}
}
private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) {
LocalPlayer player = Minecraft.getInstance().player;
if (player == null) {
return;
}
int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel);
int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel);
graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR);
graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR);
}
private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) {
int lineHeight = font.lineHeight;
int headerHeight = lineHeight + 8;
graphics.fill(0, 0, width, headerHeight, HEADER_COLOR);
graphics.text(font, title, 8, 4, TITLE_COLOR);
graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR);
}
private void drawFooter(GuiGraphicsExtractor graphics, String hint) {
int lineHeight = font.lineHeight;
int footerTop = height - lineHeight - 8;
graphics.fill(0, footerTop, width, height, HEADER_COLOR);
graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR);
}
private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) {
graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color);
}
private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) {
TileTexture existing = textures.get(key);
if (existing != null) {
return existing.id();
}
int tileWidth = image.width();
int tileHeight = image.height();
int[] argb = image.argb();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false);
for (int y = 0; y < tileHeight; y++) {
int row = y * tileWidth;
for (int x = 0; x < tileWidth; x++) {
nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x]));
}
}
DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage);
Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key));
Minecraft.getInstance().getTextureManager().register(id, texture);
textures.put(key, new TileTexture(id, texture));
return id;
}
private void evictTextures() {
if (textures.size() <= MAX_TEXTURES) {
return;
}
TextureManager manager = Minecraft.getInstance().getTextureManager();
Iterator<Map.Entry<IrisTileKey, TileTexture>> iterator = textures.entrySet().iterator();
while (textures.size() > MAX_TEXTURES && iterator.hasNext()) {
Map.Entry<IrisTileKey, TileTexture> entry = iterator.next();
manager.release(entry.getValue().id());
iterator.remove();
}
}
private void releaseTextures() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft != null) {
TextureManager manager = minecraft.getTextureManager();
for (TileTexture texture : textures.values()) {
manager.release(texture.id());
}
}
textures.clear();
}
private void syncWorld(IrisMessage.DimensionStatus status) {
if (renderedDimensionKey == null) {
renderedDimensionKey = status.dimensionKey();
return;
}
if (!renderedDimensionKey.equals(status.dimensionKey())) {
renderedDimensionKey = status.dimensionKey();
releaseTextures();
centerOnPlayer();
}
}
private void centerOnPlayer() {
LocalPlayer player = Minecraft.getInstance().player;
if (player != null) {
centerBlockX = player.getBlockX();
centerBlockZ = player.getBlockZ();
}
}
private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) {
long deltaX = (long) key.tileX() - centerTileX;
long deltaZ = (long) key.tileZ() - centerTileZ;
return deltaX * deltaX + deltaZ * deltaZ;
}
private static int toAbgr(int argb) {
int alpha = argb >>> 24 & 0xFF;
int red = argb >> 16 & 0xFF;
int green = argb >> 8 & 0xFF;
int blue = argb & 0xFF;
return alpha << 24 | blue << 16 | green << 8 | red;
}
private static String texturePath(IrisTileKey key) {
return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom();
}
private static String safe(int value) {
return value < 0 ? "m" + (-(long) value) : Integer.toString(value);
}
private static String label(IrisMessage.DimensionStatus status) {
String pack = status.packKey() == null || status.packKey().isEmpty() ? "" : " pack " + status.packKey();
return status.dimensionKey() + pack;
}
private record TileTexture(Identifier id, DynamicTexture texture) {
}
}
@@ -1,85 +0,0 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.ArrayList;
import java.util.List;
public final class IrisWhatOverlay {
private static final int PANEL_COLOR = 0xC0101010;
private static final int TITLE_COLOR = 0xFF66BB6A;
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int MUTED_COLOR = 0xFFC7C7C7;
private static final int PADDING = 4;
private static final int ROW_GAP = 2;
private static final int CURSOR_OFFSET = 12;
private IrisWhatOverlay() {
}
public static void render(GuiGraphicsExtractor graphics) {
if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || minecraft.player == null) {
return;
}
LocalPlayer player = minecraft.player;
int blockX = player.getBlockX();
int blockZ = player.getBlockZ();
HitResult hit = minecraft.hitResult;
if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) {
BlockPos pos = blockHit.getBlockPos();
blockX = pos.getX();
blockZ = pos.getZ();
}
IrisClient.cursor().requestFor(blockX, blockZ);
IrisMessage.CursorInfo info = IrisClient.cursor().latest();
List<String> lines = new ArrayList<>();
if (info == null) {
lines.add("querying " + blockX + ", " + blockZ + "...");
} else {
lines.add("Biome: " + display(info.biomeKey()));
lines.add("Region: " + display(info.regionKey()));
if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) {
lines.add("Cave: " + display(info.caveBiomeKey()));
}
lines.add("Height: " + info.height() + " (" + info.blockX() + ", " + info.blockZ() + ")");
}
draw(graphics, minecraft.font, lines);
}
private static void draw(GuiGraphicsExtractor graphics, Font font, List<String> lines) {
int lineHeight = font.lineHeight;
int contentWidth = font.width("Iris What");
for (String line : lines) {
contentWidth = Math.max(contentWidth, font.width(line));
}
int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET;
int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET;
int rows = lines.size() + 1;
int contentHeight = lineHeight * rows + ROW_GAP * rows;
graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR);
int cursorY = originY;
graphics.text(font, "Iris What", originX, cursorY, TITLE_COLOR);
cursorY += lineHeight + ROW_GAP;
for (String line : lines) {
graphics.text(font, line, originX, cursorY, line.startsWith("Height") ? MUTED_COLOR : TEXT_COLOR);
cursorY += lineHeight + ROW_GAP;
}
}
private static String display(String key) {
return key == null || key.isEmpty() ? "-" : key;
}
}
-232
View File
@@ -1,232 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
id 'net.fabricmc.fabric-loom' version '1.17.14'
alias(libs.plugins.shadow)
}
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
group = 'art.arcane'
version = irisVersion
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../minecraft-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
}
}
test {
java {
srcDir '../modded-common/src/test/java'
}
}
}
repositories {
mavenCentral()
maven {
name = 'FabricMC'
url = uri('https://maven.fabricmc.net/')
}
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') }
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
}
configurations {
bundle {
canBeConsumed = false
canBeResolved = true
}
devBundle {
canBeConsumed = false
canBeResolved = true
}
}
configurations.named('bundle').configure {
exclude(group: 'com.google.code.gson')
exclude(group: 'com.google.guava')
exclude(group: 'commons-io')
exclude(group: 'org.apache.commons', module: 'commons-lang3')
exclude(group: 'it.unimi.dsi', module: 'fastutil')
exclude(group: 'org.slf4j')
exclude(group: 'org.apache.logging.log4j')
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
configurations.testCompileClasspath.extendsFrom(configurations.devBundle)
configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle)
configurations.create('jij') {
transitive = true
}
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2')
List<Object> fabricApi = [
libs.fabricApi.base,
libs.fabricApi.registrySync,
libs.fabricApi.resourceLoader,
libs.fabricApi.lifecycleEvents,
libs.fabricApi.commandApi,
libs.fabricApi.eventsInteraction,
libs.fabricApi.networking,
libs.fabricApi.rendering,
libs.fabricApi.keyMapping
]
fabricApi.each { Object notation ->
add('jij', notation)
add('implementation', notation)
}
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
}
add('bundle', volmLibCoordinate) {
transitive = false
}
add('devBundle', volmLibCoordinate) {
transitive = false
}
add('bundle', libs.zip) {
transitive = false
}
add('devBundle', libs.zip) {
transitive = false
}
List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
libs.paralithic,
libs.lru,
libs.caffeine,
libs.dom4j,
libs.jaxen,
libs.sentry
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
}
tasks.named('test').configure {
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
}
loom {
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
runs {
server {
String parity = providers.gradleProperty('irisParity').getOrNull()
if (parity != null) {
property('iris.parity', parity)
}
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
if (parityGolden != null) {
property('iris.parity.golden', parityGolden)
}
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
if (parityDeep != null) {
property('iris.parity.deep', parityDeep)
}
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
if (worldCheck != null) {
property('iris.worldcheck', worldCheck)
}
vmArg('-Xmx8G')
programArgs('nogui')
}
}
}
processResources {
inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion)
filesMatching('fabric.mod.json') {
expand('version': project.version, 'minecraftVersion': minecraftVersion)
}
}
tasks.named('shadowJar', ShadowJar).configure {
doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar"))
}
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
configurations = [project.configurations.named('bundle').get()]
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
from(project.configurations.named('jij')) {
into('META-INF/jars')
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
}
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
}
-5
View File
@@ -1,5 +0,0 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.configuration-cache=false
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-142
View File
@@ -1,142 +0,0 @@
[21:25:27] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[21:25:27] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
-95
View File
@@ -1,95 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
maven {
name = 'FabricMC'
url = uri('https://maven.fabricmc.net/')
}
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'iris-fabric'
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../gradle/libs.versions.toml'))
}
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
includeBuild('../..') {
dependencySubstitution {
substitute(module('art.arcane:core')).using(project(':core'))
substitute(module('art.arcane:spi')).using(project(':spi'))
}
}
@@ -1,48 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedForcedDatapack;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Set;
public final class FabricForcedDatapackSources {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricForcedDatapackSources() {
}
public static void attach(PackRepository repository) {
if (repository == null) {
return;
}
try {
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
repository.sources = merged;
} catch (Throwable e) {
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
}
}
}
@@ -1,73 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.server.MinecraftServer;
import java.io.File;
import java.nio.file.Path;
public final class FabricModdedLoader implements ModdedLoader {
@Override
public String platformName() {
return "fabric";
}
@Override
public String minecraftVersion() {
return FabricLoader.getInstance().getModContainer("minecraft")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public String modVersion() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
Object instance = FabricLoader.getInstance().getGameInstance();
return instance instanceof MinecraftServer server ? server : null;
}
@Override
public boolean clientEnvironment() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
}
@Override
public Path configDir() {
return FabricLoader.getInstance().getConfigDir();
}
@Override
public File modJar() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst())
.map((Path p) -> p.toFile())
.orElse(null);
}
}
@@ -1,54 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.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);
}
}
}
@@ -1,79 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
public final class IrisFabricBootstrap implements ModInitializer {
@Override
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_STARTED.register((MinecraftServer server) -> ModdedEngineBootstrap.serverStarted(server));
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
ServerLevelEvents.LOAD.register((MinecraftServer server, ServerLevel level) -> ModdedEngineBootstrap.levelLoaded(level));
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
PlayerBlockBreakEvents.BEFORE.register((Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity) -> {
if (level instanceof ServerLevel serverLevel) {
ModdedBlockBreakHandler.prepare(serverLevel, pos, state);
}
return true;
});
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> {
ModdedEngineBootstrap.tick(server);
ModdedWandService.serverTick(server);
});
}
}
@@ -1,50 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.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());
}
}
@@ -1,57 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.modded.ModdedBlockBreakHandler;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemInstance;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.ArrayList;
import java.util.List;
@Mixin(Block.class)
public class BlockMixin {
@Inject(method = "getDrops(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/item/ItemInstance;)Ljava/util/List;", at = @At("RETURN"), cancellable = true)
private static void iris$applyBlockDrops(BlockState state, ServerLevel level, BlockPos position, BlockEntity blockEntity,
Entity breaker, ItemInstance tool, CallbackInfoReturnable<List<ItemStack>> info) {
if (!(breaker instanceof Player)) {
return;
}
ModdedBlockBreakHandler.Result result = ModdedBlockBreakHandler.completePrepared(level, position);
if (result == null) {
return;
}
List<ItemStack> drops = result.replaceVanillaDrops()
? new ArrayList<>()
: new ArrayList<>(info.getReturnValue());
drops.addAll(result.drops());
info.setReturnValue(drops);
}
}
@@ -1,36 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.fabric.mixin;
import art.arcane.iris.fabric.FabricForcedDatapackSources;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerPacksSource.class)
public class ServerPacksSourceMixin {
@Inject(method = "createPackRepository(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/server/packs/repository/PackRepository;", at = @At("RETURN"))
private static void iris$addForcedDatapackSource(LevelStorageSource.LevelStorageAccess storage, CallbackInfoReturnable<PackRepository> info) {
FabricForcedDatapackSources.attach(info.getReturnValue());
}
}
@@ -1,36 +0,0 @@
{
"schemaVersion": 1,
"id": "irisworldgen",
"version": "${version}",
"name": "Iris",
"description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)",
"authors": ["Arcane Arts (Volmit Software)"],
"contact": {
"sources": "https://github.com/VolmitSoftware/Iris"
},
"license": "GPL-3.0",
"environment": "*",
"accessWidener": "irisworldgen.accesswidener",
"mixins": ["irisworldgen.mixins.json", "irisworldgen.entity.mixins.json"],
"entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
"client": ["art.arcane.iris.fabric.IrisFabricClient"]
},
"jars": [
{ "file": "META-INF/jars/fabric-api-base.jar" },
{ "file": "META-INF/jars/fabric-registry-sync-v0.jar" },
{ "file": "META-INF/jars/fabric-resource-loader-v1.jar" },
{ "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-rendering-v1.jar" },
{ "file": "META-INF/jars/fabric-transitive-access-wideners-v1.jar" },
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }
],
"depends": {
"fabricloader": ">=0.19.3",
"minecraft": "~${minecraftVersion}",
"java": ">=25"
}
}
@@ -1,6 +0,0 @@
accessWidener v2 official
accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map;
accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor;
accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;
accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
@@ -1,13 +0,0 @@
{
"required": true,
"minVersion": "0.8",
"package": "art.arcane.iris.fabric.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"BlockMixin",
"ServerPacksSourceMixin"
],
"injectors": {
"defaultRequire": 1
}
}
-252
View File
@@ -1,252 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
id 'net.minecraftforge.gradle' version '7.0.31'
alias(libs.plugins.shadow)
}
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:4c38988b344792a79f925f57f8a675bc85fc1bed'))
Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) {
return loaderVersion.substring(coordinatePrefix.length())
}
return loaderVersion
}
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
group = 'art.arcane'
version = irisVersion
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
sourceSets {
main {
java {
srcDir '../modded-common/src/main/java'
srcDir '../minecraft-common/src/main/java'
srcDir '../client-common/src/main/java'
}
resources {
srcDir '../modded-common/src/main/resources'
}
}
test {
java {
srcDir '../modded-common/src/test/java'
}
}
}
repositories {
minecraft.mavenizer(it)
maven fg.forgeMaven
maven fg.minecraftLibsMaven
mavenCentral()
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') }
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
}
configurations {
bundle {
canBeConsumed = false
canBeResolved = true
}
devBundle {
canBeConsumed = false
canBeResolved = true
}
runBundle {
canBeConsumed = false
canBeResolved = true
}
}
configurations.named('bundle').configure {
exclude(group: 'com.google.code.gson')
exclude(group: 'com.google.guava')
exclude(group: 'commons-io')
exclude(group: 'org.apache.commons', module: 'commons-lang3')
exclude(group: 'it.unimi.dsi', module: 'fastutil')
exclude(group: 'org.slf4j')
exclude(group: 'org.apache.logging.log4j')
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.named('runBundle').configure {
exclude(group: 'de.crazydev22.slimjar.helper')
exclude(group: 'de.crazydev22.slimjar')
exclude(group: 'io.github.slimjar')
}
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
configurations.testCompileClasspath.extendsFrom(configurations.compileClasspath, configurations.devBundle)
configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, configurations.runBundle)
['compileClasspath', 'runtimeClasspath', 'testCompileClasspath', 'testRuntimeClasspath'].each { String name ->
configurations.named(name).configure {
resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') {
selectHighestVersion()
}
}
}
dependencies {
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
testImplementation('junit:junit:4.13.2')
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
}
add('bundle', volmLibCoordinate) {
transitive = false
}
add('devBundle', volmLibCoordinate) {
transitive = false
}
add('runBundle', volmLibCoordinate) {
transitive = false
}
add('bundle', libs.zip) {
transitive = false
}
add('devBundle', libs.zip) {
transitive = false
}
add('runBundle', libs.zip) {
transitive = false
}
List<Object> shared = [
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
libs.paralithic,
libs.lru,
libs.caffeine,
libs.dom4j,
libs.jaxen,
libs.sentry
]
shared.each { Object notation ->
add('bundle', notation)
add('devBundle', notation)
add('runBundle', notation)
}
}
minecraft {
accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg'))
runs {
register('server') {
workingDir = layout.projectDirectory.dir('run')
String parity = providers.gradleProperty('irisParity').getOrNull()
if (parity != null) {
systemProperty('iris.parity', parity)
}
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
if (parityGolden != null) {
systemProperty('iris.parity.golden', parityGolden)
}
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
if (parityDeep != null) {
systemProperty('iris.parity.deep', parityDeep)
}
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
if (worldCheck != null) {
systemProperty('iris.worldcheck', worldCheck)
}
jvmArgs('-Xmx8G')
args('--mixin.config', 'irisworldgen.entity.mixins.json', '--nogui')
}
}
}
tasks.named('compileJava', JavaCompile).configure {
options.encoding = 'UTF-8'
options.release.set(25)
}
tasks.named('test').configure {
systemProperty('iris.moddedCommonSources', file('../modded-common/src/main/java').absolutePath)
}
processResources {
inputs.property('version', project.version)
inputs.property('minecraftVersion', minecraftVersion)
filesMatching('META-INF/mods.toml') {
expand('version': project.version, 'minecraftVersion': minecraftVersion)
}
}
tasks.named('shadowJar', ShadowJar).configure {
doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
}
manifest {
attributes('MixinConfigs': 'irisworldgen.entity.mixins.json')
}
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
configurations = [project.configurations.named('bundle').get()]
from(sourceSets.main.output)
mergeServiceFiles()
exclude('META-INF/maven/**')
exclude('META-INF/proguard/**')
exclude('META-INF/*.SF')
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
relocate('oshi', 'art.arcane.iris.shadow.oshi')
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
exclude('oshi.properties')
exclude('org/jspecify/**')
exclude('com/google/errorprone/**')
exclude('com/google/j2objc/**')
exclude('org/checkerframework/**')
exclude('org/jetbrains/annotations/**')
exclude('org/intellij/lang/**')
}
tasks.named('assemble').configure {
dependsOn(tasks.named('shadowJar'))
}
-6
View File
@@ -1,6 +0,0 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.configuration-cache=false
net.minecraftforge.gradle.merge-source-sets=true
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-145
View File
@@ -1,145 +0,0 @@
[13Jul2026 21:25:26.811] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
-142
View File
@@ -1,142 +0,0 @@
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
-95
View File
@@ -1,95 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
gradlePluginPortal()
maven {
name = 'MinecraftForge'
url = uri('https://maven.minecraftforge.net/')
}
mavenCentral()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'iris-forge'
dependencyResolutionManagement {
versionCatalogs {
libs {
from(files('../../gradle/libs.versions.toml'))
}
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
includeBuild('../..') {
dependencySubstitution {
substitute(module('art.arcane:core')).using(project(':core'))
substitute(module('art.arcane:spi')).using(project(':spi'))
}
}
@@ -1,31 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.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));
}
}
@@ -1,71 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.server.ServerLifecycleHooks;
import java.io.File;
import java.nio.file.Path;
public final class ForgeModdedLoader implements ModdedLoader {
@Override
public String platformName() {
return "forge";
}
@Override
public String minecraftVersion() {
return FMLLoader.versionInfo().mcVersion();
}
@Override
public String modVersion() {
return ModList.getModContainerById("irisworldgen")
.map((net.minecraftforge.fml.ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
return ServerLifecycleHooks.getCurrentServer();
}
@Override
public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient();
}
@Override
public Path configDir() {
return FMLPaths.CONFIGDIR.get();
}
@Override
public File modJar() {
IModFileInfo info = ModList.getModFileById("irisworldgen");
return info == null ? null : info.getFile().getFilePath().toFile();
}
}
@@ -1,86 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.forge;
import art.arcane.iris.modded.ModdedIrisPayload;
import art.arcane.iris.modded.ModdedProtocolChannel;
import art.arcane.iris.modded.ModdedProtocolHandler;
import art.arcane.iris.spi.protocol.IrisProtocol;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.event.network.CustomPayloadEvent;
import net.minecraftforge.network.Channel;
import net.minecraftforge.network.ChannelBuilder;
import net.minecraftforge.network.NetworkProtocol;
import net.minecraftforge.network.PacketDistributor;
public final class ForgeProtocolNetworking {
private static volatile Channel<CustomPacketPayload> channel;
private ForgeProtocolNetworking() {
}
public static void register() {
Channel<CustomPacketPayload> boundChannel = ChannelBuilder.named(Identifier.parse(IrisProtocol.CHANNEL))
.optional()
.payloadChannel()
.protocol(NetworkProtocol.PLAY)
.bidirectional()
.add(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, ForgeProtocolNetworking::onPayload)
.build();
channel = boundChannel;
ModdedProtocolHandler.bindChannel(new ForgeProtocolChannel(boundChannel));
}
public static Channel<CustomPacketPayload> channel() {
return channel;
}
private static void onPayload(ModdedIrisPayload payload, CustomPayloadEvent.Context context) {
context.setPacketHandled(true);
if (context.isClientSide()) {
ForgeClientProtocol.handleInbound(context, payload.data());
return;
}
ServerPlayer player = context.getSender();
if (player == null) {
return;
}
ModdedProtocolHandler.onInbound(player, payload.data());
}
private static final class ForgeProtocolChannel implements ModdedProtocolChannel {
private final Channel<CustomPacketPayload> channel;
private ForgeProtocolChannel(Channel<CustomPacketPayload> channel) {
this.channel = channel;
}
@Override
public boolean canReceive(ServerPlayer player) {
return true;
}
@Override
public void send(ServerPlayer player, ModdedIrisPayload payload) {
channel.send(payload, PacketDistributor.PLAYER.with(player));
}
}
}

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