From f8db40d74de4af7781d04796e8d2365ec43e1d9a Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 15 Mar 2025 17:34:05 -0400 Subject: [PATCH] Improve layer group parsing (lgb files) I guessed the name, it's more like a collection of layers. This actually fixes a bunch of mistakes (on my part) as I copied the layout from Lumina, but added a few changes to make it work with modern zones. There's a whole lot more work here to do, but now it works to parse New & Old Gridania. --- src/common_file_operations.rs | 20 ++ src/layer/aetheryte.rs | 12 + src/layer/bg.rs | 32 ++ src/layer/common.rs | 40 +++ src/layer/env_set.rs | 30 ++ src/layer/exit_range.rs | 25 ++ src/layer/light.rs | 54 ++++ src/layer/mod.rs | 578 ++++++++++++++++++++++++++++++++++ src/layer/npc.rs | 74 +++++ src/layer/pop.rs | 25 ++ src/layer/position_marker.rs | 20 ++ src/layer/shared_group.rs | 66 ++++ src/layer/sound.rs | 10 + src/layer/trigger_box.rs | 27 ++ src/lgb.rs | 538 ------------------------------- src/lib.rs | 3 +- 16 files changed, 1015 insertions(+), 539 deletions(-) create mode 100644 src/layer/aetheryte.rs create mode 100644 src/layer/bg.rs create mode 100644 src/layer/common.rs create mode 100644 src/layer/env_set.rs create mode 100644 src/layer/exit_range.rs create mode 100644 src/layer/light.rs create mode 100644 src/layer/mod.rs create mode 100644 src/layer/npc.rs create mode 100644 src/layer/pop.rs create mode 100644 src/layer/position_marker.rs create mode 100644 src/layer/shared_group.rs create mode 100644 src/layer/sound.rs create mode 100644 src/layer/trigger_box.rs delete mode 100644 src/lgb.rs diff --git a/src/common_file_operations.rs b/src/common_file_operations.rs index eb49290..1cb2c6c 100644 --- a/src/common_file_operations.rs +++ b/src/common_file_operations.rs @@ -54,6 +54,26 @@ pub(crate) fn strings_parser( Ok(strings) } +#[binrw::parser(reader)] +pub(crate) fn string_from_offset(start: u64) -> BinResult { + let offset: u32 = reader.read_le::()?; + + let mut string = String::new(); + + let old_pos = reader.stream_position()?; + + reader.seek(SeekFrom::Start(start + offset as u64))?; + 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; + } + + reader.seek(SeekFrom::Start(old_pos))?; + + Ok(string) +} + fn read_half1(data: [u16; 1]) -> Half1 { Half1 { value: f16::from_bits(data[0]), diff --git a/src/layer/aetheryte.rs b/src/layer/aetheryte.rs new file mode 100644 index 0000000..d9d903b --- /dev/null +++ b/src/layer/aetheryte.rs @@ -0,0 +1,12 @@ +use binrw::binread; + +use super::GameInstanceObject; + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct AetheryteInstanceObject { + pub parent_data: GameInstanceObject, + pub bound_instance_id: u32, + padding: u32, +} diff --git a/src/layer/bg.rs b/src/layer/bg.rs new file mode 100644 index 0000000..2abacf4 --- /dev/null +++ b/src/layer/bg.rs @@ -0,0 +1,32 @@ +use binrw::{binread, binrw}; + +use super::read_bool_from; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum ModelCollisionType { + None = 0x0, + Replace = 0x1, + Box = 0x2, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct BGInstanceObject { + pub asset_path_string_offset: u32, + pub collision_asset_path_string_offset: u32, + pub collision_type: ModelCollisionType, + pub attribute_mask: u32, + pub attribute: u32, + pub collision_config: i32, + #[br(map = read_bool_from::)] + pub is_visible: bool, + #[br(map = read_bool_from::)] + pub render_shadow_enabled: bool, + #[br(map = read_bool_from::)] + pub render_light_shadow_enabeld: bool, + padding: u8, + pub render_model_clip_range: f32, +} diff --git a/src/layer/common.rs b/src/layer/common.rs new file mode 100644 index 0000000..615d1d7 --- /dev/null +++ b/src/layer/common.rs @@ -0,0 +1,40 @@ +use binrw::binrw; + +#[binrw] +#[derive(Debug)] +#[brw(little)] +pub struct RelativePositions { + pos: i32, + pos_count: i32, +} + +#[binrw] +#[derive(Debug)] +#[brw(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +pub struct Transformation { + pub translation: [f32; 3], + pub rotation: [f32; 3], + pub scale: [f32; 3], +} + +#[binrw] +#[derive(Debug)] +#[brw(little)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +#[binrw] +#[derive(Debug)] +#[brw(little)] +pub struct ColorHDRI { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, + pub intensity: f32, +} diff --git a/src/layer/env_set.rs b/src/layer/env_set.rs new file mode 100644 index 0000000..87a5203 --- /dev/null +++ b/src/layer/env_set.rs @@ -0,0 +1,30 @@ +use binrw::{binread, binrw}; + +use super::read_bool_from; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum EnvSetShape { + Ellipsoid = 0x1, + Cuboid = 0x2, + Cylinder = 0x3, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct EnvSetInstanceObject { + pub asset_path_offset: u32, + pub bound_instance_id: u32, + pub shape: EnvSetShape, + #[br(map = read_bool_from::)] + pub is_env_map_shooting_point: bool, + pub priority: u8, + padding: u16, + pub effective_range: f32, + pub interpolation_time: i32, + pub reverb: f32, + pub filter: f32, + pub sound_asset_path_offset: u32, +} diff --git a/src/layer/exit_range.rs b/src/layer/exit_range.rs new file mode 100644 index 0000000..fad4ec6 --- /dev/null +++ b/src/layer/exit_range.rs @@ -0,0 +1,25 @@ +use binrw::{binread, binrw}; + +use super::TriggerBoxInstanceObject; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum ExitType { + ZoneLine = 0x1, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct ExitRangeInstanceObject { + pub parent_data: TriggerBoxInstanceObject, + pub exit_type: ExitType, + pub zone_id: u16, + pub territory_type: u16, + pub index: i32, + pub destination_instance_id: u32, + pub return_instance_id: u32, + pub player_running_direction: f32, + padding: u32, +} diff --git a/src/layer/light.rs b/src/layer/light.rs new file mode 100644 index 0000000..145005a --- /dev/null +++ b/src/layer/light.rs @@ -0,0 +1,54 @@ +use binrw::{binread, binrw}; + +use super::{ColorHDRI, read_bool_from}; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum LightType { + None = 0x0, + Directional = 0x1, + Point = 0x2, + Spot = 0x3, + Plane = 0x4, + Line = 0x5, + Specular = 0x6, +} + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum PointLightType { + Sphere = 0x0, + Hemisphere = 0x1, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct LightInstanceObject { + pub light_type: LightType, + pub attenuation: f32, + pub range_rate: f32, + pub point_light_type: PointLightType, + pub attenuation_cone_coefficient: f32, + pub cone_degree: f32, + pub texture_path_offset: u32, + pub diffuse_color_hdri: ColorHDRI, + #[br(map = read_bool_from::)] + pub follows_directional_light: bool, + padding1: u8, + padding2: u16, + #[br(map = read_bool_from::)] + pub specular_enabled: bool, + #[br(map = read_bool_from::)] + pub bg_shadow_enabled: bool, + #[br(map = read_bool_from::)] + pub character_shadow_enabled: bool, + padding3: u8, + pub shadow_clip_range: f32, + pub plane_light_rotation_x: f32, + pub plane_light_rotation_y: f32, + pub merge_group_id: u16, + padding4: u8, +} diff --git a/src/layer/mod.rs b/src/layer/mod.rs new file mode 100644 index 0000000..404bee3 --- /dev/null +++ b/src/layer/mod.rs @@ -0,0 +1,578 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::io::{Cursor, Read, Seek, SeekFrom}; + +use crate::ByteSpan; +use crate::common_file_operations::{read_bool_from, string_from_offset}; +use binrw::binrw; +use binrw::{BinRead, BinReaderExt, binread}; + +mod aetheryte; +pub use aetheryte::AetheryteInstanceObject; + +mod bg; +pub use bg::BGInstanceObject; +pub use bg::ModelCollisionType; + +mod common; +pub use common::Color; +pub use common::ColorHDRI; +pub use common::Transformation; + +mod env_set; +pub use env_set::EnvSetInstanceObject; +pub use env_set::EnvSetShape; + +mod exit_range; +pub use exit_range::ExitRangeInstanceObject; +pub use exit_range::ExitType; + +mod light; +pub use light::LightInstanceObject; +pub use light::LightType; +pub use light::PointLightType; + +mod npc; +pub use npc::BNPCInstanceObject; +pub use npc::ENPCInstanceObject; +pub use npc::GameInstanceObject; +pub use npc::NPCInstanceObject; + +mod pop; +pub use pop::PopRangeInstanceObject; +pub use pop::PopType; + +mod position_marker; +pub use position_marker::PositionMarkerInstanceObject; +pub use position_marker::PositionMarkerType; + +mod shared_group; +pub use shared_group::ColourState; +pub use shared_group::DoorState; +pub use shared_group::RotationState; +pub use shared_group::SharedGroupInstance; +pub use shared_group::TransformState; + +mod sound; +pub use sound::SoundInstanceObject; + +mod trigger_box; +pub use trigger_box::TriggerBoxInstanceObject; +pub use trigger_box::TriggerBoxShape; + +// From https://github.com/NotAdam/Lumina/tree/40dab50183eb7ddc28344378baccc2d63ae71d35/src/Lumina/Data/Parsing/Layer +// Also see https://github.com/aers/FFXIVClientStructs/blob/6b62122cae38bfbc016bf697bef75f80f37abac1/FFXIVClientStructs/FFXIV/Client/LayoutEngine/ILayoutInstance.cs + +// TODO: convert these all to magic +#[binrw] +#[brw(repr = i32)] +#[repr(i32)] +#[derive(Debug, PartialEq)] +enum LayerEntryType { + AssetNone = 00, + BG = 0x1, + Attribute = 0x2, + LayLight = 0x3, + Vfx = 0x4, + PositionMarker = 0x5, + SharedGroup = 0x6, + Sound = 0x7, + EventNPC = 0x8, + BattleNPC = 0x9, + RoutePath = 0xA, + Character = 0xB, + Aetheryte = 0xC, + EnvSet = 0xD, + Gathering = 0xE, + HelperObject = 0xF, + Treasure = 0x10, + Clip = 0x11, + ClipCtrlPoint = 0x12, + ClipCamera = 0x13, + ClipLight = 0x14, + ClipReserve00 = 0x15, + ClipReserve01 = 0x16, + ClipReserve02 = 0x17, + ClipReserve03 = 0x18, + ClipReserve04 = 0x19, + ClipReserve05 = 0x1A, + ClipReserve06 = 0x1B, + ClipReserve07 = 0x1C, + ClipReserve08 = 0x1D, + ClipReserve09 = 0x1E, + ClipReserve10 = 0x1F, + ClipReserve11 = 0x20, + ClipReserve12 = 0x21, + ClipReserve13 = 0x22, + ClipReserve14 = 0x23, + CutAssetOnlySelectable = 0x24, + Player = 0x25, + Monster = 0x26, + Weapon = 0x27, + PopRange = 0x28, + /// Zone Transitions (the visible part is probably LineVFX?) + ExitRange = 0x29, + Lvb = 0x2A, + MapRange = 0x2B, + NaviMeshRange = 0x2C, + EventObject = 0x2D, + DemiHuman = 0x2E, + EnvLocation = 0x2F, + ControlPoint = 0x30, + EventRange = 0x31, + RestBonusRange = 0x32, + QuestMarker = 0x33, + Timeline = 0x34, + ObjectBehaviorSet = 0x35, + Movie = 0x36, + ScenarioExd = 0x37, + ScenarioText = 0x38, + CollisionBox = 0x39, + DoorRange = 0x3A, + LineVFX = 0x3B, + SoundEnvSet = 0x3C, + CutActionTimeline = 0x3D, + CharaScene = 0x3E, + CutAction = 0x3F, + EquipPreset = 0x40, + ClientPath = 0x41, + ServerPath = 0x42, + GimmickRange = 0x43, + TargetMarker = 0x44, + ChairMarker = 0x45, + ClickableRange = 0x46, + PrefetchRange = 0x47, + FateRange = 0x48, + PartyMember = 0x49, + KeepRange = 0x4A, + SphereCastRange = 0x4B, + IndoorObject = 0x4C, + OutdoorObject = 0x4D, + EditGroup = 0x4E, + StableChocobo = 0x4F, + MaxAssetType = 0x50, + Unk1 = 90, +} + +#[binread] +#[derive(Debug)] +#[br(import(magic: &LayerEntryType))] +enum LayerEntryData { + #[br(pre_assert(*magic == LayerEntryType::BG))] + BG(BGInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::LayLight))] + LayLight(LightInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::Vfx))] + Vfx(VFXInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::PositionMarker))] + PositionMarker(PositionMarkerInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::SharedGroup))] + SharedGroup(SharedGroupInstance), + #[br(pre_assert(*magic == LayerEntryType::Sound))] + Sound(SoundInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::EventNPC))] + EventNPC(ENPCInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::BattleNPC))] + BattleNPC(BNPCInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::Aetheryte))] + Aetheryte(AetheryteInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::EnvSet))] + EnvSet(EnvSetInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::Gathering))] + Gathering(GatheringInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::Treasure))] + Treasure(TreasureInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::PopRange))] + PopRange(PopRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::ExitRange))] + ExitRange(ExitRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::MapRange))] + MapRange(MapRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::EventObject))] + EventObject(EventInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::EnvLocation))] + EnvLocation(EnvLocationObject), + #[br(pre_assert(*magic == LayerEntryType::EventRange))] + EventRange(EventRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::QuestMarker))] + QuestMarker(QuestMarkerInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::CollisionBox))] + CollisionBox(CollisionBoxInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::LineVFX))] + LineVFX(LineVFXInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::ClientPath))] + ClientPath(ClientPathInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::ServerPath))] + ServerPath(ServerPathInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::GimmickRange))] + GimmickRange(GimmickRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::TargetMarker))] + TargetMarker(TargetMarkerInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::ChairMarker))] + ChairMarker(ChairMarkerInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::PrefetchRange))] + PrefetchRange(PrefetchRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::FateRange))] + FateRange(FateRangeInstanceObject), + #[br(pre_assert(*magic == LayerEntryType::Unk1))] + Unk1(), +} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct VFXInstanceObject { + asset_path_offset: u32, + soft_particle_fade_range: f32, + padding: u32, + color: Color, + #[br(map = read_bool_from::)] + auto_play: bool, + #[br(map = read_bool_from::)] + no_far_clip: bool, + padding1: u16, + fade_near_start: f32, + fade_near_end: f32, + fade_far_start: f32, + fade_far_end: f32, + z_correct: f32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct GatheringInstanceObject { + gathering_point_id: u32, + padding: u32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct TreasureInstanceObject { + nonpop_init_zone: u8, + padding1: [u8; 3], + padding2: [u32; 2], +} + +// Unimplemented because I haven't needed it yet: +#[binread] +#[derive(Debug)] +#[br(little)] +struct MapRangeInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct EventInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct EnvLocationObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct EventRangeInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct QuestMarkerInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct CollisionBoxInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct LineVFXInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct ClientPathInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct ServerPathInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct GimmickRangeInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct TargetMarkerInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct ChairMarkerInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct PrefetchRangeInstanceObject {} + +#[binread] +#[derive(Debug)] +#[br(little)] +struct FateRangeInstanceObject {} + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +enum LayerSetReferencedType { + All = 0x0, + Include = 0x1, + Exclude = 0x2, + Undetermined = 0x3, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[br(import(start: u64))] +#[allow(dead_code)] // most of the fields are unused at the moment +struct LayerHeader { + layer_id: u32, + + #[br(parse_with = string_from_offset, args(start))] + name: String, + + instance_object_offset: i32, + instance_object_count: i32, + + #[br(map = read_bool_from::)] + tool_mode_visible: bool, + #[br(map = read_bool_from::)] + tool_mode_read_only: bool, + + #[br(map = read_bool_from::)] + is_bush_layer: bool, + #[br(map = read_bool_from::)] + ps3_visible: bool, + layer_set_referenced_list_offset: i32, + festival_id: u16, + festival_phase_id: u16, + is_temporary: u8, + is_housing: u8, + version_mask: u16, + + #[br(pad_before = 4)] + ob_set_referenced_list: i32, + ob_set_referenced_list_count: i32, + ob_set_enable_referenced_list: i32, + ob_set_enable_referenced_list_count: i32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +struct LayerSetReferencedList { + referenced_type: LayerSetReferencedType, + layer_sets: i32, + layer_set_count: i32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +struct OBSetReferenced { + asset_type: LayerEntryType, + instance_id: u32, + ob_set_asset_path_offset: u32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +struct OBSetEnableReferenced { + asset_type: LayerEntryType, + instance_id: u32, + #[br(map = read_bool_from::)] + ob_set_enable: bool, + #[br(map = read_bool_from::)] + ob_set_emissive_enable: bool, + padding: [u8; 2], +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +struct LgbHeader { + #[br(count = 4)] + file_id: Vec, + file_size: i32, + total_chunk_count: i32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[allow(dead_code)] // most of the fields are unused at the moment +struct LayerChunk { + #[br(count = 4)] + chunk_id: Vec, + chunk_size: i32, + layer_group_id: i32, + name_offset: u32, + layer_offset: i32, + layer_count: i32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +#[br(import(start: u64))] +#[allow(dead_code)] // most of the fields are unused at the moment +struct InstanceObject { + asset_type: LayerEntryType, + pub instance_id: u32, + #[br(parse_with = string_from_offset, args(start))] + pub name: String, + pub transform: Transformation, + #[br(args(&asset_type))] + pub data: LayerEntryData, +} + +#[derive(Debug)] +pub struct Layer { + pub id: u32, + pub name: String, + pub objects: Vec, +} + +#[derive(Debug)] +pub struct LayerGroup { + pub layers: Vec, +} + +impl LayerGroup { + /// Reads an existing PBD file + pub fn from_existing(buffer: ByteSpan) -> Option { + let mut cursor = Cursor::new(buffer); + + let file_header = LgbHeader::read(&mut cursor).unwrap(); + if file_header.file_size < 0 || file_header.total_chunk_count < 0 { + return None; + } + + let chunk_header = LayerChunk::read(&mut cursor).unwrap(); + + let old_pos = cursor.position(); + + let mut layer_offsets = vec![0i32; chunk_header.layer_count as usize]; + for i in 0..chunk_header.layer_count { + layer_offsets[i as usize] = cursor.read_le::().unwrap(); + } + + let mut layers = Vec::new(); + + for i in 0..chunk_header.layer_count { + cursor + .seek(SeekFrom::Start(old_pos + layer_offsets[i as usize] as u64)) + .unwrap(); + + let old_pos = cursor.position(); + + let header = LayerHeader::read_le_args(&mut cursor, (old_pos,)).unwrap(); + + let mut objects = Vec::new(); + // read instance objects + { + let mut instance_offsets = vec![0i32; header.instance_object_count as usize]; + for i in 0..header.instance_object_count { + instance_offsets[i as usize] = cursor.read_le::().unwrap(); + } + + for i in 0..header.instance_object_count { + cursor + .seek(SeekFrom::Start( + old_pos + + header.instance_object_offset as u64 + + instance_offsets[i as usize] as u64, + )) + .unwrap(); + + let start = cursor.stream_position().unwrap(); + + objects.push(InstanceObject::read_le_args(&mut cursor, (start,)).unwrap()); + } + } + + // read layer set referenced list + { + // NOTE: this casting is INTENTIONALLY like this, layer_set_referenced_list_offset CAN be negative! + cursor + .seek(SeekFrom::Start( + (old_pos as i32 + header.layer_set_referenced_list_offset) as u64, + )) + .unwrap(); + let ref_list = LayerSetReferencedList::read(&mut cursor).unwrap(); + } + + // read ob set referenced + { + cursor + .seek(SeekFrom::Start( + old_pos + header.ob_set_referenced_list as u64, + )) + .unwrap(); + for _ in 0..header.ob_set_referenced_list_count { + OBSetReferenced::read(&mut cursor).unwrap(); + } + } + + // read ob set enable referenced list + { + cursor + .seek(SeekFrom::Start( + old_pos + header.ob_set_enable_referenced_list as u64, + )) + .unwrap(); + for _ in 0..header.ob_set_enable_referenced_list_count { + OBSetEnableReferenced::read(&mut cursor).unwrap(); + } + } + + layers.push(Layer { + id: header.layer_id, + name: header.name, + objects, + }); + } + + Some(LayerGroup { layers }) + } +} + +#[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 + LayerGroup::from_existing(&read(d).unwrap()); + } +} diff --git a/src/layer/npc.rs b/src/layer/npc.rs new file mode 100644 index 0000000..68c026d --- /dev/null +++ b/src/layer/npc.rs @@ -0,0 +1,74 @@ +use binrw::{binread, binrw}; + +use super::common::RelativePositions; + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct GameInstanceObject { + base_id: u32, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct NPCInstanceObject { + parent_data: GameInstanceObject, + pop_weather: u32, + pop_time_start: u8, + pop_time_end: u8, + padding: u16, + move_ai: u32, + wandering_range: u8, + route: u8, + event_group: u16, + padding1: [u32; 2], +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct ENPCInstanceObject { + pub parent_data: NPCInstanceObject, + pub behavior: u32, + padding: [u32; 2], +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct BNPCInstanceObject { + pub parent_data: NPCInstanceObject, + pub name_id: u32, + pub drop_item: u32, + pub sense_range_rate: f32, + pub level: u16, + pub active_type: u8, + pub pop_interval: u8, + pub pop_rate: u8, + pub pop_event: u8, + pub link_group: u8, + pub link_family: u8, + pub link_range: u8, + pub link_count_limit: u8, + pub nonpop_init_zone: u8, + pub invalid_repop: u8, + pub link_parent: u8, + pub link_override: u8, + pub link_reply: u8, + pub nonpop: u8, + pub relative_positions: RelativePositions, + pub horizontal_pop_range: f32, + pub vertical_pop_range: f32, + pub bnpc_base_data: i32, + pub repop_id: u8, + pub bnpc_ran_id: u8, + pub territory_range: u16, + pub bound_instance_id: u32, + pub fate_layout_label_id: u32, + pub normal_ai: u32, + pub server_path_id: u32, + pub equipment_id: u32, + pub customize_id: u32, + // TODO: read relativeposition data +} diff --git a/src/layer/pop.rs b/src/layer/pop.rs new file mode 100644 index 0000000..d2e4d45 --- /dev/null +++ b/src/layer/pop.rs @@ -0,0 +1,25 @@ +use binrw::{binread, binrw}; + +use super::common::RelativePositions; + +#[binread] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum PopType { + PC = 0x1, + Npc = 0x2, + Content = 0x3, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct PopRangeInstanceObject { + pub pop_type: PopType, + pub relative_positions: RelativePositions, + pub inner_radius_ratio: f32, + pub index: u8, + padding1: [u8; 3], + padding2: u32, + // TODO: read relative positions +} diff --git a/src/layer/position_marker.rs b/src/layer/position_marker.rs new file mode 100644 index 0000000..a76a489 --- /dev/null +++ b/src/layer/position_marker.rs @@ -0,0 +1,20 @@ +use binrw::{binread, binrw}; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum PositionMarkerType { + DebugZonePop = 0x1, + DebugJump = 0x2, + NaviMesh = 0x3, + LQEvent = 0x4, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct PositionMarkerInstanceObject { + pub position_marker_type: PositionMarkerType, + pub comment_jp_offset: u32, + pub comment_en_offset: u32, +} diff --git a/src/layer/shared_group.rs b/src/layer/shared_group.rs new file mode 100644 index 0000000..65e8ac1 --- /dev/null +++ b/src/layer/shared_group.rs @@ -0,0 +1,66 @@ +use binrw::{binread, binrw}; + +use super::read_bool_from; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum DoorState { + Auto = 0x1, + Open = 0x2, + Closed = 0x3, +} + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum RotationState { + Rounding = 0x1, + Stopped = 0x2, +} + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum TransformState { + Play = 0x0, + Stop = 0x1, + Replay = 0x2, + Reset = 0x3, +} + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum ColourState { + Play = 0x0, + Stop = 0x1, + Replay = 0x2, + Reset = 0x3, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct SharedGroupInstance { + pub asset_path_offset: u32, + pub initial_door_state: DoorState, + pub overriden_members: i32, + pub overriden_members_count: i32, + pub initial_rotation_state: RotationState, + #[br(map = read_bool_from::)] + pub random_timeline_auto_play: bool, + #[br(map = read_bool_from::)] + pub random_timeline_loop_playback: bool, + #[br(map = read_bool_from::)] + pub collision_controllable_without_eobj: bool, + padding: u8, + pub bound_client_path_instance_id: u32, + pub move_path_settings: i32, + #[br(map = read_bool_from::)] + pub not_create_navimesh_door: bool, + padding1: [u8; 3], + pub initial_transform_state: TransformState, + pub initial_color_state: ColourState, + // TODO: read move path settings +} diff --git a/src/layer/sound.rs b/src/layer/sound.rs new file mode 100644 index 0000000..eb15ffd --- /dev/null +++ b/src/layer/sound.rs @@ -0,0 +1,10 @@ +use binrw::binread; + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct SoundInstanceObject { + pub sound_effect_param: i32, + pub asset_path_offset: u32, + // TODO: read separam +} diff --git a/src/layer/trigger_box.rs b/src/layer/trigger_box.rs new file mode 100644 index 0000000..1a70bda --- /dev/null +++ b/src/layer/trigger_box.rs @@ -0,0 +1,27 @@ +use binrw::{binread, binrw}; + +use super::read_bool_from; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum TriggerBoxShape { + Box = 0x1, + Sphere = 0x2, + Cylinder = 0x3, + Board = 0x4, + Mesh = 0x5, + BoardBothSides = 0x6, +} + +#[binread] +#[derive(Debug)] +#[br(little)] +pub struct TriggerBoxInstanceObject { + pub trigger_box_shape: TriggerBoxShape, + pub priority: i16, + #[br(map = read_bool_from::)] + pub enabled: bool, + padidng: u8, + padding1: u32, +} diff --git a/src/lgb.rs b/src/lgb.rs deleted file mode 100644 index 251e06a..0000000 --- a/src/lgb.rs +++ /dev/null @@ -1,538 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Joshua Goins -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::io::{Cursor, Seek, SeekFrom}; - -use crate::ByteSpan; -use binrw::binrw; -use binrw::{BinRead, BinReaderExt, binread}; - -// From https://github.com/NotAdam/Lumina/tree/40dab50183eb7ddc28344378baccc2d63ae71d35/src/Lumina/Data/Parsing/Layer - -// TODO: convert these all to magic -#[binrw] -#[repr(i32)] -#[derive(Debug, PartialEq)] -enum LayerEntryType { - #[brw(magic = 0x0i32)] - AssetNone, - #[brw(magic = 0x1i32)] - BG { - asset_path_offset: u32, - collision_asset_path_offset: i32, - collision_type: ModelCollisionType, - attribute_mask: u32, - attribute: u32, - collision_config: u32, - is_visible: u8, - render_shadow_enabled: u8, - render_light_shadow_enabled: u8, - padding: u8, - render_model_clip_range: f32, - }, - #[brw(magic = 0x2i32)] - Attribute, - #[brw(magic = 0x3i32)] - LayLight, - #[brw(magic = 0x4i32)] - Vfx, - #[brw(magic = 0x5i32)] - PositionMarker, - #[brw(magic = 0x6i32)] - SharedGroup, - Sound = 0x7, // // - EventNPC = 0x8, // // - BattleNPC = 0x9, // // - RoutePath = 0xA, - Character = 0xB, - Aetheryte = 0xC, // // - EnvSet = 0xD, // // - Gathering = 0xE, // // - HelperObject = 0xF, // - Treasure = 0x10, // // - Clip = 0x11, - ClipCtrlPoint = 0x12, - ClipCamera = 0x13, - ClipLight = 0x14, - ClipReserve00 = 0x15, - ClipReserve01 = 0x16, - ClipReserve02 = 0x17, - ClipReserve03 = 0x18, - ClipReserve04 = 0x19, - ClipReserve05 = 0x1A, - ClipReserve06 = 0x1B, - ClipReserve07 = 0x1C, - ClipReserve08 = 0x1D, - ClipReserve09 = 0x1E, - ClipReserve10 = 0x1F, - ClipReserve11 = 0x20, - ClipReserve12 = 0x21, - ClipReserve13 = 0x22, - ClipReserve14 = 0x23, - CutAssetOnlySelectable = 0x24, - Player = 0x25, - Monster = 0x26, - Weapon = 0x27, // - PopRange = 0x28, // // - ExitRange = 0x29, // // - Lvb = 0x2A, - MapRange = 0x2B, // // - NaviMeshRange = 0x2C, // // - EventObject = 0x2D, // // - DemiHuman = 0x2E, - EnvLocation = 0x2F, // // - ControlPoint = 0x30, - EventRange = 0x31, //? // - RestBonusRange = 0x32, - QuestMarker = 0x33, // // - Timeline = 0x34, - ObjectBehaviorSet = 0x35, - Movie = 0x36, - ScenarioExd = 0x37, - ScenarioText = 0x38, - CollisionBox = 0x39, // // - DoorRange = 0x3A, // - LineVFX = 0x3B, // // - SoundEnvSet = 0x3C, - CutActionTimeline = 0x3D, - CharaScene = 0x3E, - CutAction = 0x3F, - EquipPreset = 0x40, - ClientPath = 0x41, // // - ServerPath = 0x42, // // - GimmickRange = 0x43, // // - TargetMarker = 0x44, // // - ChairMarker = 0x45, // // - ClickableRange = 0x46, // - PrefetchRange = 0x47, // // - FateRange = 0x48, // // - PartyMember = 0x49, - KeepRange = 0x4A, // - SphereCastRange = 0x4B, - IndoorObject = 0x4C, - OutdoorObject = 0x4D, - EditGroup = 0x4E, - StableChocobo = 0x4F, - MaxAssetType = 0x50, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum DoorState { - Auto = 0x1, - Open = 0x2, - Closed = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum RotationState { - Rounding = 0x1, - Stopped = 0x2, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum TransformState { - Play = 0x0, - Stop = 0x1, - Replay = 0x2, - Reset = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum ColourState { - Play = 0x0, - Stop = 0x1, - Replay = 0x2, - Reset = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum TriggerBoxShape { - Box = 0x1, - Sphere = 0x2, - Cylinder = 0x3, - Board = 0x4, - Mesh = 0x5, - BoardBothSides = 0x6, -} - -#[binrw] -#[brw(repr = i32)] -#[derive(Debug, PartialEq)] -enum ModelCollisionType { - None = 0x0, - Replace = 0x1, - Box = 0x2, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum LightType { - None = 0x0, - Directional = 0x1, - Point = 0x2, - Spot = 0x3, - Plane = 0x4, - Line = 0x5, - Specular = 0x6, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum PointLightType { - Sphere = 0x0, - Hemisphere = 0x1, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum PositionMarkerType { - DebugZonePop = 0x1, - DebugJump = 0x2, - NaviMesh = 0x3, - LQEvent = 0x4, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum EnvSetShape { - Ellipsoid = 0x1, - Cuboid = 0x2, - Cylinder = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum HelperObjectType { - ProxyActor = 0x0, - NullObject = 0x1, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum TargetType { - None = 0x0, - ENPCInstanceID = 0x1, - Player = 0x2, - PartyMember = 0x3, - ENPCDirect = 0x4, - BNPCDirect = 0x5, - BGObjInstanceID = 0x6, - SharedGroupInstanceID = 0x7, - BGObj = 0x8, - SharedGroup = 0x9, - Weapon = 0xA, - StableChocobo = 0xB, - AllianceMember = 0xC, - Max = 0xD, -} - -#[binread] -#[derive(Debug, PartialEq)] -enum PopType { - #[br(magic = 0x1u8)] - PC = 0x1, - #[br(magic = 0x2u8)] - Npc, - #[br(magic = 0x3u8)] - Content, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum ExitType { - ZoneLine = 0x1, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum RangeType { - Type01 = 0x1, - Type02 = 0x2, - Type03 = 0x3, - Type04 = 0x4, - Type05 = 0x5, - Type06 = 0x6, - Type07 = 0x7, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum LineStyle { - Red = 0x1, - Blue = 0x2, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum GimmickType { - Fishing = 0x1, - Content = 0x2, - Room = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum TargetMarkerType { - UiTarget = 0x0, - UiNameplate = 0x1, - LookAt = 0x2, - BodyDyn = 0x3, - Root = 0x4, -} - -//For ChairMarker -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum ObjectType { - ObjectChair = 0x0, - ObjectBed = 0x1, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum CharacterSize { - DefaultSize = 0x0, - VerySmall = 0x1, - Small = 0x2, - Medium = 0x3, - Large = 0x4, - VeryLarge = 0x5, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum DrawHeadParts { - Default = 0x0, - ForceOn = 0x1, - ForceOff = 0x2, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum RotationType { - NoRotate = 0x0, - AllAxis = 0x1, - YAxisOnly = 0x2, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum MovePathMode { - None = 0x0, - SharedGroupAction = 0x1, - Timeline = 0x2, -} - -#[binrw] -#[brw(repr = i32)] -#[derive(Debug, PartialEq)] -enum LayerSetReferencedType { - All = 0x0, - Include = 0x1, - Exclude = 0x2, - Undetermined = 0x3, -} - -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -enum SoundEffectType { - Point = 0x3, - PointDir = 0x4, - Line = 0x5, - PolyLine = 0x6, - Surface = 0x7, - BoardObstruction = 0x8, - BoxObstruction = 0x9, - PolyLineObstruction = 0xB, - PolygonObstruction = 0xC, - LineExtController = 0xD, - Polygon = 0xE, -} - -#[binread] -#[derive(Debug)] -#[br(little)] -#[allow(dead_code)] // most of the fields are unused at the moment -struct LayerHeader { - layer_id: u32, - name_offset: u32, - - instance_object_offset: i32, - instance_object_count: i32, - - tool_mode_visible: u8, - tool_mode_read_only: u8, - - is_bush_layer: u8, - ps3_visible: u8, - layer_set_referenced_list_offset: i32, - festival_id: u16, - festival_phase_id: u16, - is_temporary: u8, - is_housing: u8, - version_mask: u16, - - #[br(pad_before = 4)] - ob_set_referenced_list: i32, - ob_set_referenced_list_count: i32, - ob_set_enable_referenced_list: i32, - ob_set_enable_referenced_list_count: i32, -} - -#[binread] -#[derive(Debug)] -#[br(little)] -#[allow(dead_code)] // most of the fields are unused at the moment -struct LayerSetReferencedList { - referenced_type: LayerSetReferencedType, - layer_sets: i32, - layer_set_count: i32, -} - -#[binread] -#[derive(Debug)] -#[br(little)] -#[allow(dead_code)] // most of the fields are unused at the moment -struct LgbHeader { - #[br(count = 4)] - file_id: Vec, - file_size: i32, - total_chunk_count: i32, -} - -#[binread] -#[derive(Debug)] -#[br(little)] -#[allow(dead_code)] // most of the fields are unused at the moment -struct LayerChunk { - #[br(count = 4)] - chunk_id: Vec, - chunk_size: i32, - layer_group_id: i32, - name_offset: u32, - layer_offset: i32, - layer_count: i32, -} - -#[binread] -#[derive(Debug)] -#[br(little)] -#[allow(dead_code)] // most of the fields are unused at the moment -struct InstanceObject { - asset_type: LayerEntryType, - instance_id: u32, - name_offset: u32, -} - -#[derive(Debug)] -pub struct Layer {} - -impl Layer { - /// Reads an existing PBD file - pub fn from_existing(buffer: ByteSpan) -> Option { - let mut cursor = Cursor::new(buffer); - - let file_header = LgbHeader::read(&mut cursor).unwrap(); - if file_header.file_size < 0 || file_header.total_chunk_count < 0 { - return None; - } - - let chunk_header = LayerChunk::read(&mut cursor).unwrap(); - - let old_pos = cursor.position(); - - let mut layer_offsets = vec![0i32; chunk_header.layer_count as usize]; - for i in 0..chunk_header.layer_count { - layer_offsets[i as usize] = cursor.read_le::().unwrap(); - } - - for i in 0..chunk_header.layer_count { - cursor - .seek(SeekFrom::Start(old_pos + layer_offsets[i as usize] as u64)) - .unwrap(); - - let old_pos = cursor.position(); - - let header = LayerHeader::read(&mut cursor).unwrap(); - - cursor - .seek(SeekFrom::Start( - old_pos + header.instance_object_offset as u64, - )) - .unwrap(); - - let mut instance_offsets = vec![0i32; header.instance_object_count as usize]; - for i in 0..header.instance_object_count { - instance_offsets[i as usize] = cursor.read_le::().unwrap(); - } - - cursor - .seek(SeekFrom::Start( - old_pos + header.layer_set_referenced_list_offset as u64, - )) - .unwrap(); - LayerSetReferencedList::read(&mut cursor).unwrap(); - - for i in 0..header.instance_object_count { - cursor - .seek(SeekFrom::Start( - old_pos - + header.instance_object_offset as u64 - + instance_offsets[i as usize] as u64, - )) - .unwrap(); - - InstanceObject::read(&mut cursor).unwrap(); - } - } - - Some(Layer { - //header - }) - } -} - -#[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 - Layer::from_existing(&read(d).unwrap()); - } -} diff --git a/src/lib.rs b/src/lib.rs index 2c938ac..22e5288 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,8 @@ mod model_file_operations; pub mod model_vertex_declarations; -pub mod lgb; +/// Reading layer information for a map (LGB) +pub mod layer; pub mod tera;