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};
const STUB_PNG_1X1: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
pub fn write_stub_resource_dir(dir: impl AsRef<Path>) -> std::io::Result<()> {
let dir = dir.as_ref();
std::fs::create_dir_all(dir)?;
for (name, content) in [
("related.txt", &b""[..]),
("Doc8643.txt", &b""[..]),
("MapIcons.png", STUB_PNG_1X1),
] {
let path = dir.join(name);
if !path.exists() {
std::fs::write(&path, content)?;
}
}
Ok(())
}
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>,
blobs_pkg: impl Into<String>,
) -> Result<Self, String> {
let out_dir = out_dir.into();
let blobs_pkg = blobs_pkg.into();
let blobs_dir = out_dir.join(&blobs_pkg);
std::fs::create_dir_all(&blobs_dir)
.map_err(|e| format!("creating {}: {e}", blobs_dir.display()))?;
let xsb_path = blobs_dir.join("xsb_aircraft.txt");
if !xsb_path.exists() {
std::fs::write(&xsb_path, format!("EXPORT_NAME {blobs_pkg}\n"))
.map_err(|e| format!("writing {}: {e}", xsb_path.display()))?;
}
crate::load_csl_package_raw(&blobs_dir.to_string_lossy())?;
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);
Ok(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
}
pub fn load_local(&self, csl_folder: &str) -> Result<(), String> {
crate::load_csl_package_raw(csl_folder)
}
}