Skip to content

Platform Specification ​

Who is this directory for? It targets developers who want to implement a Yeow-compatible runtime (e.g., writing a runtime bridge for another platform). It describes the protocol layer between plugin JS code and the runtime: package structure, loading flow, message channel formats, task types, event data, and Native Service communication.

Regular plugin developers do not need to read this directory. For day-to-day development, use the API Reference — yeow-api encapsulates all underlying protocols.


Directory Structure ​

SubdirectoryContent
messageMessage formats for non-dispatcher channels (timer / fs / http / assets / service / debug, etc.)
taskDispatcher task type catalog (request/response formats for player.get, world.setBlock, etc.)
eventEvent subscription mechanism and data fields for each event type
native-serviceNative Service subprocess protocol (TCP framed protocol: header JSON + raw body)
runtimeRuntime mechanics (JS environment, callback system, global variables, event loop)
values.mdValue Appendix: format rules (R1–R5) and listings — platform-specific enums maintained directly (gamemode/difficulty/BossBar/scoreboard/ClickType/ItemFlag/InventoryType, etc.) + reference implementations (non-normative: DamageCause/teleport cause/regen cause); version-varying domains (blocks/items/entities/biomes/sounds/particles/enchantments/potions/attributes/damage types/game rules/translation keys/advancements/recipes) with rules and authoritative links

Protocol Overview ​

JS → Runtime: The specification defines $send(channel, t, p) — payload is a pure JS object (the runtime handles serialization and transmission). $_send, the underlying communication format (not limited to JSON), and the message loop are all internal implementation details that implementors may decide at their own discretion (see Internal Implementation Boundaries).

Runtime → JS: The callback delivery format (e.g., $hm(t="cb", p="<callbackId>",r=<data>)) is defined by the implementor and dispatched by the JS-side bootstrap script.

Lifecycle Messages: INIT / LOAD / DISABLE / RELOAD, handled by the bootstrap script.

Detailed formats are documented in each subdirectory.


Internal Implementation Boundaries ​

The Yeow Platform Specification only defines behavior at the contract level. The following are not constrained by the specification and may be decided by the implementor:

  • $_send: The underlying raw JS→runtime bridge function is an internal implementation detail — the specification only guarantees the semantics of $send(channel, t, p) (accepting pure JS objects)
  • Communication Format: The transport format between JS and the runtime is not limited to JSON (e.g., binary, structured clone, etc. may be used at the implementor's discretion); examples in the specification are presented in JSON
  • Message Loop: The JS-side message dispatch/event loop mechanism is an internal implementation detail (e.g., blocking wait, polling, event-driven); the specification only requires semantic correctness (messages are eventually processed, callbacks are delivered, lifecycle events are triggered)

Plugin code may only depend on $send and its return value semantics. It must not depend on $_send, message loop timing, or specific transport formats.

Yeow Plugin Package Structure ​

A Yeow plugin is a ZIP archive (deployed as .jar on Paper-based servers, or .yeow.zip when placed in the plugin directory — both are fundamentally ZIP files). The runtime only needs to read the ZIP and does not depend on a Java environment.

my-plugin.jar / my-plugin.yeow.zip (ZIP)
├── yeow.json              ← 插件元信息 + 权限声明(必需)
├── .yeow/
│   ├── main.js            ← esbuild 打包的插件代码(IIFE 格式,生产构建)
│   └── dev.json           ← 开发模式信息(仅 dev 构建,见下)
├── assets/                ← 打包资源(按命名空间 id 分目录)
└── plugin.yml             ← 宿主平台元信息(Paper 系需要;`.yeow.zip` 与纯平台实现可忽略)

.yeow.zip and JAR behave identically: The runtime registers them using the same logic (read yeow.json → permissions → code → startup). Placing them in the runtime data directory (official Paper implementation uses plugins/Yeow/) triggers automatic scanning and loading. They can also be loaded manually via /yeow load <path|name> (when the path is not found, match <name>-<version>.yeow.zip under plugins/Yeow/). Only one instance per plugin name is allowed; duplicate loads are rejected with a warning.

yeow.json — Plugin Metadata ​

json
{
    "name": "my-plugin",
    "version": "1.0.0",
    "author": "",
    "description": "A Yeow plugin",
    "api": "1.18",
    "java": 21,
    "permissions": ["fs:server.*", "http:requestAsync", "service:registerNative"]
}
FieldDescription
namePlugin name (injected as __plugin.name; only one instance per plugin name is allowed)
versionVersion (injected as __plugin.version)
authorAuthor (injected as __plugin.author)
api / javaAPI/Java version required by the host platform (may be ignored for other platforms)
permissionsDeveloper-declared permissions (sensitive nodes, see Permission Model below)
computedPermissionsFinal effective permissions computed at build time (merged + wildcard normalization); read by the runtime (v0 phase is incompatible with legacy package formats)
nativeNative service trustworthiness declaration (mandatory; single-file mode only): at build time merges the main project + dependency packages and computes SHA-256 under each namespace, written as [{ "serviceId": "...", "files": [{ "<path-after-packaging>": "<sha256>" }, ...], "source": "..." }]

.yeow/main.js — Plugin Code ​

An IIFE bundled by esbuild ("use strict"; (() => {...})()), with bundle: true, target: esnext. The runtime does not parse modules — it simply evaluates the entire file. When plugin code executes, it registers callbacks (onInit/onLoad/onUnload, _registerCallback, etc.) without performing game operations.

.yeow/dev.json — Development Mode (Optional) ​

json
{
    "name": "my-plugin",
    "codeFile": "/abs/path/to/dist/.dev/main.js",
    "assetsDir": "/abs/path/to/dist/.dev/.assets"
}

Only present in development builds. When the runtime detects dev.json, it should:

  • Read plugin code from codeFile (absolute file system path) instead of .yeow/main.js inside the JAR
  • Read assets resources from assetsDir (file system directory) instead of from inside the JAR

assets/ — Bundled Resources ​

At build time, each dependency (the main project and qualifying npm packages) is assigned a unique namespace id (8-character hex, not a content hash) for its assets/ directory. Contents are copied as-is into assets/<id>/ — files are not renamed via hashing, and relative references within assets/ (including cross-directory) remain valid at all times.

The JS side obtains the namespace-prefixed path via getAssetsPath() (e.g., "assets/a1b2c3d4/icon.png"). The runtime looks up this path under the JAR's assets/ directory and must not apply any additional transformation to the path.


Loading Flow ​

The standard flow for a runtime loading a Yeow package:

1. Read yeow.json          → Plugin metadata (name/version/author) and permission declarations
2. Duplicate name check    → Plugin name already exists → Reject load and warn (same for all loading paths)
3. Read plugin code:
     Has .yeow/dev.json → Read from dev.json.codeFile (development mode)
     Otherwise          → Read from .yeow/main.js (production)
4. Create JS context (isolated, plugins are sandboxed from each other)
5. Inject global functions (see "JS Runtime Injections" below)
6. evaluate(bootstrap script)   → Define $send, _registerCallback, console, $hm, etc.
7. evaluate(plugin code)        → Register onInit/onLoad/onUnload, commands, events
8. Call $hm("INIT", null, null)   → Trigger onInit callback (not queued, before all messages)
9. Start message loop
10. Deliver $hm("LOAD", null, null) → Trigger onLoad callback (game operations are now available)

Loading Sources (all behave identically, following the above flow):

  • Template JAR registration (host platform plugin mechanism, e.g., Paper's depend)
  • Data directory auto-scan (plugins/Yeow/*.yeow.zip, at startup)
  • Admin commands: load <path|url> (temporary), install <url> (download and save in standard format to data directory), update <url> (replace old package of the same name, old package moved to .backup/)

.yeow.zip takes priority: Admin commands treat .yeow.zip as the primary target. When a template JAR and .yeow.zip with the same plugin name both exist, a conflict warning is generated (duplicate load is rejected), and one must be manually removed.

Load Message: When a plugin loads successfully, a load message is printed containing the plugin name, version, and permission declarations.

Permission Model ​

Sensitive message nodes are denied by default; plugins must declare them in computedPermissions in yeow.json:

Node (may be omitted)Covers message operations
fs:server.*fs channel server prefix nodes (server root directory, e.g., fs:server.readFile); fs:plugin.* nodes (plugin data directory) need no declaration, allowed by default
fs:outer.*fs channel outer prefix nodes (arbitrary paths, e.g., fs:outer.readFile)
http:*All http channel operations (request/requestAsync/listen/respond/close)
service:registerNativeregisterNative on the service channel (spawn subprocess)

The assets channel has no permission interception: it only reads bundled resources or extracts to the plugin's own data directory (target is forcibly constrained).

Rules:

  • Node concept: Permissions are only considered by message node (channel:node). Segments in the node name (e.g., plugin in fs:plugin.readFile, player in task:player.get) are business/access scope names, not hierarchy, and do not participate in permission matching
  • Node matching: Exact node (fs:server.readFile); group wildcard fs:server.* matches all nodes under that prefix; channel wildcard fs:* matches all nodes in the fs channel — at build time, fs:* is automatically expanded in computedPermissions to fs:outer.*, fs:server.* (semantically equivalent)
  • Default allowed: Nodes outside the above default-deny nodes (e.g., service:request, service:register, service:info, service:unregister, assets:read, fs:plugin.readFile) need no declaration
  • Denial behavior: Undeclared calls return error Permission denied: <node>. Synchronous calls return error JSON directly; asynchronous calls (including those with cb) deliver {"err":"Permission denied: <node>"} via callback, manifesting as a Promise reject on the JS side
  • Other channels (task/timer/log/env/debug/lifecycle) are not constrained by the permission model
  • Permissions are read and fixed at plugin load time (cannot be changed at runtime); the load message prints the declared content — when printing, fs:* is expanded to fs:outer.*, fs:server.* (display only, to help server administrators understand the scope; permission checks still use the original value fs:*)

computedPermissions semantics: Plugin authors and dependency packages declare permissions in their respective yeow.config.json's permissions; at build time, these are merged (deduplicated + wildcard normalization — X:* supersedes X:..., X:segment.* supersedes X:segment.<op>; fs:* expands to fs:outer.*, fs:server.*) and written to computedPermissions in yeow.json. The runtime reads this field. The runtime only verifies wildcard/node matching and does not need to understand the meaning of node name segments.

Lifecycle Message Semantics:

MessageTrigger TimingJS-side Handling
INITAfter context ready, before message loopExecute __yeowInitCbs (dispatcher not yet started, game operations not available synchronously)
LOADAfter dispatcher readyExecute __yeowLoadCbs (game operations available synchronously)
DISABLEPlugin is disabledExecute __yeowUnloadCbs → After receiving unloadDone, close thread
RELOADHot reloadExecute __yeowUnloadCbs → After receiving unloadDone, destroy old context, load new code

Runtime Architecture ​

A compliant runtime contains the following components:

┌────────────────────────────────────────────────────┐
│                    Runtime                          │
│                                                    │
│  ┌──────────────┐   ┌──────────────────────────┐  │
│  │ Plugin 1     │   │ Plugin 2                 │  │
│  │ JS Context   │   │ JS Context               │  │
│  │ + Msg Loop   │   │ + Msg Loop               │  │
│  └──────┬───────┘   └──────┬───────────────────┘  │
│         │                  │                      │
│  ┌──────┴──────────────────┴───────────────────┐  │
│  │ Dispatcher (optional but recommended)        │  │
│  │ Three-level queue HIGH/NORMAL/LOW + timeslice │  │
│  └──────┬──────────────────────────────────────┘  │
│         │                                         │
│  ┌──────┴──────────────────────────────────────┐  │
│  │ Executor (task type → host platform ops)     │  │
│  │ Event bridge (subscribe/trigger/complete)     │  │
│  │ Command bridge (register/execute/complete)    │  │
│  │ Channel impl (fs/http/assets/service/timer...)│  │
│  └─────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────┘

Threading Model (recommended):

ThreadResponsibility
Main threadDispatcher tick (game tasks executed serially), event/command bridge
JS thread (per plugin)Plugin code + message loop, directly handles non-game operations like fs/http/assets
Timer thread (per plugin)setTimeout/setInterval expiration callback delivery
IO threadAsync fs/http operations

Key Principle: JS logic is separated from game operations — plugin code does not block the game main thread; game tasks are serialized through the dispatcher to avoid race conditions.


JS Runtime Injections ​

Before evaluating plugin code, the runtime must inject the following into globalThis (see Runtime Environment Standard for details):

Native Layer Injections (runtime's language host implementation) ​

GlobalSignatureDescription
__plugin{ name, version, author }From yeow.json, read-only
$devbooleanDevelopment mode flag

The native layer only needs to ensure the bootstrap script can implement $send semantics; underlying bridge functions (like $_send) are internal implementation details and are not listed here.

Bootstrap Script Layer (runtime's built-in JS bootstrap script) ​

The runtime must include a built-in bootstrap script (equivalent to init.js), executed before evaluating plugin code, defining:

GlobalDescription
$send(channel, t, p)JS→runtime entry point, accepts pure JS objects (underlying serialization at implementor's discretion)
_registerCallback(fn, opts?) / _unregisterCallback(id)Callback registry (core async primitive, format cb_N)
console.log/warn/error/infoLogging, automatically prefixed with [plugin name]
setTimeout / setInterval / clearTimeout / clearIntervalTimers (via timer channel)
fetch(url, opts?)HTTP client (via http channel)
_getCurrentCbStack()Development mode stack trace helper
reportError(e)Error reporting (via debug channel)

Message Loop (internal implementation, semantic requirements):

Loop:
  1. Pull next message from runtime (may block-wait — message-driven, no CPU usage when no messages)
  2. Call message dispatcher to process (callback / lifecycle)
  3. Drain microtask queue (Promise.then / FinalizationRegistry callbacks)
  4. If __yeowGcQueue is non-empty → Send lifecycle gc-collect
  5. Messages still in queue → Continue processing; no messages → Return to blocking wait

Task Executor / Event Listener / Command Executor ​

The complete contracts for the three major execution components (request/response formats, execution rules, sync/async semantics, event completion, command completion) are documented in their respective dedicated articles:

Native Service ​

Plugins can include native programs (Go/Rust/C++, etc.) and invoke them via the service channel. See Native Service Specification for details. Key points:

  • Binaries are placed in assets/ (namespace injected via getAssetsPath()), single file only (directory mode has been removed; binaries must be self-contained)
  • The native field must be declared in yeow.config.json (serviceId + files): at build time SHA-256 is computed per namespace and stored as plugin metadata at load; undeclared / hash mismatch → refused
  • registerNativeService extracts and spawns a subprocess based on platform (os + arch)
  • The subprocess communicates with the runtime via a TCP framed protocol (header JSON + raw body) for ready / request / response / publish

Compliant Runtime Checklist ​

Implementing a Yeow-compatible runtime requires handling:

  • [ ] Package structure parsing: Read ZIP (yeow.json, .yeow/main.js, assets/; optional dev.json), JAR and .yeow.zip are structurally identical
  • [ ] Unique name enforcement: Reject load and warn on plugin name conflict (same behavior for auto-scan / commands / host mechanism)
  • [ ] Permission model: Parse yeow.json computedPermissions; unify the gate for all message nodes (task:* is owned by default; fs:server.*, fs:outer.*, http:*, service:registerNative are denied by default; fs:plugin.* needs no declaration; assets is allowed by default, with the extraction target constrained to the plugin data directory); Workers may layer allow/deny (deny wins, cannot escalate); undeclared calls return Permission denied: <node>
  • [ ] Load message: Output load message on successful plugin load (includes plugin name, version, permission declarations)
  • [ ] JS engine: ES2025+ (Sec-Uint8Array), supports Promise/WeakRef/FinalizationRegistry/Uint8Array
  • [ ] Native injections: __plugin, $dev (underlying bridge like $_send is internal, not part of the spec)
  • [ ] Bootstrap script: $send, _registerCallback/_unregisterCallback, console, timers, fetch, $hm
  • [ ] Message loop: Pull message → $hm dispatch → Microtask → GC queue drain
  • [ ] Lifecycle: INIT / LOAD / DISABLE / RELOAD, unloadDone acknowledgment
  • [ ] Callback system: cb_N registry, persistent semantics, $hm(t="cb", "p","r") delivery
  • [ ] Dispatcher (recommended): Three-level priority, timeslice budget, optional auto-degrade/idle spin
  • [ ] Task executor: task type → host platform operation, sync/async semantics
  • [ ] Event bridge: subscribe/unsubscribe, event data extraction (basic types only), event.complete application
  • [ ] Command bridge: register/execute/tabComplete
  • [ ] Channel implementations: timer / fs / http / assets / service / log / env / dir / debug / lifecycle
  • [ ] Resource access: Read JAR resources via assets/<namespace-id>/
  • [ ] Error handling: JS exception capture, debug channel reporting
  • [ ] Health check (recommended): debug ping-pong heartbeat, callback timeout warnings
  • [ ] Hot reload (development): RELOAD → unloadDone → new context → new code; production unload uses the same logic (5s forced termination)