From 8beb92b81ec8ccdc8852fe126e408e7108262830 Mon Sep 17 00:00:00 2001 From: claimore22 Date: Wed, 30 Sep 2026 11:01:22 -0400 Subject: [PATCH] load more prx --- .../ps5-analysis/src/reports/engine_hints.rs | 27 ++- crates/ps5-cli/src/cli.rs | 5 +- crates/ps5-cli/src/deps.rs | 18 +- crates/ps5-cli/src/load.rs | 33 ++-- crates/ps5-cli/src/run.rs | 64 ++++--- crates/ps5-cli/src/util.rs | 43 ++++- crates/ps5-signatures/src/engine.rs | 164 +++++++++++++++++- data/nid_search.html | 2 + data/nids.csv | 2 + 9 files changed, 278 insertions(+), 80 deletions(-) diff --git a/crates/ps5-analysis/src/reports/engine_hints.rs b/crates/ps5-analysis/src/reports/engine_hints.rs index 6762f2a..8031699 100644 --- a/crates/ps5-analysis/src/reports/engine_hints.rs +++ b/crates/ps5-analysis/src/reports/engine_hints.rs @@ -109,15 +109,7 @@ fn analyze_engine(name: &str, doc: &ps5_image::BinaryImageDocument) -> EngineHin let mut studio_engines: Vec = Vec::new(); let mut studio_detection: Option = None; if let Some(det) = ps5_signatures::engine::detect_engine(&signals) - && ![ - "Native", - "SCE", - "Unreal Engine 4", - "Unreal Engine 5", - "Unity", - "Godot", - ] - .contains(&det.value.as_str()) + && !["Native", "SCE"].contains(&det.value.as_str()) && det.confidence >= 10 { studio_engines.push(det.value.clone()); @@ -243,10 +235,14 @@ fn analyze_engine(name: &str, doc: &ps5_image::BinaryImageDocument) -> EngineHin fn has_ue4commandline(game_name: &str) -> bool { use std::sync::OnceLock; static CACHE: OnceLock> = OnceLock::new(); - let set = CACHE.get_or_init(|| { - let mut s = std::collections::HashSet::new(); - let roms = std::path::Path::new(r"C:\Users\claimoar\Documents\ROMS\PS5"); - let mut stack = vec![roms.to_path_buf()]; +let set = CACHE.get_or_init(|| { + let mut s = std::collections::HashSet::new(); + let base_paths = [ + std::path::Path::new(r"C:\Users\claimoar\Documents\ROMS\PS5"), + std::path::Path::new(r"D:\roms"), + ]; + for base in base_paths.iter() { + let mut stack = vec![base.to_path_buf()]; let mut depth = 0; while let Some(dir) = stack.pop() { if depth > 4 { @@ -282,8 +278,9 @@ fn has_ue4commandline(game_name: &str) -> bool { } depth += 1; } - s - }); + } + s +}); let sanitized = crate::scanner::sanitize_filename(game_name); set.contains(&sanitized) || set.contains(&game_name.to_string()) diff --git a/crates/ps5-cli/src/cli.rs b/crates/ps5-cli/src/cli.rs index 3d6f4d7..00d88d6 100644 --- a/crates/ps5-cli/src/cli.rs +++ b/crates/ps5-cli/src/cli.rs @@ -123,6 +123,8 @@ pub enum Commands { /// Load a PS5 binary into the virtual memory model for inspection Load { file: PathBuf, + /// Extra directory containing PRX modules (searched before the + /// auto-detected `sce_module/`, `Media/Modules/`, `prx/`) #[arg(long, value_hint = ValueHint::DirPath)] prx_dir: Option, #[arg(long)] @@ -131,7 +133,8 @@ pub enum Commands { /// Run a PS5 eboot in the host emulator and report execution details Run { file: PathBuf, - /// Directory containing PRX modules (default: `file parent/sce_module`) + /// Extra directory containing PRX modules (searched before the + /// auto-detected `sce_module/`, `Media/Modules/`, `prx/`) #[arg(long, value_hint = ValueHint::DirPath)] prx_dir: Option, /// Emit the execution report as JSON diff --git a/crates/ps5-cli/src/deps.rs b/crates/ps5-cli/src/deps.rs index 6e75b08..ab6987f 100644 --- a/crates/ps5-cli/src/deps.rs +++ b/crates/ps5-cli/src/deps.rs @@ -42,13 +42,17 @@ pub(crate) fn cmd_deps(path: &Path, format: OutputFormat, output: &Option, json: bool) { let data = load_file(path); - let dir = prx_dir.or_else(|| { - let default = path.parent().map(|p| p.join("sce_module")); - match default { - Some(ref d) if d.is_dir() => Some(d.clone()), - _ => None, - } - }); + let mut dirs: Vec = prx_dir.into_iter().collect(); + dirs.extend(crate::util::prx_candidate_dirs(path)); - if let Some(ref dir) = dir { - cmd_load_multi(path, &data, dir, json); - } else { + if dirs.is_empty() { cmd_load_single(path, &data, json); + } else { + cmd_load_multi(path, &data, &dirs, json); } } -/// Multi-module mode: load eboot + PRXs from `--prx-dir`. -fn cmd_load_multi(path: &Path, data: &[u8], prx_dir: &Path, json: bool) { +/// Multi-module mode: load eboot + PRXs from `--prx-dir` first, then +/// every auto-detected provider dir (`find_prx` keeps first-match +/// priority). +fn cmd_load_multi(path: &Path, data: &[u8], prx_dirs: &[PathBuf], json: bool) { let container = container_name(data); let elf_bytes = get_elf_bytes(data); @@ -278,12 +275,12 @@ fn cmd_load_multi(path: &Path, data: &[u8], prx_dir: &Path, json: bool) { ); println!(); - let prx_files = scan_prx_dir(prx_dir); - println!( - "PRX directory: {} ({} files)", - prx_dir.display(), - prx_files.len() - ); + let mut prx_files = Vec::new(); + for dir in prx_dirs { + let files = scan_prx_dir(dir); + println!("PRX directory: {} ({} files)", dir.display(), files.len()); + prx_files.extend(files); + } println!(); let file_name = path diff --git a/crates/ps5-cli/src/run.rs b/crates/ps5-cli/src/run.rs index 5830fc5..a92b2f8 100644 --- a/crates/ps5-cli/src/run.rs +++ b/crates/ps5-cli/src/run.rs @@ -5,17 +5,10 @@ use ps5_loader::OfflineExportTable; use crate::load::get_elf_bytes; use crate::util::load_file; -fn prx_dir_for(file: &Path) -> Option { - let parent = file.parent().unwrap_or(Path::new(".")); - let direct = parent.join("sce_module"); - if direct.is_dir() { - return Some(direct); - } - let prx = parent.join("prx"); - if prx.is_dir() { - return Some(prx); - } - None +fn prx_dirs_for(file: &Path, prx_dir: Option) -> Vec { + let mut dirs: Vec = prx_dir.into_iter().collect(); + dirs.extend(crate::util::prx_candidate_dirs(file)); + dirs } fn offline_table() -> Option { @@ -52,35 +45,36 @@ pub(crate) fn cmd_run(file: &PathBuf, prx_dir: Option, json: bool) { let elf_bytes = get_elf_bytes(&data); tracing::info!(size = elf_bytes.len(), "run: ELF extraction ok"); - let dir = prx_dir.or_else(|| prx_dir_for(file)); - tracing::info!(dir = ?dir, "run: PRX provider dir"); + let dirs = prx_dirs_for(file, prx_dir); + tracing::info!(dirs = ?dirs, "run: PRX provider dirs"); let provider = |name: &str| -> Option> { - let dir = dir.as_ref()?; - for candidate in [dir.join(name), dir.join(format!("{name}.prx"))] { - if candidate.is_file() { - match std::fs::read(&candidate) { - Ok(bytes) => { - let kind = if bytes.len() >= 4 && &bytes[0..4] == b"\x7fELF" { - "Raw ELF" - } else if bytes.len() >= 4 { - match u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) { - 0x5414F5EE => "PS5 SELF", - 0x4F153D1D => "PS4 SELF", - _ => "Unknown", - } - } else { - "Too small" - }; - tracing::info!(module = %name, path = %candidate.display(), kind, size = bytes.len(), "run: PRX provider hit"); - return Some(bytes); - } - Err(e) => { - tracing::warn!(module = %name, path = %candidate.display(), error = %e, "run: PRX read failed"); + for dir in &dirs { + for candidate in [dir.join(name), dir.join(format!("{name}.prx"))] { + if candidate.is_file() { + match std::fs::read(&candidate) { + Ok(bytes) => { + let kind = if bytes.len() >= 4 && &bytes[0..4] == b"\x7fELF" { + "Raw ELF" + } else if bytes.len() >= 4 { + match u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) { + 0x5414F5EE => "PS5 SELF", + 0x4F153D1D => "PS4 SELF", + _ => "Unknown", + } + } else { + "Too small" + }; + tracing::info!(module = %name, path = %candidate.display(), kind, size = bytes.len(), "run: PRX provider hit"); + return Some(bytes); + } + Err(e) => { + tracing::warn!(module = %name, path = %candidate.display(), error = %e, "run: PRX read failed"); + } } } } } - tracing::warn!(module = %name, "run: PRX not found in provider dir"); + tracing::warn!(module = %name, "run: PRX not found in provider dirs"); None }; diff --git a/crates/ps5-cli/src/util.rs b/crates/ps5-cli/src/util.rs index f6b6419..ded7841 100644 --- a/crates/ps5-cli/src/util.rs +++ b/crates/ps5-cli/src/util.rs @@ -1,5 +1,21 @@ use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +/// Candidate dirs for on-demand PRX loads, in search order: +/// `sce_module/`, then Unity-layout `Media/Modules/` (Il2Cpp game +/// assemblies ship there, not beside the eboot), then legacy `prx/`. +/// Only existing dirs are returned, so absent layouts change nothing. +pub(crate) fn prx_candidate_dirs(file: &Path) -> Vec { + let parent = file.parent().unwrap_or(Path::new(".")); + [ + parent.join("sce_module"), + parent.join("Media").join("Modules"), + parent.join("prx"), + ] + .into_iter() + .filter(|d| d.is_dir()) + .collect() +} pub(crate) fn load_file(path: &PathBuf) -> Vec { std::fs::read(path).unwrap_or_else(|e| { @@ -166,4 +182,29 @@ mod tests { let expected = std::path::PathBuf::from("C:\\Games\\PS5"); assert_eq!(sanitize_path_arg(input).unwrap(), expected); } + + #[test] + fn prx_candidate_dirs_prefers_sce_module_then_unity_layout() { + let root = std::env::temp_dir().join(format!( + "ps5rs-prx-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(root.join("sce_module")).unwrap(); + std::fs::create_dir_all(root.join("Media").join("Modules")).unwrap(); + assert_eq!( + prx_candidate_dirs(&root.join("eboot.bin")), + vec![root.join("sce_module"), root.join("Media").join("Modules")] + ); + // Absent layouts are skipped, never synthesized. + std::fs::remove_dir_all(root.join("Media")).unwrap(); + assert_eq!( + prx_candidate_dirs(&root.join("eboot.bin")), + vec![root.join("sce_module")] + ); + std::fs::remove_dir_all(&root).ok(); + } } diff --git a/crates/ps5-signatures/src/engine.rs b/crates/ps5-signatures/src/engine.rs index ed3500a..bc71db7 100644 --- a/crates/ps5-signatures/src/engine.rs +++ b/crates/ps5-signatures/src/engine.rs @@ -78,10 +78,141 @@ pub const UNITY: EngineFingerprint = EngineFingerprint { pub const GODOT: EngineFingerprint = EngineFingerprint { name: "Godot", - patterns: &[("Godot Engine", 10), ("GDNative", 8)], + patterns: &[ + ("project.godot", 100), + (".godot", 60), + ("Godot Engine", 100), + ("GDNative", 60), + ], }; -pub const ALL: &[EngineFingerprint] = &[UNREAL4, UNREAL5, UNITY, GODOT]; +pub const RE_ENGINE: EngineFingerprint = EngineFingerprint { + name: "RE Engine", + patterns: &[ + ("RE Engine", 100), + (".pak", 25), + ], +}; +pub const FROSTBITE: EngineFingerprint = EngineFingerprint { + name: "Frostbite", + patterns: &[ + ("Frostbite", 100), + (".toc", 50), + ], +}; +pub const DECIMA: EngineFingerprint = EngineFingerprint { + name: "Decima", + patterns: &[ + ("Decima", 100), + (".core", 50), + ], +}; +pub const DRAGON_ENGINE: EngineFingerprint = EngineFingerprint { + name: "Dragon Engine", + patterns: &[ + ("Dragon Engine", 100), + (".par", 50), + ], +}; +pub const REDEngine: EngineFingerprint = EngineFingerprint { + name: "REDEngine", + patterns: &[ + ("REDEngine", 100), + (".archive", 50), + ], +}; +pub const CREATION_ENGINE_2: EngineFingerprint = EngineFingerprint { + name: "Creation Engine 2", + patterns: &[ + ("Creation Engine 2", 100), + (".ba2", 60), + ], +}; +pub const RAGE_ENGINE: EngineFingerprint = EngineFingerprint { + name: "RAGE Engine", + patterns: &[ + ("RAGE Engine", 100), + (".rpf", 100), + ], +}; +pub const CRYENGINE: EngineFingerprint = EngineFingerprint { + name: "CryEngine", + patterns: &[ + ("CryEngine", 100), + (".cgf", 50), + ], +}; +pub const SOURCE_ENGINE: EngineFingerprint = EngineFingerprint { + name: "Source Engine", + patterns: &[ + ("Source Engine", 100), + (".vpk", 100), + ], +}; +pub const LITHTECH: EngineFingerprint = EngineFingerprint { + name: "LithTech", + patterns: &[ + ("LithTech", 100), + (".rez", 100), + ], +}; +pub const HAVOK_VISION_ENGINE: EngineFingerprint = EngineFingerprint { + name: "Havok Vision Engine", + patterns: &[ + ("Havok Vision Engine", 100), + (".vmesh", 90) + ], +}; +pub const GAMEBRYO: EngineFingerprint = EngineFingerprint { + name: "Gamebryo", + patterns: &[ + ("Gamebryo", 100), + (".nif", 100) + ], +}; +pub const SNOWDROP: EngineFingerprint = EngineFingerprint { + name: "Snowdrop", + patterns: &[ + ("Snowdrop", 100), + (".forge", 100) + ], +}; +pub const ANVILNEXT: EngineFingerprint = EngineFingerprint { + name: "AnvilNext", + patterns: &[ + ("AnvilNext", 100), + ], +}; +pub const ID_TECH: EngineFingerprint = EngineFingerprint { + name: "id Tech", + patterns: &[ + ("id Tech", 100), + (".pk3", 100), + (".pk4", 100), + ], +}; + +pub const ALL: &[EngineFingerprint] = &[ + UNREAL4, + UNREAL5, + UNITY, + GODOT, + RE_ENGINE, + FROSTBITE, + DECIMA, + DRAGON_ENGINE, + REDEngine, + CREATION_ENGINE_2, + RAGE_ENGINE, + CRYENGINE, + SOURCE_ENGINE, + LITHTECH, + HAVOK_VISION_ENGINE, + GAMEBRYO, + SNOWDROP, + ANVILNEXT, + ID_TECH, +]; pub fn detect_engine(strings: &[String]) -> Option { let mut best: Option = None; @@ -275,10 +406,37 @@ mod tests { .any(|d| d.value.contains("HK_Project_Delivery")) ); } - #[test] fn detect_custom_forks_empty() { let strings = vec!["hello world".to_string()]; assert!(detect_custom_forks(&strings).is_empty()); } + + #[test] + fn detect_engine_new_engines() { + let engines = [ + "RE Engine", + "Frostbite", + "Decima", + "Dragon Engine", + "REDEngine", + "Creation Engine 2", + "RAGE Engine", + "CryEngine", + "Source Engine", + "LithTech", + "Havok Vision Engine", + "Gamebryo", + "Snowdrop", + "AnvilNext", + "id Tech", + ]; + for name in engines.iter() { + let strings = vec![name.to_string()]; + let det = detect_engine(&strings).expect("should detect engine"); + assert_eq!(det.value, *name); + assert_eq!(det.confidence, 100); + } + } } + diff --git a/data/nid_search.html b/data/nid_search.html index 440a9b4..bcc934e 100644 --- a/data/nid_search.html +++ b/data/nid_search.html @@ -195701,6 +195701,8 @@

NID Search

DVJKlbmOEn4,0x0D524A95B98E127E,usearch_open_67,libicu,0,DNNDHH-PS5/libicu AAAACIAAAAA,0x0000000880000000,GuestExecutableAddressCeiling,,0,sharpemu/KernelRuntimeCompatExports EP2L8t3j-aM,0x10FD8BF2DDE3FDA3,VideoOutVrrStatus_kP2L8t3j_aM,,0,KytyPS5/libVideoOut +l76jptHfizM,0x97BEA3A6D1DF8B33,Func_97BEA3A6D1DF8B33,,0,shadPS4/keyboard +F76jptHfizM,0x17BEA3A6D1DF8B33,libSceKeyboard,libSceKeyboard,0,shadPS4/keyboard