Expand description
Guest SDK for Rift WebAssembly plugins.
A Rift plugin is a WebAssembly component built for wasm32-wasip2 against
the plugin world of rift:plugin@0.1.5. This crate ships that WIT,
generates the bindings and wraps them, so a plugin is an ordinary
cdylib crate:
[lib]
crate-type = ["cdylib"]
[dependencies]
rift-plugin = "0.1"use rift_plugin::{Action, CommandSpec, Context, Hook, Plugin, Registration, SuggestKind};
#[derive(Default)]
struct Greeter;
impl Plugin for Greeter {
fn init(&mut self, _config: &str) -> Result<Registration, String> {
Ok(Registration::new()
.command(CommandSpec::new("greet").permission("greeter.use").suggest(SuggestKind::Players))
.hooks([Hook::Command]))
}
fn command(&mut self, ctx: Context) -> Action {
Action::message(format!("Hi, {}!", ctx.name))
}
}
rift_plugin::export!(Greeter);Build it with cargo build --release --target wasm32-wasip2. Rust’s
linker for that target emits a component directly; no cargo component
or wasm-tools component new step is needed.
§State
The component model calls exports as free functions, so the SDK keeps the
single plugin instance for you: export! stores one value of your type
in a thread-local, created with Default right before
Plugin::init, and every hook receives it as &mut self. Keep plugin
state in your struct’s fields. This is sound because guests are
single-threaded and the host never calls one instance concurrently or
re-entrantly. Plugin::shutdown runs last and the instance is dropped
after it returns.
§Hooks
Every Plugin method has a default that does nothing (continue,
default, none or an empty list). List the hooks you implement in
Registration::hooks so the host skips the rest; an empty list delivers
every hook.
§Host API
players, store, permissions, bus, servers, timers,
log and meta mirror the WIT interfaces of the same names. Errors
are the host’s message as a String. log also installs a ::log
logger before init, so log::info! and friends reach Rift’s log.
Plugin configuration arrives as JSON; decode it with config().
§Dialogs
players::show_dialog shows a dialog (Minecraft 1.21.6+) in the
vanilla JSON format. A button whose custom or dynamic/custom action
has an id without a namespace comes back to Plugin::dialog:
use rift_plugin::{Context, DialogResponse, Plugin, players};
use serde_json::json;
#[derive(Default)]
struct Rules;
impl Plugin for Rules {
fn join(&mut self, ctx: Context) {
let _ = players::show_dialog(&ctx.uuid, &json!({
"type": "confirmation",
"title": "Server rules",
"body": "Be nice.",
"yes": {"label": "Accept", "action": {"type": "custom", "id": "accept"}},
"no": {"label": "Leave", "action": {"type": "custom", "id": "leave"}},
}));
}
fn dialog(&mut self, response: DialogResponse) {
if response.action == "leave" {
let _ = players::kick(&response.ctx.uuid, "You declined the rules.");
}
}
}The dialog functions are new in rift:plugin 0.1.1. A plugin that calls
them needs a Rift that implements 0.1.1; older hosts refuse to load it.
A plugin that does not call them loads on every 0.1 host. The same holds
for the rich text functions of 0.1.2 and players::title_json of 0.1.4
below.
WASI 0.2 (std::fs on the granted directories, clocks, random) works
through the standard library. A wasi:http helper is not provided.
§Rich text
Messages, action bars, kicks and deny reasons are plain text by default.
For colours, line breaks, hover text and click events, pass a JSON text
component to the _json forms: Action::deny_json,
Action::message_json, players::message_json,
players::action_bar_json, players::kick_json and
players::broadcast_json. Rift spells hover and click events the way
each player’s client reads them, so either the 1.21.5+ form
(click_event, hover_event) or the older one (clickEvent,
hoverEvent) works for every client.
use rift_plugin::{Action, players};
use serde_json::json;
let invite = json!([
{"text": "Alex invited you to their party. "},
{"text": "[Accept]", "color": "green",
"click_event": {"action": "run_command", "command": "/party accept Alex"},
"hover_event": {"action": "show_text", "value": "Join Alex's party"}},
]);
players::message_json("Sam", &invite.to_string()).ok();
let banned = Action::deny_json(json!({
"text": "You are banned\n", "color": "red",
"extra": [{"text": "Reason: spam", "color": "gray"}],
}));The text builders write that JSON for you: text::mini for Rift
markup (<red>, <click:...>, see docs/text.md), text::legacy for
&c codes and text::Component for a component field by field. Markup
is only ever read from an explicit {"mini": ...} object, which
text::mini writes; a bare string is always shown as written, so text
from players can be passed safely as an argument:
use rift_plugin::{Action, TitleJsonSpec, players, text};
let welcome = text::mini("<gold>Welcome, <player>!</gold> <gray><click:run_command:/spawn>[Spawn]")
.arg("player", "Steve");
players::message_json("Steve", &welcome.to_string()).ok();
players::title_json(
"Steve",
&TitleJsonSpec::new(text::mini("<green>Lobby")).subtitle(text::legacy("&7Have fun")),
)
.ok();
let reply = Action::message_json(
text::Component::text("[Accept]")
.color("green")
.bold()
.run_command("/party accept Alex")
.hover_text("Join Alex's party"),
);§Rich suggestions and server-list text
Plugin::suggest_rich and Plugin::status_rich answer suggest and
status with JSON tooltips, version text and player-sample lines (since
0.1.4, through the optional styled export). Their defaults call
Plugin::suggest and Plugin::status, so implement whichever form
you need. Hosts before 0.1.4 call the plain hooks; the SDK then shows the
rich text as its words.
§Chat
Plugin::chat sees every chat message, and every command that reaches
the backend, before the backend does, and may drop it (since 0.1.5, on
Rift hosts that implement 0.1.5). It is the one hook a plugin opts into
when it is exported, with chat after the type:
use rift_plugin::{Action, ChatAttempt, ChatKind, Plugin};
#[derive(Default)]
struct Mute {
muted: Vec<String>,
}
impl Plugin for Mute {
fn chat(&mut self, attempt: ChatAttempt) -> Action {
if attempt.kind == ChatKind::Message && self.muted.contains(&attempt.ctx.uuid) {
return Action::Deny("You are muted.".into());
}
Action::Continue
}
}
rift_plugin::export!(Mute, chat);Without chat in export! the component has no chat export and Rift
never asks it. With it, Rift asks the plugin about every message of every
player, whatever Registration::hooks lists, and refuses to start the
plugin unless the proxy sets authentication.signed_chat = false:
dropping one signed message would break the player’s signature chain.
Hosts before 0.1.5 load such a component and never call the hook.
Re-exports§
pub use bindings::rift::plugin::types::BossBarSpec;pub use bindings::rift::plugin::types::BusMessage;pub use bindings::rift::plugin::types::ChatAttempt;pub use bindings::rift::plugin::types::ChatKind;pub use bindings::rift::plugin::types::CommandSpec;pub use bindings::rift::plugin::types::Connection;pub use bindings::rift::plugin::types::Context;pub use bindings::rift::plugin::types::DialogResponse;pub use bindings::rift::plugin::types::Hook;pub use bindings::rift::plugin::types::HttpRequest;pub use bindings::rift::plugin::types::HttpResponse;pub use bindings::rift::plugin::types::Level;pub use bindings::rift::plugin::types::Ping;pub use bindings::rift::plugin::types::PlayerInfo;pub use bindings::rift::plugin::types::Registration;pub use bindings::rift::plugin::types::RichStatusOverride;pub use bindings::rift::plugin::types::RichSuggestion;pub use bindings::rift::plugin::types::RiftEvent;pub use bindings::rift::plugin::types::RouteDecision;pub use bindings::rift::plugin::types::StatusOverride;pub use bindings::rift::plugin::types::SuggestKind;pub use bindings::rift::plugin::types::Suggestion;pub use bindings::rift::plugin::types::TitleJsonSpec;pub use bindings::rift::plugin::types::TitleSpec;
Modules§
- bindings
- The raw generated bindings for the
pluginworld, including WASI. - bus
- The message bus. Subscriptions deliver to
Plugin::message. - log
- Rift’s log stream, prefixed with the plugin name by the host.
- meta
- Plugin metadata and clocks.
- permissions
- Rift’s permission store: user nodes and group memberships.
- players
- The joined players.
targetis a name, a UUID or a connection id. - servers
- Managed servers, through the
rift.control.*operations. Results are the control service’s JSON replies. - store
- The durable key-value store. Namespaces are prefixed with
plugin.<name>.by the host. - text
- Builders for rich text: each writes the JSON that the
_jsonhost functions,Action::deny_json,Action::message_json,TitleJsonSpec,RichSuggestion::tooltipand the other-jsonfields accept. - timers
- Timers fire through
Plugin::timerwith the returned handle.
Macros§
- export
- Exports a
Pluginimplementation as the component’srift:plugin/handlerinterface. Use it exactly once per plugin crate.
Traits§
Functions§
- config
- Decodes the JSON configuration
initandreloadreceive.
Type Aliases§
- Action
- The answer to a decision hook: the contract’s
decisionvariant, which isactionplusDenyJsonandMessageJsonfor rich text (since 0.1.2).