Skip to main content

rift_plugin/
lib.rs

1//! Guest SDK for Rift WebAssembly plugins.
2//!
3//! A Rift plugin is a WebAssembly component built for `wasm32-wasip2` against
4//! the `plugin` world of `rift:plugin@0.2.0`. This crate ships that WIT,
5//! generates the bindings and wraps them, so a plugin is an ordinary
6//! `cdylib` crate:
7//!
8//! ```toml
9//! [lib]
10//! crate-type = ["cdylib"]
11//!
12//! [dependencies]
13//! rift-plugin = "0.2"
14//! ```
15//!
16//! ```no_run
17//! use rift_plugin::{Action, CommandSpec, Context, Hook, Plugin, Registration, SuggestKind};
18//!
19//! #[derive(Default)]
20//! struct Greeter;
21//!
22//! impl Plugin for Greeter {
23//!     fn init(&mut self, _config: &str) -> Result<Registration, String> {
24//!         Ok(Registration::new()
25//!             .command(
26//!                 CommandSpec::new("greet")
27//!                     .alias("hi")
28//!                     .permission("greeter.use")
29//!                     .suggest(SuggestKind::Players),
30//!             )
31//!             .hooks([Hook::Command]))
32//!     }
33//!
34//!     fn command(&mut self, ctx: Context) -> Action {
35//!         Action::message(format!("Hi, {}!", ctx.name))
36//!     }
37//! }
38//!
39//! rift_plugin::export!(Greeter);
40//! ```
41//!
42//! Build it with `cargo build --release --target wasm32-wasip2`. Rust's
43//! linker for that target emits a component directly; no `cargo component`
44//! or `wasm-tools component new` step is needed.
45//!
46//! # State
47//!
48//! The component model calls exports as free functions, so the SDK keeps the
49//! single plugin instance for you: [`export!`] stores one value of your type
50//! in a thread-local, created with [`Default`] right before
51//! [`Plugin::init`], and every hook receives it as `&mut self`. Keep plugin
52//! state in your struct's fields. This is sound because guests are
53//! single-threaded and the host never calls one instance concurrently or
54//! re-entrantly. [`Plugin::shutdown`] runs last and the instance is dropped
55//! after it returns.
56//!
57//! # Hooks
58//!
59//! Every [`Plugin`] method has a default that does nothing (`continue`,
60//! `default`, `none` or an empty list). List the hooks you implement in
61//! [`Registration::hooks`] so the host skips the rest. An empty list delivers
62//! every hook except [`Hook::Chat`] and [`Hook::Kick`], which are only ever
63//! delivered when listed. [`Plugin::dialog`] and [`Plugin::job_done`] need no
64//! entry.
65//!
66//! The decision hooks answer with an [`Action`]; each accepts a subset:
67//!
68//! | Hook | Accepts |
69//! | --- | --- |
70//! | `login`, `before_transfer`, `chat` | `Continue`, `deny` |
71//! | `initial_server`, `kick` | `Continue`, `deny`, `server` |
72//! | `command` | `Continue`, `message`, `server`, `queue`, `LeaveQueue` |
73//!
74//! # Host API
75//!
76//! [`players`], [`store`], [`permissions`], [`bus`], [`servers`], [`timers`],
77//! [`background`], [`log`] and [`meta`] mirror the WIT interfaces of the same
78//! names. Errors are the host's message as a `String`. [`log`] also installs
79//! a [`::log`] logger before `init`, so `log::info!` and friends reach Rift's
80//! log. Plugin configuration arrives as JSON; decode it with [`config()`].
81//!
82//! WASI 0.2 (`std::fs` on the granted directories, clocks, random) works
83//! through the standard library. A `wasi:http` helper is not provided.
84//!
85//! # Text
86//!
87//! Everything a player reads is a [`Text`]: [`Text::Plain`], shown exactly
88//! as written, or [`Text::Json`], a JSON text component with colours, line
89//! breaks, hover text and click events. Every function and constructor that
90//! shows text takes `impl Into<Text>`:
91//!
92//! - `&str` and `String` become plain text;
93//! - the [`text`] builders ([`text::mini`] for Rift markup, [`text::legacy`]
94//!   for `&c` codes, [`text::Component`] field by field) and a
95//!   [`serde_json::Value`] become JSON text.
96//!
97//! ```no_run
98//! use rift_plugin::{Action, TitleSpec, players, text};
99//! use serde_json::json;
100//!
101//! players::message("Steve", "Shown <exactly> as written").ok();
102//!
103//! let welcome = text::mini("<gold>Welcome, <player>!</gold> <gray><click:run_command:/spawn>[Spawn]")
104//!     .arg("player", "Steve");
105//! players::message("Steve", welcome).ok();
106//! players::title(
107//!     "Steve",
108//!     &TitleSpec::new(text::mini("<green>Lobby")).subtitle(text::legacy("&7Have fun")),
109//! )
110//! .ok();
111//!
112//! let reply = Action::message(
113//!     text::Component::text("[Accept]")
114//!         .color("green")
115//!         .bold()
116//!         .run_command("/party accept Alex")
117//!         .hover_text("Join Alex's party"),
118//! );
119//! let banned = Action::deny(json!({
120//!     "text": "You are banned\n", "color": "red",
121//!     "extra": [{"text": "Reason: spam", "color": "gray"}],
122//! }));
123//! ```
124//!
125//! Rift spells hover and click events the way each player's client reads
126//! them, so either the 1.21.5+ form (`click_event`, `hover_event`) or the
127//! older one (`clickEvent`, `hoverEvent`) works for every client. Markup is
128//! only ever read from an explicit `{"mini": ...}` object, which
129//! [`text::mini`] writes; a string is always shown as written, so text from
130//! players can be passed safely as plain text or as a markup argument.
131//!
132//! # Chat and kicks
133//!
134//! [`Plugin::chat`] sees every chat message, and every command that reaches
135//! the backend, before the backend does, and may drop it. [`Plugin::kick`]
136//! decides what happens to a player a backend kicked. Both are delivered
137//! only to a registration that lists them:
138//!
139//! ```no_run
140//! use rift_plugin::{Action, ChatAttempt, ChatKind, Context, Hook, Plugin, Registration};
141//!
142//! #[derive(Default)]
143//! struct Mute {
144//!     muted: Vec<String>,
145//! }
146//!
147//! impl Plugin for Mute {
148//!     fn init(&mut self, _config: &str) -> Result<Registration, String> {
149//!         Ok(Registration::new().hooks([Hook::Chat, Hook::Kick]))
150//!     }
151//!
152//!     fn chat(&mut self, attempt: ChatAttempt) -> Action {
153//!         if attempt.kind == ChatKind::Message && self.muted.contains(&attempt.ctx.uuid) {
154//!             return Action::deny("You are muted.");
155//!         }
156//!         Action::Continue
157//!     }
158//!
159//!     fn kick(&mut self, ctx: Context) -> Action {
160//!         match ctx.reason.as_deref() {
161//!             Some(reason) if reason.contains("restarting") => Action::server("lobby"),
162//!             _ => Action::Continue,
163//!         }
164//!     }
165//! }
166//!
167//! rift_plugin::export!(Mute);
168//! ```
169//!
170//! Rift asks a plugin that lists [`Hook::Chat`] about every message of every
171//! player. Dropping one signed message breaks the player's signature chain,
172//! so unless the proxy sets `authentication.signed_chat = false`, the first
173//! deny costs that player their chat signing until they reconnect.
174//!
175//! # Background work
176//!
177//! A plugin runs one call at a time, so a slow lookup in `login` would keep
178//! every other hook waiting. The [`background`] module moves such work to
179//! worker instances (manifest `[background] workers = N`): the hook defers
180//! its answer, queues a job and returns; a worker runs [`Plugin::work`];
181//! [`Plugin::job_done`] receives the result and gives the answer.
182//!
183//! ```no_run
184//! use rift_plugin::{Action, Context, Hook, Plugin, Registration, background};
185//! use std::collections::HashMap;
186//!
187//! #[derive(Default)]
188//! struct Bans {
189//!     /// Job handle to the ticket of the login that waits for it.
190//!     waiting: HashMap<u64, u64>,
191//! }
192//!
193//! impl Plugin for Bans {
194//!     fn init(&mut self, _config: &str) -> Result<Registration, String> {
195//!         Ok(Registration::new().hooks([Hook::Login]))
196//!     }
197//!
198//!     fn login(&mut self, ctx: Context) -> Action {
199//!         let Ok(ticket) = background::defer() else {
200//!             return Action::deny("Try again in a moment.");
201//!         };
202//!         match background::run(ctx.uuid.as_bytes()) {
203//!             Ok(job) => {
204//!                 self.waiting.insert(job, ticket);
205//!             }
206//!             Err(_) => {
207//!                 let _ = background::resolve(ticket, Action::deny("Try again in a moment."));
208//!             }
209//!         }
210//!         // Ignored: the answer is the one given to `resolve`.
211//!         Action::Continue
212//!     }
213//!
214//!     fn job_done(&mut self, job: u64, outcome: Result<Vec<u8>, String>) {
215//!         let Some(ticket) = self.waiting.remove(&job) else { return };
216//!         let action = match outcome {
217//!             Ok(reason) if reason.is_empty() => Action::Continue,
218//!             Ok(reason) => Action::deny(String::from_utf8_lossy(&reason).into_owned()),
219//!             Err(_) => Action::deny("Try again in a moment."),
220//!         };
221//!         let _ = background::resolve(ticket, action);
222//!     }
223//!
224//!     // Runs on a worker: another instance, with its own state.
225//!     fn work(&mut self, uuid: Vec<u8>) -> Result<Vec<u8>, String> {
226//!         let uuid = String::from_utf8(uuid).map_err(|error| error.to_string())?;
227//!         // The slow part; an empty answer means "not banned".
228//!         Ok(std::fs::read(format!("/data/bans/{uuid}")).unwrap_or_default())
229//!     }
230//! }
231//!
232//! rift_plugin::export!(Bans);
233//! ```
234//!
235//! A worker is a separate instance of the same component with its own
236//! memory: its state is its own `Default` value, [`Plugin::init`] never runs
237//! there, and it receives no hooks. [`Plugin::work_init`] runs once per
238//! worker before its first job, with the plugin's configuration. The
239//! deferred hook's deadline keeps running while the job does.
240//!
241//! # Dialogs
242//!
243//! [`players::show_dialog`] shows a dialog (Minecraft 1.21.6+) in the
244//! vanilla JSON format. A button whose `custom` or `dynamic/custom` action
245//! has an `id` without a namespace comes back to [`Plugin::dialog`]:
246//!
247//! ```no_run
248//! use rift_plugin::{Context, DialogResponse, Plugin, players};
249//! use serde_json::json;
250//!
251//! #[derive(Default)]
252//! struct Rules;
253//!
254//! impl Plugin for Rules {
255//!     fn join(&mut self, ctx: Context) {
256//!         let _ = players::show_dialog(&ctx.uuid, &json!({
257//!             "type": "confirmation",
258//!             "title": "Server rules",
259//!             "body": "Be nice.",
260//!             "yes": {"label": "Accept", "action": {"type": "custom", "id": "accept"}},
261//!             "no": {"label": "Leave", "action": {"type": "custom", "id": "leave"}},
262//!         }));
263//!     }
264//!
265//!     fn dialog(&mut self, response: DialogResponse) {
266//!         if response.action == "leave" {
267//!             let _ = players::kick(&response.ctx.uuid, "You declined the rules.");
268//!         }
269//!     }
270//! }
271//! # rift_plugin::export!(Rules);
272//! ```
273
274#![warn(missing_docs)]
275
276/// The raw generated bindings for the `plugin` world, including WASI.
277///
278/// Prefer the wrappers at the crate root; this is an escape hatch.
279#[allow(missing_docs, clippy::all)]
280pub mod bindings {
281    wit_bindgen::generate!({
282        path: "wit",
283        world: "rift:plugin/plugin@0.2.0",
284        pub_export_macro: true,
285        export_macro_name: "__export_rift_plugin",
286        default_bindings_module: "rift_plugin::bindings",
287        generate_all,
288    });
289}
290
291pub use bindings::rift::plugin::types::{
292    BossBarSpec, BusMessage, ChatAttempt, ChatKind, CommandSpec, Connection, Context,
293    DialogResponse, Hook, HttpRequest, HttpResponse, Level, Ping, PlayerInfo, PlayerRecord,
294    ProfileProperty, Registration, RiftEvent, RouteDecision, StatusOverride, SuggestKind,
295    Suggestion, Text, TitleSpec,
296};
297
298/// The answer to a decision hook: the contract's `decision` variant.
299///
300/// Each hook accepts a subset; an answer outside it counts as a failed call:
301/// - `login`, `before_transfer`, `chat`: `Continue`, [`deny`](Action::deny)
302/// - `initial_server`, `kick`: `Continue`, [`deny`](Action::deny),
303///   [`server`](Action::server)
304/// - `command`: `Continue`, [`message`](Action::message),
305///   [`server`](Action::server), [`queue`](Action::queue), `LeaveQueue`
306pub type Action = bindings::rift::plugin::types::Decision;
307
308use serde::de::DeserializeOwned;
309
310/// A Rift plugin. Implement the hooks you need and pass the type to
311/// [`export!`].
312///
313/// The instance is created with [`Default`] before [`Plugin::init`]; see the
314/// [crate documentation](crate#state) for how state is kept.
315#[allow(unused_variables)]
316pub trait Plugin: Default + 'static {
317    /// Called once per instance with the plugin's configuration as JSON
318    /// (`{}` without one). Create timers and bus subscriptions here.
319    fn init(&mut self, config: &str) -> Result<Registration, String> {
320        Ok(Registration::new())
321    }
322
323    /// The configuration changed and the manifest sets `hot_reload = true`.
324    fn reload(&mut self, config: &str) -> Result<(), String> {
325        Ok(())
326    }
327
328    /// The instance is about to be dropped.
329    fn shutdown(&mut self, reason: &str) {}
330
331    /// A TCP connection was accepted, before any bytes are read.
332    fn route(&mut self, connection: Connection) -> RouteDecision {
333        RouteDecision::Default
334    }
335
336    /// A player authenticated. Accepts `continue` and `deny`.
337    fn login(&mut self, ctx: Context) -> Action {
338        Action::Continue
339    }
340
341    /// Choose the first backend. Accepts `continue`, `deny` and `server`.
342    fn initial_server(&mut self, ctx: Context) -> Action {
343        Action::Continue
344    }
345
346    /// A transfer is about to happen. Accepts `continue` and `deny`.
347    fn before_transfer(&mut self, ctx: Context) -> Action {
348        Action::Continue
349    }
350
351    /// A registered command ran; `ctx.command` is its name, also when an
352    /// alias was typed, and `ctx.args` the text after it. Accepts
353    /// `continue`, `message`, `server`, `queue` and `leave-queue`.
354    ///
355    /// For a command registered with [`CommandSpec::console`], `ctx.console`
356    /// tells that the operator console runs it: there is no player then
357    /// (`ctx.uuid` is empty and `ctx.name` is `CONSOLE`), and a `message`
358    /// answer is shown to the operator.
359    fn command(&mut self, ctx: Context) -> Action {
360        Action::Continue
361    }
362
363    /// Complete an argument of a command registered with
364    /// [`SuggestKind::Dynamic`].
365    fn suggest(&mut self, ctx: Context) -> Vec<Suggestion> {
366        Vec::new()
367    }
368
369    /// A server-list ping; return the fields to replace.
370    fn status(&mut self, ping: Ping) -> Option<StatusOverride> {
371        None
372    }
373
374    /// An authenticated request to `/ext` or `/ext/*` on the web server.
375    fn http(&mut self, request: HttpRequest) -> Option<HttpResponse> {
376        None
377    }
378
379    /// A player sent a chat message, or a command that neither Rift nor a
380    /// plugin owns, and the backend has not seen it yet. Accepts `continue`,
381    /// which lets it through unchanged, and `deny`, which drops it and shows
382    /// the player the text.
383    ///
384    /// Delivered only when [`Registration::hooks`] lists [`Hook::Chat`]; see
385    /// the [crate documentation](crate#chat-and-kicks) for what that
386    /// requires of the proxy. For a command, `attempt.ctx.command` is the
387    /// root name and `attempt.ctx.args` the rest.
388    fn chat(&mut self, attempt: ChatAttempt) -> Action {
389        Action::Continue
390    }
391
392    /// A backend kicked a player in play: `ctx.server` is the backend and
393    /// `ctx.reason` its reason without formatting. `continue` leaves it to
394    /// `network.on_kick`, `deny` disconnects the player with the text and
395    /// `server` moves them there.
396    ///
397    /// Delivered only when [`Registration::hooks`] lists [`Hook::Kick`].
398    fn kick(&mut self, ctx: Context) -> Action {
399        Action::Continue
400    }
401
402    /// A player joined.
403    fn join(&mut self, ctx: Context) {}
404
405    /// A transfer finished (see `ctx.success`).
406    fn after_transfer(&mut self, ctx: Context) {}
407
408    /// A session closed (see `ctx.reason`).
409    fn disconnect(&mut self, ctx: Context) {}
410
411    /// A message arrived on a subscription made with [`bus::subscribe`].
412    fn message(&mut self, message: BusMessage) {}
413
414    /// A timer made with [`timers::every`] or [`timers::after`] fired.
415    fn timer(&mut self, handle: u64) {}
416
417    /// A Rift event (`rift.events.*`).
418    fn event(&mut self, event: RiftEvent) {}
419
420    /// A player clicked a plugin action of a dialog this plugin showed with
421    /// [`players::show_dialog`]. Delivered without being listed in
422    /// [`Registration::hooks`].
423    fn dialog(&mut self, response: DialogResponse) {}
424
425    /// A job started with [`background::run`] finished: `handle` is the one
426    /// `run` returned and `outcome` what [`Plugin::work`] returned on the
427    /// worker, or why the job failed. Delivered without being listed in
428    /// [`Registration::hooks`].
429    fn job_done(&mut self, handle: u64, outcome: Result<Vec<u8>, String>) {}
430
431    /// Background workers only: called once per worker before its first
432    /// job, with the plugin's configuration as JSON.
433    ///
434    /// A worker is a separate instance of the component. `self` is a fresh
435    /// [`Default`] value that only `work_init` and [`Plugin::work`] ever
436    /// see; [`Plugin::init`] is never called on it.
437    fn work_init(&mut self, config: &str) -> Result<(), String> {
438        Ok(())
439    }
440
441    /// Background workers only: one job queued with [`background::run`].
442    /// It may block; the plugin's hooks keep running meanwhile. The result
443    /// reaches [`Plugin::job_done`] on the plugin's main instance.
444    fn work(&mut self, payload: Vec<u8>) -> Result<Vec<u8>, String> {
445        Err("this plugin has no background work".into())
446    }
447}
448
449/// Exports a [`Plugin`] implementation as the component's
450/// `rift:plugin/handler` interface. Use it exactly once per plugin crate.
451///
452/// ```ignore
453/// rift_plugin::export!(MyPlugin);
454/// ```
455#[macro_export]
456macro_rules! export {
457    ($plugin:ty) => {
458        #[doc(hidden)]
459        type __RiftPluginExport = $crate::__private::Adapter<$plugin>;
460        $crate::bindings::__export_rift_plugin!(__RiftPluginExport with_types_in $crate::bindings);
461    };
462}
463
464/// Decodes the JSON configuration `init`, `reload` and `work_init` receive.
465///
466/// The host passes `{}` when the plugin has no configuration, so mark
467/// optional fields with `#[serde(default)]`.
468pub fn config<T: DeserializeOwned>(json: &str) -> Result<T, String> {
469    serde_json::from_str(json).map_err(|error| format!("invalid plugin configuration: {error}"))
470}
471
472impl Text {
473    /// Plain text of 1..=1024 bytes, shown exactly as written. The same as
474    /// converting a `&str` or `String`.
475    pub fn plain(text: impl Into<String>) -> Self {
476        Self::Plain(text.into())
477    }
478
479    /// A JSON text component of at most 32 KiB that is already JSON text.
480    /// The [`text`] builders and [`serde_json::Value`] convert into this
481    /// with `into()`.
482    pub fn json(json: impl Into<String>) -> Self {
483        Self::Json(json.into())
484    }
485}
486
487impl From<&str> for Text {
488    /// [`Text::Plain`].
489    fn from(text: &str) -> Self {
490        Self::Plain(text.to_owned())
491    }
492}
493
494impl From<String> for Text {
495    /// [`Text::Plain`].
496    fn from(text: String) -> Self {
497        Self::Plain(text)
498    }
499}
500
501impl From<&String> for Text {
502    /// [`Text::Plain`].
503    fn from(text: &String) -> Self {
504        Self::Plain(text.clone())
505    }
506}
507
508impl From<serde_json::Value> for Text {
509    /// [`Text::Json`]: the value is a JSON text component.
510    fn from(component: serde_json::Value) -> Self {
511        Self::Json(component.to_string())
512    }
513}
514
515impl From<&serde_json::Value> for Text {
516    /// [`Text::Json`]: the value is a JSON text component.
517    fn from(component: &serde_json::Value) -> Self {
518        Self::Json(component.to_string())
519    }
520}
521
522impl PartialEq for Text {
523    fn eq(&self, other: &Self) -> bool {
524        match (self, other) {
525            (Self::Plain(a), Self::Plain(b)) | (Self::Json(a), Self::Json(b)) => a == b,
526            _ => false,
527        }
528    }
529}
530
531impl Registration {
532    /// No commands and no hooks, which delivers every hook except
533    /// [`Hook::Chat`] and [`Hook::Kick`].
534    pub fn new() -> Self {
535        Self {
536            commands: Vec::new(),
537            hooks: Vec::new(),
538        }
539    }
540
541    /// Registers a command.
542    pub fn command(mut self, command: CommandSpec) -> Self {
543        self.commands.push(command);
544        self
545    }
546
547    /// Asks for one more hook to be delivered.
548    pub fn hook(mut self, hook: Hook) -> Self {
549        self.hooks.push(hook);
550        self
551    }
552
553    /// Asks for these hooks to be delivered.
554    pub fn hooks(mut self, hooks: impl IntoIterator<Item = Hook>) -> Self {
555        self.hooks.extend(hooks);
556        self
557    }
558}
559
560impl Default for Registration {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566impl CommandSpec {
567    /// A command without aliases, permission node or suggestions, for
568    /// players only. Without a permission every player may see and run it.
569    ///
570    /// Names are 1 to 64 bytes of `a-z0-9_-`; `server` and `hub` are
571    /// reserved.
572    pub fn new(name: impl Into<String>) -> Self {
573        Self {
574            name: name.into(),
575            aliases: Vec::new(),
576            permission: String::new(),
577            suggest: SuggestKind::None,
578            console: false,
579        }
580    }
581
582    /// Another name for the command, written like the name. A command has
583    /// at most 16; the hooks always see the name in `ctx.command`.
584    pub fn alias(mut self, alias: impl Into<String>) -> Self {
585        self.aliases.push(alias.into());
586        self
587    }
588
589    /// More names for the command; see [`CommandSpec::alias`].
590    pub fn aliases<S: Into<String>>(mut self, aliases: impl IntoIterator<Item = S>) -> Self {
591        self.aliases.extend(aliases.into_iter().map(Into::into));
592        self
593    }
594
595    /// The permission node a player needs to see and run the command. An
596    /// empty node opens the command to every player.
597    pub fn permission(mut self, node: impl Into<String>) -> Self {
598        self.permission = node.into();
599        self
600    }
601
602    /// How the command's arguments are completed.
603    pub fn suggest(mut self, kind: SuggestKind) -> Self {
604        self.suggest = kind;
605        self
606    }
607
608    /// Operators may run the command from the console (`rift admin plugins
609    /// run`); [`Plugin::command`] then sees `ctx.console`.
610    pub fn console(mut self) -> Self {
611        self.console = true;
612        self
613    }
614}
615
616impl Action {
617    /// `deny(reason)`: refuse, and show the player the reason.
618    pub fn deny(reason: impl Into<Text>) -> Self {
619        Self::Deny(reason.into())
620    }
621
622    /// `message(text)`: reply to whoever ran a command.
623    pub fn message(text: impl Into<Text>) -> Self {
624        Self::Message(text.into())
625    }
626
627    /// `server(name)`: send the player to a backend.
628    pub fn server(name: impl Into<String>) -> Self {
629        Self::Server(name.into())
630    }
631
632    /// `queue(name)`: put the player into the queue for a backend.
633    pub fn queue(name: impl Into<String>) -> Self {
634        Self::Queue(name.into())
635    }
636}
637
638impl PartialEq for Action {
639    fn eq(&self, other: &Self) -> bool {
640        match (self, other) {
641            (Self::Continue, Self::Continue) | (Self::LeaveQueue, Self::LeaveQueue) => true,
642            (Self::Deny(a), Self::Deny(b)) | (Self::Message(a), Self::Message(b)) => a == b,
643            (Self::Server(a), Self::Server(b)) | (Self::Queue(a), Self::Queue(b)) => a == b,
644            _ => false,
645        }
646    }
647}
648
649impl DialogResponse {
650    /// The player's input values, decoded from `values_json`. They come from
651    /// the client: validate them like any player input.
652    pub fn values<T: DeserializeOwned>(&self) -> Result<T, String> {
653        serde_json::from_str(&self.values_json)
654            .map_err(|error| format!("invalid dialog values: {error}"))
655    }
656
657    /// The action's `payload` or `additions`, as the plugin wrote it; `None`
658    /// when it had none.
659    pub fn payload<T: DeserializeOwned>(&self) -> Result<Option<T>, String> {
660        self.payload_json
661            .as_deref()
662            .map(serde_json::from_str)
663            .transpose()
664            .map_err(|error| format!("invalid dialog payload: {error}"))
665    }
666}
667
668impl Suggestion {
669    /// A suggestion without a tooltip.
670    pub fn new(text: impl Into<String>) -> Self {
671        Self {
672            text: text.into(),
673            tooltip: None,
674        }
675    }
676
677    /// The tooltip shown next to the suggestion.
678    pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
679        self.tooltip = Some(tooltip.into());
680        self
681    }
682}
683
684impl Default for StatusOverride {
685    /// Replaces nothing. Set the fields to replace:
686    ///
687    /// ```
688    /// use rift_plugin::{StatusOverride, text};
689    ///
690    /// let status = StatusOverride {
691    ///     motd: Some(text::mini("<gold>Rift</gold>\n<gray>Now with plugins").into()),
692    ///     max_players: Some(500),
693    ///     ..Default::default()
694    /// };
695    /// ```
696    fn default() -> Self {
697        Self {
698            motd: None,
699            version: None,
700            protocol: None,
701            online: None,
702            max_players: None,
703            sample: None,
704            favicon: None,
705        }
706    }
707}
708
709impl TitleSpec {
710    /// A title with the vanilla times (10 ticks fade in, 70 stay, 20 fade
711    /// out).
712    pub fn new(title: impl Into<Text>) -> Self {
713        Self {
714            title: Some(title.into()),
715            subtitle: None,
716            fade_in: 10,
717            stay: 70,
718            fade_out: 20,
719        }
720    }
721
722    /// Only a subtitle, with the vanilla times: the title above it stays
723    /// empty.
724    pub fn subtitle_only(subtitle: impl Into<Text>) -> Self {
725        Self {
726            title: None,
727            subtitle: Some(subtitle.into()),
728            fade_in: 10,
729            stay: 70,
730            fade_out: 20,
731        }
732    }
733
734    /// The subtitle.
735    pub fn subtitle(mut self, subtitle: impl Into<Text>) -> Self {
736        self.subtitle = Some(subtitle.into());
737        self
738    }
739
740    /// Fade-in, stay and fade-out times in ticks.
741    pub fn times(mut self, fade_in: i32, stay: i32, fade_out: i32) -> Self {
742        self.fade_in = fade_in;
743        self.stay = stay;
744        self.fade_out = fade_out;
745        self
746    }
747}
748
749/// The joined players, and what Rift remembers about those who left.
750/// `target` is a name, a UUID or a connection id.
751///
752/// The functions that show text take anything that converts into a
753/// [`Text`]: a string for plain text, a [`text`]
754/// builder or a [`serde_json::Value`] for a JSON text component. Each
755/// returns whether the player was online.
756///
757/// [`PlayerInfo`] carries what Rift knows about an online
758/// player, including `ip`, the profile `properties`, `visible_to`, `locale`
759/// (the language the client reported, `None` until the client has sent it:
760/// never in [`Plugin::login`], and in
761/// [`Plugin::join`] only for 1.20.2 and newer clients)
762/// and `language` (the one set with [`set_language`](players::set_language), else `locale`).
763///
764/// [`lookup`](players::lookup) and [`search`](players::search) answer with a
765/// [`PlayerRecord`] for every player who joined at
766/// least once, online or not.
767pub mod players {
768    use crate::Text;
769    use crate::bindings::rift::plugin::players as host;
770    pub use host::{
771        boss_bar, clear_dialog, dialog, get, list, lookup, names, search, set_language, title,
772        transfer,
773    };
774
775    /// Send a chat message to a player.
776    pub fn message(target: &str, text: impl Into<Text>) -> Result<bool, String> {
777        host::message(target, &text.into())
778    }
779
780    /// Show text above a player's hotbar.
781    pub fn action_bar(target: &str, text: impl Into<Text>) -> Result<bool, String> {
782        host::action_bar(target, &text.into())
783    }
784
785    /// Disconnect a player with a reason.
786    pub fn kick(target: &str, reason: impl Into<Text>) -> Result<bool, String> {
787        host::kick(target, &reason.into())
788    }
789
790    /// Send a chat message to every player. Returns how many received it.
791    pub fn broadcast(text: impl Into<Text>) -> Result<u32, String> {
792        host::broadcast(&text.into())
793    }
794
795    /// Replace a player's tab-list header and footer. Both `None` restores
796    /// the configured tab list.
797    ///
798    /// ```no_run
799    /// use rift_plugin::{players, text};
800    ///
801    /// players::tab_list("Steve", Some(text::mini("<gold>Rift").into()), Some("Have fun".into())).ok();
802    /// players::tab_list("Steve", None, None).ok();
803    /// ```
804    pub fn tab_list(
805        target: &str,
806        header: Option<Text>,
807        footer: Option<Text>,
808    ) -> Result<bool, String> {
809        host::tab_list(target, header.as_ref(), footer.as_ref())
810    }
811
812    /// Show a dialog in the vanilla JSON format (Minecraft 1.21.6+). Returns
813    /// false when the player is offline, older than 1.21.6 or has too many
814    /// undelivered actions; an invalid dialog is an error.
815    pub fn show_dialog(target: &str, dialog: &serde_json::Value) -> Result<bool, String> {
816        self::dialog(target, &dialog.to_string())
817    }
818}
819
820/// The durable key-value store. Namespaces are prefixed with
821/// `plugin.<name>.` by the host.
822pub mod store {
823    pub use crate::bindings::rift::plugin::store::{delete, get, increment, list, set};
824    use serde::Serialize;
825    use serde::de::DeserializeOwned;
826
827    /// Reads a JSON-encoded entry.
828    pub fn get_json<T: DeserializeOwned>(namespace: &str, key: &str) -> Result<Option<T>, String> {
829        match get(namespace, key)? {
830            Some(bytes) => serde_json::from_slice(&bytes)
831                .map(Some)
832                .map_err(|error| format!("invalid JSON in {namespace}/{key}: {error}")),
833            None => Ok(None),
834        }
835    }
836
837    /// Writes an entry as JSON.
838    pub fn set_json<T: Serialize + ?Sized>(
839        namespace: &str,
840        key: &str,
841        value: &T,
842    ) -> Result<(), String> {
843        let bytes = serde_json::to_vec(value).map_err(|error| error.to_string())?;
844        set(namespace, key, &bytes)
845    }
846}
847
848/// Rift's permission store: user nodes and group memberships.
849///
850/// `add_group(uuid, group, expires_at)` adds the user to a group, until
851/// `expires_at` Unix seconds when given (see [`meta::time`]
852/// for the current time).
853pub mod permissions {
854    pub use crate::bindings::rift::plugin::permissions::{
855        add_group, groups, has, list_groups, meta, remove_group, set, set_meta,
856    };
857
858    /// The meta key that makes a player visible only to viewers holding the
859    /// node in its value (see docs/permissions.md, "Visibility").
860    pub const VISIBLE_TO: &str = "rift.visible-to";
861}
862
863/// The message bus. Subscriptions deliver to [`Plugin::message`].
864pub mod bus {
865    use crate::BusMessage;
866    pub use crate::bindings::rift::plugin::bus::{
867        publish, request, stream_publish, subscribe, unsubscribe,
868    };
869
870    /// Publishes `payload` to the message's reply subject. Returns `None`
871    /// when the message has none, otherwise how many subscribers received it.
872    pub fn reply(message: &BusMessage, payload: &[u8]) -> Result<Option<u32>, String> {
873        match &message.reply {
874            Some(subject) => publish(subject, payload, None).map(Some),
875            None => Ok(None),
876        }
877    }
878}
879
880/// Managed servers, through the `rift.control.*` operations. Results are
881/// the control service's JSON replies.
882pub mod servers {
883    pub use crate::bindings::rift::plugin::servers::{
884        control, create, download, remove, start, stop,
885    };
886}
887
888/// Timers fire through [`Plugin::timer`] with the
889/// returned handle.
890pub mod timers {
891    pub use crate::bindings::rift::plugin::timers::{after, cancel, every};
892}
893
894/// Work that must not hold up the plugin's hooks.
895///
896/// A plugin runs one call at a time, so a hook that waits for a database or
897/// a web service keeps every other hook waiting. Background workers take
898/// that work: each is a separate instance of the component, started when the
899/// manifest sets `[background] workers`. The pattern has three steps (the
900/// [crate documentation](crate#background-work) has a complete example):
901///
902/// 1. In a decision hook (`login`, `initial_server`, `before_transfer`,
903///    `command`, `chat` or `kick`), call [`defer`](background::defer) for a ticket and [`run`](background::run)
904///    with the job's payload, remember the ticket under the job's handle and
905///    return. The hook's own return value is ignored.
906/// 2. A worker calls [`Plugin::work`] with the payload.
907///    It may block.
908/// 3. [`Plugin::job_done`] receives the handle and
909///    the result on the main instance; call [`resolve`](background::resolve) with the ticket and
910///    the answer.
911///
912/// The deferred hook's deadline keeps running: an answer that comes too late
913/// counts as a missed deadline, and [`resolve`](background::resolve) then returns `false`. [`run`](background::run)
914/// also works without [`defer`](background::defer), from any hook, for work nobody waits for.
915pub mod background {
916    use crate::Action;
917    use crate::bindings::rift::plugin::background as host;
918    pub use host::{defer, is_worker, run};
919
920    /// Answer a deferred hook with the ticket [`defer`] returned. Returns
921    /// `false` when nobody waits for the ticket any more: the deadline
922    /// passed, or it was answered already.
923    pub fn resolve(ticket: u64, action: Action) -> Result<bool, String> {
924        host::resolve(ticket, &action)
925    }
926}
927
928/// Plugin metadata and clocks. `api` is the `rift:plugin` version the host
929/// implements, such as `0.2.0`.
930pub mod meta {
931    pub use crate::bindings::rift::plugin::meta::{
932        api, clock, config_dir, data_dir, name, time, version,
933    };
934}
935
936/// Rift's log stream, prefixed with the plugin name by the host.
937///
938/// The SDK installs a [`::log`] logger before [`Plugin::init`]
939/// (and before [`Plugin::work_init`] on a worker),
940/// so the `log` crate's macros work as usual. They are re-exported here for
941/// plugins that do not depend on `log` themselves:
942/// `rift_plugin::log::info!("ready")`.
943pub mod log {
944    pub use crate::bindings::rift::plugin::log::log;
945    #[doc(no_inline)]
946    pub use ::log::{debug, error, info, trace, warn};
947
948    use crate::Level;
949
950    struct HostLogger;
951
952    static LOGGER: HostLogger = HostLogger;
953
954    impl ::log::Log for HostLogger {
955        fn enabled(&self, _metadata: &::log::Metadata) -> bool {
956            true
957        }
958
959        fn log(&self, record: &::log::Record) {
960            let level = match record.level() {
961                ::log::Level::Trace => Level::Trace,
962                ::log::Level::Debug => Level::Debug,
963                ::log::Level::Info => Level::Info,
964                ::log::Level::Warn => Level::Warn,
965                ::log::Level::Error => Level::Error,
966            };
967            match record.args().as_str() {
968                Some(message) => log(level, message),
969                None => log(level, &record.args().to_string()),
970            }
971        }
972
973        fn flush(&self) {}
974    }
975
976    /// Installs the host logger once. Later calls do nothing, so a plugin may
977    /// lower the level with `log::set_max_level` in `init`.
978    pub(crate) fn install() {
979        if ::log::set_logger(&LOGGER).is_ok() {
980            ::log::set_max_level(::log::LevelFilter::Trace);
981        }
982    }
983}
984
985/// Builders for JSON text: each converts into a [`Text::Json`](crate::Text),
986/// so it can be passed wherever text is shown.
987///
988/// - [`text::mini`] writes Rift markup as `{"mini": "...", "args": {...}}`; Rift
989///   turns it into a component. See `docs/text.md` for the tags.
990/// - [`text::legacy`] writes `&c` codes as `{"legacy": "...", "marker": "&"}`.
991/// - [`text::Component`] builds a JSON text component field by field, with the
992///   1.21.5 spellings `click_event` and `hover_event` (Rift rewrites them
993///   for older clients).
994///
995/// Every builder converts into [`Text`] and into a
996/// [`serde_json::Value`] for use inside other components, and implements
997/// [`Display`](std::fmt::Display) (its JSON text). A bare string is never
998/// read as markup, so player input passed as an argument or as text cannot
999/// add tags or click events.
1000pub mod text {
1001    use serde_json::{Map, Value};
1002    use std::fmt;
1003
1004    /// Rift markup, such as `<gold>Hi <player>!`. Add arguments with
1005    /// [`Mini::arg`] and [`Mini::arg_component`].
1006    pub fn mini(markup: impl Into<String>) -> Mini {
1007        Mini {
1008            markup: markup.into(),
1009            args: Map::new(),
1010        }
1011    }
1012
1013    /// Markup with named arguments; see [`mini`].
1014    #[derive(Debug, Clone, PartialEq)]
1015    pub struct Mini {
1016        markup: String,
1017        args: Map<String, Value>,
1018    }
1019
1020    impl Mini {
1021        /// `<name>` shows `text` exactly as written: it is never parsed, so
1022        /// a player name cannot add tags.
1023        pub fn arg(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
1024            self.args.insert(name.into(), Value::String(text.into()));
1025            self
1026        }
1027
1028        /// `<name>` shows a component with its own formatting, such as a
1029        /// [`Component`], a [`legacy`] object or a [`serde_json::Value`].
1030        pub fn arg_component(
1031            mut self,
1032            name: impl Into<String>,
1033            component: impl Into<Value>,
1034        ) -> Self {
1035            self.args.insert(name.into(), component.into());
1036            self
1037        }
1038
1039        /// The authoring object as JSON.
1040        pub fn to_value(&self) -> Value {
1041            let mut object = Map::new();
1042            object.insert("mini".into(), Value::String(self.markup.clone()));
1043            if !self.args.is_empty() {
1044                object.insert("args".into(), Value::Object(self.args.clone()));
1045            }
1046            Value::Object(object)
1047        }
1048    }
1049
1050    /// Legacy formatting codes with the `&` marker, such as `&cRed &lbold`;
1051    /// `&#rrggbb` writes a hex colour. Use [`Legacy::marker`] for another
1052    /// marker.
1053    pub fn legacy(text: impl Into<String>) -> Legacy {
1054        Legacy {
1055            text: text.into(),
1056            marker: '&',
1057        }
1058    }
1059
1060    /// Legacy codes; see [`legacy`].
1061    #[derive(Debug, Clone, PartialEq)]
1062    pub struct Legacy {
1063        text: String,
1064        marker: char,
1065    }
1066
1067    impl Legacy {
1068        /// The character that starts a code, such as `§`; one symbol, not a
1069        /// letter, digit or space.
1070        pub fn marker(mut self, marker: char) -> Self {
1071            self.marker = marker;
1072            self
1073        }
1074
1075        /// The authoring object as JSON.
1076        pub fn to_value(&self) -> Value {
1077            let mut object = Map::new();
1078            object.insert("legacy".into(), Value::String(self.text.clone()));
1079            object.insert("marker".into(), Value::String(self.marker.to_string()));
1080            Value::Object(object)
1081        }
1082    }
1083
1084    /// A JSON text component, built field by field.
1085    ///
1086    /// ```
1087    /// use rift_plugin::text::Component;
1088    ///
1089    /// let invite = Component::text("Alex invited you. ")
1090    ///     .extra(Component::text("[Accept]").color("green").bold().run_command("/party accept Alex"));
1091    /// assert_eq!(
1092    ///     serde_json::Value::from(invite),
1093    ///     serde_json::json!({
1094    ///         "text": "Alex invited you. ",
1095    ///         "extra": [{
1096    ///             "text": "[Accept]", "color": "green", "bold": true,
1097    ///             "click_event": {"action": "run_command", "command": "/party accept Alex"},
1098    ///         }],
1099    ///     })
1100    /// );
1101    /// ```
1102    #[derive(Debug, Clone, PartialEq)]
1103    pub struct Component(Map<String, Value>);
1104
1105    impl Component {
1106        /// A component showing `text`.
1107        pub fn text(text: impl Into<String>) -> Self {
1108            let mut fields = Map::new();
1109            fields.insert("text".into(), Value::String(text.into()));
1110            Self(fields)
1111        }
1112
1113        /// A colour name such as `red` or `#ff8800`.
1114        pub fn color(self, color: impl Into<String>) -> Self {
1115            self.field("color", color.into())
1116        }
1117
1118        /// Bold text.
1119        pub fn bold(self) -> Self {
1120            self.field("bold", true)
1121        }
1122
1123        /// Italic text.
1124        pub fn italic(self) -> Self {
1125            self.field("italic", true)
1126        }
1127
1128        /// Underlined text.
1129        pub fn underlined(self) -> Self {
1130            self.field("underlined", true)
1131        }
1132
1133        /// Struck-through text.
1134        pub fn strikethrough(self) -> Self {
1135            self.field("strikethrough", true)
1136        }
1137
1138        /// Obfuscated (scrambling) text.
1139        pub fn obfuscated(self) -> Self {
1140            self.field("obfuscated", true)
1141        }
1142
1143        fn click(self, action: &str, key: &str, value: impl Into<String>) -> Self {
1144            let mut click = Map::new();
1145            click.insert("action".into(), Value::String(action.into()));
1146            click.insert(key.into(), Value::String(value.into()));
1147            self.field("click_event", Value::Object(click))
1148        }
1149
1150        /// A click runs `command` (with its leading `/`) as the player.
1151        pub fn run_command(self, command: impl Into<String>) -> Self {
1152            self.click("run_command", "command", command)
1153        }
1154
1155        /// A click puts `command` into the player's chat box.
1156        pub fn suggest_command(self, command: impl Into<String>) -> Self {
1157            self.click("suggest_command", "command", command)
1158        }
1159
1160        /// A click opens `url` (`http` or `https`) after a confirmation.
1161        pub fn open_url(self, url: impl Into<String>) -> Self {
1162            self.click("open_url", "url", url)
1163        }
1164
1165        /// A click copies `text` to the clipboard.
1166        pub fn copy_to_clipboard(self, text: impl Into<String>) -> Self {
1167            self.click("copy_to_clipboard", "value", text)
1168        }
1169
1170        /// Hover text shown as written.
1171        pub fn hover_text(self, text: impl Into<String>) -> Self {
1172            self.hover(Value::String(text.into()))
1173        }
1174
1175        /// Hover text as a component, such as another [`Component`] or a
1176        /// [`mini`] object.
1177        pub fn hover(self, component: impl Into<Value>) -> Self {
1178            let mut hover = Map::new();
1179            hover.insert("action".into(), Value::String("show_text".into()));
1180            hover.insert("value".into(), component.into());
1181            self.field("hover_event", Value::Object(hover))
1182        }
1183
1184        /// Text inserted into the chat box on shift-click.
1185        pub fn insertion(self, text: impl Into<String>) -> Self {
1186            self.field("insertion", text.into())
1187        }
1188
1189        /// Appends a child: a [`Component`], an authoring object or a string
1190        /// shown as written. Children inherit this component's style.
1191        pub fn extra(mut self, child: impl Into<Value>) -> Self {
1192            let child = child.into();
1193            match self.0.get_mut("extra") {
1194                Some(Value::Array(children)) => children.push(child),
1195                _ => {
1196                    self.0.insert("extra".into(), Value::Array(vec![child]));
1197                }
1198            }
1199            self
1200        }
1201
1202        /// Sets any other field, such as `font`, `shadow_color` or
1203        /// `"italic": false`.
1204        pub fn field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1205            self.0.insert(key.into(), value.into());
1206            self
1207        }
1208
1209        /// The component as JSON.
1210        pub fn to_value(&self) -> Value {
1211            Value::Object(self.0.clone())
1212        }
1213    }
1214
1215    macro_rules! json_text {
1216        ($($builder:ty),*) => {$(
1217            impl fmt::Display for $builder {
1218                /// The JSON text.
1219                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1220                    fmt::Display::fmt(&self.to_value(), f)
1221                }
1222            }
1223
1224            impl From<$builder> for Value {
1225                fn from(builder: $builder) -> Value {
1226                    builder.to_value()
1227                }
1228            }
1229
1230            impl From<$builder> for crate::Text {
1231                /// [`Text::Json`](crate::Text::Json) with the builder's JSON.
1232                fn from(builder: $builder) -> crate::Text {
1233                    crate::Text::Json(builder.to_string())
1234                }
1235            }
1236
1237            impl From<&$builder> for crate::Text {
1238                /// [`Text::Json`](crate::Text::Json) with the builder's JSON.
1239                fn from(builder: &$builder) -> crate::Text {
1240                    crate::Text::Json(builder.to_string())
1241                }
1242            }
1243        )*};
1244    }
1245
1246    json_text!(Mini, Legacy, Component);
1247}
1248
1249#[doc(hidden)]
1250pub mod __private {
1251    use crate::bindings::exports::rift::plugin::handler::Guest;
1252    use crate::*;
1253    use std::any::Any;
1254    use std::cell::RefCell;
1255    use std::marker::PhantomData;
1256
1257    thread_local! {
1258        static INSTANCE: RefCell<Option<Box<dyn Any>>> = const { RefCell::new(None) };
1259    }
1260
1261    /// Runs `f` on the instance, creating it first when there is none: a
1262    /// background worker never sees `init`.
1263    fn with<P: Plugin, R>(f: impl FnOnce(&mut P) -> R) -> R {
1264        INSTANCE.with(|slot| {
1265            let mut slot = slot.borrow_mut();
1266            let plugin = slot
1267                .get_or_insert_with(|| Box::new(P::default()))
1268                .downcast_mut::<P>()
1269                .expect("one plugin type per component");
1270            f(plugin)
1271        })
1272    }
1273
1274    /// Connects a [`Plugin`] to the generated `Guest` trait.
1275    pub struct Adapter<P>(PhantomData<P>);
1276
1277    impl<P: Plugin> Guest for Adapter<P> {
1278        fn init(config_json: String) -> Result<Registration, String> {
1279            crate::log::install();
1280            INSTANCE.with(|slot| *slot.borrow_mut() = Some(Box::new(P::default())));
1281            with(|p: &mut P| p.init(&config_json))
1282        }
1283
1284        fn reload(config_json: String) -> Result<(), String> {
1285            with(|p: &mut P| p.reload(&config_json))
1286        }
1287
1288        fn shutdown(reason: String) {
1289            with(|p: &mut P| p.shutdown(&reason));
1290            let instance = INSTANCE.with(|slot| slot.borrow_mut().take());
1291            drop(instance);
1292        }
1293
1294        fn route(connection: Connection) -> RouteDecision {
1295            with(|p: &mut P| p.route(connection))
1296        }
1297
1298        fn login(ctx: Context) -> Action {
1299            with(|p: &mut P| p.login(ctx))
1300        }
1301
1302        fn initial_server(ctx: Context) -> Action {
1303            with(|p: &mut P| p.initial_server(ctx))
1304        }
1305
1306        fn before_transfer(ctx: Context) -> Action {
1307            with(|p: &mut P| p.before_transfer(ctx))
1308        }
1309
1310        fn command(ctx: Context) -> Action {
1311            with(|p: &mut P| p.command(ctx))
1312        }
1313
1314        fn suggest(ctx: Context) -> Vec<Suggestion> {
1315            with(|p: &mut P| p.suggest(ctx))
1316        }
1317
1318        fn status(ping: Ping) -> Option<StatusOverride> {
1319            with(|p: &mut P| p.status(ping))
1320        }
1321
1322        fn http(request: HttpRequest) -> Option<HttpResponse> {
1323            with(|p: &mut P| p.http(request))
1324        }
1325
1326        fn chat(attempt: ChatAttempt) -> Action {
1327            with(|p: &mut P| p.chat(attempt))
1328        }
1329
1330        fn kick(ctx: Context) -> Action {
1331            with(|p: &mut P| p.kick(ctx))
1332        }
1333
1334        fn join(ctx: Context) {
1335            with(|p: &mut P| p.join(ctx))
1336        }
1337
1338        fn after_transfer(ctx: Context) {
1339            with(|p: &mut P| p.after_transfer(ctx))
1340        }
1341
1342        fn disconnect(ctx: Context) {
1343            with(|p: &mut P| p.disconnect(ctx))
1344        }
1345
1346        fn message(message: BusMessage) {
1347            with(|p: &mut P| p.message(message))
1348        }
1349
1350        fn timer(handle: u64) {
1351            with(|p: &mut P| p.timer(handle))
1352        }
1353
1354        fn event(event: RiftEvent) {
1355            with(|p: &mut P| p.event(event))
1356        }
1357
1358        fn dialog(response: DialogResponse) {
1359            with(|p: &mut P| p.dialog(response))
1360        }
1361
1362        fn job_done(handle: u64, outcome: Result<Vec<u8>, String>) {
1363            with(|p: &mut P| p.job_done(handle, outcome))
1364        }
1365
1366        // A worker instance: `init` never ran here, so the logger is
1367        // installed and the instance created on the first of these calls.
1368        fn work_init(config_json: String) -> Result<(), String> {
1369            crate::log::install();
1370            with(|p: &mut P| p.work_init(&config_json))
1371        }
1372
1373        fn work(payload: Vec<u8>) -> Result<Vec<u8>, String> {
1374            crate::log::install();
1375            with(|p: &mut P| p.work(payload))
1376        }
1377    }
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382    use super::*;
1383    use serde_json::json;
1384
1385    #[test]
1386    fn builders_fill_the_wit_records() {
1387        let registration = Registration::new()
1388            .command(
1389                CommandSpec::new("hello")
1390                    .alias("hi")
1391                    .aliases(["hey", "yo"])
1392                    .permission("hello.use")
1393                    .suggest(SuggestKind::Players)
1394                    .console(),
1395            )
1396            .command(CommandSpec::new("plain"))
1397            .hooks([Hook::Login, Hook::Command])
1398            .hook(Hook::Chat);
1399        let hello = &registration.commands[0];
1400        assert_eq!(hello.name, "hello");
1401        assert_eq!(hello.aliases, ["hi", "hey", "yo"]);
1402        assert_eq!(hello.permission, "hello.use");
1403        assert!(matches!(hello.suggest, SuggestKind::Players));
1404        assert!(hello.console);
1405        let plain = &registration.commands[1];
1406        assert!(plain.aliases.is_empty() && plain.permission.is_empty() && !plain.console);
1407        assert!(matches!(plain.suggest, SuggestKind::None));
1408        assert_eq!(
1409            registration.hooks,
1410            vec![Hook::Login, Hook::Command, Hook::Chat]
1411        );
1412        assert!(Registration::default().commands.is_empty());
1413        assert!(Registration::default().hooks.is_empty());
1414    }
1415
1416    #[test]
1417    fn strings_are_plain_and_builders_are_json() {
1418        assert_eq!(Text::from("<red>hi"), Text::Plain("<red>hi".into()));
1419        assert_eq!(Text::from(String::from("a")), Text::plain("a"));
1420        assert_eq!(Text::from(&String::from("a")), Text::plain("a"));
1421        assert_eq!(
1422            Text::from(text::mini("<red>x")),
1423            Text::Json(r#"{"mini":"<red>x"}"#.into())
1424        );
1425        assert_eq!(
1426            Text::from(text::legacy("&cx")),
1427            Text::json(r#"{"legacy":"&cx","marker":"&"}"#)
1428        );
1429        assert_eq!(
1430            Text::from(&text::Component::text("x").bold()),
1431            Text::json(r#"{"bold":true,"text":"x"}"#)
1432        );
1433        assert_eq!(
1434            Text::from(json!({"text": "x"})),
1435            Text::json(r#"{"text":"x"}"#)
1436        );
1437        // A JSON string is still JSON text: shown as written by the host.
1438        assert_eq!(Text::from(&json!("a\"b")), Text::json(r#""a\"b""#));
1439        assert_ne!(Text::plain("x"), Text::json("x"));
1440    }
1441
1442    #[test]
1443    fn actions_take_any_text() {
1444        assert_eq!(Action::deny("no"), Action::Deny(Text::Plain("no".into())));
1445        assert_eq!(
1446            Action::deny(json!({"text": "Banned", "color": "red"})),
1447            Action::Deny(Text::Json(r#"{"color":"red","text":"Banned"}"#.into()))
1448        );
1449        assert_eq!(
1450            Action::message(format!("Hi, {}!", "Alex")),
1451            Action::Message(Text::Plain("Hi, Alex!".into()))
1452        );
1453        assert_eq!(
1454            Action::message(text::mini("<gold>Hi")),
1455            Action::Message(Text::Json(r#"{"mini":"<gold>Hi"}"#.into()))
1456        );
1457        assert_eq!(Action::server("lobby"), Action::Server("lobby".into()));
1458        assert_eq!(Action::queue("game"), Action::Queue("game".into()));
1459        assert_ne!(Action::server("a"), Action::queue("a"));
1460        assert_ne!(Action::deny("a"), Action::message("a"));
1461        assert_ne!(Action::Continue, Action::LeaveQueue);
1462    }
1463
1464    #[test]
1465    fn titles_suggestions_and_status_take_any_text() {
1466        let title = TitleSpec::new(text::mini("<red>T"))
1467            .subtitle("sub")
1468            .times(1, 2, 3);
1469        assert_eq!(title.title, Some(Text::json(r#"{"mini":"<red>T"}"#)));
1470        assert_eq!(title.subtitle, Some(Text::plain("sub")));
1471        assert_eq!((title.fade_in, title.stay, title.fade_out), (1, 2, 3));
1472        let plain = TitleSpec::new("T");
1473        assert_eq!(plain.subtitle, None);
1474        assert_eq!((plain.fade_in, plain.stay, plain.fade_out), (10, 70, 20));
1475        let subtitle = TitleSpec::subtitle_only("only").times(0, 20, 0);
1476        assert_eq!(subtitle.title, None);
1477        assert_eq!(subtitle.subtitle, Some(Text::plain("only")));
1478        assert_eq!(subtitle.stay, 20);
1479
1480        assert_eq!(Suggestion::new("a").tooltip, None);
1481        assert_eq!(
1482            Suggestion::new("a").tooltip("tip").tooltip,
1483            Some(Text::plain("tip"))
1484        );
1485        assert_eq!(
1486            Suggestion::new("a").tooltip(text::legacy("&7tip")).tooltip,
1487            Some(Text::json(r#"{"legacy":"&7tip","marker":"&"}"#))
1488        );
1489
1490        let status = StatusOverride {
1491            version: Some("Rift".into()),
1492            sample: Some(vec!["one".into(), text::mini("<green>two").into()]),
1493            ..Default::default()
1494        };
1495        assert_eq!(status.version, Some(Text::plain("Rift")));
1496        assert_eq!(status.sample.map(|lines| lines.len()), Some(2));
1497        let nothing = StatusOverride::default();
1498        assert!(nothing.motd.is_none() && nothing.version.is_none() && nothing.sample.is_none());
1499        assert!(nothing.protocol.is_none() && nothing.online.is_none());
1500        assert!(nothing.max_players.is_none() && nothing.favicon.is_none());
1501    }
1502
1503    #[test]
1504    fn hooks_have_defaults() {
1505        #[derive(Default)]
1506        struct Nothing;
1507        impl Plugin for Nothing {}
1508        let mut plugin = Nothing;
1509        let registration = plugin.init("{}").unwrap();
1510        assert!(registration.commands.is_empty() && registration.hooks.is_empty());
1511        assert_eq!(plugin.work_init("{}"), Ok(()));
1512        assert_eq!(
1513            plugin.work(b"job".to_vec()),
1514            Err("this plugin has no background work".to_owned())
1515        );
1516        plugin.job_done(1, Ok(Vec::new()));
1517        plugin.job_done(2, Err("failed".into()));
1518    }
1519
1520    #[test]
1521    fn text_builders_write_escaped_json() {
1522        let welcome = text::mini("<gold>Hi \"<player>\"</gold>")
1523            .arg("player", "St<b>eve\n")
1524            .arg_component("rank", text::legacy("&cAdmin"));
1525        let value: serde_json::Value = serde_json::from_str(&welcome.to_string()).unwrap();
1526        assert_eq!(
1527            value,
1528            json!({
1529                "mini": "<gold>Hi \"<player>\"</gold>",
1530                "args": {"player": "St<b>eve\n", "rank": {"legacy": "&cAdmin", "marker": "&"}},
1531            })
1532        );
1533        assert_eq!(text::mini("<red>x").to_string(), r#"{"mini":"<red>x"}"#);
1534        assert_eq!(
1535            text::legacy("§aHi").marker('§').to_string(),
1536            r#"{"legacy":"§aHi","marker":"§"}"#
1537        );
1538        let component = text::Component::text("[a\"b]")
1539            .color("#ff8800")
1540            .bold()
1541            .italic()
1542            .underlined()
1543            .strikethrough()
1544            .obfuscated()
1545            .suggest_command("/msg ")
1546            .hover(text::mini("<green>Hover"))
1547            .insertion("ins")
1548            .extra("plain")
1549            .extra(text::Component::text("child").open_url("https://example.org"))
1550            .field("font", "minecraft:uniform");
1551        let value: serde_json::Value = serde_json::from_str(&component.to_string()).unwrap();
1552        assert_eq!(
1553            value,
1554            json!({
1555                "text": "[a\"b]", "color": "#ff8800", "bold": true, "italic": true,
1556                "underlined": true, "strikethrough": true, "obfuscated": true,
1557                "click_event": {"action": "suggest_command", "command": "/msg "},
1558                "hover_event": {"action": "show_text", "value": {"mini": "<green>Hover"}},
1559                "insertion": "ins",
1560                "extra": ["plain", {"text": "child", "click_event": {"action": "open_url", "url": "https://example.org"}}],
1561                "font": "minecraft:uniform",
1562            })
1563        );
1564        let value: serde_json::Value = text::Component::text("c")
1565            .copy_to_clipboard("v")
1566            .run_command("/x")
1567            .hover_text("t")
1568            .into();
1569        assert_eq!(
1570            value["click_event"],
1571            json!({"action": "run_command", "command": "/x"})
1572        );
1573        assert_eq!(
1574            value["hover_event"],
1575            json!({"action": "show_text", "value": "t"})
1576        );
1577    }
1578
1579    #[test]
1580    fn config_decodes_json() {
1581        #[derive(serde::Deserialize)]
1582        struct Config {
1583            #[serde(default)]
1584            deny: Option<String>,
1585        }
1586        let config: Config = config(r#"{"deny":"Mallory"}"#).unwrap();
1587        assert_eq!(config.deny.as_deref(), Some("Mallory"));
1588        let empty: Config = super::config("{}").unwrap();
1589        assert_eq!(empty.deny, None);
1590        assert!(super::config::<Config>("nope").is_err());
1591    }
1592}