This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-09 13:54:59 -04:00
1 parent d1fec88769
commit 0a7a531646
259 files changed
+11055 -884

No files matched your search

@@ -0,0 +1,178 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.core.pack.ContentKeyValidator.ContentKeyError;
import art.arcane.iris.core.pack.ContentKeyValidator.ContentRegistry;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ContentKeyValidatorTest {
private static PlatformRegistries registries() {
return new FakeRegistries(
List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:oak_log", "minecraft:grass_block"),
List.of("minecraft:diamond", "minecraft:wooden_pickaxe", "minecraft:stone_pickaxe"),
List.of("minecraft:zombie", "minecraft:creeper"));
}
@Test
public void validateFlagsUnknownBlockKeyWithSingleError() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("minecraft:stone", "minecraft:not_a_real_block"), List.of(), List.of());
assertEquals(1, errors.size());
ContentKeyError error = errors.get(0);
assertEquals("minecraft:not_a_real_block", error.key());
assertEquals(ContentRegistry.BLOCK, error.registry());
assertTrue(error.namespaceLoaded());
}
@Test
public void validateFlagsUnloadedNamespaceWhenNamespaceMissing() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("create:cogwheel"), List.of(), List.of());
assertEquals(1, errors.size());
assertFalse(errors.get(0).namespaceLoaded());
assertEquals("create", ContentKeyValidator.namespaceOf(errors.get(0).key()));
}
@Test
public void validateSuggestsNearestKeyForTypo() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("minecraft:cobblstone"), List.of(), List.of());
assertEquals(1, errors.size());
assertEquals("minecraft:cobblestone", errors.get(0).suggestion());
}
@Test
public void validateDedupsRepeatedKeyIntoOneError() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("minecraft:ghostblock", "minecraft:ghostblock", "ghostblock"), List.of(), List.of());
assertEquals(1, errors.size());
assertEquals("minecraft:ghostblock", errors.get(0).key());
}
@Test
public void validateDefaultsBareKeyToMinecraftNamespace() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("stone", "oak_log"), List.of(), List.of());
assertTrue(errors.isEmpty());
}
@Test
public void validateStripsBlockStateBeforeLookup() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of("minecraft:oak_log[axis=y]"), List.of(), List.of());
assertTrue(errors.isEmpty());
}
@Test
public void validateReportsUnknownItemAndEntityPerRegistry() {
List<ContentKeyError> errors = ContentKeyValidator.validate(registries(),
List.of(), List.of("minecraft:not_an_item"), List.of("minecraft:not_an_entity"));
assertEquals(2, errors.size());
assertTrue(errors.stream().anyMatch(e -> e.registry() == ContentRegistry.ITEM && e.key().equals("minecraft:not_an_item")));
assertTrue(errors.stream().anyMatch(e -> e.registry() == ContentRegistry.ENTITY && e.key().equals("minecraft:not_an_entity")));
}
@Test
public void validateReturnsEmptyWhenRegistriesNull() {
assertTrue(ContentKeyValidator.validate(null, List.of("minecraft:whatever"), List.of(), List.of()).isEmpty());
}
private record FakeRegistries(List<String> blocks, List<String> items, List<String> entities) implements PlatformRegistries {
@Override
public PlatformBlockState block(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key, boolean warn) {
return null;
}
@Override
public PlatformBlockState air() {
return null;
}
@Override
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
return null;
}
@Override
public PlatformBiome biome(String key) {
return null;
}
@Override
public PlatformItem item(String key) {
return null;
}
@Override
public PlatformEntityType entity(String key) {
return null;
}
@Override
public List<String> blockKeys() {
return blocks;
}
@Override
public List<String> biomeKeys() {
return List.of();
}
@Override
public List<String> structureKeys() {
return List.of();
}
@Override
public List<String> itemKeys() {
return items;
}
@Override
public List<String> entityKeys() {
return entities;
}
@Override
public List<String> blockTypeKeys() {
return List.of();
}
@Override
public List<String> enchantmentKeys() {
return List.of();
}
@Override
public List<String> potionEffectKeys() {
return List.of();
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
return Map.of();
}
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import org.junit.After;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenCacheExecutorRegistrationTest {
private static final class RecordingPreservation implements PreservationRegistry {
private final List<ExecutorService> executors = new CopyOnWriteArrayList<>();
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
executors.add(service);
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
@After
public void clearServices() {
IrisServices.clear();
}
@Test
public void dispatcherExecutorIsRegisteredForShutdown() throws Exception {
RecordingPreservation preservation = new RecordingPreservation();
IrisServices.register(PreservationRegistry.class, preservation);
File directory = Files.createTempDirectory("iris-pregen-exec-reg").toFile();
PregenCache.create(directory);
assertFalse("PregenCacheImpl must register its dispatcher executor for shutdown", preservation.executors.isEmpty());
for (ExecutorService service : preservation.executors) {
assertFalse("registered executor must be live so preservation can shut it down later", service.isShutdown());
}
}
}
@@ -0,0 +1,332 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class SchemaBuilderParityTest {
private static final List<String> POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
private static final List<String> ENCHANT_KEYS = List.of("minecraft:sharpness", "cool_mod:vorpal");
private static final List<String> ITEM_KEYS = List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
private static final List<String> ENTITY_KEYS = List.of("minecraft:zombie", "cool_mod:grizzly_bear");
private static final List<String> EXPECTED_POTIONS = List.of("SPEED", "SLOW_FALLING", "MEGA_BOOST");
private static final List<String> EXPECTED_ENCHANTS = List.of("sharpness", "vorpal");
private static final List<String> EXPECTED_ITEMS = List.of("stone", "diamond_sword", "cool_mod:ruby");
private static final List<String> EXPECTED_ENTITIES = List.of("minecraft:zombie", "cool_mod:grizzly_bear");
private static final String EXPECTED_COUNT_DESCRIPTION = "count\nThe count.\n\nInteger\n* Default Value is 5\n* Minimum allowed is 2\n* Maximum allowed is 9";
private static final String EXPECTED_COUNT_HTML_DESCRIPTION = "<h>count</h><br>The count.<hr></hr><br><h>Integer</h><br>* Default Value is 5<br>* Minimum allowed is 2<br>* Maximum allowed is 9";
private IrisPlatform previous;
@Before
public void bindFakePlatform() {
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
if (previous != null) {
IrisPlatforms.unbind();
}
IrisPlatforms.bind(new FakePlatform());
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
@Test
public void registryDependentEnumsMatchLegacyTransforms() {
JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions");
assertEquals(EXPECTED_POTIONS, enumValues(definitions, "enum-potion-effect-type"));
assertEquals(EXPECTED_ENCHANTS, enumValues(definitions, "enum-enchantment"));
assertEquals(EXPECTED_ITEMS, enumValues(definitions, "enum-item-type"));
assertEquals(EXPECTED_ENTITIES, enumValues(definitions, "enum-entity-type"));
}
@Test
public void registryIndependentPathsAreByteIdentical() {
JSONObject schema = new SchemaBuilder(IndependentModel.class, (IrisData) null).construct();
JSONObject count = schema.getJSONObject("properties").getJSONObject("count");
assertEquals("integer", count.getString("type"));
assertEquals(2, count.getInt("minimum"));
assertEquals(9, count.getInt("maximum"));
assertEquals(EXPECTED_COUNT_DESCRIPTION, count.getString("description"));
assertEquals(EXPECTED_COUNT_HTML_DESCRIPTION, count.getString("x-intellij-html-description"));
JSONObject flavor = schema.getJSONObject("properties").getJSONObject("flavor");
assertEquals("string", flavor.getString("type"));
assertEquals(List.of("ALPHA", "BETA"), enumValues(schema.getJSONObject("definitions"), flavorDefinitionKey()));
}
private static String flavorDefinitionKey() {
return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
}
private static List<String> enumValues(JSONObject definitions, String key) {
JSONArray array = definitions.getJSONObject(key).getJSONArray("enum");
List<String> values = new ArrayList<>(array.length());
for (int index = 0; index < array.length(); index++) {
values.add(array.getString(index));
}
return values;
}
@Desc("Registry dependent model.")
public static class RegistryModel {
@Desc("Potion field.")
@RegistryListPotionEffect
private String potion = "";
@Desc("Enchant field.")
@RegistryListEnchantment
private String enchant = "";
@Desc("Item field.")
@RegistryListItemType
private String item = "";
@Desc("Entity field.")
@RegistryListEntityType
private String entity = "";
}
@Desc("Independent model.")
public static class IndependentModel {
@Desc("The count.")
@MinNumber(2)
@MaxNumber(9)
private int count = 5;
@Desc("A flag.")
private boolean flag = false;
@Desc("A flavor.")
private Flavor flavor = Flavor.ALPHA;
}
public enum Flavor {
ALPHA,
BETA
}
private static final class FakeRegistries implements PlatformRegistries {
@Override
public PlatformBlockState block(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key, boolean warn) {
return null;
}
@Override
public PlatformBlockState air() {
return null;
}
@Override
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
return null;
}
@Override
public PlatformBiome biome(String key) {
return null;
}
@Override
public PlatformItem item(String key) {
return null;
}
@Override
public PlatformEntityType entity(String key) {
return null;
}
@Override
public List<String> blockKeys() {
return List.of();
}
@Override
public List<String> biomeKeys() {
return List.of();
}
@Override
public List<String> structureKeys() {
return List.of();
}
@Override
public List<String> itemKeys() {
return ITEM_KEYS;
}
@Override
public List<String> entityKeys() {
return ENTITY_KEYS;
}
@Override
public List<String> blockTypeKeys() {
return List.of();
}
@Override
public List<String> enchantmentKeys() {
return ENCHANT_KEYS;
}
@Override
public List<String> potionEffectKeys() {
return POTION_KEYS;
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
return Map.of();
}
}
private static final class FakePlatform implements IrisPlatform {
private final PlatformRegistries registries = new FakeRegistries();
@Override
public String platformName() {
return "fake";
}
@Override
public String minecraftVersion() {
return "0.0.0";
}
@Override
public PlatformRegistries registries() {
return registries;
}
@Override
public PlatformScheduler scheduler() {
return null;
}
@Override
public PlatformStructureHooks structureHooks() {
return null;
}
@Override
public PlatformBiomeWriter biomeWriter() {
return null;
}
@Override
public File dataFolder() {
return new File(".");
}
@Override
public File dataFile(String... path) {
return new File(".");
}
@Override
public File pluginJar() {
return new File(".");
}
@Override
public int irisVersionNumber() {
return 0;
}
@Override
public int minecraftVersionNumber() {
return 0;
}
@Override
public void callEvent(Object event) {
}
@Override
public void dispatchConsoleCommand(String command) {
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
return false;
}
@Override
public void log(LogLevel level, String message) {
}
@Override
public void msg(String message) {
}
@Override
public void reportError(Throwable error) {
}
}
}
@@ -0,0 +1,96 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.protocol.IrisMessage;
import org.junit.Test;
import java.lang.reflect.Proxy;
import static org.junit.Assert.assertEquals;
public class IrisCursorResolverTest {
@Test
public void resolvesBiomeRegionCaveHeightAndDimensionKeys() {
IrisBiome biome = new IrisBiome();
biome.setLoadKey("iris:plains");
IrisRegion region = new IrisRegion();
region.setLoadKey("iris:temperate");
IrisBiome cave = new IrisBiome();
cave.setLoadKey("iris:lush_cave");
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
Engine engine = engine(biome, region, cave, 72, dimension);
IrisMessage.CursorInfo info = IrisCursorResolver.resolve(engine, 100, -200);
assertEquals(100, info.blockX());
assertEquals(-200, info.blockZ());
assertEquals("iris:plains", info.biomeKey());
assertEquals("iris:temperate", info.regionKey());
assertEquals("iris:lush_cave", info.caveBiomeKey());
assertEquals(72, info.height());
assertEquals("overworld", info.dimensionKey());
}
@Test
public void missingCaveAndNullKeysCollapseToEmptyStrings() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
Engine engine = engine(null, null, null, 0, dimension);
IrisMessage.CursorInfo info = IrisCursorResolver.resolve(engine, 0, 0);
assertEquals("", info.biomeKey());
assertEquals("", info.regionKey());
assertEquals("", info.caveBiomeKey());
assertEquals("overworld", info.dimensionKey());
}
private static Engine engine(IrisBiome biome, IrisRegion region, IrisBiome cave, int height, IrisDimension dimension) {
return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class[]{Engine.class}, (proxy, method, args) -> switch (method.getName()) {
case "getSurfaceBiome" -> biome;
case "getRegion" -> region;
case "getCaveBiome" -> cave;
case "getHeight" -> height;
case "getDimension" -> dimension;
case "toString" -> "proxyEngine";
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
default -> defaultReturn(method.getReturnType());
});
}
private static Object defaultReturn(Class<?> returnType) {
if (returnType == int.class) {
return 0;
}
if (returnType == long.class) {
return 0L;
}
if (returnType == boolean.class) {
return false;
}
return null;
}
}
@@ -0,0 +1,327 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.spi.protocol.IrisWireReader;
import art.arcane.iris.spi.protocol.IrisWireWriter;
import art.arcane.iris.spi.protocol.ProtocolException;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisMessageCodecTest {
@Test
public void clientHelloRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.ClientHello original = new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void serverHelloRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.ServerHello original = new IrisMessage.ServerHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN, "Paper", true);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void pregenProgressRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.PregenProgress original = new IrisMessage.PregenProgress(42L, 128L, 4096L, 512.25D, 60000L, IrisMessage.PregenProgress.STATE_RUNNING);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void pregenEndRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.PregenEnd completed = new IrisMessage.PregenEnd(7L, true);
IrisMessage.PregenEnd aborted = new IrisMessage.PregenEnd(9L, false);
assertEquals(completed, IrisMessageCodec.decode(IrisMessageCodec.encode(completed)));
assertEquals(aborted, IrisMessageCodec.decode(IrisMessageCodec.encode(aborted)));
}
@Test
public void negativeAndExtremePrimitivesRoundTrip() throws ProtocolException {
IrisMessage.ClientHello hello = new IrisMessage.ClientHello(Integer.MIN_VALUE, Long.MIN_VALUE);
IrisMessage.PregenProgress progress = new IrisMessage.PregenProgress(Long.MAX_VALUE, Long.MIN_VALUE, -1L, -12.5D, -1L, Integer.MIN_VALUE);
assertEquals(hello, IrisMessageCodec.decode(IrisMessageCodec.encode(hello)));
assertEquals(progress, IrisMessageCodec.decode(IrisMessageCodec.encode(progress)));
}
@Test
public void emptyAndUnicodeStringsRoundTrip() throws ProtocolException {
IrisMessage.ServerHello empty = new IrisMessage.ServerHello(1, 0L, "", false);
IrisMessage.ServerHello unicode = new IrisMessage.ServerHello(1, 0L, "Paper🌍日本語", true);
assertEquals(empty, IrisMessageCodec.decode(IrisMessageCodec.encode(empty)));
assertEquals(unicode, IrisMessageCodec.decode(IrisMessageCodec.encode(unicode)));
}
@Test
public void varintRoundTripsBoundaryValues() throws ProtocolException {
int[] values = {0, 1, 127, 128, 16383, 16384, Integer.MAX_VALUE, -1, Integer.MIN_VALUE};
for (int value : values) {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(value);
IrisWireReader reader = new IrisWireReader(writer.toByteArray());
assertEquals(value, reader.readVarInt());
}
}
@Test
public void maxWidthVarintEncodesToFiveBytes() {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(-1);
assertEquals(5, writer.toByteArray().length);
}
@Test
public void everyPrefixOfAValidFrameRejectsCleanly() {
byte[][] frames = {
IrisMessageCodec.encode(new IrisMessage.ClientHello(1, IrisProtocol.CAPABILITY_PREGEN)),
IrisMessageCodec.encode(new IrisMessage.ServerHello(1, 0L, "Paper🌍", true)),
IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 2L, 3L, 4.5D, 6L, 1)),
IrisMessageCodec.encode(new IrisMessage.PregenEnd(1L, true))
};
for (byte[] frame : frames) {
for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) {
byte[] prefix = Arrays.copyOf(frame, prefixLength);
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix));
}
}
}
@Test
public void encodingBeyondFrameCapThrowsIllegalState() {
String oversizedBrand = "a".repeat(IrisProtocol.MAX_FRAME_BYTES);
IrisMessage.ServerHello oversized = new IrisMessage.ServerHello(1, 0L, oversizedBrand, true);
assertThrows(IllegalStateException.class, () -> IrisMessageCodec.encode(oversized));
}
@Test
public void decodingFrameBeyondCapRejects() {
byte[] oversized = new byte[IrisProtocol.MAX_FRAME_BYTES + 1];
oversized[0] = (byte) IrisProtocol.TYPE_CLIENT_HELLO;
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(oversized));
}
@Test
public void poisonedStringLengthRejectsWithoutAllocatingClaimedSize() {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(IrisProtocol.TYPE_SERVER_HELLO);
writer.writeInt(1);
writer.writeLong(0L);
writer.writeVarInt(Integer.MAX_VALUE);
byte[] poisoned = writer.toByteArray();
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned));
}
@Test
public void unknownMessageTypeDecodesToIgnorableNull() throws ProtocolException {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(9999);
writer.writeLong(123L);
assertNull(IrisMessageCodec.decode(writer.toByteArray()));
}
@Test
public void dimensionStatusRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.DimensionStatus original = new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true);
IrisMessage.DimensionStatus disabled = new IrisMessage.DimensionStatus("", "", Long.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, false);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
assertEquals(disabled, IrisMessageCodec.decode(IrisMessageCodec.encode(disabled)));
}
@Test
public void cursorInfoRequestRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.CursorInfoRequest original = new IrisMessage.CursorInfoRequest(1024, -2048);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void cursorInfoRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.CursorInfo original = new IrisMessage.CursorInfo(12, -34, "iris:plains", "iris:temperate", "", 72, "overworld");
IrisMessage.CursorInfo withCave = new IrisMessage.CursorInfo(Integer.MIN_VALUE, Integer.MAX_VALUE, "iris:desert", "iris:arid", "iris:lush_cave", -1, "minecraft:the_nether");
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
assertEquals(withCave, IrisMessageCodec.decode(IrisMessageCodec.encode(withCave)));
}
@Test
public void visionTileRequestRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.VisionTileRequest original = new IrisMessage.VisionTileRequest(-9, 15, 3);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void visionTileRoundTripsThroughCodec() throws ProtocolException {
byte[] payload = {1, 2, 3, 4, 5, -1, -2, 127, 0};
IrisMessage.VisionTile original = new IrisMessage.VisionTile(3, -4, 2, 7, 1, 5, payload);
IrisMessage.VisionTile decoded = (IrisMessage.VisionTile) IrisMessageCodec.decode(IrisMessageCodec.encode(original));
assertEquals(original.tileX(), decoded.tileX());
assertEquals(original.tileZ(), decoded.tileZ());
assertEquals(original.zoomLevel(), decoded.zoomLevel());
assertEquals(original.sequence(), decoded.sequence());
assertEquals(original.chunkIndex(), decoded.chunkIndex());
assertEquals(original.chunkCount(), decoded.chunkCount());
assertArrayEquals(payload, decoded.data());
}
@Test
public void visionTileEmptyDataRoundTrips() throws ProtocolException {
IrisMessage.VisionTile original = new IrisMessage.VisionTile(0, 0, 0, 0, 0, 1, new byte[0]);
IrisMessage.VisionTile decoded = (IrisMessage.VisionTile) IrisMessageCodec.decode(IrisMessageCodec.encode(original));
assertArrayEquals(new byte[0], decoded.data());
}
@Test
public void visionMarkersRoundTripsThroughCodec() throws ProtocolException {
List<IrisMessage.VisionMarkers.Marker> markers = List.of(
new IrisMessage.VisionMarkers.Marker(10, 20, 1, "spawn"),
new IrisMessage.VisionMarkers.Marker(-5, 7, 2, ""),
new IrisMessage.VisionMarkers.Marker(Integer.MIN_VALUE, Integer.MAX_VALUE, 3, "日本語"));
IrisMessage.VisionMarkers original = new IrisMessage.VisionMarkers(4, -8, 1, markers);
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void visionMarkersEmptyListRoundTrips() throws ProtocolException {
IrisMessage.VisionMarkers original = new IrisMessage.VisionMarkers(0, 0, 0, List.of());
assertEquals(original, IrisMessageCodec.decode(IrisMessageCodec.encode(original)));
}
@Test
public void everyPrefixOfANewFrameRejectsCleanly() {
byte[][] frames = {
IrisMessageCodec.encode(new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true)),
IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(100, -200)),
IrisMessageCodec.encode(new IrisMessage.CursorInfo(1, 2, "plains", "region", "", 72, "overworld")),
IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(3, 4, 2)),
IrisMessageCodec.encode(new IrisMessage.VisionTile(1, 2, 0, 0, 0, 1, new byte[]{9, 8, 7})),
IrisMessageCodec.encode(new IrisMessage.VisionMarkers(1, 2, 0, List.of(new IrisMessage.VisionMarkers.Marker(1, 2, 0, "a"))))
};
for (byte[] frame : frames) {
for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) {
byte[] prefix = Arrays.copyOf(frame, prefixLength);
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix));
}
}
}
@Test
public void visionTileChunkAtCapFitsUnderFrameCap() {
byte[] data = new byte[IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES];
IrisMessage.VisionTile tile = new IrisMessage.VisionTile(Integer.MAX_VALUE, Integer.MIN_VALUE, IrisProtocol.MAX_FRAME_BYTES, Integer.MAX_VALUE, 4095, 4096, data);
byte[] frame = IrisMessageCodec.encode(tile);
assertTrue("vision tile chunk frame must fit under cap", frame.length <= IrisProtocol.MAX_FRAME_BYTES);
assertTrue("header math must reserve room for the max chunk", IrisProtocol.VISION_TILE_HEADER_BYTES + 3 + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES <= IrisProtocol.MAX_FRAME_BYTES);
}
@Test
public void visionMarkersAtCapFitUnderFrameCap() {
List<IrisMessage.VisionMarkers.Marker> markers = new ArrayList<>(IrisProtocol.MAX_VISION_MARKERS);
for (int index = 0; index < IrisProtocol.MAX_VISION_MARKERS; index++) {
markers.add(new IrisMessage.VisionMarkers.Marker(index, -index, index & 7, "m" + index));
}
byte[] frame = IrisMessageCodec.encode(new IrisMessage.VisionMarkers(1, 2, 0, markers));
assertTrue("marker frame at cap must fit under cap", frame.length <= IrisProtocol.MAX_FRAME_BYTES);
}
@Test
public void poisonedMarkerCountRejectsWithoutOverread() {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(IrisProtocol.TYPE_VISION_MARKERS);
writer.writeInt(0);
writer.writeInt(0);
writer.writeInt(0);
writer.writeVarInt(Integer.MAX_VALUE);
byte[] poisoned = writer.toByteArray();
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned));
}
@Test
public void pregenRegionDeltaRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.PregenRegionDelta generating = new IrisMessage.PregenRegionDelta(42L, 3, -7, IrisMessage.PregenRegionDelta.STATE_GENERATING);
IrisMessage.PregenRegionDelta done = new IrisMessage.PregenRegionDelta(Long.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, IrisMessage.PregenRegionDelta.STATE_DONE);
assertEquals(generating, IrisMessageCodec.decode(IrisMessageCodec.encode(generating)));
assertEquals(done, IrisMessageCodec.decode(IrisMessageCodec.encode(done)));
}
@Test
public void studioHotloadRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.StudioHotload ok = new IrisMessage.StudioHotload("overworld", 12, false, "");
IrisMessage.StudioHotload failed = new IrisMessage.StudioHotload("iris:desert", Integer.MIN_VALUE, true, "IllegalStateException: bad pack🌍");
assertEquals(ok, IrisMessageCodec.decode(IrisMessageCodec.encode(ok)));
assertEquals(failed, IrisMessageCodec.decode(IrisMessageCodec.encode(failed)));
}
@Test
public void toastRoundTripsThroughCodec() throws ProtocolException {
IrisMessage.Toast info = new IrisMessage.Toast(IrisMessage.Toast.KIND_INFO, "Title", "Body");
IrisMessage.Toast error = new IrisMessage.Toast(IrisMessage.Toast.KIND_ERROR, "", "日本語");
assertEquals(info, IrisMessageCodec.decode(IrisMessageCodec.encode(info)));
assertEquals(error, IrisMessageCodec.decode(IrisMessageCodec.encode(error)));
}
@Test
public void everyPrefixOfAStudioComfortFrameRejectsCleanly() {
byte[][] frames = {
IrisMessageCodec.encode(new IrisMessage.PregenRegionDelta(9L, 4, -5, IrisMessage.PregenRegionDelta.STATE_GENERATING)),
IrisMessageCodec.encode(new IrisMessage.StudioHotload("overworld", 3, true, "boom")),
IrisMessageCodec.encode(new IrisMessage.Toast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld"))
};
for (byte[] frame : frames) {
for (int prefixLength = 0; prefixLength < frame.length; prefixLength++) {
byte[] prefix = Arrays.copyOf(frame, prefixLength);
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(prefix));
}
}
}
@Test
public void studioHotloadOversizePackKeyThrowsIllegalState() {
String oversized = "a".repeat(IrisProtocol.MAX_FRAME_BYTES);
IrisMessage.StudioHotload hotload = new IrisMessage.StudioHotload(oversized, 0, false, "");
assertThrows(IllegalStateException.class, () -> IrisMessageCodec.encode(hotload));
}
@Test
public void poisonedStudioHotloadPackKeyLengthRejectsWithoutAllocatingClaimedSize() {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(IrisProtocol.TYPE_STUDIO_HOTLOAD);
writer.writeVarInt(Integer.MAX_VALUE);
byte[] poisoned = writer.toByteArray();
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned));
}
@Test
public void poisonedToastTitleLengthRejectsWithoutAllocatingClaimedSize() {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(IrisProtocol.TYPE_TOAST);
writer.writeInt(IrisMessage.Toast.KIND_INFO);
writer.writeVarInt(Integer.MAX_VALUE);
byte[] poisoned = writer.toByteArray();
assertThrows(ProtocolException.class, () -> IrisMessageCodec.decode(poisoned));
}
}
@@ -0,0 +1,531 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import art.arcane.iris.spi.protocol.ProtocolException;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisProtocolServerTest {
private static final String BRAND = "TestBrand";
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN;
@Test
public void frameBeforeHelloIsDroppedAndCounted() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING)));
assertEquals(IrisSession.State.AWAITING_HELLO, session.state());
assertEquals(1L, server.droppedBeforeHelloCount());
assertEquals(0, transport.sent.size());
}
@Test
public void wrongProtocolVersionNeverReachesReady() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION + 999, IrisProtocol.CAPABILITY_PREGEN)));
assertEquals(IrisSession.State.AWAITING_HELLO, session.state());
assertEquals(1L, server.versionMismatchCount());
assertEquals(0, transport.sent.size());
}
@Test
public void matchingHelloReachesReadyAndAnswersServerHello() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
assertEquals(IrisSession.State.READY, session.state());
assertEquals(IrisProtocol.CAPABILITY_PREGEN, session.capabilities());
assertEquals(1, transport.sent.size());
IrisMessage.ServerHello answer = (IrisMessage.ServerHello) transport.sent.get(0);
assertEquals(IrisProtocol.PROTOCOL_VERSION, answer.protocolVersion());
assertEquals(SERVER_CAPABILITIES, answer.capabilities());
assertEquals(BRAND, answer.serverBrand());
assertTrue(answer.irisActive());
}
@Test
public void readySessionReceivesProgressAndEndBroadcasts() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
server.broadcastPregenProgress(5L, 10L, 100L, 3.0D, 2000L, IrisMessage.PregenProgress.STATE_PAUSED);
server.pregenEnd(5L, true);
assertEquals(3, transport.sent.size());
IrisMessage.PregenProgress progress = (IrisMessage.PregenProgress) transport.sent.get(1);
assertEquals(new IrisMessage.PregenProgress(5L, 10L, 100L, 3.0D, 2000L, IrisMessage.PregenProgress.STATE_PAUSED), progress);
assertEquals(new IrisMessage.PregenEnd(5L, true), transport.sent.get(2));
}
@Test
public void unregisteringStopsBroadcasts() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
int afterHandshake = transport.sent.size();
registry.unregister("s1");
server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING);
server.pregenEnd(1L, true);
assertEquals(afterHandshake, transport.sent.size());
assertTrue(registry.isEmpty());
}
@Test
public void broadcastToEmptyRegistryDoesNothing() {
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING);
server.pregenEnd(1L, false);
assertTrue(registry.isEmpty());
}
@Test
public void awaitingSessionDoesNotReceiveBroadcasts() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING);
assertEquals(0, transport.sent.size());
}
@Test
public void readySessionWithoutPregenCapabilityIsNotBroadcastTo() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, 0L)));
server.broadcastPregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING);
assertFalse(session.hasCapability(IrisProtocol.CAPABILITY_PREGEN));
assertEquals(1, transport.sent.size());
}
@Test
public void burstBeyondRateLimitDropsWithoutDispatching() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true, () -> 1000L);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
int overflow = 5;
int total = IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND + overflow;
byte[] preHelloFrame = IrisMessageCodec.encode(new IrisMessage.PregenProgress(1L, 1L, 1L, 1.0D, 1L, IrisMessage.PregenProgress.STATE_RUNNING));
for (int index = 0; index < total; index++) {
server.onClientFrame("s1", preHelloFrame);
}
assertEquals(IrisProtocol.MAX_INBOUND_FRAMES_PER_SECOND, server.droppedBeforeHelloCount());
assertEquals(overflow, server.rateLimitedFrameCount());
}
@Test
public void cursorInfoRequestServedForCapableReadySession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR)));
server.setEngineResolver(sessionId -> cursorEngine("iris:plains", "iris:temperate", "iris:lush_cave", 88, "overworld"));
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(10, -20)));
assertEquals(1L, server.cursorInfoServedCount());
IrisMessage.CursorInfo info = (IrisMessage.CursorInfo) transport.sent.get(1);
assertEquals(10, info.blockX());
assertEquals(-20, info.blockZ());
assertEquals("iris:plains", info.biomeKey());
assertEquals("iris:temperate", info.regionKey());
assertEquals("iris:lush_cave", info.caveBiomeKey());
assertEquals(88, info.height());
assertEquals("overworld", info.dimensionKey());
}
@Test
public void cursorInfoRequestWithoutCapabilityRejectedWithoutResolvingEngine() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, 0L)));
server.setEngineResolver(sessionId -> {
throw new AssertionError("engine must not be resolved without cursor capability");
});
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(1, 2)));
assertEquals(1L, server.capabilityRejectedCount());
assertEquals(1, transport.sent.size());
}
@Test
public void cursorInfoRequestWithNullEngineCountsDrop() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR)));
server.setEngineResolver(sessionId -> null);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(1, 2)));
assertEquals(1L, server.noEngineDropCount());
assertEquals(0L, server.cursorInfoServedCount());
assertEquals(1, transport.sent.size());
}
@Test
public void visionTileRequestForwardedForCapableReadySession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION)));
RecordingVisionHandler handler = new RecordingVisionHandler();
server.setVisionTileHandler(handler);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(7, -9, 3)));
assertEquals(1, handler.count);
assertEquals(7, handler.lastTileX);
assertEquals(-9, handler.lastTileZ);
assertEquals(3, handler.lastZoom);
assertEquals(1L, server.visionTileForwardedCount());
}
@Test
public void visionTileRequestBudgetDropsExcessWithinWindow() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true, () -> 1000L);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION)));
RecordingVisionHandler handler = new RecordingVisionHandler();
server.setVisionTileHandler(handler);
int overflow = 3;
int total = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND + overflow;
byte[] request = IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0));
for (int index = 0; index < total; index++) {
server.onClientFrame("s1", request);
}
assertEquals(IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND, handler.count);
assertEquals(IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND, server.visionTileForwardedCount());
assertEquals(overflow, server.visionRateLimitedCount());
}
@Test
public void visionTileRequestWithoutCapabilityRejected() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR)));
RecordingVisionHandler handler = new RecordingVisionHandler();
server.setVisionTileHandler(handler);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0)));
assertEquals(0, handler.count);
assertEquals(1L, server.capabilityRejectedCount());
}
@Test
public void visionTileRequestWithoutHandlerCountsDrop() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION)));
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(1, 1, 0)));
assertEquals(1L, server.noEngineDropCount());
}
@Test
public void readySessionReceivesDimensionStatusBroadcast() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_VISION)));
server.broadcastDimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true);
assertEquals(new IrisMessage.DimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true), transport.sent.get(1));
}
@Test
public void awaitingSessionDoesNotReceiveDimensionStatusBroadcast() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.broadcastDimensionStatus("minecraft:overworld", "overworld", 1337L, -64, 320, true);
assertEquals(0, transport.sent.size());
}
@Test
public void sendDimensionStatusTargetsSingleReadySession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR)));
server.sendDimensionStatus("s1", "minecraft:the_nether", "nether", 42L, 0, 256, true);
assertEquals(new IrisMessage.DimensionStatus("minecraft:the_nether", "nether", 42L, 0, 256, true), transport.sent.get(1));
}
@Test
public void pregenRegionDeltaReachesPregenCapableSession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
server.broadcastPregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_GENERATING);
assertEquals(new IrisMessage.PregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_GENERATING), transport.sent.get(1));
assertEquals(1L, server.pregenRegionDeltasBroadcastCount());
}
@Test
public void pregenRegionDeltaNotSentToSessionWithoutPregenCapability() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO)));
server.broadcastPregenRegionDelta(7L, 3, -4, IrisMessage.PregenRegionDelta.STATE_DONE);
assertEquals(1, transport.sent.size());
assertEquals(0L, server.pregenRegionDeltasBroadcastCount());
}
@Test
public void studioHotloadReachesStudioCapableSession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO)));
server.broadcastStudioHotload("overworld", 5, false, "");
assertEquals(new IrisMessage.StudioHotload("overworld", 5, false, ""), transport.sent.get(1));
assertEquals(1L, server.studioHotloadsBroadcastCount());
}
@Test
public void readySessionWithoutStudioCapabilityReceivesNoStudioMessages() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
server.broadcastStudioHotload("overworld", 5, true, "boom");
server.broadcastToast(IrisMessage.Toast.KIND_ERROR, "Studio Hotload", "overworld failed");
assertFalse(session.hasCapability(IrisProtocol.CAPABILITY_STUDIO));
assertEquals(1, transport.sent.size());
assertEquals(0L, server.studioHotloadsBroadcastCount());
assertEquals(0L, server.toastsBroadcastCount());
}
@Test
public void broadcastToastReachesStudioCapableSession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO)));
server.broadcastToast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld");
assertEquals(new IrisMessage.Toast(IrisMessage.Toast.KIND_SUCCESS, "Studio Hotload", "overworld"), transport.sent.get(1));
assertEquals(1L, server.toastsBroadcastCount());
}
@Test
public void sendToastTargetsStudioCapableSession() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_STUDIO)));
server.sendToast("s1", IrisMessage.Toast.KIND_INFO, "Hello", "World");
assertEquals(new IrisMessage.Toast(IrisMessage.Toast.KIND_INFO, "Hello", "World"), transport.sent.get(1));
assertEquals(1L, server.toastsSentCount());
}
@Test
public void sendToastWithoutStudioCapabilityDropsWithoutCounting() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_PREGEN)));
server.sendToast("s1", IrisMessage.Toast.KIND_INFO, "Hello", "World");
assertEquals(1, transport.sent.size());
assertEquals(0L, server.toastsSentCount());
}
@Test
public void awaitingSessionDoesNotReceiveStudioHotloadBroadcast() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.broadcastStudioHotload("overworld", 1, false, "");
assertEquals(0, transport.sent.size());
assertEquals(0L, server.studioHotloadsBroadcastCount());
}
private static Engine cursorEngine(String biomeKey, String regionKey, String caveKey, int height, String dimensionKey) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(biomeKey);
IrisRegion region = new IrisRegion();
region.setLoadKey(regionKey);
IrisBiome cave = new IrisBiome();
cave.setLoadKey(caveKey);
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey(dimensionKey);
return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class[]{Engine.class}, (proxy, method, args) -> switch (method.getName()) {
case "getSurfaceBiome" -> biome;
case "getRegion" -> region;
case "getCaveBiome" -> cave;
case "getHeight" -> height;
case "getDimension" -> dimension;
case "toString" -> "proxyEngine";
case "hashCode" -> System.identityHashCode(proxy);
case "equals" -> proxy == args[0];
default -> null;
});
}
private static final class RecordingVisionHandler implements VisionTileRequestHandler {
private int count;
private int lastTileX;
private int lastTileZ;
private int lastZoom;
@Override
public void handle(String sessionId, int tileX, int tileZ, int zoomLevel) {
count++;
lastTileX = tileX;
lastTileZ = tileZ;
lastZoom = zoomLevel;
}
}
private static final class RecordingTransport implements IrisServerTransport {
private final List<IrisMessage> sent = new ArrayList<>();
@Override
public void sendToClient(String sessionId, byte[] frame) {
try {
sent.add(IrisMessageCodec.decode(frame));
} catch (ProtocolException rejected) {
throw new AssertionError(rejected);
}
}
}
}
@@ -0,0 +1,187 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisMessageCodec;
import art.arcane.iris.spi.protocol.IrisProtocol;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisTileEncoderTest {
private record DecodedBlob(int width, int height, int mode, int[] pixels) {
}
@Test
public void paletteModeRoundTripsToIdenticalPixels() throws Exception {
int[] pixels = new int[256];
for (int index = 0; index < pixels.length; index++) {
pixels[index] = (index << 16) | (index << 8) | index;
}
byte[] blob = IrisTileEncoder.encodePixels(pixels, 16, 16);
DecodedBlob decoded = decodeBlob(blob);
assertEquals(IrisTileEncoder.MODE_PALETTE, decoded.mode());
assertEquals(16, decoded.width());
assertEquals(16, decoded.height());
assertArrayEquals(pixels, decoded.pixels());
}
@Test
public void rawModeRoundTripsToIdenticalPixels() throws Exception {
int[] pixels = new int[1024];
for (int index = 0; index < pixels.length; index++) {
pixels[index] = index;
}
byte[] blob = IrisTileEncoder.encodePixels(pixels, 32, 32);
DecodedBlob decoded = decodeBlob(blob);
assertEquals(IrisTileEncoder.MODE_RAW_RGB, decoded.mode());
assertArrayEquals(pixels, decoded.pixels());
}
@Test
public void alphaBitsAreStrippedBeforeEncoding() throws Exception {
int[] pixels = new int[4];
pixels[0] = 0xFF123456;
pixels[1] = 0x00123456;
pixels[2] = 0xFFABCDEF;
pixels[3] = 0x77ABCDEF;
byte[] blob = IrisTileEncoder.encodePixels(pixels, 2, 2);
DecodedBlob decoded = decodeBlob(blob);
assertEquals(IrisTileEncoder.MODE_PALETTE, decoded.mode());
assertArrayEquals(new int[]{0x123456, 0x123456, 0xABCDEF, 0xABCDEF}, decoded.pixels());
}
@Test
public void chunkSplitAndReassembleReproducesBlob() {
byte[] blob = new byte[60000];
for (int index = 0; index < blob.length; index++) {
blob[index] = (byte) (index * 31);
}
List<IrisMessage.VisionTile> chunks = IrisTileEncoder.splitIntoChunks(blob, 5, -3, 2, 9);
int expectedCount = (blob.length + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES - 1) / IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES;
assertEquals(expectedCount, chunks.size());
assertTrue("large blob must span multiple chunks", chunks.size() >= 2);
for (int index = 0; index < chunks.size(); index++) {
IrisMessage.VisionTile chunk = chunks.get(index);
assertEquals(5, chunk.tileX());
assertEquals(-3, chunk.tileZ());
assertEquals(2, chunk.zoomLevel());
assertEquals(9, chunk.sequence());
assertEquals(index, chunk.chunkIndex());
assertEquals(expectedCount, chunk.chunkCount());
assertTrue(chunk.data().length <= IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES);
assertTrue("each chunk frame must fit under cap", IrisMessageCodec.encode(chunk).length <= IrisProtocol.MAX_FRAME_BYTES);
}
assertArrayEquals(blob, reassemble(chunks));
}
@Test
public void emptyBlobProducesSingleEmptyChunk() {
List<IrisMessage.VisionTile> chunks = IrisTileEncoder.splitIntoChunks(new byte[0], 0, 0, 0, 0);
assertEquals(1, chunks.size());
assertEquals(1, chunks.get(0).chunkCount());
assertEquals(0, chunks.get(0).data().length);
}
@Test
public void fullPipelineReassemblesToIdenticalPixels() throws Exception {
int[] pixels = new int[4096];
for (int index = 0; index < pixels.length; index++) {
pixels[index] = (index * 2654435761L & 0xFFFFFFL) == 0 ? 1 : (int) (index * 2654435761L & 0xFFFFFFL);
}
byte[] blob = IrisTileEncoder.encodePixels(pixels, 64, 64);
List<IrisMessage.VisionTile> chunks = IrisTileEncoder.splitIntoChunks(blob, 1, 2, 0, 3);
byte[] reassembled = reassemble(chunks);
assertArrayEquals(blob, reassembled);
DecodedBlob decoded = decodeBlob(reassembled);
assertArrayEquals(pixels, decoded.pixels());
for (IrisMessage.VisionTile chunk : chunks) {
assertTrue(IrisMessageCodec.encode(chunk).length <= IrisProtocol.MAX_FRAME_BYTES);
}
}
private static byte[] reassemble(List<IrisMessage.VisionTile> chunks) {
IrisMessage.VisionTile[] ordered = new IrisMessage.VisionTile[chunks.get(0).chunkCount()];
for (IrisMessage.VisionTile chunk : chunks) {
ordered[chunk.chunkIndex()] = chunk;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (IrisMessage.VisionTile chunk : ordered) {
out.writeBytes(chunk.data());
}
return out.toByteArray();
}
private static DecodedBlob decodeBlob(byte[] blob) throws Exception {
byte[] raw = inflate(blob);
DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw));
int width = in.readInt();
int height = in.readInt();
int mode = in.readUnsignedByte();
int count = width * height;
int[] pixels = new int[count];
if (mode == IrisTileEncoder.MODE_PALETTE) {
int paletteSize = in.readInt();
int[] palette = new int[paletteSize];
for (int index = 0; index < paletteSize; index++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
palette[index] = (red << 16) | (green << 8) | blue;
}
for (int index = 0; index < count; index++) {
pixels[index] = palette[in.readUnsignedByte()];
}
} else {
for (int index = 0; index < count; index++) {
int red = in.readUnsignedByte();
int green = in.readUnsignedByte();
int blue = in.readUnsignedByte();
pixels[index] = (red << 16) | (green << 8) | blue;
}
}
return new DecodedBlob(width, height, mode, pixels);
}
private static byte[] inflate(byte[] input) throws DataFormatException {
Inflater inflater = new Inflater();
inflater.setInput(input);
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2));
byte[] buffer = new byte[8192];
while (!inflater.finished()) {
int produced = inflater.inflate(buffer);
if (produced == 0 && inflater.needsInput()) {
break;
}
out.write(buffer, 0, produced);
}
inflater.end();
return out.toByteArray();
}
}
@@ -0,0 +1,105 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.spi.protocol.IrisProtocol;
import org.junit.Test;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
public class IrisVisionRequestServiceTest {
private static final Executor DIRECT = Runnable::run;
private static final Executor DISABLED = runnable -> {
};
@Test
public void saturationDropsOldestRequestsAndCounts() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
EngineResolver resolver = sessionId -> {
throw new AssertionError("disabled executor must not process requests");
};
int maxPending = 4;
int overflow = 6;
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DISABLED, maxPending);
for (int index = 0; index < maxPending + overflow; index++) {
service.handle("s1", index, 0, 0);
}
assertEquals(overflow, service.droppedSaturatedCount());
assertEquals(maxPending, service.pendingSize());
}
@Test
public void nullEngineDropsAndCounts() {
IrisSessionRegistry registry = new IrisSessionRegistry();
CountingTransport transport = registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
EngineResolver resolver = sessionId -> null;
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DIRECT, 8);
service.handle("s1", 0, 0, 0);
assertEquals(1L, service.droppedNoEngineCount());
assertEquals(0L, service.tilesEncodedCount());
assertEquals(0, transport.sent.get());
}
@Test
public void missingReadyOrCapableSessionDropsWithoutResolvingEngine() {
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisSession awaiting = new IrisSession("awaiting", new CountingTransport());
registry.register(awaiting);
IrisSession noCapability = new IrisSession("nocap", new CountingTransport());
noCapability.markReady(IrisProtocol.PROTOCOL_VERSION, 0L);
registry.register(noCapability);
EngineResolver resolver = sessionId -> {
throw new AssertionError("engine must not be resolved for an ineligible session");
};
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, DIRECT, 8);
service.handle("unregistered", 0, 0, 0);
service.handle("awaiting", 0, 0, 0);
service.handle("nocap", 0, 0, 0);
assertEquals(3L, service.droppedNoSessionCount());
assertEquals(0L, service.droppedNoEngineCount());
assertEquals(0L, service.tilesEncodedCount());
}
private static CountingTransport registerReady(IrisSessionRegistry registry, String sessionId, long capabilities) {
CountingTransport transport = new CountingTransport();
IrisSession session = new IrisSession(sessionId, transport);
session.markReady(IrisProtocol.PROTOCOL_VERSION, capabilities);
registry.register(session);
return transport;
}
private static final class CountingTransport implements IrisServerTransport {
private final AtomicInteger sent = new AtomicInteger(0);
@Override
public void sendToClient(String sessionId, byte[] frame) {
sent.incrementAndGet();
}
}
}
@@ -7,9 +7,17 @@ import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DecoratorCoreTest {
@@ -20,16 +20,23 @@ package art.arcane.iris.platform.bukkit;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.data.BlockData;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
@@ -38,6 +45,7 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
@@ -62,9 +70,28 @@ public class BukkitSpiConformanceTest {
private static BlockData blockData(String asString) {
BlockData data = mock(BlockData.class);
doReturn(asString).when(data).getAsString();
doReturn(asString).when(data).getAsString(anyBoolean());
return data;
}
private static PlatformRegistries registries() {
return new BukkitRegistries();
}
private static void assertNamespacedRegistryList(Supplier<List<String>> supplier) {
List<String> keys;
try {
keys = supplier.get();
} catch (Throwable unavailable) {
Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable);
return;
}
assertFalse(keys.isEmpty());
for (String key : keys) {
assertTrue("expected namespaced key but was '" + key + "'", key.contains(":"));
}
}
@Test
public void blockStateInterningReturnsSameInstanceForSameKey() {
BlockData first = blockData("iristest:intern_block[axis=y]");
@@ -129,4 +156,62 @@ public class BukkitSpiConformanceTest {
}
assertSame(first, IrisPlatforms.get());
}
@Test
public void itemKeysAreNamespacedAndNonEmpty() {
assertNamespacedRegistryList(() -> registries().itemKeys());
}
@Test
public void entityKeysAreNamespacedAndNonEmpty() {
assertNamespacedRegistryList(() -> registries().entityKeys());
}
@Test
public void enchantmentKeysAreNamespacedAndNonEmpty() {
assertNamespacedRegistryList(() -> registries().enchantmentKeys());
}
@Test
public void potionEffectKeysAreNamespacedAndNonEmpty() {
assertNamespacedRegistryList(() -> registries().potionEffectKeys());
}
@Test
public void blockTypeKeysAreNamespacedAndNonEmpty() {
assertNamespacedRegistryList(() -> registries().blockTypeKeys());
}
@Test
public void blockTypeKeysMatchAuthorableBlockTypeSource() {
List<String> keys;
List<String> legacy;
try {
keys = registries().blockTypeKeys();
legacy = Arrays.asList(BukkitBlockResolution.getBlockTypes());
} catch (Throwable unavailable) {
Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable);
return;
}
assertEquals(legacy, keys);
}
@Test
public void blockStatePropertiesAreKeyedByNamespacedBlock() {
Map<String, List<PlatformBlockProperty>> states;
try {
states = registries().blockStateProperties();
} catch (Throwable unavailable) {
Assume.assumeNoException("live Bukkit registry unavailable in this environment", unavailable);
return;
}
assertFalse(states.isEmpty());
for (Map.Entry<String, List<PlatformBlockProperty>> entry : states.entrySet()) {
assertTrue("expected namespaced block key but was '" + entry.getKey() + "'", entry.getKey().contains(":"));
for (PlatformBlockProperty property : entry.getValue()) {
assertFalse(property.name().isEmpty());
assertFalse(property.jsonType().isEmpty());
}
}
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.spi;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisPlatformsBindLifecycleTest {
@Before
public void resetBinding() {
IrisPlatforms.unbind();
}
@After
public void clearBinding() {
IrisPlatforms.unbind();
}
@Test
public void rebindsAfterUnbind() {
IrisPlatform first = mock(IrisPlatform.class);
IrisPlatforms.bind(first);
assertTrue(IrisPlatforms.isBound());
assertSame(first, IrisPlatforms.get());
IrisPlatforms.unbind();
assertFalse(IrisPlatforms.isBound());
IrisPlatform second = mock(IrisPlatform.class);
IrisPlatforms.bind(second);
assertTrue(IrisPlatforms.isBound());
assertSame(second, IrisPlatforms.get());
}
@Test
public void unbindIsIdempotentWhenUnbound() {
IrisPlatforms.unbind();
assertFalse(IrisPlatforms.isBound());
IrisPlatforms.unbind();
assertFalse(IrisPlatforms.isBound());
}
}
@@ -0,0 +1,53 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.spi;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class IrisServicesTest {
@Test
public void removeThenReregisterRebindsService() {
Sample first = new Sample();
Sample second = new Sample();
IrisServices.register(Sample.class, first);
assertSame(first, IrisServices.get(Sample.class));
IrisServices.remove(Sample.class);
assertNull(IrisServices.getOrNull(Sample.class));
IrisServices.register(Sample.class, second);
assertSame(second, IrisServices.get(Sample.class));
IrisServices.remove(Sample.class);
assertNull(IrisServices.getOrNull(Sample.class));
}
@Test
public void removeMissingServiceIsNoOp() {
IrisServices.remove(Sample.class);
assertNull(IrisServices.getOrNull(Sample.class));
}
private static final class Sample {
}
}