Enhance Volition format definitions for Saints Row series
Major improvements to VPP, PEG, and ASM parsers: vpp.xscript: - Support all VPP versions (4-10) for Saints Row 1-4 and Red Faction - Add container type detection (str2, packfile, etc.) - Improved compression handling (zlib, LZO, auto-detection) - Better recursive parsing of nested archives peg.xscript: - Full PEG texture container parsing - Support for multiple texture formats (DXT1, DXT3, DXT5, etc.) - Xbox 360 and PC format variants asm.xscript: - Animation state machine container parsing New format definitions: - anim.xscript: Animation data - audio.xscript: Audio containers (bank files) - mesh.xscript: 3D mesh geometry - morph.xscript: Morph targets - rig.xscript: Skeletal rigs - sim.xscript: Simulation data - zone.xscript: Level zone data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a86c5f592c
commit
908100f487
89
definitions/volition/anim.xscript
Normal file
89
definitions/volition/anim.xscript
Normal file
@ -0,0 +1,89 @@
|
||||
// Volition Animation Format (.anim_pc)
|
||||
// Used by: Saints Row 3/4, Red Faction series
|
||||
// Crunched animation with keyframe data
|
||||
|
||||
type volition_anim ui("Volition Animation", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "anim_pc" || _ext == "anim_xbox2" || _ext == "anim_ps3";
|
||||
// Basic sanity: version byte should be reasonable (1-50)
|
||||
ver = u8at(4);
|
||||
require ver > 0 && ver < 50;
|
||||
}
|
||||
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox || is_ps3) {
|
||||
set_name("Animation (Console)");
|
||||
parse_here("volition_anim_be");
|
||||
} else {
|
||||
set_name("Animation (PC)");
|
||||
parse_here("volition_anim_impl");
|
||||
}
|
||||
}
|
||||
|
||||
type volition_anim_be ui("Animation") byteorder BE
|
||||
{
|
||||
parse_here("volition_anim_impl");
|
||||
}
|
||||
|
||||
type volition_anim_impl ui("Animation")
|
||||
{
|
||||
i32 anim_id ui("Animation ID");
|
||||
u8 version ui("Version");
|
||||
u8 flags ui("Flags");
|
||||
u16 end_frame ui("End Frame");
|
||||
u8 ramp_in ui("Ramp In");
|
||||
u8 ramp_out ui("Ramp Out");
|
||||
u8 num_bones ui("Bone Count");
|
||||
u8 num_rig_bones ui("Rig Bone Count");
|
||||
|
||||
// Total rotation quaternion
|
||||
f32 rot_x ui("Total Rot X");
|
||||
f32 rot_y ui("Total Rot Y");
|
||||
f32 rot_z ui("Total Rot Z");
|
||||
f32 rot_w ui("Total Rot W");
|
||||
|
||||
// Total translation
|
||||
f32 trans_x ui("Total Trans X");
|
||||
f32 trans_y ui("Total Trans Y");
|
||||
f32 trans_z ui("Total Trans Z");
|
||||
|
||||
// Decode flags
|
||||
has_packed = (flags & 0x01) != 0;
|
||||
has_time_shorts = (flags & 0x02) != 0;
|
||||
has_delta_times = (flags & 0x04) != 0;
|
||||
has_motion = (flags & 0x08) != 0;
|
||||
has_morph = (flags & 0x10) != 0;
|
||||
has_long_trans = (flags & 0x20) != 0;
|
||||
has_weight_keys = (flags & 0x40) != 0;
|
||||
has_short_counts = (flags & 0x80) != 0;
|
||||
|
||||
flags_desc = "";
|
||||
if (has_packed) { flags_desc = flags_desc + "PACKED "; }
|
||||
if (has_time_shorts) { flags_desc = flags_desc + "TIME_SHORTS "; }
|
||||
if (has_delta_times) { flags_desc = flags_desc + "DELTA_TIMES "; }
|
||||
if (has_motion) { flags_desc = flags_desc + "MOTION "; }
|
||||
if (has_morph) { flags_desc = flags_desc + "MORPH "; }
|
||||
if (has_long_trans) { flags_desc = flags_desc + "LONG_TRANS "; }
|
||||
if (has_weight_keys) { flags_desc = flags_desc + "WEIGHT_KEYS "; }
|
||||
if (has_short_counts) { flags_desc = flags_desc + "SHORT_COUNTS "; }
|
||||
flag_display = trim(flags_desc) ui("Flag Names");
|
||||
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Nested type for VPP
|
||||
type volition_anim_nested ui("Animation") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_anim_be");
|
||||
} else {
|
||||
parse_here("volition_anim_impl");
|
||||
}
|
||||
}
|
||||
@ -7,15 +7,23 @@
|
||||
type volition_asm ui("Volition ASM File", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "asm_pc" || _ext == "asm" || _ext == "asm_xbox2";
|
||||
require _ext == "asm_pc" || _ext == "asm" || _ext == "asm_xbox2" || _ext == "asm_ps3";
|
||||
le_magic = u32at(0);
|
||||
require le_magic == 0xBEEFFEED || le_magic == 0xEDFEEFBE;
|
||||
}
|
||||
|
||||
// Detect platform
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
// Detect endianness from magic
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0xEDFEEFBE) {
|
||||
set_name("ASM File (Xbox 360)");
|
||||
if (is_ps3) {
|
||||
set_name("ASM File (PS3)");
|
||||
} else {
|
||||
set_name("ASM File (Xbox 360)");
|
||||
}
|
||||
parse_here("volition_asm_be");
|
||||
} else {
|
||||
set_name("ASM File (PC)");
|
||||
@ -44,6 +52,11 @@ type volition_asm_impl ui("ASM")
|
||||
u16 at_name_len;
|
||||
at_name = ascii(read(at_name_len));
|
||||
u8 at_id;
|
||||
ctx_set("_asm_type_name", at_name);
|
||||
ctx_set("_asm_type_id", at_id);
|
||||
at_entry = bytesat(0, 1) |> parse volition_asm_type_entry;
|
||||
push("allocator_types", at_entry);
|
||||
at_entry = 0;
|
||||
alloc_idx = alloc_idx + 1;
|
||||
}
|
||||
|
||||
@ -53,6 +66,11 @@ type volition_asm_impl ui("ASM")
|
||||
u16 pt_name_len;
|
||||
pt_name = ascii(read(pt_name_len));
|
||||
u8 pt_id;
|
||||
ctx_set("_asm_type_name", pt_name);
|
||||
ctx_set("_asm_type_id", pt_id);
|
||||
pt_entry = bytesat(0, 1) |> parse volition_asm_type_entry;
|
||||
push("primitive_types", pt_entry);
|
||||
pt_entry = 0;
|
||||
prim_idx = prim_idx + 1;
|
||||
}
|
||||
|
||||
@ -62,6 +80,11 @@ type volition_asm_impl ui("ASM")
|
||||
u16 ct_name_len;
|
||||
ct_name = ascii(read(ct_name_len));
|
||||
u8 ct_id;
|
||||
ctx_set("_asm_type_name", ct_name);
|
||||
ctx_set("_asm_type_id", ct_id);
|
||||
ct_entry = bytesat(0, 1) |> parse volition_asm_type_entry;
|
||||
push("container_types", ct_entry);
|
||||
ct_entry = 0;
|
||||
cont_idx = cont_idx + 1;
|
||||
}
|
||||
}
|
||||
@ -104,7 +127,8 @@ type volition_asm_impl ui("ASM")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse primitives
|
||||
// Parse primitives and collect into array
|
||||
primitives_list = make_list();
|
||||
prim_idx = 0;
|
||||
repeat(c_primitive_count) {
|
||||
u16 p_name_len;
|
||||
@ -116,15 +140,27 @@ type volition_asm_impl ui("ASM")
|
||||
i32 p_cpu_size;
|
||||
i32 p_gpu_size;
|
||||
u8 p_unknown7;
|
||||
|
||||
// Create primitive entry
|
||||
ctx_set("_prim_name", p_name);
|
||||
ctx_set("_prim_type", p_type);
|
||||
ctx_set("_prim_cpu_size", p_cpu_size);
|
||||
ctx_set("_prim_gpu_size", p_gpu_size);
|
||||
prim_entry = bytesat(0, 1) |> parse volition_asm_primitive;
|
||||
push(primitives_list, prim_entry);
|
||||
prim_entry = 0;
|
||||
|
||||
prim_idx = prim_idx + 1;
|
||||
}
|
||||
|
||||
// Create container child
|
||||
// Create container child with primitives
|
||||
ctx_set("_asm_container_name", c_name);
|
||||
ctx_set("_asm_container_prim_count", c_primitive_count);
|
||||
ctx_set("_asm_container_primitives", primitives_list);
|
||||
dummy = bytesat(0, 1);
|
||||
container_child = dummy |> parse volition_asm_container;
|
||||
push("containers", container_child);
|
||||
container_child = 0; // Clear to avoid duplicate tree node
|
||||
|
||||
c_idx = c_idx + 1;
|
||||
}
|
||||
@ -139,5 +175,42 @@ type volition_asm_container ui("Container")
|
||||
container_name = ctx_get("_asm_container_name");
|
||||
set_name(container_name);
|
||||
prim_count = ctx_get("_asm_container_prim_count");
|
||||
primitives = prim_count ui("Primitives");
|
||||
primitive_count = prim_count ui("Primitive Count");
|
||||
primitives = ctx_get("_asm_container_primitives");
|
||||
}
|
||||
|
||||
// Primitive entry - inherits byteorder from parent
|
||||
type volition_asm_primitive ui("Primitive")
|
||||
{
|
||||
prim_name = ctx_get("_prim_name");
|
||||
set_name(prim_name);
|
||||
type_id = ctx_get("_prim_type") ui("Type");
|
||||
cpu_size = ctx_get("_prim_cpu_size") ui("CPU Size");
|
||||
gpu_size = ctx_get("_prim_gpu_size") ui("GPU Size");
|
||||
}
|
||||
|
||||
// Type registry entry (allocator, primitive, or container type)
|
||||
type volition_asm_type_entry ui("Type Entry")
|
||||
{
|
||||
type_name = ctx_get("_asm_type_name");
|
||||
set_name(type_name);
|
||||
name = type_name ui("Name");
|
||||
id = ctx_get("_asm_type_id") ui("ID");
|
||||
}
|
||||
|
||||
// Nested ASM type - for parsing ASM files within VPP archives
|
||||
// No criteria block needed since called directly via parse()
|
||||
type volition_asm_nested ui("ASM Manifest") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
|
||||
// Detect endianness from magic
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0xEDFEEFBE) {
|
||||
set_name(file_name + " (BE)");
|
||||
parse_here("volition_asm_be");
|
||||
} else {
|
||||
set_name(file_name);
|
||||
parse_here("volition_asm_impl");
|
||||
}
|
||||
}
|
||||
|
||||
86
definitions/volition/audio.xscript
Normal file
86
definitions/volition/audio.xscript
Normal file
@ -0,0 +1,86 @@
|
||||
// Wwise Audio Bank Format (.bnk_pc)
|
||||
// Used by: Saints Row 3/4, many Wwise-based games
|
||||
// Section-based: BKHD (header), DIDX (index), DATA (audio), HIRC (hierarchy)
|
||||
|
||||
type volition_bnk ui("Wwise Audio Bank", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "bnk_pc" || _ext == "bnk_xbox2" || _ext == "bnk_ps3";
|
||||
// Check for BKHD magic
|
||||
require ascii(bytesat(0, 4)) == "BKHD";
|
||||
}
|
||||
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox || is_ps3) {
|
||||
set_name("Audio Bank (Console)");
|
||||
parse_here("volition_bnk_be");
|
||||
} else {
|
||||
set_name("Audio Bank (PC)");
|
||||
parse_here("volition_bnk_impl");
|
||||
}
|
||||
}
|
||||
|
||||
type volition_bnk_be ui("Audio Bank") byteorder BE
|
||||
{
|
||||
parse_here("volition_bnk_impl");
|
||||
}
|
||||
|
||||
type volition_bnk_impl ui("Audio Bank")
|
||||
{
|
||||
wem_count = 0;
|
||||
data_size = 0;
|
||||
hirc_objects = 0;
|
||||
|
||||
// Parse sections until EOF
|
||||
while (pos() < size() - 8) {
|
||||
section_magic = ascii(read(4));
|
||||
u32 section_size;
|
||||
|
||||
if (section_magic == "BKHD") {
|
||||
u32 wwise_version ui("Wwise Version");
|
||||
u32 bank_id ui("Bank ID");
|
||||
// Skip rest of BKHD section
|
||||
remaining = section_size - 8;
|
||||
if (remaining > 0) { skip(remaining); }
|
||||
} else if (section_magic == "DIDX") {
|
||||
// Data index - count WEM entries (12 bytes each)
|
||||
wem_count = section_size / 12;
|
||||
skip(section_size);
|
||||
} else if (section_magic == "DATA") {
|
||||
data_size = section_size;
|
||||
skip(section_size);
|
||||
} else if (section_magic == "HIRC") {
|
||||
u32 hirc_count;
|
||||
hirc_objects = hirc_count;
|
||||
skip(section_size - 4);
|
||||
} else if (section_magic == "STID") {
|
||||
u32 stid_type;
|
||||
u32 string_count ui("String Count");
|
||||
skip(section_size - 8);
|
||||
} else {
|
||||
// Unknown section - skip
|
||||
skip(section_size);
|
||||
}
|
||||
}
|
||||
|
||||
embedded_wem = wem_count ui("Embedded WEM Files");
|
||||
audio_objects = hirc_objects ui("Audio Objects");
|
||||
audio_data_size = data_size ui("Audio Data Size");
|
||||
total_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Nested type for VPP
|
||||
type volition_bnk_nested ui("Audio Bank") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_bnk_be");
|
||||
} else {
|
||||
parse_here("volition_bnk_impl");
|
||||
}
|
||||
}
|
||||
125
definitions/volition/mesh.xscript
Normal file
125
definitions/volition/mesh.xscript
Normal file
@ -0,0 +1,125 @@
|
||||
// Volition Mesh Format (csmesh/ccmesh/clmesh)
|
||||
// Used by: Saints Row 3/4, Red Faction series
|
||||
// Magic: 0x00043854 (v_file_header signature)
|
||||
// c* = CPU header data, g* = GPU geometry data
|
||||
// ccmesh = character (rigged), csmesh = static, clmesh = level
|
||||
|
||||
type volition_mesh ui("Volition Mesh", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "csmesh_pc" || _ext == "csmesh_xbox2" || _ext == "csmesh_ps3" ||
|
||||
_ext == "ccmesh_pc" || _ext == "ccmesh_xbox2" || _ext == "ccmesh_ps3" ||
|
||||
_ext == "clmesh_pc" || _ext == "clmesh_xbox2" || _ext == "clmesh_ps3";
|
||||
require u32at(0) == 0x00043854;
|
||||
}
|
||||
|
||||
// Detect platform
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_mesh_be");
|
||||
} else {
|
||||
parse_here("volition_mesh_impl");
|
||||
}
|
||||
}
|
||||
|
||||
// Big-endian wrapper for Xbox 360/PS3
|
||||
type volition_mesh_be ui("Mesh") byteorder BE
|
||||
{
|
||||
parse_here("volition_mesh_impl");
|
||||
}
|
||||
|
||||
// Shared implementation
|
||||
type volition_mesh_impl ui("Mesh")
|
||||
{
|
||||
// Detect mesh type from extension
|
||||
is_character = contains(_ext, "ccmesh");
|
||||
is_static = contains(_ext, "csmesh");
|
||||
is_level = contains(_ext, "clmesh");
|
||||
|
||||
mesh_type = "Unknown Mesh";
|
||||
if (is_character) { mesh_type = "Character Mesh (Rigged)"; }
|
||||
if (is_static) { mesh_type = "Static Mesh"; }
|
||||
if (is_level) { mesh_type = "Level Mesh"; }
|
||||
set_name(mesh_type);
|
||||
|
||||
// v_file_header (streaming reference header)
|
||||
u32 signature ui("Signature"); // 0x00043854
|
||||
u32 data_size ui("Data Size");
|
||||
u32 unknown_08 ui("Unknown 0x08");
|
||||
u32 file_count ui("File Count");
|
||||
|
||||
// Mesh info header
|
||||
u32 mesh_magic ui("Mesh Magic"); // 0x424bdood
|
||||
u32 mesh_version ui("Version"); // Usually 0x2a (42)
|
||||
|
||||
// Model data block
|
||||
u32 nine ui("Nine"); // Always 9
|
||||
u32 crc ui("CRC");
|
||||
u32 model_data_size ui("Model Data Size");
|
||||
u32 gmesh_size ui("GPU Mesh Size");
|
||||
u32 flags ui("Flags");
|
||||
u32 indices_count ui("Index Count");
|
||||
u32 index_stride ui("Index Stride");
|
||||
u32 bones_count ui("Bone Count");
|
||||
f32 scale_x ui("Scale X");
|
||||
f32 scale_y ui("Scale Y");
|
||||
f32 scale_z ui("Scale Z");
|
||||
u32 vertex_count ui("Vertex Count");
|
||||
u32 vertex_stride ui("Vertex Stride");
|
||||
|
||||
// Bounding sphere
|
||||
f32 bound_x ui("Bound Center X");
|
||||
f32 bound_y ui("Bound Center Y");
|
||||
f32 bound_z ui("Bound Center Z");
|
||||
f32 bound_radius ui("Bound Radius");
|
||||
|
||||
// Texture name count
|
||||
u32 texture_count ui("Texture Count");
|
||||
|
||||
// Parse texture names
|
||||
if (texture_count > 0 && texture_count < 100) {
|
||||
tex_idx = 0;
|
||||
repeat(texture_count) {
|
||||
tex_name = cstring();
|
||||
ctx_set("_mesh_tex_name", tex_name);
|
||||
ctx_set("_mesh_tex_idx", tex_idx);
|
||||
tex_entry = bytesat(0, 1) |> parse volition_mesh_texture_ref;
|
||||
push("textures", tex_entry);
|
||||
tex_entry = 0;
|
||||
tex_idx = tex_idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary info
|
||||
type_display = mesh_type ui("Mesh Type");
|
||||
total_file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Texture reference entry
|
||||
type volition_mesh_texture_ref ui("Texture Reference")
|
||||
{
|
||||
tex_name = ctx_get("_mesh_tex_name");
|
||||
set_name(tex_name);
|
||||
index = ctx_get("_mesh_tex_idx") ui("Index");
|
||||
name = tex_name ui("Texture Name");
|
||||
}
|
||||
|
||||
// Nested mesh type for VPP parsing
|
||||
type volition_mesh_nested ui("Mesh") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
|
||||
// Detect platform from context
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
|
||||
// Use truthiness check for boolean context values
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_mesh_be");
|
||||
} else {
|
||||
parse_here("volition_mesh_impl");
|
||||
}
|
||||
}
|
||||
54
definitions/volition/morph.xscript
Normal file
54
definitions/volition/morph.xscript
Normal file
@ -0,0 +1,54 @@
|
||||
// Volition Morph Target Format (.cmorph_pc)
|
||||
// Used by: Saints Row 3/4
|
||||
// Contains blend shape/morph target data for character customization
|
||||
|
||||
type volition_morph ui("Morph Targets", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "cmorph_pc" || _ext == "cmorph_xbox2" || _ext == "cmorph_ps3";
|
||||
// Check for reasonable version values
|
||||
ver = i32at(4);
|
||||
require ver > 0 && ver < 20;
|
||||
}
|
||||
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox || is_ps3) {
|
||||
set_name("Morph Targets (Console)");
|
||||
parse_here("volition_morph_be");
|
||||
} else {
|
||||
set_name("Morph Targets (PC)");
|
||||
parse_here("volition_morph_impl");
|
||||
}
|
||||
}
|
||||
|
||||
type volition_morph_be ui("Morph Targets") byteorder BE
|
||||
{
|
||||
parse_here("volition_morph_impl");
|
||||
}
|
||||
|
||||
type volition_morph_impl ui("Morph Targets")
|
||||
{
|
||||
i32 signature ui("Signature");
|
||||
i32 version ui("Version");
|
||||
i32 target_format ui("Target Format");
|
||||
i32 num_targets ui("Target Count");
|
||||
u32 targets_ptr ui("Targets Pointer");
|
||||
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Nested type for VPP
|
||||
type volition_morph_nested ui("Morph Targets") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_morph_be");
|
||||
} else {
|
||||
parse_here("volition_morph_impl");
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,67 @@
|
||||
// Volition PEG Texture Format
|
||||
// Used by: Saints Row 2, Red Faction: Guerrilla, Red Faction: Armageddon
|
||||
// Magic: 0x564B4547 ("GEKV" / "VKEG")
|
||||
// Volition PEG/CPEG/GPEG Texture Format
|
||||
// Used by: Saints Row 2/3/4, Red Faction: Guerrilla, Red Faction: Armageddon
|
||||
// Magic: 0x564B4547 ("GEKV" LE/PC) or 0x47454B56 ("VKEG" BE/Xbox360)
|
||||
// Versions: 10, 13
|
||||
// Frame size: 48 bytes (0x30)
|
||||
// CPEG = CPU texture header, GPEG = GPU texture data (paired files)
|
||||
|
||||
type volition_peg ui("Volition PEG Texture", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "peg_pc" || _ext == "g_peg_pc" || _ext == "cvbm_pc" || _ext == "peg";
|
||||
// PC formats
|
||||
require _ext == "peg_pc" || _ext == "g_peg_pc" || _ext == "cvbm_pc" || _ext == "gvbm_pc" || _ext == "peg" ||
|
||||
// Xbox 360 formats
|
||||
_ext == "cpeg_xbox2" || _ext == "gpeg_xbox2" || _ext == "cvbm_xbox2" || _ext == "gvbm_xbox2" ||
|
||||
// PS3 formats
|
||||
_ext == "cpeg_ps3" || _ext == "gpeg_ps3" || _ext == "cvbm_ps3" || _ext == "gvbm_ps3";
|
||||
// Check magic in both endianness
|
||||
le_magic = u32at(0);
|
||||
require le_magic == 0x564B4547 || le_magic == 0x47454B56;
|
||||
}
|
||||
|
||||
// Save original filename for display
|
||||
original_name = _name;
|
||||
|
||||
// Detect platform from extension
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
// Detect endianness from magic
|
||||
// GEKV (0x47454B56 in file) reads as 0x564B4547 in LE mode = PC
|
||||
// VKEG (0x564B4547 in file) reads as 0x47454B56 in LE mode = Xbox/PS3
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0x47454B56) {
|
||||
_is_big_endian = 1;
|
||||
set_display("PEG Texture (Big Endian)");
|
||||
// Big-endian (Xbox 360 / PS3)
|
||||
if (is_ps3) {
|
||||
set_name(original_name + " (PS3)");
|
||||
} else {
|
||||
set_name(original_name + " (Xbox 360)");
|
||||
}
|
||||
parse_here("volition_peg_be");
|
||||
} else {
|
||||
_is_big_endian = 0;
|
||||
// Little-endian (PC)
|
||||
set_name(original_name + " (PC)");
|
||||
parse_here("volition_peg_impl");
|
||||
}
|
||||
}
|
||||
|
||||
// Big-Endian wrapper for Xbox 360 / PS3
|
||||
type volition_peg_be ui("PEG Texture") byteorder BE
|
||||
{
|
||||
parse_here("volition_peg_impl");
|
||||
}
|
||||
|
||||
// Shared implementation - inherits byteorder from caller
|
||||
type volition_peg_impl ui("PEG Texture")
|
||||
{
|
||||
magic = hex(read(4)) ui("Magic");
|
||||
u16 version ui("Version");
|
||||
u16 platform ui("Platform");
|
||||
|
||||
if (version == 10) {
|
||||
parse_here("volition_peg_v10");
|
||||
parse_here("volition_peg_v10_body");
|
||||
} else if (version == 13) {
|
||||
parse_here("volition_peg_v13");
|
||||
parse_here("volition_peg_v13_body");
|
||||
} else {
|
||||
unknown_version = version ui("Unsupported Version");
|
||||
set_preview("peg_data.bin", read(size() - pos()));
|
||||
@ -37,8 +69,38 @@ type volition_peg ui("Volition PEG Texture", root) byteorder LE
|
||||
}
|
||||
}
|
||||
|
||||
// PEG Version 10 - Saints Row 2
|
||||
type volition_peg_v10 ui("PEG V10") byteorder LE
|
||||
// Helper function to get format name (shared by all versions)
|
||||
// Includes both PC format codes (400-410) and Xbox 360 format codes (0x52-0x86)
|
||||
type volition_peg_format_helper ui("Format Helper")
|
||||
{
|
||||
f_format = ctx_get("_peg_format_code");
|
||||
|
||||
// PC format codes (decimal 400-410)
|
||||
if (f_format == 400) { format_name = "DXT1"; }
|
||||
else if (f_format == 401) { format_name = "DXT3"; }
|
||||
else if (f_format == 402) { format_name = "DXT5"; }
|
||||
else if (f_format == 403) { format_name = "R5G6B5"; }
|
||||
else if (f_format == 404) { format_name = "A1R5G5B5"; }
|
||||
else if (f_format == 405) { format_name = "A4R4G4B4"; }
|
||||
else if (f_format == 406) { format_name = "R8G8B8"; }
|
||||
else if (f_format == 407) { format_name = "A8R8G8B8"; }
|
||||
else if (f_format == 408) { format_name = "V8U8"; }
|
||||
else if (f_format == 409) { format_name = "CxV8U8"; }
|
||||
else if (f_format == 410) { format_name = "A8"; }
|
||||
else if (f_format == 603) { format_name = "Unknown_603"; }
|
||||
// Xbox 360 format codes (hex 0x52-0x86)
|
||||
else if (f_format == 0x52) { format_name = "DXT1 (X360)"; }
|
||||
else if (f_format == 0x53) { format_name = "DXT3 (X360)"; }
|
||||
else if (f_format == 0x54) { format_name = "DXT5 (X360)"; }
|
||||
else if (f_format == 0x71) { format_name = "X8R8G8B8 (X360)"; }
|
||||
else if (f_format == 0x86) { format_name = "A8R8G8B8 (X360)"; }
|
||||
else { format_name = "Unknown_" + f_format; }
|
||||
|
||||
ctx_set("_peg_format_name", format_name);
|
||||
}
|
||||
|
||||
// PEG Version 10 body - Saints Row 2 (inherits byteorder)
|
||||
type volition_peg_v10_body ui("PEG V10")
|
||||
{
|
||||
set_name("PEG Texture (V10)");
|
||||
|
||||
@ -78,20 +140,10 @@ type volition_peg_v10 ui("PEG V10") byteorder LE
|
||||
ctx_set("_peg_frame_size", f_size);
|
||||
ctx_set("_peg_frame_flags", f_flags);
|
||||
|
||||
// Determine format name
|
||||
if (f_format == 400) { format_name = "DXT1"; }
|
||||
else if (f_format == 401) { format_name = "DXT3"; }
|
||||
else if (f_format == 402) { format_name = "DXT5"; }
|
||||
else if (f_format == 403) { format_name = "R5G6B5"; }
|
||||
else if (f_format == 404) { format_name = "A1R5G5B5"; }
|
||||
else if (f_format == 405) { format_name = "A4R4G4B4"; }
|
||||
else if (f_format == 406) { format_name = "R8G8B8"; }
|
||||
else if (f_format == 407) { format_name = "A8R8G8B8"; }
|
||||
else if (f_format == 408) { format_name = "V8U8"; }
|
||||
else if (f_format == 409) { format_name = "CxV8U8"; }
|
||||
else if (f_format == 410) { format_name = "A8"; }
|
||||
else if (f_format == 603) { format_name = "Unknown_603"; }
|
||||
else { format_name = "Unknown"; }
|
||||
// Get format name using shared helper
|
||||
ctx_set("_peg_format_code", f_format);
|
||||
_dummy = bytesat(0, 1) |> parse volition_peg_format_helper;
|
||||
format_name = ctx_get("_peg_format_name");
|
||||
ctx_set("_peg_frame_format_name", format_name);
|
||||
|
||||
dummy = bytesat(0, 1);
|
||||
@ -111,13 +163,71 @@ type volition_peg_v10 ui("PEG V10") byteorder LE
|
||||
|
||||
ui_table("frames", "frames", "Index,Width,Height,Format,Size,Flags");
|
||||
|
||||
// Extract textures from paired GPEG if available
|
||||
has_gpeg = ctx_get("_peg_has_gpeg");
|
||||
if (has_gpeg == 1) {
|
||||
gpeg_data = ctx_get("_peg_gpeg_data");
|
||||
gpeg_len = len(gpeg_data);
|
||||
|
||||
// Initialize texture index counter
|
||||
ctx_set("_peg_tex_idx", 0);
|
||||
|
||||
// Re-parse frames to extract texture data
|
||||
seek(20); // Skip back to frame table (after 8-byte header + 12 bytes we read)
|
||||
tex_idx = 0;
|
||||
repeat(frame_count) {
|
||||
u32 tex_data_offset;
|
||||
u16 tex_width;
|
||||
u16 tex_height;
|
||||
u32 tex_format;
|
||||
skip(4); // unknown0C
|
||||
skip(4); // frame_count + flags
|
||||
skip(4); // name_offset
|
||||
skip(4); // unknown18
|
||||
u32 tex_size;
|
||||
skip(12); // remaining unknowns
|
||||
|
||||
// Extract texture data from GPEG
|
||||
if (tex_data_offset + tex_size <= gpeg_len && tex_size > 0) {
|
||||
// Get texture name
|
||||
tex_name = ctx_get("_peg_tex_name_" + tex_idx);
|
||||
if (len(tex_name) == 0) {
|
||||
tex_name = "texture_" + tex_idx;
|
||||
}
|
||||
|
||||
// Get format name
|
||||
ctx_set("_peg_format_code", tex_format);
|
||||
_dummy2 = bytesat(0, 1) |> parse volition_peg_format_helper;
|
||||
tex_format_name = ctx_get("_peg_format_name");
|
||||
|
||||
// Store texture info in context for extraction
|
||||
ctx_set("_peg_extract_offset", tex_data_offset);
|
||||
ctx_set("_peg_extract_size", tex_size);
|
||||
ctx_set("_peg_extract_name", tex_name);
|
||||
ctx_set("_peg_extract_width", tex_width);
|
||||
ctx_set("_peg_extract_height", tex_height);
|
||||
ctx_set("_peg_extract_format", tex_format_name);
|
||||
|
||||
// Parse GPEG data to extract texture - the extractor reads from gpeg_data stream
|
||||
tex_child = gpeg_data |> parse volition_peg_texture_extractor;
|
||||
push("extracted_textures", tex_child);
|
||||
}
|
||||
|
||||
tex_idx = tex_idx + 1;
|
||||
}
|
||||
|
||||
gpeg_paired = "Yes (" + gpeg_len + " bytes)" ui("GPEG Paired");
|
||||
} else {
|
||||
gpeg_paired = "No" ui("GPEG Paired");
|
||||
}
|
||||
|
||||
total_frames = frame_count ui("Total Frames");
|
||||
total_textures = texture_count ui("Total Textures");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// PEG Version 13 - Red Faction: Armageddon
|
||||
type volition_peg_v13 ui("PEG V13") byteorder LE
|
||||
// PEG Version 13 body - Red Faction: Armageddon (inherits byteorder)
|
||||
type volition_peg_v13_body ui("PEG V13")
|
||||
{
|
||||
set_name("PEG Texture (V13)");
|
||||
|
||||
@ -156,20 +266,10 @@ type volition_peg_v13 ui("PEG V13") byteorder LE
|
||||
ctx_set("_peg_frame_size", f_size);
|
||||
ctx_set("_peg_frame_flags", f_flags);
|
||||
|
||||
// Determine format name
|
||||
if (f_format == 400) { format_name = "DXT1"; }
|
||||
else if (f_format == 401) { format_name = "DXT3"; }
|
||||
else if (f_format == 402) { format_name = "DXT5"; }
|
||||
else if (f_format == 403) { format_name = "R5G6B5"; }
|
||||
else if (f_format == 404) { format_name = "A1R5G5B5"; }
|
||||
else if (f_format == 405) { format_name = "A4R4G4B4"; }
|
||||
else if (f_format == 406) { format_name = "R8G8B8"; }
|
||||
else if (f_format == 407) { format_name = "A8R8G8B8"; }
|
||||
else if (f_format == 408) { format_name = "V8U8"; }
|
||||
else if (f_format == 409) { format_name = "CxV8U8"; }
|
||||
else if (f_format == 410) { format_name = "A8"; }
|
||||
else if (f_format == 603) { format_name = "Unknown_603"; }
|
||||
else { format_name = "Unknown"; }
|
||||
// Get format name using shared helper
|
||||
ctx_set("_peg_format_code", f_format);
|
||||
_dummy = bytesat(0, 1) |> parse volition_peg_format_helper;
|
||||
format_name = ctx_get("_peg_format_name");
|
||||
ctx_set("_peg_frame_format_name", format_name);
|
||||
|
||||
dummy = bytesat(0, 1);
|
||||
@ -189,13 +289,66 @@ type volition_peg_v13 ui("PEG V13") byteorder LE
|
||||
|
||||
ui_table("frames", "frames", "Index,Width,Height,Format,Size,Flags");
|
||||
|
||||
// Extract textures from paired GPEG if available
|
||||
has_gpeg = ctx_get("_peg_has_gpeg");
|
||||
if (has_gpeg == 1) {
|
||||
gpeg_data = ctx_get("_peg_gpeg_data");
|
||||
gpeg_len = len(gpeg_data);
|
||||
|
||||
// Initialize texture index counter
|
||||
ctx_set("_peg_tex_idx", 0);
|
||||
|
||||
// Re-parse frames to extract texture data
|
||||
seek(20); // Skip back to frame table
|
||||
tex_idx = 0;
|
||||
repeat(frame_count) {
|
||||
u32 tex_data_offset;
|
||||
u16 tex_width;
|
||||
u16 tex_height;
|
||||
u32 tex_format;
|
||||
skip(4); // unknown0C
|
||||
skip(4); // frame_count + flags
|
||||
skip(4); // name_offset
|
||||
skip(4); // unknown18
|
||||
u32 tex_size;
|
||||
skip(12); // remaining unknowns
|
||||
|
||||
if (tex_data_offset + tex_size <= gpeg_len && tex_size > 0) {
|
||||
tex_name = ctx_get("_peg_tex_name_" + tex_idx);
|
||||
if (len(tex_name) == 0) {
|
||||
tex_name = "texture_" + tex_idx;
|
||||
}
|
||||
|
||||
ctx_set("_peg_format_code", tex_format);
|
||||
_dummy2 = bytesat(0, 1) |> parse volition_peg_format_helper;
|
||||
tex_format_name = ctx_get("_peg_format_name");
|
||||
|
||||
ctx_set("_peg_extract_offset", tex_data_offset);
|
||||
ctx_set("_peg_extract_size", tex_size);
|
||||
ctx_set("_peg_extract_name", tex_name);
|
||||
ctx_set("_peg_extract_width", tex_width);
|
||||
ctx_set("_peg_extract_height", tex_height);
|
||||
ctx_set("_peg_extract_format", tex_format_name);
|
||||
|
||||
tex_child = gpeg_data |> parse volition_peg_texture_extractor;
|
||||
push("extracted_textures", tex_child);
|
||||
}
|
||||
|
||||
tex_idx = tex_idx + 1;
|
||||
}
|
||||
|
||||
gpeg_paired = "Yes (" + gpeg_len + " bytes)" ui("GPEG Paired");
|
||||
} else {
|
||||
gpeg_paired = "No" ui("GPEG Paired");
|
||||
}
|
||||
|
||||
total_frames = frame_count ui("Total Frames");
|
||||
total_textures = texture_count ui("Total Textures");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Frame row for table display
|
||||
type volition_peg_frame_row ui("Frame") byteorder LE
|
||||
// Frame row for table display (inherits byteorder, but only uses context)
|
||||
type volition_peg_frame_row ui("Frame")
|
||||
{
|
||||
set_hidden();
|
||||
Index = ctx_get("_peg_frame_idx");
|
||||
@ -204,4 +357,45 @@ type volition_peg_frame_row ui("Frame") byteorder LE
|
||||
Format = ctx_get("_peg_frame_format_name");
|
||||
Size = ctx_get("_peg_frame_size");
|
||||
Flags = ctx_get("_peg_frame_flags");
|
||||
set_name(Format + " " + Width + "x" + Height);
|
||||
}
|
||||
|
||||
// Texture extractor - reads from GPEG data stream using context info
|
||||
// This type is parsed with GPEG data as the stream, not CPEG
|
||||
type volition_peg_texture_extractor ui("Texture")
|
||||
{
|
||||
// Get texture info from context (set by v10/v13 body before calling)
|
||||
tex_offset = ctx_get("_peg_extract_offset");
|
||||
tex_size = ctx_get("_peg_extract_size");
|
||||
tex_name = ctx_get("_peg_extract_name");
|
||||
tex_width = ctx_get("_peg_extract_width");
|
||||
tex_height = ctx_get("_peg_extract_height");
|
||||
tex_format = ctx_get("_peg_extract_format");
|
||||
|
||||
set_name(tex_name);
|
||||
|
||||
// Extract texture data from GPEG stream (this type receives GPEG data)
|
||||
seek(tex_offset);
|
||||
tex_data = read(tex_size);
|
||||
|
||||
// Display texture metadata
|
||||
width = tex_width ui("Width");
|
||||
height = tex_height ui("Height");
|
||||
format = tex_format ui("Format");
|
||||
data_size = len(tex_data) ui("Data Size");
|
||||
offset = tex_offset ui("GPEG Offset");
|
||||
|
||||
// Determine if this is a DXT format that can be previewed
|
||||
is_dxt = contains(tex_format, "DXT");
|
||||
|
||||
if (is_dxt && len(tex_data) > 0) {
|
||||
// Set preview with raw DXT data
|
||||
// The image widget will attempt to decode based on extension
|
||||
set_preview(tex_name + ".dxt", tex_data);
|
||||
set_viewer("image");
|
||||
} else {
|
||||
// Non-DXT format - show as hex
|
||||
set_preview(tex_name + ".bin", tex_data);
|
||||
set_viewer("hex");
|
||||
}
|
||||
}
|
||||
|
||||
126
definitions/volition/rig.xscript
Normal file
126
definitions/volition/rig.xscript
Normal file
@ -0,0 +1,126 @@
|
||||
// Volition Rig/Skeleton Format
|
||||
// Used by: Saints Row 3/4, Red Faction series
|
||||
// Contains bone hierarchy for character animation
|
||||
|
||||
type volition_rig ui("Volition Rig", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "rig_pc" || _ext == "rig_xbox2" || _ext == "rig_ps3";
|
||||
// Rig files typically have reasonable bone counts (1-200)
|
||||
bone_count_check = u32at(0);
|
||||
require bone_count_check > 0 && bone_count_check < 500;
|
||||
}
|
||||
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox) {
|
||||
set_name("Rig (Xbox 360)");
|
||||
parse_here("volition_rig_be");
|
||||
} else if (is_ps3) {
|
||||
set_name("Rig (PS3)");
|
||||
parse_here("volition_rig_be");
|
||||
} else {
|
||||
set_name("Rig (PC)");
|
||||
parse_here("volition_rig_impl");
|
||||
}
|
||||
}
|
||||
|
||||
type volition_rig_be ui("Rig") byteorder BE
|
||||
{
|
||||
parse_here("volition_rig_impl");
|
||||
}
|
||||
|
||||
type volition_rig_impl ui("Rig")
|
||||
{
|
||||
// Rig header - 36 bytes of padding/reserved data
|
||||
skip(36);
|
||||
|
||||
u32 bone_count ui("Bone Count");
|
||||
u32 rig_flags ui("Flags");
|
||||
u32 rig_value ui("Value");
|
||||
|
||||
// Parse bone data
|
||||
if (bone_count > 0 && bone_count < 500) {
|
||||
bone_idx = 0;
|
||||
repeat(bone_count) {
|
||||
i16 parent_id;
|
||||
skip(2); // padding
|
||||
f32 pos_x;
|
||||
f32 pos_y;
|
||||
f32 pos_z;
|
||||
f32 rot_x;
|
||||
f32 rot_y;
|
||||
f32 rot_z;
|
||||
f32 rot_w;
|
||||
|
||||
ctx_set("_rig_bone_idx", bone_idx);
|
||||
ctx_set("_rig_parent_id", parent_id);
|
||||
ctx_set("_rig_pos", pos_x + ", " + pos_y + ", " + pos_z);
|
||||
bone_entry = bytesat(0, 1) |> parse volition_rig_bone;
|
||||
push("bones", bone_entry);
|
||||
bone_entry = 0;
|
||||
|
||||
bone_idx = bone_idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Bone names at end (null-terminated strings)
|
||||
names_start = pos();
|
||||
remaining = size() - pos();
|
||||
if (remaining > 0) {
|
||||
name_idx = 0;
|
||||
while (pos() < size() && name_idx < bone_count) {
|
||||
bone_name = cstring();
|
||||
if (len(bone_name) > 0) {
|
||||
ctx_set("_rig_name_idx", name_idx);
|
||||
ctx_set("_rig_bone_name", bone_name);
|
||||
name_entry = bytesat(0, 1) |> parse volition_rig_bone_name;
|
||||
push("bone_names", name_entry);
|
||||
name_entry = 0;
|
||||
name_idx = name_idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bones = bone_count ui("Total Bones");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
type volition_rig_bone ui("Bone")
|
||||
{
|
||||
idx = ctx_get("_rig_bone_idx");
|
||||
parent = ctx_get("_rig_parent_id");
|
||||
position = ctx_get("_rig_pos");
|
||||
|
||||
set_name("Bone " + idx);
|
||||
index = idx ui("Index");
|
||||
parent_id = parent ui("Parent ID");
|
||||
pos_display = position ui("Position");
|
||||
}
|
||||
|
||||
type volition_rig_bone_name ui("Bone Name")
|
||||
{
|
||||
idx = ctx_get("_rig_name_idx");
|
||||
name = ctx_get("_rig_bone_name");
|
||||
set_name(name);
|
||||
index = idx ui("Index");
|
||||
bone_name = name ui("Name");
|
||||
}
|
||||
|
||||
// Nested type for VPP parsing
|
||||
type volition_rig_nested ui("Rig") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
|
||||
// Use truthiness check instead of == 1 for boolean context values
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_rig_be");
|
||||
} else {
|
||||
parse_here("volition_rig_impl");
|
||||
}
|
||||
}
|
||||
71
definitions/volition/sim.xscript
Normal file
71
definitions/volition/sim.xscript
Normal file
@ -0,0 +1,71 @@
|
||||
// Volition Cloth Simulation Format (.sim_pc)
|
||||
// Used by: Saints Row 3/4
|
||||
// Contains cloth physics parameters and node data
|
||||
|
||||
type volition_sim ui("Cloth Simulation", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "sim_pc" || _ext == "sim_xbox2" || _ext == "sim_ps3";
|
||||
// Version should be 2 (SRTT/SRIV) or 5 (GOOH)
|
||||
ver = i32at(0);
|
||||
require ver == 2 || ver == 5;
|
||||
}
|
||||
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
if (is_xbox || is_ps3) {
|
||||
set_name("Cloth Simulation (Console)");
|
||||
parse_here("volition_sim_be");
|
||||
} else {
|
||||
set_name("Cloth Simulation (PC)");
|
||||
parse_here("volition_sim_impl");
|
||||
}
|
||||
}
|
||||
|
||||
type volition_sim_be ui("Cloth Simulation") byteorder BE
|
||||
{
|
||||
parse_here("volition_sim_impl");
|
||||
}
|
||||
|
||||
type volition_sim_impl ui("Cloth Simulation")
|
||||
{
|
||||
i32 version ui("Version");
|
||||
i32 data_size ui("Data Size");
|
||||
|
||||
// 28-byte name
|
||||
sim_name = ascii(read(28));
|
||||
// Trim null bytes
|
||||
name_display = trim(sim_name) ui("Name");
|
||||
|
||||
i32 num_passes ui("Simulation Passes");
|
||||
f32 air_resistance ui("Air Resistance");
|
||||
f32 wind_multiplier ui("Wind Multiplier");
|
||||
f32 wind_const ui("Wind Constant");
|
||||
f32 gravity_multiplier ui("Gravity Multiplier");
|
||||
f32 obj_velocity_inherit ui("Velocity Inheritance");
|
||||
f32 obj_position_inherit ui("Position Inheritance");
|
||||
f32 obj_rotation_inherit ui("Rotation Inheritance");
|
||||
i32 wind_type ui("Wind Type");
|
||||
i32 num_nodes ui("Node Count");
|
||||
i32 num_anchor_nodes ui("Anchor Node Count");
|
||||
i32 num_node_links ui("Node Link Count");
|
||||
i32 num_ropes ui("Rope Count");
|
||||
i32 num_colliders ui("Collider Count");
|
||||
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Nested type for VPP
|
||||
type volition_sim_nested ui("Cloth Simulation") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
is_xbox = ctx_get("_vpp_is_xbox");
|
||||
is_ps3 = ctx_get("_vpp_is_ps3");
|
||||
if (is_xbox || is_ps3) {
|
||||
parse_here("volition_sim_be");
|
||||
} else {
|
||||
parse_here("volition_sim_impl");
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,10 @@ type volition_vpp ui("VPP Archives", root) byteorder LE
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
// Store platform in context for child file parsing
|
||||
ctx_set("_vpp_is_xbox", is_xbox);
|
||||
ctx_set("_vpp_is_ps3", is_ps3);
|
||||
|
||||
// Detect endianness from magic
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0xCE0A8951) {
|
||||
@ -52,12 +56,17 @@ type volition_vpp_impl ui("VPP Archives")
|
||||
u32 magic ui("Magic");
|
||||
u32 version ui("Version");
|
||||
|
||||
file_size_total = size() ui("File Size");
|
||||
|
||||
if (version == 3) {
|
||||
parse_here("volition_vpp_v3");
|
||||
} else if (version == 4) {
|
||||
parse_here("volition_vpp_v4");
|
||||
} else if (version == 6) {
|
||||
parse_here("volition_vpp_v6");
|
||||
} else if (version == 10) {
|
||||
// Version 10 - Saints Row IV, Agents of Mayhem
|
||||
parse_here("volition_vpp_v10");
|
||||
} else {
|
||||
unknown_version = version ui("Unsupported Version");
|
||||
set_preview("vpp_data.bin", read(size() - pos()));
|
||||
@ -112,10 +121,15 @@ type volition_vpp_v3 ui("VPP Archives")
|
||||
|
||||
saved_pos = pos();
|
||||
|
||||
// Read name from names section
|
||||
// Read name from names section (with bounds check)
|
||||
names_offset = dir_offset + dir_size;
|
||||
seek(names_offset + e_name_offset);
|
||||
entry_name = cstring();
|
||||
name_offset_abs = names_offset + e_name_offset;
|
||||
if (name_offset_abs < size()) {
|
||||
seek(name_offset_abs);
|
||||
entry_name = cstring();
|
||||
} else {
|
||||
entry_name = "[Invalid: offset beyond EOF]";
|
||||
}
|
||||
ctx_set("_vpp_entry_name", entry_name);
|
||||
|
||||
seek(saved_pos);
|
||||
@ -175,10 +189,15 @@ type volition_vpp_v4 ui("VPP Archives")
|
||||
|
||||
saved_pos = pos();
|
||||
|
||||
// Read name from names section
|
||||
// Read name from names section (with bounds check)
|
||||
names_offset = dir_offset + dir_size;
|
||||
seek(names_offset + e_name_offset);
|
||||
entry_name = cstring();
|
||||
name_offset_abs = names_offset + e_name_offset;
|
||||
if (name_offset_abs < size()) {
|
||||
seek(name_offset_abs);
|
||||
entry_name = cstring();
|
||||
} else {
|
||||
entry_name = "[Invalid: offset beyond EOF]";
|
||||
}
|
||||
ctx_set("_vpp_entry_name", entry_name);
|
||||
|
||||
seek(saved_pos);
|
||||
@ -283,9 +302,22 @@ type volition_vpp_v6 ui("VPP Archives")
|
||||
data_offset_base = ((data_offset_base + 2047) / 2048) * 2048;
|
||||
data_base_display = data_offset_base ui("Data Section Base");
|
||||
|
||||
// Parse directory and create file children
|
||||
// Check nesting depth (to prevent deep recursion in nested VPPs)
|
||||
vpp_depth = ctx_get("_vpp_depth");
|
||||
if (vpp_depth == 0) { vpp_depth = 0; }
|
||||
|
||||
// PASS 1: GPEG collection disabled for now (causes issues with nested VPPs)
|
||||
// TODO: Re-enable once nested parsing is stable
|
||||
|
||||
// PASS 2: Parse directory and create file children (V6)
|
||||
seek(dir_offset);
|
||||
skip_tree("entries");
|
||||
entry_idx = 0;
|
||||
|
||||
// Track cumulative compressed offset for STR2 containers where files are individually compressed
|
||||
// In such cases, e_data_offset is virtual (based on uncompressed sizes), but data stores compressed files
|
||||
compressed_offset = 0;
|
||||
|
||||
repeat(dir_count) {
|
||||
u32 e_name_offset;
|
||||
u32 e_runtime_offset;
|
||||
@ -296,34 +328,67 @@ type volition_vpp_v6 ui("VPP Archives")
|
||||
|
||||
saved_pos = pos();
|
||||
|
||||
// Read filename
|
||||
seek(names_offset + e_name_offset);
|
||||
entry_name = cstring();
|
||||
|
||||
// Determine actual size to read
|
||||
if (e_compressed_size == 0xFFFFFFFF) {
|
||||
read_size = e_uncompressed_size;
|
||||
// Read filename (with bounds check)
|
||||
name_offset_abs = names_offset + e_name_offset;
|
||||
if (name_offset_abs < size()) {
|
||||
seek(name_offset_abs);
|
||||
entry_name = cstring();
|
||||
} else {
|
||||
read_size = e_compressed_size;
|
||||
entry_name = "[Invalid: offset beyond EOF]";
|
||||
}
|
||||
|
||||
// Read file data and create child
|
||||
file_offset = data_offset_base + e_data_offset;
|
||||
if (file_offset + read_size <= size() && read_size > 0) {
|
||||
seek(file_offset);
|
||||
file_data = read(read_size);
|
||||
// Always create entry row for table display
|
||||
ctx_set("_vpp_entry_name", entry_name);
|
||||
ctx_set("_vpp_entry_data_off", e_data_offset);
|
||||
ctx_set("_vpp_entry_size", e_uncompressed_size);
|
||||
ctx_set("_vpp_entry_csize", e_compressed_size);
|
||||
ctx_set("_vpp_entry_idx", entry_idx);
|
||||
|
||||
ctx_set("_vpp_file_name", entry_name);
|
||||
ctx_set("_vpp_file_usize", e_uncompressed_size);
|
||||
ctx_set("_vpp_file_csize", e_compressed_size);
|
||||
file_child = file_data |> parse volition_vpp_file;
|
||||
push("files", file_child);
|
||||
dummy = bytesat(0, 1);
|
||||
entry_child = dummy |> parse volition_vpp_entry_row;
|
||||
push("entries", entry_child);
|
||||
|
||||
// Parse file children at depth 0 (top-level VPP) and depth 1 (STR2 inside VPP)
|
||||
// Stop at depth 2+ to prevent infinite recursion
|
||||
if (vpp_depth <= 1) {
|
||||
// Determine actual size to read and file position
|
||||
// Two cases:
|
||||
// 1. Uncompressed files (csize == 0xFFFFFFFF): e_data_offset is actual offset, use directly
|
||||
// 2. Compressed files (csize < usize): e_data_offset is virtual, use cumulative compressed offset
|
||||
if (e_compressed_size == 0xFFFFFFFF) {
|
||||
// Uncompressed file - e_data_offset is the actual position
|
||||
read_size = e_uncompressed_size;
|
||||
actual_csize = e_compressed_size;
|
||||
file_offset = data_offset_base + e_data_offset;
|
||||
} else {
|
||||
// Compressed file - files are packed consecutively by compressed size
|
||||
// e_data_offset is virtual (uncompressed layout), use cumulative compressed offset
|
||||
read_size = e_compressed_size;
|
||||
actual_csize = e_compressed_size;
|
||||
file_offset = data_offset_base + compressed_offset;
|
||||
compressed_offset = compressed_offset + e_compressed_size;
|
||||
}
|
||||
|
||||
// Read file data and create child
|
||||
if (file_offset + read_size <= size() && read_size > 0) {
|
||||
seek(file_offset);
|
||||
file_data = read(read_size);
|
||||
|
||||
ctx_set("_vpp_file_name", entry_name);
|
||||
ctx_set("_vpp_file_usize", e_uncompressed_size);
|
||||
ctx_set("_vpp_file_csize", actual_csize);
|
||||
file_child = file_data |> parse volition_vpp_file;
|
||||
push("files", file_child);
|
||||
file_child = 0; // Clear to avoid duplicate (last file)
|
||||
}
|
||||
}
|
||||
|
||||
seek(saved_pos);
|
||||
entry_idx = entry_idx + 1;
|
||||
}
|
||||
|
||||
ui_table("entries", "entries", "Index,Name,Offset,Size,CompSize");
|
||||
|
||||
total_entries = dir_count ui("Total Entries");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
@ -334,6 +399,7 @@ type volition_vpp_entry_row ui("Entry")
|
||||
set_hidden();
|
||||
Index = ctx_get("_vpp_entry_idx");
|
||||
Name = ctx_get("_vpp_entry_name");
|
||||
set_name(Name);
|
||||
Offset = ctx_get("_vpp_entry_data_off");
|
||||
Size = ctx_get("_vpp_entry_size");
|
||||
CompSize = ctx_get("_vpp_entry_csize");
|
||||
@ -348,35 +414,93 @@ type volition_vpp_file ui("VPP File") byteorder LE
|
||||
usize = ctx_get("_vpp_file_usize");
|
||||
csize = ctx_get("_vpp_file_csize");
|
||||
|
||||
raw_data = read(size());
|
||||
// Read first 8 bytes to check for SR compression header before consuming stream
|
||||
total_size = size();
|
||||
first_byte = u8at(0);
|
||||
second_byte = u8at(1);
|
||||
third_byte = u8at(2);
|
||||
fourth_byte = u8at(3);
|
||||
|
||||
// Check for Saints Row header: bytes 4-7 = uncompressed size (BE)
|
||||
sr_header_usize = 0;
|
||||
if (total_size >= 8) {
|
||||
sr_header_usize = (u8at(4) * 16777216) + (u8at(5) * 65536) + (u8at(6) * 256) + u8at(7);
|
||||
}
|
||||
|
||||
raw_data = read(total_size);
|
||||
|
||||
// Determine if decompression is needed
|
||||
is_compressed = (csize != 0xFFFFFFFF) && (csize < usize) && (size() > 0);
|
||||
is_compressed = (csize != 0xFFFFFFFF) && (csize < usize) && (total_size > 0);
|
||||
|
||||
if (is_compressed) {
|
||||
// Check first byte for compression format
|
||||
// Check magic bytes for compression format
|
||||
first_byte = u8at(0);
|
||||
second_byte = u8at(1);
|
||||
third_byte = u8at(2);
|
||||
fourth_byte = u8at(3);
|
||||
|
||||
if (first_byte == 0x78) {
|
||||
// Standard zlib compression (78 xx header)
|
||||
file_data = raw_data |> zlib;
|
||||
compression_used = "ZLIB" ui("Compression");
|
||||
} else if (first_byte == 0xFF) {
|
||||
// Xbox 360 VPP: header is FF + u16be usize, then data
|
||||
payload = bytesat(3, size() - 3);
|
||||
file_data = payload |> xmem;
|
||||
compression_used = "XMem (Xbox)" ui("Compression");
|
||||
} else if (first_byte == 0x0F) {
|
||||
// XMem/LZX format (0x0FF512xx headers)
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else if (first_byte == 0x0F && second_byte == 0xF5 && third_byte == 0x12 &&
|
||||
(fourth_byte == 0xEE || fourth_byte == 0xED)) {
|
||||
// XMem/LZX with proper header (LZXNATIVE=0x0FF512EE or LZXTDECODE=0x0FF512ED)
|
||||
file_data = raw_data |> xmem;
|
||||
compression_used = "XMem" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else if (first_byte == 0x00 && total_size >= 8 && sr_header_usize == usize) {
|
||||
// Saints Row custom compression header format:
|
||||
// Bytes 0-3: Header with compressed payload size
|
||||
// Bytes 4-7: Uncompressed size (BE u32) - already checked as sr_header_usize
|
||||
// Bytes 8+: LZO compressed payload
|
||||
// Extract payload (skip 8-byte header)
|
||||
payload = bytesof(raw_data, 8, total_size - 8);
|
||||
file_data = lzo(payload, usize);
|
||||
if (len(file_data) == usize) {
|
||||
compression_used = "LZO (SR header)" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
// LZO failed - use raw data
|
||||
file_data = raw_data;
|
||||
compression_used = "Unknown (raw)" ui("Compression");
|
||||
}
|
||||
} else if (first_byte == 0x00 && total_size >= 8) {
|
||||
// First byte is 0 but header doesn't match expected size - try other decompression
|
||||
file_data = raw_data |> zlib_auto;
|
||||
if (len(file_data) > 0 && len(file_data) >= usize / 2) {
|
||||
compression_used = "ZLIB (auto)" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
file_data = lzo(raw_data, usize);
|
||||
if (len(file_data) == usize) {
|
||||
compression_used = "LZO" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
file_data = raw_data;
|
||||
compression_used = "Unknown (raw)" ui("Compression");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try raw deflate
|
||||
file_data = raw_data |> deflate;
|
||||
compression_used = "Deflate" ui("Compression");
|
||||
// Unknown compression format - try zlib auto-detect first
|
||||
file_data = raw_data |> zlib_auto;
|
||||
if (len(file_data) > 0 && len(file_data) >= usize / 2) {
|
||||
compression_used = "ZLIB (auto)" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
// ZLIB failed - try LZO (commonly used by Saints Row PS3/Xbox)
|
||||
file_data = lzo(raw_data, usize);
|
||||
if (len(file_data) == usize) {
|
||||
compression_used = "LZO" ui("Compression");
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
// LZO failed too - use raw data
|
||||
file_data = raw_data;
|
||||
compression_used = "Unknown (raw)" ui("Compression");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decompressed_size = len(file_data) ui("Decompressed Size");
|
||||
} else {
|
||||
file_data = raw_data;
|
||||
}
|
||||
@ -396,5 +520,450 @@ type volition_vpp_file ui("VPP File") byteorder LE
|
||||
set_text(ascii(file_data));
|
||||
}
|
||||
|
||||
// Check for ASM manifest files
|
||||
is_asm = ends_with(file_name, ".asm_pc") ||
|
||||
ends_with(file_name, ".asm_xbox2") ||
|
||||
ends_with(file_name, ".asm_ps3");
|
||||
|
||||
if (is_asm && len(file_data) > 8) {
|
||||
// Check for ASM magic (0xBEEFFEED LE or 0xEDFEEFBE BE)
|
||||
asm_magic = u32at(0);
|
||||
if (asm_magic == 0xBEEFFEED || asm_magic == 0xEDFEEFBE) {
|
||||
set_display("ASM Manifest");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
asm_child = file_data |> parse volition_asm_nested;
|
||||
push("manifests", asm_child);
|
||||
asm_child = 0; // Clear to avoid duplicate
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested STR2 archives (streaming containers)
|
||||
is_str2 = ends_with(file_name, ".str2_pc") ||
|
||||
ends_with(file_name, ".str2_xbox2") ||
|
||||
ends_with(file_name, ".str2_ps3");
|
||||
|
||||
if (is_str2 && len(file_data) > 8) {
|
||||
str2_magic = u32at(0);
|
||||
if (str2_magic == 0x51890ACE || str2_magic == 0xCE0A8951) {
|
||||
set_display("STR2 Container");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
nested_vpp = file_data |> parse volition_vpp_nested;
|
||||
push("nested", nested_vpp);
|
||||
nested_vpp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for CPEG/GPEG/CVBM/GVBM texture files
|
||||
// CPEG/CVBM = CPU texture header, GPEG/GVBM = GPU pixel data
|
||||
is_cpeg = ends_with(file_name, ".cpeg_pc") || ends_with(file_name, ".cpeg_xbox2") ||
|
||||
ends_with(file_name, ".cpeg_ps3") || ends_with(file_name, ".peg_pc") ||
|
||||
ends_with(file_name, ".cvbm_pc") || ends_with(file_name, ".cvbm_xbox2") ||
|
||||
ends_with(file_name, ".cvbm_ps3");
|
||||
|
||||
is_gpeg = ends_with(file_name, ".gpeg_pc") || ends_with(file_name, ".gpeg_xbox2") ||
|
||||
ends_with(file_name, ".gpeg_ps3") || ends_with(file_name, ".g_peg_pc") ||
|
||||
ends_with(file_name, ".gvbm_pc") || ends_with(file_name, ".gvbm_xbox2") ||
|
||||
ends_with(file_name, ".gvbm_ps3");
|
||||
|
||||
if (is_cpeg && len(file_data) > 8) {
|
||||
// Check for PEG magic (GEKV or VKEG)
|
||||
peg_magic = u32at(0);
|
||||
if (peg_magic == 0x564B4547 || peg_magic == 0x47454B56) {
|
||||
set_display("PEG Texture");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
|
||||
// Look up paired GPEG data (stored in context by VPP parser pass 1)
|
||||
cpeg_base = basename(file_name);
|
||||
gpeg_exists = ctx_get("_gpeg_exists_" + cpeg_base);
|
||||
|
||||
// Pass GPEG data to PEG parser if available
|
||||
if (gpeg_exists == 1) {
|
||||
gpeg_data = ctx_get("_gpeg_data_" + cpeg_base);
|
||||
ctx_set("_peg_gpeg_data", gpeg_data);
|
||||
ctx_set("_peg_has_gpeg", 1);
|
||||
} else {
|
||||
ctx_set("_peg_has_gpeg", 0);
|
||||
}
|
||||
|
||||
peg_child = file_data |> parse volition_peg_nested;
|
||||
push("textures", peg_child);
|
||||
peg_child = 0; // Clear to avoid duplicate tree node
|
||||
}
|
||||
}
|
||||
|
||||
// GPEG files (pixel data only) - just show as binary preview
|
||||
if (is_gpeg && len(file_data) > 0) {
|
||||
set_display("GPEG Pixel Data");
|
||||
set_viewer("hex");
|
||||
}
|
||||
|
||||
// Check for mesh files (csmesh, ccmesh, clmesh)
|
||||
is_mesh = ends_with(file_name, ".csmesh_pc") || ends_with(file_name, ".csmesh_xbox2") ||
|
||||
ends_with(file_name, ".csmesh_ps3") ||
|
||||
ends_with(file_name, ".ccmesh_pc") || ends_with(file_name, ".ccmesh_xbox2") ||
|
||||
ends_with(file_name, ".ccmesh_ps3") ||
|
||||
ends_with(file_name, ".clmesh_pc") || ends_with(file_name, ".clmesh_xbox2") ||
|
||||
ends_with(file_name, ".clmesh_ps3");
|
||||
|
||||
if (is_mesh && len(file_data) > 32) {
|
||||
// Mesh files are validated by extension, magic check happens in parser
|
||||
set_display("Mesh");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
mesh_child = file_data |> parse volition_mesh_nested;
|
||||
push("meshes", mesh_child);
|
||||
mesh_child = 0;
|
||||
}
|
||||
|
||||
// Check for material library files
|
||||
is_matlib = ends_with(file_name, ".matlib_pc") || ends_with(file_name, ".matlib_xbox2") ||
|
||||
ends_with(file_name, ".matlib_ps3");
|
||||
if (is_matlib && len(file_data) > 0) {
|
||||
set_display("Material Library");
|
||||
set_viewer("text");
|
||||
set_text(ascii(file_data));
|
||||
}
|
||||
|
||||
// Check for rig/skeleton files
|
||||
is_rig = ends_with(file_name, ".rig_pc") || ends_with(file_name, ".rig_xbox2") ||
|
||||
ends_with(file_name, ".rig_ps3");
|
||||
if (is_rig && len(file_data) > 16) {
|
||||
// Rig files don't have a magic number, just parse by extension
|
||||
set_display("Rig/Skeleton");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
rig_child = file_data |> parse volition_rig_nested;
|
||||
push("rigs", rig_child);
|
||||
rig_child = 0;
|
||||
}
|
||||
|
||||
// Check for zone files (czh, czn, gzn)
|
||||
is_zone = ends_with(file_name, ".czh_pc") || ends_with(file_name, ".czh_xbox2") ||
|
||||
ends_with(file_name, ".czh_ps3") ||
|
||||
ends_with(file_name, ".czn_pc") || ends_with(file_name, ".czn_xbox2") ||
|
||||
ends_with(file_name, ".czn_ps3") ||
|
||||
ends_with(file_name, ".gzn_pc") || ends_with(file_name, ".gzn_xbox2") ||
|
||||
ends_with(file_name, ".gzn_ps3");
|
||||
if (is_zone && len(file_data) > 20) {
|
||||
// Zone files are validated by extension, signature check happens in parser
|
||||
zone_display = "Zone";
|
||||
if (contains(file_name, ".czh")) { zone_display = "Zone Header"; }
|
||||
if (contains(file_name, ".czn")) { zone_display = "Zone Data"; }
|
||||
if (contains(file_name, ".gzn")) { zone_display = "Zone GPU"; }
|
||||
set_display(zone_display);
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
zone_child = file_data |> parse volition_zone_nested;
|
||||
push("zones", zone_child);
|
||||
zone_child = 0;
|
||||
}
|
||||
|
||||
// Lua scripts
|
||||
is_lua = ends_with(file_name, ".lua");
|
||||
if (is_lua) {
|
||||
set_display("Lua Script");
|
||||
set_viewer("text");
|
||||
set_text(ascii(file_data));
|
||||
}
|
||||
|
||||
// Other text files
|
||||
is_text = ends_with(file_name, ".txt") || ends_with(file_name, ".csv") ||
|
||||
ends_with(file_name, ".cfg") || ends_with(file_name, ".log");
|
||||
if (is_text) {
|
||||
set_display("Text File");
|
||||
set_viewer("text");
|
||||
set_text(ascii(file_data));
|
||||
}
|
||||
|
||||
// Audio banks (Wwise BNK)
|
||||
is_bnk = ends_with(file_name, ".bnk_pc") || ends_with(file_name, ".bnk_xbox2") ||
|
||||
ends_with(file_name, ".bnk_ps3");
|
||||
if (is_bnk && len(file_data) > 12) {
|
||||
bnk_magic = ascii(bytesat(0, 4));
|
||||
if (bnk_magic == "BKHD") {
|
||||
set_display("Audio Bank");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
bnk_child = file_data |> parse volition_bnk_nested;
|
||||
push("audio", bnk_child);
|
||||
bnk_child = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation files
|
||||
is_anim = ends_with(file_name, ".anim_pc") || ends_with(file_name, ".anim_xbox2") ||
|
||||
ends_with(file_name, ".anim_ps3");
|
||||
if (is_anim && len(file_data) > 40) {
|
||||
set_display("Animation");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
anim_child = file_data |> parse volition_anim_nested;
|
||||
push("animations", anim_child);
|
||||
anim_child = 0;
|
||||
}
|
||||
|
||||
// Cloth simulation files
|
||||
is_sim = ends_with(file_name, ".sim_pc") || ends_with(file_name, ".sim_xbox2") ||
|
||||
ends_with(file_name, ".sim_ps3");
|
||||
if (is_sim && len(file_data) > 80) {
|
||||
sim_ver = i32at(0);
|
||||
if (sim_ver == 2 || sim_ver == 5) {
|
||||
set_display("Cloth Simulation");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
sim_child = file_data |> parse volition_sim_nested;
|
||||
push("simulations", sim_child);
|
||||
sim_child = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Morph target files
|
||||
is_morph = ends_with(file_name, ".cmorph_pc") || ends_with(file_name, ".cmorph_xbox2") ||
|
||||
ends_with(file_name, ".cmorph_ps3");
|
||||
if (is_morph && len(file_data) > 20) {
|
||||
set_display("Morph Targets");
|
||||
ctx_set("_vpp_file_name", file_name);
|
||||
morph_child = file_data |> parse volition_morph_nested;
|
||||
push("morphs", morph_child);
|
||||
morph_child = 0;
|
||||
}
|
||||
|
||||
// GPU data files - label properly (raw binary, hex viewer OK)
|
||||
is_gpu_mesh = ends_with(file_name, ".gcmesh_pc") || ends_with(file_name, ".gcmesh_xbox2") ||
|
||||
ends_with(file_name, ".gcmesh_ps3") ||
|
||||
ends_with(file_name, ".gsmesh_pc") || ends_with(file_name, ".gsmesh_xbox2") ||
|
||||
ends_with(file_name, ".gsmesh_ps3") ||
|
||||
ends_with(file_name, ".glmesh_pc") || ends_with(file_name, ".glmesh_xbox2") ||
|
||||
ends_with(file_name, ".glmesh_ps3");
|
||||
if (is_gpu_mesh) {
|
||||
set_display("GPU Mesh Data");
|
||||
}
|
||||
|
||||
is_gpu_morph = ends_with(file_name, ".gmorph_pc") || ends_with(file_name, ".gmorph_xbox2") ||
|
||||
ends_with(file_name, ".gmorph_ps3");
|
||||
if (is_gpu_morph) {
|
||||
set_display("GPU Morph Data");
|
||||
}
|
||||
|
||||
// Check for v_file_header (streaming reference header)
|
||||
// Signature 0x3854 at start of file indicates streaming references
|
||||
if (len(file_data) > 28) {
|
||||
vfh_sig = u16at(0);
|
||||
if (vfh_sig == 0x3854) {
|
||||
ctx_set("_vfh_file_name", file_name);
|
||||
vfh_child = file_data |> parse volition_v_file_header;
|
||||
push("headers", vfh_child);
|
||||
vfh_child = 0;
|
||||
}
|
||||
}
|
||||
|
||||
set_preview(file_name, file_data);
|
||||
}
|
||||
|
||||
// VPP Version 10 - Saints Row IV remaster, Agents of Mayhem
|
||||
// Inherits byteorder from caller
|
||||
type volition_vpp_v10 ui("VPP Archives")
|
||||
{
|
||||
// V10 has similar structure to V6 but different offsets
|
||||
// Header is at 0x150
|
||||
seek(0x150);
|
||||
u32 flags ui("Flags");
|
||||
u32 dir_count ui("Entry Count");
|
||||
u32 package_size ui("Package Size");
|
||||
u32 dir_size ui("Directory Size");
|
||||
u32 names_size ui("Names Size");
|
||||
u32 uncompressed_size ui("Uncompressed Size");
|
||||
u32 compressed_size ui("Compressed Size");
|
||||
skip(4); // padding
|
||||
u32 data_offset ui("Data Offset");
|
||||
|
||||
is_compressed = (flags & 1) != 0;
|
||||
is_condensed = (flags & 2) != 0;
|
||||
compression_type = "None";
|
||||
if (is_compressed) { compression_type = "ZLIB"; }
|
||||
if (is_condensed) { compression_type = "Condensed"; }
|
||||
compression = compression_type ui("Compression");
|
||||
|
||||
// Directory starts at 2048 bytes
|
||||
dir_offset = 2048;
|
||||
|
||||
// Calculate section offsets
|
||||
names_offset = dir_offset + dir_size;
|
||||
names_offset = ((names_offset + 2047) / 2048) * 2048;
|
||||
data_offset_base = names_offset + names_size;
|
||||
data_offset_base = ((data_offset_base + 2047) / 2048) * 2048;
|
||||
data_base_display = data_offset_base ui("Data Section Base");
|
||||
|
||||
// Check nesting depth (to prevent deep recursion in nested VPPs)
|
||||
vpp_depth = ctx_get("_vpp_depth");
|
||||
if (vpp_depth == 0) { vpp_depth = 0; }
|
||||
|
||||
// PASS 1: GPEG collection disabled for now (causes issues with nested VPPs)
|
||||
// TODO: Re-enable once nested parsing is stable
|
||||
|
||||
// PASS 2: Parse directory and create file children (V10)
|
||||
seek(dir_offset);
|
||||
skip_tree("entries");
|
||||
entry_idx = 0;
|
||||
|
||||
// Track cumulative compressed offset for STR2 containers where files are individually compressed
|
||||
compressed_offset = 0;
|
||||
|
||||
repeat(dir_count) {
|
||||
u32 e_name_offset;
|
||||
u32 e_runtime_offset;
|
||||
u32 e_data_offset;
|
||||
u32 e_uncompressed_size;
|
||||
u32 e_compressed_size;
|
||||
u32 e_package_ptr;
|
||||
|
||||
saved_pos = pos();
|
||||
|
||||
// Read filename (with bounds check)
|
||||
name_offset_abs = names_offset + e_name_offset;
|
||||
if (name_offset_abs < size()) {
|
||||
seek(name_offset_abs);
|
||||
entry_name = cstring();
|
||||
} else {
|
||||
entry_name = "[Invalid: offset beyond EOF]";
|
||||
}
|
||||
|
||||
// Always create entry row for table display
|
||||
ctx_set("_vpp_entry_name", entry_name);
|
||||
ctx_set("_vpp_entry_data_off", e_data_offset);
|
||||
ctx_set("_vpp_entry_size", e_uncompressed_size);
|
||||
ctx_set("_vpp_entry_csize", e_compressed_size);
|
||||
ctx_set("_vpp_entry_idx", entry_idx);
|
||||
|
||||
dummy = bytesat(0, 1);
|
||||
entry_child = dummy |> parse volition_vpp_entry_row;
|
||||
push("entries", entry_child);
|
||||
|
||||
// Parse file children at depth 0 (top-level VPP) and depth 1 (STR2 inside VPP)
|
||||
// Stop at depth 2+ to prevent infinite recursion
|
||||
if (vpp_depth <= 1) {
|
||||
// Determine actual size to read and file position
|
||||
// Two cases:
|
||||
// 1. Uncompressed files (csize == 0xFFFFFFFF): e_data_offset is actual offset, use directly
|
||||
// 2. Compressed files (csize < usize): e_data_offset is virtual, use cumulative compressed offset
|
||||
if (e_compressed_size == 0xFFFFFFFF) {
|
||||
// Uncompressed file - e_data_offset is the actual position
|
||||
read_size = e_uncompressed_size;
|
||||
actual_csize = e_compressed_size;
|
||||
file_offset = data_offset_base + e_data_offset;
|
||||
} else {
|
||||
// Compressed file - files are packed consecutively by compressed size
|
||||
// e_data_offset is virtual (uncompressed layout), use cumulative compressed offset
|
||||
read_size = e_compressed_size;
|
||||
actual_csize = e_compressed_size;
|
||||
file_offset = data_offset_base + compressed_offset;
|
||||
compressed_offset = compressed_offset + e_compressed_size;
|
||||
}
|
||||
|
||||
// Read file data and create child
|
||||
if (file_offset + read_size <= size() && read_size > 0) {
|
||||
seek(file_offset);
|
||||
file_data = read(read_size);
|
||||
|
||||
ctx_set("_vpp_file_name", entry_name);
|
||||
ctx_set("_vpp_file_usize", e_uncompressed_size);
|
||||
ctx_set("_vpp_file_csize", actual_csize);
|
||||
file_child = file_data |> parse volition_vpp_file;
|
||||
push("files", file_child);
|
||||
file_child = 0; // Clear to avoid duplicate (last file)
|
||||
}
|
||||
}
|
||||
|
||||
seek(saved_pos);
|
||||
entry_idx = entry_idx + 1;
|
||||
}
|
||||
|
||||
ui_table("entries", "entries", "Index,Name,Offset,Size,CompSize");
|
||||
|
||||
total_entries = dir_count ui("Total Entries");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
// Nested VPP type - for parsing STR2 files within VPP archives
|
||||
// No criteria block needed since called directly via parse()
|
||||
type volition_vpp_nested ui("STR2 Container") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name + " [Contents]"); // Distinct from parent volition_vpp_file
|
||||
|
||||
// Increment depth to signal nested parsing
|
||||
current_depth = ctx_get("_vpp_depth");
|
||||
if (current_depth == 0) { current_depth = 0; }
|
||||
ctx_set("_vpp_depth", current_depth + 1);
|
||||
|
||||
// Detect endianness from magic
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0xCE0A8951) {
|
||||
// Big-endian (Xbox 360/PS3)
|
||||
parse_here("volition_vpp_be");
|
||||
} else {
|
||||
// Little-endian (PC)
|
||||
parse_here("volition_vpp_impl");
|
||||
}
|
||||
|
||||
// Restore depth
|
||||
ctx_set("_vpp_depth", current_depth);
|
||||
}
|
||||
|
||||
// Nested PEG type - for parsing CPEG/GPEG files within VPP archives
|
||||
// No criteria block needed since called directly via parse()
|
||||
type volition_peg_nested ui("PEG Texture") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name + " [Texture]"); // Distinct from parent volition_vpp_file
|
||||
|
||||
// Detect endianness from magic
|
||||
// GEKV (0x47454B56 in file) reads as 0x564B4547 in LE mode = PC
|
||||
// VKEG (0x564B4547 in file) reads as 0x47454B56 in LE mode = Xbox/PS3
|
||||
magic_check = u32at(0);
|
||||
if (magic_check == 0x47454B56) {
|
||||
// Big-endian (Xbox 360/PS3)
|
||||
parse_here("volition_peg_be");
|
||||
} else {
|
||||
// Little-endian (PC)
|
||||
parse_here("volition_peg_impl");
|
||||
}
|
||||
}
|
||||
|
||||
// v_file_header - Streaming reference header
|
||||
// Found at start of files in STR2 containers (zone files, meshes, etc.)
|
||||
// Signature: 0x3854
|
||||
type volition_v_file_header ui("Streaming Header") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vfh_file_name");
|
||||
set_name(file_name + " [Header]");
|
||||
|
||||
u16 signature ui("Signature"); // 0x3854
|
||||
u16 version ui("Version"); // 4
|
||||
u32 ref_data_size ui("Reference Data Size");
|
||||
u32 ref_data_start ui("Reference Data Start");
|
||||
u32 ref_count ui("Reference Count");
|
||||
skip(16); // padding
|
||||
|
||||
header_size = 28 ui("Header Size");
|
||||
|
||||
// Parse reference strings
|
||||
if (ref_count > 0 && ref_data_start > 0) {
|
||||
seek(ref_data_start);
|
||||
ref_idx = 0;
|
||||
repeat(ref_count) {
|
||||
ref_name = cstring();
|
||||
ctx_set("_vfh_ref_name", ref_name);
|
||||
ctx_set("_vfh_ref_idx", ref_idx);
|
||||
ref_entry = bytesat(0, 1) |> parse volition_v_file_ref;
|
||||
push("references", ref_entry);
|
||||
ref_entry = 0;
|
||||
ref_idx = ref_idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
total_refs = ref_count ui("Total References");
|
||||
}
|
||||
|
||||
// Reference entry for v_file_header
|
||||
type volition_v_file_ref ui("Reference")
|
||||
{
|
||||
ref_name = ctx_get("_vfh_ref_name");
|
||||
set_name(ref_name);
|
||||
index = ctx_get("_vfh_ref_idx") ui("Index");
|
||||
name = ref_name ui("Referenced File");
|
||||
}
|
||||
|
||||
148
definitions/volition/zone.xscript
Normal file
148
definitions/volition/zone.xscript
Normal file
@ -0,0 +1,148 @@
|
||||
// Volition Zone Format (czh/czn/gzn)
|
||||
// Used by: Saints Row 3/4
|
||||
// CZH = Zone Header, CZN = Zone CPU Data, GZN = Zone GPU Data
|
||||
// v_file_header signature: 0x3854
|
||||
// world_zone_header signature: 'Z3RS' (0x5A335253)
|
||||
|
||||
type volition_zone ui("Volition Zone", root) byteorder LE
|
||||
{
|
||||
criteria {
|
||||
require _ext == "czh_pc" || _ext == "czh_xbox2" || _ext == "czh_ps3" ||
|
||||
_ext == "czn_pc" || _ext == "czn_xbox2" || _ext == "czn_ps3" ||
|
||||
_ext == "gzn_pc" || _ext == "gzn_xbox2" || _ext == "gzn_ps3";
|
||||
// Check for v_file_header signature
|
||||
require u16at(0) == 0x3854;
|
||||
}
|
||||
|
||||
is_header = contains(_ext, "czh");
|
||||
is_cpu_data = contains(_ext, "czn");
|
||||
is_gpu_data = contains(_ext, "gzn");
|
||||
is_xbox = ends_with(_ext, "xbox2");
|
||||
is_ps3 = ends_with(_ext, "ps3");
|
||||
|
||||
zone_type = "Zone";
|
||||
if (is_header) { zone_type = "Zone Header"; }
|
||||
if (is_cpu_data) { zone_type = "Zone Data (CPU)"; }
|
||||
if (is_gpu_data) { zone_type = "Zone Data (GPU)"; }
|
||||
|
||||
if (is_xbox) {
|
||||
set_name(zone_type + " (Xbox 360)");
|
||||
} else if (is_ps3) {
|
||||
set_name(zone_type + " (PS3)");
|
||||
} else {
|
||||
set_name(zone_type + " (PC)");
|
||||
}
|
||||
|
||||
ctx_set("_zone_is_header", is_header);
|
||||
ctx_set("_zone_is_cpu", is_cpu_data);
|
||||
ctx_set("_zone_is_gpu", is_gpu_data);
|
||||
|
||||
parse_here("volition_zone_impl");
|
||||
}
|
||||
|
||||
type volition_zone_impl ui("Zone")
|
||||
{
|
||||
// v_file_header
|
||||
u16 vfh_signature ui("VFH Signature"); // 0x3854
|
||||
u16 vfh_version ui("VFH Version");
|
||||
u32 reference_data_size ui("Reference Data Size");
|
||||
u32 reference_data_start ui("Reference Data Start");
|
||||
u32 reference_count ui("Reference Count");
|
||||
u8 initialized ui("Initialized");
|
||||
skip(15); // padding
|
||||
|
||||
is_header = ctx_get("_zone_is_header");
|
||||
is_gpu = ctx_get("_zone_is_gpu");
|
||||
|
||||
// Check for world_zone_header signature
|
||||
if (pos() < size() - 8) {
|
||||
wzh_sig_check = u32at(pos());
|
||||
|
||||
// 'Z3RS' = 0x5A335253 (LE) or 'SR3Z' = 0x53523358
|
||||
if (wzh_sig_check == 0x5A335253 || wzh_sig_check == 0x53523358) {
|
||||
u32 wzh_signature ui("Zone Signature"); // Z3RS
|
||||
u32 wzh_version ui("Zone Version");
|
||||
|
||||
// Parse sections if this is zone data (not just header)
|
||||
if (is_header != 1 && pos() < size() - 8) {
|
||||
section_count = 0;
|
||||
while (pos() < size() - 8 && section_count < 100) {
|
||||
section_id_raw = u32at(pos());
|
||||
|
||||
// Valid section IDs are in 0x2233+ range
|
||||
if (section_id_raw >= 0x2233 && section_id_raw < 0x3000) {
|
||||
u32 section_id;
|
||||
u32 cpu_size;
|
||||
|
||||
has_gpu = (section_id_raw & 0x80000000) != 0;
|
||||
actual_id = section_id_raw & 0x7FFFFFFF;
|
||||
|
||||
gpu_size_val = 0;
|
||||
if (has_gpu) {
|
||||
u32 gpu_size;
|
||||
gpu_size_val = gpu_size;
|
||||
}
|
||||
|
||||
ctx_set("_zone_section_id", actual_id);
|
||||
ctx_set("_zone_section_cpu", cpu_size);
|
||||
ctx_set("_zone_section_gpu", gpu_size_val);
|
||||
section_entry = bytesat(0, 1) |> parse volition_zone_section;
|
||||
push("sections", section_entry);
|
||||
section_entry = 0;
|
||||
|
||||
// Skip section data
|
||||
skip(cpu_size);
|
||||
section_count = section_count + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
total_sections = section_count ui("Section Count");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zone_type_display = "Unknown";
|
||||
if (is_header == 1) { zone_type_display = "Zone Header (CZH)"; }
|
||||
if (ctx_get("_zone_is_cpu") == 1) { zone_type_display = "Zone CPU Data (CZN)"; }
|
||||
if (is_gpu == 1) { zone_type_display = "Zone GPU Data (GZN)"; }
|
||||
|
||||
type_display = zone_type_display ui("Zone Type");
|
||||
ref_count_display = reference_count ui("References");
|
||||
file_size = size() ui("File Size");
|
||||
}
|
||||
|
||||
type volition_zone_section ui("Section")
|
||||
{
|
||||
section_id = ctx_get("_zone_section_id");
|
||||
cpu_size = ctx_get("_zone_section_cpu");
|
||||
gpu_size = ctx_get("_zone_section_gpu");
|
||||
|
||||
// Map section ID to name
|
||||
section_name = "Unknown Section";
|
||||
if (section_id == 0x2233) { section_name = "Reference Geometry"; }
|
||||
if (section_id == 0x2234) { section_name = "Objects"; }
|
||||
if (section_id == 0x2235) { section_name = "Navigation Mesh"; }
|
||||
if (section_id == 0x2236) { section_name = "Traffic Data"; }
|
||||
if (section_id == 0x2244) { section_name = "Heightmap"; }
|
||||
if (section_id == 0x2247) { section_name = "Water Volumes"; }
|
||||
|
||||
set_name(section_name);
|
||||
id = section_id ui("Section ID");
|
||||
cpu_data_size = cpu_size ui("CPU Size");
|
||||
gpu_data_size = gpu_size ui("GPU Size");
|
||||
}
|
||||
|
||||
// Nested type for VPP parsing
|
||||
type volition_zone_nested ui("Zone") byteorder LE
|
||||
{
|
||||
file_name = ctx_get("_vpp_file_name");
|
||||
set_name(file_name);
|
||||
|
||||
ctx_set("_zone_is_header", contains(file_name, ".czh"));
|
||||
ctx_set("_zone_is_cpu", contains(file_name, ".czn"));
|
||||
ctx_set("_zone_is_gpu", contains(file_name, ".gzn"));
|
||||
|
||||
parse_here("volition_zone_impl");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user