Created
August 28, 2022 20:05
-
-
Save barncastle/fbab86d6a4d5a78333c73115d3ba9209 to your computer and use it in GitHub Desktop.
Code for reading Neopets Java Mobile game's PAK files
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; | |
public class NeopetsPak | |
{ | |
private static readonly HashSet<int> MidiFiles = new HashSet<int> | |
{ | |
58, 57, 56, 49, 55 | |
}; | |
private static readonly HashSet<int> ImageFiles = new HashSet<int> | |
{ | |
0, 1, 2, 3, 4, 5, 8, 9, 11, 18, 19, 20, 21, | |
24, 29, 30, 31, 32, 35, 46, 52, 53, 62 | |
}; | |
private static int[] ArchiveDeltas; | |
private static long[] CRCTable; | |
private byte[] Buffer; | |
private static int BitPosition; | |
private string CurrentFile; | |
private readonly short[] FileOffsets; | |
private int FileCount; | |
private int FileIndex; | |
public static void Export() | |
{ | |
for (var i = 0; i < 63; i++) | |
{ | |
if (ImageFiles.Contains(i)) | |
File.WriteAllBytes($"Dump\\{i}.png", GetImageBytes(i)); | |
else if (MidiFiles.Contains(i)) | |
File.WriteAllBytes($"Dump\\{i}.midi", GetFileBytes(i)); | |
else | |
File.WriteAllBytes($"Dump\\{i}.bin", GetFileBytes(i)); | |
} | |
} | |
public static void LoadIFO() | |
{ | |
var buffer = File.ReadAllBytes("Pack.ifo"); | |
int count = (buffer[0] << 8) | buffer[1]; // big endian | |
// [2,3] = unused | |
// pak archive name deltas | |
ArchiveDeltas = new int[count]; | |
for (int i = 0; i < count; i++) | |
ArchiveDeltas[i] = buffer[i + 4]; | |
} | |
public static byte[] GetFileBytes(int fileId) | |
{ | |
if (ArchiveDeltas == null) | |
LoadIFO(); | |
NeopetsPak reader = new NeopetsPak(fileId); | |
int size = reader.ReadInt32(); | |
return reader.ReadInt8Array(size); | |
} | |
public static byte[] GetImageBytes(int fileId) | |
{ | |
if (ArchiveDeltas == null) | |
LoadIFO(); | |
if (CRCTable == null) | |
InitCRC(); | |
NeopetsPak reader = new NeopetsPak(fileId); | |
reader.ReadInt32(); | |
// read meta data | |
int bitDepth = reader.ReadBits(8) + 1; | |
int chunks = reader.ReadBits(8) + 1; // unused | |
int width = reader.ReadBits(16) + 1; | |
int height = reader.ReadBits(16) + 1; | |
int paletteSize = reader.ReadBits(8) + 1; | |
int pixelFormat = reader.ReadBits(4); | |
int i; | |
int hasTransparency = 0; | |
int transparencyCount = -1; | |
if (reader.ReadBits(4) == 1) | |
{ | |
hasTransparency = 1; | |
transparencyCount = reader.ReadBits(8); | |
} | |
// read and convert palette data | |
int[] palette = new int[paletteSize]; | |
for (i = 0; i < paletteSize; i++) | |
palette[i] = PixelToRGB(reader.ReadBits(1 + pixelFormat << 3), pixelFormat); | |
// read image data | |
int imageDataSize = reader.ReadBits(16); | |
byte[] imageData = reader.ReadInt8Array(imageDataSize); | |
byte[] pngBuffer = new byte[69 + hasTransparency * (12 + transparencyCount + 1) + paletteSize * 3 + imageDataSize]; | |
// PNG header | |
WriteBitsMSB(pngBuffer, 000, 32, 0x89504E47); | |
WriteBitsMSB(pngBuffer, 032, 32, 0xD0A1A0A); | |
// IHDR | |
WriteBitsMSB(pngBuffer, 064, 32, 13); | |
WriteBitsMSB(pngBuffer, 096, 32, 0x49484452); | |
WriteBitsMSB(pngBuffer, 128, 32, width); | |
WriteBitsMSB(pngBuffer, 160, 32, height); | |
WriteBitsMSB(pngBuffer, 192, 8, bitDepth); | |
WriteBitsMSB(pngBuffer, 200, 32, 0x3000000); | |
// PLTE | |
int crc = CRCValue(pngBuffer, 12, 29); | |
WriteBitsMSB(pngBuffer, 232, 32, crc); | |
WriteBitsMSB(pngBuffer, 264, 32, paletteSize * 3); | |
WriteBitsMSB(pngBuffer, 296, 32, 0x504C5445); | |
for (i = 0; i < paletteSize; i++) | |
WriteBitsMSB(pngBuffer, 328 + i * 24, 24, palette[i]); | |
crc = CRCValue(pngBuffer, 37, 41 + paletteSize * 3); | |
WriteBitsMSB(pngBuffer, 264 + (8 + paletteSize * 3 << 3), 32, crc); | |
int offset = 264 + (8 + paletteSize * 3 + 4 << 3); | |
// tRNS | |
if (hasTransparency == 1) | |
{ | |
WriteBitsMSB(pngBuffer, offset, 32, transparencyCount + 1); | |
WriteBitsMSB(pngBuffer, offset + 32, 32, 0x74524E53); | |
for (i = 0; i < transparencyCount; i++) | |
WriteBitsMSB(pngBuffer, offset + 64 + (i << 3), 8, 0xFF); | |
WriteBitsMSB(pngBuffer, offset + 64 + (i << 3), 8, 0); | |
crc = CRCValue(pngBuffer, (offset >> 3) + 4, (offset >> 3) + 4 + 4 + transparencyCount + 1); | |
WriteBitsMSB(pngBuffer, offset + (8 + transparencyCount + 1 << 3), 32, crc); | |
offset += 8 + transparencyCount + 1 + 4 << 3; | |
} | |
// IDAT | |
WriteBitsMSB(pngBuffer, offset, 32, imageDataSize); | |
WriteBitsMSB(pngBuffer, offset + 32, 32, 0x49444154); | |
for (i = 0; i < imageDataSize; i++) | |
WriteBitsMSB(pngBuffer, offset + 64 + (i << 3), 8, imageData[i]); | |
crc = CRCValue(pngBuffer, (offset >> 3) + 4, (offset >> 3) + 4 + 4 + imageDataSize); | |
WriteBitsMSB(pngBuffer, offset + (8 + imageDataSize << 3), 32, crc); | |
offset += 8 + imageDataSize + 4 << 3; | |
// IEND | |
WriteBitsMSB(pngBuffer, offset, 32, 0); | |
WriteBitsMSB(pngBuffer, offset + 32, 32, 0x49454E44); | |
crc = CRCValue(pngBuffer, (offset >> 3) + 4, (offset >> 3) + 4 + 4 + 0); | |
WriteBitsMSB(pngBuffer, offset + 64, 32, crc); | |
return pngBuffer; | |
} | |
private NeopetsPak(int fileId) | |
{ | |
string pakArchiveName = GetPakArchiveName(fileId); | |
if (pakArchiveName != CurrentFile) | |
{ | |
Clear(); | |
CurrentFile = pakArchiveName; | |
LoadPAK(pakArchiveName); | |
FileOffsets = new short[FileCount]; | |
for (byte b = 0; b < FileCount; b++) | |
FileOffsets[b] = ReadInt16(); | |
} | |
BitPosition = FileCount * 2 * 8; // bit position set after offsets | |
BitPosition += FileOffsets[FileIndex] * 8; // seek to target file | |
} | |
private byte ReadInt8() => (byte)ReadBits(8); | |
private short ReadInt16() => (short)ReadBits(16); | |
private int ReadInt32() => ReadBits(32); | |
private byte[] ReadInt8Array(int byteCount) | |
{ | |
byte[] result = new byte[byteCount]; | |
if ((BitPosition & 0x7) == 0) | |
{ | |
// aligned read, direct copy | |
Array.Copy(Buffer, BitPosition / 8, result, 0, byteCount); | |
BitPosition += byteCount * 8; | |
} | |
else | |
{ | |
// unaligned read, use bits | |
for (int i = 0; i < byteCount; i++) | |
Buffer[i] = ReadInt8(); | |
} | |
return result; | |
} | |
private string GetPakArchiveName(int fileId) | |
{ | |
int pakIndex = 0; | |
for (int i = 0; i < ArchiveDeltas.Length && fileId >= ArchiveDeltas[i]; i++) | |
{ | |
pakIndex += ArchiveDeltas[i]; | |
fileId -= ArchiveDeltas[i]; | |
} | |
FileIndex = fileId; | |
return $"r{pakIndex:000}.pak"; | |
} | |
private void Clear() | |
{ | |
Buffer = null; | |
CurrentFile = null; | |
} | |
private void LoadPAK(string name) | |
{ | |
if (!File.Exists(name)) | |
throw new Exception($"Unable to find {name}"); | |
using (var fs = File.OpenRead(name)) | |
{ | |
Buffer = new byte[fs.Length - 1]; | |
BitPosition = 0; | |
FileCount = fs.ReadByte(); | |
fs.Read(Buffer, 0, Buffer.Length); | |
} | |
} | |
private int ReadBits(int bitCount) | |
{ | |
int i = ReadBitsMSB(Buffer, BitPosition, bitCount); | |
BitPosition += bitCount; | |
return i; | |
} | |
#region Image Methods | |
private static int PixelToRGB(int value, int format) | |
{ | |
int i, k, m; | |
switch (format) | |
{ | |
case 0: | |
i = (value >> 5) * 0xFF / 7; | |
k = (value >> 2 & 0x7) * 0xFF / 7; | |
m = (value & 0x3) * 0xFF / 3; | |
return i << 16 | k << 8 | m; | |
case 1: | |
i = (value >> 11) * 0xFF / 31; | |
k = (value >> 5 & 0x3F) * 0xFF / 63; | |
m = (value & 0x1F) * 0xFF / 31; | |
return i << 16 | k << 8 | m; | |
default: | |
return value; | |
} | |
} | |
private static void InitCRC() | |
{ | |
CRCTable = new long[0x100]; | |
for (int i = 0; i < 0x100; i++) | |
{ | |
long l = i; | |
for (int j = 0; j < 8; j++) | |
l = ((l & 0x1L) == 1L) ? (0xEDB88320L ^ l >> 1) : (l >> 1); | |
CRCTable[i] = l; | |
} | |
} | |
private static int CRCValue(byte[] buffer, int start, int end) | |
{ | |
long crc = 0xFFFFFFFF; | |
for (int i = start; i < end; i++) | |
crc = CRCTable[(int)((crc ^ buffer[i]) & 0xFF)] ^ crc >> 8; | |
return (int)(crc ^ 0xFFFFFFFF); | |
} | |
#endregion | |
private static int ReadBitsMSB(byte[] buffer, int bitPosition, int bitCount) | |
{ | |
int result = 0; | |
for (int index = bitPosition; index < bitPosition + bitCount; index++) | |
{ | |
int byteOffset = index / 8; | |
int bitIndex = 7 - (index % 8); | |
int position = bitCount - 1 - (index - bitPosition); | |
result |= ((buffer[byteOffset] >> bitIndex) & 0x1) << position; | |
} | |
return result; | |
} | |
private static void WriteBitsMSB(byte[] buffer, int bitOffset, int bitCount, long value) | |
{ | |
for (int index = bitOffset; index < bitOffset + bitCount; index++) | |
{ | |
int position = bitCount - 1 - (index - bitOffset); | |
bool bitValue = ((value >> position) & 1) > 0; | |
int byteOffset = index / 8; | |
int bitIndex = 7 - (index % 8); | |
if (bitValue) | |
buffer[byteOffset] |= (byte)(1 << bitIndex); | |
else | |
buffer[byteOffset] &= (byte)~(1 << bitIndex); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Put NeopetsPakExporter.exe next to the
pack.ifo
and*.pak
files and run. The tool will create a folder called Dump containing all of the game's assets. There are no file names so files will be named as their archive index. Extensions will be correct forpng
andmidi
assets, all others will be.bin
.