#![warn(missing_docs)]
#[allow(missing_docs, clippy::all)]
pub mod bindings {
wit_bindgen::generate!({
path: "wit",
world: "rift:plugin/plugin@0.1.5",
pub_export_macro: true,
export_macro_name: "__export_rift_plugin",
default_bindings_module: "rift_plugin::bindings",
generate_all,
});
}
#[doc(hidden)]
#[allow(missing_docs, clippy::all)]
pub mod chat_bindings {
wit_bindgen::generate!({
path: "wit",
world: "rift:plugin/optional-chat@0.1.5",
pub_export_macro: true,
export_macro_name: "__export_rift_plugin_chat",
default_bindings_module: "rift_plugin::chat_bindings",
with: {
"rift:plugin/types@0.1.5": crate::bindings::rift::plugin::types,
},
});
}
pub use bindings::rift::plugin::types::{
BossBarSpec, BusMessage, ChatAttempt, ChatKind, CommandSpec, Connection, Context,
DialogResponse, Hook, HttpRequest, HttpResponse, Level, Ping, PlayerInfo, Registration,
RichStatusOverride, RichSuggestion, RiftEvent, RouteDecision, StatusOverride, SuggestKind,
Suggestion, TitleJsonSpec, TitleSpec,
};
pub type Action = bindings::rift::plugin::types::Decision;
use serde::de::DeserializeOwned;
#[allow(unused_variables)]
pub trait Plugin: Default + 'static {
fn init(&mut self, config: &str) -> Result<Registration, String> {
Ok(Registration::new())
}
fn reload(&mut self, config: &str) -> Result<(), String> {
Ok(())
}
fn shutdown(&mut self, reason: &str) {}
fn route(&mut self, connection: Connection) -> RouteDecision {
RouteDecision::Default
}
fn login(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn initial_server(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn before_transfer(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn command(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn suggest(&mut self, ctx: Context) -> Vec<Suggestion> {
Vec::new()
}
fn status(&mut self, ping: Ping) -> Option<StatusOverride> {
None
}
fn suggest_rich(&mut self, ctx: Context) -> Vec<RichSuggestion> {
self.suggest(ctx)
.into_iter()
.map(RichSuggestion::from)
.collect()
}
fn status_rich(&mut self, ping: Ping) -> Option<RichStatusOverride> {
self.status(ping).map(RichStatusOverride::from)
}
fn http(&mut self, request: HttpRequest) -> Option<HttpResponse> {
None
}
fn join(&mut self, ctx: Context) {}
fn after_transfer(&mut self, ctx: Context) {}
fn disconnect(&mut self, ctx: Context) {}
fn message(&mut self, message: BusMessage) {}
fn timer(&mut self, handle: u64) {}
fn event(&mut self, event: RiftEvent) {}
fn dialog(&mut self, response: DialogResponse) {}
fn chat(&mut self, attempt: ChatAttempt) -> Action {
Action::Continue
}
}
#[macro_export]
macro_rules! export {
($plugin:ty) => {
#[doc(hidden)]
type __RiftPluginExport = $crate::__private::Adapter<$plugin>;
$crate::bindings::__export_rift_plugin!(__RiftPluginExport with_types_in $crate::bindings);
};
($plugin:ty, chat) => {
$crate::export!($plugin);
$crate::chat_bindings::__export_rift_plugin_chat!(__RiftPluginExport with_types_in $crate::chat_bindings);
};
}
pub fn config<T: DeserializeOwned>(json: &str) -> Result<T, String> {
serde_json::from_str(json).map_err(|error| format!("invalid plugin configuration: {error}"))
}
impl Registration {
pub fn new() -> Self {
Self {
commands: Vec::new(),
hooks: Vec::new(),
}
}
pub fn command(mut self, command: CommandSpec) -> Self {
self.commands.push(command);
self
}
pub fn hook(mut self, hook: Hook) -> Self {
self.hooks.push(hook);
self
}
pub fn hooks(mut self, hooks: impl IntoIterator<Item = Hook>) -> Self {
self.hooks.extend(hooks);
self
}
}
impl Default for Registration {
fn default() -> Self {
Self::new()
}
}
impl CommandSpec {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
permission: String::new(),
suggest: SuggestKind::None,
}
}
pub fn permission(mut self, node: impl Into<String>) -> Self {
self.permission = node.into();
self
}
pub fn suggest(mut self, kind: SuggestKind) -> Self {
self.suggest = kind;
self
}
}
impl Action {
pub fn deny(reason: impl Into<String>) -> Self {
Self::Deny(reason.into())
}
pub fn deny_json(reason: impl ToString) -> Self {
Self::DenyJson(reason.to_string())
}
pub fn message(text: impl Into<String>) -> Self {
Self::Message(text.into())
}
pub fn message_json(text: impl ToString) -> Self {
Self::MessageJson(text.to_string())
}
pub fn server(name: impl Into<String>) -> Self {
Self::Server(name.into())
}
}
impl DialogResponse {
pub fn values<T: DeserializeOwned>(&self) -> Result<T, String> {
serde_json::from_str(&self.values_json)
.map_err(|error| format!("invalid dialog values: {error}"))
}
pub fn payload<T: DeserializeOwned>(&self) -> Result<Option<T>, String> {
self.payload_json
.as_deref()
.map(serde_json::from_str)
.transpose()
.map_err(|error| format!("invalid dialog payload: {error}"))
}
}
impl Suggestion {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
tooltip: None,
}
}
}
impl RichSuggestion {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
tooltip_json: None,
}
}
pub fn tooltip(mut self, tooltip: impl ToString) -> Self {
self.tooltip_json = Some(tooltip.to_string());
self
}
}
impl From<Suggestion> for RichSuggestion {
fn from(suggestion: Suggestion) -> Self {
Self {
text: suggestion.text,
tooltip_json: suggestion.tooltip.map(|tooltip| text::quote(&tooltip)),
}
}
}
impl From<StatusOverride> for RichStatusOverride {
fn from(status: StatusOverride) -> Self {
Self {
motd_json: status.motd_json,
version_json: status.version.map(|version| text::quote(&version)),
protocol: status.protocol,
online: status.online,
max_players: status.max_players,
sample_json: status
.sample
.map(|lines| lines.iter().map(|line| text::quote(line)).collect()),
favicon: status.favicon,
}
}
}
impl Default for RichStatusOverride {
fn default() -> Self {
Self {
motd_json: None,
version_json: None,
protocol: None,
online: None,
max_players: None,
sample_json: None,
favicon: None,
}
}
}
impl TitleJsonSpec {
pub fn new(title: impl ToString) -> Self {
Self {
title_json: title.to_string(),
subtitle_json: None,
fade_in: 10,
stay: 70,
fade_out: 20,
}
}
pub fn subtitle(mut self, subtitle: impl ToString) -> Self {
self.subtitle_json = Some(subtitle.to_string());
self
}
pub fn times(mut self, fade_in: i32, stay: i32, fade_out: i32) -> Self {
self.fade_in = fade_in;
self.stay = stay;
self.fade_out = fade_out;
self
}
}
pub mod players {
pub use crate::bindings::rift::plugin::players::{
action_bar, action_bar_json, boss_bar, broadcast, broadcast_json, clear_dialog, dialog,
get, kick, kick_json, list, locale, message, message_json, names, tab_list, title,
title_json, transfer, visible_to,
};
pub fn show_dialog(target: &str, dialog: &serde_json::Value) -> Result<bool, String> {
self::dialog(target, &dialog.to_string())
}
}
pub mod store {
pub use crate::bindings::rift::plugin::store::{delete, get, increment, list, set};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub fn get_json<T: DeserializeOwned>(namespace: &str, key: &str) -> Result<Option<T>, String> {
match get(namespace, key)? {
Some(bytes) => serde_json::from_slice(&bytes)
.map(Some)
.map_err(|error| format!("invalid JSON in {namespace}/{key}: {error}")),
None => Ok(None),
}
}
pub fn set_json<T: Serialize + ?Sized>(
namespace: &str,
key: &str,
value: &T,
) -> Result<(), String> {
let bytes = serde_json::to_vec(value).map_err(|error| error.to_string())?;
set(namespace, key, &bytes)
}
}
pub mod permissions {
pub use crate::bindings::rift::plugin::permissions::{
add_group, groups, has, list_groups, meta, remove_group, set, set_meta,
};
pub const VISIBLE_TO: &str = "rift.visible-to";
}
pub mod bus {
use crate::BusMessage;
pub use crate::bindings::rift::plugin::bus::{
publish, request, stream_publish, subscribe, unsubscribe,
};
pub fn reply(message: &BusMessage, payload: &[u8]) -> Result<Option<u32>, String> {
match &message.reply {
Some(subject) => publish(subject, payload, None).map(Some),
None => Ok(None),
}
}
}
pub mod servers {
pub use crate::bindings::rift::plugin::servers::{
control, create, download, remove, start, stop,
};
}
pub mod timers {
pub use crate::bindings::rift::plugin::timers::{after, cancel, every};
}
pub mod meta {
pub use crate::bindings::rift::plugin::meta::{
clock, config_dir, data_dir, name, time, version,
};
}
pub mod log {
pub use crate::bindings::rift::plugin::log::log;
#[doc(no_inline)]
pub use ::log::{debug, error, info, trace, warn};
use crate::Level;
struct HostLogger;
static LOGGER: HostLogger = HostLogger;
impl ::log::Log for HostLogger {
fn enabled(&self, _metadata: &::log::Metadata) -> bool {
true
}
fn log(&self, record: &::log::Record) {
let level = match record.level() {
::log::Level::Trace => Level::Trace,
::log::Level::Debug => Level::Debug,
::log::Level::Info => Level::Info,
::log::Level::Warn => Level::Warn,
::log::Level::Error => Level::Error,
};
match record.args().as_str() {
Some(message) => log(level, message),
None => log(level, &record.args().to_string()),
}
}
fn flush(&self) {}
}
pub(crate) fn install() {
if ::log::set_logger(&LOGGER).is_ok() {
::log::set_max_level(::log::LevelFilter::Trace);
}
}
}
pub mod text {
use serde_json::{Map, Value};
use std::fmt;
pub fn mini(markup: impl Into<String>) -> Mini {
Mini {
markup: markup.into(),
args: Map::new(),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mini {
markup: String,
args: Map<String, Value>,
}
impl Mini {
pub fn arg(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
self.args.insert(name.into(), Value::String(text.into()));
self
}
pub fn arg_component(
mut self,
name: impl Into<String>,
component: impl Into<Value>,
) -> Self {
self.args.insert(name.into(), component.into());
self
}
pub fn to_value(&self) -> Value {
let mut object = Map::new();
object.insert("mini".into(), Value::String(self.markup.clone()));
if !self.args.is_empty() {
object.insert("args".into(), Value::Object(self.args.clone()));
}
Value::Object(object)
}
}
pub fn legacy(text: impl Into<String>) -> Legacy {
Legacy {
text: text.into(),
marker: '&',
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Legacy {
text: String,
marker: char,
}
impl Legacy {
pub fn marker(mut self, marker: char) -> Self {
self.marker = marker;
self
}
pub fn to_value(&self) -> Value {
let mut object = Map::new();
object.insert("legacy".into(), Value::String(self.text.clone()));
object.insert("marker".into(), Value::String(self.marker.to_string()));
Value::Object(object)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Component(Map<String, Value>);
impl Component {
pub fn text(text: impl Into<String>) -> Self {
let mut fields = Map::new();
fields.insert("text".into(), Value::String(text.into()));
Self(fields)
}
pub fn color(self, color: impl Into<String>) -> Self {
self.field("color", color.into())
}
pub fn bold(self) -> Self {
self.field("bold", true)
}
pub fn italic(self) -> Self {
self.field("italic", true)
}
pub fn underlined(self) -> Self {
self.field("underlined", true)
}
pub fn strikethrough(self) -> Self {
self.field("strikethrough", true)
}
pub fn obfuscated(self) -> Self {
self.field("obfuscated", true)
}
fn click(self, action: &str, key: &str, value: impl Into<String>) -> Self {
let mut click = Map::new();
click.insert("action".into(), Value::String(action.into()));
click.insert(key.into(), Value::String(value.into()));
self.field("click_event", Value::Object(click))
}
pub fn run_command(self, command: impl Into<String>) -> Self {
self.click("run_command", "command", command)
}
pub fn suggest_command(self, command: impl Into<String>) -> Self {
self.click("suggest_command", "command", command)
}
pub fn open_url(self, url: impl Into<String>) -> Self {
self.click("open_url", "url", url)
}
pub fn copy_to_clipboard(self, text: impl Into<String>) -> Self {
self.click("copy_to_clipboard", "value", text)
}
pub fn hover_text(self, text: impl Into<String>) -> Self {
self.hover(Value::String(text.into()))
}
pub fn hover(self, component: impl Into<Value>) -> Self {
let mut hover = Map::new();
hover.insert("action".into(), Value::String("show_text".into()));
hover.insert("value".into(), component.into());
self.field("hover_event", Value::Object(hover))
}
pub fn insertion(self, text: impl Into<String>) -> Self {
self.field("insertion", text.into())
}
pub fn extra(mut self, child: impl Into<Value>) -> Self {
let child = child.into();
match self.0.get_mut("extra") {
Some(Value::Array(children)) => children.push(child),
_ => {
self.0.insert("extra".into(), Value::Array(vec![child]));
}
}
self
}
pub fn field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.0.insert(key.into(), value.into());
self
}
pub fn to_value(&self) -> Value {
Value::Object(self.0.clone())
}
}
macro_rules! json_text {
($($builder:ty),*) => {$(
impl fmt::Display for $builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.to_value(), f)
}
}
impl From<$builder> for Value {
fn from(builder: $builder) -> Value {
builder.to_value()
}
}
)*};
}
json_text!(Mini, Legacy, Component);
pub(crate) fn quote(text: &str) -> String {
Value::String(text.to_owned()).to_string()
}
pub(crate) fn strip_markup(
markup: &str,
out: &mut String,
mut arg: impl FnMut(&str, &mut String),
) {
let chars: Vec<char> = markup.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '\\' && matches!(chars.get(i + 1), Some('<' | '\\')) {
out.push(chars[i + 1]);
i += 2;
continue;
}
if c != '<' {
out.push(c);
i += 1;
continue;
}
let mut end = None;
let mut quote = None;
let mut j = i + 1;
while j < chars.len() {
match (chars[j], quote) {
(q @ ('\'' | '"'), None) => quote = Some(q),
(q, Some(open)) if q == open => quote = None,
(_, Some(_)) => {}
('>', None) => {
end = Some(j);
break;
}
(space, None) if space.is_whitespace() => break,
_ => {}
}
j += 1;
}
match end {
Some(end) if end > i + 1 => {
let tag: String = chars[i + 1..end].iter().collect();
let name = tag
.split(':')
.next()
.unwrap_or_default()
.to_ascii_lowercase();
match name.as_str() {
"newline" | "br" => out.push('\n'),
_ if name.starts_with(['/', '!']) => {}
_ if !tag.contains(':') => arg(&name, out),
_ => {}
}
i = end + 1;
}
_ => {
out.push('<');
i += 1;
}
}
}
}
pub(crate) fn strip_legacy(text: &str, marker: char, out: &mut String) {
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == marker {
match chars.get(i + 1) {
Some('#')
if chars.len() >= i + 8
&& chars[i + 2..i + 8].iter().all(|c| c.is_ascii_hexdigit()) =>
{
i += 8;
continue;
}
Some(code) if "0123456789abcdefklmnorx".contains(code.to_ascii_lowercase()) => {
i += 2;
continue;
}
_ => {}
}
}
out.push(chars[i]);
i += 1;
}
}
}
#[doc(hidden)]
pub mod __private {
use crate::bindings::exports::rift::plugin::handler::Guest;
use crate::*;
use std::any::Any;
use std::cell::RefCell;
use std::marker::PhantomData;
thread_local! {
static INSTANCE: RefCell<Option<Box<dyn Any>>> = const { RefCell::new(None) };
}
fn with<P: Plugin, R>(f: impl FnOnce(&mut P) -> R) -> R {
INSTANCE.with(|slot| {
let mut slot = slot.borrow_mut();
let plugin = slot
.get_or_insert_with(|| Box::new(P::default()))
.downcast_mut::<P>()
.expect("one plugin type per component");
f(plugin)
})
}
pub struct Adapter<P>(PhantomData<P>);
impl<P: Plugin> Guest for Adapter<P> {
fn init(config_json: String) -> Result<Registration, String> {
crate::log::install();
INSTANCE.with(|slot| *slot.borrow_mut() = Some(Box::new(P::default())));
with(|p: &mut P| p.init(&config_json))
}
fn reload(config_json: String) -> Result<(), String> {
with(|p: &mut P| p.reload(&config_json))
}
fn shutdown(reason: String) {
with(|p: &mut P| p.shutdown(&reason));
let instance = INSTANCE.with(|slot| slot.borrow_mut().take());
drop(instance);
}
fn route(connection: Connection) -> RouteDecision {
with(|p: &mut P| p.route(connection))
}
fn login(ctx: Context) -> PlainAction {
plain(with(|p: &mut P| p.login(ctx)))
}
fn initial_server(ctx: Context) -> PlainAction {
plain(with(|p: &mut P| p.initial_server(ctx)))
}
fn before_transfer(ctx: Context) -> PlainAction {
plain(with(|p: &mut P| p.before_transfer(ctx)))
}
fn command(ctx: Context) -> PlainAction {
plain(with(|p: &mut P| p.command(ctx)))
}
fn suggest(ctx: Context) -> Vec<Suggestion> {
let suggestions = with(|p: &mut P| p.suggest_rich(ctx));
suggestions
.into_iter()
.map(|suggestion| Suggestion {
text: suggestion.text,
tooltip: suggestion
.tooltip_json
.map(|tooltip| words(&tooltip))
.filter(|tooltip| !tooltip.is_empty()),
})
.collect()
}
fn status(ping: Ping) -> Option<StatusOverride> {
let status = with(|p: &mut P| p.status_rich(ping))?;
Some(StatusOverride {
motd_json: status.motd_json,
version: status.version_json.map(|version| words(&version)),
protocol: status.protocol,
online: status.online,
max_players: status.max_players,
sample: status
.sample_json
.map(|lines| lines.iter().map(|line| words(line)).collect()),
favicon: status.favicon,
})
}
fn http(request: HttpRequest) -> Option<HttpResponse> {
with(|p: &mut P| p.http(request))
}
fn join(ctx: Context) {
with(|p: &mut P| p.join(ctx))
}
fn after_transfer(ctx: Context) {
with(|p: &mut P| p.after_transfer(ctx))
}
fn disconnect(ctx: Context) {
with(|p: &mut P| p.disconnect(ctx))
}
fn message(message: BusMessage) {
with(|p: &mut P| p.message(message))
}
fn timer(handle: u64) {
with(|p: &mut P| p.timer(handle))
}
fn event(event: RiftEvent) {
with(|p: &mut P| p.event(event))
}
}
impl<P: Plugin> crate::bindings::exports::rift::plugin::dialogs::Guest for Adapter<P> {
fn dialog(response: DialogResponse) {
with(|p: &mut P| p.dialog(response))
}
}
impl<P: Plugin> crate::chat_bindings::exports::rift::plugin::chat::Guest for Adapter<P> {
fn chat(attempt: ChatAttempt) -> Action {
with(|p: &mut P| p.chat(attempt))
}
}
impl<P: Plugin> crate::bindings::exports::rift::plugin::decisions::Guest for Adapter<P> {
fn login(ctx: Context) -> Action {
with(|p: &mut P| p.login(ctx))
}
fn initial_server(ctx: Context) -> Action {
with(|p: &mut P| p.initial_server(ctx))
}
fn before_transfer(ctx: Context) -> Action {
with(|p: &mut P| p.before_transfer(ctx))
}
fn command(ctx: Context) -> Action {
with(|p: &mut P| p.command(ctx))
}
}
impl<P: Plugin> crate::bindings::exports::rift::plugin::styled::Guest for Adapter<P> {
fn suggest(ctx: Context) -> Vec<RichSuggestion> {
with(|p: &mut P| p.suggest_rich(ctx))
}
fn status(ping: Ping) -> Option<RichStatusOverride> {
with(|p: &mut P| p.status_rich(ping))
}
}
use crate::bindings::rift::plugin::types::Action as PlainAction;
pub(crate) fn plain(action: Action) -> PlainAction {
match action {
Action::Continue => PlainAction::Continue,
Action::Deny(reason) => PlainAction::Deny(reason),
Action::Server(server) => PlainAction::Server(server),
Action::Message(text) => PlainAction::Message(text),
Action::Queue(server) => PlainAction::Queue(server),
Action::LeaveQueue => PlainAction::LeaveQueue,
Action::DenyJson(reason) => PlainAction::Deny(words(&reason)),
Action::MessageJson(text) => PlainAction::Message(words(&text)),
}
}
pub(crate) fn words(json: &str) -> String {
fn collect(value: &serde_json::Value, out: &mut String) {
match value {
serde_json::Value::String(text) => out.push_str(text),
serde_json::Value::Array(items) => items.iter().for_each(|item| collect(item, out)),
serde_json::Value::Object(fields) => {
if let Some(markup) = fields.get("mini").and_then(|value| value.as_str()) {
let args = fields.get("args").and_then(|args| args.as_object());
crate::text::strip_markup(markup, out, |name, out| {
match args.and_then(|args| {
args.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value)
}) {
Some(serde_json::Value::String(text)) => out.push_str(text),
Some(
value @ (serde_json::Value::Object(_)
| serde_json::Value::Array(_)),
) => collect(value, out),
Some(value) => out.push_str(&value.to_string()),
None => {}
}
});
return;
}
if let Some(legacy) = fields.get("legacy").and_then(|value| value.as_str()) {
let marker = fields
.get("marker")
.and_then(|value| value.as_str())
.and_then(|marker| marker.chars().next())
.unwrap_or('&');
crate::text::strip_legacy(legacy, marker, out);
return;
}
if let Some(text) = ["text", "fallback", "translate"]
.iter()
.find_map(|key| fields.get(*key).and_then(|value| value.as_str()))
{
out.push_str(text);
}
if let Some(extra) = fields.get("extra") {
collect(extra, out);
}
}
_ => {}
}
}
match serde_json::from_str(json) {
Ok(value) => {
let mut out = String::new();
collect(&value, &mut out);
out
}
Err(_) => json.to_owned(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builders_fill_the_wit_records() {
let registration = Registration::new()
.command(
CommandSpec::new("hello")
.permission("hello.use")
.suggest(SuggestKind::Players),
)
.hooks([Hook::Login, Hook::Command]);
assert_eq!(registration.commands[0].name, "hello");
assert_eq!(registration.commands[0].permission, "hello.use");
assert!(matches!(
registration.commands[0].suggest,
SuggestKind::Players
));
assert_eq!(registration.hooks, vec![Hook::Login, Hook::Command]);
assert!(Registration::default().commands.is_empty());
}
#[test]
fn older_hosts_get_the_words_of_rich_decisions() {
use crate::bindings::rift::plugin::types::Action as PlainAction;
let banned = Action::deny_json(serde_json::json!({
"text": "Banned", "color": "red", "extra": [{"text": ": spam"}],
}));
assert!(matches!(
crate::__private::plain(banned),
PlainAction::Deny(reason) if reason == "Banned: spam"
));
assert!(matches!(
crate::__private::plain(Action::message_json("[\"a\",{\"text\":\"b\"}]")),
PlainAction::Message(text) if text == "ab"
));
assert!(matches!(
crate::__private::plain(Action::deny("x")),
PlainAction::Deny(reason) if reason == "x"
));
}
#[test]
fn text_builders_write_escaped_json() {
use serde_json::json;
let welcome = text::mini("<gold>Hi \"<player>\"</gold>")
.arg("player", "St<b>eve\n")
.arg_component("rank", text::legacy("&cAdmin"));
let value: serde_json::Value = serde_json::from_str(&welcome.to_string()).unwrap();
assert_eq!(
value,
json!({
"mini": "<gold>Hi \"<player>\"</gold>",
"args": {"player": "St<b>eve\n", "rank": {"legacy": "&cAdmin", "marker": "&"}},
})
);
assert_eq!(text::mini("<red>x").to_string(), r#"{"mini":"<red>x"}"#);
assert_eq!(
text::legacy("§aHi").marker('§').to_string(),
r#"{"legacy":"§aHi","marker":"§"}"#
);
let component = text::Component::text("[a\"b]")
.color("#ff8800")
.bold()
.italic()
.underlined()
.strikethrough()
.obfuscated()
.suggest_command("/msg ")
.hover(text::mini("<green>Hover"))
.insertion("ins")
.extra("plain")
.extra(text::Component::text("child").open_url("https://example.org"))
.field("font", "minecraft:uniform");
let value: serde_json::Value = serde_json::from_str(&component.to_string()).unwrap();
assert_eq!(
value,
json!({
"text": "[a\"b]", "color": "#ff8800", "bold": true, "italic": true,
"underlined": true, "strikethrough": true, "obfuscated": true,
"click_event": {"action": "suggest_command", "command": "/msg "},
"hover_event": {"action": "show_text", "value": {"mini": "<green>Hover"}},
"insertion": "ins",
"extra": ["plain", {"text": "child", "click_event": {"action": "open_url", "url": "https://example.org"}}],
"font": "minecraft:uniform",
})
);
let value: serde_json::Value = text::Component::text("c")
.copy_to_clipboard("v")
.run_command("/x")
.hover_text("t")
.into();
assert_eq!(
value["click_event"],
json!({"action": "run_command", "command": "/x"})
);
assert_eq!(
value["hover_event"],
json!({"action": "show_text", "value": "t"})
);
}
#[test]
fn older_hosts_get_the_words_of_rich_text() {
use crate::__private::words;
let mini = text::mini(
"<gold>Hi <player>!</gold><br>a < b \\<c> <hover:show_text:'<red>x y'>z</hover>",
)
.arg("player", "Steve")
.arg_component("unused", text::Component::text("no"));
assert_eq!(words(&mini.to_string()), "Hi Steve!\na < b <c> z");
assert_eq!(
words(&text::legacy("&cRed &#ff8800hex &zkeep").to_string()),
"Red hex &zkeep"
);
assert_eq!(words("\"<red>shown\""), "<red>shown");
let rich = RichSuggestion::from(Suggestion {
text: "a".into(),
tooltip: Some("{\"not\":\"json\"}".into()),
});
assert_eq!(
rich.tooltip_json.as_deref(),
Some(r#""{\"not\":\"json\"}""#)
);
assert_eq!(
words(rich.tooltip_json.as_deref().unwrap()),
"{\"not\":\"json\"}"
);
let status = RichStatusOverride::from(StatusOverride {
motd_json: None,
version: Some("Rift".into()),
protocol: None,
online: None,
max_players: None,
sample: Some(vec!["one".into()]),
favicon: None,
});
assert_eq!(status.version_json.as_deref(), Some("\"Rift\""));
assert_eq!(status.sample_json, Some(vec!["\"one\"".to_owned()]));
let title = TitleJsonSpec::new(text::mini("<red>T"))
.subtitle("sub")
.times(1, 2, 3);
assert_eq!(title.title_json, r#"{"mini":"<red>T"}"#);
assert_eq!(title.subtitle_json.as_deref(), Some("sub"));
assert_eq!((title.fade_in, title.stay, title.fade_out), (1, 2, 3));
}
#[test]
fn config_decodes_json() {
#[derive(serde::Deserialize)]
struct Config {
#[serde(default)]
deny: Option<String>,
}
let config: Config = config(r#"{"deny":"Mallory"}"#).unwrap();
assert_eq!(config.deny.as_deref(), Some("Mallory"));
let empty: Config = super::config("{}").unwrap();
assert_eq!(empty.deny, None);
assert!(super::config::<Config>("nope").is_err());
}
}