This commit is contained in:
Brian Neumann-Fopiano
2026-07-30 12:31:39 -04:00
parent 0afbfcb7a2
commit 324cf9e095
374 changed files with 22705 additions and 25650 deletions
@@ -21,6 +21,17 @@ package art.arcane.iris.spi;
import java.util.IllegalFormatException;
import java.util.regex.Pattern;
/**
* Logging front door for core: routes through the bound {@link IrisPlatform} when there is one and falls back
* to {@code System.out}/{@code System.err} when there is not, so code that runs before adapter startup, in
* tests, or in the standalone probe still logs.
* <p>
* Every method is safe from any thread and swallows its own failures - logging never throws. Formatting is
* lenient: a malformed format string is emitted verbatim rather than raising
* {@link java.util.IllegalFormatException}, and a null message renders as {@code "null"}.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisLogging {
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
private static final Pattern MINI_MESSAGE_TAG = Pattern.compile("(?i)</?(?:reset|bold|b|italic|i|underlined?|u|strikethrough|st|obfuscated?|obf|black|dark_blue|dark_green|dark_aqua|dark_red|dark_purple|gold|gray|dark_gray|blue|green|aqua|red|light_purple|yellow|white|gradient|font|hover|click|rainbow)(?::[^>\\n]{0,96})?>|<#[0-9a-f]{6}>");
@@ -28,22 +39,39 @@ public final class IrisLogging {
private IrisLogging() {
}
/**
* Logs at {@link LogLevel#INFO}, applying {@link #format(String, Object...)} to the arguments.
*/
public static void info(String format, Object... args) {
emit(LogLevel.INFO, format(format, args));
}
/**
* Logs a literal message at {@link LogLevel#DEBUG}. Takes no format arguments, so a message containing
* {@code %} needs no escaping.
*/
public static void debug(String message) {
emit(LogLevel.DEBUG, message);
}
/**
* Logs at {@link LogLevel#WARN}, applying {@link #format(String, Object...)} to the arguments.
*/
public static void warn(String format, Object... args) {
emit(LogLevel.WARN, format(format, args));
}
/**
* Logs at {@link LogLevel#ERROR}, applying {@link #format(String, Object...)} to the arguments.
*/
public static void error(String format, Object... args) {
emit(LogLevel.ERROR, format(format, args));
}
/**
* Emits a player-facing message to the console, preserving colour markup when a platform is bound and
* stripping it via {@link #clean(String)} when one is not.
*/
public static void msg(String message) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().msg(message);
@@ -53,6 +81,11 @@ public final class IrisLogging {
System.out.println("[Iris] " + clean(message));
}
/**
* Logs {@code context} at {@link LogLevel#ERROR} and then reports {@code error}. A null or blank
* {@code context} gets a generic description; a null {@code error} is replaced with a synthetic
* {@link IllegalStateException} so the report is never empty.
*/
public static void reportError(String context, Throwable error) {
Throwable cause = error == null ? new IllegalStateException("Unknown Iris failure") : error;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
@@ -65,9 +98,12 @@ public final class IrisLogging {
}
reportError(cause);
cause.printStackTrace(System.err);
}
/**
* Hands {@code error} to the platform's error reporting, or prints it to {@code System.err} when no
* platform is bound. A null {@code error} is ignored.
*/
public static void reportError(Throwable error) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().reportError(error);
@@ -80,6 +116,11 @@ public final class IrisLogging {
}
}
/**
* {@link String#format(String, Object...)} that cannot throw: a null format renders as {@code "null"},
* no arguments returns {@code format} unchanged, and a mismatched format string is returned verbatim.
* Never returns null.
*/
public static String format(String format, Object... args) {
if (format == null) {
return "null";
@@ -96,6 +137,10 @@ public final class IrisLogging {
}
}
/**
* Strips legacy section-sign colour codes and MiniMessage tags, for sinks that render neither. A null
* message renders as {@code "null"}. Never returns null.
*/
public static String clean(String message) {
if (message == null) {
return "null";
@@ -22,22 +22,59 @@ import java.io.File;
/**
* Root platform service provided by each adapter; the single entry point core uses to reach the host platform.
* <p>
* Implementations are shared by every Iris thread and must be thread-safe. Accessor methods
* ({@link #registries()}, {@link #scheduler()}, {@link #structureHooks()}, {@link #biomeWriter()}, the
* version and path methods) are called from generation threads and must not block on the server thread.
* The mutating methods ({@link #callEvent(Object)}, {@link #dispatchConsoleCommand(String)},
* {@link #spawnEntity(PlatformWorld, String, double, double, double)}) touch live server state and are
* expected to be invoked on the thread that owns it - the global/server thread, or the region thread
* owning the target chunk on regionized platforms. Use {@link #scheduler()} to get there.
* <p>
* This interface is internal to Iris. It is not a published integration surface and changes without a
* deprecation cycle; adapters in this repository are its only supported implementors.
*/
public interface IrisPlatform {
/**
* Short adapter identity, for example {@code Bukkit} or the mod loader name. Never null.
*/
String platformName();
/**
* Minecraft version string reported by the host, for example {@code 26.2}. Never null.
*/
String minecraftVersion();
/**
* Registry lookups for blocks, biomes, items and entity types. Never null; may be called off the
* server thread.
*/
PlatformRegistries registries();
/**
* Task dispatch onto the platform's threading model. Never null.
*/
PlatformScheduler scheduler();
/**
* Structure, structure-set and configured-feature access. Never null.
*/
PlatformStructureHooks structureHooks();
/**
* Biome id resolution used when injecting biomes into the mantle. Never null.
*/
PlatformBiomeWriter biomeWriter();
/**
* Root folder Iris owns for packs, settings and generated data. Created if missing. Never null.
*/
File dataFolder();
/**
* {@link #dataFolder()} resolved against {@code path} segments, creating the folder and its parents.
* A null or empty {@code path} returns {@link #dataFolder()}. Never null.
*/
default File dataFolder(String... path) {
if (path == null || path.length == 0) {
return dataFolder();
@@ -48,6 +85,10 @@ public interface IrisPlatform {
return folder;
}
/**
* Same resolution as {@link #dataFolder(String...)} without creating anything on disk. The returned
* {@link File} may not exist. Never null.
*/
default File dataFolderNoCreate(String... path) {
if (path == null || path.length == 0) {
return dataFolder();
@@ -56,23 +97,72 @@ public interface IrisPlatform {
return new File(dataFolder(), String.join(File.separator, path));
}
/**
* A file inside {@link #dataFolder()}, with its parent directories created. The file itself is not
* created. Never null.
*/
File dataFile(String... path);
/**
* The Iris artifact this runtime was loaded from: the plugin jar on Bukkit, the mod jar on a mod
* loader. The Bukkit-flavoured name is retained for source compatibility. Never null; adapters that
* cannot locate the real artifact return a placeholder path inside {@link #dataFolder()}.
*/
File pluginJar();
/**
* Iris's own version as a comparable integer, derived from the artifact version.
*/
int irisVersionNumber();
/**
* The host Minecraft version as a comparable integer, derived from {@link #minecraftVersion()}.
*/
int minecraftVersionNumber();
/**
* Publishes an Iris event on the host's event bus.
* <p>
* The parameter is untyped by design: the event object is a platform type that this module cannot
* name. On Bukkit it must be an {@code org.bukkit.event.Event} and the adapter casts it; platforms
* without an event bus - every mod loader adapter - ignore the call. Callers therefore must not rely
* on delivery, and must construct events from the platform module that owns the type. {@code event} must
* not be null and must be the type the active adapter expects; a mismatch fails inside the adapter.
* <p>
* Invoke on the server thread. Bukkit's event bus is not thread-safe.
*/
void callEvent(Object event);
/**
* Runs {@code command} as the server console. Invoke on the server thread.
*/
void dispatchConsoleCommand(String command);
boolean spawnEntity(Object world, String entityKey, double x, double y, double z);
/**
* Spawns a vanilla entity by namespaced key at the given block-space position.
* <p>
* Adapters unwrap {@link PlatformWorld#nativeHandle()} to reach the host world, so {@code world} must
* be a {@link PlatformWorld} produced by the active adapter. Returns false - never throws - when
* {@code world} or {@code entityKey} is null, the world belongs to a different adapter, the key does
* not parse, the entity type is unknown, or the platform refuses the spawn.
* <p>
* Invoke on the thread owning the target chunk.
*/
boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z);
/**
* Routes a log line to the host logger at {@code level}. Safe from any thread. Prefer
* {@link IrisLogging}, which tolerates an unbound platform.
*/
void log(LogLevel level, String message);
/**
* Routes a formatted, player-facing message to the console. Safe from any thread.
*/
void msg(String message);
/**
* Hands a throwable to the host's error reporting. Safe from any thread; must not rethrow.
*/
void reportError(Throwable error);
}
@@ -20,6 +20,12 @@ package art.arcane.iris.spi;
/**
* Static holder binding the active platform adapter for the lifetime of the runtime.
* <p>
* Exactly one adapter binds itself during startup; core reaches it through {@link #get()} from every thread.
* The binding is volatile, so reads are safe from any thread and see the bind that happened before them;
* {@link #bind(IrisPlatform)} and {@link #unbind()} serialize against each other.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisPlatforms {
private static volatile IrisPlatform platform;
@@ -27,6 +33,11 @@ public final class IrisPlatforms {
private IrisPlatforms() {
}
/**
* Binds {@code p} as the active platform. Rebinding the same instance is a no-op.
*
* @throws IllegalStateException if a different platform is already bound
*/
public static synchronized void bind(IrisPlatform p) {
if (platform != null && platform != p) {
throw new IllegalStateException("Iris platform is already bound to a different instance");
@@ -34,10 +45,19 @@ public final class IrisPlatforms {
platform = p;
}
/**
* Clears the binding. Safe to call when nothing is bound.
*/
public static synchronized void unbind() {
platform = null;
}
/**
* The bound platform. Never returns null.
*
* @throws IllegalStateException if no platform is bound, which means Iris was reached before adapter
* startup or after shutdown
*/
public static IrisPlatform get() {
IrisPlatform bound = platform;
if (bound == null) {
@@ -46,6 +66,10 @@ public final class IrisPlatforms {
return bound;
}
/**
* Whether a platform is bound. Use before {@link #get()} on paths that must tolerate a bare JVM, such as
* logging during startup or in tests.
*/
public static boolean isBound() {
return platform != null;
}
@@ -20,16 +20,41 @@ package art.arcane.iris.spi;
import java.util.concurrent.ConcurrentHashMap;
/**
* Process-wide registry mapping a service interface to the single implementation the active adapter
* installed for it. Core resolves platform-provided collaborators through here instead of importing them.
* <p>
* Backed by a {@link ConcurrentHashMap}; every method is safe from any thread. Registration happens during
* adapter startup and removal during shutdown, so a lookup racing a shutdown can legitimately miss - resolve
* lazily at the point of use rather than caching, and prefer {@link #getOrNull(Class)} on optional paths.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisServices {
private static final ConcurrentHashMap<Class<?>, Object> SERVICES = new ConcurrentHashMap<>();
private IrisServices() {
}
/**
* Binds {@code implementation} as the provider for {@code type}, replacing any previous binding.
* <p>
* Both parameters are untyped rather than a generic {@code <T> (Class<T>, T)} pair on purpose: callers
* register from wildcard-typed loops - {@code Class<? extends IrisService>} paired with an
* {@code IrisService} instance - and from maps whose value type is the supertype, neither of which a
* generic signature accepts without casts at every call site. The pairing is enforced at runtime by
* {@link Class#cast(Object)}, which throws {@link ClassCastException} on a mismatch, so a wrong pairing
* fails at registration rather than at first use. Null arguments throw.
*/
public static void register(Class<?> type, Object implementation) {
SERVICES.put(type, type.cast(implementation));
}
/**
* The provider registered for {@code type}. Never returns null.
*
* @throws IllegalStateException if nothing is registered for {@code type}
*/
public static <T> T get(Class<T> type) {
Object implementation = SERVICES.get(type);
if (implementation == null) {
@@ -38,15 +63,24 @@ public final class IrisServices {
return type.cast(implementation);
}
/**
* The provider registered for {@code type}, or null when nothing is registered.
*/
public static <T> T getOrNull(Class<T> type) {
Object implementation = SERVICES.get(type);
return implementation == null ? null : type.cast(implementation);
}
/**
* Unbinds {@code type}. No-op when nothing is registered.
*/
public static void remove(Class<?> type) {
SERVICES.remove(type);
}
/**
* Unbinds every service. Called on adapter shutdown and between tests.
*/
public static void clear() {
SERVICES.clear();
}
@@ -19,11 +19,22 @@
package art.arcane.iris.spi;
/**
* Severity levels for platform-routed log messages.
* Severity levels for platform-routed log messages. Adapters map these onto the host logger; the unbound
* fallback in {@link IrisLogging} sends {@link #WARN} and {@link #ERROR} to {@code System.err} and the rest to
* {@code System.out}.
* <p>
* Constants may be added. Switch expressions over this enum need a {@code default} arm.
*/
public enum LogLevel {
/**
* Diagnostic detail. Adapters route it to the host logger's own debug channel unless Iris debug logging is
* enabled, so it is normally invisible in server output.
*/
DEBUG,
/** Normal operational messages. */
INFO,
/** Recoverable problems and misconfiguration. */
WARN,
/** Failures; usually paired with {@link IrisPlatform#reportError(Throwable)}. */
ERROR
}
@@ -20,11 +20,25 @@ package art.arcane.iris.spi;
/**
* Neutral handle for a resolved biome backed by an adapter-owned native handle.
* <p>
* Immutable and safe to share across threads. Internal to Iris; not a published integration surface.
*
* @see PlatformRegistries#biome(String)
*/
public interface PlatformBiome {
/**
* Canonical {@code namespace:path} biome key. Never null.
*/
String key();
/**
* Namespace half of {@link #key()}. Never null.
*/
String namespace();
/**
* The adapter's backing biome object - {@code org.bukkit.block.Biome} on Bukkit, a {@code Biome} registry
* value on a mod loader. Never null. Only code inside the owning adapter may cast it.
*/
Object nativeHandle();
}
@@ -22,9 +22,25 @@ import java.util.List;
/**
* Resolves pack biome keys to platform biome ids for mantle injection and enumerates the platform's biome registry.
* <p>
* Called from generation threads for every biome the pack names, so implementations must be thread-safe and
* should cache their registry lookups.
* <p>
* Internal to Iris; not a published integration surface.
*/
public interface PlatformBiomeWriter {
/**
* The numeric registry id the host uses for {@code key}, which is what gets written into biome storage.
* <p>
* Ids are registry-order dependent and therefore valid only for the current server session; never persist
* one. Adapters resolve the key directly, then fall back to a derived match, and finally to a safe default
* id rather than failing - so a nonsense key yields a wrong biome, not an exception. Validate keys with
* {@link PlatformRegistries#biome(String)} when you need to know they exist.
*/
int biomeIdFor(String key);
/**
* Every biome in the host registry, including datapack and mod biomes. Never null.
*/
List<PlatformBiome> allBiomes();
}
@@ -20,7 +20,23 @@ package art.arcane.iris.spi;
import java.util.List;
/**
* One block state property as JSON schema vocabulary, so pack schema generation can describe block keys without
* knowing platform property types.
* <p>
* Immutable. Produced by {@link PlatformRegistries#blockStateProperties()} and consumed only by schema
* generation, never on the generation path. Internal to Iris; not a published integration surface.
*
* @param name the property name as it appears in a block key, for example {@code waterlogged}
* @param jsonType JSON schema type: {@code boolean}, {@code integer} or {@code string}. Never null or empty
* @param defaultValue the value the host's default state carries, boxed as its JSON representation
* @param allowedValues every legal value, empty when the adapter cannot enumerate them. Never null
* @param numericRange bounds for a numeric property, null for {@code boolean} and {@code string}
*/
public record PlatformBlockProperty(String name, String jsonType, Object defaultValue, List<Object> allowedValues, PlatformNumericRange numericRange) {
/**
* Whether {@link #numericRange()} is present, and therefore whether schema output should emit bounds.
*/
public boolean hasNumericRange() {
return numericRange != null;
}
@@ -20,67 +20,182 @@ package art.arcane.iris.spi;
/**
* Neutral handle for a resolved block state; the canonical key is the config currency and the native handle is adapter-owned.
* <p>
* Instances are immutable value handles, interned by the adapter, and read from generation threads in bulk -
* every predicate here must be a cached field read or cheap computation, never a registry or world lookup.
* Compare with {@link #matches(PlatformBlockState)} rather than {@code equals}; only interned singletons such as
* {@link PlatformRegistries#air()} are safe to compare by identity.
* <p>
* Internal to Iris; not a published integration surface.
*
* @see PlatformRegistries#block(String)
*/
public interface PlatformBlockState {
/**
* Canonical {@code namespace:path[prop=value,...]} key, the form packs and objects store. Never null.
*/
String key();
/**
* Namespace half of {@link #key()}, for example {@code minecraft}. Never null.
*/
String namespace();
/**
* The key with any property block stripped, memoized by implementors that intern their states.
* Returning null means "not memoized"; callers must then derive it from {@link #key()}.
*/
default String materialKey() {
return null;
}
/**
* Whether this is air, including cave and void air.
*/
boolean isAir();
/**
* Whether the host treats this as a solid collision block.
*/
boolean isSolid();
/**
* Whether this blocks light fully. Drives Iris's own light and surface reasoning.
*/
boolean isOccluding();
/**
* Whether this state came from a custom-content provider rather than a vanilla registry entry. Custom
* states carry a {@link #deferredPlacementKey()} and a {@link #placementBaseState()}.
*/
boolean isCustom();
/**
* For a custom state, the provider key to hand back after the block is written, so the provider can finish
* placement once the chunk exists. Null for ordinary states.
*/
default String deferredPlacementKey() {
return null;
}
/**
* The vanilla state to actually write for a custom state - the placeholder the provider later replaces.
* Returns {@code this} for ordinary states. Never null.
*/
default PlatformBlockState placementBaseState() {
return this;
}
/**
* Whether this is a fluid block, water or lava.
*/
boolean isFluid();
/**
* Whether this is water specifically.
*/
boolean isWater();
/**
* Whether this state carries a {@code waterlogged=true} property.
*/
boolean isWaterLogged();
/**
* Whether this state carries a {@code lit=true} property.
*/
boolean isLit();
/**
* Whether the host needs a block update after this is placed - stairs, fences, redstone and other states
* that resolve their shape from neighbours.
*/
boolean isUpdatable();
/**
* Whether this is plant foliage: grass, ferns, flowers and similar decoration.
*/
boolean isFoliage();
/**
* Whether this is part of a tree - tagged as a log or as leaves.
*/
boolean isTreeBlock();
/**
* Whether foliage can be planted on top of this block.
*/
boolean isFoliagePlantable();
/**
* Whether this is a decorant: a thin block that sits on a surface and cannot support anything.
*/
boolean isDecorant();
/**
* Whether this block has an inventory Iris can fill from a loot table.
*/
boolean isStorage();
/**
* Whether this is specifically a chest, which needs the pairing and orientation handling chests require.
*/
boolean isStorageChest();
/**
* Whether this is an ore block, and therefore a candidate for
* {@link PlatformRegistries#deepSlateOre(PlatformBlockState, PlatformBlockState)}.
*/
boolean isOre();
/**
* Whether this is deepslate or a deepslate variant.
*/
boolean isDeepSlate();
/**
* Whether this is a vine-like block that attaches to a face and hangs.
*/
boolean isVineBlock();
/**
* Whether this block can be placed onto {@code onto} - the support check Iris runs before writing
* decoration. Passing a state from a different adapter fails at runtime.
*/
boolean canPlaceOnto(PlatformBlockState onto);
/**
* Whether the two states are the same block with the same properties. Prefer this to {@code equals}, which
* is identity-based on some adapters.
*/
boolean matches(PlatformBlockState state);
/**
* Convenience for the common "nothing solid here" test.
*/
default boolean isAirOrFluid() {
return isAir() || isFluid();
}
/**
* Whether the host attaches a block entity to this block - signs, chests, spawners and similar. Such blocks
* need their tile data applied after the block write.
*/
boolean hasTileEntity();
/**
* This state with one property overridden, merged into {@link #key()} and re-resolved. Never mutates the
* receiver.
* <p>
* Strict: a property name or value the block does not accept throws rather than being dropped. Validate
* against {@link PlatformRegistries#blockStateProperties()} first, or resolve the full key through
* {@link PlatformRegistries#blockOrNull(String)} instead.
*/
PlatformBlockState withProperty(String name, String value);
/**
* The adapter's backing state object - {@code org.bukkit.block.data.BlockData} on Bukkit,
* {@code BlockState} on a mod loader. Never null. Only code inside the owning adapter may cast it.
*/
Object nativeHandle();
}
@@ -20,13 +20,32 @@ package art.arcane.iris.spi;
/**
* Neutral handle for a resolved entity type backed by an adapter-owned native handle.
* <p>
* Immutable and safe to share across threads. Internal to Iris; not a published integration surface.
*
* @see PlatformRegistries#entity(String)
*/
public interface PlatformEntityType {
/**
* Canonical {@code namespace:path} entity type key. Never null.
*/
String key();
/**
* Namespace half of {@link #key()}. Never null.
*/
String namespace();
/**
* The host's spawn category, lowercased - {@code monster}, {@code creature}, {@code ambient} and so on.
* Iris matches it against pack spawn rules, so the string form is the contract rather than any enum. Never
* null.
*/
String spawnCategory();
/**
* The adapter's backing entity type object - {@code org.bukkit.entity.EntityType} on Bukkit,
* {@code EntityType} on a mod loader. Never null. Only code inside the owning adapter may cast it.
*/
Object nativeHandle();
}
@@ -20,11 +20,26 @@ package art.arcane.iris.spi;
/**
* Neutral handle for a resolved item type backed by an adapter-owned native handle.
* <p>
* Describes an item type, not a stack - no count, no components. Immutable and safe to share across threads.
* Internal to Iris; not a published integration surface.
*
* @see PlatformRegistries#item(String)
*/
public interface PlatformItem {
/**
* Canonical {@code namespace:path} item key. Never null.
*/
String key();
/**
* Namespace half of {@link #key()}. Never null.
*/
String namespace();
/**
* The adapter's backing item object - {@code org.bukkit.Material} on Bukkit, an {@code Item} registry value
* on a mod loader. Never null. Only code inside the owning adapter may cast it.
*/
Object nativeHandle();
}
@@ -18,5 +18,18 @@
package art.arcane.iris.spi;
/**
* Inclusive-or-exclusive numeric bounds for a {@link PlatformBlockProperty}, mirroring JSON schema's
* {@code minimum}/{@code maximum} pair.
* <p>
* Immutable. Bounds are carried as {@code double} regardless of the property's JSON type; an
* {@code integer} property's bounds are whole numbers and are narrowed by the schema writer. Internal to Iris;
* not a published integration surface.
*
* @param minimum lower bound
* @param maximum upper bound
* @param exclusiveMinimum whether {@code minimum} itself is disallowed
* @param exclusiveMaximum whether {@code maximum} itself is disallowed
*/
public record PlatformNumericRange(double minimum, double maximum, boolean exclusiveMinimum, boolean exclusiveMaximum) {
}
@@ -23,39 +23,105 @@ import java.util.Map;
/**
* Resolves namespaced string keys against the platform's live registries into interned neutral handles.
* <p>
* Keys are the pack's currency: {@code namespace:path} with optional {@code [prop=value,...]} block state
* properties. Resolution runs on generation threads for every block a pack names, so implementations must be
* thread-safe and must intern or cache their results - a key that resolves once should not re-parse.
* <p>
* Internal to Iris; not a published integration surface.
*/
public interface PlatformRegistries {
/**
* Resolves a block key through the platform's compatibility layer, which rewrites keys that moved between
* Minecraft versions and consults registered custom-content providers. An unresolvable key is reported and
* falls back to air; a key that cannot be parsed at all may come back null. Callers that need to tell
* absence from air use {@link #blockOrNull(String)}.
*/
PlatformBlockState block(String key);
/**
* Resolves a block key, returning null instead of an air fallback when it does not resolve. Silent.
*/
PlatformBlockState blockOrNull(String key);
/**
* {@link #blockOrNull(String)} with control over whether an unresolved key is logged. Pass
* {@code warn = false} for speculative lookups.
*/
PlatformBlockState blockOrNull(String key, boolean warn);
/**
* The interned air state. Never null; identity-comparable across calls.
*/
PlatformBlockState air();
/**
* The deepslate variant of {@code ore} when {@code block} is deepslate, otherwise {@code ore} unchanged.
* Lets ore placement follow the host stone without the pack enumerating variants.
*/
PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore);
/**
* Resolves a biome key against the live biome registry, including datapack and mod biomes. Null when the
* key does not parse or is not registered.
*/
PlatformBiome biome(String key);
/**
* Resolves an item key. Null when unknown.
*/
PlatformItem item(String key);
/**
* Resolves an entity type key. Null when unknown.
*/
PlatformEntityType entity(String key);
/**
* Every registered block state key, properties included. Drives schema completion and command
* suggestions, not the generation path. Never null.
*/
List<String> blockKeys();
/**
* Every registered biome key. Never null.
*/
List<String> biomeKeys();
/**
* Every registered structure key. Never null.
*/
List<String> structureKeys();
/**
* Every registered item key. Never null.
*/
List<String> itemKeys();
/**
* Every registered entity type key. Never null.
*/
List<String> entityKeys();
/**
* Every registered block key without state properties - the material-level view of
* {@link #blockKeys()}. Never null.
*/
List<String> blockTypeKeys();
/**
* Every registered enchantment key. Never null.
*/
List<String> enchantmentKeys();
/**
* Every registered potion effect key. Never null.
*/
List<String> potionEffectKeys();
/**
* Block key to its declared state properties, used to generate pack schema enums and numeric ranges.
* Keyed by material-level block key. Never null.
*/
Map<String, List<PlatformBlockProperty>> blockStateProperties();
}
@@ -20,15 +20,45 @@ package art.arcane.iris.spi;
/**
* Platform task dispatch; region scheduling targets the owning region thread on regionized platforms and the global thread elsewhere.
* <p>
* Every method is safe to call from any thread. Delivery timing is deliberately unspecified: submitting from the
* thread that already owns the target may run the task inline before returning, or queue it for the next tick,
* depending on the adapter. Callers must assume neither - do not treat return as completion, and do not assume
* the task has not already run. Only a {@code later*} call with a positive delay guarantees a deferral. Tasks that throw are
* reported rather than killing the scheduler, and there is no handle to cancel with; a task that must stop early
* checks its own state.
* <p>
* Internal to Iris; not a published integration surface.
*/
public interface PlatformScheduler {
/**
* Runs {@code task} on the server thread - the global region thread on regionized platforms.
*/
void global(Runnable task);
/**
* Runs {@code task} on the thread owning chunk {@code (chunkX, chunkZ)} in {@code world}, which is that
* chunk's region thread on regionized platforms and the server thread elsewhere. Use for anything that
* touches blocks or entities in a known chunk. Adapters that cannot resolve a region owner fall back to
* {@link #global(Runnable)}.
*/
void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task);
/**
* Runs {@code task} on a pooled thread off the server thread. Must not touch world state.
*/
void async(Runnable task);
/**
* {@link #global(Runnable)} delayed by {@code ticks}. A non-positive delay degrades to a plain
* {@link #global(Runnable)}, inheriting its unspecified timing.
*/
void laterGlobal(Runnable task, int ticks);
/**
* {@link #region(PlatformWorld, int, int, Runnable)} delayed by {@code ticks}, with the same non-positive-delay
* degradation as {@link #laterGlobal(Runnable, int)}. Adapters without regions fall back to the delayed global
* queue.
*/
void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks);
}
@@ -22,31 +22,81 @@ import java.util.List;
/**
* Neutral access to the host platform's structure, structure-set and configured-feature registries plus placement entry points.
* <p>
* The key enumerations read registries and are safe from any thread. The two placement methods write blocks
* into a live world and must run on the thread owning the target chunks - the region thread on regionized
* platforms, the server thread elsewhere. They are used by the authoring tools that capture vanilla and
* datapack structures into Iris objects, not by the generation path.
* <p>
* Internal to Iris; not a published integration surface.
*/
public interface PlatformStructureHooks {
/**
* Every registered structure key. Never null.
*/
List<String> structureKeys();
/**
* Registered jigsaw structure keys - the subset of {@link #structureKeys()} assembled from template pools.
* Empty when the adapter cannot distinguish them.
*/
default List<String> jigsawStructureKeys() {
return List.of();
}
/**
* Registered jigsaw template pool keys. Empty when the adapter cannot enumerate them.
*/
default List<String> templatePoolKeys() {
return List.of();
}
/**
* Every registered structure-set key, the placement grouping that decides structure spacing. Never null.
*/
List<String> structureSetKeys();
/**
* Biome keys the given structure is allowed to generate in. Empty when the structure is unknown or declares
* no biome filter. Never null.
*/
List<String> structureBiomeKeys(String structureKey);
/**
* Configured-feature keys that Iris can place as objects - trees, patches and other single-shot features.
* Never null.
*/
List<String> objectFeatureKeys();
/**
* Structure keys actually reachable in {@code world}, after its dimension's structure sets and biome filter
* are applied. Narrower than {@link #structureKeys()}. Never null.
*/
List<String> reachableStructureKeys(PlatformWorld world);
/**
* Biome keys the world's biome source can emit. Never null.
*/
List<String> possibleBiomeKeys(PlatformWorld world);
/**
* Places a configured feature at world coordinates with the given seed. Returns false when the key is
* unknown or the feature declines to place. Mutates the world; see the threading note on this interface.
*/
boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed);
/**
* Generates and places a structure anchored at the given chunk.
*
* @param maxSpan reject the placement if the structure's bounding box exceeds this span in blocks
* @return the placed bounding box as {@code {minX, minY, minZ, maxX, maxY, maxZ}}, or null when the key is
* unknown, the structure produced no valid start, or the box exceeded {@code maxSpan}
*/
int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan);
/**
* Whether this adapter implements {@link #placeStructure(PlatformWorld, int, int, String, long, int)}.
* Check before offering structure capture; adapters without the required host access return false.
*/
boolean supportsStructurePlacement();
}
@@ -20,29 +20,78 @@ package art.arcane.iris.spi;
/**
* Neutral view of a loaded world for edit and lifecycle paths; never used on the generation hot path.
* <p>
* Metadata reads ({@link #name()}, {@link #seed()}, the height bounds) are cheap and safe from any thread. The
* block, biome and weather accessors read or mutate live world state and must be called on the thread that
* owns the target chunk - the region thread on regionized platforms, the server thread elsewhere. Reach that
* thread with {@link PlatformScheduler#region(PlatformWorld, int, int, Runnable)}.
* <p>
* Internal to Iris; not a published integration surface.
*/
public interface PlatformWorld {
/**
* The host's world name. Never null.
*/
String name();
/**
* The world seed, as the host reports it.
*/
long seed();
/**
* Lowest buildable Y, inclusive. Usually negative.
*/
int minHeight();
/**
* Highest buildable Y, exclusive.
*/
int maxHeight();
/**
* The block state at world coordinates. Loads the chunk if it is not resident, so treat it as blocking.
* Adapters delegate straight to the host, so a coordinate outside {@link #minHeight()}/{@link #maxHeight()}
* gets whatever the host does with it - clamp, air, or throw. Bounds-check first.
*/
PlatformBlockState getBlock(int x, int y, int z);
/**
* Writes a block state at world coordinates. Same out-of-bounds caveat as {@link #getBlock(int, int, int)}.
*
* @param flags platform update flags; bit 0 requests neighbour/physics updates, higher bits are
* adapter-specific. Pass 0 for a silent write
*/
void setBlock(int x, int y, int z, PlatformBlockState block, int flags);
/**
* The biome at world coordinates. Loads the chunk if it is not resident.
*/
PlatformBiome getBiome(int x, int y, int z);
/**
* Whether the chunk is currently resident. Cheap; the only accessor here that never triggers a load.
*/
boolean isChunkLoaded(int chunkX, int chunkZ);
/**
* The world's time of day in ticks.
*/
long getTime();
/**
* Whether it is raining or snowing.
*/
boolean isStorming();
/**
* Whether a thunderstorm is active.
*/
boolean isThundering();
/**
* The adapter's backing world object - {@code org.bukkit.World} on Bukkit, {@code ServerLevel} on a mod
* loader. Never null. Only code inside the owning adapter may cast it; core must not.
*/
Object nativeHandle();
}
@@ -20,9 +20,24 @@ package art.arcane.iris.spi.protocol;
import java.util.List;
/**
* Every message the Iris plugin channel carries, as immutable records under one sealed interface.
* <p>
* Sealed so {@link IrisMessageCodec} can switch exhaustively - adding a permitted record forces the codec to
* handle it. Records are immutable and safe to hand between threads, except that {@link VisionTile} holds its
* payload array by reference and must not be mutated after construction.
* <p>
* Internal to Iris; not a published integration surface.
*/
public sealed interface IrisMessage {
/**
* The {@code IrisProtocol.TYPE_*} discriminator written as the first field of the encoded frame.
*/
int messageTypeId();
/**
* Client to server, first frame: the client's protocol version and the capability bits it wants.
*/
record ClientHello(int protocolVersion, long capabilities) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -30,6 +45,10 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client, handshake reply: the version and capability bits actually granted, the server brand, and
* whether any Iris world is live.
*/
record ServerHello(int protocolVersion, long capabilities, String serverBrand, boolean irisActive) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -37,8 +56,13 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: periodic pregeneration progress for one job.
*/
record PregenProgress(long jobId, long chunksDone, long chunksTotal, double chunksPerSecond, long etaMillis, int state) implements IrisMessage {
/** The job is generating. */
public static final int STATE_RUNNING = 0;
/** The job is paused and will resume. */
public static final int STATE_PAUSED = 1;
@Override
@@ -47,6 +71,10 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: a pregeneration job stopped. {@code completed} distinguishes finishing from being
* cancelled or failing.
*/
record PregenEnd(long jobId, boolean completed) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -54,6 +82,10 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: which pack and height bounds back the dimension the player is in. {@code irisWorld} is
* false for a vanilla dimension, in which case the other fields are placeholders.
*/
record DimensionStatus(String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -61,6 +93,10 @@ public sealed interface IrisMessage {
}
}
/**
* Client to server: what does the generator say about this column. Rate-limited by
* {@link IrisProtocol#MAX_INBOUND_FRAMES_PER_SECOND}.
*/
record CursorInfoRequest(int blockX, int blockZ) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -68,6 +104,10 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: the answer to a {@link CursorInfoRequest}. {@code caveBiomeKey} is empty when no cave
* biome applies at that column.
*/
record CursorInfo(int blockX, int blockZ, String biomeKey, String regionKey, String caveBiomeKey, int height, String dimensionKey) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -75,6 +115,10 @@ public sealed interface IrisMessage {
}
}
/**
* Client to server: render and send one vision map tile. Rate-limited by
* {@link IrisProtocol#MAX_VISION_TILE_REQUESTS_PER_SECOND}.
*/
record VisionTileRequest(int tileX, int tileZ, int zoomLevel) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -82,6 +126,12 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: one chunk of a rendered tile. A tile larger than
* {@link IrisProtocol#VISION_TILE_MAX_CHUNK_BYTES} arrives as {@code chunkCount} frames sharing a
* {@code sequence}; the client reassembles by {@code chunkIndex} and discards a partial set when the
* sequence changes. {@code data} is held by reference - do not mutate it after construction.
*/
record VisionTile(int tileX, int tileZ, int zoomLevel, int sequence, int chunkIndex, int chunkCount, byte[] data) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -89,7 +139,14 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: point-of-interest overlay for a tile, capped at {@link IrisProtocol#MAX_VISION_MARKERS}
* entries.
*/
record VisionMarkers(int tileX, int tileZ, int zoomLevel, List<Marker> markers) implements IrisMessage {
/**
* One overlay marker at a block position. {@code kind} is a client-side icon selector.
*/
public record Marker(int blockX, int blockZ, int kind, String label) {
}
@@ -99,9 +156,16 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: one region of a pregeneration job changed state. Sent as a delta so the client can paint
* a live progress grid without a full snapshot per tick.
*/
record PregenRegionDelta(long jobId, int regionX, int regionZ, int state) implements IrisMessage {
/** Queued, not started. */
public static final int STATE_PENDING = 0;
/** Currently generating. */
public static final int STATE_GENERATING = 1;
/** Finished and saved. */
public static final int STATE_DONE = 2;
@Override
@@ -110,6 +174,10 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: a studio pack reload happened. {@code failed} marks a reload that did not apply, with
* {@code message} carrying the reason.
*/
record StudioHotload(String packKey, int changedFiles, boolean failed, String message) implements IrisMessage {
@Override
public int messageTypeId() {
@@ -117,10 +185,17 @@ public sealed interface IrisMessage {
}
}
/**
* Server to client: show a transient notification.
*/
record Toast(int kind, String title, String body) implements IrisMessage {
/** Neutral notice. */
public static final int KIND_INFO = 0;
/** Operation succeeded. */
public static final int KIND_SUCCESS = 1;
/** Something needs attention. */
public static final int KIND_WARNING = 2;
/** Operation failed. */
public static final int KIND_ERROR = 3;
@Override
@@ -21,10 +21,25 @@ package art.arcane.iris.spi.protocol;
import java.util.ArrayList;
import java.util.List;
/**
* Encodes and decodes {@link IrisMessage} frames for the Iris plugin channel.
* <p>
* Stateless; safe from any thread. Each call allocates its own {@link IrisWireWriter} or
* {@link IrisWireReader}, so no instance is shared. Encoding and decoding must stay symmetric - the field order
* in each switch arm is the wire format.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisMessageCodec {
private IrisMessageCodec() {
}
/**
* Encodes {@code message} into a frame: type id as a varint, then the record's fields in declaration order.
* Never returns null.
*
* @throws IllegalStateException if the encoded form exceeds {@link IrisProtocol#MAX_FRAME_BYTES}
*/
public static byte[] encode(IrisMessage message) {
IrisWireWriter writer = new IrisWireWriter();
writer.writeVarInt(message.messageTypeId());
@@ -120,6 +135,14 @@ public final class IrisMessageCodec {
return writer.toByteArray();
}
/**
* Decodes a frame produced by {@link #encode(IrisMessage)}.
*
* @return the decoded message, or null when the type id is unknown - which is how a newer peer's messages are
* skipped rather than treated as corruption. Callers must handle null.
* @throws ProtocolException if {@code frame} is null, exceeds {@link IrisProtocol#MAX_FRAME_BYTES}, or is
* truncated or otherwise malformed
*/
public static IrisMessage decode(byte[] frame) throws ProtocolException {
if (frame == null) {
throw new ProtocolException("null frame");
@@ -18,18 +18,40 @@
package art.arcane.iris.spi.protocol;
/**
* Wire constants for the Iris client/server plugin channel: version, channel name, size and rate caps,
* capability bits and message type ids.
* <p>
* Both ends of the channel compile against this class, so every value here is part of the wire contract. Adding
* a message type or capability bit is compatible; changing an existing value is not and requires bumping
* {@link #PROTOCOL_VERSION}. Constants only - no state, safe from any thread.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisProtocol {
/** Wire version exchanged in the hello handshake. Bumped on any incompatible change. */
public static final int PROTOCOL_VERSION = 1;
/** Plugin channel both ends register. */
public static final String CHANNEL = "irisworldgen:main";
/** Hard cap on a single encoded frame. {@link IrisWireWriter} refuses to exceed it; the decoder rejects larger. */
public static final int MAX_FRAME_BYTES = 24576;
/** Inbound frames accepted per client per second before the server sheds. */
public static final int MAX_INBOUND_FRAMES_PER_SECOND = 32;
/** Vision tile requests accepted per client per second, tighter than the general frame budget because each one costs a render. */
public static final int MAX_VISION_TILE_REQUESTS_PER_SECOND = 8;
/** Fixed header size of a vision tile frame, subtracted when splitting a tile into chunks. */
public static final int VISION_TILE_HEADER_BYTES = 25;
/** Largest payload carried by one vision tile chunk. */
public static final int VISION_TILE_MAX_CHUNK_BYTES = 24000;
/** Cap on markers in one {@link IrisMessage.VisionMarkers} frame. */
public static final int MAX_VISION_MARKERS = 256;
/** Capability bit: pregeneration progress streaming. */
public static final long CAPABILITY_PREGEN = 1L << 0;
/** Capability bit: vision map tiles and markers. */
public static final long CAPABILITY_VISION = 1L << 1;
/** Capability bit: cursor coordinate lookups. */
public static final long CAPABILITY_CURSOR = 1L << 2;
/** Capability bit: studio hotload notifications. */
public static final long CAPABILITY_STUDIO = 1L << 3;
public static final int TYPE_CLIENT_HELLO = 1;
public static final int TYPE_SERVER_HELLO = 2;
@@ -20,17 +20,37 @@ package art.arcane.iris.spi.protocol;
import java.nio.charset.StandardCharsets;
/**
* Sequential big-endian reader over one protocol frame.
* <p>
* Not thread-safe and not reusable: it carries a read position, so one instance serves one frame on one thread.
* Every read is bounds-checked against the frame length and throws {@link ProtocolException} rather than
* {@link ArrayIndexOutOfBoundsException}, so a hostile or truncated frame cannot read past its end. Length
* prefixes are validated against the remaining bytes before any allocation, so a forged length cannot force a
* large allocation.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisWireReader {
private final byte[] frame;
private final int limit;
private int position;
/**
* Wraps {@code frame} for reading from offset zero. The array is held by reference and must not be mutated
* while the reader is in use.
*/
public IrisWireReader(byte[] frame) {
this.frame = frame;
this.limit = frame.length;
this.position = 0;
}
/**
* Reads a 7-bit-continuation varint.
*
* @throws ProtocolException if the frame ends mid-varint or the encoding exceeds five bytes
*/
public int readVarInt() throws ProtocolException {
int result = 0;
int shift = 0;
@@ -46,6 +66,11 @@ public final class IrisWireReader {
throw new ProtocolException("varint exceeds 5 bytes");
}
/**
* Reads a fixed four-byte big-endian int.
*
* @throws ProtocolException if fewer than four bytes remain
*/
public int readInt() throws ProtocolException {
requireRemaining(4);
int value = ((frame[position] & 0xFF) << 24)
@@ -56,6 +81,11 @@ public final class IrisWireReader {
return value;
}
/**
* Reads a fixed eight-byte big-endian long.
*
* @throws ProtocolException if fewer than eight bytes remain
*/
public long readLong() throws ProtocolException {
requireRemaining(8);
long value = ((long) (frame[position] & 0xFF) << 56)
@@ -70,15 +100,30 @@ public final class IrisWireReader {
return value;
}
/**
* Reads a double from its IEEE 754 bit pattern.
*
* @throws ProtocolException if fewer than eight bytes remain
*/
public double readDouble() throws ProtocolException {
return Double.longBitsToDouble(readLong());
}
/**
* Reads one byte as a boolean; any non-zero value is true.
*
* @throws ProtocolException if no bytes remain
*/
public boolean readBoolean() throws ProtocolException {
requireRemaining(1);
return frame[position++] != 0;
}
/**
* Reads a varint-length-prefixed UTF-8 string. Never returns null; an empty string is legal.
*
* @throws ProtocolException if the length is negative or exceeds the remaining bytes
*/
public String readString() throws ProtocolException {
int declaredLength = readVarInt();
if (declaredLength < 0) {
@@ -92,6 +137,11 @@ public final class IrisWireReader {
return value;
}
/**
* Reads a varint-length-prefixed byte array into a fresh copy. Never returns null.
*
* @throws ProtocolException if the length is negative or exceeds the remaining bytes
*/
public byte[] readBytes() throws ProtocolException {
int declaredLength = readVarInt();
if (declaredLength < 0) {
@@ -21,15 +21,31 @@ package art.arcane.iris.spi.protocol;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Sequential big-endian writer that builds one protocol frame into a growing byte buffer.
* <p>
* Not thread-safe: it carries a write position, so one instance serves one frame on one thread. The buffer
* doubles as needed and is hard-capped at {@link IrisProtocol#MAX_FRAME_BYTES} - exceeding it throws
* {@link IllegalStateException} at the write that overflows rather than emitting an oversized frame the peer
* would reject. Field order here is the wire format and must mirror {@link IrisWireReader}.
* <p>
* Internal to Iris; not a published integration surface.
*/
public final class IrisWireWriter {
private byte[] buffer;
private int length;
/**
* Creates an empty writer with a small buffer that grows on demand.
*/
public IrisWireWriter() {
this.buffer = new byte[64];
this.length = 0;
}
/**
* Writes a 7-bit-continuation varint. Negative values encode as five bytes.
*/
public void writeVarInt(int value) {
int remaining = value;
while (true) {
@@ -44,6 +60,9 @@ public final class IrisWireWriter {
}
}
/**
* Writes a fixed four-byte big-endian int.
*/
public void writeInt(int value) {
ensure(4);
buffer[length++] = (byte) (value >>> 24);
@@ -52,6 +71,9 @@ public final class IrisWireWriter {
buffer[length++] = (byte) value;
}
/**
* Writes a fixed eight-byte big-endian long.
*/
public void writeLong(long value) {
ensure(8);
buffer[length++] = (byte) (value >>> 56);
@@ -64,14 +86,23 @@ public final class IrisWireWriter {
buffer[length++] = (byte) value;
}
/**
* Writes a double as its IEEE 754 bit pattern.
*/
public void writeDouble(double value) {
writeLong(Double.doubleToLongBits(value));
}
/**
* Writes a boolean as one byte, {@code 1} or {@code 0}.
*/
public void writeBoolean(boolean value) {
writeByte(value ? 1 : 0);
}
/**
* Writes a varint-length-prefixed UTF-8 string. {@code value} must not be null.
*/
public void writeString(String value) {
byte[] encoded = value.getBytes(StandardCharsets.UTF_8);
writeVarInt(encoded.length);
@@ -80,6 +111,9 @@ public final class IrisWireWriter {
length += encoded.length;
}
/**
* Writes a varint-length-prefixed byte array, copying the contents. {@code value} must not be null.
*/
public void writeBytes(byte[] value) {
writeVarInt(value.length);
ensure(value.length);
@@ -87,6 +121,10 @@ public final class IrisWireWriter {
length += value.length;
}
/**
* The bytes written so far, as a fresh copy trimmed to length. The writer stays usable afterwards. Never
* returns null.
*/
public byte[] toByteArray() {
return Arrays.copyOf(buffer, length);
}
@@ -18,7 +18,19 @@
package art.arcane.iris.spi.protocol;
/**
* Signals a malformed protocol frame: truncated, over the size cap, or carrying an impossible length prefix.
* <p>
* Checked on purpose - a bad frame is an expected condition on a public channel, not a bug, and the handler is
* meant to drop the frame and carry on rather than propagate. Never used for an unknown message type;
* {@link IrisMessageCodec#decode(byte[])} returns null for that.
* <p>
* Internal to Iris; not a published integration surface.
*/
public class ProtocolException extends Exception {
/**
* @param message what was wrong with the frame, including the byte counts involved where relevant
*/
public ProtocolException(String message) {
super(message);
}