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>
402 lines
13 KiB
Plaintext
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");
|
|
}
|
|
}
|