Here's what I do in the PKBlaster program:
Magic
I expect all PKB files to start with the following integers (little endian):
0xc9000b11
0x01040202
0x0000e14a
I don't know what this means, but I found this sequence at the start of every PKB file that I investigated.
Then I read:
Code:
FileHeader {
int32 firstMagicIdentifier;
int32 stringDataOffset;
int64 secondMagicIdentifier;
}
I don't know what the first and second magic IDs are, but I preserve them when modifying files, for good measure. As I recall, it looks like the second magic ID was often something roughly equal to 2x the length of the file in bytes, plus a little bit more. I'm not sure, so I just hold these values intact when loading and saving. I never change the length of a file, so I luck out and don't have a problem.
Nodes
Until you reach the byte index specified by "stringDataOffset", we first want to read the nodes.
The nodes have a very similar structure between them, but in order to parse their contents you would need to know all of the node types by name, which I do not know.
Code:
Node {
int32 byteLength; // exclusive with regards to itself, this is the sum byte length of all other data in Node
int8 magic32ValueByte; // should always contain 0x20, or "32" in decimal
int32 messageTypeStringKey; // match against string table to know the name of the type of the node
int16 fieldCount; // see "Interpretation" below
{data bytes}
}
For me, the parsing of these nodes takes place in a simple loop that reads until we reach
stringByteOffset
. In java, that looks like this:
Java:
while (buffer.position() < stringDataOffset) {
final int length = buffer.getInt();
final byte magic32ValueByte = buffer.get(buffer.position());
if (magic32ValueByte != 0x20) {
throw new IllegalStateException("Not 32 bit (0x20): " + magic32ValueByte);
}
final int messageType = buffer.getInt(buffer.position() + 1);
final short fieldCount = buffer.getShort(buffer.position() + 5);
final ByteBuffer chunkContentsBuffer = ByteBuffer.allocate(length - 7);
chunkContentsBuffer.clear();
for (int i = 7; i < length; i++) {
chunkContentsBuffer.put(buffer.get(buffer.position() + i));
}
nodes.add(new Node(messageType, chunkContentsBuffer));
buffer.position(buffer.position() + length);
}
Strings
The strings table, as its name implies, is just a giant list of all strings used in the file.
This can be read by reading:
Code:
StringsTable {
int32 stringCount;
{data}
}
To read the data, use a for loop that runs
stringCount
number of times. At each step, read 1
uint8
to represent the length of the string, then read N chars where N equals the length value uint8 that you just read in. So, strings are not padded and are not consistent in how long they are, and they can only be up to 255 bytes in length.
Interpretation
All of the above specification was something I invented by just hunting and tinkering with a hex editor and the contents of my Reforged installation. This is not exactly reasonable, but after I read on the PopcornFx website that you cannot change the color of an emitter in your Reforged Map without emailing Blizzard to ask for the source asset -- because modifying the compiled assets is too complicated and they don't support it -- I didn't feel like dealing with that company or even investigating their software that was used to make these files since they are anti-modding.
Obviously that would be great if you could parse out the "data bytes" of what I call the "Nodes". Actually I don't even know if they should be called "Nodes". You can see in the PKBlaster program source code on GitHub that in the source I called them Chunks even though by the time I created a UI for it, I was calling them "Nodes". Just now after ranting about PopcornFx in the last paragraph, I visited their website and found in their documentation wiki a screenshot of their "Editor UI" in a section called "Nodegraph" so maybe calling them Nodes is reasonable. Here's what that image looked like:
So, we're dealing with this spaghetti of UI wires between the nodes, probably. What I found is that what I refer to as {data bytes} in my definition of the nodes has a consistent format but it only makes sense if you have the full internal spec of the PKB that I do not have. It appears to be exactly
fieldCount
number of repeating groups that are kind of like key/value pairs. I assume they exist to represent the lighter colored boxes inside the big boxes above, or something like that. But the problem is that they are all of the form:
Code:
Field {
int16 fieldType;
{data}
}
The format does not include the length of the data, unfortunately, as far as I can tell. If you can imagine, in order to parse this, you have to know the length of all field types. So you need application/business logic in order to do the parsing. There seem to be quite a lot of the field types and these files can get quite large, and sometimes the field will contain more data with dynamic size in itself depending on what it means to the application.
At this point in my program to attempt to do recolor shenanigans on this format, what I decided to do is only try to make a list of the Field types necessary for Nodes who have
stringTable[node.messageTypeStringKey] == "CParticleNodeSamplerData_Curve"
. These are Nodes whose type, as far as I understand based on that name, is a Node Sampler Data. In some circumstances, these sampler data nodes will include contents that can define color, and this was done often enough in Reforged that we can use it for recolors to achieve reasonable results.
Here are the list of
CParticleNodeSamplerData_Curve
fields that I allow for recoloring in my hacked together PKBlaster solution:
Code:
Type 0 {
int32 unknown1;
int32 unknown2;
}
Type 7 {
int32 unknown;
int32 propertyIndex; // maybe indexes into the list of nodes, or of strings, I forget
}
Type 9 {
int32 unknown;
}
Type 10 {
int32 unknown;
}
Type 14 {
int32 unknown;
}
Type 16 {
int32 numberOfFloats;
float32 floats[numberOfFloats];
}
Type 17 {
int32 numberOfFloats;
float32 floats[numberOfFloats];
}
Type 18 {
int32 numberOfFloats;
float32 floats[numberOfFloats];
}
So, again that looks like a lot of "unknown" data but this is actually incredibly useful because I am using it for the length of the fields, so that I know how much to skip and can go on to process other fields.
To perform a recolor of the file, given a destination color, I will only change the above data in the case of Field Type 17 when we have
numberOfFloats==12
, or in the case of Field Type 18 when we have
numberOfFloats==24
. In both cases, I consider the floats to be repeating groups of RGBA, RGBA, RGBA...
It's not foolproof and I got reports of times when this just randomly messed up the color of some PKBs, but in many cases these floats do end up being RGBA or possibly a rate of change of RGBA, probably. Not from experience but just by guessing looking at the data I assumed there might be some of these "rate of change of RGBA" values so in all of my recolor solutions I attempt to keep the sign and total magnitude of these floating point values intact while just shifting their color distribution for the most part.