use crate::memory::{self, MemorySource};
use anyhow::{Context, Result};
use byteorder::ByteOrder;
use std::collections::HashMap;
pub fn handle_list_uclasses(
source: &dyn MemorySource,
limit: usize,
filter: Option<&str>,
) -> 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);
println!(
"Finding all UClass instances (ClassPrivate == {:#x})...\n",
memory::UCLASS_METACLASS_ADDR
);
let classes = memory::find_all_uclasses(source, &mut fname_reader)
.context("Failed to enumerate UClass instances")?;
let filtered: Vec<_> = if let Some(pattern) = filter {
let pattern_lower = pattern.to_lowercase();
classes
.iter()
.filter(|c| c.name.to_lowercase().contains(&pattern_lower))
.collect()
} else {
classes.iter().collect()
};
println!(
"Found {} UClass instances{}\n",
filtered.len(),
filter
.map(|f| format!(" matching '{}'", f))
.unwrap_or_default()
);
let show_count = if limit == 0 {
filtered.len()
} else {
limit.min(filtered.len())
};
for class in filtered.iter().take(show_count) {
println!(
" {:#x}: {} (FName {})",
class.address, class.name, class.name_index
);
}
if show_count < filtered.len() {
println!(
"\n ... and {} more (use --limit 0 to show all)",
filtered.len() - show_count
);
}
let game_classes: Vec<_> = filtered
.iter()
.filter(|c| c.name.starts_with("U") || c.name.starts_with("A") || c.name.contains("_"))
.collect();
let core_classes: Vec<_> = filtered
.iter()
.filter(|c| !c.name.starts_with("U") && !c.name.starts_with("A") && !c.name.contains("_"))
.collect();
println!("\nClass categories:");
println!(" Game classes (U*/A*/*_*): {}", game_classes.len());
println!(" Core/Native classes: {}", core_classes.len());
Ok(())
}
pub fn handle_list_objects(
source: &dyn MemorySource,
limit: usize,
class_filter: Option<&str>,
name_filter: Option<&str>,
stats: bool,
) -> Result<()> {
eprintln!("Searching for GNames pool...");
let gnames = memory::discover_gnames(source).context("Failed to discover GNames")?;
eprintln!("GNames found at: {:#x}\n", gnames.address);
eprintln!("Searching for GUObjectArray...");
let guobj = memory::discover_guobject_array(source, gnames.address)
.context("Failed to discover GUObjectArray")?;
let pool = memory::FNamePool::discover(source).context("Failed to discover FNamePool")?;
let mut fname_reader = memory::FNameReader::new(pool);
println!("Enumerating UObjects from GUObjectArray...");
println!(" Total objects: {}", guobj.num_elements);
println!(" Item size: {} bytes\n", guobj.item_size);
let mut total_valid = 0usize;
let mut class_counts: HashMap<String, usize> = HashMap::new();
let mut shown = 0usize;
let class_filter_lower = class_filter.map(|s| s.to_lowercase());
let name_filter_lower = name_filter.map(|s| s.to_lowercase());
for (idx, obj_ptr) in guobj.iter_objects(source) {
let obj_data = match source.read_bytes(obj_ptr, memory::UOBJECT_HEADER_SIZE) {
Ok(d) => d,
Err(_) => continue,
};
let class_ptr = byteorder::LE::read_u64(
&obj_data[memory::UOBJECT_CLASS_OFFSET..memory::UOBJECT_CLASS_OFFSET + 8],
) as usize;
let name_idx = byteorder::LE::read_u32(
&obj_data[memory::UOBJECT_NAME_OFFSET..memory::UOBJECT_NAME_OFFSET + 4],
);
let obj_name = fname_reader
.read_name(source, name_idx)
.unwrap_or_else(|_| format!("FName_{}", name_idx));
let class_name = if class_ptr != 0 {
if let Ok(class_data) = source.read_bytes(class_ptr, memory::UOBJECT_HEADER_SIZE) {
let class_name_idx = byteorder::LE::read_u32(
&class_data[memory::UOBJECT_NAME_OFFSET..memory::UOBJECT_NAME_OFFSET + 4],
);
fname_reader
.read_name(source, class_name_idx)
.unwrap_or_else(|_| format!("FName_{}", class_name_idx))
} else {
"Unknown".to_string()
}
} else {
"Null".to_string()
};
total_valid += 1;
*class_counts.entry(class_name.clone()).or_insert(0) += 1;
let class_match = class_filter_lower
.as_ref()
.map(|f| class_name.to_lowercase().contains(f))
.unwrap_or(true);
let name_match = name_filter_lower
.as_ref()
.map(|f| obj_name.to_lowercase().contains(f))
.unwrap_or(true);
if class_match && name_match && !stats && shown < limit {
println!("[{}] {:#x}: {} ({})", idx, obj_ptr, obj_name, class_name);
shown += 1;
}
if total_valid.is_multiple_of(50000) {
eprint!("\r Scanned {} objects...", total_valid);
}
}
eprintln!("\r Scanned {} valid objects total.", total_valid);
if stats || class_filter.is_some() || name_filter.is_some() {
println!("\nStatistics:");
println!(" Total valid objects: {}", total_valid);
println!(" Unique classes: {}", class_counts.len());
let mut sorted_classes: Vec<_> = class_counts.into_iter().collect();
sorted_classes.sort_by(|a, b| b.1.cmp(&a.1));
println!("\nTop 20 classes by instance count:");
for (class_name, count) in sorted_classes.iter().take(20) {
println!(" {:6} {}", count, class_name);
}
}
if !stats && shown >= limit && limit > 0 {
println!(
"\n... showing first {} matches (use --limit N to see more)",
limit
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::source::tests::MockMemorySource;
#[test]
fn test_handle_list_uclasses_empty_source() {
let source = MockMemorySource::new(vec![], 0x1000);
let result = handle_list_uclasses(&source, 10, None);
assert!(result.is_err());
}
#[test]
fn test_handle_list_objects_empty_source() {
let source = MockMemorySource::new(vec![], 0x1000);
let result = handle_list_objects(&source, 10, None, None, false);
assert!(result.is_err());
}
}