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}