Skip to content

Public API ​

Compile-time dependency ​

xml
<repositories>
  <repository><id>jiuwu-releases</id><url>https://repo.crypticlib.com/repository/maven-public/</url></repository>
</repositories>
<dependency>
  <groupId>emaki.jiuwu.craft</groupId><artifactId>emaki-corelib-api</artifactId><version>4.8.28</version><scope>provided</scope>
</dependency>
kotlin
repositories { maven("https://repo.crypticlib.com/repository/maven-public/") }
dependencies { compileOnly("emaki.jiuwu.craft:emaki-corelib-api:4.8.28") }

Compile only — never install, shade, or relocate this jar

The runtime jar already embeds the same un-relocated API classes. A second copy gives Bukkit events and static bridges different class identities

Narrow third-party facade ​

Use EmakiCoreLibApi.status().usable() before relying on a bridge. This facade is intentionally narrow for third-party plugins; Emaki runtime modules link the CoreLib implementation directly. dialogs() exposes vanilla dialogs, scheduling() exposes Folia-safe scheduling, and implementation services such as rendering, YAML, GUI internals, expressions, lifecycle, economy, and PDC internals remain hidden

The facade also exposes both itemDisplayName overloads, configured-item build/patch overloads, item component capabilities, and owner-scoped stage registration through registerActionStage, registerActionSource, and registerActionGate. There is no source dimension and no unregister-by-id method: each call returns a closeable CoreStageRegistration, and a stage is revoked through that handle or automatically when its owner is disabled. install, uninstall, and Bridge are internal runtime contracts: do not call or implement them

executeActionLineAsync(Plugin, String, CoreActionExecutionContext) is callable from any thread. Its future completes on either the calling thread or the final stage's execution domain — the completion thread is not fixed; schedule through scheduling() before a continuation touches Bukkit. Compilation and business failures complete normally as structured results rather than as exceptional completion

The result is CoreActionExecutionResult, with status, reasonKey/reasonArguments, diagnostics, ordered stages, and keptTargets. Statuses are SUCCESS, SKIPPED, PARTIAL, COMPILE_FAILED, EXECUTION_FAILED, INVALID_REQUEST, and UNAVAILABLE

Read-only registry queries are actionStages() / actionStage(id) and actionTriggers() / actionTrigger(id). onStageRegistryRebuilt(owner, callback) keeps its compatibility replacement semantics: a later callback for the same owner replaces the earlier one. Use addStageRegistryRebuildListener(owner, callback) for independent callbacks from one plugin; callbacks are appended independently and return a closeable CoreStageRebuildRegistration

Animation entry points ​

CoreLib's animation timing infrastructure (definition registration, K-frame scheduling, priority pre-emption and conflict arbitration) is exposed through the same facade. Its contract types live in emaki.jiuwu.craft.corelib.api.animation: AnimationDefinition, AnimationKeyframe, AnimationPriority, AnimationConflictPolicy, AnimationPlaybackHandle, AnimationListener, and AnimationRegistration

  • registerAnimation(Plugin, AnimationDefinition) registers one owner-scoped definition and returns a closeable AnimationRegistration. Re-registering the same id under the same owner replaces the previous definition and stops future playbacks of the old one
  • playAnimationAsync(Plugin, Entity, String, CoreActionExecutionContext) returns a CompletableFuture<EmakiResult<AnimationPlaybackHandle>>. It may be called from any thread and the future's completion thread is not fixed. An unknown definition, a disabled owner, or an AnimationConflictPolicy.REJECT conflict completes with a structured failure. A higher-priority playback pre-empts the current one; equal priorities follow the definition's own conflict policy. Keyframe action lines use the supplied context as their execution input (the playing entity is added as caster when the context has none), and their Bukkit access happens on the entity's owner thread
  • stopAnimation(Plugin, Entity, String) stops this owner's playback of one definition on that entity; a null or blank definitionId stops all of this owner's playbacks on that entity. Returns whether a live playback was actually stopped
  • addAnimationListener(Plugin, AnimationListener) registers a playback listener and returns a closeable AnimationRegistration; registering again for the same owner replaces the previous listener. Its callbacks (onPlay / onKeyframe / onStop) run on the entity's owner thread
  • AnimationPlaybackHandle has the three states PLAYING / FINISHED / CANCELLED; playing() reports whether it is still advancing, and cancel() is idempotent and never throws after the playback has finished or been cancelled
  • Handles must be closed in onDisable; when an owner plugin is disabled CoreLib revokes its definitions, listeners, and in-flight playbacks automatically
  • The animation definition registry survives a CoreLib reload, so no rebuild callback is required. This differs from the action stage table, which is rebuilt on reload and must be replayed through onStageRegistryRebuilt / addStageRegistryRebuildListener (see above)
  • With CoreLib unavailable the registration and listener entry points return a stable inactive handle, playAnimationAsync completes with EmakiResult.unavailable(), and stopAnimation returns false — none of them throw

Readiness contract ​

Every Emaki module publishes its state to CoreLib when its own data finishes loading, when it enters a reload, and after it shuts down. Consumers use this to distinguish "data not ready yet" from "that entry does not exist"

whenReady(Plugin owner, String moduleName, Runnable callback) runs the callback once the target module's data is loaded and returns a closeable ReadinessRegistration:

  • The callback runs once. A reloading module goes back to not-ready and becomes ready again, but an already-fired callback is not replayed. To follow every reload use addModuleListener (see below), or simply re-check at the point of use
  • Re-registering from inside the callback to emulate a standing listener is forbidden: the module is already marked ready when callbacks run, so the re-registration takes the already-ready path, fires synchronously, registers again, and recurses until StackOverflowError
  • Registering the same owner twice adds a second callback rather than replacing the first
  • Pass moduleName as a literal such as "EmakiItem" rather than a constant from that module's API jar, for the same class-loading reason documented on ApiCapability.of(String). Matching is case-insensitive
  • If the module is already ready the callback runs synchronously and the returned handle is inactive. An inactive handle is also returned when EmakiCoreLib is unavailable or the arguments are unusable
  • Thread: callable from any thread. The callback runs on whichever thread marked the module ready, which is not guaranteed to be a Bukkit owner thread. Schedule explicitly before accessing players, inventories, worlds, or GUIs
  • Callbacks registered by an owner plugin are dropped when that plugin is disabled

isModuleReady(String moduleName) is the polling counterpart, meant for diagnostics or a call site that can simply skip its work. Prefer whenReady when the work must run exactly once after readiness

Listening for reloads ​

whenReady answers "has it loaded yet" but not "has it reloaded since". A consumer that caches another module's content needs the latter:

java
EmakiCoreLibApi.addModuleListener(this, "EmakiItem", phase -> {
    switch (phase) {
        case LOADING, ABSENT -> myCache.invalidate();
        case READY -> myCache.rebuild();
    }
});

The three ModuleReadinessPhase values:

PhaseMeaningWhat to do
LOADINGThe module started replacing its dataInvalidate caches now
READYData is loaded and usableRebuild caches. Fires on first load and after every reload
ABSENTThe module was disabledDrop caches. It may be enabled again in the same session, followed by LOADING then READY

How it differs from whenReady:

  • Standing: notified on every transition until the handle is closed
  • Registering the same owner for the same module again replaces the previous listener rather than adding a second one, so a plugin whose onEnable runs twice does not rebuild its cache twice
  • Registering while the module is already ready does not invoke the listener immediately. That immediate call exists to close whenReady's missed-signal window; a standing listener has none. Query isModuleReady if you need the state at registration time
  • Republishing the same state notifies nobody: phases fire only on a real transition. Several modules publish ready from both their synchronous and asynchronous reload paths, and the de-duplication lives inside CoreLib
  • Within one READY, standing listeners run before whenReady's one-shot callbacks, so a consumer holding both has its one-off initialisation read an already-rebuilt cache
  • Thread: same as whenReady — the listener runs on whichever thread published the transition, not guaranteed to be a Bukkit owner thread
  • Listeners are dropped when their owner plugin is disabled. The ABSENT phase does not drop them, because a consumer has no way to notice it would need to re-register

Use this when you only need to know that a reload happened. CoreLib exposes no way for a third party to trigger another module's reload

There is a window between LOADING and the data actually being replaced: modules that reload asynchronously (Item, Attribute, Cooking, Gem, Skills, Strengthen) may briefly still return the old data after LOADING. That is existing timing behaviour, not a guarantee

A module's status().ready() now means "data is loaded", not "components were constructed". During a reload window status() reports loading and methods returning EmakiResult return unavailable(). Plain queries returning Set, List, or boolean have no way to express unavailability, so during a reload they return stale or empty content: do not read an empty result as "no such entry"

Result contract ​

EmakiResult.Success<T> carries a complete non-null value. Partial<T> carries the achieved value and a stable shortfall key. Failure<T> carries no value, a FailureKind, a stable reason key, and immutable placeholders. Use optionalValue() for a value from either Success or Partial; orElse, failureKind, reasonKey, and reasonPlaceholders preserve the same distinction. Void-style operations return EmakiResult<Unit> (EmakiResult.ok() carries Unit.INSTANCE)

FailureKind is exactly: UNAVAILABLE, NOT_FOUND, INVALID_INPUT, REJECTED, CANCELLED, TARGET_OFFLINE, WRONG_THREAD, and INTERNAL_ERROR. REJECTED includes configuration-disabled business features; there is no separate disabled kind