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