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