Skip to main content

Crate rift_plugin

Crate rift_plugin 

Source
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 plugin world, 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. target is 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 _json host functions, Action::deny_json, Action::message_json, TitleJsonSpec, RichSuggestion::tooltip and the other -json fields accept.
timers
Timers fire through Plugin::timer with the returned handle.

Macros§

export
Exports a Plugin implementation as the component’s rift:plugin/handler interface. Use it exactly once per plugin crate.

Traits§

Plugin
A Rift plugin. Implement the hooks you need and pass the type to export!.

Functions§

config
Decodes the JSON configuration init and reload receive.

Type Aliases§

Action
The answer to a decision hook: the contract’s decision variant, which is action plus DenyJson and MessageJson for rich text (since 0.1.2).