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
17pub type Client = Stubs<Channel>;
19
20pub fn connect() -> Result<Client> {
37 let connexion = Channel::connect()?;
38 Ok(Stubs::from(connexion))
39}
40
41pub fn connect_to(address: &str) -> Result<Client> {
57 let connexion = Channel::connect_to(address)?;
58 Ok(Stubs::from(connexion))
59}
60
61pub type Result<T> = std::result::Result<T, Error>;
63
64#[derive(thiserror::Error, Debug)]
66pub enum Error {
67 #[error("communication failure: {0}")]
72 CommunicationFailure(#[from] std::io::Error),
73
74 #[error("protocol error: {0}.")]
78 ProtocolError(String),
79
80 #[error("protobuf serialization error: {0}.")]
85 ProtobufError(#[from] prost::DecodeError),
86
87 #[error("failed to bind {0}.")]
92 FailedToBind(String),
93
94 #[error("RPC error: {result}.")]
96 RpcError {
97 result: CommandResult,
99 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}