mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
🧹
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user