-
-
Save GMMan/d274e4fd07ccd51c7d3b85bc3645ec85 to your computer and use it in GitHub Desktop.
THEA500 Mini bodypack unpacking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Security.Cryptography; | |
using System.Text; | |
namespace BodUnpack | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
string inPath = @"C:\projects\a500mini\resources.bod"; | |
string outPath = @"C:\projects\a500mini\resources_unpacked"; | |
int totalEntries = 147; | |
int dataStartOffset = 0x1f790; | |
string[] gameNames = new[] | |
{ | |
"alien_breed_3d", | |
"alien_breed_se", | |
"another_world", | |
"arcade_pool", | |
"atr", | |
"battle_chess", | |
"cadaver", | |
"california_games", | |
"dragons_breath", | |
"f16_combat_pilot", | |
"kick_off_2", | |
"paradroid90", | |
"pinball_dreams", | |
"project_x_se", | |
"qwak", | |
"simon_the_sorcerer", | |
"speedball_2", | |
"stunt_car_racer", | |
"super_cars_ii", | |
"chaos_engine", | |
"lost_patrol", | |
"sentinel", | |
"titus_the_fox", | |
"usb", // non-official name | |
"worms_dc", | |
"zool", | |
}; | |
Dictionary<int, string> nameMapping = new Dictionary<int, string>() | |
{ | |
[0x80] = "font_atlas.bin", | |
[0x81] = "fragment.glsl", | |
[0x82] = "vertex.glsl", | |
[0x83] = "music.mp3", | |
[0x84] = "splash.mp3", | |
[0x85] = "ui_atlas.png", | |
[0x86] = "ctrlatlas.png", | |
[0x87] = "Roboto-Regular.ttf", | |
[0x88] = "RobotoMono-Regular.ttf", | |
[0x89] = "licenses.txt", // non-official name | |
[0x8a] = "close.wav", | |
[0x8b] = "flip.wav", | |
[0x8c] = "open.wav", | |
[0x8d] = "select.wav", | |
[0x8e] = "deny.wav", | |
[0x8f] = "pop.wav", | |
[0x90] = "select_game.wav", | |
[0x91] = "click.wav", | |
}; | |
Dictionary<int, string> extMapping = new Dictionary<int, string>() | |
{ | |
[0] = ".png", | |
[1] = ".uae", | |
[2] = ".uss", // GZipped | |
[3] = ".bin", // Font atlas | |
[4] = ".glsl", | |
[5] = ".mp3", | |
[6] = ".ttf", | |
[7] = ".txt", | |
[8] = ".wav", | |
}; | |
//List<string> ussNames = new List<string>(); | |
Directory.CreateDirectory(outPath); | |
using (FileStream fs = File.OpenRead(inPath)) | |
{ | |
if (!VerifyHash(fs)) throw new InvalidDataException("Hash mismatch"); | |
BinaryReader br = new BinaryReader(fs); | |
fs.Seek(0x20, SeekOrigin.Begin); | |
List<int> offsets = new List<int>(); | |
for (int i = 0; i < totalEntries; ++i) | |
{ | |
offsets.Add(br.ReadInt32()); | |
} | |
for (int i = 0; i < offsets.Count - 1; ++i) | |
{ | |
// Read metadata | |
// Each file has some padding and file type field and padding+meta length | |
// Extra bytes pad out to 8-byte alignment | |
fs.Seek(dataStartOffset + offsets[i + 1] - 4, SeekOrigin.Begin); | |
ushort fileType = br.ReadUInt16(); | |
ushort metaSize = br.ReadUInt16(); | |
if (!extMapping.TryGetValue(fileType, out string ext)) | |
{ | |
ext = ".bin"; | |
} | |
if (!nameMapping.TryGetValue(i, out string fileName)) | |
{ | |
fileName = $"{i}{ext}"; | |
} | |
if (i < 128) | |
{ | |
int j = i; | |
// USB entry doesn't have .uae and .uss files, account for this | |
if (j >= 118) j += 2; | |
string append = string.Empty; | |
switch (j % 5) | |
{ | |
case 0: | |
append = "_cover.png"; | |
break; | |
case 1: | |
append = "_screen1.png"; | |
break; | |
case 2: | |
append = "_screen2.png"; | |
break; | |
case 3: | |
append = ".uae"; | |
break; | |
case 4: | |
append = ".uss"; | |
break; | |
} | |
fileName = $"{gameNames[j / 5]}{append}"; | |
} | |
Console.WriteLine($"{i} {fileType} {fileName}"); | |
int fileSize = offsets[i + 1] - offsets[i] - metaSize; | |
fs.Seek(dataStartOffset + offsets[i], SeekOrigin.Begin); | |
byte[] data = br.ReadBytes(fileSize); | |
if (fileType == 2) | |
{ | |
// Try to extract filename | |
if (data[0] == 0x1f && data[1] == 0x8b && data[3] == 0x08) | |
{ | |
for (int j = 10; ; ++j) | |
{ | |
if (data[j] == 0) | |
{ | |
//fileName = Encoding.ASCII.GetString(data, 10, j - 10); | |
//ussNames.Add(fileName); | |
break; | |
} | |
} | |
} | |
using (MemoryStream ms = new MemoryStream(data)) | |
using (GZipStream gz = new GZipStream(ms, CompressionMode.Decompress)) | |
using (MemoryStream oms = new MemoryStream()) | |
{ | |
gz.CopyTo(oms); | |
oms.Flush(); | |
data = oms.ToArray(); | |
} | |
} | |
File.WriteAllBytes(Path.Combine(outPath, fileName), data); | |
} | |
} | |
//Console.WriteLine(); | |
//foreach (var name in ussNames) | |
//{ | |
// Console.WriteLine(name); | |
//} | |
} | |
static bool VerifyHash(Stream stream) | |
{ | |
// 32 bytes @ 0x7414c per Ghidra | |
byte[] salt = { | |
// Gotta find your own here | |
}; | |
using (SHA256 sha = SHA256.Create()) | |
{ | |
sha.TransformBlock(salt, 0, salt.Length, salt, 0); | |
BinaryReader br = new BinaryReader(stream); | |
stream.Seek(0x20, SeekOrigin.Begin); | |
while (stream.Position < stream.Length) | |
{ | |
int readCount = (int)Math.Min(4096, stream.Length - stream.Position); | |
byte[] buf = br.ReadBytes(readCount); | |
if (readCount >= 4096) | |
sha.TransformBlock(buf, 0, buf.Length, buf, 0); | |
else | |
sha.TransformFinalBlock(buf, 0, buf.Length); | |
} | |
stream.Seek(0, SeekOrigin.Begin); | |
byte[] expectedHash = br.ReadBytes(0x20); | |
for (int i = 0; i < expectedHash.Length; ++i) | |
{ | |
if (expectedHash[i] != sha.Hash[i]) return false; | |
} | |
} | |
return true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment