Skip to main content

neuro_sama/
schema.rs

1//! The schema as described in [the specification](https://github.com/VedalAI/neuro-game-sdk/blob/31e36c1a479faa256896a3e172c8d5a96bd462c6/API/SPECIFICATION.md).
2use std::borrow::Cow;
3
4use serde::{Deserialize, Serialize};
5
6/// A registerable command that Neuro can execute whenever she wants.
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8pub struct Action {
9    /// The name of the action, which is its *unique identifier*. This should be a lowercase string, with words separated by underscores or dashes (e.g. `"join_friend_lobby"`, `"use_item"`).
10    pub name: Cow<'static, str>,
11    /// A plaintext description of what this action does. **This information will be directly received by Neuro.**
12    pub description: Cow<'static, str>,
13    /// A **valid** simple JSON schema object that describes how the response data should look like. If your action does not have any parameters, you can omit this field or set it to `{}`.
14    #[serde(default)]
15    pub schema: schemars::schema::RootSchema,
16}
17
18/// Client command contents (everything except the `game` field). See `ClientCommand` docs for more
19/// info.
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[non_exhaustive]
22#[serde(tag = "command", content = "data")]
23pub enum ClientCommandContents {
24    /// This message should be sent as soon as the game starts, to let Neuro know that the game is running.
25    ///
26    /// This message clears all previously registered actions for this game and does initial setup, and as such should be the very first message that you send.
27    #[serde(rename = "startup")]
28    Startup,
29    /// This message can be sent to let Neuro know about something that is happening in game.
30    #[serde(rename = "context")]
31    Context {
32        /// A plaintext message that describes what is happening in the game. **This information will be directly received by Neuro.**
33        message: Cow<'static, str>,
34        /// If `true`, the message will be added to Neuro's context without prompting her to respond to it. If `false`, Neuro might respond to the message directly, unless she is busy talking to someone else or to chat.
35        silent: bool,
36    },
37    /// This message registers one or more actions for Neuro to use.
38    #[serde(rename = "actions/register")]
39    RegisterActions {
40        /// An array of actions to be registered. If you try to register an action that is already registered, it will be ignored.
41        actions: Vec<Action>,
42    },
43    /// This message unregisters one or more actions, preventing Neuro from using them anymore.
44    #[serde(rename = "actions/unregister")]
45    UnregisterActions {
46        /// The names of the actions to unregister. If you try to unregister an action that isn't registered, there will be no problem.
47        action_names: Vec<Cow<'static, str>>,
48    },
49    /// This message forces Neuro to execute one of the listed actions as soon as possible. Note that this might take a bit if she is already talking.
50    #[serde(rename = "actions/force")]
51    ForceActions {
52        /// An arbitrary string that describes the current state of the game. This can be plaintext, JSON, Markdown, or any other format. **This information will be directly received by Neuro.**
53        state: Option<Cow<'static, str>>,
54        /// A plaintext message that tells Neuro what she is currently supposed to be doing (e.g. `"It is now your turn. Please perform an action. If you want to use any items, you should use them before picking up the shotgun."`). **This information will be directly received by Neuro.**
55        query: Cow<'static, str>,
56        /// If `false`, the context provided in the `state` and `query` parameters will be remembered by Neuro after the actions force is compelted. If `true`, Neuro will only remember it for the duration of the actions force.
57        ephemeral_context: Option<bool>,
58        /// The names of the actions that Neuro should choose from.
59        action_names: Vec<Cow<'static, str>>,
60    },
61    /// This message needs to be sent as soon as possible after an action is validated, to allow Neuro to continue.
62    ///
63    /// # Important
64    ///
65    /// Until you send an action result, Neuro will just be waiting for the result of her action!
66    /// Please make sure to send this as soon as possible.
67    /// It should usually be sent after validating the action parameters, before it is actually executed in-game.
68    ///
69    /// # Tip
70    ///
71    /// Since setting `success` to false will retry the action force if there was one, if the action was not successful but you don't want it to be retried, you should set `success` to `true` and provide an error message in the `message` field.
72    #[serde(rename = "action/result")]
73    ActionResult {
74        /// The id of the action that this result is for. This is grabbed from the action message directly.
75        id: String,
76        /// Whether or not the action was successful. *If this is `false` and this action is part of an actions force, the whole actions force will be immediately retried by Neuro.*
77        success: bool,
78        /// A plaintext message that describes what happened when the action was executed. If not successful, this should be an error message. If successful, this can either be empty, or provide a *small* context to Neuro regarding the action she just took (e.g. `"Remember to not share this with anyone."`). **This information will be directly received by Neuro.**
79        message: Option<Cow<'static, str>>,
80    },
81    /// This message should be sent as a response to a graceful or an imminent shutdown request, after progress has been saved. After this is sent, Neuro will close the game herself by terminating the process, so to reiterate you must definitely ensure that progress has already been saved.
82    ///
83    /// # Note
84    ///
85    /// This is part of the game automation API, which will only be used for games that Neuro can launch by herself.
86    /// As such, most games will not need to implement this.
87    #[cfg(feature = "proposals")]
88    #[serde(rename = "shutdown/ready")]
89    ShutdownReady,
90}
91
92/// A client to server (game to Neuro) message.
93#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
94pub struct ClientCommand {
95    /// The command itself.
96    #[serde(flatten)]
97    pub command: ClientCommandContents,
98    /// The game name. This is used to identify the game. It should *always* be the same and should not change. You should use the game's display name, including any spaces and symbols (e.g. `"Buckshot Roulette"`). The server will not include this field.
99    pub game: Cow<'static, str>,
100}
101
102/// A server to client (Neuro to game) message.
103#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
104#[serde(tag = "command", content = "data")]
105#[non_exhaustive]
106pub enum ServerCommand {
107    #[serde(rename = "action")]
108    Action {
109        /// A unique id for the action. You should use it when sending back the action result.
110        id: String,
111        /// The name of the action that Neuro is trying to execute.
112        name: String,
113        /// The JSON-stringified data for the action, as sent by Neuro. This *should* be an object that matches the JSON schema you provided when registering the action. If you did not provide a schema, this parameter will usually be undefined.
114        #[serde(default, skip_serializing_if = "Option::is_none")]
115        data: Option<String>,
116    },
117    /// If there is a problem mid-game and Neuro crashes, upon reconnection this message might be sent in order to reregister all actions that were previously registered. You should respond to this with an actions register containing all actions that are currently supposed to be registered.
118    #[cfg(feature = "proposals")]
119    #[serde(rename = "actions/reregister_all")]
120    ReregisterAllActions,
121    /// This message will be sent when Neuro decides to stop playing a game, or upon manual intervention from the dashboard. You should create or identify graceful shutdown points where the game can be closed gracefully after saving progress. You should store the latest received `wants_shutdown` value, and if it is `true{} when a graceful shutdown point is reached, you should save the game and quit to main menu, then send back a shutdown ready message.
122    ///
123    /// # Note
124    ///
125    /// This is part of the game automation API, which will only be used for games that Neuro can launch by herself.
126    /// As such, most games will not need to implement this.
127    ///
128    /// **Please don't actually close the game, just quit to main menu. Neuro will close the game herself.**
129    #[cfg(feature = "proposals")]
130    #[serde(rename = "shutdown/graceful")]
131    GracefulShutdown {
132        /// Whether the game should shutdown at the next graceful shutdown point. `true` means shutdown is requested, `false` means to cancel the previous shutdown request.
133        wants_shutdown: bool,
134    },
135    /// This message will be sent when the game needs to be shutdown immediately. You have only a handful of seconds to save as much progress as possible. After you have saved, you can send back a shutdown ready message.
136    ///
137    /// # Note
138    ///
139    /// This is part of the game automation API, which will only be used for games that Neuro can launch by herself.
140    /// As such, most games will not need to implement this.
141    ///
142    /// **Please don't actually close the game, just save the current progress that can be saved. Neuro will close the game herself.**
143    #[cfg(feature = "proposals")]
144    #[serde(rename = "shutdown/immediate")]
145    ImmediateShutdown,
146}
147
148#[cfg(test)]
149mod tests {
150    use schemars::schema::{InstanceType, Schema, SingleOrVec};
151
152    use super::*;
153
154    fn parse<'a, T: serde::Deserialize<'a>>(data: &'a str) -> T {
155        serde_json::from_str(data).unwrap()
156    }
157
158    fn ser<T: serde::Serialize>(x: &T) -> String {
159        // its easier to work with string slices and this is tests dont judge ok?
160        crate::to_string(x).unwrap()
161    }
162
163    #[test]
164    fn test_action_roundtrip() {
165        // no schema
166        const SAMPLE1: &str = r#"{"name":"test","description":"abcd","schema":{}}"#;
167        const SAMPLE2: &str = r#"{"name":"test","description":"abcd"}"#;
168        let a: Action = parse(SAMPLE1);
169        let b: Action = parse(SAMPLE2);
170        assert_eq!(&a.name, "test");
171        assert_eq!(&a.description, "abcd");
172        assert_eq!(a, b);
173        assert_eq!(&ser(&a), SAMPLE1);
174        assert_eq!(ser(&a), ser(&b));
175        // yes schema
176        const SAMPLE3: &str = r#"{"name":"test","description":"abcd","schema":{"type":"object","properties":{"test":{"type":"string"}},"required":["test"]}}"#;
177        let c: Action = parse(SAMPLE3);
178        let schema = c.schema.schema;
179        assert!(
180            matches!(schema.instance_type.as_ref().unwrap(), SingleOrVec::Single(x) if **x == InstanceType::Object)
181        );
182        let object_schema = schema.object.unwrap();
183        assert!(object_schema.required.contains("test"));
184        let Schema::Object(prop_schema) = object_schema.properties.get("test").unwrap() else {
185            panic!()
186        };
187        assert!(
188            matches!(prop_schema.instance_type.as_ref().unwrap(), SingleOrVec::Single(x) if **x == InstanceType::String)
189        );
190        assert!(object_schema.required.contains("test"));
191    }
192
193    #[test]
194    fn test_command_roundtrip() {
195        let neuro_cmd = ServerCommand::Action {
196            id: "abcd".to_owned(),
197            name: "efgh".to_owned(),
198            data: None,
199        };
200        const SAMPLE_ACTION: &str = r#"{"command":"action","data":{"id":"abcd","name":"efgh"}}"#;
201        assert_eq!(parse::<ServerCommand>(SAMPLE_ACTION), neuro_cmd);
202        assert_eq!(SAMPLE_ACTION, ser(&neuro_cmd));
203
204        let startup = ClientCommand {
205            game: "game".into(),
206            command: ClientCommandContents::Startup,
207        };
208        const STARTUP: &str = r#"{"command":"startup","game":"game"}"#;
209        assert_eq!(parse::<ClientCommand>(STARTUP), startup);
210        assert_eq!(STARTUP, ser(&startup));
211
212        let context = ClientCommand {
213            game: "game".into(),
214            command: ClientCommandContents::Context {
215                message: "test".into(),
216                silent: false,
217            },
218        };
219        const CONTEXT: &str =
220            r#"{"command":"context","data":{"message":"test","silent":false},"game":"game"}"#;
221        assert_eq!(parse::<ClientCommand>(CONTEXT), context);
222        assert_eq!(CONTEXT, ser(&context));
223    }
224}