Alternative OPT File
Reading
Last Updated
July 18, 1998
By 3DOXprt
Introduction
This document wouldn't exist if it weren't for the hard work of my buds, Stealthjedi and Lt. Hag. Also, I would like to thank all others who have contributed to the OPT Project since its birth.
This document is to serve as a guide to creating a robust X-Wing vs. Tie Fighter OPT File reader using a method that we will call Jump Blocking. I have a tendency for dramatics, so you will have to forgive the storybook style in which I present this article. I would like to make one thing clear, and that is that this is an exercise not a solution. Our understanding of OPT Files is literally changing every hour, as such it would be wrong if I said this is how to read OPT files, this is simply my preferred method. Visit code-alliance.com to get all the latest news on this file format.
A Description
OPT Files can easily be seen as horrendously complex and convoluted. In fact they are, if you try to read them at face value. We will try to read them as I believe they were meant to be.
As stated in the Unofficial OPT Specs, an OPT is a complex collection of jumps (offsets to different parts of the file) and Headers. A Header could be anything from a mesh declaration, to a grouping of faces. We have found, and declared, many such headers. But it has gotten to the point where some groups of data are so nested away within the file that our naming conventions are tiring and failing.
I describe the OPT file as having three distinct parts: a Header, Jump Blocks, and Data Blocks. There is only one header in the file, but there can be any number of Jump Blocks and Data Blocks. I have written a paradigm that I use to use to explain the format: Jump Blocks are simply 'way stations' creating a hierarchy that is beneficial to an animation engine but serve no other purpose. Each Jump Block should be followed through for the sole purpose of searching for Data Blocks. Data Blocks contain specific information about the object and are terminators of the Block Hierarchy.
I will use a 32-bit C++ naming convention through the file. Types used include:
int | 32-bit Signed Integer |
float | 32-bit Floating-point value |
char | 8-bit Signed Integer |
a[x] | An array of x items of type a from 0 to x-1 |
The OPT Format
Like all other files, we begin with the header. The OPT File Header takes this form:
int | FileStart | Always 0xFFFFFFFF |
int | FileSize | Excluding FileSize and FileStart (-8) |
int | GlobalOffset | Very important value used to locate Jump offsets |
char[2] | <noname> | Always {2, 0} |
int | NumJumps | |
int | JumpToJumps | A Jump to the first Jump in the list of Jumps |
int[NumJumps] | Jumps | The List of Jumps |
I need to take a minute discuss Jumping and the role of the GlobalOffset. A Jump is simply an address to another part of the file. The catch is, they are inflated by GlobalOffset. In order to see where in the file a jump points to, you must first subtract the GlobalOffset. It is also very important that you subtract 8 from the read GlobalValue to find the one that we will use.
Furthermore, whenever you see a "JumpToJumps", "JumpToFloats" or likewise, use that value. Do not simply assume that the Jumps will immediately follow the JumpToJumps.
A detail: In our desire to organize OPT files, we have taken the stand that the Jumps declared in the header of the file represent individual meshes (collections of vertices, and faces). This is a good way to organize your own reader.
It's time now to declare the Jump Block structure. This is a key element to reading OPTs. As stated, A Jump Block is simply a collection of Jumps. These Jumps could point to Data Blocks or to yet another Jump Block. You should assume no patterns exist in the files.
int | <no name> | Always (usually) 0 |
int | <no name> | Always (usually) 0 |
int | NumJumps | |
int | JumpToJumps | A Jump to the first Jump in the list of Jumps |
int | <no name> | Always (usually) 1 |
int | ReverseJump | |
int | <no name> | Always (usually) 1 -- Does not always Exist! |
int[NumJumps] | Jumps | List of Jumps |
This is not the most complex structure. In fact, the ReverseJump is our only new piece of data. The ReverseJump acts a bit like the GlobalOffset. It exists to maintain the hierarchy. A child JumpBlock (that is, it is pointed to by this Block), can subtract its ReverseJump to get back to this Block (its parent). As of now, we have found no real use for these numbers, but who knows what the future will hold.
The last part of the file are Data Blocks. Although all Data Blocks have a different structure (because they hold different types of data), they each have a similar header. Their header is only two longs:
int | BlockBegin | Usually 0 (Exception is TextA Blocks) |
int | Identifier | Tells what kind of Data Block this is |
Not so bad eh? The Identifier is the most important part. It tells us what kind of Data Block this is (vertex, face, texture, hardpoint, etc) and from this we can dispatch the data to different functions. We would have a function ReadVertexBlock(), ReadFaceBlock(), and so on.
We currently know of these types of Data Blocks:
1 | 0x01 | Face Data |
3 | 0x03 | Vertex Data |
7 | 0x07 | Texture "B" Blocks |
11 | 0x0B | Vertex Normals |
13 | 0x0D | Texture Vertices |
20 | 0x14 | Texture "A" Blocks |
21 | 0x15 | Face Grouping Block |
22 | 0x16 | Hardpoints Block (Turrents, Blasters) |
23 | 0x17 | Insertion Offset ? |
25 | 0x19 | Minimum/Maximum Mesh Information |
Please consult the OPT Unofficial Specs for specific information on these Blocks.
How to Load an OPT File
OK, so now we know all about how an OPT File is structured. It's time that we begin reading them. This section is short because the read process using Jump Blocking is a relatively easy and straightfoward process.
The process can be summed into the following process:
So basically, we just process jumps until we have found a Data Block. If we find that this Jump Block is really a data Block, then we call the appropriate data loader function (ReadVertexBlock(), etc.). This continues until all jumps have been read, and the hierarchy has expired.
A C++ Class to Load OPT Files
Enough theory, let's create a class to do the loading. For simplification, the class will only support reading vertices, but don't fret, adding other data readers will be a snap.
The first class I wrote I named COptBlock. That is, because it can act either as a Jump Block or a Data Block. It is programmed to check which one it is and take the appropriate action. If it is a Data Block, it simply determines which data loader function to call, and that function is responsible for loading its data into the mesh pointed to by the COptBlock. If it is a Jump Block (second int == 0), then it creates the appropriate number of COptBlocks and calls the Read() and Process() functions of each. This creates and maintains a file hierarchy of classes while still completely independent of the order of things within the file. Here is the declaration of the COptBlock:
class COptBlock { public: int goff; FILE *pfile; int filepos; COptBlock *jumps; int njumps; int jumpsoff; int reverse; COptMesh *mesh; int Read(); int Process(); void CallDataBlockReader(int ind); void ReadVertexBlock(); COptBlock(); ~COptBlock(); }; |
Everything should look familiar. goff is the GlobalOffset that has to be passed down to the Block, pfile is a pointer to the open OPT File, filepos is the file offset to this Block, jumps is a pointer that may or maynot be used to hold other COptBlocks, njumps is the number of jumps to be read/processed, jumpsoff is the offset to the list of jumps, reverse is the ReverseJump used to go back to the parent (though not supported in this class). Read() determines what type of Block this is (Data or Jump), calls CallDataBlockReader() if it is a Data Block, otherwise, it loads the various jumps. Process() will call the Read() and Process() functions for each Jump in this Block.
Included are the memory managing constructors and destructors:
COptBlock::COptBlock() { jumps=0; njumps=0; } COptBlock::~COptBlock() { if (jumps) delete[] jumps; } |
Nothing really complex there. Only one note: After constructing a COptBlock, it is imparative that you set the goff, pfile, filepos, and mesh members to their proper values. As you can see, there is no initialization for their values.
The Read() function is the brains of the COptBlock object:
int COptBlock::Read() { // Seek to the beginning of this Block fseek(pfile, filepos, SEEK_SET); // Check if this is a Jump Block or Data Block int ind; fseek(pfile, 4L, SEEK_CUR); fread(&ind,sizeof(ind),1,pfile); if (ind > 0) { CallDataBlockReader(ind); return 0; } // Read the number of jumps fread(&njumps,sizeof(njumps),1,pfile); fread(&jumpsoff,sizeof(jumpsoff),1,pfile); jumpsoff -= goff; // Read the reverse fseek(pfile, 4L, SEEK_CUR); fread(&reverse,sizeof(reverse),1,pfile); // Read the jumps jumps = new COptBlock[njumps]; fseek(pfile, jumpsoff, SEEK_SET); for (int i=0;i<njumps;++i) { fread(&jumps[i].filepos,sizeof(int),1,pfile); jumps[i].filepos -= goff; jumps[i].goff = goff; jumps[i].pfile = pfile; jumps[i].mesh = mesh; } return njumps; } |
This code, too, is pretty straight-forward. Read the second int, call the DatBlock dispatch function if necessary, or simply read the jumps.
Next in line is the CallDataBlockReader() which simply decides which function to call:
void COptBlock::CallDataBlockReader(int ind) { switch (ind) { case 3: ReadVertexBlock(); break; } } |
Of course, this function would be much longer if we were loading more Data Blocks. But, this is adequate for our purposes.
And now we have the Process() function. This will simply call the COptBlocks that we created in the Read() function.
int COptBlock::Process() { for (int i=0;i<njumps;++i) { jumps[i].Read(); jumps[i].Process(); } return njumps; } |
Finally, we have our Vertex Data Block Reader. This function simply appends the vertices declared in it to the vertex list already contained in the COptMesh (which I will declare later). The appending process is quite ugly, but I truly do not believe it will ever be called. Though, you will have to use a similar method when dealing with faces, for there are usually multiple face groups.
void COptBlock::ReadVertexBlock() { fseek(pfile,filepos,SEEK_SET); // Get the number of vertices int nv; fseek(pfile,16L,SEEK_CUR); fread(&nv,sizeof(nv),1,pfile); // Grow our vertex list for this mesh int i; if (mesh->verts) { // create the new list to a temp pointer COptVertex *tp = new COptVertex[mesh->nverts+nv]; // copy the old list to the new list for (i=0;i<mesh->nverts;++i) tp[i] = mesh->verts[i]; // kill the old list delete[] mesh->verts; // point it to the new list mesh->verts = tp; } else { mesh->verts = new COptVertex[nv]; } // Get the position of the floats int flts; fread(&flts,sizeof(flts),1,pfile); flts -= moff; // Read all of the floats fseek(pfile,flts,SEEK_SET); for (i=0;i<nv;++i) { fread(&(mesh->verts[mesh->nverts+i].x),4,1,pfile); fread(&(mesh->verts[mesh->nverts+i].y),4,1,pfile); fread(&(mesh->verts[mesh->nverts+i].z),4,1,pfile); } // Update the list count mesh->nverts += nv; } |
Like I said, the vertex "appending" process is pretty crude, if you know of a better technique, please post on the message board at code-alliance.
So that's all there is to reading an OPT! The rest of the code I post is just the details :-) First, I need to declare COptMesh and COptVertex and their constructors:
class COptVertex { public: float x,y,z; COptVertex(); }; class COptMesh { public: COptVertex *verts; int nverts; COptMesh(); ~COptMesh(); }; COptVertex::COptVertex() { x=y=z=0.0f; } COptMesh::COptMesh() { verts = 0; nverts = 0; } |
No Mysteries there either. It's basically just a container for our datd to keep it all nice and tidy. If you had further developed these classes, you would add pointers to face data, texture data, and whatever else. Now, we have only one class left to define, and that is the main class. This is the one you would be dealing with if you wrote a OPT File reader. COptFile is simply a container for the meshes, and has a function (Load()) that reads the OPT Header and gets the reading process going. The constructor for this class automatically loads the file, and the destructor takes care of all our memory issues.
class COptFile { public: char filename[128]; FILE *pfile; COptBlock *jumps; COptMesh *meshes; int njumps; int nmeshes; int goff; int jumpoff; COptFile(char *fn); ~COptFile(); void Load(); }; COptFile::COptFile(char *fn) { // Initialize jumps = 0; meshes = 0; njumps=nmeshes=0; // Load the file strcpy(filename,fn); Load(); } COptFile::~COptFile() { if (jumps) delete[] jumps; if (meshes) delete[] meshes; } |
The Load() function is very similar to the COptBlock::Read() function, but also handles the opening and closing of the file:
void COptFile::Load() { // safeguard types used if (sizeof(int) != 4) { return; } // Open the file pfile = fopen(filename,"r"); if (pfile == 0) { return; } // Read the Master Offset (moff) fseek(pfile,8L,SEEK_SET); fread(&goff,sizeof(goff),1,pfile); goff -= 8; // Read the number of master jumps/meshes fseek(pfile,2L,SEEK_CUR); fread(&njumps,sizeof(njumps),1,pfile); nmeshes=njumps; meshes = new COptMesh[njumps]; jumps = new COptBlock[njumps]; // Read the jumps offset fread(&jumpoff,sizeof(jumpoff),1,pfile); jumpoff -= moff; // Read the jumps fseek(pfile, jumpsoff, SEEK_SET); for (int i=0;i<njumps;++i) { fread(&jumps[i].filepos,4,1,pfile); jumps[i].filepos -= goff; jumps[i].goff = goff; jumps[i].pfile = pfile; jumps[i].mesh = &(meshes[i]); } // Process the jumps for (i=0;i<njumps;++i) { jumps[i].Read(); jumps[i].Process(); } // Close the file fclose(pfile); } |
Guess what? We're done! We can now easily and efficiently read all the vertices out of an OPT File. Using this class is as easy as typing one line of code: COptFile opt("c:\\xwing.opt");
In Closing
I hope that this document will help you as a programmer understand the OPT File format. Even if you do not use the described approach, I hope that maybe this document will help clarify a couple things for you.
Until next time, May the Force be With You.
This code is released to the OPT Project, if you make modifications that you feel improve it, please post them on the message board at code-alliance.
You may spread this document but you may NOT modify it in ANY form.
X-Wing vs. Tie Fighter is (c) LucasArts Entertainment Co. and Totally Games.