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}
30
31enum PobRequest {
33 Parse {
34 xml: String,
35 reply: oneshot::Sender<Result<Vec<u8>, PobParseError>>,
36 },
37 Query {
38 xml: String,
39 query: PobQuery,
40 reply: oneshot::Sender<Result<serde_json::Value, PobParseError>>,
41 },
42}
43
44#[derive(Debug, thiserror::Error)]
46pub enum PobParseError {
47 #[error("invalid build: {0}")]
49 InvalidBuild(String),
50
51 #[error("parser unavailable")]
53 Unavailable,
54}
55
56pub struct PobParser {
61 sender: Option<mpsc::Sender<PobRequest>>,
62 _thread: Option<thread::JoinHandle<()>>,
63}
64
65impl PobParser {
66 pub async fn new(pob_path: &Path) -> Result<Self, anyhow::Error> {
71 let (tx, rx) = mpsc::channel::<PobRequest>();
72 let (init_tx, init_rx) = oneshot::channel::<Result<(), String>>();
73
74 let pob_path_abs = pob_path
75 .canonicalize()
76 .map_err(|e| anyhow::anyhow!("pob_path {}: {e}", pob_path.display()))?;
77 let pob_path_str = pob_path_abs
78 .to_str()
79 .ok_or_else(|| anyhow::anyhow!("pob_path is not valid UTF-8"))?
80 .to_owned();
81
82 let handle = thread::spawn(move || {
83 run_parser_thread(&pob_path_str, init_tx, rx);
84 });
85
86 let init_result = init_rx
87 .await
88 .map_err(|_| anyhow::anyhow!("parser thread died during init"))?;
89
90 init_result.map_err(|e| anyhow::anyhow!("PobHeadless init failed: {e}"))?;
91
92 tracing::info!("PobParser ready");
93 Ok(Self {
94 sender: Some(tx),
95 _thread: Some(handle),
96 })
97 }
98
99 pub async fn parse(&self, xml: &[u8]) -> Result<Vec<u8>, PobParseError> {
101 let xml_str =
102 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
103
104 let (reply_tx, reply_rx) = oneshot::channel();
105
106 self.sender
107 .as_ref()
108 .ok_or(PobParseError::Unavailable)?
109 .send(PobRequest::Parse {
110 xml: xml_str.to_owned(),
111 reply: reply_tx,
112 })
113 .map_err(|_| PobParseError::Unavailable)?;
114
115 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
116 }
117
118 pub async fn query(
121 &self,
122 xml: &[u8],
123 query: PobQuery,
124 ) -> Result<serde_json::Value, PobParseError> {
125 let xml_str =
126 std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
127
128 let (reply_tx, reply_rx) = oneshot::channel();
129
130 self.sender
131 .as_ref()
132 .ok_or(PobParseError::Unavailable)?
133 .send(PobRequest::Query {
134 xml: xml_str.to_owned(),
135 query,
136 reply: reply_tx,
137 })
138 .map_err(|_| PobParseError::Unavailable)?;
139
140 reply_rx.await.map_err(|_| PobParseError::Unavailable)?
141 }
142}
143
144impl Drop for PobParser {
145 fn drop(&mut self) {
146 self.sender.take();
151 if let Some(handle) = self._thread.take() {
152 let _ = handle.join();
153 }
154 }
155}
156
157fn run_parser_thread(
159 pob_path: &str,
160 init_tx: oneshot::Sender<Result<(), String>>,
161 rx: mpsc::Receiver<PobRequest>,
162) {
163 let mut pob = match PobHeadless::new() {
164 Ok(p) => p,
165 Err(e) => {
166 let _ = init_tx.send(Err(format!("failed to create Lua runtime: {e}")));
167 return;
168 }
169 };
170
171 if let Err(e) = pob.init(pob_path) {
172 let _ = init_tx.send(Err(e.to_string()));
173 return;
174 }
175
176 let _ = init_tx.send(Ok(()));
177
178 for req in &rx {
180 match req {
181 PobRequest::Parse { xml, reply } => {
182 let result = parse_one(&pob, &xml);
183 let _ = reply.send(result);
184 }
185 PobRequest::Query { xml, query, reply } => {
186 let result = load_and_query(&pob, &xml, &query);
187 let _ = reply.send(result);
188 }
189 }
190 }
191
192 tracing::info!("parser thread shutting down");
193}
194
195fn parse_one(pob: &PobHeadless, xml: &str) -> Result<Vec<u8>, PobParseError> {
197 pob.load_build_xml(xml)
198 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
199
200 let stats = pob
201 .calculate()
202 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
203
204 serde_json::to_vec(&stats).map_err(|e| PobParseError::InvalidBuild(e.to_string()))
205}
206
207fn load_and_query(
209 pob: &PobHeadless,
210 xml: &str,
211 query: &PobQuery,
212) -> Result<serde_json::Value, PobParseError> {
213 pob.load_build_xml(xml)
214 .map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
215
216 let result = match query {
217 PobQuery::BuildStats => pob.query_build_stats(),
218 PobQuery::SkillList => pob.query_skill_list(),
219 PobQuery::Config => pob.query_config(),
220 PobQuery::Item(ref slot) => pob.query_item(slot),
221 PobQuery::Jewel(node_id) => pob.query_jewel(*node_id),
222 PobQuery::PassiveTree => pob.query_passive_tree(),
223 };
224
225 result.map_err(|e| PobParseError::InvalidBuild(e.to_string()))
226}