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:
@@ -0,0 +1,25 @@
|
||||
//! Game files built into the executable by `build.rs`.
|
||||
|
||||
static PACK: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/embedded.bin"));
|
||||
|
||||
/// Every embedded file as (archive path, contents). Empty when the build
|
||||
/// found no disk images.
|
||||
pub fn files() -> Vec<(String, &'static [u8])> {
|
||||
parse(PACK).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn parse(d: &'static [u8]) -> Option<Vec<(String, &'static [u8])>> {
|
||||
let rest = d.strip_prefix(b"SKIDPAK1")?;
|
||||
let (count, mut rest) = rest.split_at_checked(4)?;
|
||||
let count = u32::from_le_bytes(count.try_into().ok()?);
|
||||
let mut out = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
let (len, r) = rest.split_at_checked(2)?;
|
||||
let (name, r) = r.split_at_checked(u16::from_le_bytes(len.try_into().ok()?) as usize)?;
|
||||
let (len, r) = r.split_at_checked(4)?;
|
||||
let (data, r) = r.split_at_checked(u32::from_le_bytes(len.try_into().ok()?) as usize)?;
|
||||
out.push((String::from_utf8_lossy(name).into_owned(), data));
|
||||
rest = r;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
+23
-10
@@ -1,9 +1,12 @@
|
||||
//! Locates and decodes the original game data.
|
||||
//!
|
||||
//! Data can be supplied as the original ADF images, as the LHA archives taken
|
||||
//! off them, or as an already-extracted directory tree containing `track/` and
|
||||
//! `vehicles/`. Nothing from the original game is embedded in this program.
|
||||
//! The default build embeds the data from the ADF images it was built
|
||||
//! alongside (see `build.rs`), so no disks are needed at run time. Extra or
|
||||
//! replacement data can still be supplied as ADF images, the LHA archives
|
||||
//! from them, or an extracted tree with `track/`, `vehicles/` and `palettes/`.
|
||||
//! Files on disk override the built-in ones.
|
||||
|
||||
pub mod embedded;
|
||||
pub mod palette;
|
||||
pub mod track;
|
||||
pub mod vehicle;
|
||||
@@ -25,12 +28,18 @@ pub struct FileSet {
|
||||
}
|
||||
|
||||
impl FileSet {
|
||||
pub fn scan(root: &Path) -> FileSet {
|
||||
pub fn embedded() -> FileSet {
|
||||
let mut fs_ = FileSet::default();
|
||||
fs_.scan_dir(root, 0);
|
||||
for (path, data) in embedded::files() {
|
||||
fs_.add(&path, data.to_vec());
|
||||
}
|
||||
fs_
|
||||
}
|
||||
|
||||
pub fn scan(&mut self, root: &Path) {
|
||||
self.scan_dir(root, 0);
|
||||
}
|
||||
|
||||
fn scan_dir(&mut self, dir: &Path, depth: usize) {
|
||||
let Ok(rd) = fs::read_dir(dir) else {
|
||||
self.warnings
|
||||
@@ -116,8 +125,12 @@ pub struct GameData {
|
||||
}
|
||||
|
||||
impl GameData {
|
||||
pub fn load(root: &Path) -> Result<GameData, String> {
|
||||
let files = FileSet::scan(root);
|
||||
/// Loads the built-in data, overlaid with whatever is under `dir`.
|
||||
pub fn load(dir: Option<&Path>) -> Result<GameData, String> {
|
||||
let mut files = FileSet::embedded();
|
||||
if let Some(dir) = dir {
|
||||
files.scan(dir);
|
||||
}
|
||||
let mut warnings = files.warnings.clone();
|
||||
|
||||
let mut track_files = Vec::new();
|
||||
@@ -145,9 +158,9 @@ impl GameData {
|
||||
|
||||
if track_files.is_empty() || vehicles.is_empty() {
|
||||
return Err(format!(
|
||||
"no usable game data under {} (found {} track files, {} vehicles).\n\
|
||||
Point --data at a folder containing the Super Skidmarks ADF images.{}",
|
||||
root.display(),
|
||||
"no usable game data (found {} track files, {} vehicles).\n\
|
||||
This build has no data built in: point --data at a folder containing the \
|
||||
Super Skidmarks ADF images, or rebuild with the images next to Cargo.toml.{}",
|
||||
track_files.len(),
|
||||
vehicles.len(),
|
||||
if warnings.is_empty() {
|
||||
|
||||
+21
-13
@@ -30,9 +30,11 @@ USAGE:
|
||||
skids trace [--data DIR] --out FILE.png [--track N] [--seconds S]
|
||||
draw the computer cars' paths on a track
|
||||
|
||||
The data directory should hold the Super Skidmarks ADF images (or the LHA
|
||||
archives from them, or extracted track/ and vehicles/ folders). It defaults to
|
||||
the current directory, then the directory the executable is in.
|
||||
The game data is built into the executable, so no disks are needed. --data
|
||||
adds to or overrides it with ADF images, LHA archives, or extracted track/,
|
||||
vehicles/ and palettes/ folders (e.g. disk 1, or real palettes). Without
|
||||
--data, a folder like that in the current directory or next to the
|
||||
executable is picked up automatically.
|
||||
|
||||
CONTROLS:
|
||||
Player 1: arrow keys (or W A S D in one-player games)
|
||||
@@ -83,9 +85,12 @@ fn parse_args() -> Result<Args, String> {
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
fn find_data(explicit: Option<PathBuf>) -> PathBuf {
|
||||
if let Some(p) = explicit {
|
||||
return p;
|
||||
/// An explicit --data directory, or a nearby folder that looks like it holds
|
||||
/// game data (e.g. disk 1 dropped next to the executable). The built-in data
|
||||
/// is always loaded first, so finding nothing is fine.
|
||||
fn find_data(explicit: Option<PathBuf>) -> Option<PathBuf> {
|
||||
if explicit.is_some() {
|
||||
return explicit;
|
||||
}
|
||||
let looks_right = |d: &Path| {
|
||||
std::fs::read_dir(d).is_ok_and(|rd| {
|
||||
@@ -103,10 +108,7 @@ fn find_data(explicit: Option<PathBuf>) -> PathBuf {
|
||||
candidates.push(dir);
|
||||
}
|
||||
candidates.push(PathBuf::from("data"));
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|d| looks_right(d))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
candidates.into_iter().find(|d| looks_right(d))
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -125,7 +127,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
};
|
||||
let root = find_data(args.data.clone());
|
||||
let data = match GameData::load(&root) {
|
||||
let data = match GameData::load(root.as_deref()) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
@@ -137,7 +139,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
let result = match args.cmd.as_str() {
|
||||
"info" => {
|
||||
info(&data);
|
||||
info(&data, root.as_deref());
|
||||
Ok(())
|
||||
}
|
||||
"extract" => extract(&data, args.out.as_deref()),
|
||||
@@ -155,7 +157,13 @@ fn main() -> ExitCode {
|
||||
}
|
||||
}
|
||||
|
||||
fn info(data: &GameData) {
|
||||
fn info(data: &GameData, dir: Option<&Path>) {
|
||||
let built_in = data::embedded::files().len();
|
||||
match (built_in, dir) {
|
||||
(0, Some(d)) => println!("Data: {}", d.display()),
|
||||
(n, Some(d)) => println!("Data: built in ({n} files), plus {}", d.display()),
|
||||
(n, None) => println!("Data: built in ({n} files)"),
|
||||
}
|
||||
println!("Tracks:");
|
||||
for (f, s) in data.track_list() {
|
||||
let tf = &data.track_files[f];
|
||||
|
||||
Reference in New Issue
Block a user