tts_external_api/
messages.rs

1//! Incoming and Outgoing messages
2
3use crate::{error::Error, tcp::ExternalEditorApi, Value};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde::{__private::ser::FlatMapSerializer, ser::SerializeMap};
6use std::io::{self};
7
8/////////////////////////////////////////////////////////////////////////////
9
10/// Represents outgoing messages sent to Tabletop Simulator
11#[derive(Debug)]
12pub enum Message {
13    /// Represents [Get Lua Scripts](https://api.tabletopsimulator.com/externaleditorapi/#get-lua-scripts)
14    MessageGetScripts(MessageGetScripts),
15    /// Represents [Save & Play](https://api.tabletopsimulator.com/externaleditorapi/#get-lua-scripts)
16    MessageReload(MessageReload),
17    /// Represents [Custom Message](https://api.tabletopsimulator.com/externaleditorapi/#custom-message)
18    MessageCustomMessage(MessageCustomMessage),
19    /// Represents [Execute Lua Code](https://api.tabletopsimulator.com/externaleditorapi/#execute-lua-code)
20    MessageExecute(MessageExecute),
21}
22
23// Workaround for: https://github.com/serde-rs/serde/issues/745
24// https://stackoverflow.com/questions/65575385/deserialization-of-json-with-serde-by-a-numerical-value-as-type-identifier/65576570#65576570
25//
26// #[derive(Serialize, Debug)]
27// #[serde(tag = "messageID")]
28// pub enum Message {
29//     #[serde(rename = 0)]
30//     MessageGetScripts(MessageGetScripts),
31//     #[serde(rename = 1)]
32//     MessageReload(MessageReload),
33//     #[serde(rename = 2)]
34//     MessageCustomMessage(MessageCustomMessage),
35//     #[serde(rename = 3)]
36//     MessageExecute(MessageExecute),
37// }
38//
39impl Serialize for Message {
40    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
41    where
42        S: Serializer,
43    {
44        let mut s = serializer.serialize_map(None)?;
45
46        let id_ = &match self {
47            Message::MessageGetScripts(_) => 0,
48            Message::MessageReload(_) => 1,
49            Message::MessageCustomMessage(_) => 2,
50            Message::MessageExecute(_) => 3,
51        };
52        s.serialize_entry("messageID", &id_)?;
53
54        match self {
55            Message::MessageGetScripts(t) => t.serialize(FlatMapSerializer(&mut s))?,
56            Message::MessageReload(t) => t.serialize(FlatMapSerializer(&mut s))?,
57            Message::MessageCustomMessage(t) => t.serialize(FlatMapSerializer(&mut s))?,
58            Message::MessageExecute(t) => t.serialize(FlatMapSerializer(&mut s))?,
59        }
60
61        s.end()
62    }
63}
64
65/// Get a list containing the states for every object. Returns an [`AnswerReload`] message.
66#[derive(Serialize, Debug)]
67pub struct MessageGetScripts {}
68
69impl TryFrom<Message> for MessageGetScripts {
70    type Error = Error;
71    fn try_from(message: Message) -> Result<Self, Self::Error> {
72        match message {
73            Message::MessageGetScripts(message) => Ok(message),
74            other => Err(Error::MessageError(other)),
75        }
76    }
77}
78
79impl MessageGetScripts {
80    /// Constructs a new Get Lua Scripts Message
81    pub fn new() -> Self {
82        Self {}
83    }
84
85    /// Returns self as [`Message::MessageGetScripts`]
86    pub fn as_message(self) -> Message {
87        Message::MessageGetScripts(self)
88    }
89}
90
91/// Update the Lua scripts and UI XML for any objects listed in the message,
92/// and then reloads the save file, the same way it does when pressing "Save & Play" within the in-game editor.
93/// Returns an [`AnswerReload`] message.
94///
95/// Any objects mentioned have both their Lua script and their UI XML updated.
96/// If no value is set for either the "script" or "ui" key then the
97/// corresponding Lua script or UI XML is deleted.
98#[derive(Serialize, Debug)]
99pub struct MessageReload {
100    /// Contains a list objects and their state
101    #[serde(rename = "scriptStates")]
102    pub script_states: Value,
103}
104
105impl TryFrom<Message> for MessageReload {
106    type Error = Error;
107    fn try_from(message: Message) -> Result<Self, Self::Error> {
108        match message {
109            Message::MessageReload(message) => Ok(message),
110            other => Err(Error::MessageError(other)),
111        }
112    }
113}
114
115impl MessageReload {
116    /// Constructs a new Save & Play Message
117    pub fn new(script_states: Value) -> Self {
118        Self { script_states }
119    }
120
121    /// Returns self as [`Message::MessageReload`]
122    pub fn as_message(self) -> Message {
123        Message::MessageReload(self)
124    }
125}
126
127/// Send a custom message to be forwarded to the `onExternalMessage` event handler
128/// in the currently loaded game. The value of customMessage must be an object,
129/// and is passed as a parameter to the event handler.
130/// If this value is not an object then the event is not triggered.
131#[derive(Serialize, Debug)]
132pub struct MessageCustomMessage {
133    /// Custom message that gets forwarded
134    #[serde(rename = "customMessage")]
135    pub custom_message: Value,
136}
137
138impl TryFrom<Message> for MessageCustomMessage {
139    type Error = Error;
140    fn try_from(message: Message) -> Result<Self, Self::Error> {
141        match message {
142            Message::MessageCustomMessage(message) => Ok(message),
143            other => Err(Error::MessageError(other)),
144        }
145    }
146}
147
148impl MessageCustomMessage {
149    /// Constructs a new Custom Message
150    pub fn new(custom_message: Value) -> Self {
151        Self { custom_message }
152    }
153
154    /// Returns self as [`Message::MessageCustomMessage`]
155    pub fn as_message(self) -> Message {
156        Message::MessageCustomMessage(self)
157    }
158}
159
160/// Executes a lua script and returns the value in a [`AnswerReturn`] message.
161/// Using a guid of "-1" runs the script globally.
162#[derive(Serialize, Debug)]
163pub struct MessageExecute {
164    /// Return Id of the execute message
165    #[serde(rename = "returnID")]
166    pub return_id: u64,
167    /// The guid the message gets executed on
168    #[serde(rename = "guid")]
169    pub guid: String,
170    /// The script that gets executed
171    #[serde(rename = "script")]
172    pub script: String,
173}
174
175impl TryFrom<Message> for MessageExecute {
176    type Error = Error;
177    fn try_from(message: Message) -> Result<Self, Self::Error> {
178        match message {
179            Message::MessageExecute(message) => Ok(message),
180            other => Err(Error::MessageError(other)),
181        }
182    }
183}
184
185impl MessageExecute {
186    /// Constructs a new Execute Lua Code Message that executes code globally
187    pub fn new(script: String) -> Self {
188        Self {
189            return_id: 5,
190            guid: String::from("-1"),
191            script,
192        }
193    }
194
195    /// Constructs a new Execute Lua Code Message that executes code on an object
196    pub fn new_object(script: String, guid: String) -> Self {
197        Self {
198            return_id: 5,
199            guid,
200            script,
201        }
202    }
203
204    /// Returns self as [`Message::MessageExecute`]
205    pub fn as_message(self) -> Message {
206        Message::MessageExecute(self)
207    }
208}
209
210/////////////////////////////////////////////////////////////////////////////
211
212/// Represents incoming messages sent by Tabletop Simulator.
213#[derive(Debug)]
214pub enum Answer {
215    /// Represents [Pushing New Object](https://api.tabletopsimulator.com/externaleditorapi/#pushing-new-object)
216    AnswerNewObject(AnswerNewObject),
217    /// Represents [Loading a new Game](https://api.tabletopsimulator.com/externaleditorapi/#loading-a-new-game)
218    AnswerReload(AnswerReload),
219    /// Represents [Print/Debug Messages](https://api.tabletopsimulator.com/externaleditorapi/#printdebug-messages)
220    AnswerPrint(AnswerPrint),
221    /// Represents [Error Messages](https://api.tabletopsimulator.com/externaleditorapi/#error-messages)
222    AnswerError(AnswerError),
223    /// Represents [Custom Messages](https://api.tabletopsimulator.com/externaleditorapi/#custom-messages)
224    AnswerCustomMessage(AnswerCustomMessage),
225    /// Represents [Return Messages](https://api.tabletopsimulator.com/externaleditorapi/#return-messages)
226    AnswerReturn(AnswerReturn),
227    /// Represents [Game Saved](https://api.tabletopsimulator.com/externaleditorapi/#game-saved)
228    AnswerGameSaved(AnswerGameSaved),
229    /// Represents [Object Created](https://api.tabletopsimulator.com/externaleditorapi/#object-created)
230    AnswerObjectCreated(AnswerObjectCreated),
231}
232
233// Workaround for: https://github.com/serde-rs/serde/issues/745
234// https://stackoverflow.com/questions/65575385/deserialization-of-json-with-serde-by-a-numerical-value-as-type-identifier/65576570#65576570
235//
236// #[derive(Deserialize, Debug)]
237// #[serde(tag = "messageID")]
238// pub enum Answer {
239//     #[serde(rename = 0)]
240//     AnswerNewObject(AnswerNewObject),
241//     #[serde(rename = 1)]
242//     AnswerReload(AnswerReload),
243//     #[serde(rename = 2)]
244//     AnswerPrint(AnswerPrint),
245//     #[serde(rename = 3)]
246//     AnswerError(AnswerError),
247//     #[serde(rename = 4)]
248//     AnswerCustomMessage(AnswerCustomMessage),
249//     #[serde(rename = 5)]
250//     AnswerReturn(AnswerReturn),
251//     #[serde(rename = 6)]
252//     AnswerGameSaved(AnswerGameSaved),
253//     #[serde(rename = 7)]
254//     AnswerObjectCreated(AnswerObjectCreated),
255// }
256//
257impl<'de> serde::Deserialize<'de> for Answer {
258    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
259        let value = Value::deserialize(d)?;
260
261        Ok(
262            match value.get("messageID").and_then(Value::as_u64).unwrap() {
263                0 => Answer::AnswerNewObject(AnswerNewObject::deserialize(value).unwrap()),
264                1 => Answer::AnswerReload(AnswerReload::deserialize(value).unwrap()),
265                2 => Answer::AnswerPrint(AnswerPrint::deserialize(value).unwrap()),
266                3 => Answer::AnswerError(AnswerError::deserialize(value).unwrap()),
267                4 => Answer::AnswerCustomMessage(AnswerCustomMessage::deserialize(value).unwrap()),
268                5 => Answer::AnswerReturn(AnswerReturn::deserialize(value).unwrap()),
269                6 => Answer::AnswerGameSaved(AnswerGameSaved::deserialize(value).unwrap()),
270                7 => Answer::AnswerObjectCreated(AnswerObjectCreated::deserialize(value).unwrap()),
271                id_ => panic!("unsupported id {:?}", id_),
272            },
273        )
274    }
275}
276
277/// When clicking on "Scripting Editor" in the right click contextual menu
278/// in TTS for an object that doesn't have a Lua Script yet, TTS will send
279/// an [`AnswerNewObject`] message containing data for the object.
280///
281/// # Example
282/// ```json
283/// {
284///     "message_id": 0,
285///     "script_states": [
286///         {
287///             "name": "Chess Pawn",
288///             "guid": "db3f06",
289///             "script": ""
290///         }
291///     ]
292/// }
293/// ```
294#[derive(Deserialize, Debug)]
295pub struct AnswerNewObject {
296    /// Contains the state of the object
297    #[serde(rename = "scriptStates")]
298    pub script_states: Value,
299}
300
301impl TryFrom<Answer> for AnswerNewObject {
302    type Error = Error;
303    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
304        match answer {
305            Answer::AnswerNewObject(message) => Ok(message),
306            other => Err(Error::AnswerError(other)),
307        }
308    }
309}
310
311/// After loading a new game in TTS, TTS will send all the Lua scripts
312/// and UI XML from the new game as an [`AnswerReload`].
313///
314/// TTS sends this message as a response to [`MessageGetScripts`] and [`MessageReload`].
315///
316/// # Example
317/// ```json
318/// {
319///     "message_id": 1,
320///     "script_states": [
321///         {
322///             "name": "Global",
323///             "guid": "-1",
324///             "script": "...",
325///             "ui": "..."
326///         },
327///         {
328///             "name": "BlackJack Dealer's Deck",
329///             "guid": "a0b2d5",
330///             "script": "..."
331///         },
332///     ]
333/// }
334/// ```
335#[derive(Deserialize, Debug)]
336pub struct AnswerReload {
337    /// Path to the save file of the current save
338    #[serde(rename = "savePath")]
339    pub save_path: String,
340    /// Contains a list objects and their state
341    #[serde(rename = "scriptStates")]
342    pub script_states: Value,
343}
344
345impl TryFrom<Answer> for AnswerReload {
346    type Error = Error;
347    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
348        match answer {
349            Answer::AnswerReload(message) => Ok(message),
350            other => Err(Error::AnswerError(other)),
351        }
352    }
353}
354
355/// TTS sends all `print()` messages in a [`AnswerPrint`] response.
356///
357/// # Example
358/// ```json
359/// {
360///     "message_id": 2,
361///     "message": "Hit player! White"
362/// }
363/// ```
364#[derive(Deserialize, Debug)]
365pub struct AnswerPrint {
366    /// Message that got printed
367    #[serde(rename = "message")]
368    pub message: String,
369}
370
371impl TryFrom<Answer> for AnswerPrint {
372    type Error = Error;
373    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
374        match answer {
375            Answer::AnswerPrint(message) => Ok(message),
376            other => Err(Error::AnswerError(other)),
377        }
378    }
379}
380
381/// TTS sends all error messages in a [`AnswerError`] response.
382///
383/// # Example
384/// ```json
385/// {
386///     "message_id": 3,
387///     "error": "chunk_0:(36,4-8): unexpected symbol near 'deck'",
388///     "guid": "-1",
389///     "errorMessagePrefix": "Error in Global Script: "
390/// }
391/// ```
392#[derive(Deserialize, Debug)]
393pub struct AnswerError {
394    /// Description of the error
395    #[serde(rename = "error")]
396    pub error: String,
397    /// Guid of the object that has the error
398    #[serde(rename = "guid")]
399    pub guid: String,
400    /// Description of the error
401    #[serde(rename = "errorMessagePrefix")]
402    pub error_message_prefix: String,
403}
404
405impl TryFrom<Answer> for AnswerError {
406    type Error = Error;
407    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
408        match answer {
409            Answer::AnswerError(message) => Ok(message),
410            other => Err(Error::AnswerError(other)),
411        }
412    }
413}
414
415/// Custom Messages are sent by calling `sendExternalMessage` with the table of data you wish to send.
416///
417/// # Example
418/// ```json
419/// {
420///     "message_id": 4,
421///     "custom_message": { "foo": "Hello", "bar": "World"}
422/// }
423/// ```
424#[derive(Deserialize, Debug)]
425pub struct AnswerCustomMessage {
426    /// Content of the custom message
427    #[serde(rename = "customMessage")]
428    pub custom_message: Value,
429}
430
431impl TryFrom<Answer> for AnswerCustomMessage {
432    type Error = Error;
433    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
434        match answer {
435            Answer::AnswerCustomMessage(message) => Ok(message),
436            other => Err(Error::AnswerError(other)),
437        }
438    }
439}
440
441/// If code executed with a [`MessageExecute`] message returns a value,
442/// it will be sent back in a [`AnswerReturn`] message.
443///
444/// # Example
445/// ```json
446/// {
447///     "message_id": 5,
448///     "return_value": true
449/// }
450/// ```
451#[derive(Deserialize, Debug)]
452pub struct AnswerReturn {
453    /// Return Id of message that got executed
454    #[serde(rename = "returnID")]
455    pub return_id: u64,
456    #[serde(
457        rename = "returnValue",
458        deserialize_with = "deserialize_json_string",
459        default
460    )]
461    /// The Value that got returned
462    pub return_value: Value,
463}
464
465impl TryFrom<Answer> for AnswerReturn {
466    type Error = Error;
467    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
468        match answer {
469            Answer::AnswerReturn(message) => Ok(message),
470            other => Err(Error::AnswerError(other)),
471        }
472    }
473}
474
475/// Returns the return value of the message as a [`Value`]. Valid JSON strings get deserialized if possible.
476/// If deserialization fails JSON strings get returned as a [`Value::String`] instead.
477fn deserialize_json_string<'de, D>(deserializer: D) -> Result<Value, D::Error>
478where
479    D: Deserializer<'de>,
480{
481    match Option::deserialize(deserializer)? {
482        Some(val) => match val {
483            Value::String(val) => Ok(serde_json::from_str(&val).unwrap_or(Value::String(val))),
484            other => Ok(other),
485        },
486        None => Ok(Value::Null),
487    }
488}
489
490/// Whenever the player saves the game in TTS, [`AnswerGameSaved`] is sent as a response.
491#[derive(Deserialize, Debug)]
492pub struct AnswerGameSaved {}
493
494impl TryFrom<Answer> for AnswerGameSaved {
495    type Error = Error;
496    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
497        match answer {
498            Answer::AnswerGameSaved(message) => Ok(message),
499            other => Err(Error::AnswerError(other)),
500        }
501    }
502}
503
504/// Whenever the player saves the game in TTS, [`AnswerObjectCreated`] is sent as a response.
505///
506/// # Example
507/// ```json
508/// {
509///     "message_id": 7,
510///     "guid": "abcdef"
511/// }
512/// ```
513#[derive(Deserialize, Debug)]
514pub struct AnswerObjectCreated {
515    /// Guid of the object that got created
516    #[serde(rename = "guid")]
517    pub guid: String,
518}
519
520impl TryFrom<Answer> for AnswerObjectCreated {
521    type Error = Error;
522    fn try_from(answer: Answer) -> Result<Self, Self::Error> {
523        match answer {
524            Answer::AnswerObjectCreated(message) => Ok(message),
525            other => Err(Error::AnswerError(other)),
526        }
527    }
528}
529
530/////////////////////////////////////////////////////////////////////////////
531
532impl ExternalEditorApi {
533    /// Get a list containing the states for every object. Returns an [`AnswerReload`] message on success.
534    /// If no connection to the game can be established, an [`io::Error`] gets returned instead.
535    pub fn get_scripts(&self) -> io::Result<AnswerReload> {
536        self.send(MessageGetScripts::new().as_message())?;
537        Ok(self.wait())
538    }
539
540    /// Update the Lua scripts and UI XML for any objects listed in the message,
541    /// and then reloads the save file, the same way it does when pressing "Save & Play" within the in-game editor.
542    /// Returns an [`AnswerReload`] message.
543    /// If no connection to the game can be established, an [`io::Error`] gets returned instead.
544    ///
545    /// Any objects mentioned have both their Lua script and their UI XML updated.
546    /// If no value is set for either the "script" or "ui" key then the
547    /// corresponding Lua script or UI XML is deleted.
548    pub fn reload(&self, script_states: Value) -> io::Result<AnswerReload> {
549        self.send(MessageReload::new(script_states).as_message())?;
550        Ok(self.wait())
551    }
552
553    /// Send a custom message to be forwarded to the `onExternalMessage` event handler
554    /// in the currently loaded game. The value of customMessage must be an object,
555    /// and is passed as a parameter to the event handler.
556    /// If no connection to the game can be established, an [`io::Error`] gets returned.
557    ///
558    /// If this value is not an object then the event is not triggered.
559    pub fn custom_message(&self, message: Value) -> io::Result<()> {
560        self.send(MessageCustomMessage::new(message).as_message())?;
561        Ok(())
562    }
563
564    /// Executes a lua script globally and returns the value in a [`AnswerReturn`] message.
565    /// If no connection to the game can be established, an [`io::Error`] gets returned instead.
566    pub fn execute(&self, script: String) -> io::Result<AnswerReturn> {
567        self.send(MessageExecute::new(script).as_message())?;
568        Ok(self.wait())
569    }
570
571    /// Executes a lua script on an object and returns the value in a [`AnswerReturn`] message.
572    /// If no connection to the game can be established, an [`io::Error`] gets returned instead.
573    ///
574    /// To execute Lua code for an object in the game that object must have an associated script in TTS.
575    /// Otherwise the TTS scripting engine will fail with an error "function \<executeScript>:
576    /// Object reference not set to an instance of an object".
577    /// Once the in-game editor shows a script associated with an object
578    /// then TTS will be able to execute Lua code sent via JSON message for that object.
579    pub fn execute_on_object(&self, script: String, guid: String) -> io::Result<AnswerReturn> {
580        self.send(MessageExecute::new_object(script, guid).as_message())?;
581        Ok(self.wait())
582    }
583}