1use std::path::Path;
7use std::sync::mpsc;
8use std::thread;
9
10use tokio::sync::oneshot;
11
12use crate::pob::PobHeadless;
13
14#[derive(Debug, Clone)]
16pub enum PobQuery {
17 BuildStats,
19 SkillList,
21 Config,
23 Item(String),
25 Jewel(i64),
27 PassiveTree,
29 PassiveStats { stat: String, radius: u32 },
31 EmptySlots,
33 UnallocatedAscendancy,
35}
36
37enum PobRequest {
39 Parse {
40 xml: String,
41 reply: oneshot::Sender<Result<Vec<u8>, PobParseError>>,
42 },
43 Query {
44 xml: String,
45 query: PobQuery,
46 reply: oneshot::Sender<Result<serde_json::Value, PobParseError>>,
47 },
48}
49
50#[derive(Debug, thiserror::Error)]
52pub enum PobParseError {
53 #[error("invalid build: {0}")]
55 InvalidBuild(String),
56
57 #[error("parser unavailable")]
59 Unavailable,
60}
61
62pub struct PobParser {
67 sender: Option<mpsc::Sender<PobRequest>>,
68 _thread: Option<thread::JoinHandle<()>>,
69}
70
71impl PobParser {
72 pub async fn new(pob_path: &Path) -> Result<Self, anyhow::Error> {
77 let (tx, rx) = mpsc::channel::<PobRequest>();
78 let (init_tx, init_rx) = oneshot::channel::<Result<(), String>>();
79
80 let pob_path_abs = pob_path
81 .canonicalize()
82 .map_err(|e| anyhow::anyhow!("pob_path {}: {e}", pob_path.display()))?;
83 let pob_path_str = pob_path_abs
84 .to_str()
85 .ok_or_else(|| anyhow::anyhow!("pob_path is not valid UTF-8"))?
86 .to_owned();
87
88 let handle = thread::spawn(move || {
89 run_parser_thread(&pob_path_str, init_tx, rx);
90 });
91
92 let init_result = init_rx
93 .await
94 .map_err(|_| anyhow::anyhow!("parser thread died during init"))?;
95
96 init_result.map_err(|e| anyhow::anyhow!("PobHeadless init failed: {e}"))?;
97
98 tracing::info!("PobParser ready");
99 Ok(Self {
100 sender: Some(tx),
101 _thread: Some(handle),
102 })
103 }
104
105 pub async fn parse(&self, xml: &[u8]) -> Result<Vec<u8>, PobParseError> {
107 let xml_str =
108 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
109
110 let (reply_tx, reply_rx) = oneshot::channel();
111
112 self.sender
113 .as_ref()
114 .ok_or(PobParseError::Unavailable)?
115 .send(PobRequest::Parse {
116 xml: xml_str.to_owned(),
117 reply: reply_tx,
118 })
119 .map_err(|_| PobParseError::Unavailable)?;
120
121 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
122 }
123
124 pub async fn query(
127 &self,
128 xml: &[u8],
129 query: PobQuery,
130 ) -> Result<serde_json::Value, PobParseError> {
131 let xml_str =
132 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
133
134 let (reply_tx, reply_rx) = oneshot::channel();
135
136 self.sender
137 .as_ref()
138 .ok_or(PobParseError::Unavailable)?
139 .send(PobRequest::Query {
140 xml: xml_str.to_owned(),
141 query,
142 reply: reply_tx,
143 })
144 .map_err(|_| PobParseError::Unavailable)?;
145
146 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
147 }
148}
149
150impl Drop for PobParser {
151 fn drop(&mut self) {
152 self.sender.take();
157 if let Some(handle) = self._thread.take() {
158 let _ = handle.join();
159 }
160 }
161}
162
163fn run_parser_thread(
165 pob_path: &str,
166 init_tx: oneshot::Sender<Result<(), String>>,
167 rx: mpsc::Receiver<PobRequest>,
168) {
169 let mut pob = match PobHeadless::new() {
170 Ok(p) => p,
171 Err(e) => {
172 let _ = init_tx.send(Err(format!("failed to create Lua runtime: {e}")));
173 return;
174 }
175 };
176
177 if let Err(e) = pob.init(pob_path) {
178 let _ = init_tx.send(Err(e.to_string()));
179 return;
180 }
181
182 let _ = init_tx.send(Ok(()));
183
184 for req in &rx {
186 match req {
187 PobRequest::Parse { xml, reply } => {
188 let result = parse_one(&pob, &xml);
189 let _ = reply.send(result);
190 }
191 PobRequest::Query { xml, query, reply } => {
192 let result = load_and_query(&pob, &xml, &query);
193 let _ = reply.send(result);
194 }
195 }
196 }
197
198 tracing::info!("parser thread shutting down");
199}
200
201fn parse_one(pob: &PobHeadless, xml: &str) -> Result<Vec<u8>, PobParseError> {
203 pob.load_build_xml(xml)
204 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
205
206 let stats = pob
207 .calculate()
208 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
209
210 serde_json::to_vec(&stats).map_err(|e| PobParseError::InvalidBuild(e.to_string()))
211}
212
213fn load_and_query(
215 pob: &PobHeadless,
216 xml: &str,
217 query: &PobQuery,
218) -> Result<serde_json::Value, PobParseError> {
219 pob.load_build_xml(xml)
220 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
221
222 let result = match query {
223 PobQuery::BuildStats => pob.query_build_stats(),
224 PobQuery::SkillList => pob.query_skill_list(),
225 PobQuery::Config => pob.query_config(),
226 PobQuery::Item(ref slot) => pob.query_item(slot),
227 PobQuery::Jewel(node_id) => pob.query_jewel(*node_id),
228 PobQuery::PassiveTree => pob.query_passive_tree(),
229 PobQuery::PassiveStats { ref stat, radius } => pob.query_passive_stats(stat, *radius),
230 PobQuery::EmptySlots => pob.query_empty_slots(),
231 PobQuery::UnallocatedAscendancy => pob.query_unallocated_ascendancy(),
232 };
233
234 result.map_err(|e| PobParseError::InvalidBuild(e.to_string()))
235}