From a8cb676e975d8b14b186598ea95e7026d72fe41d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 4 Jul 2025 11:11:05 -0400 Subject: [PATCH] Begin parsing LVB files These are actually quite useful, and contains stuff like the LGBs used for the territory. --- src/common_file_operations.rs | 12 ++ src/lib.rs | 3 + src/lvb.rs | 209 ++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 src/lvb.rs diff --git a/src/common_file_operations.rs b/src/common_file_operations.rs index eb49290..63301bd 100644 --- a/src/common_file_operations.rs +++ b/src/common_file_operations.rs @@ -54,6 +54,18 @@ pub(crate) fn strings_parser( Ok(strings) } +#[binrw::parser(reader)] +pub(crate) fn read_string_until_null() -> BinResult { + let mut string = String::new(); + + let mut next_char = reader.read_le::().unwrap() as char; + while next_char != '\0' { + string.push(next_char); + next_char = reader.read_le::().unwrap() as char; + } + Ok(string) +} + fn read_half1(data: [u16; 1]) -> Half1 { Half1 { value: f16::from_bits(data[0]), diff --git a/src/lib.rs b/src/lib.rs index 348297a..6b2ecf8 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,6 +143,9 @@ pub mod uwb; /// Reading LCB files pub mod lcb; +/// Reading LVB files +pub mod lvb; + mod bcn; mod error; diff --git a/src/lvb.rs b/src/lvb.rs new file mode 100644 index 0000000..ca32cbe --- /dev/null +++ b/src/lvb.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::io::Cursor; +use std::io::SeekFrom; + +use crate::ByteSpan; +use crate::common_file_operations::read_bool_from; +use crate::common_file_operations::read_string_until_null; +use binrw::BinRead; +use binrw::BinReaderExt; +use binrw::BinResult; +use binrw::binread; + +#[binread] +#[derive(Debug)] +#[brw(little)] +#[brw(magic = b"LVB1")] +pub struct Lvb { + /// Including this header + pub file_size: u32, + /// Number of Scn's + #[br(temp)] + #[bw(calc = scns.len() as u32)] + scn_count: u32, + #[br(count = scn_count)] + pub scns: Vec, +} + +#[binread] +#[derive(Debug)] +#[brw(little)] +#[brw(magic = b"SCN1")] +pub struct Scn { + total_size: u32, + pub header: ScnHeader, + #[br(seek_before = SeekFrom::Current(header.offset_general as i64 - ScnHeader::SIZE as i64))] + #[br(restore_position)] + pub general: ScnGeneralSection, + #[br(seek_before = SeekFrom::Current(header.offset_unk1 as i64 - ScnHeader::SIZE as i64))] + #[br(restore_position)] + pub unk1: ScnUnknown1Section, + #[br(seek_before = SeekFrom::Current(header.offset_unk2 as i64 - ScnHeader::SIZE as i64))] + #[br(restore_position)] + pub unk2: ScnUnknown2Section, +} + +#[binrw::parser(reader)] +pub(crate) fn strings_from_offsets(offsets: &Vec) -> BinResult> { + let base_offset = reader.stream_position()?; + + let mut strings: Vec = vec![]; + + for offset in offsets { + let string_offset = *offset as u64; + + let mut string = String::new(); + + reader.seek(SeekFrom::Start(base_offset + string_offset))?; + let mut next_char = reader.read_le::().unwrap() as char; + while next_char != '\0' { + string.push(next_char); + next_char = reader.read_le::().unwrap() as char; + } + + strings.push(string); + } + + Ok(strings) +} + +#[binread] +#[derive(Debug)] +#[brw(little)] +pub struct ScnHeader { + /// offset to FileLayerGroupHeader[NumEmbeddedLayerGroups] + offset_embedded_layer_groups: i32, + num_embedded_layer_groups: i32, + /// offset to FileSceneGeneral + offset_general: i32, + /// offset to FileSceneFilterList + offset_filters: i32, + offset_unk1: i32, + /// offset to a list of path offsets (ints) + offset_layer_group_resources: i32, + num_layer_group_resources: i32, + unk2: i32, + offset_unk2: i32, + unk4: i32, + unk5: i32, + unk6: i32, + unk7: i32, + unk8: i32, + unk9: i32, + unk10: i32, + + #[br(count = num_layer_group_resources)] + #[br(seek_before = SeekFrom::Current(offset_layer_group_resources as i64 - ScnHeader::SIZE as i64))] + #[br(restore_position)] + offset_path_layer_group_resources: Vec, + + #[br(parse_with = strings_from_offsets)] + #[br(args(&offset_path_layer_group_resources))] + #[br(restore_position)] + #[br(seek_before = SeekFrom::Current(offset_layer_group_resources as i64 - ScnHeader::SIZE as i64))] + pub path_layer_group_resources: Vec, +} + +impl ScnHeader { + pub const SIZE: usize = 0x40; +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct ScnGeneralSection { + #[br(map = read_bool_from::)] + pub have_layer_groups: bool, + offset_path_terrain: i32, + offset_env_spaces: i32, + num_env_spaces: i32, + unk1: i32, + offset_path_sky_visibility: i32, + unk2: i32, + unk3: i32, + unk4: i32, + unk5: i32, + unk6: i32, + unk7: i32, + unk8: i32, + offset_path_lcb: i32, + unk10: i32, + unk11: i32, + unk12: i32, + unk13: i32, + unk14: i32, + unk15: i32, + unk16: i32, + #[br(map = read_bool_from::)] + pub have_lcbuw: bool, + + #[br(seek_before = SeekFrom::Current(offset_path_terrain as i64 - ScnGeneralSection::SIZE as i64))] + #[br(restore_position, parse_with = read_string_until_null)] + pub path_terrain: String, + + #[br(seek_before = SeekFrom::Current(offset_path_sky_visibility as i64 - ScnGeneralSection::SIZE as i64))] + #[br(restore_position, parse_with = read_string_until_null)] + pub path_sky_visibility: String, + + #[br(seek_before = SeekFrom::Current(offset_path_lcb as i64 - ScnGeneralSection::SIZE as i64))] + #[br(restore_position, parse_with = read_string_until_null)] + pub path_lcb: String, +} + +impl ScnGeneralSection { + pub const SIZE: usize = 0x58; +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct ScnUnknown1Section { + unk1: i32, + unk2: i32, +} + +impl ScnUnknown1Section { + pub const SIZE: usize = 0x8; +} + +// TODO: definitely not correct +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct ScnUnknown2Section { + #[br(dbg)] + unk1: i32, + unk2: i32, +} + +impl ScnUnknown2Section { + pub const SIZE: usize = 0x8; +} + +impl Lvb { + /// Reads an existing UWB file + pub fn from_existing(buffer: ByteSpan) -> Option { + let mut cursor = Cursor::new(buffer); + Lvb::read(&mut cursor).ok() + } +} + +#[cfg(test)] +mod tests { + use std::fs::read; + use std::path::PathBuf; + + use super::*; + + #[test] + fn test_invalid() { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("random"); + + // Feeding it invalid data should not panic + Lvb::from_existing(&read(d).unwrap()); + } +}