Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions crates/ps5-analysis/src/reports/engine_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,7 @@ fn analyze_engine(name: &str, doc: &ps5_image::BinaryImageDocument) -> EngineHin
let mut studio_engines: Vec<String> = Vec::new();
let mut studio_detection: Option<ps5_image::Detection> = 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());
Expand Down Expand Up @@ -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<std::collections::HashSet<String>> = 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 {
Expand Down Expand Up @@ -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())
Expand Down
5 changes: 4 additions & 1 deletion crates/ps5-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
#[arg(long)]
Expand All @@ -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<PathBuf>,
/// Emit the execution report as JSON
Expand Down
18 changes: 11 additions & 7 deletions crates/ps5-cli/src/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,17 @@ pub(crate) fn cmd_deps(path: &Path, format: OutputFormat, output: &Option<std::p
.unwrap_or("game")
.to_string();
g.add_node(&name, &[]);
// Add dummy edges for demonstration (scan prx)
if let Ok(prx_entries) = std::fs::read_dir(p.join("sce_module")) {
for prx in prx_entries.flatten() {
if let Some(fname) = prx.path().file_name().and_then(|n| n.to_str())
&& fname.ends_with(".prx")
{
g.add_edge(&name, fname);
// Add dummy edges for demonstration (scan prx);
// same provider dirs as `load`/`run`.
for dir in crate::util::prx_candidate_dirs(&p.join("eboot.bin")) {
if let Ok(prx_entries) = std::fs::read_dir(dir) {
for prx in prx_entries.flatten() {
if let Some(fname) =
prx.path().file_name().and_then(|n| n.to_str())
&& fname.ends_with(".prx")
{
g.add_edge(&name, fname);
}
}
}
}
Expand Down
33 changes: 15 additions & 18 deletions crates/ps5-cli/src/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,23 +245,20 @@ fn print_modules(ctx: &ps5_loader::ModuleContext) {
pub(crate) fn cmd_load(path: &PathBuf, prx_dir: Option<PathBuf>, 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<PathBuf> = 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);

Expand All @@ -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
Expand Down
64 changes: 29 additions & 35 deletions crates/ps5-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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<PathBuf>) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = prx_dir.into_iter().collect();
dirs.extend(crate::util::prx_candidate_dirs(file));
dirs
}

fn offline_table() -> Option<OfflineExportTable> {
Expand Down Expand Up @@ -52,35 +45,36 @@ pub(crate) fn cmd_run(file: &PathBuf, prx_dir: Option<PathBuf>, 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<Vec<u8>> {
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
};

Expand Down
43 changes: 42 additions & 1 deletion crates/ps5-cli/src/util.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> {
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<u8> {
std::fs::read(path).unwrap_or_else(|e| {
Expand Down Expand Up @@ -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();
}
}
Loading
Loading