Author: MaKiPLshakotay2 (XeNTaX)HalferYagami Light
Complete list of original battle stages: List of battle stages
Info
.X file is uncompressed 3D stage model with texture embedeed. The file contains unused info (not used by FF8 engine), camera data, movement, translations and geometry data.
Battle stages DOES NOT contain pointers to next sections. All pointers are HARDCODED in FF8.EXE. Click me for battle stage pointer list
| Name | Usually starting with: | Description |
|---|---|---|
| PlayStation MIPS assembly | E8 FF BD 27 01 00 02 | Used only in PS version; skipped in PC |
| Camera data + movement | 02 00 08 00 20 00 | Camera animations, movement (pre-keyed) |
| Model section | 06 00 00 00 (See geometry section info) | Divides to object (group) and it’s sub-objects + vert/triangle grouping |
| Texture | 10 00 00 00 09 (Always) | Contains one .TIM texture, 8 BPP |
How is this file handled by the engine?
FF8 loads specific file and reads it from hardcoded (written in FF8 code) position (or reads MIPS PlayStation assembly that contains battle stage load code). This hardcoded position points to camera data (in PC). Camera data has size uint16, which is calculated and relative jump is made. Just after camera, a typical section based file handling is made.
| Offset | Length | Description |
|---|---|---|
| 0 | uint32 | Number of sections |
| 4 | uint32 | Objects group #1 |
| 8 | uint32 | Objects group #2 |
| 12 | uint32 | Objects group #3 |
| 16 | uint32 | Objects group #4 |
| 20 | uint32 | End of groups = Texture animation block. Equals the texture offset below when the stage has no animation |
| 24 | uint32 | Texture |
| 28 | uint32 | Relative to EOF [unused in code] |
The first value (Number of sections) is fixed 06 00 00 00; groups #1..#4 always follow, and group #1 is always at offset 20 (0x20). The last value (Relative to EOF) is the geometry section size and, like Number of sections, is not read by the engine.
Objects Group
| Offset | Length | Description |
|---|---|---|
| 0 | uint32 | Number of sections |
| 4 | uint32 | Pointer to Settings #1 |
| 8 | uint32 | Pointer to ObjectsList |
| 12 | uint32 | Pointer to Settings #2 |
| 16 | uint32 | Relative to end_of_group |
Objects List
| Offset | Length | Description |
|---|---|---|
| 0 | uint32 | Number of objects |
| Num_of_obj*4 | uint32 | Relative pointer to segment (one .obj file) [01 00 01 00] |
Settings 1
| Offset | Length | Description |
|---|---|---|
| 0 | uint16 | Unknown, fixed 2? |
| 2 | sint16 (probably, or uint16) | Scale (01 is the smallest) |
| 4 | 4 bytes | PADDING |
| 8 | 4 bytes | Nothing? |
| 12 | 4 bytes | Nothing? |
| 16 | 2 bytes | if not FF FF, then model vanishes |
| 18 | sint16 | X or Z Axis rot origin (Translation) |
| 20 | 44 bytes | Left null for storing some engine data AFTER loaded in-game |
| 64 | 2 bytes | if not FF FF or 00 00, then model vanishes |
| 66 | 2 bytes | Nothing? |
| 68 | …up to end | Left null for storing some engine data AFTER loaded in-game |
This might look difficult, but it’s like multi-pointer file that starts at different location given by executable. See list at the beginning of this wiki to see which stages camera starts at which position (where FF8.exe really starts to read file as BattleStage) [FF8 stores whole file, even with useless data before camera] If this still makes trouble, see this video: TODO
Camera data
Starts at ~0x5d4 (see How the engine handles this file?). [.text:00509820]
| Offset | Length | Description |
|---|---|---|
| 0 | uInt16 | Pointers count. Fixed (02 00) [Unused in code] |
| 2 | uInt16 | Relative jump to CameraSetting. Should be (08 00) |
| 4 | uInt16 | Relative jump to CameraAnimationCollection. Should be (20 00) |
| 6 | uInt16 | Camera data size (starting from 0) [unused in code] |
| 8 | 24 bytes | Camera Settings |
| 32 | ?? bytes | Camera Animation Collection |
Camera Setting
This 24-byte block is not a fixed structure: it is byte-code executed every frame by the shared battle animation-sequence VM (computeAnimationSequence — the same interpreter used by monster animation sequences and camera sequences). BS_UpdateCameraSequence runs it with the camera opcode handlers (opcodes < 0xC0 are camera actions dispatched by CameraSeq_DispatchActionOpcode (renamed from a2ParamAnimSeqForCamera); 0xC0+ are the generic VM control/arithmetic opcodes). See Function addresses.
This script drives the battle-intro camera. Stage scripts typically start with camera opcode 05 00 = “play the camera animation whose ID is read from camera variable 0” — and variable 0 is filled at battle load with the encounter’s camera data byte from scene.out. This is how each encounter picks its intro camera animation out of the stage’s collection below.
Example — a0stg000.x camera setting bytes: 05 00 C3 10 E5 7F C1 00 CB 7F EA 06 E6 07 E6 02 01 ... (play anim from var 0, then loop while the camera-animation-playing flag is set, then end).
Camera Animations Collection
| Offset | Length | Description |
|---|---|---|
| 0 | uint16 | NumOfSets |
| 2 *(Set index) | uint16 | Relative pointer to camera Animation Set |
| 2 *(Set index)+ 2 | uint16 | Camera EOF |
| CameraAnimationSetPointer | CameraAnimationSet | CameraAnimationSet |
CameraAnimationSet
Each set holds exactly 8 animation pointers: the engine masks the requested animation index with & 7 (BS_GetCameraAnimationPointer). The same function bounds-checks the requested set index against NumOfSets and silently ignores the request if it is out of range (no animation plays).
The 8 pointers are signed int16, each relative to the set start and multiplied by 2 to get the byte offset. A slot is empty when its target word is 0xFFFF (idle monsters reuse one 0xFFFF for several slots, so consecutive empty pointers increase by 1).
| Offset | Length | Description |
|---|---|---|
| 0 | int16 (signed) [8 of] | AnimPointer (x2 = byte offset from set start) |
| 2*AnimPointer | Chain of control-word blocks | CameraAnimation |
Camera Animation (WIP)
All this is based on best effort knowledge from reversing by Maki. I'm just trying to understand it.
Control Word
| Offset | Length | Description |
|---|---|---|
| 0 | uint16_t (bit varies) | Main controller. If 0xFFFFU END |
FOV
FOV mode = (Control Word >> 6) & 0b11. Modes 2/3 read one/two words respectively; there is no padding word (earlier revisions of this page listed one - the “Same” case is a single word, “Different” is two consecutive words).
0 Keep
| Offset | Length | Description |
|---|---|---|
| None | None | FOV unchanged (keeps the previous value) |
1 Default
| Offset | Length | Description |
|---|---|---|
| None | None | Start = 0x200 |
| None | None | End = 0x200 |
2 Same
| Offset | Length | Description |
|---|---|---|
| 0 | uint16_t | Start |
| None | None | End = Start |
3 Different
| Offset | Length | Description |
|---|---|---|
| 0 | uint16_t | Start |
| 2 | uint16_t | End |
ROLL
ROLL mode = (Control Word >> 8) & 0b11.
0 Copy current
| Offset | Length | Description |
|---|---|---|
| None | None | Start = End = the camera’s current roll value |
1 Default
| Offset | Length | Description |
|---|---|---|
| None | None | Start = 0x000 |
| None | None | End = 0x000 |
2 Same
| Offset | Length | Description |
|---|---|---|
| 0 | uint16_t | Start |
| None | None | End = Start |
3 Different
| Offset | Length | Description |
|---|---|---|
| 0 | uint16_t | Start |
| 2 | uint16_t | End |
LAYOUT
Control Word & 0b0000'0000'0000'0001U
LAYOUT = Control Word & 1. Both layout paths produce the same 18-byte on-disk frames (the difference is only whether coordinates are resolved through the camera translation at parse time vs. play time; LAYOUT=1 forces interpolation mode 0xFB). You loop reading 18-byte Animation Frames until the next word has bit 15 set (int16 < 0, i.e. 0xFFFF), which terminates the list and is consumed.
0 Default
| Offset | Length | Description |
|---|---|---|
| 0 | uint16 | Frame duration; if bit 15 is set (int16 < 0) the list ends |
| … | 18 bytes | Animation Frame (the duration is its first field) |
1 Other
Same 18-byte frame layout as mode 0; coordinates are pre-resolved through the camera translation and interpolation mode is forced to 0xFB.
Time
Time is calculated from number of frames. You set a starting camera position + look-at and an ending one, then the frame's duration = how many frames to interpolate between them. The engine accumulates duration*16 as the running timing value. The frame list ends when the next word has bit 15 set (an int16 < 0, in practice 0xFFFF); that terminator word is consumed.
Animation Frame
Each frame is 18 bytes (verified against BS_Camera_ReadAnimation @0x503C70 and every vanilla monster camera section). The two interpolation-mode bytes were previously missing (the frame was documented as 16 bytes), and the coordinates are signed int16 in Z, X, Y order - not unsigned x,y,z.
| Offset | Length | Description |
|---|---|---|
| 0 | uint16 | Frame duration (ticks to interpolate over) |
| 2 | uint8 | Position interpolation mode |
| 3 | uint8 | Padding (skipped; always 0 in vanilla data) |
| 4 | int16 | Camera position Z |
| 6 | int16 | Camera position X |
| 8 | int16 | Camera position Y |
| 10 | uint8 | Look-at interpolation mode |
| 11 | uint8 | Padding (skipped) |
| 12 | int16 | Look-at Z |
| 14 | int16 | Look-at X |
| 16 | int16 | Look-at Y |
Dependency
` CAMERA: -Camera Settings -Camera Animation Collection (even three collections in a0stg006.x and up to 7 in a0stg101.x!) | | -Camera animation Set (Always 8 camera animations, may be empty (0xFFFF); look for example to a0stg127.x collectionId==1; there are 8 animations, but 7 of them are 0xFFFF (pointers increase by 2)) | | - Camera animation`
Example
a0stg006.x:
0x5d8 -> 02 00 08 00 20 00
- Get EOF-> *(0x5d8 + 8) -> 32
- Jump to EOF -> 0x5d8 + 32 = 0x5F8 (This is now Camera Animation Collection)
- Get pointer to correct anim collection. In this case we will use AnimCollectionID == 0, so: *(0x5F8 + 0*2 + 2) -> 0x0c
- Jump to Anim collection data: 0x5F8 + 0x0c = 0x604 (This is now Camera Animation Set)
- Jump to Camera animation by cameraAnimSetID, let’s take for example cameraAnimSetID == 7, so: *(0x604 + 7*2) -> 0x25E. Now carefully, jump by multiplying it by 2!
- 0x604 + (0x25E * 2) = 0xAC0
Therefore: a0stg006.x Camera animation 7 in camera collection 0 is at 0xAC0
Geometry
Geometry contains groups, that contains Triangles and/or quads poligons. Models always stars at 01 00 01 00, and needs to be after camera data, either it’s not model itself, but some other data.
Group
| Offset | Length | Description |
|---|---|---|
| 0 | 4 bytes | Always 01 00 01 00 / Header of object |
| 4 | 2 bytes | Uint16 / number of vertices. |
| 6 | Number of vertices * 6 | Vertex data, short X; short Y; short Z; |
| 6 + (Number of vertices *6) | (AbsolutePosition MOD 4) + 4 | Padding |
| varies (just after above) | 2 bytes | uint16 / number of triangles |
| varies (just after above) | 2 bytes | uint16 / number of quads |
| varies (just after above) | 4 bytes | padding |
| varies (just after above) | number of triangles * 20 | Triangle data. If NumOfTriangles = 0, then instead of any triangle data, there’s quad data. |
| varies (just after above) | number of quads * 24 | Triangle data. If NumOfQuads = 0, then instead of any quad data, there’s either next header 01 00 01 00, or end of group. |
Triangle
| Name | Type | Description |
|---|---|---|
| F1 (A) | uint16 | A of face indice |
| F2 (B) | uint16 | B of face indice |
| F3 (C) | uint16 | C of face indice |
| U1 | Byte | U of first texture coordinate |
| V1 | Byte | V of first texture coordinate |
| U2 | Byte | U of second texture coordinate |
| V2 | Byte | V of second texture coordinate |
| CLUT_ID | UInt16 (BIT operated)* | Index to CLUT_ID operated by BIT (see below) |
| U3 | Byte | U of third texture coordinate |
| V3 | Byte | V of third texture coordinate |
| Special (see below / After QUAD table) | 8 Byte | See below (after QUAD table) |
| Hide | Byte/Bool | Bool. Hides or shows texture |
| Red | Byte | Texture colourization (Red) |
| Green | Byte | Texture colourization (Green) |
| Blue | Byte | Texture colourization (Blue) |
| PSone GPU related | Byte | PSOne instruction |
Quad
| Name | Type | Description |
|---|---|---|
| F1 (A) | uint16 | A of face indice |
| F2 (B) | uint16 | B of face indice |
| F3 (C) | uint16 | C of face indice |
| F4 (D) | uint16 | D of face indice |
| U1 | Byte | U of first texture coordinate |
| V1 | Byte | V of first texture coordinate |
| CLUT_ID | UInt16 (BIT operated)* | Index to CLUT_ID operated by BIT (see below) |
| U2 | Byte | U of second texture coordinate |
| V2 | Byte | V of second texture coordinate |
| Special (see below) | 8 Byte | See below (after table) |
| Hide | Byte/Bool | Bool. Hides or shows texture |
| U3 | Byte | U of third texture coordinate |
| V3 | Byte | V of third texture coordinate |
| U4 | Byte | U of fourth texture coordinate |
| V4 | Byte | V of fourth texture coordinate |
| Red | Byte | Texture colourization (Red) |
| Green | Byte | Texture colourization (Green) |
| Blue | Byte | Texture colourization (Blue) |
| PSone GPU related | Byte | PSOne instruction |
Special Byte: This byte, needs to be divided to two bytes. So, for example, a 0xE5 needs to be treated like two bytes: 0x0E 0x05
| Singular of char | Example | Description |
|---|---|---|
| First char/ 4 BIT | B2 –> 0B | Unknown |
| Second char / 4 BIT | B2 –> 02 | Texture page number |
CLUT ID
The most important bit’s are the first two on one byte, and last two on last byte. Example: 00000000 00111100 (00 3C) should be read like this: 00111100 00000000 (3C 00) And the CLUT ID is revealed by watching this four bits: 001111[00 00]000000
Same applies to reading CLUT colors (16bits) where they are: 1b-T, 5b-B, 5b-G, 5b-R (Or RGBT if reordered as shown above)
Complete list (without replacing bit order - as is in HEX editor/memory):
| CLUT ID | BIT | HEX |
|---|---|---|
| #1 | 00000000 00111100 | 00 3C |
| #2 | 01000000 00111100 | 40 3C |
| #3 | 10000000 00111100 | 80 3C |
| #4 | 11000000 00111100 | C0 3C |
| #5 | 00000000 00111101 | 00 3D |
| #6 | 01000000 00111101 | 40 3D |
| #7 | 10000000 00111101 | 80 3D |
| #8 | 11000000 00111101 | C0 3D |
| #9 | 00000000 00111110 | 00 3E |
| #10 | 01000000 00111110 | 40 3E |
| #11 | 10000000 00111110 | 80 3E |
| #12 | 11000000 00111110 | C0 3E |
| #13 | 00000000 00111111 | 00 3F |
| #14 | 01000000 00111111 | 40 3F |
| #15 | 10000000 00111111 | 80 3F |
| #16 | 11000000 00111111 | C0 3F |
Texture animation
Optional block pointed to by geometry header offset 20 (present when that value differs from the texture offset). It animates parts of the stage texture by copying rectangles inside VRAM every few frames — waterfalls, scrolling conveyors, blinking lights. The engine runs it each frame in BS_UpdateTextureAnimation; the pointer to the block is kept in dword_1D98A50. See Function addresses.
The block starts with an array of uint16 entry offsets (in words, relative to the block start; a 0 entry is an unused slot), each pointing to one animated region:
| Offset | Length | Description |
|---|---|---|
| 0 | sint16 | Destination X (VRAM) |
| 2 | sint16 | Destination Y (VRAM) |
| 4 | sint16 | Width |
| 6 | sint16 | Height |
| 8 | uint8 | Number of frames (0 = continuous 1-line scroll) |
| 9 | uint8 | Speed |
| 10 | uint16 | Frame counter [runtime] |
| 12 | sint16 * 2 * nbFrames | Per-frame source origin (X, Y) in VRAM (Y is stored minus 256) |
Each tick the counter advances by speed; on a frame change the engine blits the source rectangle (srcX, srcY + 256, width, height) over the destination rectangle, cycling through the frames. Only 17 retail stages use this block.
Texture
Contains one TIMs..
UV calculation algorithm
Float U = (float)U_Byte / (float)(TIM_Texture_Width * 2) + ((float)Texture_Page/(TIM_Texture_Width * 2));
Texture page calculation
Byte TPage = InputBytes[TexturePage_index] & 0F;
Bitwise TPage byte AND 0F, to delete first 4 bits
int TPageINT = TPage * 64;
For 16 bit TIM’s, the texture page is 64 pixels wide
Example
For 24-bit TIM:
0xB2 byte is: 2*48 = 96
Unsure about this one. It could be 42.667 instead of 48. It takes 1.5x the space of 16 bit. I haven’t seen a file that uses 24-bit yet.
For 16-bit TIM:
0xB2 byte is: 2*64= 128
For 8-bit TIM:
0xB2 byte is: 2*128 = 256
For 4-bit TIM:
0xB2 byte is: 2*256 = 512
Each time you half the number of bits you double the amount of data you can store in the same space. So the calculated Texture Page is different.
Face order / Translation/ triangulation
Quad wing order
` ABCD —> ABDC example: f 1 2 3 4 to: f 1 2 4 3`
Quad triangulation face order
` ABDC > ABD ACD (same for VT) example: f 1 2 4 3 to: f 1 2 4 f 1 3 4`
Triangles VT order
` A/T1 > A/T2 B/T2 > B/T3 C/T3 > C/T1`
Example
` (Also with quad triangulation face order!) f 1/1 2/2 7/4 6/3 to: f 1/1 2/2 7/4 f 1/1 6/3 7/4 where: A=1 B=2 C=6 D=7`
Function addresses
| Function | Address | Description |
|---|---|---|
computeAnimationSequence | 0x50DB40 | Shared animation-sequence VM (verified IDA function) |
BS_UpdateCameraSequence | 0x509610 | Runs the camera byte-code with camera opcode handlers (verified IDA function) |
CameraSeq_DispatchActionOpcode (formerly documented as a2ParamAnimSeqForCamera) | 0x509810 | Dispatches camera opcodes below 0xC0 (verified IDA function) |
BS_GetCameraAnimationPointer | 0x503520 | Masks/bounds-checks the animation index and resolves its pointer (verified IDA function) |
BS_UpdateTextureAnimation | 0x50CB20 | Per-frame stage texture animation (VRAM blit) (verified IDA function) |