1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-26 14:17:45 +00:00
physis/src/layer/mod.rs

579 lines
15 KiB
Rust
Raw Normal View History

// 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)]
2025-03-15 17:47:29 -04:00
pub 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))]
2025-03-15 17:47:29 -04:00
pub 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)]
2025-03-15 17:47:29 -04:00
pub 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)]
2025-03-15 17:47:29 -04:00
pub struct GatheringInstanceObject {
gathering_point_id: u32,
padding: u32,
}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub 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)]
2025-03-15 17:47:29 -04:00
pub struct MapRangeInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct EventInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct EnvLocationObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct EventRangeInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct QuestMarkerInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct CollisionBoxInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct LineVFXInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct ClientPathInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct ServerPathInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct GimmickRangeInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct TargetMarkerInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct ChairMarkerInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub struct PrefetchRangeInstanceObject {}
#[binread]
#[derive(Debug)]
#[br(little)]
2025-03-15 17:47:29 -04:00
pub 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
2025-03-15 17:47:29 -04:00
pub 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());
}
}