//! The `gc-trace` command line. //! //! ```text //! gc-trace compare-tfrag-light --level N //! gc-trace info //! gc-trace dump-ee [--out FILE] //! gc-trace find --bytes HEX //! gc-trace read --addr HEX --len N //! gc-trace pine-info //! gc-trace pine-savestate SLOT //! ``` //! //! `` is where the memory comes from, and it is one of: //! //! | Flag | Reads | //! |---|---| //! | `--state FILE` | a PCSX2 `.p2s` savestate | //! | `--state latest` | the newest state in PCSX2's folder | //! | `--ee FILE` | a raw 32 MiB EE RAM dump | //! | `--pine [slot]` | the running emulator over PINE | use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use gc_trace::ee::EeRam; use gc_trace::{compare, LevelInputs}; fn main() { let args: Vec = std::env::args().skip(1).collect(); match run(&args) { Ok(code) => std::process::exit(code), Err(e) => { eprintln!("error: {e:#}"); std::process::exit(1); } } } /// Returns the process exit code. fn run(args: &[String]) -> Result { let Some(cmd) = args.first() else { print!("{USAGE}"); return Ok(0); }; match cmd.as_str() { "info" => { let src = Source::parse(&args[1..])?; info(&src) } "dump-ee" => { let (src, rest) = Source::parse_with_rest(&args[1..])?; let out = flag(&rest, "--out").unwrap_or_else(|| "ee.bin".into()); dump_ee(&src, &out) } "find" => { let (src, rest) = Source::parse_with_rest(&args[1..])?; let hex = require(&rest, "--bytes")?; find(&src, &hex) } "read" => { let (src, rest) = Source::parse_with_rest(&args[1..])?; let addr = require(&rest, "--addr")?; let len = require(&rest, "--len")?; read(&src, &addr, &len) } "verify-overlay-base" => { // gc-trace verify-overlay-base let (src, rest) = Source::parse_with_rest(&args[1..])?; let (dir, level) = match rest.as_slice() { [dir, level, ..] => (PathBuf::from(dir), level.parse::().context("level")?), _ => { return Err(anyhow!( "usage: gc-trace verify-overlay-base " )) } }; let overlay = std::fs::read(dir.join(format!("level{level}/overlay.bin"))) .with_context(|| format!("level{level}/overlay.bin"))?; let bytes = src.load()?; let ram = EeRam::new(&bytes)?; let report = gc_trace::overlay_base::verify(&overlay, &ram)?; let mut text = String::new(); gc_trace::overlay_base::print_report(&report, &mut text)?; println!("{text}"); Ok(i32::from(!report.names.iter().all(|&b| b))) } "verify-tie-records" => { // gc-trace verify-tie-records let (src, rest) = Source::parse_with_rest(&args[1..])?; let (dir, level, class, lod) = match rest.as_slice() { [dir, level, class, lod, ..] => ( PathBuf::from(dir), level.parse::().context("level")?, class.parse::().context("class")?, lod.parse::().context("lod")?, ), _ => { return Err(anyhow!( "usage: gc-trace verify-tie-records " )) } }; let idx = std::fs::read(dir.join(format!("level{level}/core_index.bin"))) .with_context(|| format!("level{level}/core_index.bin"))?; let core = std::fs::read(dir.join(format!("level{level}/core_data.bin"))) .with_context(|| format!("level{level}/core_data.bin"))?; let bytes = src.load()?; let ram = EeRam::new(&bytes)?; let report = gc_trace::tie_verify::verify(&idx, &core, class, lod, &ram)?; let mut text = String::new(); gc_trace::tie_verify::print_report(&report, &mut text)?; println!("{text}"); Ok(report.exit_code()) } "compare-tfrag-light" => { let (src, rest) = Source::parse_with_rest(&args[1..])?; let dir = rest .first() .map(PathBuf::from) .ok_or_else(|| anyhow!("which unpacked level directory?"))?; let level: usize = flag(&rest, "--level") .unwrap_or_else(|| "0".into()) .parse() .context("--level")?; compare_tfrag_light(&dir, level, &src) } "pine-info" => pine_info(), "pine-savestate" => { let slot: u8 = args .get(1) .ok_or_else(|| anyhow!("which slot?"))? .parse() .context("slot")?; pine_savestate(slot) } _ => { print!("{USAGE}"); Ok(0) } } } const USAGE: &str = "\ usage: gc-trace ... compare-tfrag-light [--level N] verify-tie-records verify-overlay-base info dump-ee [--out FILE] find --bytes HEX read --addr HEX --len N pine-info pine-savestate SLOT is one of: --state FILE a PCSX2 .p2s savestate --state latest the newest state in PCSX2's folder --ee FILE a raw 32 MiB EE RAM dump --pine [slot] the running emulator over PINE "; /// Where a memory image comes from. enum Source { State(PathBuf), Latest, Raw(PathBuf), Pine(u32), } impl Source { /// The first three arguments after the command. fn parse(args: &[String]) -> Result { Ok(Self::parse_with_rest(args)?.0) } /// The source flag, and everything else. /// /// The flag may sit anywhere: the usage line puts the level directory /// first, which is the order people type, so a first-argument-only reader /// would reject its own documented invocation. Whatever is left after the /// flag and its value is passed back for the caller's own flags. fn parse_with_rest(args: &[String]) -> Result<(Source, Vec)> { let Some(at) = args .iter() .position(|a| matches!(a.as_str(), "--state" | "--ee" | "--pine")) else { return Err(anyhow!("no source; pass --state FILE, --ee FILE or --pine")); }; let (flag, tail) = (args[at].as_str(), &args[at + 1..]); let (src, used) = match flag { "--state" => { let v = tail .first() .ok_or_else(|| anyhow!("--state needs a path or `latest`"))?; let src = if v == "latest" { Source::Latest } else { Source::State(PathBuf::from(v)) }; (src, 1) } "--ee" => { let v = tail.first().ok_or_else(|| anyhow!("--ee needs a path"))?; (Source::Raw(PathBuf::from(v)), 1) } // `--pine [slot]`: a following slot number is consumed, and // anything else -- a directory, another flag -- is not. _ => { let slot = match tail.first() { Some(s) if s.parse::().is_ok() => s.parse()?, _ => gc_trace::pine::DEFAULT_SLOT, }; let used = usize::from(tail.first().is_some_and(|s| s.parse::().is_ok())); (Source::Pine(slot), used) } }; let mut rest: Vec = args[..at].to_vec(); rest.extend_from_slice(&tail[used..]); Ok((src, rest)) } /// Load the RAM image this source names. fn load(&self) -> Result> { match self { Source::State(p) => gc_trace::Savestate::open(p) .and_then(|s| s.ee_ram_owned()) .with_context(|| format!("savestate {}", p.display())), Source::Latest => { let p = latest_state()?; let bytes = gc_trace::Savestate::open(&p).and_then(|s| s.ee_ram_owned()); bytes.with_context(|| format!("savestate {} (latest)", p.display())) } Source::Raw(p) => { let b = std::fs::read(p).with_context(|| format!("read {}", p.display()))?; Ok(b) } Source::Pine(slot) => gc_trace::pine::Pine::connect_slot(*slot) .and_then(|p| p.dump_ee()) .context("PINE dump"), } } } fn latest_state() -> Result { let dir = pcsx2_state_dir(); let mut newest: Option<(std::time::SystemTime, PathBuf)> = None; for entry in std::fs::read_dir(&dir).into_iter().flatten().flatten() { let p = entry.path(); if p.extension().map(|e| e == "p2s").unwrap_or(false) { if let Ok(meta) = entry.metadata() { if let Ok(t) = meta.modified() { if newest.as_ref().is_none_or(|(nt, _)| t > *nt) { newest = Some((t, p)); } } } } } newest .map(|(_, p)| p) .ok_or_else(|| anyhow!("no .p2s savestates in {}", dir.display())) } /// PCSX2's savestate folder, per platform. fn pcsx2_state_dir() -> PathBuf { if cfg!(target_os = "macos") { let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); PathBuf::from(home).join("Library/Application Support/PCSX2/sstates") } else { let base = std::env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .unwrap_or_else(|| { let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); PathBuf::from(home).join(".config") }); base.join("PCSX2/sstates") } } fn info(src: &Source) -> Result { match src { Source::State(p) => info_state(p)?, Source::Latest => info_state(&latest_state()?)?, Source::Raw(_) => { let bytes = src.load()?; let ram = EeRam::new(&bytes)?; println!("raw EE RAM image, {} bytes", ram.bytes().len()); } Source::Pine(slot) => { let pine = gc_trace::pine::Pine::connect_slot(*slot)?; println!("PINE: version {}", pine.version()?); println!("PINE: title {}", pine.title()?); } } Ok(0) } fn info_state(p: &std::path::Path) -> Result<()> { let state = gc_trace::Savestate::open(p)?; println!( "{}: v{:#010x} build {}", p.display(), state.version.unwrap_or(0), state.build ); for e in state.entries() { println!(" {e}"); } Ok(()) } fn dump_ee(src: &Source, out: &str) -> Result { let bytes = src.load()?; let ram = EeRam::new(&bytes)?; std::fs::write(out, ram.bytes()).with_context(|| format!("write {out}"))?; println!("wrote {} bytes to {out}", ram.bytes().len()); Ok(0) } fn find(src: &Source, hex: &str) -> Result { let bytes = src.load()?; let ram = EeRam::new(&bytes)?; let needle = parse_hex(hex)?; for at in ram.find_all(&needle) { println!("{at:#x}"); } Ok(0) } fn read(src: &Source, addr_hex: &str, len_hex: &str) -> Result { let bytes = src.load()?; let ram = EeRam::new(&bytes)?; let addr = usize::from_str_radix(addr_hex.trim_start_matches("0x"), 16)?; let len = usize::from_str_radix(len_hex.trim_start_matches("0x"), 16)?; let slice = ram .slice(addr, len) .ok_or_else(|| anyhow!("{addr:#x}+{len:#x} runs past the RAM image"))?; for (i, row) in slice.chunks(16).enumerate() { let hex: Vec = row.iter().map(|b| format!("{b:02x}")).collect(); println!("{:08x} {}", addr + i * 16, hex.join(" ")); } Ok(0) } fn compare_tfrag_light(dir: &std::path::Path, level: usize, src: &Source) -> Result { let inputs = LevelInputs::load(dir, level)?; let bytes = src.load()?; let ram = EeRam::new(&bytes)?; let report = compare(&inputs, level, &ram)?; let mut text = String::new(); gc_trace::print_report(&report, &mut text)?; println!("{text}"); Ok(report.exit_code()) } fn pine_info() -> Result { let pine = gc_trace::pine::Pine::connect()?; println!("version: {}", pine.version()?); println!("title: {}", pine.title()?); println!( "PINE is up on {}", gc_trace::pine::socket_path(gc_trace::pine::DEFAULT_SLOT).display() ); Ok(0) } fn pine_savestate(slot: u8) -> Result { let pine = gc_trace::pine::Pine::connect()?; pine.save_state(slot)?; println!("savestate requested for slot {slot}"); Ok(0) } /// Parse `hex` bytes, ignoring whitespace. fn parse_hex(s: &str) -> Result> { let compact: String = s.chars().filter(|c| !c.is_whitespace()).collect(); if compact.len() % 2 != 0 { return Err(anyhow!("hex string has an odd length")); } (0..compact.len()) .step_by(2) .map(|i| u8::from_str_radix(&compact[i..i + 2], 16).map_err(Into::into)) .collect() } /// Look up `--flag VALUE` in the rest. fn flag(args: &[String], name: &str) -> Option { let at = args.iter().position(|a| a == name)?; args.get(at + 1).cloned() } fn require(args: &[String], name: &str) -> Result { flag(args, name).ok_or_else(|| anyhow!("missing {name}")) }