dcss_api/
lib.rs

1//! An API library to interact with [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
2
3mod api_errors;
4mod common;
5mod lobby;
6mod play;
7
8pub use api_errors::{BlockingError, Error};
9
10use api_errors::blocking_messages;
11use flate2::Decompress;
12use serde_json::Value;
13use std::collections::VecDeque;
14use std::net::TcpStream;
15use std::result::Result;
16use std::str;
17use std::thread;
18use std::time::{Duration, SystemTime};
19use tungstenite::Message;
20use tungstenite::{self, protocol::WebSocket, stream::MaybeTlsStream};
21
22/// Webtile connection, using websocket ([tungstenite]) and a Deflate decoder ([flate2]).
23#[derive(Debug)]
24pub struct Webtile {
25    /// Websocket (using [tungstenite::WebSocket]) to send and receive data from
26    /// [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
27    socket: WebSocket<MaybeTlsStream<TcpStream>>,
28
29    /// A [flate2::Decompress] decompression object (Deflate) to decompress data received
30    /// by [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
31    decompressor: Decompress,
32
33    /// [SystemTime] of the last sent message. Used to limit the rate for
34    /// running the bot on someone else's server.
35    last_send: SystemTime,
36
37    /// A [bool] of if the searched for data (in the websocket) has been found.
38    message_found: bool,
39
40    /// Speed limit in milliseconds between each command sent to DCSS Webtiles.
41    speed_ms: u32,
42
43    /// [VecDeque] of messages received from DCSS.
44    received_messages: VecDeque<Value>,
45
46    /// The version ("0.33") of the DCSS game, if one was started.
47    version: Option<String>,
48}
49
50impl Webtile {
51    /// Connects to a websocket URL, prepares the decompressor (using [flate2::Decompress]) and
52    /// returns a [Webtile] connection object.
53    ///
54    /// # Arguments
55    ///
56    /// * `url` - A [&str] that holds the `ws://` or `wss://` URL
57    /// * `speed_ms` - A [u32] that depicts the speed limit in milliseconds between
58    ///   each command sent to DCSS Webtiles.
59    ///     
60    /// # Example
61    ///
62    /// ```no_run
63    /// let mut webtile = Webtile::connect("ws://localhost:8080/socket", 100)?;
64    /// ```
65    pub fn connect(url: &str, speed_ms: u32) -> Result<Self, Box<Error>> {
66        // Open connection
67        let (socket, _response) = tungstenite::connect(url).map_err(Error::Websocket)?;
68
69        // Init decompressor (see https://rustpython.github.io/website/src/rustpython_vm/stdlib/zlib.rs.html)
70        let wbits = 15; // Windows bits fixed (goes to -15 in flate2 because of zlib_header = false)
71        let decompressor = Decompress::new_with_window_bits(false, wbits);
72
73        // Create webtile object
74        let mut webtile = Self {
75            socket,
76            decompressor,
77            last_send: SystemTime::now(),
78            speed_ms,
79            message_found: false,
80            received_messages: VecDeque::new(),
81            version: None,
82        };
83
84        // Wait until the "lobby_complete" message is received -- meaning a
85        // successful connection
86        webtile.read_until("lobby_complete", None, None)?;
87
88        Ok(webtile)
89    }
90
91    /// Close the websocket connection.
92    ///
93    /// # Example
94    ///
95    /// ```no_run
96    /// webtile.disconnect()?;
97    /// ```
98    pub fn disconnect(&mut self) -> Result<(), Box<Error>> {
99        self.socket.close(None).map_err(Error::Websocket)?;
100
101        Ok(())
102    }
103
104    /// Read the websocket messages until a specified message is found. Stores the
105    /// messages in a [VecDeque] that can be accessed by the user through the
106    /// [`Webtile::get_message()`] function. Any known blocking message (e.g.
107    /// a 'more' log statement) will return a [api_errors::BlockingError].
108    ///
109    /// Will block forever if the expected message never comes.
110    ///
111    /// # Arguments
112    ///
113    /// * `msg` - A [&str] that holds the value expected in the "msg" field of any returned message.
114    /// * `key` - A optional [&str] with the name of the specific key in the json data to search for.
115    /// * `value` - A optional [u64] with the value of the `key`, only if u64. Specifically meant to
116    ///   distinguish between types of "modes" for the input_mode.
117    ///
118    /// # Example
119    ///
120    /// ```no_run
121    ///
122    /// // Read until the "close_all_menu" message is received
123    /// webtile.read_until("close_all_menus", None, None)
124    ///
125    /// // Read until the "input_mode" message is received, with mode == 1
126    /// webtile.read_until("input_mode", Some("mode"), Some(1))
127    /// ```
128    pub fn read_until(
129        &mut self,
130        msg: &str,
131        key: Option<&str>,
132        value: Option<u64>,
133    ) -> Result<(), Box<Error>> {
134        // loop until break (found expected results or found a blocking type)
135        // use self variable in order to retain the info when there is a blocking error
136        while !self.message_found {
137            // Read the message from the socket into Vec<u8> -- it will be compressed
138            let mut compressed_msg = self
139                .socket
140                .read()
141                .map_err(Error::Websocket)?
142                .into_data()
143                .to_vec();
144
145            // Decompress the message and return JSON Value
146            let messages = common::deflate_to_json(&mut self.decompressor, &mut compressed_msg)?;
147
148            // Alert if blocking
149            let mut blocking = Ok(());
150
151            // Will get array of message, go through them until what is expected is found
152            for message in messages["msgs"].as_array().unwrap() {
153                // Send data to a VecDeque to be pulled by user;
154                self.received_messages.push_back(message.to_owned());
155
156                // Pre-process the data to identify blocking
157                if let Err(e) = blocking_messages(message) {
158                    match *e {
159                        Error::Blocking(BlockingError::Died) => return Err(e), // Automatic return when death
160                        _ => blocking = Err(e),
161                    }
162                };
163
164                // If searching for key-value
165                let message_msg = message["msg"].as_str().unwrap().to_owned();
166
167                // Same message
168                if msg == message_msg &&
169                    // And no key
170                    (key.is_none() ||
171                    // or And contains key
172                    (message.as_object().unwrap().contains_key(&key.unwrap().to_owned()) &&
173                        // And no value
174                        (value.is_none() ||
175                        // or And value correct
176                        message[key.unwrap()].as_u64().unwrap() == value.unwrap())))
177                {
178                    self.message_found = true;
179                }
180            }
181
182            blocking?
183        }
184
185        self.message_found = false;
186
187        Ok(())
188    }
189
190    /// Write a [serde_json::Value] to the websocket. Will only send if sufficient time has
191    /// elapsed since the last sent data, according to the [`Webtile::connect`] speed_ms option.
192    ///
193    /// # Arguments
194    ///
195    /// * `json_val` - A [serde_json::Value] to send to DCSS Webtiles.
196    ///
197    /// # Example
198    ///
199    /// ```no_run
200    /// // Send the login command
201    /// webtile.write_json(json!({
202    ///     "msg": "login",
203    ///     "username": "Username",
204    ///     "password": "Password",
205    /// }))?;
206    /// ```
207    pub fn write_json(&mut self, json_val: Value) -> Result<(), Box<Error>> {
208        // Pause while min time not met
209        while SystemTime::now()
210            .duration_since(self.last_send)
211            .expect("Time failed")
212            .as_millis()
213            < self.speed_ms as u128
214        {
215            thread::sleep(Duration::from_millis(10));
216        }
217        self.last_send = SystemTime::now();
218
219        self.socket
220            .send(Message::Text(json_val.to_string().into()))
221            .map_err(Error::Websocket)?;
222
223        Ok(())
224    }
225
226    /// Write a string slice (processed by the crate) to the websocket. Special
227    /// characters starting with `key_` will be sent as a keycode (e.g. `key_esc` will
228    /// send the `esc` character). Will only send if sufficient time has elapsed since
229    /// the last sent data, according to the [`Webtile::connect`] speed_ms option.
230    ///
231    /// Special keys:
232    /// * CTRL+char = `key_ctrl_a` to `key_ctrl_z`
233    /// * Special chars = `key_tab`, `key_esc` and `key_enter`
234    /// * Cardinal directions: `key_dir_n`, `key_dir_ne`, `key_dir_e`, `key_dir_se`,
235    ///   `key_dir_s`, `key_dir_sw`, `key_dir_w` and `key_dir_nw`
236    /// * Stairs: `key_stair_down` and `key_stair_up`
237    ///
238    /// # Arguments
239    ///
240    /// * `key` - A string slice to be sent to DCSS (after processing).
241    ///
242    /// # Example
243    ///
244    /// ```no_run
245    /// // Send the `esc` key
246    /// webtile.write_key("key_esc")
247    ///
248    /// // Send the 'a' key
249    /// webtile.write_key("a")
250    ///
251    /// // Send a string of keys that will move left, open a menu and drop an item (slot a)
252    /// webtile.write_key("6iad")
253    /// ```
254    pub fn write_key(&mut self, key: &str) -> Result<(), Box<Error>> {
255        // Pause while min time not met
256        while SystemTime::now()
257            .duration_since(self.last_send)
258            .expect("Time failed")
259            .as_millis()
260            < self.speed_ms as u128
261        {
262            thread::sleep(Duration::from_millis(10));
263        }
264        self.last_send = SystemTime::now();
265
266        let json_key = common::keys(key);
267        self.socket
268            .send(Message::Text(json_key.to_string().into()))
269            .map_err(Error::Websocket)?;
270
271        Ok(())
272    }
273
274    /// Get the messages received by the DCSS Webtile (as [serde_json::Value]), in
275    /// order of reception. Will return [None] if the queue is empty.
276    ///
277    /// # Example
278    ///
279    /// ```no_run
280    /// // Print the messages received, until the queue is empty
281    /// while let Some(message) = webtile.get_message() {
282    ///     println!("{:?}", message)
283    /// }
284    /// ```
285    pub fn get_message(&mut self) -> Option<Value> {
286        self.received_messages.pop_front()
287    }
288
289    /// Get a copy of the messages currently in the received queue.
290    pub(crate) fn read_only_messages(&self) -> Vec<Value> {
291        self.received_messages
292            .clone()
293            .into_iter()
294            .collect::<Vec<Value>>()
295    }
296}