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}