Skip to main content

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