Embed the game data so the disks are not needed at run time

build.rs unpacks the ADF images next to Cargo.toml (or in
SKIDS_EMBED_DIR) with the same ADF/LHA readers the game uses and packs
the files into the executable. At run time the built-in data loads
first and anything given with --data, or found next to the executable,
overrides it. The embed feature is on by default; builds without it
behave as before and ask for --data. CI now runs the binary from an
empty directory.
This commit is contained in:
2026-10-19 18:45:01 +00:00
parent f88ba23282
commit bfaa1a287b
7 changed files with 186 additions and 34 deletions
+84
View File
@@ -0,0 +1,84 @@
//! Embeds the game data in the executable so it runs without the disks.
//!
//! The ADF images next to Cargo.toml (or in `SKIDS_EMBED_DIR`) are unpacked
//! at build time and the files inside their LHA archives are packed into
//! `$OUT_DIR/embedded.bin`, which `src/data/embedded.rs` includes. If no
//! images are found the pack is empty and the game asks for `--data`.
//! Disable with `--no-default-features --features audio`.
#[path = "src/formats/mod.rs"]
#[allow(dead_code)]
mod formats;
use formats::{adf::Adf, lha};
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=SKIDS_EMBED_DIR");
let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("embedded.bin");
let mut files: Vec<(String, Vec<u8>)> = Vec::new();
if env::var_os("CARGO_FEATURE_EMBED").is_some() {
let dir = env::var_os("SKIDS_EMBED_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()));
println!("cargo:rerun-if-changed={}", dir.display());
let mut images: Vec<PathBuf> = fs::read_dir(&dir)
.map(|rd| rd.flatten().map(|e| e.path()).collect())
.unwrap_or_default();
images.retain(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("adf")));
images.sort();
for image in images {
println!("cargo:rerun-if-changed={}", image.display());
let name = image.display().to_string();
let disk = match fs::read(&image)
.map_err(|e| e.to_string())
.and_then(|d| Adf::open(d).map_err(|e| e.to_string()))
.and_then(|a| a.files().map_err(|e| e.to_string()))
{
Ok(f) => f,
Err(e) => {
println!("cargo:warning=skipping {name}: {e}");
continue;
}
};
for f in disk {
if !f.path.to_lowercase().ends_with(".lha") {
files.push((f.path, f.data));
continue;
}
match lha::parse(&f.data) {
Ok(entries) => {
for e in entries {
if let Some(d) = e.data {
files.push((e.path, d));
}
}
}
Err(e) => println!("cargo:warning=skipping {name}/{}: {e}", f.path),
}
}
}
if files.is_empty() {
println!(
"cargo:warning=no Super Skidmarks ADF images in {}; the game will need --data",
dir.display()
);
}
}
// Pack: "SKIDPAK1", u32 count, then per file u16 name length, name,
// u32 data length, data. All little-endian.
let mut pack = b"SKIDPAK1".to_vec();
pack.extend_from_slice(&(files.len() as u32).to_le_bytes());
for (name, data) in &files {
pack.extend_from_slice(&(name.len() as u16).to_le_bytes());
pack.extend_from_slice(name.as_bytes());
pack.extend_from_slice(&(data.len() as u32).to_le_bytes());
pack.extend_from_slice(data);
}
fs::write(out, pack).unwrap();
}