Public API
Compile-time dependency
<repositories>
<repository><id>emaki-public</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.1</version><scope>provided</scope>
</dependency>repositories { maven("https://repo.crypticlib.com/repository/maven-public/") }
dependencies { compileOnly("emaki.jiuwu.craft:emaki-corelib-api:4.8.1") }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 may complete on the calling thread or in the final stage's execution domain; schedule through scheduling() before a continuation touches Bukkit. Compilation and business failures complete normally as structured results rather than 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.
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 tell "the data is not loaded yet" apart from "that entry genuinely 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. - Do not re-register from inside the callback to emulate a standing listener: 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
moduleNameas a literal such as"EmakiItem"rather than a constant from that module's API jar, for the same class-loading reason documented onApiCapability.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 touching 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:
EmakiCoreLibApi.addModuleListener(this, "EmakiItem", phase -> {
switch (phase) {
case LOADING, ABSENT -> myCache.invalidate();
case READY -> myCache.rebuild();
}
});The three ModuleReadinessPhase values:
| Phase | Meaning | What to do |
|---|---|---|
LOADING | The module started replacing its data | Invalidate caches now |
READY | Data is loaded and usable | Rebuild caches. Fires on first load and after every reload |
ABSENT | The module was disabled | Drop 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
onEnableruns 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. QueryisModuleReadyif 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 beforewhenReady'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
ABSENTphase 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 may 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.