use std::fmt;
use std::io::{Read, Write};
use anyhow::Result;
use crate::resource::prp::PlasmaRead;
use super::load_mask::LoadMask;
use super::location::Location;
const HAS_CLONE_IDS: u8 = 0x1;
const HAS_LOAD_MASK: u8 = 0x2;
#[derive(Clone)]
pub struct Uoid {
pub location: Location,
pub class_type: u16,
pub object_name: String,
pub object_id: u32,
pub load_mask: LoadMask,
pub clone_id: u16,
pub clone_player_id: u32,
}
impl Uoid {
pub fn invalid() -> Self {
Self {
location: Location::invalid(),
class_type: 0,
object_name: String::new(),
object_id: 0,
load_mask: LoadMask::ALWAYS,
clone_id: 0,
clone_player_id: 0,
}
}
pub fn new(location: Location, class_type: u16, object_name: String) -> Self {
Self {
location,
class_type,
object_name,
object_id: 0,
load_mask: LoadMask::ALWAYS,
clone_id: 0,
clone_player_id: 0,
}
}
pub fn is_valid(&self) -> bool {
self.location.is_valid() && !self.object_name.is_empty()
}
pub fn is_clone(&self) -> bool {
self.clone_id != 0
}
pub fn read(reader: &mut impl Read) -> Result<Self> {
let contents = reader.read_u8()?;
let location = Location::read(reader)?;
let load_mask = if contents & HAS_LOAD_MASK != 0 {
LoadMask::read(reader)?
} else {
LoadMask::ALWAYS
};
let class_type = reader.read_u16()?;
let object_id = reader.read_u32()?;
let object_name = reader.read_safe_string()?;
let (clone_id, clone_player_id) = if contents & HAS_CLONE_IDS != 0 {
let clone_id = reader.read_u16()?;
let _reserved = reader.read_u16()?;
let clone_player_id = reader.read_u32()?;
(clone_id, clone_player_id)
} else {
(0, 0)
};
Ok(Self {
location,
class_type,
object_name,
object_id,
load_mask,
clone_id,
clone_player_id,
})
}
pub fn write(&self, writer: &mut impl Write) -> Result<()> {
let mut contents: u8 = 0;
if self.is_clone() {
contents |= HAS_CLONE_IDS;
}
if self.load_mask.is_used() {
contents |= HAS_LOAD_MASK;
}
writer.write_all(&[contents])?;
self.location.write(writer)?;
if contents & HAS_LOAD_MASK != 0 {
self.load_mask.write(writer)?;
}
writer.write_all(&self.class_type.to_le_bytes())?;
writer.write_all(&self.object_id.to_le_bytes())?;
write_safe_string(writer, &self.object_name)?;
if contents & HAS_CLONE_IDS != 0 {
writer.write_all(&self.clone_id.to_le_bytes())?;
writer.write_all(&0u16.to_le_bytes())?; writer.write_all(&self.clone_player_id.to_le_bytes())?;
}
Ok(())
}
}
impl PartialEq for Uoid {
fn eq(&self, other: &Self) -> bool {
self.location == other.location
&& self.load_mask == other.load_mask
&& self.class_type == other.class_type
&& self.object_name == other.object_name
&& self.object_id == other.object_id
&& self.clone_id == other.clone_id
&& self.clone_player_id == other.clone_player_id
}
}
impl Eq for Uoid {}
impl std::hash::Hash for Uoid {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.location.hash(state);
self.load_mask.hash(state);
self.class_type.hash(state);
self.object_name.hash(state);
self.object_id.hash(state);
self.clone_id.hash(state);
self.clone_player_id.hash(state);
}
}
impl Default for Uoid {
fn default() -> Self {
Self::invalid()
}
}
impl fmt::Debug for Uoid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Uoid(S:{:#x} F:{:#x} T:{:#06x} I:{} N:{:?} C:[{},{}])",
self.location.sequence_number,
self.location.flags,
self.class_type,
self.object_id,
self.object_name,
self.clone_player_id,
self.clone_id,
)
}
}
impl fmt::Display for Uoid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{:#06x}]{}", self.class_type, self.object_name)
}
}
pub fn read_key_uoid(reader: &mut impl Read) -> Result<Option<Uoid>> {
let non_nil = reader.read_u8()?;
if non_nil == 0 {
return Ok(None);
}
Ok(Some(Uoid::read(reader)?))
}
pub fn write_key_uoid(writer: &mut impl Write, uoid: Option<&Uoid>) -> Result<()> {
match uoid {
Some(uoid) => {
writer.write_all(&[1u8])?;
uoid.write(writer)?;
}
None => {
writer.write_all(&[0u8])?;
}
}
Ok(())
}
fn write_safe_string(writer: &mut impl Write, s: &str) -> Result<()> {
let bytes = s.as_bytes();
let len = bytes.len() + 1; let raw_len = (len as u16) | 0xF000; writer.write_all(&raw_len.to_le_bytes())?;
let mut inverted: Vec<u8> = bytes.iter().map(|b| !b).collect();
inverted.push(!0u8); writer.write_all(&inverted)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_invalid() {
let u = Uoid::invalid();
assert!(!u.is_valid());
assert!(!u.is_clone());
}
#[test]
fn test_new() {
let loc = Location::new(100, 0);
let u = Uoid::new(loc, 0x004C, "TestObject".into());
assert!(u.is_valid());
assert!(!u.is_clone());
}
#[test]
fn test_round_trip_simple() {
let loc = Location::new(0x1234, 0);
let u = Uoid::new(loc, 0x004C, "TestDrawable".into());
let mut buf = Vec::new();
u.write(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let u2 = Uoid::read(&mut cursor).unwrap();
assert_eq!(u, u2);
assert_eq!(u2.object_name, "TestDrawable");
assert_eq!(u2.class_type, 0x004C);
}
#[test]
fn test_round_trip_with_load_mask() {
let mut u = Uoid::new(Location::new(100, 0), 0x0004, "TextureLOD".into());
u.load_mask = LoadMask::new(0xF3, 0xF1);
let mut buf = Vec::new();
u.write(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let u2 = Uoid::read(&mut cursor).unwrap();
assert_eq!(u, u2);
assert!(u2.load_mask.is_used());
}
#[test]
fn test_round_trip_with_clone() {
let mut u = Uoid::new(Location::new(100, 0), 0x0001, "Avatar".into());
u.clone_id = 5;
u.clone_player_id = 12345;
let mut buf = Vec::new();
u.write(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let u2 = Uoid::read(&mut cursor).unwrap();
assert_eq!(u, u2);
assert!(u2.is_clone());
assert_eq!(u2.clone_id, 5);
assert_eq!(u2.clone_player_id, 12345);
}
#[test]
fn test_key_ref_round_trip() {
let u = Uoid::new(Location::new(42, 0), 0x0007, "Material01".into());
let mut buf = Vec::new();
write_key_uoid(&mut buf, Some(&u)).unwrap();
let mut cursor = Cursor::new(&buf);
let u2 = read_key_uoid(&mut cursor).unwrap();
assert!(u2.is_some());
assert_eq!(u, u2.unwrap());
}
#[test]
fn test_null_key_ref() {
let mut buf = Vec::new();
write_key_uoid(&mut buf, None).unwrap();
assert_eq!(buf.len(), 1);
assert_eq!(buf[0], 0);
let mut cursor = Cursor::new(&buf);
let u = read_key_uoid(&mut cursor).unwrap();
assert!(u.is_none());
}
#[test]
fn test_parse_prp_keys_match() {
use crate::resource::prp::PrpPage;
use std::path::Path;
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() {
eprintln!("Skipping PRP test: {:?} not found", path);
return;
}
let page = PrpPage::from_file(path).unwrap();
assert!(!page.keys.is_empty(), "Expected keys in Cleft page");
let mut valid_count = 0;
for key in &page.keys {
if key.class_type > crate::core::class_index::ClassIndex::MAX_CLASS_INDEX {
continue;
}
assert!(key.location_sequence != 0xFFFFFFFF,
"Invalid location for key {}", key.object_name);
assert!(!key.object_name.is_empty(),
"Empty name for key with class 0x{:04X}", key.class_type);
valid_count += 1;
}
eprintln!("Verified {}/{} keys from Cleft_District_Cleft.prp", valid_count, page.keys.len());
}
#[test]
fn test_parse_real_key_ref() {
use crate::resource::prp::{PrpPage, class_types};
use std::path::Path;
let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
if !path.exists() {
eprintln!("Skipping PRP test: {:?} not found", path);
return;
}
let page = PrpPage::from_file(path).unwrap();
let drawable_keys: Vec<_> = page.keys_of_type(class_types::PL_DRAWABLE_SPANS);
assert!(!drawable_keys.is_empty(), "Expected drawable spans in Cleft");
for dkey in &drawable_keys {
if let Some(data) = page.object_data(dkey) {
let mut cursor = Cursor::new(data);
let _class_idx = cursor.read_u16().unwrap();
let uoid = read_key_uoid(&mut cursor).unwrap();
assert!(uoid.is_some(), "Self-key should be non-nil for {}", dkey.object_name);
let uoid = uoid.unwrap();
assert_eq!(uoid.object_name, dkey.object_name,
"Uoid name should match ObjectKey name");
assert_eq!(uoid.class_type, dkey.class_type,
"Uoid class type should match ObjectKey class type");
assert_eq!(uoid.location.sequence_number, dkey.location_sequence,
"Uoid location should match ObjectKey location");
assert_eq!(uoid.location.flags, dkey.location_flags,
"Uoid location flags should match ObjectKey location flags");
assert_eq!(uoid.object_id, dkey.object_id,
"Uoid object_id should match ObjectKey object_id");
}
}
eprintln!("Verified {} drawable key refs from Cleft", drawable_keys.len());
}
}