use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::thread;
use anyhow::{Context, Result};
use csl_client::ModelClient;
use xplm::processing::{FlightLoop, FlightLoopPhase};
use crate::Multiplayer;
pub struct FetchedPackage {
pub package_dir: PathBuf,
pub csl_id: String,
}
struct Job {
icao: Option<String>,
airline: Option<String>,
livery: Option<String>,
seed: Option<u8>,
callback: Box<dyn FnOnce(Result<FetchedPackage>) + Send>,
}
fn read_csl_id(package_dir: &Path) -> Result<String> {
let root = package_dir
.file_name()
.and_then(|name| name.to_str())
.with_context(|| format!("{} has no usable file name", package_dir.display()))?;
let xsb_path = package_dir.join("xsb_aircraft.txt");
let content = std::fs::read_to_string(&xsb_path)
.with_context(|| format!("reading {}", xsb_path.display()))?;
let id = content
.lines()
.find_map(|line| line.strip_prefix("OBJ8_AIRCRAFT ").map(str::trim))
.with_context(|| format!("no OBJ8_AIRCRAFT line in {}", xsb_path.display()))?;
Ok(format!("{root}/{id}"))
}
pub struct CslCache {
jobs_tx: mpsc::Sender<Job>,
out_dir: PathBuf,
_poll_loop: FlightLoop,
}
impl CslCache {
pub fn new(
_multiplayer: &Multiplayer,
base_url: impl Into<String>,
out_dir: impl Into<PathBuf>,
) -> Self {
let out_dir = out_dir.into();
let (jobs_tx, jobs_rx) = mpsc::channel::<Job>();
let (completed_tx, completed_rx) = mpsc::channel::<Box<dyn FnOnce() + Send>>();
let worker_base_url = base_url.into();
let worker_out_dir = out_dir.clone();
thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return; };
let client = ModelClient::new(reqwest::Client::new(), worker_base_url, worker_out_dir);
for job in jobs_rx {
let result = runtime
.block_on(client.request(
job.icao.as_deref(),
job.airline.as_deref(),
job.livery.as_deref(),
job.seed,
))
.and_then(|model| {
let csl_id = read_csl_id(&model.package_dir)?;
Ok(FetchedPackage {
package_dir: model.package_dir,
csl_id,
})
});
let callback = job.callback;
let thunk: Box<dyn FnOnce() + Send> = Box::new(move || {
let result = result.and_then(|fetched| {
crate::load_csl_package_raw(&fetched.package_dir.to_string_lossy())
.map_err(|message| anyhow::anyhow!(message))?;
Ok(fetched)
});
callback(result);
});
if completed_tx.send(thunk).is_err() {
break; }
}
});
let poll_loop = FlightLoop::new(FlightLoopPhase::AfterFlightModel, move |_, _, _| {
for thunk in completed_rx.try_iter() {
thunk();
}
-1.0 });
poll_loop.schedule(-1.0, true);
Self {
jobs_tx,
out_dir,
_poll_loop: poll_loop,
}
}
pub fn request(
&self,
icao: Option<&str>,
airline: Option<&str>,
livery: Option<&str>,
seed: Option<u8>,
callback: impl FnOnce(Result<FetchedPackage>) + Send + 'static,
) {
let _ = self.jobs_tx.send(Job {
icao: icao.map(str::to_string),
airline: airline.map(str::to_string),
livery: livery.map(str::to_string),
seed,
callback: Box::new(callback),
});
}
pub fn out_dir(&self) -> &Path {
&self.out_dir
}
}