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
15pub type Client = Stubs<Channel>;
17
18pub fn connect() -> Result<Client> {
35 let connexion = Channel::connect()?;
36 Ok(Stubs::from(connexion))
37}
38
39pub fn connect_to(address: &str) -> Result<Client> {
55 let connexion = Channel::connect_to(address)?;
56 Ok(Stubs::from(connexion))
57}
58
59pub type Result<T> = std::result::Result<T, Error>;
61
62#[derive(thiserror::Error, Debug)]
64pub enum Error {
65 #[error("communication failure: {0}")]
70 CommunicationFailure(#[from] std::io::Error),
71
72 #[error("protocol error: {0}.")]
76 ProtocolError(String),
77
78 #[error("protobuf serialization error: {0}.")]
83 ProtobufError(#[from] protobuf::Error),
84
85 #[error("failed to bind {0}.")]
90 FailedToBind(String),
91
92 #[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}