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 { stats: Vec<String>, radius: u32 },
31 EquippedItems,
33 UnallocatedAscendancy,
35 SkillBreakdown(String),
37 GearModAnalysis(String),
39 SearchGems {
41 query: Option<String>,
42 gem_type: Option<String>,
43 tags: Vec<String>,
44 },
45 SearchUniques {
47 query: Option<String>,
48 slot: Option<String>,
49 min_level: Option<u32>,
50 max_level: Option<u32>,
51 },
52 ListCharms,
54 SearchRunes {
56 query: Option<String>,
57 slot: Option<String>,
58 },
59 SearchBases {
61 item_type: Option<String>,
62 query: Option<String>,
63 },
64 SearchMods {
66 query: Option<String>,
67 item_type_tag: Option<String>,
68 mod_type: Option<String>,
69 },
70 CreateItem { slot: String, item_text: String },
72}
73
74enum PobRequest {
76 Parse {
77 xml: String,
78 reply: oneshot::Sender<Result<Vec<u8>, PobParseError>>,
79 },
80 Query {
81 xml: String,
82 query: PobQuery,
83 reply: oneshot::Sender<Result<serde_json::Value, PobParseError>>,
84 },
85}
86
87#[derive(Debug, thiserror::Error)]
89pub enum PobParseError {
90 #[error("invalid build: {0}")]
92 InvalidBuild(String),
93
94 #[error("parser unavailable")]
96 Unavailable,
97}
98
99pub struct PobParser {
104 sender: Option<mpsc::Sender<PobRequest>>,
105 _thread: Option<thread::JoinHandle<()>>,
106}
107
108impl PobParser {
109 pub async fn new(pob_path: &Path) -> Result<Self, anyhow::Error> {
114 let (tx, rx) = mpsc::channel::<PobRequest>();
115 let (init_tx, init_rx) = oneshot::channel::<Result<(), String>>();
116
117 let pob_path_abs = pob_path
118 .canonicalize()
119 .map_err(|e| anyhow::anyhow!("pob_path {}: {e}", pob_path.display()))?;
120 let pob_path_str = pob_path_abs
121 .to_str()
122 .ok_or_else(|| anyhow::anyhow!("pob_path is not valid UTF-8"))?
123 .to_owned();
124
125 let handle = thread::spawn(move || {
126 run_parser_thread(&pob_path_str, init_tx, rx);
127 });
128
129 let init_result = init_rx
130 .await
131 .map_err(|_| anyhow::anyhow!("parser thread died during init"))?;
132
133 init_result.map_err(|e| anyhow::anyhow!("PobHeadless init failed: {e}"))?;
134
135 tracing::info!("PobParser ready");
136 Ok(Self {
137 sender: Some(tx),
138 _thread: Some(handle),
139 })
140 }
141
142 pub async fn parse(&self, xml: &[u8]) -> Result<Vec<u8>, PobParseError> {
144 let xml_str =
145 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
146
147 let (reply_tx, reply_rx) = oneshot::channel();
148
149 self.sender
150 .as_ref()
151 .ok_or(PobParseError::Unavailable)?
152 .send(PobRequest::Parse {
153 xml: xml_str.to_owned(),
154 reply: reply_tx,
155 })
156 .map_err(|_| PobParseError::Unavailable)?;
157
158 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
159 }
160
161 pub async fn query(
164 &self,
165 xml: &[u8],
166 query: PobQuery,
167 ) -> Result<serde_json::Value, PobParseError> {
168 let xml_str =
169 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
170
171 let (reply_tx, reply_rx) = oneshot::channel();
172
173 self.sender
174 .as_ref()
175 .ok_or(PobParseError::Unavailable)?
176 .send(PobRequest::Query {
177 xml: xml_str.to_owned(),
178 query,
179 reply: reply_tx,
180 })
181 .map_err(|_| PobParseError::Unavailable)?;
182
183 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
184 }
185}
186
187impl Drop for PobParser {
188 fn drop(&mut self) {
189 self.sender.take();
194 if let Some(handle) = self._thread.take() {
195 let _ = handle.join();
196 }
197 }
198}
199
200fn run_parser_thread(
202 pob_path: &str,
203 init_tx: oneshot::Sender<Result<(), String>>,
204 rx: mpsc::Receiver<PobRequest>,
205) {
206 let mut pob = match PobHeadless::new() {
207 Ok(p) => p,
208 Err(e) => {
209 let _ = init_tx.send(Err(format!("failed to create Lua runtime: {e}")));
210 return;
211 }
212 };
213
214 if let Err(e) = pob.init(pob_path) {
215 let _ = init_tx.send(Err(e.to_string()));
216 return;
217 }
218
219 let _ = init_tx.send(Ok(()));
220
221 for req in &rx {
223 match req {
224 PobRequest::Parse { xml, reply } => {
225 let result = parse_one(&pob, &xml);
226 let _ = reply.send(result);
227 }
228 PobRequest::Query { xml, query, reply } => {
229 let result = load_and_query(&pob, &xml, &query);
230 let _ = reply.send(result);
231 }
232 }
233 }
234
235 tracing::info!("parser thread shutting down");
236}
237
238fn parse_one(pob: &PobHeadless, xml: &str) -> Result<Vec<u8>, PobParseError> {
240 pob.load_build_xml(xml)
241 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
242
243 let stats = pob
244 .calculate()
245 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
246
247 serde_json::to_vec(&stats).map_err(|e| PobParseError::InvalidBuild(e.to_string()))
248}
249
250fn load_and_query(
252 pob: &PobHeadless,
253 xml: &str,
254 query: &PobQuery,
255) -> Result<serde_json::Value, PobParseError> {
256 pob.load_build_xml(xml)
257 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
258
259 let result = match query {
260 PobQuery::BuildStats => pob.query_build_stats(),
261 PobQuery::SkillList => pob.query_skill_list(),
262 PobQuery::Config => pob.query_config(),
263 PobQuery::Item(ref slot) => pob.query_item(slot),
264 PobQuery::Jewel(node_id) => pob.query_jewel(*node_id),
265 PobQuery::PassiveTree => pob.query_passive_tree(),
266 PobQuery::PassiveStats { ref stats, radius } => pob.query_passive_stats(stats, *radius),
267 PobQuery::EquippedItems => pob.query_equipped_items(),
268 PobQuery::UnallocatedAscendancy => pob.query_unallocated_ascendancy(),
269 PobQuery::SkillBreakdown(ref skill) => pob.query_skill_breakdown(skill),
270 PobQuery::GearModAnalysis(ref slot) => pob.query_gear_mod_analysis(slot),
271 PobQuery::SearchGems {
272 ref query,
273 ref gem_type,
274 ref tags,
275 } => pob.query_search_gems(query.as_deref(), gem_type.as_deref(), tags),
276 PobQuery::SearchUniques {
277 ref query,
278 ref slot,
279 ref min_level,
280 ref max_level,
281 } => pob.query_search_uniques(query.as_deref(), slot.as_deref(), *min_level, *max_level),
282 PobQuery::ListCharms => pob.query_list_charms(),
283 PobQuery::SearchRunes {
284 ref query,
285 ref slot,
286 } => pob.query_search_runes(query.as_deref(), slot.as_deref()),
287 PobQuery::SearchBases {
288 ref item_type,
289 ref query,
290 } => pob.query_search_bases(item_type.as_deref(), query.as_deref()),
291 PobQuery::SearchMods {
292 ref query,
293 ref item_type_tag,
294 ref mod_type,
295 } => pob.query_search_mods(
296 query.as_deref(),
297 item_type_tag.as_deref(),
298 mod_type.as_deref(),
299 ),
300 PobQuery::CreateItem {
301 ref slot,
302 ref item_text,
303 } => pob.create_item(slot, item_text),
304 };
305
306 result.map_err(|e| PobParseError::InvalidBuild(e.to_string()))
307}