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
16pub type Client = Stubs<Channel>;
18
19pub fn connect() -> Result<Client> {
36 let connexion = Channel::connect()?;
37 Ok(Stubs::from(connexion))
38}
39
40pub fn connect_to(address: &str) -> Result<Client> {
56 let connexion = Channel::connect_to(address)?;
57 Ok(Stubs::from(connexion))
58}
59
60pub type Result<T> = std::result::Result<T, Error>;
62
63#[derive(thiserror::Error, Debug)]
65pub enum Error {
66 #[error("communication failure: {0}")]
71 CommunicationFailure(#[from] std::io::Error),
72
73 #[error("protocol error: {0}.")]
77 ProtocolError(String),
78
79 #[error("protobuf serialization error: {0}.")]
84 ProtobufError(#[from] protobuf::Error),
85
86 #[error("failed to bind {0}.")]
91 FailedToBind(String),
92
93 #[error("RPC error: {result}.")]
95 RpcError {
96 result: CommandResult,
98 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}