use std::path::Path;
use std::sync::mpsc;
use std::thread;
use tokio::sync::oneshot;
use crate::pob::PobHeadless;
#[derive(Debug, Clone)]
pub enum PobQuery {
BuildStats,
SkillList,
Config,
Item(String),
Jewel(i64),
PassiveTree,
}
enum PobRequest {
Parse {
xml: String,
reply: oneshot::Sender<Result<Vec<u8>, PobParseError>>,
},
Query {
xml: String,
query: PobQuery,
reply: oneshot::Sender<Result<serde_json::Value, PobParseError>>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum PobParseError {
#[error("invalid build: {0}")]
InvalidBuild(String),
#[error("parser unavailable")]
Unavailable,
}
pub struct PobParser {
sender: Option<mpsc::Sender<PobRequest>>,
_thread: Option<thread::JoinHandle<()>>,
}
impl PobParser {
pub async fn new(pob_path: &Path) -> Result<Self, anyhow::Error> {
let (tx, rx) = mpsc::channel::<PobRequest>();
let (init_tx, init_rx) = oneshot::channel::<Result<(), String>>();
let pob_path_abs = pob_path
.canonicalize()
.map_err(|e| anyhow::anyhow!("pob_path {}: {e}", pob_path.display()))?;
let pob_path_str = pob_path_abs
.to_str()
.ok_or_else(|| anyhow::anyhow!("pob_path is not valid UTF-8"))?
.to_owned();
let handle = thread::spawn(move || {
run_parser_thread(&pob_path_str, init_tx, rx);
});
let init_result = init_rx
.await
.map_err(|_| anyhow::anyhow!("parser thread died during init"))?;
init_result.map_err(|e| anyhow::anyhow!("PobHeadless init failed: {e}"))?;
tracing::info!("PobParser ready");
Ok(Self {
sender: Some(tx),
_thread: Some(handle),
})
}
pub async fn parse(&self, xml: &[u8]) -> Result<Vec<u8>, PobParseError> {
let xml_str =
std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
let (reply_tx, reply_rx) = oneshot::channel();
self.sender
.as_ref()
.ok_or(PobParseError::Unavailable)?
.send(PobRequest::Parse {
xml: xml_str.to_owned(),
reply: reply_tx,
})
.map_err(|_| PobParseError::Unavailable)?;
reply_rx.await.map_err(|_| PobParseError::Unavailable)?
}
pub async fn query(
&self,
xml: &[u8],
query: PobQuery,
) -> Result<serde_json::Value, PobParseError> {
let xml_str =
std::str::from_utf8(xml).map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
let (reply_tx, reply_rx) = oneshot::channel();
self.sender
.as_ref()
.ok_or(PobParseError::Unavailable)?
.send(PobRequest::Query {
xml: xml_str.to_owned(),
query,
reply: reply_tx,
})
.map_err(|_| PobParseError::Unavailable)?;
reply_rx.await.map_err(|_| PobParseError::Unavailable)?
}
}
impl Drop for PobParser {
fn drop(&mut self) {
self.sender.take();
if let Some(handle) = self._thread.take() {
let _ = handle.join();
}
}
}
fn run_parser_thread(
pob_path: &str,
init_tx: oneshot::Sender<Result<(), String>>,
rx: mpsc::Receiver<PobRequest>,
) {
let mut pob = match PobHeadless::new() {
Ok(p) => p,
Err(e) => {
let _ = init_tx.send(Err(format!("failed to create Lua runtime: {e}")));
return;
}
};
if let Err(e) = pob.init(pob_path) {
let _ = init_tx.send(Err(e.to_string()));
return;
}
let _ = init_tx.send(Ok(()));
for req in &rx {
match req {
PobRequest::Parse { xml, reply } => {
let result = parse_one(&pob, &xml);
let _ = reply.send(result);
}
PobRequest::Query { xml, query, reply } => {
let result = load_and_query(&pob, &xml, &query);
let _ = reply.send(result);
}
}
}
tracing::info!("parser thread shutting down");
}
fn parse_one(pob: &PobHeadless, xml: &str) -> Result<Vec<u8>, PobParseError> {
pob.load_build_xml(xml)
.map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
let stats = pob
.calculate()
.map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
serde_json::to_vec(&stats).map_err(|e| PobParseError::InvalidBuild(e.to_string()))
}
fn load_and_query(
pob: &PobHeadless,
xml: &str,
query: &PobQuery,
) -> Result<serde_json::Value, PobParseError> {
pob.load_build_xml(xml)
.map_err(|e| PobParseError::InvalidBuild(e.to_string()))?;
let result = match query {
PobQuery::BuildStats => pob.query_build_stats(),
PobQuery::SkillList => pob.query_skill_list(),
PobQuery::Config => pob.query_config(),
PobQuery::Item(ref slot) => pob.query_item(slot),
PobQuery::Jewel(node_id) => pob.query_jewel(*node_id),
PobQuery::PassiveTree => pob.query_passive_tree(),
};
result.map_err(|e| PobParseError::InvalidBuild(e.to_string()))
}