All types are little endian.
byte
unsigned 8-bit integeruint16
unsigned 16-bit integeruint32
unsigned 32-bit integeruint64
unsigned 64-bit integerindex
32-bit integer encoded as described belowname
8-bit text string encoded as described below
A compressed 32 bit integer. Uses the two high bits of the first byte to indicate if its a negative number and if there's more data to read. Then each following byte the high bit is used to indicate additional bits. The following C++ code illustrates how to read it:
int32_t ReadIndex()
{
uint8_t value = ReadByte();
bool signbit = value & (1 << 7);
bool nextbyte = value & (1 << 6);
int32_t index = value & 0x3f;
if (nextbyte)
{
int shift = 6;
do
{
value = ReadByte();
index |= static_cast<int32_t>(value & 0x7f) << shift;
shift += 7;
} while ((value & (1 << 7)) && shift < 32);
}
if (signbit)
index = -index;
return index;
}
In packages older than 64 a name string was a zero terminated list types. In newer versions its a index size
followed by the string. The following C++ code illustrates how to read it:
std::string ReadString()
{
if (FileHeader.PackageVersion >= 64)
{
int len = ReadIndex();
std::vector<char> s = ReadBytes(len);
s.push_back(0);
return s.data();
}
else
{
std::string s;
while (true)
{
char c = ReadByte();
if (c == 0) break;
s.push_back(c);
}
return s;
}
}
struct FileHeader
{
uint32 Signature = 0x9E2A83C1
uint16 PackageVersion
uint16 LicenseeMode
uint32 PackageFlags
uint32 NameCount
uint32 NameOffset // Offset to name table
uint32 ExportCount
uint32 ExportOffset // offset to export table
uint32 ImportCount
uint32 ImportOffset // offset to import table
if PackageVersion < 68
uint32 HeritageCount
uint32 HeritageOffset // offset to heritable table
else
byte Guid[16]
uint32 GenerationCount
struct
{
uint32 ExportCount
uint32 NameCount
} Generations[GenerationCount]
endif
}
The package format was clearly inspired by DLL files (oh what a bad idea that was). Each package has objects in it, and references to objects in other package files. The export table lists all objects in the package, while the import table lists all objects referenced by the objects in the package. The name table contains all text strings in the package.
struct NameTableEntry
{
name Name
uint32 Flags
} NameTable[FileHeader.NameCount]
struct ExportTableEntry
{
index Class
index Base
uint32 Package
index ObjectName
index ObjectFlags
index Size
index Offset
} ExportTable[FileHeader.ExportCount]
The list of all objects in the package.
Class
and Base
are references to the object's class and base objects. If the reference is a positive number then it is an index to an entry in the export table. If it is a negative number then it is an entry in the import table. If the value is zero then it is null.
object GetObject(index objref)
{
if (objref > 0) return ExportTable[objref - 1];
else if (objref < 0) return ImportTable[-objref - 1];
else return null;
}
If Class
is zero then the object is a class object. Class objects are the classes in UnrealScript. Base
then points at the base class.
If Class
is not zero then the object is an instance of the class. Class
points at the class type of the object. For example, a texture has Class
pointing at the Texture
class object.
Size
is the byte size of the object in the package file. Offset
is the file offset where the object data begins.
struct ImportTableEntry
{
index ClassPackage
index ClassName
uint32 ObjectPackage
index ObjectName
} ImportTable[FileHeader.ImportCount]
This is the list of objects not present in this package. They are referenced by the Class
and Base
fields in the export table.
The exact rules for how to look up objects here is not fully understood yet. See the Package::GetUObject
and Package::FindObjectReference
functions in Surreal Engine's Package.cpp for how to do it. These rules were derived from the UShock project and why they have to look like they do is still unknown. There's also some hacky stuff going on here in the Unreal Engine where the UnrealI
and UnrealShare
packages share objects.
struct HeritageTableEntry
{
byte Guid[16]
} HeritageTable[FileHeader.HeritageCount]
No idea what this table was about. Most likely some kind of revision history baked into the package. Don't think it is used for anything.
All objects begin with the following header:
struct ObjectHeader
{
// Savegame objects serializes their unrealscript state
if ExportTable[obj].ObjectFlags & HasStack
index Node
index StateNode
uint64 ProbeMask
uint32 LatentAction
if (Node != 0)
index Offset
endif
endif
// Instanced objects serializes their unrealscript property values
if ExportTable[obj].Class == 0
propertyblock properties
endif
}
There are basically three different variants of the object header. First, there's class objects, which have an empty header. Then there are normal instanced objects (i.e. a texture or a level), they begin with an unrealscript property block. And then there's savegame objects, which also stores information about the unrealscript state for the object when being saved.
The data for each object type follows the header.
The property block is a list of properties terminated by a None
name:
while (true)
{
auto entry = NameTable[ReadIndex()];
if (entry.Name == "None") break;
Properties.Set(entry.Name, ReadPropertyValue());
}
Each property value begins with a small header:
struct PropertyHeader
{
UnrealPropertyType type
int arrayIndex
bool boolValue
index structName
int size
}
enum UnrealPropertyType
{
UPT_Invalid = 0
UPT_Byte = 1
UPT_Int = 2
UPT_Bool = 3
UPT_Float = 4
UPT_Object = 5
UPT_Name = 6
UPT_String = 7
UPT_Class = 8
UPT_Array = 9
UPT_Struct = 10
UPT_Vector = 11
UPT_Rotator = 12
UPT_Str = 13
UPT_Map = 14
UPT_FixedArray = 15
}
However, not all the fields of the header are always present. The header is decoded as follows:
uint8_t info = ReadByte();
bool infoBit = info & 0x80;
PropertyHeader header;
header.type = (UnrealPropertyType)(info & 0x0f);
if (header.type == UPT_Struct)
header.structName = ReadIndex();
switch ((info & 0x70) >> 4)
{
case 0: header.size = 1; break;
case 1: header.size = 2; break;
case 2: header.size = 4; break;
case 3: header.size = 12; break;
case 4: header.size = 16; break;
case 5: header.size = ReadByte(); break;
case 6: header.size = ReadUInt16(); break;
case 7: header.size = ReadUInt32(); break;
}
header.arrayIndex = 0;
if (infoBit && header.type != UPT_Bool)
{
int byte1 = ReadByte();
if ((byte1 & 0xc0) == 0xc0)
{
byte1 &= 0x3f;
int byte2 = ReadByte();
int byte3 = ReadByte();
int byte4 = ReadByte();
header.arrayIndex = (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4;
}
else if (byte1 & 0x80)
{
byte1 &= 0x7f;
int byte2 = ReadByte();
header.arrayIndex = (byte1 << 8) | byte2;
}
else
{
header.arrayIndex = byte1;
}
}
else if (header.type == UPT_Bool)
{
header.boolValue = infoBit;
}
For some of the property types the value is stored in the header itself. For the remaining types the data follows the header:
- UPT_Invalid: Should never be present in a package file
- UPT_Byte:
byte
follows - UPT_Int:
uint32
follows - UPT_Bool:
header.boolValue
contains the value - UPT_Float:
float
follows - UPT_Object:
index
follows (object reference) - UPT_Name:
index
follows (index intoNameTable
) - UPT_String:
byte[header.size]
follows - UPT_Class: Unknown. Not seen in 436
- UPT_Array: Unknown. Not seen in 436
- UPT_Struct:
header.structName
is the struct name of the data that follows - UPT_Vector:
float x,y,z
follows - UPT_Rotator:
uint32 pitch,yaw,roll
follows - UPT_Str:
name
follows - UPT_Map:
header.size
bytes follows - UPT_FixedArray:
header.size
bytes follows
header.arrayIndex
is the index of the value if the property is an array.
struct Bitmap : ObjectHeader
{
}
The bitmap object contains no data on its own. It is the base class of the Texture object.
struct Texture : Bitmap
{
byte MipsCount
struct Mipmap
{
if FileHeader.PackageVersion >= 63
uint32 OffsetToWidth
endif
index Size
byte Data[Size]
uint32 Width
uint32 Height
byte UBits
byte VBits
} Mipmaps[MipsCount]
if Properties.bHasComp == true
byte CompMipsCount
struct CompMipmap
{
if FileHeader.PackageVersion >= 68
uint32 OffsetToWidth
endif
index Size
byte Data[Size]
uint32 Width
uint32 Height
byte UBits
byte VBits
} CompMipmaps[CompMipsCount]
endif
}
The image format used by the texture is stored in the Format
object property. If the bHasComp
property exists and is true, then the texture contains additional data for a compressed version of the texture. The image format for the compressed version is stored in the CompFormat
property.
struct FractalTexture : Texture
{
byte Pixels[Properties.UClamp * Properties.VClamp]
}
Not really sure what this image is used for. Maybe IceTexture
or one of the other derived texture objects uses it for something?
struct FireTexture : FractalTexture
{
index SparksCount
struct Spark
{
byte Type
byte Heat
byte X
byte Y
byte ByteA
byte ByteB
byte ByteC
byte ByteD
} Sparks[SparksCount]
}
Fire textures are updated each frame by spawning sparks as described by the sparks array (particle generators on a texture). The Type
field of the spark sets what kind of particle effect each emitter creates.
struct Palette : ObjectHeader
{
index Count
uint32 Colors[Count] // Alpha,Red,Green,Blue
if FileHeader.PackageVersion < 66
alpha channel is not set in the Colors array
endif
}
The palette used by a 8-bit texture.
struct Font : ObjectHeader
{
if FileHeader.PackageVersion <= 63
Unknown
else
index PageCount
struct Page
{
index Texture // object reference to a Texture object
index CharacterCount
struct Character
{
uint32 StartU
uint32 StartV
uint32 USize
uint32 VSize
} Characters[CharacterCount]
} Pages[PageCount]
uint32 CharactersPerPage
endif
}
The font object describes where each character glyph is positioned in font textures.
struct Sound : ObjectHeader
{
index Format // Index into NameTable
if FileHeader.PackageVersion >= 63
uint32 SkipOffset
endif
index Size
byte Data[Size]
}
Sound effects. Format
is the file extension for the sound.
struct Music : ObjectHeader
{
index Format // Index into NameTable
if FileHeader.PackageVersion >= 61
uint32 SkipOffset
endif
index Size
byte Data[Size]
}
Tracker music. Format
is the file extension for the music.
struct TextBuffer : ObjectHeader
{
uint32 Pos
uint32 Top
index Size
byte Text[Size]
}
Unrealscript source code.