Skip to content

Phira-mp+ Plugin Development Documentation ​

Phira-mp+ supports WASM plugins — compiled to WebAssembly and dynamically loaded via wasmtime.

The built-in plugin system (NativePlugin trait) has been merged into the server core code, and independent native plugin registration is no longer supported. For external extensions, please use WASM plugins.

WASM Plugins ​

WASM plugins are dynamically loaded via the wasmtime runtime and deployed in the plugins/ directory (can be customized with the -d parameter).

JSON ABI ​

WASM plugins communicate with the host via JSON strings:

Exported Functions:

FunctionSignatureDescription
phira_init() -> i32Initialize the plugin, returns 0 for success
phira_get_info() -> ()Write plugin metadata to memory offset 0
phira_cleanup() -> ()Clean up when plugin is unloaded
phira_on_event(ptr: i32, len: i32) -> i32Event handler, returns 0=handled 1=not handled
phira_alloc(size: i32) -> i32Allocate linear memory, returns pointer
phira_dealloc(ptr: i32, size: i32)Free memory

Imported Functions (provided by host):

FunctionDescription
phira:host/log(level_ptr, level_len, msg_ptr, msg_len)Log a message
phira:host/uuid(out_ptr, out_len)Generate UUID v4
phira:host/time() -> i64Get Unix timestamp (milliseconds)
phira:host/api(method_ptr, method_len, args_ptr, args_len, out_ptr, out_len) -> i32General API bridge

General API (phira:host/api) ​

All methods are called with JSON parameters and return JSON results. Return codes: 0=success, non-zero=error.

MethodParametersReturnsDescription
state.query{ method, params }Query resultUnified state query entry point
player.touches{ user_id }Recent touch dataQuery user's recent touch frames
player.judges{ user_id }Recent judgment dataQuery user's recent judgment events
round.data{ round_uuid, player_id }Full Touches/JudgesRound data query
round.list{}Round listAll recorded rounds
send.to_user{ user_id, message }"ok"Send a message to a specific user
send.to_room{ room_id, message }"ok"Broadcast a message to a room
send.to_all{ message }"ok"Broadcast to all users
ext.get_user{ user_id, key }Field valueGet user extension data
ext.set_user{ user_id, key, value }"ok"Set user extension data
ext.get_room{ room_id, key }Field valueGet room extension data
ext.set_room{ room_id, key, value }"ok"Set room extension data
room.kick{ room_id, target_id }{"ok": true}Kick a user from a room
room.transfer_host{ room_id, target_id }{"ok": true}Transfer room host
room.set_lock{ room_id, locked }{"ok": true}Lock/unlock a room
room.close{ room_id }{"ok": true}Disband a room
admin.kick_user{ user_id, reason }{"ok": true}Kick a user from the server
admin.ban_user{ user_id, reason }{"ok": true}Ban a user
admin.unban_user{ user_id }{"ok": true}Unban a user
admin.is_banned{ user_id }{"banned": bool}Check ban status
admin.ban_list{}Ban listGet all bans
admin.list_users{}User listList all online users
plugin.api_call{ plugin, method, args }Call resultCall an API registered by another plugin
plugin.api_register{ method }Registration confirmationRegister this plugin's API for other plugins to call
config.get{ key }Config valueGet plugin configuration
config.set{ key, value }"ok"Set plugin configuration
http.get{ url }Response bodyHTTP GET request
http.post{ url, body, content_type }Response bodyHTTP POST request
file.read{ path }File contentRead a plugin data file
file.write{ path, content }"ok"Write a plugin data file
uuid.v4{}UUID stringGenerate a UUID
time.now{}Unix timestampGet the current time

Full WIT definitions can be found in wit/phira/mpplus.wit.

Plugin Events ​

WASM plugins receive events via phira_on_event, with events passed in JSON format. The type field identifies the event type:

typeData FieldsTrigger Condition
user_connectuser_id, user_name, user_ipAfter user authentication succeeds
user_disconnectuser_id, user_nameUser disconnects
room_createuser_id, room_idRoom created
room_joinuser_id, room_id, is_monitorJoined a room
room_leaveuser_id, room_idLeft a room
room_modifyuser_id, room_id, dataRoom settings modified
game_startuser_id, room_idGame started
game_enduser_id, user_name, room_id, score, accuracy, perfect, good, bad, miss, max_combo, full_comboPlayer submitted score
player_touchesuser_id, room_id, dataPlayer touch event
player_judgesuser_id, room_id, dataPlayer judgment event
round_completeroom_id, chart_id, chart_nameA round of the game completed

Example — listening for user connection events:

javascript
// In phira_on_event:
function phira_on_event(event_ptr, event_len) {
  let json = memory_to_string(event_ptr, event_len);
  let event = JSON.parse(json);
  if (event.type === 'user_connect') {
    console.log(`User ${event.user_name}(${event.user_id}) connected from ${event.user_ip}`);
  }
  return 0; // 0 = handled
}

Plugin Metadata ​

WASM plugins can declare metadata in two ways:

  1. Export phira_get_info — The plugin writes JSON metadata to linear memory itself
  2. Memory offset 0 — Place a length-prefixed JSON at offset 0 of the WASM module (compatibility mode)
json
{
  "name": "my-plugin",
  "version": "0.1.0",
  "author": "me",
  "description": "My WASM plugin"
}

Inter-Plugin API Calls ​

WASM plugins can call APIs registered by other plugins via plugin.api_call:

javascript
// Call the count method of the playtime-tracker plugin
let result = api.call('plugin.api_call', {
  plugin: 'player-tracker',
  method: 'count',
  args: []
});
// result = {"count": 42}

Register your own API via plugin.api_register:

javascript
api.call('plugin.api_register', {
  method: 'my_custom_api'
});

Note: Full bidirectional WASM callbacks are currently stubs; registered APIs can be called but return fixed responses. Natively registered Rust APIs (via PluginManager::register_plugin_api) can be called normally by WASM plugins.

Real-time Data Streams ​

Touches/Judges data is pushed to the WASM plugin's phira_on_event handler via player_touches and player_judges events, with no need for active polling.

Building WASM Plugins ​

WASM plugins must be compiled for the wasm32-unknown-unknown target:

bash
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

Place the generated .wasm file in the server's plugins/ directory. It will be loaded on the next startup or when plugin reload is executed.

Server Configuration ​

WASM plugins can read/write their own configuration via the config.get / config.set API (in-memory, lost on restart). For persistent configuration, it is recommended to use file.read / file.write operations on the data/plugins/<plugin_name>/ directory.

WIT Interface Definitions ​

Complete WIT interface definitions can be found in wit/phira/mpplus.wit, including the following interfaces:

  • user-events — User event listening
  • user-info — User info querying
  • room-info — Room info querying
  • messaging — Message sending
  • room-management — Room management
  • user-management — User management
  • utilities — Utility functions
  • database — Database interface (reserved)
  • plugin-config — Plugin configuration
  • plugin — Plugin main entry
  • cli — CLI command interface

Last modified byFireflyF09on2026-06-26 15:41

Built with VitePress