use crate::memory::{self, MemorySource};
use anyhow::{Context, Result};
use byteorder::ByteOrder;
pub fn handle_discover(source: &dyn MemorySource, target: &str) -> Result<()> {
match target.to_lowercase().as_str() {
"gnames" | "all" => {
println!("Searching for GNames pool...");
match memory::discover_gnames(source) {
Ok(gnames) => {
println!("GNames found at: {:#x}", gnames.address);
println!("\nSample names:");
for (idx, name) in &gnames.sample_names {
println!(" [{}] {}", idx, name);
}
if target == "all" {
println!("\nSearching for GUObjectArray...");
match memory::discover_guobject_array(source, gnames.address) {
Ok(arr) => {
println!("GUObjectArray found at: {:#x}", arr.address);
println!(" Objects ptr: {:#x}", arr.objects_ptr);
println!(" NumElements: {}", arr.num_elements);
println!(" MaxElements: {}", arr.max_elements);
}
Err(e) => {
eprintln!("GUObjectArray not found: {}", e);
}
}
}
}
Err(e) => {
eprintln!("GNames not found: {}", e);
}
}
}
"guobjectarray" => {
println!("Searching for GNames pool first...");
match memory::discover_gnames(source) {
Ok(gnames) => {
println!("GNames at: {:#x}", gnames.address);
println!("\nSearching for GUObjectArray...");
match memory::discover_guobject_array(source, gnames.address) {
Ok(arr) => {
println!("GUObjectArray found at: {:#x}", arr.address);
println!(" Objects ptr: {:#x}", arr.objects_ptr);
println!(" NumElements: {}", arr.num_elements);
println!(" MaxElements: {}", arr.max_elements);
}
Err(e) => {
eprintln!("GUObjectArray not found: {}", e);
}
}
}
Err(e) => {
eprintln!("GNames not found (required for GUObjectArray): {}", e);
}
}
}
"classuclass" => {
println!("Searching for Class UClass (self-referential)...");
match memory::discover_class_uclass(source) {
Ok(addr) => {
println!("Class UClass found at: {:#x}", addr);
println!("\nUObject structure dump:");
for offset in (0..0x40usize).step_by(8) {
if let Ok(val) = source.read_u64(addr + offset) {
println!(" +{:#04x}: {:#018x}", offset, val);
}
}
}
Err(e) => {
eprintln!("Class UClass not found: {}", e);
}
}
}
_ => {
eprintln!(
"Unknown target: {}. Use 'gnames', 'guobjectarray', 'classuclass', or 'all'",
target
);
}
}
Ok(())
}
pub fn handle_objects(source: &dyn MemorySource, class: Option<&str>, limit: usize) -> Result<()> {
let gnames = memory::discover_gnames(source).context("Failed to find GNames pool")?;
println!("GNames at: {:#x}", gnames.address);
if let Some(class_name) = class {
println!("Searching for '{}' in FName pool...", class_name);
let pattern = class_name.as_bytes();
let results = memory::scan_pattern(source, pattern, &vec![1u8; pattern.len()])?;
println!(
"Found {} occurrences of '{}':",
results.len().min(limit),
class_name
);
for (i, addr) in results.iter().take(limit).enumerate() {
println!(" {}: {:#x}", i + 1, addr);
if let Ok(context) = source.read_bytes(addr.saturating_sub(16), 64) {
print!(" ");
for byte in &context[..32.min(context.len())] {
print!("{:02x} ", byte);
}
println!();
print!(" ");
for byte in &context[..32.min(context.len())] {
let c = *byte as char;
if c.is_ascii_graphic() || c == ' ' {
print!("{}", c);
} else {
print!(".");
}
}
println!();
}
}
if results.len() > limit {
println!("... and {} more", results.len() - limit);
}
} else {
println!("No class filter specified. Showing FName pool sample:");
for (idx, name) in &gnames.sample_names {
println!(" [{}] {}", idx, name);
}
println!("\nUse --class <name> to search for specific classes");
println!("Example: bl4 inject objects --class RarityWeightData");
}
Ok(())
}
pub fn handle_find_class_uclass(source: &dyn MemorySource) -> Result<()> {
let _gnames = memory::discover_gnames(source).context("Failed to find GNames pool")?;
let pool = memory::FNamePool::discover(source).context("Failed to discover FNamePool")?;
let mut fname_reader = memory::FNameReader::new(pool);
let code_bounds = memory::find_code_bounds(source)?;
println!("Searching for Class UClass...");
println!(" Code bounds: {} ranges", code_bounds.ranges.len());
let offset_combos: &[(usize, usize, &str)] = &[
(0x18, 0x30, "BL4 (0x18/0x30)"),
(0x10, 0x18, "Standard UE5"),
(0x10, 0x30, "Mixed A"),
(0x20, 0x38, "Offset +8"),
];
for &(class_off, name_off, desc) in offset_combos {
println!(
"\nTrying {} - ClassPrivate={:#x}, NamePrivate={:#x}...",
desc, class_off, name_off
);
let mut found_self_refs: Vec<(usize, usize, u32, String)> = Vec::new();
let mut found_class = false;
let header_size = name_off + 8;
for region in source.regions() {
if !region.is_readable() {
continue;
}
let in_pe = region.start >= 0x140000000 && region.start <= 0x175000000;
let in_heap = region.start >= 0x1000000 && region.start < 0x140000000;
if !in_pe && !in_heap {
continue;
}
if region.size() > 100 * 1024 * 1024 {
continue;
}
let data = match source.read_bytes(region.start, region.size()) {
Ok(d) => d,
Err(_) => continue,
};
for offset in (0..data.len().saturating_sub(header_size)).step_by(8) {
let obj_addr = region.start + offset;
let vtable_ptr = byteorder::LE::read_u64(&data[offset..offset + 8]) as usize;
if !(0x140000000..=0x175000000).contains(&vtable_ptr) {
continue;
}
let first_func = match source.read_bytes(vtable_ptr, 8) {
Ok(vt) => byteorder::LE::read_u64(&vt) as usize,
Err(_) => continue,
};
if !code_bounds.contains(first_func) {
continue;
}
let class_ptr =
byteorder::LE::read_u64(&data[offset + class_off..offset + class_off + 8])
as usize;
if class_ptr != obj_addr {
continue;
}
let fname_idx =
byteorder::LE::read_u32(&data[offset + name_off..offset + name_off + 4]);
let name = fname_reader
.read_name(source, fname_idx)
.unwrap_or_else(|_| format!("<idx:{}>", fname_idx));
found_self_refs.push((obj_addr, vtable_ptr, fname_idx, name.clone()));
if fname_idx == memory::FNAME_CLASS_INDEX || name == "Class" {
println!("\n*** FOUND Class UClass at {:#x} ***", obj_addr);
println!(" VTable: {:#x}, vtable[0]: {:#x}", vtable_ptr, first_func);
println!(" FName index: {} = \"{}\"", fname_idx, name);
found_class = true;
}
}
}
println!(
" Found {} self-referential objects:",
found_self_refs.len()
);
for (addr, vtable, fname_idx, name) in found_self_refs.iter().take(10) {
let marker = if *fname_idx == memory::FNAME_CLASS_INDEX {
" <-- CLASS!"
} else {
""
};
println!(
" {:#x}: vtable={:#x}, fname={} \"{}\"{}",
addr, vtable, fname_idx, name, marker
);
}
if found_class {
println!("\n=== SUCCESS with {} offsets! ===", desc);
break;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::source::tests::MockMemorySource;
#[test]
fn test_handle_discover_unknown_target() {
let source = MockMemorySource::new(vec![], 0x1000);
let result = handle_discover(&source, "unknown_target");
assert!(result.is_ok());
}
#[test]
fn test_handle_objects_no_class_filter() {
let source = MockMemorySource::new(vec![], 0x1000);
let result = handle_objects(&source, None, 10);
assert!(result.is_err());
}
#[test]
fn test_handle_find_class_uclass_empty_source() {
let source = MockMemorySource::new(vec![], 0x1000);
let result = handle_find_class_uclass(&source);
assert!(result.is_err());
}
}