Question about bml files

tofuman

Administrator
Staff member
Yes they are compressed in PRS format. I made a util some time back which will decompress files from the cabinet file. Although I never got around to adding the code for inserting files into it. The format is really straight forward though.

You can download my app here: https://dl.dropboxusercontent.com/u/312 ... ractor.rar

The version above has the PRS decompression code.
 

kion

Garbage Human
Normally files inside the bml are packed into prs files, but with gamecube it looks like all of the files are packed into one larger file that needs to be compressed before the individual files can be extracted (i'm guessing).

Though specifically what I'm trying to get at is the EpII enemy animation .njm files. In PSOBB the .njm files for episodes 1&2 seem to be compressed in someway, so I'm looking through the gamecube files to see if there's a different approach to extract them and running into more of the same problems.
 

tofuman

Administrator
Staff member
I've extracted the njm files in the past and even edited them. But haven't worked with GC files. I'll see if I can decompress the files tonight.

EDIT: I've just tested extracting NJM from the PSOBB version of that file and it extracted and decompressed them fine. Although yes it seems GC is different and just crashes the file. Its caused by GC using big endian instead of the little endian that PSOBB uses. I could easily add support for GC files it's just a matter of switching to big endian.
 

kion

Garbage Human
I was able to extract a gamecube BML. My problem was that they crammed the files so close to each other that it looked like one huge continuous file. In other versions they were a lot more generous with the amount of space they put between each one.

I can extract them now by copying and pasting in a hex editor, and it doesn't seem like it would be too hard to write an extractor, but if it's easy to implement than having a tool would be ideal.

I went back and looked at the EPII .njm's. To be more specific, the problem isn't that I can't extract them, it's that Noesis doesn't render the file correctly. Here's an extracted rappy from Dreamcast v1 with the walk animation versus an extracted rappy from psobb doing the same animation.

SirH2As.png


Here are the sample files: http://www.filedropper.com/rappysample
The ep1_walk.njm works with both rappy models in Noesis, but the ep2 animations doesn't (to say the least). Not exactly sure what's going with PsoBB's animation format.
 

tofuman

Administrator
Staff member
The NJM's reference the nodes in the model. I assume that you are using the PSOBB model also with the PSOBB animation file?
 

kion

Garbage Human
EP1 animation works with both models. EP2 animation shows the same result for both models.
 

kion

Garbage Human
Starting to look at the episode 2 njm files. It looks like they are the exact same file, just in a slightly different formation.

The dreamcast .njm files have NMDM on the front and then a pointer to the end of the file. This does not seem to be part of the actual file definition and as such all of the pointers need to be adjusted for this change by adding 8 to them.

The first three 4 bytes are, pointer to the motion list, number of frames and type. Type generally seems to be more or less always defined as "03000200".

typedef struct {
void *mdata; /* Array for object tree */
Uint16 nbFrame; /* Number of motion frames */
Uint16 type; /* Motion element bit string */
} NJS_MOTION;

The motion data pointer points to an array of type NJM_MDATA

typedef struct {
void *p[1]; /* Motion pointer */
Uint32 nb[1]; /* Number of keyframes */
} NJS_MDATA1;

though i'm not sure on the actual size of the structure. As it seems to go:
{
pointer: 4 bytes,
null: 4 bytes,
frames : 4 bytes,
null : 4 bytes
},

also there's an offset from the first pointer, it goes {pointer,null, frames, null} for the first entry, followed by 20 empty bytes before the net entry. After that all of the entries are 16 bytes.

As for the data that those pointers point to the actual motion data seem stupidly simple. I think they go by
{
key_frame_number : 4 bytes
keys : [3](float+)
}

I wrote float(+) for the floats, since it seems to have two bytes for the actual floats, followed by two useless bytes. They will either be FFFF or 0000. I tried replacing all occurrences of FFFF in a file to 0000 and there seemed to be no change in the animation playback in Noesis. This might sound complicated, but once you see it, it's stupidly simple.

Here's an mdata entry:

8C180000 00000000 28000000 00000000
Pointer is: 188C
Frames: 28(40)

So going to 188C we see:
00000000 FEFFFFFF A0110000 B0450000
01000000 FEFFFFFF F6110000 C1450000
02000000 FEFFFFFF DC120000 EB450000
03000000 FEFFFFFF 29140000 23460000
04000000 FEFFFFFF B4150000 5A460000
05000000 FEFFFFFF 53170000 85460000
06000000 FEFFFFFF DE180000 96460000
07000000 FEFFFFFF 2B1A0000 81460000
08000000 FEFFFFFF 111B0000 38460000
09000000 FEFFFFFF 671B0000 B0450000
0A000000 FEFFFFFF 271B0000 D7440000
0B000000 FEFFFFFF 791A0000 B3430000
0C000000 FEFFFFFF 75190000 56420000
0D000000 FEFFFFFF 37180000 D3400000
0E000000 FEFFFFFF D8160000 3F3F0000
0F000000 FEFFFFFF 73150000 AB3D0000
10000000 FEFFFFFF 20140000 2B3C0000
11000000 FEFFFFFF FB120000 D23A0000
12000000 FEFFFFFF 1D120000 B4390000
13000000 FEFFFFFF A0110000 E3380000
14000000 FEFFFFFF 89110000 50380000
15000000 FEFFFFFF C1110000 DF370000
16000000 FEFFFFFF 34120000 90370000
17000000 FEFFFFFF D2120000 61370000
18000000 FEFFFFFF 89130000 53370000
19000000 FEFFFFFF 46140000 65370000
1A000000 FEFFFFFF F9140000 96370000
1B000000 FEFFFFFF 8E150000 E7370000
1C000000 FEFFFFFF F5150000 56380000
1D000000 FEFFFFFF 1B160000 E3380000
1E000000 FEFFFFFF 01160000 B0390000
1F000000 FEFFFFFF B7150000 D23A0000
20000000 FEFFFFFF 4A150000 313C0000
21000000 FEFFFFFF C3140000 B93D0000
22000000 FEFFFFFF 2C140000 533F0000
23000000 FEFFFFFF 90130000 EA400000
24000000 FEFFFFFF F9120000 68420000
25000000 FEFFFFFF 71120000 B6430000
26000000 FEFFFFFF 04120000 C1440000
27000000 FEFFFFFF BB110000 70450000

First four bytes counting up to the number of frames, followed by three floats, with two bytes on the end of each one being either FFFF or 0000. I'm probably missing a lot of nuances, but as far as episode 1 goes, this seems to be the basic pattern.
 

kion

Garbage Human
To continue the conversation (with myself), why do i think episode 1 .njm's and episode 2 .njm's are actually the same file?

For starters, episode 1 animations work on episode 2 models just fine. If there was something inherently different about them, this probably wouldn't be the case.

Secondly, it's sega. I don't think they would go back and redo all of their animations in a new format. They tend to pile stuff on as they go.

And lastly looking at the pointer references with the motion data list mentioned above, we get the following.

Episode 1 rappy walk njm
[ { pointer: 44, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 668, unkown1: 0, frames: 18, unkown2: 0 },
{ pointer: 956, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 1516, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 2156, unkown1: 0, frames: 30, unkown2: 0 },
{ pointer: 2636, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 3196, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 3820, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 4428, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 5068, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 5676, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 6284, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 6924, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 7548, unkown1: 0, frames: 37, unkown2: 0 },
{ pointer: 8140, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 8780, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 9420, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 10028, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 10668, unkown1: 0, frames: 33, unkown2: 0 },
{ pointer: 11196, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 11836, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 12476, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 13116, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 13756, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 14316, unkown1: 0, frames: 40, unkown2: 0 } ]

episode 2 rappy walk .njm
[ { pointer: 1068, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 1380, unkown1: 0, frames: 18, unkown2: 0 },
{ pointer: 1524, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 1804, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 2124, unkown1: 0, frames: 30, unkown2: 0 },
{ pointer: 2364, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 2644, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 2956, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 3260, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 3580, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 3884, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 4188, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 4508, unkown1: 0, frames: 39, unkown2: 0 },
{ pointer: 4820, unkown1: 0, frames: 37, unkown2: 0 },
{ pointer: 5116, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 5436, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 5756, unkown1: 0, frames: 38, unkown2: 0 },
{ pointer: 6060, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 6380, unkown1: 0, frames: 33, unkown2: 0 },
{ pointer: 6644, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 6964, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 7284, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 7604, unkown1: 0, frames: 40, unkown2: 0 },
{ pointer: 7924, unkown1: 0, frames: 35, unkown2: 0 },
{ pointer: 8204, unkown1: 0, frames: 40, unkown2: 0 } ]

We get the exact number of frames for the same animation as we did before. The difference is that if we look up the corresponding pointer for the example in the previous post, we get this unholy mess:

0000FEFF A011B045 0100FEFF F611C145
0200FEFF DC12EB45 0300FEFF 29142346
0400FEFF B4155A46 0500FEFF 53178546
0600FEFF DE189646 0700FEFF 2B1A8146
0800FEFF 111B3846 0900FEFF 671BB045
0A00FEFF 271BD744 0B00FEFF 791AB343
0C00FEFF 75195642 0D00FEFF 3718D340
0E00FEFF D8163F3F 0F00FEFF 7315AB3D
1000FEFF 20142B3C 1100FEFF FB12D23A
1200FEFF 1D12B439 1300FEFF A011E338
1400FEFF 89115038 1500FEFF C111DF37
1600FEFF 34129037 1700FEFF D2126137
1800FEFF 89135337 1900FEFF 46146537
1A00FEFF F9149637 1B00FEFF 8E15E737
1C00FEFF F5155638 1D00FEFF 1B16E338
1E00FEFF 0116B039 1F00FEFF B715D23A
2000FEFF 4A15313C 2100FEFF C314B93D
2200FEFF 2C14533F 2300FEFF 9013EA40
2400FEFF F9126842 2500FEFF 7112B643
2600FEFF 0412C144 2700FEFF BB117045

Which is why Noesis freaks the hell out when trying to play the file, because it's trying to do so in the format posted from the dreamcast 1 data.
 

kion

Garbage Human
Okay a few hours and 128 lines of code later, I think I have a episode 2 .njm to episode 1 .njm converter that mostly works (not garanteed).

Tuh72et.png


Usage:
node script.js <file.njm>

Still in debug mode, so it just exports to output.njm.

var fs = require("fs");
var util = require("util");

if(process.argv.length != 3){
console.log("Usage node edit02.js <file.njm>");
process.exit();
}

var buffer = fs.readFileSync(process.argv[2]);
if(!buffer){
console.log("Could not open file");
process.exit();
}

const NMDM = "4E4D444D";
const NULL = "00000000";
const TYPE = "03000200";

var data = "";
var offset = 12;
var hex, struct, pos, length, frames, head;
var motion_list = [];
var eof = buffer.readUInt16LE(4);
var pointer = buffer.readUInt16LE(8);
var number_frames = buffer.readUInt16LE(12);

console.log("EOF pointer: %d", eof);
console.log("Motion data: %d", pointer);
console.log("Number of frames: %d", number_frames);

pointer = pointer + 8;
var first = true;
while(pointer < eof){

struct = {
pointer : buffer.readUInt16LE(pointer),
unkown1 : buffer.readUInt16LE(pointer+4),
frames : buffer.readUInt16LE(pointer+8),
unkown2 : buffer.readUInt16LE(pointer+12)
};

pointer += 16;

if(struct.unkown2 && !first)
break;

motion_list.push(struct);

if(first){
console.log(struct);
first = false;
pointer += 4;
}

}

console.log(motion_list);

for(var i = 0; i < motion_list.length; i++){
if(!motion_list.pointer)
continue;

pos = motion_list.pointer + 8;
length = motion_list.frames * 8;
hex = buffer.toString("hex", pos, pos+length);
hex = hex.match(/.{1,4}/g)

for(var v in hex){
if(hex[v].lastIndexOf("ff") > 1){
hex[v] = hex[v].substr(0,2) + "0000ff";
}else{
hex[v] = hex[v] + "0000";
}
}

motion_list.offset = offset;
offset += hex.length * 4;

data += hex.join("");
}

for(var i = 0; i < motion_list.length; i++){
if(!motion_list.pointer){
data += util.format("%s%s%s%s",NULL,NULL,NULL,NULL);
continue;
}

pos = le_string(motion_list.offset, true);
length = le_string(motion_list.frames);

data += pos;
data += NULL;
data += length;
data += NULL;

if(!i){
data += NULL;
}
}


offset = le_string(offset);
frames = le_string(number_frames);
head = offset + frames + TYPE;
data = head + data;

eof = le_string(data.length / 2);
head = NMDM + eof;
data = head + data;

fs.writeFileSync("output.njm", data, {encoding:"hex"});

function le_string(num){
var h = num.toString(16);

if(h.length%2)
h = "0" + h;

h = h.match(/.{1,2}/g);
h = h.reverse();

h = h.join("");

while(h.length < 8)
h += "0";

return h;
}
 
Top