Skip to content

Instantly share code, notes, and snippets.

@barncastle
Created August 28, 2022 20:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barncastle/fbab86d6a4d5a78333c73115d3ba9209 to your computer and use it in GitHub Desktop.
Save barncastle/fbab86d6a4d5a78333c73115d3ba9209 to your computer and use it in GitHub Desktop.
Code for reading Neopets Java Mobile game's PAK files
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);
}
}
}
@barncastle
Copy link
Author

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 for png and midi assets, all others will be .bin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment