use std::{
borrow::Cow,
ops::{Deref, DerefMut},
};
use crate::schema::{self, ClientCommandContents, ServerCommand};
mod glue;
pub use glue::{ActionMetadata, Actions};
use schemars::schema::{Schema, SchemaObject, SingleOrVec};
use thiserror::Error;
#[neuro_sama_derive::generic_mutability(GameMut)]
pub trait Game: Sized {
const NAME: &'static str;
type Actions<'a>: Actions<'a>;
fn handle_action<'a>(
&self,
action: Self::Actions<'a>,
) -> Result<
Option<impl 'static + Into<Cow<'static, str>>>,
Option<impl 'static + Into<Cow<'static, str>>>,
>;
fn reregister_actions(&self);
#[cfg(feature = "proposals")]
fn graceful_shutdown_wanted(&self, wants_shutdown: bool) {
let _ = wants_shutdown;
}
#[cfg(feature = "proposals")]
fn immediate_shutdown(&self) {}
fn send_command(&self, message: tungstenite::Message);
}
impl<G: Game, T: Deref<Target = G>> Game for T {
const NAME: &'static str = G::NAME;
type Actions<'a> = G::Actions<'a>;
fn handle_action<'a>(
&self,
action: Self::Actions<'a>,
) -> Result<
Option<impl 'static + Into<Cow<'static, str>>>,
Option<impl 'static + Into<Cow<'static, str>>>,
> {
self.deref()
.handle_action(action)
.map(|x| x.map(Into::into))
.map_err(|x| x.map(Into::into))
}
fn reregister_actions(&self) {
self.deref().reregister_actions();
}
#[cfg(feature = "proposals")]
fn graceful_shutdown_wanted(&self, wants_shutdown: bool) {
self.deref().graceful_shutdown_wanted(wants_shutdown);
}
#[cfg(feature = "proposals")]
fn immediate_shutdown(&self) {
self.deref().immediate_shutdown();
}
fn send_command(&self, message: tungstenite::Message) {
self.deref().send_command(message);
}
}
impl<G: GameMut, T: DerefMut<Target = G>> GameMut for T {
const NAME: &'static str = G::NAME;
type Actions<'a> = G::Actions<'a>;
fn handle_action<'a>(
&mut self,
action: Self::Actions<'a>,
) -> Result<
Option<impl 'static + Into<Cow<'static, str>>>,
Option<impl 'static + Into<Cow<'static, str>>>,
> {
self.deref_mut()
.handle_action(action)
.map(|x| x.map(Into::into))
.map_err(|x| x.map(Into::into))
}
fn reregister_actions(&mut self) {
self.deref_mut().reregister_actions();
}
#[cfg(feature = "proposals")]
fn graceful_shutdown_wanted(&mut self, wants_shutdown: bool) {
self.deref_mut().graceful_shutdown_wanted(wants_shutdown);
}
#[cfg(feature = "proposals")]
fn immediate_shutdown(&mut self) {
self.deref_mut().immediate_shutdown();
}
fn send_command(&mut self, message: tungstenite::Message) {
self.deref_mut().send_command(message);
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum Error {
#[error("json error: {0}")]
Json(
#[from]
#[source]
serde_json::Error,
),
}
pub trait Action: schemars::JsonSchema {
fn name() -> &'static str;
fn description() -> &'static str;
}
fn cleanup_action(action: &mut schema::Action) {
fn visit_schema(schema: &mut Schema) {
match schema {
Schema::Object(obj) => visit_schema_obj(obj),
Schema::Bool(_) => {}
}
}
fn visit_schema_obj(schema: &mut SchemaObject) {
if let Some(meta) = schema.metadata.as_mut() {
meta.description = None;
meta.title = None;
}
if let Some(arr) = schema.array.as_mut() {
for x in &mut arr.items {
match x {
SingleOrVec::Single(schema) => visit_schema(schema),
SingleOrVec::Vec(schemas) => {
for schema in schemas {
visit_schema(schema);
}
}
}
}
for x in arr
.contains
.iter_mut()
.chain(arr.additional_items.iter_mut())
{
visit_schema(x);
}
}
if let Some(obj) = schema.object.as_mut() {
for schema in obj
.properties
.values_mut()
.chain(obj.pattern_properties.values_mut())
.chain(
obj.additional_properties
.iter_mut()
.chain(obj.property_names.iter_mut())
.map(|x| &mut **x),
)
{
visit_schema(schema);
}
}
if let Some(sub) = schema.subschemas.as_mut() {
for schema in sub
.all_of
.iter_mut()
.chain(sub.any_of.iter_mut())
.chain(sub.one_of.iter_mut())
.flat_map(|x| x.iter_mut())
.chain(
sub.not
.iter_mut()
.chain(sub.if_schema.iter_mut())
.chain(sub.then_schema.iter_mut())
.chain(sub.else_schema.iter_mut())
.map(|x| &mut **x),
)
{
visit_schema(schema);
}
}
}
action.schema.meta_schema = None;
visit_schema_obj(&mut action.schema.schema);
}
fn send_ws_command<G: Game>(game: &G, cmd: schema::ClientCommandContents) -> Result<(), Error> {
let data = serde_json::to_string(&schema::ClientCommand {
command: cmd,
game: G::NAME.into(),
})?;
game.send_command(tungstenite::Message::text(data));
Ok(())
}
fn send_ws_command_mut<G: GameMut>(
game: &mut G,
cmd: schema::ClientCommandContents,
) -> Result<(), Error> {
let data = serde_json::to_string(&schema::ClientCommand {
command: cmd,
game: G::NAME.into(),
})?;
game.send_command(tungstenite::Message::text(data));
Ok(())
}
impl<T: Game> Api for T {}
impl<T: GameMut> ApiMut for T {}
#[neuro_sama_derive::generic_mutability(ApiMut, GameMut)]
pub trait Api: Game {
fn initialize(&self) -> Result<(), Error> {
let ret = send_ws_command(self, ClientCommandContents::Startup);
if ret.is_ok() {
self.reregister_actions();
}
ret
}
fn context(&self, context: impl Into<Cow<'static, str>>, silent: bool) -> Result<(), Error> {
send_ws_command(
self,
ClientCommandContents::Context {
message: context.into(),
silent,
},
)
}
fn register_actions<A: ActionMetadata>(&self) -> Result<(), Error> {
let mut actions = A::actions();
for action in &mut actions {
cleanup_action(action);
}
self.register_actions_raw(actions)
}
fn unregister_actions<A: ActionMetadata>(&self) -> Result<(), Error> {
self.unregister_actions_raw(A::names())
}
fn unregister_actions_raw(&self, action_names: Vec<Cow<'static, str>>) -> Result<(), Error> {
send_ws_command(
self,
ClientCommandContents::UnregisterActions { action_names },
)
}
fn register_actions_raw(&self, actions: Vec<schema::Action>) -> Result<(), Error> {
send_ws_command(self, ClientCommandContents::RegisterActions { actions })
}
fn handle_message(&self, message: tungstenite::Message) -> Result<(), Error> {
let message = match message {
tungstenite::Message::Text(s) => serde_json::from_str(&s)?,
tungstenite::Message::Binary(b) => serde_json::from_slice(&b)?,
_ => return Ok(()),
};
let (id, res) = match message {
ServerCommand::Action { id, name, data } => {
let res = data.as_ref().filter(|x| !x.trim().is_empty()).map_or_else(
|| {
<Self::Actions<'_> as Actions>::deserialize(
&name,
serde::de::value::UnitDeserializer::new(),
)
},
|data| {
json5::Deserializer::from_str(data).and_then(|mut de| {
<Self::Actions<'_> as Actions>::deserialize(&name, &mut de)
})
},
);
let data = match res {
Ok(data) => data,
Err(err) => {
return send_ws_command(
self,
ClientCommandContents::ActionResult {
id,
success: false,
message: Some(
("Failed to deserialize Neuro-provided action data: "
.to_owned()
+ &err.to_string())
.into(),
),
},
);
}
};
(id, self.handle_action(data))
}
#[cfg(feature = "proposals")]
ServerCommand::ReregisterAllActions => {
self.reregister_actions();
return Ok(());
}
#[cfg(feature = "proposals")]
ServerCommand::GracefulShutdown { wants_shutdown } => {
self.graceful_shutdown_wanted(wants_shutdown);
return Ok(());
}
#[cfg(feature = "proposals")]
ServerCommand::ImmediateShutdown => {
self.immediate_shutdown();
return Ok(());
}
};
let res = match res {
Ok(msg) => ClientCommandContents::ActionResult {
id,
success: true,
message: msg.map(Into::into),
},
Err(msg) => ClientCommandContents::ActionResult {
id,
success: false,
message: msg.map(Into::into),
},
};
send_ws_command(self, res)
}
#[must_use]
fn force_actions<T: ActionMetadata>(
&self,
query: Cow<'static, str>,
) -> ForceActionsBuilder<Self> {
self.force_actions_raw(query, T::names())
}
#[must_use]
fn force_actions_raw(
&self,
query: Cow<'static, str>,
action_names: Vec<Cow<'static, str>>,
) -> ForceActionsBuilder<Self> {
ForceActionsBuilder {
api: self,
state: None,
query,
ephemeral_context: None,
action_names,
}
}
}
pub struct ForceActionsBuilder<'a, G: Api> {
api: &'a G,
state: Option<Cow<'static, str>>,
query: Cow<'static, str>,
ephemeral_context: Option<bool>,
action_names: Vec<Cow<'static, str>>,
}
pub struct ForceActionsBuilderMut<'a, G: ApiMut> {
api: &'a mut G,
state: Option<Cow<'static, str>>,
query: Cow<'static, str>,
ephemeral_context: Option<bool>,
action_names: Vec<Cow<'static, str>>,
}
#[neuro_sama_derive::generic_mutability(ForceActionsBuilderMut, ApiMut)]
impl<'a, G: Api> ForceActionsBuilder<'a, G> {
#[must_use]
pub fn with_ephemeral_context(mut self, ephemeral_context: bool) -> Self {
self.ephemeral_context = Some(ephemeral_context);
self
}
#[must_use]
pub fn with_state(mut self, state: impl Into<Cow<'static, str>>) -> Self {
self.state = Some(state.into());
self
}
pub fn send(self) -> Result<(), Error> {
send_ws_command(
self.api,
schema::ClientCommandContents::ForceActions {
state: self.state,
query: self.query,
ephemeral_context: self.ephemeral_context,
action_names: self.action_names,
},
)
}
}
#[cfg(test)]
mod test {
use serde::Deserialize;
use crate::{
self as neuro_sama,
game::{cleanup_action, ActionMetadata},
};
#[derive(Debug, schemars::JsonSchema, Deserialize, PartialEq)]
struct Move {
x: u32,
y: u32,
}
#[derive(Debug, schemars::JsonSchema, Deserialize, PartialEq)]
struct Shoot;
#[derive(crate::derive::Actions, Debug, PartialEq)]
enum Action {
#[name = "move"]
Move(Move),
#[name = "shoot"]
Shoot(Shoot),
}
#[test]
fn test() {
use super::Actions;
let mut deser = serde_json::Deserializer::from_str(r#"{"x":5,"y":6}"#);
let action = <Action as Actions>::deserialize("move", &mut deser).unwrap();
assert_eq!(action, Action::Move(Move { x: 5, y: 6 }));
let mut deser = json5::Deserializer::from_str(r#"null"#).unwrap();
let action = <Action as Actions>::deserialize("shoot", &mut deser).unwrap();
assert_eq!(action, Action::Shoot(Shoot));
let mut actions = <Action as ActionMetadata>::actions();
for action in &mut actions {
cleanup_action(action);
}
assert_eq!(
serde_json::to_string(&actions).unwrap(),
r#"[
{
"name": "move",
"description": "test 1",
"schema": {
"type": "object",
"required": [ "x", "y" ],
"properties": {
"x": { "type": "integer", "format": "uint32", "minimum": 0.0 },
"y": { "type": "integer", "format": "uint32", "minimum": 0.0 }
}
}
},
{
"name": "shoot",
"description": "test 2",
"schema": {
"type": "null"
}
}
]"#
.to_string()
.replace(|x| x == ' ' || x == '\n', "")
);
}
}