physis/
lvb.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 crate::common_file_operations::read_bool_from;
9use crate::common_file_operations::read_string_until_null;
10use binrw::BinRead;
11use binrw::BinReaderExt;
12use binrw::BinResult;
13use binrw::binread;
14
15#[binread]
16#[derive(Debug)]
17#[brw(little)]
18#[brw(magic = b"LVB1")]
19pub struct Lvb {
20    /// Including this header
21    pub file_size: u32,
22    /// Number of Scn's
23    #[br(temp)]
24    #[bw(calc = scns.len() as u32)]
25    scn_count: u32,
26    #[br(count = scn_count)]
27    pub scns: Vec<Scn>,
28}
29
30#[binread]
31#[derive(Debug)]
32#[brw(little)]
33#[brw(magic = b"SCN1")]
34pub struct Scn {
35    total_size: u32,
36    pub header: ScnHeader,
37    #[br(seek_before = SeekFrom::Current(header.offset_general as i64 - ScnHeader::SIZE as i64))]
38    #[br(restore_position)]
39    pub general: ScnGeneralSection,
40    #[br(seek_before = SeekFrom::Current(header.offset_unk1 as i64 - ScnHeader::SIZE as i64))]
41    #[br(restore_position)]
42    pub unk1: ScnUnknown1Section,
43    #[br(seek_before = SeekFrom::Current(header.offset_unk2 as i64 - ScnHeader::SIZE as i64))]
44    #[br(restore_position)]
45    pub unk2: ScnUnknown2Section,
46}
47
48#[binrw::parser(reader)]
49pub(crate) fn strings_from_offsets(offsets: &Vec<i32>) -> BinResult<Vec<String>> {
50    let base_offset = reader.stream_position()?;
51
52    let mut strings: Vec<String> = vec![];
53
54    for offset in offsets {
55        let string_offset = *offset as u64;
56
57        let mut string = String::new();
58
59        reader.seek(SeekFrom::Start(base_offset + string_offset))?;
60        let mut next_char = reader.read_le::<u8>().unwrap() as char;
61        while next_char != '\0' {
62            string.push(next_char);
63            next_char = reader.read_le::<u8>().unwrap() as char;
64        }
65
66        strings.push(string);
67    }
68
69    Ok(strings)
70}
71
72#[binread]
73#[derive(Debug)]
74#[brw(little)]
75pub struct ScnHeader {
76    /// offset to FileLayerGroupHeader[NumEmbeddedLayerGroups]
77    offset_embedded_layer_groups: i32,
78    num_embedded_layer_groups: i32,
79    /// offset to FileSceneGeneral
80    offset_general: i32,
81    /// offset to FileSceneFilterList
82    offset_filters: i32,
83    offset_unk1: i32,
84    /// offset to a list of path offsets (ints)
85    offset_layer_group_resources: i32,
86    num_layer_group_resources: i32,
87    unk2: i32,
88    offset_unk2: i32,
89    unk4: i32,
90    unk5: i32,
91    unk6: i32,
92    unk7: i32,
93    unk8: i32,
94    unk9: i32,
95    unk10: i32,
96
97    #[br(count = num_layer_group_resources)]
98    #[br(seek_before = SeekFrom::Current(offset_layer_group_resources as i64 - ScnHeader::SIZE as i64))]
99    #[br(restore_position)]
100    offset_path_layer_group_resources: Vec<i32>,
101
102    #[br(parse_with = strings_from_offsets)]
103    #[br(args(&offset_path_layer_group_resources))]
104    #[br(restore_position)]
105    #[br(seek_before = SeekFrom::Current(offset_layer_group_resources as i64 - ScnHeader::SIZE as i64))]
106    pub path_layer_group_resources: Vec<String>,
107}
108
109impl ScnHeader {
110    pub const SIZE: usize = 0x40;
111}
112
113#[binread]
114#[derive(Debug)]
115#[br(little)]
116pub struct ScnGeneralSection {
117    #[br(map = read_bool_from::<i32>)]
118    pub have_layer_groups: bool,
119    offset_path_terrain: i32,
120    offset_env_spaces: i32,
121    num_env_spaces: i32,
122    unk1: i32,
123    offset_path_sky_visibility: i32,
124    unk2: i32,
125    unk3: i32,
126    unk4: i32,
127    unk5: i32,
128    unk6: i32,
129    unk7: i32,
130    unk8: i32,
131    offset_path_lcb: i32,
132    unk10: i32,
133    unk11: i32,
134    unk12: i32,
135    unk13: i32,
136    unk14: i32,
137    unk15: i32,
138    unk16: i32,
139    #[br(map = read_bool_from::<i32>)]
140    pub have_lcbuw: bool,
141
142    #[br(seek_before = SeekFrom::Current(offset_path_terrain as i64 - ScnGeneralSection::SIZE as i64))]
143    #[br(restore_position, parse_with = read_string_until_null)]
144    pub path_terrain: String,
145
146    #[br(seek_before = SeekFrom::Current(offset_path_sky_visibility as i64 - ScnGeneralSection::SIZE as i64))]
147    #[br(restore_position, parse_with = read_string_until_null)]
148    pub path_sky_visibility: String,
149
150    #[br(seek_before = SeekFrom::Current(offset_path_lcb as i64 - ScnGeneralSection::SIZE as i64))]
151    #[br(restore_position, parse_with = read_string_until_null)]
152    pub path_lcb: String,
153}
154
155impl ScnGeneralSection {
156    pub const SIZE: usize = 0x58;
157}
158
159#[binread]
160#[derive(Debug)]
161#[br(little)]
162pub struct ScnUnknown1Section {
163    unk1: i32,
164    unk2: i32,
165}
166
167impl ScnUnknown1Section {
168    pub const SIZE: usize = 0x8;
169}
170
171// TODO: definitely not correct
172#[binread]
173#[derive(Debug)]
174#[br(little)]
175pub struct ScnUnknown2Section {
176    unk1: i32,
177    unk2: i32,
178}
179
180impl ScnUnknown2Section {
181    pub const SIZE: usize = 0x8;
182}
183
184impl Lvb {
185    /// Reads an existing UWB file
186    pub fn from_existing(buffer: ByteSpan) -> Option<Self> {
187        let mut cursor = Cursor::new(buffer);
188        Lvb::read(&mut cursor).ok()
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use std::fs::read;
195    use std::path::PathBuf;
196
197    use super::*;
198
199    #[test]
200    fn test_invalid() {
201        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
202        d.push("resources/tests");
203        d.push("random");
204
205        // Feeding it invalid data should not panic
206        Lvb::from_existing(&read(d).unwrap());
207    }
208}