Additional
pkg.idx format
Code:
Offset Bytes Description
0x0000 72 Unknown
0x0048 N×592 Records (see below)
0x0004 4 Unknown. My guess is a checksum.
Record format
Code:
Offset Bytes Type Description
0x0000 4 Unknown
0x0004 4 Integer File counter
0x0008 4 Pointer Package offset, in the corresponding pkg-file.
0x000C 4 Pointer Index offset, in this file. I don't know what this is for. My guess would be some deprecation mechanism.
0x0010 4 Integer Size of compressed data.
0x0014 16 4×Integer Unknown. Sometimes parts are zero, and sometimes seems to contain random data.
0x0024 8 Integer Timestamp 1. Not sure what each of these represents.
0x002C 8 Integer Timestamp 2.
0x0034 8 Integer Timestamp 3.
0x003C 4 Integer Unpacked size.
0x0040 260 String Filename.
0x0144 260 String Path. Ends with a backslash.
0x0248 4 Unknown Always zero.
0x024C 4 Integer Package number. 1 = pkg001.pkg etc.
BMS Script
Code:
# Aeriagames pkg.idx/pkg???.pkg
# Eden Eternal
# Kitsu Saga
# script for QuickBMS
# quickbms . aluigi . org
getdstring DUMMY 260
getdstring SIGN 32
math PKG_OLD = -1
get FULLSIZE asize
do
get DUMMY long
get OFFSET long
get DUMMY long
get ZSIZE long
getdstring DUMMY 0x28
get SIZE long
getdstring NAME2 260
getdstring NAME 260
get DUMMY long
get PKG long
get DUMMY long
if PKG != PKG_OLD
string PKG_NAME p= "pkg%03d.pkg" PKG
open FDSE PKG_NAME 1
math PKG_OLD = PKG
endif
string NAME += NAME2
clog NAME OFFSET ZSIZE SIZE 1
savepos CURR
while CURR < FULLSIZE
Python script to extract the packages. Run it in the Grand Fantasia folder and it will put all the files into a sub-folder named "out". Requires Python v2.5.2+
Code:
from __future__ import with_statement
import sys
import os
import zlib
import struct
from operator import itemgetter
class Record(tuple):
def __new__(cls, *result):
if len(result) != 17:
raise ValueError("Requires 17 arguments. Recieved %d" % (len(result,)))
return tuple.__new__(cls,result)
unknown00 = property(itemgetter(0))
filenum = property(itemgetter(1))
package_offset = property(itemgetter(2))
index_offset = property(itemgetter(3))
package_bytes = property(itemgetter(4))
unknown05 = property(itemgetter(5))
unknown06 = property(itemgetter(6))
unknown07 = property(itemgetter(7))
unknown08 = property(itemgetter(8))
timestamp1 = property(itemgetter(9))
timestamp2 = property(itemgetter(10))
timestamp3 = property(itemgetter(11))
unpackedsize = property(itemgetter(12))
name = property(itemgetter(13))
path = property(itemgetter(14))
unknown16 = property(itemgetter(15))
pkgnum = property(itemgetter(16))
def read_index():
index_byte_size = os.stat('pkg.idx')[6] # st_size
with open('pkg.idx','rb') as f:
header = f.read(288)
data = f.read(index_byte_size - 288 - 4)
checksum = f.read(4)
if len(data) % 592 != 0:
raise IOError("Invalid filesize: Not a multiple of %d plus %d" % (size, 288 + 4))
files = parse_index(data)
return header,files,checksum
def parse_index(data):
format = '<9L3QL260s260s2L'
size = struct.calcsize(format) # 592 bytes
files = []
for offset in xrange(0,len(data),size):
record = list(struct.unpack(format,data[offset:offset+size]))
record[13] = record[13].partition('\0')[0]
record[14] = record[14].partition('\0')[0]
record = Record(*record)
files.append(record)
return files
file_cache = {}
def unpack(pkgnum,offset,size,filename):
pkgname = "pkg%03d.pkg" % (pkgnum,)
f = file_cache.get(pkgname)
if f is None:
f = file_cache[pkgname] = open(pkgname,"rb")
f.seek(offset)
data = f.read(size)
data = data.decode('zlib')
filename = os.path.join('out',filename)
path,name = os.path.split(filename)
if not os.path.isdir(path):
os.makedirs(path)
with open(filename, 'wb') as f:
f.write(data)
headers,files,checksum = read_index()
n = len(files)
sort_key = lambda o: (o.pkgnum,o.package_offset)
for i,rec in enumerate(sorted(files,key=sort_key)):
fn = os.path.join(rec.path,rec.name)
print '(%6.2f%%) Unpacking %s...' % (100.*i/n,fn)
unpack(rec.pkgnum,rec.package_offset,rec.package_bytes,fn)
May be useful