dfhack_remote/
lib.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use num_enum::TryFromPrimitiveError;
5
6mod channel;
7mod message;
8
9pub use channel::Channel;
10#[doc(no_inline)]
11pub use dfhack_proto::messages::*;
12pub use dfhack_proto::stubs::*;
13use message::CommandResult;
14
15/// DFHack client, build it with [connect] or [connect_to]
16pub type Client = Stubs<Channel>;
17
18/// Connect to Dwarf Fortress using the default settings
19///
20/// It will try to connect to `127.0.0.1:5000`, DFHack default address.
21/// The port can be overriden with `DFHACK_PORT`, which is also taken in account by DFHack.
22///
23/// For remote connexion, see [connect_to].
24///
25/// # Examples
26///
27/// ```no_run
28/// use dfhack_remote;
29///
30/// let mut dfhack = dfhack_remote::connect().unwrap();
31/// let df_version = dfhack.core().get_df_version().unwrap();
32/// println!("DwarfFortress {}",  df_version);
33/// ```
34pub fn connect() -> Result<Client> {
35    let connexion = Channel::connect()?;
36    Ok(Stubs::from(connexion))
37}
38
39/// Connect to Dwarf Fortress with a given address
40///
41/// # Arguments
42///
43/// * `address` - Address of the DFHack server. By default, DFHack runs of `127.0.0.1:5000`
44///
45/// # Examples
46///
47/// ```no_run
48/// use dfhack_remote;
49/// let mut dfhack = dfhack_remote::connect_to("127.0.0.1:5000").unwrap();
50/// let df_version = dfhack.core().get_df_version().unwrap();
51/// println!("DwarfFortress {}",  df_version);
52/// ```
53///
54pub fn connect_to(address: &str) -> Result<Client> {
55    let connexion = Channel::connect_to(address)?;
56    Ok(Stubs::from(connexion))
57}
58
59/// Result type emitted by DFHack API calls
60pub type Result<T> = std::result::Result<T, Error>;
61
62/// Error type emitted by DFHack API calls
63#[derive(thiserror::Error, Debug)]
64pub enum Error {
65    /// A low level connexion error
66    ///
67    /// This can mean that the address is wrong,
68    /// that Dwarf Fortress crashed, or a library bug occured.
69    #[error("communication failure: {0}")]
70    CommunicationFailure(#[from] std::io::Error),
71
72    /// The data exchange did not happen as expected.
73    ///
74    /// This is likely a bug.
75    #[error("protocol error: {0}.")]
76    ProtocolError(String),
77
78    /// Protobuf serialization or deserialization error
79    ///
80    /// This can indicate that updating the generated code
81    /// is necessary
82    #[error("protobuf serialization error: {0}.")]
83    ProtobufError(#[from] protobuf::Error),
84
85    /// Failed to bind the method
86    ///
87    /// This can indicate that updating the generated code
88    /// is necessary
89    #[error("failed to bind {0}.")]
90    FailedToBind(String),
91
92    /// DFHack RPC Error
93    #[error("RPC error: {0}.")]
94    RpcError(CommandResult),
95}
96
97impl From<TryFromPrimitiveError<message::RpcReplyCode>> for Error {
98    fn from(err: TryFromPrimitiveError<message::RpcReplyCode>) -> Self {
99        Self::ProtocolError(format!("Unknown DFHackReplyCode : {}", err.number))
100    }
101}
102
103impl From<std::string::FromUtf8Error> for Error {
104    fn from(err: std::string::FromUtf8Error) -> Self {
105        Self::ProtocolError(format!("Invalid string error: {}", err))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    #[ctor::ctor]
112    fn init() {
113        env_logger::init();
114    }
115    #[cfg(feature = "test-with-df")]
116    mod withdf {
117        use std::process::Child;
118        use std::sync::Mutex;
119
120        use rand::Rng;
121        #[cfg(test)]
122        lazy_static::lazy_static! {
123            static ref DF_PROCESS: Mutex<Option<Child>> = Mutex::new(Option::<Child>::None);
124        }
125
126        #[ctor::ctor]
127        fn init() {
128            let port = rand::thread_rng().gen_range(49152..65535).to_string();
129            std::env::set_var("DFHACK_PORT", port);
130
131            use std::{path::PathBuf, process::Command};
132            let df_exe = PathBuf::from(std::env::var("DF_EXE").unwrap());
133            let df_folder = df_exe.parent().unwrap();
134
135            let df = Command::new(&df_exe)
136                .args(["+load-save", "region1"])
137                .current_dir(df_folder)
138                .spawn()
139                .unwrap();
140            let mut process_guard = DF_PROCESS.lock().unwrap();
141            process_guard.replace(df);
142        }
143
144        #[ctor::dtor]
145        fn exit() {
146            let mut process_guard = DF_PROCESS.lock().unwrap();
147            let df = process_guard.take();
148            if let Some(mut df) = df {
149                df.kill().unwrap();
150            }
151        }
152
153        #[test]
154        fn get_version() {
155            let mut client = crate::connect().unwrap();
156            let version = client.core().get_df_version().unwrap();
157            assert!(version.len() > 0);
158        }
159
160        #[test]
161        fn pause_unpause() {
162            let mut client = crate::connect().unwrap();
163
164            let initial_pause_status = client.remote_fortress_reader().get_pause_state().unwrap();
165
166            client
167                .remote_fortress_reader()
168                .set_pause_state(!initial_pause_status)
169                .unwrap();
170
171            let new_pause_status = client.remote_fortress_reader().get_pause_state().unwrap();
172
173            assert!(initial_pause_status != new_pause_status);
174        }
175    }
176}