plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! Camera brains — third-person follow, fixed, first-person, circle, drive.
//!
//! C++ ref: pfCamera/plCameraBrain.h/.cpp
//!
//! Inheritance hierarchy:
//!   plCameraBrain1 (0x0099) — base
//!   ├── plCameraBrain1_Avatar (0x009E) — third-person follow
//!   │   └── plCameraBrain1_FirstPerson (0x00B3) — head cam
//!   ├── plCameraBrain1_Fixed (0x009F) — locked to position
//!   │   └── plCameraBrain1_Circle (0x00C2) — orbits a point
//!   └── plCameraBrain1_Drive (0x009C) — WASD free-fly (built-in, no PRP data)

use std::io::Read;

use anyhow::Result;

use crate::core::uoid::{Uoid, read_key_uoid};
use crate::resource::prp::PlasmaRead;

// ─── Brain flags (plCameraBrain.h:60-88) ───────────────────────────────────

#[allow(dead_code)]
pub mod brain_flags {
    pub const CUT_POS: u32                  = 1 << 0;
    pub const CUT_POS_ONCE: u32             = 1 << 1;
    pub const CUT_POA: u32                  = 1 << 2;
    pub const CUT_POA_ONCE: u32             = 1 << 3;
    pub const ANIMATE_FOV: u32              = 1 << 4;
    pub const FOLLOW_LOCAL_AVATAR: u32      = 1 << 5;
    pub const PANIC_VELOCITY: u32           = 1 << 6;
    pub const RAIL_COMPONENT: u32           = 1 << 7;
    pub const SUBJECT: u32                  = 1 << 8;
    pub const CIRCLE_TARGET: u32            = 1 << 9;
    pub const MAINTAIN_LOS: u32             = 1 << 10;
    pub const ZOOM_ENABLED: u32             = 1 << 11;
    pub const IS_TRANSITION_CAMERA: u32     = 1 << 12;
    pub const WORLDSPACE_POA: u32           = 1 << 13;
    pub const WORLDSPACE_POS: u32           = 1 << 14;
    pub const CUT_POS_WHILE_PAN: u32        = 1 << 15;
    pub const CUT_POA_WHILE_PAN: u32        = 1 << 16;
    pub const NON_PHYS: u32                 = 1 << 17;
    pub const NEVER_ANIMATE_FOV: u32        = 1 << 18;
    pub const IGNORE_SUBWORLD_MOVEMENT: u32 = 1 << 19;
    pub const FALLING: u32                  = 1 << 20;
    pub const RUNNING: u32                  = 1 << 21;
    pub const VERTICAL_WHEN_FALLING: u32    = 1 << 22;
    pub const SPEED_UP_WHEN_RUNNING: u32    = 1 << 23;
    pub const FALLING_STOPPED: u32          = 1 << 24;
    pub const BEGIN_FALLING: u32            = 1 << 25;
}

// ─── Circle camera flags (plCameraBrain.h:354-363) ─────────────────────────

#[allow(dead_code)]
pub mod circle_flags {
    pub const LAGGED: u32              = 0x01;
    pub const ABSOLUTE_LAG: u32        = 0x03; // kAbsoluteLag = kLagged | 0x02
    pub const FARTHEST: u32            = 0x04;
    pub const TARGETTED: u32           = 0x08;
    pub const HAS_CENTER_OBJECT: u32   = 0x10;
    pub const POA_OBJECT: u32          = 0x20;
    pub const CIRCLE_LOCAL_AVATAR: u32 = 0x40;
}

// ─── Camera acceleration ───────────────────────────────────────────────────

/// Camera brain acceleration parameters.
#[derive(Debug, Clone, Copy)]
pub struct CameraAccel {
    pub accel: f32,
    pub decel: f32,
    pub max_vel: f32,
}

impl Default for CameraAccel {
    fn default() -> Self {
        // Plasma defaults from plCameraBrain1 constructor (plCameraBrain.cpp:98-114)
        Self {
            accel: 30.0,
            decel: 30.0,
            max_vel: 30.0,
        }
    }
}

// ─── Base brain fields ─────────────────────────────────────────────────────

/// Shared fields from plCameraBrain1::Read (plCameraBrain.cpp:482-513).
#[derive(Debug, Clone)]
pub struct BrainBase {
    pub self_key: Option<Uoid>,
    pub poa_offset: [f32; 3],
    pub subject_key: Option<Uoid>,
    pub rail_key: Option<Uoid>,
    pub flags: u32,
    pub accel: CameraAccel,
    pub poa_accel: CameraAccel,
    pub x_pan_limit: f32,
    pub z_pan_limit: f32,
    pub zoom_rate: f32,
    pub zoom_min: f32,
    pub zoom_max: f32,
}

impl BrainBase {
    pub fn has_flag(&self, flag: u32) -> bool {
        self.flags & flag != 0
    }

    /// Read plCameraBrain1 base fields.
    /// C++ ref: plCameraBrain.cpp:482-513
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let self_key = read_key_uoid(reader)?;

        let poa_x = reader.read_f32()?;
        let poa_y = reader.read_f32()?;
        let poa_z = reader.read_f32()?;

        let subject_key = read_key_uoid(reader)?;
        let rail_key = read_key_uoid(reader)?;

        // fFlags — hsBitVector: u32 count, then count × u32
        let num_words = reader.read_u32()?;
        let mut flags: u32 = 0;
        for i in 0..num_words {
            let bits = reader.read_u32()?;
            if i == 0 {
                flags = bits;
            }
        }

        let accel_val = reader.read_f32()?;
        let decel_val = reader.read_f32()?;
        let velocity = reader.read_f32()?;
        let poa_accel_val = reader.read_f32()?;
        let poa_decel_val = reader.read_f32()?;
        let poa_velocity = reader.read_f32()?;

        let x_pan_limit = reader.read_f32()?;
        let z_pan_limit = reader.read_f32()?;

        let zoom_rate = reader.read_f32()?;
        let zoom_min = reader.read_f32()?;
        let zoom_max = reader.read_f32()?;

        Ok(Self {
            self_key,
            poa_offset: [poa_x, poa_y, poa_z],
            subject_key,
            rail_key,
            flags,
            accel: CameraAccel { accel: accel_val, decel: decel_val, max_vel: velocity },
            poa_accel: CameraAccel { accel: poa_accel_val, decel: poa_decel_val, max_vel: poa_velocity },
            x_pan_limit,
            z_pan_limit,
            zoom_rate,
            zoom_min,
            zoom_max,
        })
    }
}

// ─── Avatar brain (third-person follow) ────────────────────────────────────

/// plCameraBrain1_Avatar (0x009E).
/// C++ ref: plCameraBrain.cpp:1251-1254
#[derive(Debug, Clone)]
pub struct AvatarBrainParams {
    pub base: BrainBase,
    pub offset: [f32; 3],
}

impl AvatarBrainParams {
    /// Read plCameraBrain1_Avatar from PRP stream.
    /// Format: BrainBase + fOffset(3f)
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = BrainBase::read(reader)?;
        let off_x = reader.read_f32()?;
        let off_y = reader.read_f32()?;
        let off_z = reader.read_f32()?;

        Ok(Self {
            base,
            offset: [off_x, off_y, off_z],
        })
    }
}

// ─── First-person brain ────────────────────────────────────────────────────

/// plCameraBrain1_FirstPerson (0x00B3).
/// Same binary format as Avatar — different runtime behavior only.
#[derive(Debug, Clone)]
pub struct FirstPersonBrainParams {
    pub base: BrainBase,
    pub offset: [f32; 3],
}

impl FirstPersonBrainParams {
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = BrainBase::read(reader)?;
        let off_x = reader.read_f32()?;
        let off_y = reader.read_f32()?;
        let off_z = reader.read_f32()?;

        Ok(Self {
            base,
            offset: [off_x, off_y, off_z],
        })
    }
}

// ─── Fixed brain ───────────────────────────────────────────────────────────

/// plCameraBrain1_Fixed (0x009F).
/// C++ ref: plCameraBrain.cpp:1455-1459
#[derive(Debug, Clone)]
pub struct FixedBrainParams {
    pub base: BrainBase,
    pub target_point: Option<Uoid>,
}

impl FixedBrainParams {
    /// Read plCameraBrain1_Fixed: BrainBase + target_point_key
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = BrainBase::read(reader)?;
        let target_point = read_key_uoid(reader)?;

        Ok(Self {
            base,
            target_point,
        })
    }
}

// ─── Circle brain ──────────────────────────────────────────────────────────

/// plCameraBrain1_Circle (0x00C2).
/// C++ ref: plCameraBrain.cpp:1721-1741
#[derive(Debug, Clone)]
pub struct CircleBrainParams {
    pub base: BrainBase,
    pub circle_flags: u32,
    pub center: [f32; 3],
    pub radius: f32,
    pub center_object: Option<Uoid>,
    pub poa_object: Option<Uoid>,
    pub cir_per_sec: f32,
}

impl CircleBrainParams {
    /// Read plCameraBrain1_Circle from PRP stream.
    /// C++ Read chain: plCameraBrain1::Read (NOT Fixed::Read) then circle-specific fields.
    pub fn read(reader: &mut impl Read) -> Result<Self> {
        let base = BrainBase::read(reader)?;

        let circle_flags = reader.read_u32()?;

        let cx = reader.read_f32()?;
        let cy = reader.read_f32()?;
        let cz = reader.read_f32()?;

        let radius = reader.read_f32()?;

        let center_object = read_key_uoid(reader)?;
        let poa_object = read_key_uoid(reader)?;

        let cir_per_sec = reader.read_f32()?;

        Ok(Self {
            base,
            circle_flags,
            center: [cx, cy, cz],
            radius,
            center_object,
            poa_object,
            cir_per_sec,
        })
    }
}

// ─── Unified brain enum ────────────────────────────────────────────────────

/// All camera brain types parsed from PRP.
#[derive(Debug, Clone)]
pub enum CameraBrain {
    Avatar(AvatarBrainParams),
    FirstPerson(FirstPersonBrainParams),
    Fixed(FixedBrainParams),
    Circle(CircleBrainParams),
    Drive,
    Base(BrainBase),
}

impl CameraBrain {
    pub fn base(&self) -> Option<&BrainBase> {
        match self {
            CameraBrain::Avatar(p) => Some(&p.base),
            CameraBrain::FirstPerson(p) => Some(&p.base),
            CameraBrain::Fixed(p) => Some(&p.base),
            CameraBrain::Circle(p) => Some(&p.base),
            CameraBrain::Base(b) => Some(b),
            CameraBrain::Drive => None,
        }
    }

    pub fn self_key(&self) -> Option<&Uoid> {
        self.base().and_then(|b| b.self_key.as_ref())
    }

    /// Parse a camera brain from PRP stream given its class ID.
    pub fn read_by_class(class_id: u16, reader: &mut impl Read) -> Result<Self> {
        use crate::core::class_index::ClassIndex;
        match class_id {
            ClassIndex::PL_CAMERA_BRAIN1_AVATAR => {
                Ok(CameraBrain::Avatar(AvatarBrainParams::read(reader)?))
            }
            ClassIndex::PL_CAMERA_BRAIN1_FIRST_PERSON => {
                Ok(CameraBrain::FirstPerson(FirstPersonBrainParams::read(reader)?))
            }
            ClassIndex::PL_CAMERA_BRAIN1_FIXED => {
                Ok(CameraBrain::Fixed(FixedBrainParams::read(reader)?))
            }
            ClassIndex::PL_CAMERA_BRAIN1_CIRCLE => {
                Ok(CameraBrain::Circle(CircleBrainParams::read(reader)?))
            }
            ClassIndex::PL_CAMERA_BRAIN1 => {
                Ok(CameraBrain::Base(BrainBase::read(reader)?))
            }
            _ => anyhow::bail!("Unknown camera brain class 0x{:04X}", class_id),
        }
    }

    pub fn type_name(&self) -> &'static str {
        match self {
            CameraBrain::Avatar(_) => "Avatar",
            CameraBrain::FirstPerson(_) => "FirstPerson",
            CameraBrain::Fixed(_) => "Fixed",
            CameraBrain::Circle(_) => "Circle",
            CameraBrain::Drive => "Drive",
            CameraBrain::Base(_) => "Base",
        }
    }
}