mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
Dont ask
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+58
@@ -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 {
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user