physis/
pcb.rs

1// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::io::Cursor;
5use std::io::SeekFrom;
6
7use crate::ByteSpan;
8use binrw::BinRead;
9use binrw::BinResult;
10use binrw::binrw;
11
12#[binrw]
13#[derive(Debug)]
14#[brw(little)]
15struct PcbResourceHeader {
16    pcb_type: u32, // Lumina: 0x0 is resource, 0x1 is list?
17    version: u32,  // ClientStructs: 0 is 'legacy', 1/4 are 'normal', rest unsupported
18    total_nodes: u32,
19    total_polygons: u32,
20}
21
22#[binrw::parser(reader)]
23fn parse_resource_node_children(
24    child1_offset: u32,
25    child2_offset: u32,
26) -> BinResult<Vec<ResourceNode>> {
27    let initial_position = reader.stream_position().unwrap();
28    let struct_start = initial_position - ResourceNode::HEADER_SIZE as u64;
29
30    let mut children = Vec::new();
31    if child1_offset != 0 {
32        reader
33            .seek(SeekFrom::Start(struct_start + child1_offset as u64))
34            .unwrap();
35        children.push(ResourceNode::read_le(reader)?);
36    }
37
38    if child2_offset != 0 {
39        reader
40            .seek(SeekFrom::Start(struct_start + child2_offset as u64))
41            .unwrap();
42        children.push(ResourceNode::read_le(reader)?);
43    }
44
45    Ok(children)
46}
47
48/// Transform compressed vertices from 0-65535 to local_bounds.min-local_bounds.max
49fn uncompress_vertices(local_bounds: &AABB, vertex: &[u16; 3]) -> [f32; 3] {
50    let x_scale = (local_bounds.max[0] - local_bounds.min[0]) / u16::MAX as f32;
51    let y_scale = (local_bounds.max[1] - local_bounds.min[1]) / u16::MAX as f32;
52    let z_scale = (local_bounds.max[2] - local_bounds.min[2]) / u16::MAX as f32;
53
54    [
55        local_bounds.min[0] + x_scale * (vertex[0] as f32),
56        local_bounds.min[1] + y_scale * (vertex[1] as f32),
57        local_bounds.min[2] + z_scale * (vertex[2] as f32),
58    ]
59}
60
61#[binrw]
62#[derive(Debug)]
63#[brw(little)]
64pub struct ResourceNode {
65    // TODO: figure out what these two values are
66    magic: u32,   // pretty terrible magic if you ask me, lumina calls it so
67    version: u32, // usually 0x0, is this really a version?!
68
69    child1_offset: u32,
70    child2_offset: u32,
71
72    /// The bounding box of this node.
73    pub local_bounds: AABB,
74
75    num_vert_f16: u16,
76    num_polygons: u16,
77    #[brw(pad_after = 2)] // padding, supposedly
78    num_vert_f32: u16,
79
80    /// The children of this node.
81    #[br(parse_with = parse_resource_node_children, args(child1_offset, child2_offset))]
82    #[br(restore_position)]
83    pub children: Vec<ResourceNode>,
84
85    #[br(count = num_vert_f32)]
86    f32_vertices: Vec<[f32; 3]>,
87    #[br(count = num_vert_f16)]
88    #[bw(ignore)]
89    f16_vertices: Vec<[u16; 3]>,
90
91    /// This node's vertices.
92    #[br(calc = f32_vertices.clone().into_iter().chain(f16_vertices.iter().map(|vec| uncompress_vertices(&local_bounds, vec))).collect())]
93    #[bw(ignore)]
94    pub vertices: Vec<[f32; 3]>,
95
96    /// This node's polygons, which include index data.
97    #[br(count = num_polygons)]
98    pub polygons: Vec<Polygon>,
99}
100
101impl ResourceNode {
102    pub const HEADER_SIZE: usize = 0x30;
103}
104
105#[binrw]
106#[derive(Debug, Clone, PartialEq)]
107#[allow(dead_code)]
108pub struct AABB {
109    pub min: [f32; 3],
110    pub max: [f32; 3],
111}
112
113#[binrw]
114#[derive(Debug, Clone, PartialEq)]
115#[allow(dead_code)]
116pub struct Polygon {
117    #[brw(pad_after = 1)] // padding
118    pub vertex_indices: [u8; 3],
119    pub material: u64,
120}
121
122#[binrw]
123#[derive(Debug)]
124#[brw(little)]
125pub struct Pcb {
126    header: PcbResourceHeader,
127    /// The root node of this PCB.
128    pub root_node: ResourceNode,
129}
130
131impl Pcb {
132    /// Reads an existing PCB file
133    pub fn from_existing(buffer: ByteSpan) -> Option<Self> {
134        let mut cursor = Cursor::new(buffer);
135        Pcb::read(&mut cursor).ok()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::fs::read;
142    use std::path::PathBuf;
143
144    use super::*;
145
146    #[test]
147    fn test_invalid() {
148        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
149        d.push("resources/tests");
150        d.push("random");
151
152        // Feeding it invalid data should not panic
153        Pcb::from_existing(&read(d).unwrap());
154    }
155}