[Jongware]
Most recent update (All By Hand(TM)): 30-Dec-2006 19:19

 

Starry, starry night
Paint your palette blue and gray
Look out on a summer's day
With eyes that know the darkness in my soul...

Don McClean, Vincent

 

The Frontier Galaxy VII: Frontier Objects

As mentioned first in Part II: Distant Suns, the 3D objects in the game are all referenced by a single number in a single large list. All of the objects in the entire galaxy! That means everything from a humble Escape Pod (est. size: 4 meters) up to the map locator grid (est. size: 4ly).

The game is quite clever in the way 3d objects are represented: a single structure defines each and everyone of them by size, shape and color. The coordinates are simple integers, but one of the members of this structure is Scale; the coordinates defining the object are multiplied by this (actually, more like shifted left).

The 3D shapes are drawn by a clever routine, reading sort of a command language that defines it as we go. The language can make objects be drawn differently for size, distance, rotation and even external parameters, such as time or global game preferences.

The commands in Frontier:First Encounters are clearly based on those in Frontier:Elite 2 -- there are a few small differences, mostly to do with suddenly having megabytes of memory instead of kilobytes; if you visit the FrontierVerse you can find my original doc outlining the FE2 objects. This one is all about FFE.

You might want to download the companion program ShowMesh which I wrote to, er, "disassemble" the models into ASCII text, but this produces a long, long listing which won't mean anything to you -- yet! Read on...

 

As usual, I'll start with the base structures. And (also as usual) there are again a few unknowns in here...

The Object List

In FE2 there are three different lists of objects: the main objects appearing in the game proper (starships, planets, cities, you name it), a list of specials (vector fonts and the tombstone when you failed to dodge something lethal), and the intro objects (an Explorer happily dodging Eagles and the odd space station). In FFE there is only one big list in which you can find everything.

The list itself is nothing but a long list of pointers to individual objects. There appears to be no definition of how many objects there are in the list (apparently those who create it knew how many there were). Each pointer points to either zero (no object for that number; hopefully not used in the game!), or to a Model structure.

The Model Structure

Every single 3D model is defined with exactly the same structure. If you ever wondered how it is possible to use an asteroid by way of spaceship, read on! It's just a matter of plugging in the right numbers in the right locations.

Each object starts like this:

typedef struct {
    unsigned short * Mesh_ptr;
    signed char *    Vertices_ptr;
    int              NumVertices;
    signed char *    Normals_ptr;
    int              NumNormals;
    int              Scale;
    int              Scale2;
    int              Radius;
    int              Primitives;
    color_t          DotColor;
    char             padding;
    int              field_28;
    int              field_2C;
    int              field_30;
    unsigned short * Collision_ptr;
    ShipDef_t *      Shipdef_ptr;
    int              LinefeedCharacter;
    unsigned short * Character[1];
} Model_t;

Mesh_ptr points to the start of the drawing commands

Vertices_ptr points to the start of the vertex coordinates

NumVertices is the number of coordinates times 2. The vertices defined here are the even-numbered ones. The odd numbered have their x coordinate negated.

Normals_ptr points to Normals. These are used to determine if a certain surface is visible at the time of drawing. It may be 0 if the current model doesn't need visibility checking.

NumNormals is the number of normals; also times 2, the same as with the vertices, but this gets an additional plus 2. If the number is "2", there are zero normals; if it is "4" there are actually 2 (and only one will appear in the code, the other gets flipped from this one).

Scale should be applied to the coordinates by shifting them left with this number. The smallest objects have a scale of 3 (pilot's head; clock in church), the largest possible value appears to be 16h (22), and with this value Scale2 seems to kick in.

Scale2 ... actually, a guess. It is 0 for most "smallish" objects, and larger for stars, planets and (weird) the map locator crosses.

Radius -- at some point in my life I was convinced this was the largest x coordinate, but checking against the coordinates this doesn't seem to be true. Ah well.

Primitives has to do with the number of surfaces of this model, tho' I don't know where it is used...

DotColor is an RGB triple defining the color of the dot drawn when an object is too far for real 3D. If the red component of the color (the lowermost byte) has bit 7 set (the entire word and 0080h) the object is "shiny" -- from far it will be displayed as a star. If not, it will be displayed as a single small dot. Stars, orbital stations and the crashed Thargoid have this bit set, planets and ships do not (except for the Thargoid Transporter, and the INRA Command Ship -- the latter displays a black star).
It may also contain extra information in the red byte if the object emits light. This value may be in the range of 10 to 50, and sets the ambient color Red, Yellow, White, Cyan or Blue. See the page on Bitmaps and Colors for more information.

padding is a single byte, usually zero, and pads the structure to words again.

field_28 is an unknown; usually set to a smallish value (about 10).

field_2C is another unknown, usually set to some word value.

field_30 is yet another unknown. It may be set to the same value as field_2C and may also be -1 for reasons unknown.

Collision_ptr is either zero, or points to a word containing the value '0', or to a small array of unknown values. I read somewhere its name was 'collision' but can't remember where (it might have been the Teddy sources).

Shipdef_ptr is either zero (model is not a ship) or it points to a ShipDef_t structure. See below.

LinefeedCharacter is the vertex number which transforms the current text cursor point one line down. Add to your original x,y and z-pos and hey! You're there. See the command TEXT for more.

Character[1] is the first of an array of drawing commands for each individual character in the font. It is zero when not a font; if it is a font, any number of offsets may follow.

Additional structures

Structure color_t is a simple RGB triplet, containing true color values (0..255) for red, green and blue. This structure is used in a few places, sometimes with and sometimes without a padding byte following it. Where padding is required I've seperated it from this structure.

ShipDef_t is -- by FFE standards -- a well-known structure. By altering this values, you can change just about every item of interest on your own ship! Beware, though: giving your Cobra a forward thrust of 100g will also make every pirate flying one deadly fast! (That'll teach you to cheat!)

typedef struct {
    short ForwardThrust ;
    short RearThrust ;
    char  Gunmountings ;
    char  FuelScoop ;
    short Mass ;
    short Capacity ;
    short Price ;
    short Scale ;
    short Description ;
    short Crew ;
    short Missiles ;
    char  Drive ;
    char  IntegralDrive ;
    short EliteBonus ;
    short Camera1_x, Camera1_y, Camera2_x, Camera2_y ;
    short frontMount_x, frontMount_y ;
    short rearMount_x, rearMount_y ;
    short topMount_x, topMount_y ;
    short bottomMount_x, bottomMount_y ;
} ShipDef_t ;

ForwardThrust is a signed int. To convert it to gees divide by 545 (you may need a decimal point). Check out the specs for missiles: 60g!

RearThrust is also a signed int, and usually a negative number (!). Also divide by 545 to get gees. Looking at the code it seems this same value is used -- negated to get it positive again... -- for the side thrusters.

Gunmountings is... the number of gun mountings! Any number between 0 and 4; this is a hardcoded limit, so you can forget hacking in a Death Star with a thousand turrets.

FuelScoop defines if the scoop can be mounted. Either 0 (nope) or 1 (yep).

Mass is the "fully laden" value in the Shipyard screen. I seem to recall it is used to calculate fuel consumption (Confirm?).

Capacity is the "capacity" value in the Shipyard screen.

Price is the "new" price in kilocredits; mutiply with one thousand to get the price in the Shipyard screen.

Scale relates to the size of the Scale bar in the Shipyard screen. It is only used there, not for collision testing or anything.

Description is a number into the ubiquitous string pool. It is usally a number >= 4000h ("Mine") but zero for one model: the Asteroid -- though why this one needs a ShipDef_t structure remains a mystery. Because it is a freely moving object? Because it needs a crew of 1?

Crew -- the number of crew needed including the captain, so at least 1. This also includes the Asteroid and, for reasons not to be guessed at, the mines and missiles.

Missiles -- Number of missile pods on this ship. Zero for the IP Shuttle, up to 8 for the Panther Clipper. This max is hardcoded in the games, so forget your Battlestar Galactica dream.

Drive is the default "Drive Fitted". Values from 0 ("None") up to 14 ("Turner Class Drive").

IntegralDrive -- If zero the drive is removable, Integral otherwise (Shuttle, Lifter, Imp Courier, etc.)

EliteBonus gets added to your Elite score when you shoot one of this models down. Fighters, Missiles, the Escape pod, and the Lifter add 256 (the latter two probably never appear as agressive opponents...), freighters usually score low.

Camera1_x, y and Camera2_x, _y are the camera positions for forward and backward view (though it seems from the code Camera2_* is not used, and cam pos 1 is mirrored).

frontMount_x and y, and rearMount_*, topMount_* and bottomMount_*: these are the locations of the guns on the 3D model. The coordinates are not defined in the same scale as the ships themselves; instead of by Scale they should be multiplied by 8 (?). Also, note the order: if there is only one gun mounting, it is taken to be the first -- front. The second is always back mounted, the next two top and bottom. It is possible the top and bottom coordinates are y and z coordinates -- the turrets appear always centred on the ship.

Vertex and Normal definitions

These are arrays of 6 bytes each. For vertices, the first byte determines the type of coordinate, then a padding byte follows, next are 3 signed characters for x,y and z, and closed with another padding byte.

typedef struct {
	unsigned char type;
	unsigned char pad1;
	signed char x,y,z;
	unsigned char pad2;
} Vertex_t ;

As said above, this structure only defines the even-numbered vertices. Whereever you need an odd numbered vertex, first calculate the even one, then copy it for the odd vertex and negate the x value.

There are a lot of different types; I don't know the meaning of all of them. A few:

Type 1 is a normal vertex and defines a 3D point as usual everywhere in the rest of the world.

Type 5 is a negate-all vertex. The z member is another vertex number; this one gets the same x, y and z coordinates, all negated. The odd-numbered vertex has its x coordinate negated as usual.

Type 7 is a weird one, it is used when this point should ramble around at random. Experimenting with it, it seems the x value here is the number of the actual vertex (taken from the same list) serving as base, and the z value is some max amount of random movement.

Type 11 is another pretty weird one. Its y and z values are taken as vertex numbers, and this point should be exactly inbetween those. The x member appears to be (mostly) 1, and is not used.

Type 19 uses yet another feature: external variables! The variable number is in the x value, and its value should be interpolated between the vertices indexed by the y and z values. I take it to be something as

; As seen in the Adder model:
#52/34h:  19, -63, 46, 50   // Interpolate(50,46, LOCAL[1])

where 52/34h is this vertex number;

19 is the type

-63 is the variable number (C1h in hex);

45 is the first coordinate to interpolate between;

50 is the other coordinate.

The C0h prefix means "use local variable"; what remains is "1", so, use the value of local variable #1 (whatever that is at the moment). I'm not sure on the max value for the variable and whether they can have a sign; my wireframe model display program is broken (again!) so I can't try it out.

Actual coordinate values can be defined recursively, where, for example, point A is halfway between point B, which is calculated from a variable, and point C, which is fixed. When drawing, you'll need to calculate them one by one. Also note that for every odd vertex number, you'll need to calculate the even member first and then apply the negative x thingy.

 

The Normals also have a "type" value -- this is, in fact, a vertex coordinate, which should be used as the reference point for visibility checking. I got it working somehow by rotating the normal and this reference vertex, and then check if their scalar(?) value > 0 ... sort of. If you take any normal x,y,z and calculate SQRT(x2 + y2 + z2) you always get something near 127. Odd-numbered normals are mirrored along their x-axis, just like the vertices.

The additional +2 is because "no normal needed" is encoded in the objects as the number '0'. If a normal is needed, its number goes up from 2 (and you should take normal number minus 2 from the array for its values).

Variables

The variables are either 16-bit words from the Global pool (variables defined in the game itself, such as "Display grid lines"; also, things like "Do you have a Scanner?"; "Which missile is in mounting #2?"; "How much throttle for the rear thruster?") or 32-bit long words from a small local pool. This local pool seems to get defined once for each separate object-to-be-drawn, and its contents can be adjusted in the drawing code. It is used for intermediate calculation results and for correct positioning of sub-objects (which don't get their own local pool [confirm that? -- it seems logical, though]).

If a variable is needed somewhere, you must inspect the bits to see where to get its value from (or store to). The def bits are the two highest ones, so masking with C0h there are four possibilities:

    00h: immediate value in range 0..63
    40h: large immediate value. Shift left 10 bits for anything from 0 to &FC00
         (in steps of &400).
    80h: a value from the Global pool, numbered 0..63. Variable #0 is the TIME,
         as can be seen in some animated objects
    C0h: local variable, numbered 0..6. Should be initialized somewhere before being used!

The game uses local variables 0..6 only. The results of math calculations are always put into the local pool (oddly, the code doesn't seem to require the C prefix for the destination variable, although the game always uses it).

The global variable list gets filled for each model instance. A model instance is a single unique occurrence of any model in the current game; if you see three Vipers on your screen, each one is a single instance of the Viper model. This can be seen in the (far from complete!) list of global variables, where you can find the unique id of each model:

 0: Game Time/1024
 1: Landing State, Flags
 2: Unique_Id, low word
 3: Unique_Id, high word
 6: Game Time, low word
 7: Game Time, high word
 8: Game Date, low word
 9: Game Date, high word
10: Planet Orbital Radius
...
13: Current (left?) side thrust
14: Current (right?) side thrust
15: Current Main thrust
...
22: Equipment bytes 0 and 1 (bits are set for equipment)
23: Equipment bytes 2 and 3
27: Front and Rear Gun types
28: Top and Bottom Gun types
29: Missile 0 and 1 types
30: Missile 2 and 3 types
31: Missile 4 and 5 types
32: Missile 6 and 7 types
33: Missile 8 and 9 types
34: Hull Mass
35: Number of Shields

Variable number 0 is the game time divided by 1024. The game runs internally on 49710 ticks per second; this is sort-of conversion to get a 50 fps timer resolution.

Some variables can have different meanings depending on what model they are for.

A few graphic commands use a variant of the variable retrieval routine, where either an immediate value or a variable is needed:

    00h and 40h: immediate value of ((Value & 0x7f)<<9)+((Value >> 7) & 0x1ff)
    80h: Global one as above
    C0h: Local one as above

Command codes

The structure member Mesh_ptr points to an array of words, which draw the model itself. There are 32 different commands; the command number is in the lower 5 bits of the low byte of the word. The remaining bits may define additional data.

The byte order for these words is Intel-mode; I stuck to it when I realized it was a complete mess. There are cases where high and low bytes should be swapped, and others where they shouldn't.

I'll try to give a relevant example for each command, extracted using my proggie ShowMesh. If you'd like to see all definitions-as-I-understand-them, download and run it. It requires to be in the same directory as an original firstenc.dat (size 2,101,248 bytes) as provided in, amongst others, JJFFE, and it is a console program, meaning you have to read very fast or redirect its output into a file. A number of text editors can run an external program and capture its output automatically; Jongware's personal recommendation is TextPad. If you don't have one of those, redirect the output to a file like this: in a command window, type showmesh > output.txt and hit Enter; output.txt can be viewed with any program which loads plain text.

The first block after the command name are the parameters word-wise; if a word should be splitted into bytes, they are shown with a vertical bar inbetween, high byte on the left side, low byte on the right.

The example code dump is in hex, its comments are in decimal. In the explanations it should be clear from the context where I switch between those!

0 = End

0000

Example: in just about every model.

0000 ; End

In model #0 (Vector Font):

0720 ; End

End. Stop. Halt. Do No More.

Usually the entire word is 0000h, but in the vector fonts each separate character ends with some value in it. This value (shifted right by 6) points to a vertex in the font model. This vertex is used only to transform the text cursor position to the right -- e.g., transform this vertex, then add its x, y, and z to your current text position. That's the next character's position. If a character value is less than 32 (space) you should insert a linefeed -- its transformation value is the vertex referenced by the LinefeedCharacter field in the model definition.

Every individual model should end with the End command. If a sub-object is ended, you should continue drawing its parent.

1 = Ball

0001 MATERIAL RADIUS VERTEX|NORMAL
A simple example first; the common mine, if it is far away:
0001 0666 8025 2200 ; Ball(RGB(6,6,6), Radius(19200), Vertex(34), Normal(NORMAL_NONE))
NORMAL_NONE in the comment is my own predefined value for normal number 0 and means, well, no normal, so don't bother about doing any math. Written out in full in my comments as a reminder that there is no normal.
In the Escape Capsule (its big yellow light):
0001 2EE0 E02E 0482 ; Ball(UNLIT | COLOR_YELLOW, Radius(24000), Vertex(4), Normal(2 | LIGHT))

The ball is coded to use normal #2 here, but by looking at the code I understand the normal value isn't used at all... The bit 80h in the normal defines a LIGHT, which should be drawn starry-like when viewed from afar, and in two colors when viewed a-near.

2 = Line

0002 MATERIAL STARTPOINT|ENDPOINT

An easy one. Check this out (half of the graphics font '+' when viewed from afar; the other half looks the same but uses other vertices):

0002 3000 070D ; Line(LOCALCOLOUR | UNLIT, 13,7)

Don't ask about the MATERIAL parameter and the corresponding color comment; this will be explained later.

A line can always be seen from every angle so it does not use a Normal (that would be Abnormal).

3 = Triangle

0003 MATERIAL POINT1|POINT2 POINT3|NORMAL

Also, an easy one. A single triangle from the StowMaster Fighter looks like this:

0003 4034 0809 0204 ; Triangle(Texture_034h, 8,9,2, Normal(4))

It's a fair bet you can optimize your drawing code if you first calculate the normal and check if the triangle is visible. If so, fetch the coordinates for the vertices (including properly re-calculating them, and in the case of coordinate number 9 here, negating its z), transform those to the screen and blit.

4 = Square

0004 MATERIAL POINT3|POINT1 POINT2|POINT4 0|NORMAL

It'll get complicated later on; this one is ee-zee too, apart from the vertex order. See this snippet (top [or bottom] from the Lifter):

0004 4016 0605 0704 0002 ; Square(Texture_016h, 5,7,6,4, Normal(2))

The zero aside the normal is not used. The vertex order is "heuristically derived", that means "I guessed it". If you get a strange x shape instead of a square swap coordinates until it works OK.

5 = Polygon

0005 MATERIAL NBYTES|NORMAL [POLYCMD]+ 0000

Previous commands to easy for you? This one is a even-odd filled polygon command, including holes and curves, and may intersect itself (hence the "even-odd"). It took me a fair bit of hacking before I got to grips with it; still haven't figgered out all possibilities.

NBYTES is the number of bytes that follow the 6 byte header, including the double-zero at the end. POLYCMD words are actually bytes, where every first byte defines the type of line and is followed by a fixed number of vertex numbers per each type.

First is first in reading order in this hex dump! It's the first byte of an Intel-type word, so it is actually the second byte in the original code (and its first parameter byte is the first byte of the pair).

I'll give examples of each sub-type since it is probably (somewhat) easier to understand that way.

 

Object #312:Six-sided Landing pad:

0005 2444 0E00 0400 0002 0604 0605 0603 0601 0000
; Poly(UNLIT | COLOR_GRAY, Normal(NORMAL_NONE),
	StartLine(0,2),
	ContinueLine(4),
	ContinueLine(5),
	ContinueLine(3),
	ContinueLine(1),
	EndPoly() )

Following the command code 0005 itself and the required material 2444, the first word is 0E00: there are 14 additional bytes (count'em), and this object uses no normal (if you see the landing pad from the bottom up you descended just a tad too fast).

The first byte is a 04, meaning it is a Start Line. It has two additional bytes: the start coordinate (which is in the same word, the 0) and the end coordinate (next word, low byte: 2). The high byte of the second word is not used, and should be skipped.

The next byte is 6, meaning a Continue Line. It has one additional byte, obviously the next coordinate.

The next bytes are again 6s, drawing the next three lines. Now you have five sides of the hexagon defined.

The last command, 0000 ends the polygon. Because the last line doesn't end at the starting point you'll have to close it yourself; always do so with a straight line. Fill with a nice shade of gray and you're done.

 

Object #35:Gyr attack fighter:

0005 0353 0C02 020A 0A0C 0D0B 0617 0616 0000
; Poly(RGB(3,5,3), Normal(2),
	StartCurve(10,12,13,11),
	ContinueLine(23),
	ContinueLine(22),
	EndPoly() )

This one starts at the fourth word with a 02: a Start Curve, from coordinate 10 to 11 and using coordinates 12 and 13 as control points. For reasons unknown, the coordinate 10 is repeated in the first word; the next word defines the start and end, followed by the control points.

 

Object #34:Viper Defence MkII:

0005 4028 0E0F 0203 030F 0D07 0809 1715 0603 0000
; Poly(Texture_028h, Normal(15),
	StartCurve(3,15,13,7),
	ContinueCurve(9,23,21),
	ContinueLine(3),
	EndPoly() )


This ship has so many vertices that the model gets obscured by the numbers, so they are not shown here.

This one also starts with a curve (the words 0203 030F 0D07), but the next byte is 08, for a Continue Curve, where the first point is the last one from the previous command. This means an additional three points are needed: the end point is the number 9 in the same word, the next word 1715 defines the two control points.

The next line is a straight one, from the last point of the curve to coordinate number 3. Since this closes the figure nicely there is no close line needed.

 

So far every polygon started with a Start type and continued with continue types. It is possible to make intersecting figures with these (on the side of the "2001" orbiting station there are gray markings which do just this); it is also possible to forcibly end a segment and start a new set of lines, creating a hole.

0005 3000 1800 0228 2826 2729 0809 0828 0A00 020E 0E0A 0B0F 0811 100E 0000
; Poly(LOCALCOLOUR | UNLIT, Normal(NORMAL_NONE),
	StartCurve(40,38,39,41),
	ContinueCurve(9,8,40),
	EndSegment(),
	StartCurve(14,10,11,15),
	ContinueCurve(17,16,14),
	EndPoly() )

Here we have the letter 'O' in the vector font (object #0), which should have a big hole in the middle. Ah, there it is! Command 0A (with a zero in its low byte) ends the current segment; the next segment should start with either StartLine or StartCurve (fortunately, it does so in every polygon where it is used).

Easy to decode, hard to draw. To draw the big texts in my map program FFEStarSys I tried several different polyfill routines but found none which could handle both curves and holes with grace and speed. I resorted to cheating: I converted the entire vector font to EPS images and from that to a real TrueType font, and use FreeType to display it.

 

The final subtype is to add a Circle to the polygon, using 0C and apparently followed by a center vertex, a normal, and a radius. The clock on the tower in Object #369:Church is defined as a circle:

0005 0888 0602 0C12 0902 0000
; Poly(COLOR_WHITE, Normal(2),
	Circle(Vertex(18),Normal(2),Radius(9)),
	EndPoly() )

... on each of the four sides, but I have no real clue on how to work with it! I can't get anything useful on my screen in the right orientation.

To make it just that more complicated, the circle command can be used more than once in a single polygon command:

Object #61:Boa Freighter

0005 20EE 0E0A 0C0E 030A 0C0F 030A 0C10 030A 0000
; Poly(UNLIT | COLOR_CYAN, Normal(10),
	Circle(Vertex(14),Normal(10),Radius(3)),
	Circle(Vertex(15),Normal(10),Radius(3)),
	Circle(Vertex(16),Normal(10),Radius(3)),
	EndPoly() )

... draws three circles at once. I've seen no combination of "regular" polygons and circles in one single polygon, so this one may be a quick hack courtesy of the original coders...

 

It is entirely possible to define a 3-dimensional shape with a single polygon command; the Imperial Courier does this. But since there is a single Normal defined for the entire plane you can get unexpected results when you rotate such an object: the polygon is sometimes drawn when it shouldn't and sometimes not when it should. See also the Imp Courier, where its sides appear and disappear just before they should. A new version really needs Bezier patches for this!

6 = TRANSFORMATION?

XXX6 [ADDITIONAL]*

Used a lot. It sets or resets the active transformation matrix. Very complicated stuff. Maybe I'll tell you about it later...

There may be data in the 11 unused bits (shown as Xes above) and some additional words may follow, which clearly belong to the same command but are also unknown. Looking at the code, I've deducted that the remaining bits define how much bytes there are, in this way:

switch (originalCmd >> 13)
{
    case 1: Skip bytes until you see a FFh, then read another word
    case 2: No additional bytes
    case 3: Skip bytes until you see a FFh, then read another word
    case 7: if (originalCmd >> 5) is one of
        2045: two additional bytes
        2046: none
        2047: none
        default: two additional bytes
    default: no additional bytes
}

... and I'm not even sure if I'm right ...

Wanna see examples? All taken from the original code!

0006 ; Cmd 6 [nop]
4746 ; Cmd 6 [2]
FFE6 ; Cmd 6 [7]: 2047
6FC6 FF26 002C ; Cmd 6 [3]
E406 FCE0 ; Cmd 6 [7]: 1824
E8E6 FFE3 ; Cmd 6 [7]: 1863

... where the bracketed number is my sub-classification, and the purpose of the number (derived from its parameters) is unknown.

7 = Mirror Triangle

0007 MATERIAL POINT1|POINT2 POINT3|NORMAL

Two triangles for the price of one in the Sidewinder:

0007 4027 0002 0406
        ; Triangle(Texture_027h, 0,2,4, Normal(6))
        ; Triangle(Texture_027h, 1,3,5, Normal(7))

The first triangle is defined the same way as number 3, the normal triangle; for the second one, use mirror coordinates and normal. This does not mean 'take the next one' (coordinate number 1 becomes #2) but instead flip the lowest bit (mirror of number 1 is number 0 and vice versa). The same holds for the normal.

8 = Mirror Square

0008 MATERIAL POINT1|POINT2 POINT3|POINT4 0|NORMAL

Two squares, also for the price of one, in the generic missile:

0008 1000 1604 1802 0008
        ; Square(LOCALCOLOUR, 22,4,24,2, Normal(8))
        ; Square(LOCALCOLOUR, 23,5,25,3, Normal(9))

Pretty much the same as the mirror triangle.

9 = Pine

0009 MATERIAL STARTPOINT|ENDPOINT XXXX

Should be named 'ellipsoid' but I like the name 'pine' coz it's used for a patch of pine trees. The XXXX may define the width; I'm not sure how to draw an ellipsoid so can't tell how it works out. If you take a close look to the texture mapped engine flames, you can see it's probably drawn as two intersecting bezier curves, with an additional circle thrown in if you look straight on it.

This pair of jet flames comes from the Eagle LR fighter:

0009 20EE 302A 04B0 ; Pine(UNLIT | COLOR_CYAN, ..)
0009 20EE 312B 04B0 ; Pine(UNLIT | COLOR_CYAN, ..)

A side note: the pine trees aren't texture-mapped, but jet flames are. Pretty weird, 'cause their color is usually defined to be plain cyan.

10 (0Ah) = Text

000A MATERIAL FONT_SCALE|NORMAL ORIENTATION|VERTEX TEXTID

The byte FONT_SCALE is composed of 4 high bits FONT and 4 low bits SCALE. FONT is one of 0 (Vector font), 1 (Graphics font: windows and doors), or 2 (Times-like bitmap font) -- these are the actual object numbers of the font objects in the main list. SCALE is a number between 1 and 15, where "1" is very small, as in the "RADIATION PERSISTS EVEN IF ENGINES HAVE COOLED" poster on the back of the Cobra Mk III [*], and 15 is extremely large, as in the station name near the black&white domes -- large enough to land on.

[*] Did anyone ever notice that? In the game when zooming in you can barely see something is there, but I remember a close-up in a games magazine where it was big enough to read! Imagine my surprise when I found it is actually there. This Is Not A Red Herring!

The VERTEX defines the left starting point for the text string; it goes to the right from there. Where text seems centred it is just positioned carefully. The ORIENTATION value is the same-ish as used elsewhere.

There is something odd about the orientations. Transforming the matrices to cater for rotation and mirroring toggles a single bit flag on and off, which is only checked in the DrawText routine. This flag determines if the current total of rotations would yield the text upside down, and adjusts the matrix accordingly. The problem is, every time I change just about anything in the relevant code, some of my texts are upside-down or mirrored, where others stay in the correct position!

The TEXTID is a value in... the string pool! Seen as constants such as 406Fh ("Rockforth Legal Academy" <grin> -- the former academy, that is), 407Ch ("RIP"), 9C00h ("GAME OVER"), and 40C6h ("FIRST ENCOUNTERS"). It may also contain variable texts: 9C01h ("COMMANDER \xff\x30\x16 DIED \xFF\x30\x41", where the 0xff's introduce even more variables, to be expanded before this string is drawn) and 3016h (usually just a ship identifier code).

The Graphics font consists of windows and doors; they point in the code to text strings like "0J" and more of this ilk, and are pasted onto the sides of buildings.

As an example: the identifier on the side of the Interplanetary Shuttle:

000A 0000 0607 4612 3016
    ; Text(COLOR_BLACK, Normal(7), Vertex(18), Scale(6), VECTOR_FONT, 3016h)

... where 3016h is the ship identifier; and the text on a jettisoned cargo box:

000A 0888 0406 4218 3022
    ; Text(COLOR_WHITE, Normal(6), Vertex(24), Scale(4), VECTOR_FONT, 3022h)

... where 3022h presumably holds the "current contents" string.

11 (0Bh) = Skip If Not Visible/Skip If Further Than

dddB DISTANCE_OR_NORMAL

Two commands in one. If DISTANCE_OR_NORMAL has bit 15 cleared (& 8000h equals 0), the command is this:

dddB DISTANCE

(where, obviously, DISTANCE should be less than 32768, or 8000h in hex).

If the bit is set the command is

dddB 80h | NORMAL

If the NORMAL is not visible, or if the current DISTANCE to the model (in whatever units you might be working...) is further away than the given one, the ddd byte-and-a-half is the number of bytes that should be skipped right after this command and its parameter word[*]. A special case is distance "000"; this is the same as END.

[*] The skip bits used are the 11 top bits. Shifting the command right by 5 (to get rid of the command code) yields the number of words to skip, but since every command code and its parameters are always words I used an easy explanation. There are another 2 commands which use this same mechanism; their codes have a '1' in the upper nibble, so using this same text there will not work.

Example: in the vector font, the comma won't be displayed if it gets very small:

       008B 0258 ; if (DISTANCE > 600) goto L137
       0003 3000 000A 0C00 ; Triangle(LOCALCOLOUR | UNLIT, 0,10,12, Normal(NORMAL_NONE))
L137:  0620 ; End

(This could have been coded as 000B 0258, but in the fonts END apparently holds an important value, so the longer route is taken.)

In the generic missile, first the body of the missile gets drawn using a few rectangles. Then:

000B 0248 ; if (DISTANCE > 584) end

..and the tail fins are drawn. After that:

000B 01A4 ; if (DISTANCE > 420) end

.. and the fins at the warhead are drawn. The further away the missile is, the less is drawn!

The Not Visible command works quite the same. A check is made if the Normal is visible; if not, bytes are skipped.

A common example, taken here from the Sidewinder, is whether to display sub-objects:

       004B 8002 ; if !Visible(Normal(2)) goto L161
       188E 4012 ; Subobject(196:ECM Antenna, Vertex(18), Orientation(40h))
L161:  ...(more code)

Normal number 2 apparently points upwards, and if you can't see the top of the Sidewinder there is no point in drawing the ECM antenna there.

12 (0Ch) = Skip If Visible/Skip If Closer Than

dddC DISTANCE_OR_NORMAL

Again, two commands in one. If DISTANCE_OR_NORMAL has bit 15 cleared (& 8000h equals 0), the command is this:

dddC DISTANCE

(where, again (and hopefully again obvious), DISTANCE should be less than 32768, or 8000h in hex).

If the bit is set the command is

dddC 80h | NORMAL

Pretty much the same as the previous command, just the other way around. I'll suffice with a few examples.

Again one from the generic missile (see also #11 above). It actually starts with these commands:

008C 0334 ; if (DISTANCE < 820) goto L37
0002 1000 1800 ; Line(LOCALCOLOUR, 0,24)
0000 ; End

Meaning, if you are up close to this missile, fill in the details (and hope it is your own missile); otherwise, draw just a tiny line.

And a snippet of the Imp Explorer:

      02AC 8018 ; if Visible(Normal(24)) goto L493
      0004 4034 3632 3830 002A ; Square(Texture_034h, 50,56,54,48, Normal(42))
      0003 4034 3034 3E36 ; Triangle(Texture_034h, 48,52,62, Normal(54))
      0003 4034 3234 3838 ; Triangle(Texture_034h, 50,52,56, Normal(56))
      0003 4034 3438 3E3A ; Triangle(Texture_034h, 52,56,62, Normal(58))
      0003 4034 3036 3E3C ; Triangle(Texture_034h, 48,54,62, Normal(60))
L493: ...(more code)

... where, apparently, normal #24 points in the other direction -- the one you're not facing.

13 (0Dh) and 29 (1Dh) = Math

DEST|(OPERAND|0Dh) SOURCE1|SOURCE2

Looks complicated, doesn't it? DEST, in the high byte of the command, is a variable, either a global or a local one. The low byte of the command has an operand type in the higher four bits, and the lower four bits are always 0Dh. This command is grouped with command 1Dh so the bit in the upper nibble can be used in the operand type.

SOURCE1 and SOURCE2 are the two sources, and can be a variable or a constant.

The possible operands and their results are the following:

 0: SOURCE1 + SOURCE2
 1: SOURCE1 - SOURCE2
 2: SOURCE1 * SOURCE2
 3: SOURCE1 / SOURCE2
 4: SOURCE1 >> SOURCE2 ; Logical shift
 5: SOURCE1 << SOURCE2
 6: MAX(SOURCE1, SOURCE2) ; Not entirely sure...
 7: MIN(SOURCE1, SOURCE2) ; Ditto...
 8: SOURCE1 * 10000h / SOURCE2
 9: SOURCE1 SAR SOURCE2 ; Arithmetic shift
10: SOURCE1 unknown_op SOURCE2 ; ...
11: SOURCE1 if SOURCE1 is less than SOURCE2, 0 otherwise ; Not sure...
12: SOURCE1 if SOURCE1 is greater than SOURCE2, 0 otherwise ; Not sure...
13: SOURCE1 * SIN(SOURCE2)
14: SOURCE1 * COS(SOURCE2)
15: SOURCE1 AND SOURCE2 ; Bitwise AND

I'm quite unsure about all of this!

The input for the SIN and COS are probably in the range 0...16383, there is a sin/cos table of that size somewhere in the code. And the rest... Don't take my word for it.

The Skeet Cruiser has a big humping thing on the top; it is moved using

C35D 0A80 ; LOCAL[3] = TIME << 10
C3DD C35F ; LOCAL[3] = 31744 * SIN(LOCAL[3])
C31D 5FC3 ; LOCAL[3] = LOCAL[3] - 31744

where TIME is Global Variable #0 and LOCAL[3] is used to interpolate between the top and bottom coordinates of the big thingy. The Turner Class uses the same code to wiggle its tail.

If your ship has a scanner fitted, it is rotated using

C35D 0980 ; LOCAL[3] = TIME << 9
103C 00C3 ; Rotate -- default 00C3
18CE 060E ; Subobject(198:Scanner antenna, Vertex(14), Orientation(06h))

(but see the Rotate command for second thoughts on that)

14 (0Eh) = Subobject

objE ORIENTATION|VERTEX ; Bit 7 of ORIENTATION clear
objE ORIENTATION|VERTEX VERTEX2|VERTEX1 VERTEX4|VERTEX3 ; Bit 7 of ORIENTATION set

This is a sub-object -- it's a regular object from elsewhere in the list, with the number in the high 11 bits in obj. It should be drawn with the origin on VERTEX.

If bit 6 (40h) of ORIENTATION is set, the light position should be updated(?). The remaining 6 bits define three rotations and three mirror operations. Unfortunately, this orientation defies human understanding (as I know it), so my missiles and scanners tend to get drawn upside down or pointing the wrong direction.

There is also the special case of orientation 06. I haven't checked the maths, but apparently this combination is worthless. Instead, if the value 06 is used, the actual orientation of the object should not be taken from the default model matrix but from an extra matrix, which is set with command 0006.

 

If the high bit of ORIENTATION is clear the object is to be drawn normally. The Lifter has three sub-objects Cargo hanging under it. Each one is drawn "hanging" from its own vertex at the bottom of the Lifter:

C20D 8382 ; LOCAL[2] = GLOBAL[2] + GLOBAL[3]
C1FD 1FC2 ; LOCAL[1] = LOCAL[2] & 31
132E 0B24 ; Subobject(153:Cargo, Vertex(36), Orientation(0Bh))
C24D 05C2 ; LOCAL[2] = LOCAL[2] >> 5
C1FD 1FC2 ; LOCAL[1] = LOCAL[2] & 31
132E 0B26 ; Subobject(153:Cargo, Vertex(38), Orientation(0Bh))
C24D 05C2 ; LOCAL[2] = LOCAL[2] >> 5
C1FD 1FC2 ; LOCAL[1] = LOCAL[2] & 31
132E 0B28 ; Subobject(153:Cargo, Vertex(40), Orientation(0Bh))

Only local variables 0, 1, and 2 are passed on to the sub-object; in addition, LOCAL[0] is passed on as LOCAL[0]+1 (for reasons unknown, but hey! it works).

The Cargo object examines the variable LOCAL[1] to determine if it should draw a cargo box, a huge sphere, or something like an A-bomb (two spheres inside a frame).


The engine cones are drawn as circles on their start and end points.

 

Missiles and other equipment bits and pieces are always drawn as sub-objects, where a global variable is tested to check if this equipment is actually in your possession. The missile itself is also an interesting example. I mentioned the "generic missile" a few times. All missiles refer to the same object; this is then drawn as a sub-object, where a local variable defines the colors of the missile, and for the rest they are all the same.

 

If bit 7 of ORIENTATION is set, four more vertices follow the command. These vertices are passed on to the sub-object as vertices 251 to 254 (these special vertex numbers should only be used in "real" subobjects). In a few sub-objects the number "255" pops up as vertex. this is the original origin vertex supplied in the subobject command.

An example is the Imperial Courier engine pod. It's connected to the ship with a square. In the Engine Pod one finds:

0004 0666 FDFC FE1A 0000 ; Square(RGB(6,6,6), 252,254,253,26, Normal(NORMAL_NONE))

This engine pod only defines 34 vertices, so the numbers 252 to 254 are out of range. They should be taken from the values passed on from the main definition of the Courier, where it looks like this:

1AEE 9028 2614 7F24
    ; Subobject(215:Imperial Courier engine pod, Vertex(40), Orientation(10h), 38,20,127,36)

Note that extra vertex number 4 has a value of 127, which is way out of range for the coordinates of the Courier, but since this particular coordinate isn't used in the engine pod it is apparently never calculated and thus not a problem.

15 (0Fh) = Never used!

000F

What can I say? Since this code is never used, I can't tell you anything about it. We'll have to wait for Elite 4 and hope it pops up there.

16 (10h) = Open Cone

0010 ...

The command codes are a re-interpretation of those in Frontier:Elite 2 (with a few small differences), and there this code defines a cone without top and bottom caps. The engines on the Lifter, for example, are huge grey cones, and if you rotate the Lifter you can see they are open. In FFE this command is unused; the next command is used whereever a cone is deemed necessary.

17 (11h) = Cone

0011 MATERIAL VERTEX2|VERTEX1 NORMAL1|RADIUS1 NORMAL2|RADIUS2 MATERIAL1 MATERIAL2

The cone is drawn between VERTEX1 and VERTEX2; its diameter at the start and end are RADIUS1 and RADIUS2. The color of the cone's side is the first MATERIAL; the circle cap at the one point has the color MATERIAL1, and the other one MATERIAL2. If NORMAL1 is not visible, this end cap should not be drawn; the same for the other cap with NORMAL2.

From the Puma Clipper (one of the tiny engine cones at the back; there are actually three of'em):

0011 4008 100E 0106 0286 0000 20EE
    ; Cone(Texture_008h, Vertex(14), Vertex(16),
        Radius(6), Normal(1), COLOR_BLACK,
        Radius(134), Normal(2), UNLIT | COLOR_CYAN)

18 (12h) = DisplayText

VERTEX|12 TEXTID

The VERTEX number resides in the 11 top bits of the command; at this location, draw the text from the string pool in the 8x8 bitmap font. That's what I understand.

The proverbial example should be the IMRA Command Ship, where apparently

0012 4041 ; DisplayText (0, "\n\n Vaccine Carrier")

this appears. Being not such a wonderful Elite Commander, I wouldn't know because I never got that far.

Thumbing through my mental notes: I once traced the full code to draw a sector map in FFE, and I seem to recall that object #171 (Dummy Star Sector, with 128 zero vertices and not a lot more) gets filled with actual 3D star coordinates and the names of the stars are pasted on using this command.

There is another strange object (one of the rotating space stations) which uses this snippet:

0312 40C9 ; DisplayText (24, "12")
03D2 40CA ; DisplayText (30, "15")
0352 40CB ; DisplayText (26, "13")
0332 40CC ; DisplayText (25, "12x")
0372 40CD ; DisplayText (27, "13x")

Anyone seen that in the game?

19 (13h) = Skip If Bit Clear

ddd|13 BIT|VARIABLE

ddd is the number of words (see the note on Cmd #11) to skip if VARIABLE AND (1 << (BIT-1)) is zero. BIT should be a value in the range 1..16, so only words can be tested. If BIT is 0 it tests if VARIABLE is equal to 0.

If the skip distance ddd is zero this command ends drawing the current model.

The following snippet controls the alternating blinking landing lights in the Eagle LR fighter:

       01F3 0680 ; if !(TIME & 32) goto L220
       01B3 0580 ; if !(TIME & 16) goto L220
       0006 ; Cmd 6 [nop]
       0093 0480 ; if !(TIME & 8) goto L214
       0001 20E0 E001 1080 ; Ball(UNLIT | COLOR_GREEN, Radius(960), Vertex(16), Normal(0 | LIGHT))
L214:  0094 0480 ; if (TIME & 8) goto L220
       0001 2E00 E001 1180 ; Ball(UNLIT | COLOR_RED, Radius(960), Vertex(17), Normal(0 | LIGHT))
L220:  ... (more code)

Where TIME is my definition of Global Variable #0 (and don't pay attention to the 0006, it seems to do nothing here).

20 (14h) = Skip If Bit Set

ddd|14 BIT|VARIABLE

Exactly the same as the previous code, only in reverse. ddd is the number of words (see the note on Cmd #11) to skip if VARIABLE AND (1 << (BIT-1)) is zero. BIT should again be a value in the range 1..16, and the special value 0 tests if VARIABLE is not equal to 0.

If the skip distance ddd is zero this command ends drawing the current model.

21 (15h) = Process Vertex?

ddd|15

This command takes the vertex in the upper 11 bits ddd and... does something? I'm quite sure it is a vertex number, see these two snippets from the Osprey:

0615 ; Process vertex #48?
006E 0030 ; Subobject(3:Generic Missile, Vertex(48), Orientation(00h))
0635 ; Process vertex #49?
006E 0031 ; Subobject(3:Generic Missile, Vertex(49), Orientation(00h))

..where right after this command something is done using the same vertex number.

The highest bit may be set, and in that case the number is definitely not a vertex number... Also in the Osprey:

F015 ; Nop

...where, arguably, the comment "Nop" should have been "Nop????"

22 (16h) = Curve

0016 MATERIAL POINT1|POINT2 POINT3|POINT4 0000

I used to call this command "Bezier" but got mail telling me off! Well, "curve" is such a generic description, hope I don't tread on someone's toes with that.

Use this to draw a nice curvy curve between POINT1 and POINT4 using POINT2 and POINT3 as control points (the latter may use the same vertex twice).

See, for example, the suspension bridge in New San Francisco (Sol(0,0)III: Earth).

0016 0444 010A 0B00 0000 ; Curve(COLOR_GRAY, 1,10,11,0)
0016 0444 111A 1B10 0000 ; Curve(COLOR_GRAY, 17,26,27,16)
0016 0444 0E0C 0C00 0000 ; Curve(COLOR_GRAY, 14,12,12,0)
0016 0444 0F0D 0D01 0000 ; Curve(COLOR_GRAY, 15,13,13,1)
0016 0444 1E1C 1C10 0000 ; Curve(COLOR_GRAY, 30,28,28,16)
0016 0444 1F1D 1D11 0000 ; Curve(COLOR_GRAY, 31,29,29,17)

The 0000 at the end is a parameter which is, from observation, always 0000h. Imagine that.

23 (17h) = Not Used

0017

Not used. If you encounter this as a command code there is something wrong with your program. Sorry.

24 (18h) = Ball Array

0018 MATERIAL RADIUS [VERTEX|VERTEX]+

Get you ball arrays here! It seems balls are so popular, everybody wants one. Preferably more than one! This command reads the bytes after RADIUS and interprets them as single vertex numbers to display a ball on until it reads the value 7Fh. If there is an even number of balls in this array, it ends with the double code 7F7Fh; for an odd number, the last word is xx7Fh -- where the high byte contains a valid vertex number.

The RADIUS can be a constant or a variable; being variable is quite useful to display growing or shrinking circles. Yes, we're talking Hyperspace cloud here!

But first, a simple example. This one is a complete 3D object! It's object #374, and depicts a group of 14 trees (that's what I think it is meant to be) inside the transparent biodomes.


The coordinates 11 and 15 appear because not every point has to be used; they
are mirrors of #10 and #14 (which are used).

0018 2080 C409 0001 0405 0809 0C0D 0607 0A02 030E 7F7F
    ; BallArray(UNLIT | RGB(0,8,0), Radius(5000, ..)
0000 ; End

The ... at the end is because I don't think it is useful to list all vertices, you can quite easily read'em in the original code.

To show growing and shrinking balls a variable radius is needed, this rather large example from the Hyperspace warp (object #154) shows how:

C25D 0D80 ; LOCAL[2] = TIME << 13
C24D 05C2 ; LOCAL[2] = LOCAL[2] >> 5
C14D 0284 ; LOCAL[1] = GLOBAL[4] >> 2
C10D C2C1 ; LOCAL[1] = LOCAL[1] + LOCAL[2]
0694 1081 ; if (GLOBAL[1] != 0) goto L64
0018 246E 00C1 007F ; BallArray(UNLIT | RGB(4,6,14), Radius(LOCAL[1], ..)
05CB 0926 ; if (DISTANCE > 2342) goto L64
C14D 01C1 ; LOCAL[1] = LOCAL[1] >> 1
C24D 02C1 ; LOCAL[2] = LOCAL[1] >> 2
C10D C2C1 ; LOCAL[1] = LOCAL[1] + LOCAL[2]
0018 268E 00C1 007F ; BallArray(UNLIT | RGB(6,8,14), Radius(LOCAL[1], ..)
044B 0752 ; if (DISTANCE > 1874) goto L64
C14D 01C1 ; LOCAL[1] = LOCAL[1] >> 1
C24D 02C1 ; LOCAL[2] = LOCAL[1] >> 2
C10D C2C1 ; LOCAL[1] = LOCAL[1] + LOCAL[2]
0018 28AE 00C1 007F ; BallArray(UNLIT | RGB(8,10,14), Radius(LOCAL[1], ..)
02CB 057E ; if (DISTANCE > 1406) goto L64
C14D 01C1 ; LOCAL[1] = LOCAL[1] >> 1
C24D 02C1 ; LOCAL[2] = LOCAL[1] >> 2
C10D C2C1 ; LOCAL[1] = LOCAL[1] + LOCAL[2]
0018 2ACE 00C1 007F ; BallArray(UNLIT | RGB(10,12,14), Radius(LOCAL[1], ..)
014B 0464 ; if (DISTANCE > 1124) goto L64
C14D 01C1 ; LOCAL[1] = LOCAL[1] >> 1
C24D 02C1 ; LOCAL[2] = LOCAL[1] >> 2
C10D C2C1 ; LOCAL[1] = LOCAL[1] + LOCAL[2]
0018 2CEE 00C1 007F ; BallArray(UNLIT | RGB(12,14,14), Radius(LOCAL[1], ..)

Assuming my interpretations of the math commands are OK, you can see how (sort of) LOCAL[1] gets initialized with some value and then draws a series of blue-to-white balls on top of each other (all centred on vertex #0). After each one is drawn, the variable LOCAL[1] is decreased a bit, leading to ever decreasing radii.

This same model also draws red-to-yellow globs in quite the same way; one of the first commands in the code selects which part to use and whether to add sparkly blue lines or not.

25 (19h) = View matrix?

ddd19

This is another unknown. The 11 top bits ddd may be 001 (leading to a full command word 0039h), or its highest bit may be set (which is in full 8019h). The latter command resets the view transformation matrix, so any objects drawn afterwards always face the camera; the first command does ... something ... else ...

26 (1Ah) = Set Color

001A MATERIAL 00|NORMAL
001A MATERIAL1 VARIABLE|00 MATERIAL2 MATERIAL3 MATERIAL4
    MATERIAL5 MATERIAL6 MATERIAL7 MATERIAL8

The MATERIAL parameter in the graphics commands may have a special bit 1000h set (I named it the LOCALCOLOUR bit). This means its actual value is to be taken from an internal variable, and that is very useful! If you don't want an object to be the same color over and over again, you can set the LOCALCOLOUR bit in its color and define this with a variable before drawing.

There are two variants: the short one just sets the material to use, with an associated NORMAL to apply shading. The longer version, where NORMAL is zero, assumes VARIABLE is a variable code (either global or local; at least the 80h bit should be set) and uses the last 3 bits of this VARIABLE to index one of the eight defined colors. It is possible the lower byte 00h might also be a shading normal (00h is the code for no normal), but it is zero everywhere.

A simple example from the Cobra Mk III:

001A 0444 000C ; SetColor(Normal(12), COLOR_GRAY)
177B 0654 0000 3000
    ; ScaledSubobject (187:Nose wheel, Vertex(84),Orientation(06h),Scale(0x0000),LOCALCOLOUR | UNLIT)
177B 0652 1000 3000
    ; ScaledSubobject (187:Nose wheel, Vertex(82),Orientation(06h),Scale(0x1000),LOCALCOLOUR | UNLIT)
177B 0653 1000 3000
    ; ScaledSubobject (187:Nose wheel, Vertex(83),Orientation(06h),Scale(0x1000),LOCALCOLOUR | UNLIT)

(The command ScaledSubobject is explained right after this.) The Nose wheel is used for a few different ships, and apparently its closing hatch should have a color matching the ship's hull.

A rather funny example is the pilot head in the Eagle. Here you can see how global variable [2] is used to determine its color.

001A 2E00 8200 20E0 2EE0 200E 2EEE 2E80 2E0E 20EE ; SetColor(GLOBAL[2],
    UNLIT | COLOR_RED,
    UNLIT | COLOR_GREEN,
    UNLIT | COLOR_YELLOW,
    UNLIT | COLOR_BLUE,
    UNLIT | RGB(14,14,14),
    UNLIT | RGB(14,8,0),
    UNLIT | RGB(14,0,14),
    UNLIT | COLOR_CYAN)
0001 3000 A014 0600 ; Ball(LOCALCOLOUR | UNLIT, Radius(10560), Vertex(6), Normal(NORMAL_NONE))

I think GLOBAL[2] refers to the internal numeric code for the unique ship identifier, so in a group of Eagles you can see a range of different colored helmets. You can see this in the Shipyard when more than a single Eagle is on offer.

In the image you can see two vertices 14 and 15. Between those the text ".IXI." is drawn -- it mystified me for a while, then I realized it is meant to look like a steering wheel!

27 (1Bh) = Scaled Sub-object

obj1B ORIENTATION|VERTEX SCALE MATERIAL ; Bit 7 of ORIENTATION clear
obj1B ORIENTATION|VERTEX SCALE MATERIAL VERTEX2|VERTEX1 VERTEX4|VERTEX3 ; Bit 7 of ORIENTATION set

Pretty much like the normal sub-object but with two added twists. This one defines an additional SCALE -- at least, that's what I think it does. It appears to take any number between 0000h and F000h, where, oddly, the lower byte always appears to be 00h.

Another addition is the MATERIAL parameter. I assume it is used when the sub-object uses the LOCALCOLOUR bit.

As in the normal subobject, the ORIENTATION can have bit 7 (80h) set, indicating that a set of 4 pass-through vertices follow. See this snippet of the Krait Assault Craft:

167B 804A 4000 5009 7F7F 7F4C
    ; ScaledSubobject (179:object_179, Vertex(74),Orientation(00h),Scale(0x4000),
      LOCALCOLOUR | Texture_Metal_Black_0, 127,127,127,76)
...
167B 944B 4000 5009 7F7F 7F4D
    ; ScaledSubobject (179:object_179, Vertex(75),Orientation(14h),Scale(0x4000),
      LOCALCOLOUR | Texture_Metal_Black_0, 127,127,127,77)

There are a few weird vertex values 127 in this example but, again, these vertex numbers aren't used in the sub-object and so can be any value. Object 179 consists of a couple of cones and I can't seem to find out where they appear on the ship...

Another example: the laser turret on the Griffin Carrier:

1ABB 1048 F000 4026
    ; ScaledSubobject (213:Turret, Vertex(72),Orientation(10h),Scale(0xF000),Texture_026h)

28 (1Ch) = Rotate

ddd1C VALUE

This is another command on which I am stymied. It seems ddd is a certain value, and VALUE may be a number or a variable.

I am 100% sure of the function, though. See this piece of the Interplanetary Shuttle.

191C 0000 ; Rotate -- default 0000
833C FD44 ; Rotate -- default FD44
000A 0000 0606 4610 3016 ; Text(COLOR_BLACK, Normal(6), Vertex(16), Scale(6), VECTOR_FONT, 3016h)
191C 0040 ; Rotate -- default 0040
833C FD44 ; Rotate -- default FD44
000A 0000 0607 4612 3016 ; Text(COLOR_BLACK, Normal(7), Vertex(18), Scale(6), VECTOR_FONT, 3016h)

Recall that text code 3016h draws the ship identifier. The texts on the sides of the shuttle run in opposite directions: the text on its right side goes towards the nose, the text on the left side towards the aft. They are both slanted a bit from upright position, so they are drawn parallel ("on") to the slanted top panels. If you look at the first ROTATE commands of the two pairs, you see that one has a parameter 0000h and the other is 0040h. It occurred to me this can also be 4000h (that blasted byte order confusion again), which could be a 180° turn around the horizontal axis for the second text (and the first text would not rotate horizontally). If that rotation is done, the other one (FD44h) could mean the lengthwise tilt.

FFE uses matrices to perform its rotations and I really can't get my head around those! That might explain a bit why this command still doesn't make any sense to me.

The "might-be-variable" idea comes from observing scanners. They are usually plugged in as a sub-object, and right before they are, something like this appears (in the Cobra Mk III):

C35D 0980 ; LOCAL[3] = TIME << 9
183C 00C3 ; Rotate -- default 00C3
004B 800E ; if !Visible(Normal(14)) goto L228
18CE 0646 ; Subobject(198:Scanner antenna, Vertex(70), Orientation(06h))

Local variable 3 is coded as C3h, the number used in the ROTATE command.

Part of my confusion may arise from the orientation problems I've encountered, because this orientation/rotation (or mirroring) obviously needs to be concatenated with any previous number of ROTATE commands.

29 (1Dh) = Math part 2

DEST|(OPERAND|1Dh) SOURCE1|SOURCE2

This is the second part of the MATH command. See the full ramblings on that under the heading 13 (0Dh) and 29 (1Dh) = Math above.

30 (1Eh) = Unknown!

ddd1E

Yet another unknown! The numeric parameter in the command itself is usually seen as 600h, and once as 400h.

Example: the green star map grid (object #166)

C01E ; Cmd 1Eh (parm. 0C01h)

The original code is a real mess there; for the Star Map Grid it appears to draw the stars (!), in other cases it does something with the aforementioned alternate rotation matrix.

31 (1Fh) = Planet!?

001F MATERIAL VERTEX2|VERTEX1 VERTEX4|VERTEX3 EXTRA1 EXTRA2 EXTRA3
    [ADDITIONAL]* 0000

Rather a simple code but with very graphic consequences. VERTEX1 might be the planet center, with VERTEX2, VERTEX3 and VERTEX4 as the three main radii. There are three of them so you can draw an ellipsoid star or asteroid.

The MATERIAL is sort-of default but after the command and vertices a number of additional color commands may follow. Somehow this command defines colored atmopheres and tesselated planet surfaces!

ADDITIONAL consist of subsets, each 8 words long, which appear to end with the (always supplied) 0000h code.

There may be zero additional subsets, in which case this code is just 6 words long. All stars and a few planets consist of just this one command. A small example is Object #144 (Type'B'hot blue star):

001F 0DEF 0206 0004 0009 1999 0001 ; Cmd 1Fh(RGB(13,14,15), 6,2,4,0, 0x0009,0x1999,0x0001,
    0000    ; 0x0000)
0000 ; End

The EXTRA1..3 numbers here must be connected somehow to the atmosphere colors...

A somewhat larger example (the "small barren sphere of rock"):

001F 0333 0206 0004 0002 03D7 05F4 ; Cmd 1Fh(RGB(3,3,3), 6,2,4,0, 0x0002,0x03D7,0x05F4,
0807 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 7,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
087D 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 125,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
08E3 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 227,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
092F 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 47,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09AA 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 170,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09BA 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 186,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09CC 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 204,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A48 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 72,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A4F 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 79,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A79 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 121,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0ABD 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 189,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0B56 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 86,11,12 , 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0C40 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 64,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0C7C 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 124,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0CAE 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 174,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0CD9 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 217,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0000			; 0x0000)

The translations in the comment to bytes and words is pure guesswork!

Just to show how difficult it is to make something out of this command, here is the code for the "World with indigenous life and oxygen atmosphere":

001F 0667 0206 0004 0000 0B85 0478 ; Cmd 1Fh(RGB(6,6,7), 6,2,4,0, 0x0000,0x0B85,0x0478,
0807 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 7,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
087D 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 125,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
08E3 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 227,8,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
092F 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 47,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09AA 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 170,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09BA 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 186,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
09CC 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 204,9,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A48 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 72,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A4F 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 79,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0A79 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 121,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0ABD 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 189,10,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0B56 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 86,11,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0C40 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 64,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0C7C 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 124,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0CAE 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 174,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0CD9 000C 0040 00F4 FFB3 FFFF 00CE FFFF ; 217,12,12, 0x0040,0x00F4,0xFFB3,0xFFFF,0x00CE,0xFFFF,
0000            ; 0x0000)

It looks startingly the same...

 

 

Next time I'll explain the MATERIAL parameter, and, since this ties in to the bitmap graphics in the game, I might as well tell you about those as well.

Now I'm gonna have a nice long lie-down with a wet towel on my head...

<< Previous :: Next >>

[Jongware]

Based on original data and algorithms from Frontier:Elite 2 and Frontier:First Encounters by David Braben (Frontier Developments)

Original copyright holders:
Elite 4: The Next Encounter David Braben 2011?
First Encounters David Braben 1995
Frontier David Braben 1993
Elite David Braben and Ian Bell 1984

 

After penning this story down I think I should have titled it "Frontier Objects Explaining". If you're as confused as I am let met know at jongware.