Worker-safe. Plugins extend a gamekit game definition with cross-cutting behavior (chat, emotes, vote-to-kick, spectator pings) without forking the host game. Composition happens at the gamekit-source level viaDocumentation Index
Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
Use this file to discover all available pages before exploring further.
withPlugins(baseDef, plugins), which finalizes to a fully wired GameDefinition ready to pass to the runtime.
Install
What plugins can do
- Declare a state slice that lives at
G.plugins.<id>and rides along on every snapshot the server sends to clients. - Expose moves namespaced as
${pluginID}__${moveName}on the host game’s dispatch surface. - Dispatchable by any seated player regardless of whose turn it is, so chat / votes / pings work off-turn.
withPlugins widens activePlayers to the full seated roster on every phase. Host moves remain turn-gated by their own inline move.invalid("not_your_turn") checks (the same pattern they use without plugins).
Authoring
definePlugin(plugin)
Identity helper that preserves literal types on the plugin’s id, slice, and moves map.
definePluginMove(definition)
Identity helper for a single plugin move. Use when you want explicit <TSlice, TArgs> typing without inlining the move into the plugin’s moves map.
Plugin move outcomes
Plugin moves return one of two outcomes:| Outcome | Effect |
|---|---|
{ kind: "stay", patch?: Partial<TSlice> } | Shallow-merges patch into G.plugins.<id>. Keeps phase and turn unchanged. |
{ kind: "invalid", reason?: string, details?: JsonValue } | Rejects the dispatch; the plugin slice is unchanged. |
Restricting dispatch
Any seated player may dispatch any plugin move. To restrict, perform the check inline inrun and return an invalid outcome:
Composition
withPlugins(base, plugins)
Compose a base gamekit definition with one or more plugins and finalize via defineGame. The returned value is a fully wired GameDefinition — callers do not need to wrap it in defineGame again.
defineGame:
maxPlayersform — pool size, default IDs"0","1", …playerIDsform — named seats, e.g.playerIDs: ["white", "black"] as const
setup, moves, views, etc. — authoring a game with plugins is the same as authoring one without.
Plugin slices live at runtime under G.plugins.<id>; they are not surfaced on the static TState, since doing so would force every host move’s patch to acknowledge plugin keys it never touches. Reach into G.plugins.<id> via the plugin’s own typed accessors / the namespaced dispatch surface, not directly off the host state.
View merging
When the base game declaresviews.player or views.public, withPlugins wraps each view to merge G.plugins into the projection. Plugin slices ride along on every snapshot the client receives. If a base view already produces a plugins field of its own, it is preserved as-is (the plugin merge is skipped).
Errors at composition time
withPlugins throws synchronously when:
- Two plugins share the same
id. - A namespaced plugin move (
${pluginID}__${moveName}) collides with an existing host move name. - The base game’s
setupalready returns apluginskey on its state.
Constants
| Constant | Value | Purpose |
|---|---|---|
PLUGIN_STATE_KEY | "plugins" | Reserved key on the host game’s G where plugin slices live. Authors should avoid using this key in their own state. |
PLUGIN_MOVE_SEPARATOR | "__" | Joiner between a plugin id and a move name when registering on the host gamekit moves map. |
Key types
PluginDefinition<TID, TSlice, TMoves>— full plugin shape:{ id, setup, moves }.PluginMoveDefinition<TSlice, TArgs, TPlayerID>—{ args?, run }for a single plugin move.PluginMoveOutcome<TSlice>—{ kind: "stay", patch? } | { kind: "invalid", reason?, details? }.PluginMoveRunContext<TSlice, TArgs, TPlayerID>— theruncontext:{ G, args, player, rng }.PluginsState<TPlugins>—{ plugins: { [id]: TSlice } }. Intersected with the baseTPublic/TPlayeron the result ofwithPlugins.PluginsSliceMap<TPlugins>— the{ [id]: TSlice }map alone.PluginMoveName<TID, TMoveName>—${TID}__${TMoveName}template literal.AnyPlugin,AnyPluginMoveDefinition— wide types for collections of plugins or moves with mixed parameters.
See also
@openturn/plugin-chat— first-party in-room chat built on this package.- Reference:
@openturn/gamekit— base authoring surface that plugins extend. packages/plugins/src/index.test.ts— the canonical test forwithPluginssemantics.