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}