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