XPlor/definitions/volition/peg.xscript
njohnson 908100f487 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>
2026-01-12 20:53:59 -05:00

402 lines
13 KiB
Plaintext

// 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 {
// 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) {
// 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 {
// 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_body");
} else if (version == 13) {
parse_here("volition_peg_v13_body");
} else {
unknown_version = version ui("Unsupported Version");
set_preview("peg_data.bin", read(size() - pos()));
set_viewer("hex");
}
}
// 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)");
// Header continues after magic/version/platform (already read 8 bytes)
u32 header_size ui("Header Size");
u32 data_size ui("Data Size");
u16 texture_count ui("Texture Count");
u16 unknown0E ui("Unknown 0x0E");
u16 frame_count ui("Frame Count");
u8 unknown12 ui("Unknown 0x12");
u8 unknown13 ui("Unknown 0x13");
// Parse frames (48 bytes each)
skip_tree("frames");
frame_idx = 0;
repeat(frame_count) {
u32 f_data_offset;
u16 f_width;
u16 f_height;
u32 f_format;
u32 f_unknown0C;
u16 f_frame_count;
u16 f_flags;
u32 f_name_offset;
u32 f_unknown18;
u32 f_size;
u32 f_unknown20;
u32 f_unknown24;
u32 f_unknown28;
u32 f_unknown2C;
ctx_set("_peg_frame_idx", frame_idx);
ctx_set("_peg_frame_offset", f_data_offset);
ctx_set("_peg_frame_width", f_width);
ctx_set("_peg_frame_height", f_height);
ctx_set("_peg_frame_format", f_format);
ctx_set("_peg_frame_size", f_size);
ctx_set("_peg_frame_flags", f_flags);
// 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);
frame_child = dummy |> parse volition_peg_frame_row;
push("frames", frame_child);
frame_idx = frame_idx + 1;
}
// Parse texture names (null-terminated strings)
name_idx = 0;
repeat(texture_count) {
tex_name = cstring();
ctx_set("_peg_tex_name_" + name_idx, tex_name);
name_idx = name_idx + 1;
}
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 body - Red Faction: Armageddon (inherits byteorder)
type volition_peg_v13_body ui("PEG V13")
{
set_name("PEG Texture (V13)");
// Header continues after magic/version/platform (already read 8 bytes)
u32 header_size ui("Header Size");
u32 data_size ui("Data Size");
u16 texture_count ui("Texture Count");
u16 unknown0E ui("Unknown 0x0E");
u16 frame_count ui("Frame Count");
u16 unknown12 ui("Unknown 0x12");
// Parse frames (48 bytes each - same as V10)
skip_tree("frames");
frame_idx = 0;
repeat(frame_count) {
u32 f_data_offset;
u16 f_width;
u16 f_height;
u32 f_format;
u32 f_unknown0C;
u16 f_frame_count;
u16 f_flags;
u32 f_name_offset;
u32 f_unknown18;
u32 f_size;
u32 f_unknown20;
u32 f_unknown24;
u32 f_unknown28;
u32 f_unknown2C;
ctx_set("_peg_frame_idx", frame_idx);
ctx_set("_peg_frame_offset", f_data_offset);
ctx_set("_peg_frame_width", f_width);
ctx_set("_peg_frame_height", f_height);
ctx_set("_peg_frame_format", f_format);
ctx_set("_peg_frame_size", f_size);
ctx_set("_peg_frame_flags", f_flags);
// 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);
frame_child = dummy |> parse volition_peg_frame_row;
push("frames", frame_child);
frame_idx = frame_idx + 1;
}
// Parse texture names (null-terminated strings)
name_idx = 0;
repeat(texture_count) {
tex_name = cstring();
ctx_set("_peg_tex_name_" + name_idx, tex_name);
name_idx = name_idx + 1;
}
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 (inherits byteorder, but only uses context)
type volition_peg_frame_row ui("Frame")
{
set_hidden();
Index = ctx_get("_peg_frame_idx");
Width = ctx_get("_peg_frame_width");
Height = ctx_get("_peg_frame_height");
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");
}
}