1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-20 03:37:47 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-15 17:34:05 -04:00
parent 4269d41179
commit f8db40d74d
16 changed files with 1015 additions and 539 deletions

View file

@ -54,6 +54,26 @@ pub(crate) fn strings_parser(
Ok(strings) Ok(strings)
} }
#[binrw::parser(reader)]
pub(crate) fn string_from_offset(start: u64) -> BinResult<String> {
let offset: u32 = reader.read_le::<u32>()?;
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::<u8>().unwrap() as char;
while next_char != '\0' {
string.push(next_char);
next_char = reader.read_le::<u8>().unwrap() as char;
}
reader.seek(SeekFrom::Start(old_pos))?;
Ok(string)
}
fn read_half1(data: [u16; 1]) -> Half1 { fn read_half1(data: [u16; 1]) -> Half1 {
Half1 { Half1 {
value: f16::from_bits(data[0]), value: f16::from_bits(data[0]),

12
src/layer/aetheryte.rs Normal file
View file

@ -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,
}

32
src/layer/bg.rs Normal file
View file

@ -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::<u8>)]
pub is_visible: bool,
#[br(map = read_bool_from::<u8>)]
pub render_shadow_enabled: bool,
#[br(map = read_bool_from::<u8>)]
pub render_light_shadow_enabeld: bool,
padding: u8,
pub render_model_clip_range: f32,
}

40
src/layer/common.rs Normal file
View file

@ -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,
}

30
src/layer/env_set.rs Normal file
View file

@ -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::<u8>)]
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,
}

25
src/layer/exit_range.rs Normal file
View file

@ -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,
}

54
src/layer/light.rs Normal file
View file

@ -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::<u8>)]
pub follows_directional_light: bool,
padding1: u8,
padding2: u16,
#[br(map = read_bool_from::<u8>)]
pub specular_enabled: bool,
#[br(map = read_bool_from::<u8>)]
pub bg_shadow_enabled: bool,
#[br(map = read_bool_from::<u8>)]
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,
}

578
src/layer/mod.rs Normal file
View file

@ -0,0 +1,578 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// 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::<u8>)]
auto_play: bool,
#[br(map = read_bool_from::<u8>)]
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::<u8>)]
tool_mode_visible: bool,
#[br(map = read_bool_from::<u8>)]
tool_mode_read_only: bool,
#[br(map = read_bool_from::<u8>)]
is_bush_layer: bool,
#[br(map = read_bool_from::<u8>)]
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::<u8>)]
ob_set_enable: bool,
#[br(map = read_bool_from::<u8>)]
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<u8>,
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<u8>,
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<InstanceObject>,
}
#[derive(Debug)]
pub struct LayerGroup {
pub layers: Vec<Layer>,
}
impl LayerGroup {
/// Reads an existing PBD file
pub fn from_existing(buffer: ByteSpan) -> Option<LayerGroup> {
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::<i32>().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::<i32>().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());
}
}

74
src/layer/npc.rs Normal file
View file

@ -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
}

25
src/layer/pop.rs Normal file
View file

@ -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
}

View file

@ -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,
}

66
src/layer/shared_group.rs Normal file
View file

@ -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::<u8>)]
pub random_timeline_auto_play: bool,
#[br(map = read_bool_from::<u8>)]
pub random_timeline_loop_playback: bool,
#[br(map = read_bool_from::<u8>)]
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::<u8>)]
pub not_create_navimesh_door: bool,
padding1: [u8; 3],
pub initial_transform_state: TransformState,
pub initial_color_state: ColourState,
// TODO: read move path settings
}

10
src/layer/sound.rs Normal file
View file

@ -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
}

27
src/layer/trigger_box.rs Normal file
View file

@ -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::<u8>)]
pub enabled: bool,
padidng: u8,
padding1: u32,
}

View file

@ -1,538 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// 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<u8>,
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<u8>,
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<Layer> {
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::<i32>().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::<i32>().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());
}
}

View file

@ -94,7 +94,8 @@ mod model_file_operations;
pub mod model_vertex_declarations; pub mod model_vertex_declarations;
pub mod lgb; /// Reading layer information for a map (LGB)
pub mod layer;
pub mod tera; pub mod tera;