use registry::Declared;
use crate::locks::RelockRw;
pub mod sheet;
use core::ffi::c_void;
use std::cell::RefCell;
use std::sync::{Arc, RwLock};
use crate::PlayerKey;
use crate::abi::{ActionCall, Str, read_headed};
use crate::i18n::{Language, Texts};
use crate::world::Player;
use crate::{Load, Off};
const PLATFORM_TEXTS: &str = r#"
[en]
"actions.usage" = "Actions, each after the mod's id:"
"actions.unknown" = "No action {word}; help lists them."
"actions.denied" = "You may not do that."
"actions.no_player" = "No player named {name}."
"actions.several" = "Several players are named {name}."
"actions.bad" = "Write it so: {usage}"
"actions.player" = "player"
"actions.on" = "on"
"actions.off" = "off"
[uk]
"actions.usage" = "Дії, кожна після id мода:"
"actions.unknown" = "Немає дії {word}; help перелічує їх."
"actions.denied" = "Тобі цього не можна."
"actions.no_player" = "Немає гравця на ім'я {name}."
"actions.several" = "Кілька гравців мають ім'я {name}."
"actions.bad" = "Пиши так: {usage}"
"actions.player" = "гравець"
"actions.on" = "on"
"actions.off" = "off"
"#;
pub const MAX_ID: usize = 32;
pub const MAX_PLAYERS: usize = 32;
type Allow = Box<dyn Fn(&Player) -> bool + Send + Sync>;
type At = Box<dyn Fn(&Ask<'_>) -> Result<Subject, String> + Send + Sync>;
type On = Box<dyn Fn(&Ask<'_>, Option<&Subject>) -> Vec<(PlayerKey, String)> + Send + Sync>;
type Handler = Box<dyn Fn(&Request<'_>) -> Outcome + Send + Sync>;
pub struct Action {
id: String,
label: String,
help: String,
args: Vec<Arg>,
hint: bool,
allow: Option<Allow>,
at: Option<At>,
on: Option<On>,
}
impl core::fmt::Debug for Action {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Action")
.field("id", &self.id)
.field("label", &self.label)
.field("args", &self.args)
.field("hint", &self.hint)
.field("per_player", &self.on.is_some())
.finish_non_exhaustive()
}
}
impl Action {
#[must_use]
pub fn new(id: &str, label: &str) -> Self {
Self {
id: id.to_owned(),
label: label.to_owned(),
help: String::new(),
args: Vec::new(),
hint: false,
allow: None,
at: None,
on: None,
}
}
#[must_use]
pub fn per_player<F>(id: &str, label: &str, on: F) -> Self
where
F: Fn(&Ask<'_>, Option<&Subject>) -> Vec<(PlayerKey, String)> + Send + Sync + 'static,
{
let mut action = Self::new(id, label);
action.on = Some(Box::new(on));
action
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn help(mut self, help: &str) -> Self {
help.clone_into(&mut self.help);
self
}
#[must_use]
pub fn arg(mut self, arg: Arg) -> Self {
self.args.push(arg);
self
}
#[must_use]
pub fn allow<F>(mut self, allow: F) -> Self
where
F: Fn(&Player) -> bool + Send + Sync + 'static,
{
self.allow = Some(Box::new(allow));
self
}
#[must_use]
pub fn admins(self) -> Self {
self.allow(Player::is_admin)
}
#[must_use]
pub fn at<F>(mut self, at: F) -> Self
where
F: Fn(&Ask<'_>) -> Result<Subject, String> + Send + Sync + 'static,
{
self.at = Some(Box::new(at));
self
}
#[must_use]
pub fn hint(mut self) -> Self {
self.hint = true;
self
}
}
#[derive(Clone)]
pub struct Actions {
registry: Arc<Registry>,
}
impl core::fmt::Debug for Actions {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Actions").finish_non_exhaustive()
}
}
impl Actions {
pub fn add<F>(&self, action: Action, handler: F) -> Result<(), Off>
where
F: Fn(&Request<'_>) -> Outcome + Send + Sync + 'static,
{
let id = action.id.clone();
if !is_valid_id(&id) || id == "help" {
return Err(Off::new(format!(
"action {id}: ids are 1 to 32 of [a-z0-9-], not help"
)));
}
if action.on.is_some() && !action.args.is_empty() {
return Err(Off::new(format!("action {id}: a list takes no arguments")));
}
if action.hint && action.at.is_none() {
return Err(Off::new(format!("action {id}: a hint needs a subject")));
}
let mut declared = self.registry.declared.relock_write();
if declared.iter().any(|d| d.action.id == id) {
return Err(Off::new(format!("action {id}: the id is taken")));
}
declared.push(Declared {
action,
handler: Box::new(handler),
});
Ok(())
}
}
thread_local! {
static REPLY: RefCell<String> = const { RefCell::new(String::new()) };
}
unsafe extern "system" fn answer(call: *const ActionCall, user: *mut c_void, out: *mut Str) {
let Some(call) = (unsafe { read_headed(call, size_of::<ActionCall>()) }) else {
return;
};
let registry = unsafe { &*user.cast::<Arc<Registry>>() };
let player = Player::from_info(&call.player);
let language = Language(call.language);
let action = unsafe { call.action.as_str() }.unwrap_or("");
let args = unsafe { call.args.as_str() }.unwrap_or("");
let reply = crate::host::guard("panic in an action", String::new, || match call.kind {
ActionCall::SHEET => registry.sheet(&player, language),
ActionCall::RUN => registry.run_from_interface(&player, action, args, language),
ActionCall::HINTS => registry.hints(&player, language),
ActionCall::CONSOLE => {
let outcome = registry.run_from_console(&player, action);
format!("{}{}", u8::from(outcome.done), outcome.text)
}
_ => String::new(),
});
if out.is_null() {
return;
}
REPLY.with_borrow_mut(|kept| {
*kept = reply;
unsafe { out.write(Str::new(kept)) };
});
}
impl Load<'_> {
pub fn actions(&mut self, texts: Texts) -> Result<Actions, Off> {
let mut all = Texts::parse(PLATFORM_TEXTS)?;
all.merge(texts);
let host = self.host();
let registry = Arc::new(Registry {
texts: all,
host,
declared: RwLock::new(Vec::new()),
});
crate::load::hand_over(
Arc::clone(®istry),
|user| (host.api().actions)(host.handle(), answer, user),
"actions",
)?;
Ok(Actions { registry })
}
}
fn is_valid_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= MAX_ID
&& id
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
#[cfg(test)]
mod tests {
use super::registry::{fits, toggle_word};
use super::*;
#[test]
fn toggle_words_read_both_ways() {
let texts = Texts::parse(PLATFORM_TEXTS).unwrap();
assert_eq!(toggle_word("ON", &texts, Language::FALLBACK), Some(true));
assert_eq!(toggle_word("deny", &texts, Language::UK_UA), Some(false));
assert_eq!(toggle_word("maybe", &texts, Language::FALLBACK), None);
}
#[test]
fn values_fit_their_arguments() {
let number = Arg::number("n", 5.0..=1.0, -1.0);
assert_eq!(number.range, (1.0, 5.0));
assert!((number.step - 1.0).abs() < f64::EPSILON);
assert!(fits(&Value::Number(3.0), &number));
assert!(!fits(&Value::Number(6.0), &number));
assert!(!fits(&Value::Toggle(true), &number));
let choice = Arg::choice("c", &[("a", "A")]);
assert!(fits(&Value::Choice(0), &choice));
assert!(!fits(&Value::Choice(1), &choice));
}
}
mod registry;
mod request;
use registry::Registry;
pub use request::*;