use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use anyhow::{Context, Result};
use csl_client::ModelClient;
use xplm::processing::{FlightLoop, FlightLoopPhase};
use crate::Multiplayer;
#[derive(Clone)]
pub struct FetchedPackage {
pub package_dir: PathBuf,
pub csl_id: String,
}
type RequestKey = (Option<String>, Option<String>, Option<String>, Option<u8>);
struct Job {
key: RequestKey,
icao: Option<String>,
airline: Option<String>,
livery: Option<String>,
seed: Option<u8>,
}
type Waiters = Arc<Mutex<HashMap<RequestKey, Vec<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,
in_flight: Waiters,
_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::<(RequestKey, Result<FetchedPackage, String>)>();
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,
})
})
.map_err(|err| err.to_string());
if completed_tx.send((job.key, result)).is_err() {
break; }
}
});
let in_flight: Waiters = Arc::new(Mutex::new(HashMap::new()));
let in_flight_for_loop = Arc::clone(&in_flight);
let poll_loop = FlightLoop::new(FlightLoopPhase::AfterFlightModel, move |_, _, _| {
for (key, result) in completed_rx.try_iter() {
let waiters = in_flight_for_loop
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.remove(&key)
.unwrap_or_default();
if waiters.is_empty() {
continue;
}
let loaded = result.and_then(|fetched| {
crate::load_csl_package_raw(&fetched.package_dir.to_string_lossy())?;
Ok(fetched)
});
for callback in waiters {
callback(loaded.clone().map_err(|message| anyhow::anyhow!(message)));
}
}
-1.0 });
poll_loop.schedule(-1.0, true);
Self {
jobs_tx,
out_dir,
in_flight,
_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 key = (
icao.map(str::to_string),
airline.map(str::to_string),
livery.map(str::to_string),
seed,
);
let mut in_flight = self
.in_flight
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(waiters) = in_flight.get_mut(&key) {
waiters.push(Box::new(callback));
return;
}
in_flight.insert(key.clone(), vec![Box::new(callback)]);
drop(in_flight);
let _ = self.jobs_tx.send(Job {
key,
icao: icao.map(str::to_string),
airline: airline.map(str::to_string),
livery: livery.map(str::to_string),
seed,
});
}
pub fn out_dir(&self) -> &Path {
&self.out_dir
}
}