Skip to content

Action Pipeline Extension SDK ​

A third-party plugin registers its own sources, gates, and actions into the Emaki action pipeline so server owners can invoke those stages from any pipeline line. The registration specification covers registration entry points, domain declarations, contract declarations, Folia notes, and verification

The same specification ships with the API jar as javadoc (the package-info of emaki.jiuwu.craft.corelib.api.action and the CoreActionExecutionDomain enum) — hover it in your IDE; the page and that javadoc are kept in sync

Registration entry points ​

All six entry points live on the EmakiCoreLibApi facade, scoped by owner (the calling plugin), and each returns a closeable handle:

Entry pointStage roleHow it appears in a pipeline
registerActionSource(Plugin, CoreActionSource)Produces the target flowAt line start, e.g. <your stage> radius=8
registerActionGate(Plugin, CoreActionGate)Filters / reorders / controls flow... | <your stage> ...
registerActionStage(Plugin, CoreActionStage)Produces an effect... | <your stage> amount=3
registerActionTrigger(Plugin, CoreActionTrigger)Trigger entryInvoked by the trigger mechanism
registerTargetCondition(Plugin, CoreTargetCondition)Target condition typefilter '<your condition>=value'
registerTargetIdentityProvider(Plugin, CoreTargetIdentityProvider)External-system identityIdentity recognition and <system id>_level conditions

No source parameter, no unregister-by-id

A stage is revoked only through the returned handle's close(), or automatically by CoreLib when the owner plugin is disabled. A plugin cannot revoke a stage registered by another plugin. Re-registering the same id (same owner) replaces the old stage

Domain declaration checklist (mandatory) ​

Every source/action must implement executionTarget(CoreStagePlanningContext) and return a CoreActionExecutionTarget with a declared scheduling domain. A stage declared UNDECLARED is refused at registration — on Folia such a stage crashes the server rather than raising a handleable error

CoreActionExecutionDomainMeaningTypical use
SERVER_GLOBALServer global threadServer-wide player lookups, game rules, config writes
CONTEXT_ENTITYThe current target entity's owner threadInventory edits, teleport, glowing, player messages
LOCATION_REGIONLocation/region domainNearby entity search, particles/blocks at the origin
ASYNC_COMPUTEPure async computationMust not require targets (targetRequirement must be NONE, otherwise registration is refused)
UNDECLAREDUndeclaredForbidden

Gates are dispatched through CoreGateThread: PURE (touches no Bukkit state, always safe on Folia, and the default) or NEEDS_ENTITY_READ (reads entity state)

Contract declaration checklist ​

Run this list before registering; each item is checked at registration, and a mistake returns a reasonKey:

  1. id: non-empty, lowercase, no =; registering an id that already exists returns duplicate_id (with the existing owner name)
  2. Domain: see above; async domain + required targets = async_needs_target
  3. Parameter table: CoreStageParameter.required(name, type, desc) / optional(name, type, default, desc) / positional(name, type, desc). At compile time an undeclared argument reports unknown_argument and a missing required one reports missing_required_argument
  4. Target requirement: targetRequirement() is NONE (no target needed, e.g. broadcast) / OPTIONAL / REQUIRED_ENTITY / REQUIRED_LOCATION / REQUIRED_ANY. Declaring REQUIRED_* means calls without a target are skipped (action.run.no_target) — no manual null checks needed
  5. Context keys: declare the CoreActionKeys your stage reads in requiredContext(); the compiler checks them against the trigger's PhaseContract, so a missing key is a load-time error, not a runtime NPE
  6. Argument types: CoreStageParameterType (STRING/INTEGER/DOUBLE/BOOLEAN/DURATION/ENTITY_TYPE/MATERIAL/PERCENTAGE/EXPRESSION/SOUND). Literal (non-placeholder) values are type-checked at compile time (invalid_argument_type)

Folia notes ​

  • All 15 Emaki runtime modules declare folia-supported: true. A stage executes in whichever domain it declared — never assume the main thread: a CONTEXT_ENTITY stage runs on the entity owner thread, a LOCATION_REGION stage on the region-domain thread
  • A domain declaration tightens rather than loosens: the narrower the declaration, the more restricted the Bukkit API you may call; declaring too wide (e.g. an inventory-editing stage as SERVER_GLOBAL) throws on Folia
  • For cross-domain hops use EmakiCoreLibApi.scheduling() — owner-aware global/entity/location/async scheduling; accessing Bukkit from an async body is forbidden
  • Async work a stage starts is not managed by CoreLib; the registering owner plugin must cancel it on disable

Re-register after reload ​

The action stage table is rebuilt when CoreLib reloads, and previously registered stages become invalid. Always attach a rebuild callback after registering:

java
EmakiCoreLibApi.onStageRegistryRebuilt(this, () -> {
    registerMyStages();
});

Use addStageRegistryRebuildListener(owner, callback) when one plugin needs several independent callbacks (each appended independently, returns a closeable handle). The animation definition registry is not rebuilt on reload and needs no callback

Registration timing in onEnable: check EmakiCoreLibApi.status().usable() first; when CoreLib is unavailable the entry points return inactive handles and never throw

How to verify ​

  1. List registered stages: <any plugin root> debug action registry (e.g. /emakicorelib debug action registry) — lists every stage grouped by source/gate/action with the owner plugin name; third-party stages are directly identifiable
  2. Static check: debug action check "<your stage> args" — parse + validate, outputs every diagnostic (with column and candidate suggestions)
  3. Dry run: debug action run shadow "<pipeline line>" — sources/gates evaluate for real, actions only print what would run, stepping through the stage's behavior in the pipeline

Full example ​

A minimal third-party extension skeleton (1 source + 1 gate + 1 action):

java
public final class ExampleExtensionPlugin extends JavaPlugin {

    private CoreStageRegistration sourceHandle;
    private CoreStageRegistration gateHandle;
    private CoreStageRegistration actionHandle;

    @Override
    public void onEnable() {
        if (!EmakiCoreLibApi.status().usable()) {
            getLogger().warning("EmakiCoreLib unavailable, skipping stage registration");
            return;
        }
        registerStages();
        EmakiCoreLibApi.onStageRegistryRebuilt(this, this::registerStages);
    }

    private void registerStages() {
        sourceHandle = EmakiCoreLibApi.registerActionSource(this, new HighLevelPlayersSource());
        gateHandle = EmakiCoreLibApi.registerActionGate(this, new FreshSpawnGate());
        actionHandle = EmakiCoreLibApi.registerActionStage(this, new BroadcastConsoleAction());
    }

    @Override
    public void onDisable() {
        close(sourceHandle);
        close(gateHandle);
        close(actionHandle);
    }

    private static void close(@Nullable CoreStageRegistration handle) {
        if (handle != null) {
            handle.close();
        }
    }
}

Key points for the three stages:

java
final class HighLevelPlayersSource implements CoreActionSource {
    @Override public String id() { return "example_high_level_players"; }
    @Override public String description() { return "Online players at or above a level"; }
    @Override public List<CoreStageParameter> parameters() {
        return List.of(CoreStageParameter.optional("level", CoreStageParameterType.INTEGER, "10", "Minimum level"));
    }
    @Override public CoreActionExecutionTarget executionTarget(CoreStagePlanningContext context) {
        return CoreActionExecutionTarget.global();
    }
    @Override public CoreSourceResult select(CoreStageContext context, CoreResolvedArguments arguments) {
        int min = arguments.getInt("level", 10);
        List<CoreActionSubject> matched = new ArrayList<>();
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (player.getLevel() >= min) matched.add(CoreActionSubject.of(player));
        }
        return matched.isEmpty()
                ? CoreSourceResult.empty("example.source.no_match")
                : CoreSourceResult.selected(matched);
    }
}
  • Gate: implement apply(context, inbound, arguments) and return CoreGateResult.passed(kept flow) / halted(reasonKey) / invalid(reasonKey); pick PURE / NEEDS_ENTITY_READ for threadNeed() depending on whether you read entity state
  • Action: implement execute(context, arguments), act on context.currentTarget(), and return CoreActionOutcome.success() / skipped(reasonKey) / failure(kind, reasonKey); declare the domain matching the Bukkit state you actually change
  • Custom reasonKeys (e.g. example.source.no_match) resolve their text from the registering plugin's own lang/; when unconfigured, DiagnosticRenderer falls back to the raw key instead of throwing

Argument types quick reference ​

TypeCompile-time check (no placeholders)
STRING / SOUNDalways passes
INTEGER / DOUBLEnumber parsing
BOOLEANtrue / false
DURATIONtick/time format (1s, 20t...)
ENTITY_TYPE / MATERIALvanilla enum/material exists
PERCENTAGE0~1 decimal or x/y fraction
EXPRESSIONexpression engine can evaluate

Read arguments through CoreResolvedArguments: getString / getInt / getDouble / getBoolean / getDurationTicks / getChance / getEntityType / getMaterial / getExpression; every accessor has a fallback, and an optional parameter with a declared defaultValue falls back to that default when omitted