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:
njohnson 2026-01-12 20:53:59 -05:00
parent a86c5f592c
commit 908100f487
10 changed files with 1623 additions and 88 deletions

View 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");
}
}

View File

@ -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) {
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");
}
}

View 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");
}
}

View 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");
}
}

View 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");
}
}

View File

@ -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 {
_is_big_endian = 0;
set_name(original_name + " (Xbox 360)");
}
parse_here("volition_peg_be");
} else {
// 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");
}
}

View 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");
}
}

View 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");
}
}

View File

@ -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);
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);
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);
// 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();
// Determine actual size to read
if (e_compressed_size == 0xFFFFFFFF) {
read_size = e_uncompressed_size;
} 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
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);
ctx_set("_vpp_file_name", entry_name);
ctx_set("_vpp_file_usize", e_uncompressed_size);
ctx_set("_vpp_file_csize", e_compressed_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");
} else {
// Try raw deflate
file_data = raw_data |> deflate;
compression_used = "Deflate" 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 {
// 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");
}
}
}
} 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");
}

View 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");
}