Ghosts (Fantome\, .ght)
Time attack's ghost race (menu 18's third type, docs/systems/front-end.md, "Time attack") races against recorded runs. The game keeps up to five per circuit and direction in its ghost folder (Fantome\, the string at POD 0x4eba30) and moves them in and out as .ght bundles: action 194 imports Scope\Load\<title>[M].ght (POD 0x4a76e0), action 195 exports the chosen ones to Scope\Export\<title>[M].ght (0x4a7d9c; Scope\ is folder_path's folder 11, 0x4e5f88, under the root, observed); <title> is the circuit's title (0x633354[c] + 0x14) and M is added when any mirror bit (0x4fdf16) is set. Addresses are POD's WINPOD.EXE.
Reader and writer: crates/pod-formats/src/ghost.rs (Bundle, and Writer in the export's order). State: the bundle is decoded and round-trip on both samples there are (measured), and Writer makes both samples' bytes from their ghosts (tests/ghost.rs); the ghost's sequence is read only as far as the menus read it. POD's disc ships no ghost: its DATA holds BINARY and RECORDS only (observed). The two samples are the fan tracks Everest and Natura of the extras (docs/extras.md; 143,640 and 134,552 bytes, one ghost each); crates/pod-formats/tests/ghost.rs reads every .ght under the extras and on the disc.
The ghost slots
A ghost file's name (0x4b192c, circuit in EAX, the mirror mask in EDX, the slot 1 to 5 in EBX): the circuit's file name (0x633354[c] + 0), padded with _ to 8 characters (strncpy from "_________", 0x4e8211, for 8 − length), the slot's digit at [8], and M at [9] when the mask is not zero: Bank____1, Beltane_3M (observed). The folder prefix comes from 0x4b1a14 (the path at 0x6afa68). The files go through a file-system layer of the game's own (drivers registered at 0x6a9944, 0x4adfac; exists 0x4ae224, delete 0x4ae1f8, rename 0x4ae534, inferred from their use; not traced). The work file is FantBuf (0x4ebb70).
ta_ghost_list (0x4b39c8) keeps the five slots sorted by time:
0x4b3914closes the gaps: each existing slot after an empty one is renamed down.- For each slot n from 5 down to 1 that exists:
FantBufis deleted, slot n renamed toFantBuf, the gaps closed again, and its place found (0x4b3690): the first slot whose ghost is slower thanFantBuf(0x4b3620returns 1), else the slot after the last that exists, else 1. When the place is above 1 and the ghost before it has the same time (0x4b3620returns 0), the place is n. A place up to 5 takes it (0x4b377c: slot 5 deleted, slots 4 down to the place renamed one up,FantBufrenamed to the place). FantBufis deleted.
0x4b3620 compares two ghosts' totals: −1 when either is not above 0, 2 when the first is slower, 0 when equal, 1 when faster.
The bundle
| Offset | Size | Content |
|---|---|---|
0x00 | 4 | Version, 1 (0x4eb6d8; the import stops on any other) |
0x04 | 8 | Magic words 0x456f1e9c, 0xfa48efb4 (checked at 0x4a78cd, 0x4a78de) |
0x0c | 4 | Count of ghosts |
0x10 | 4 | The circuit's number (0x633344), which the export writes (0x4a81fb, observed) and the import does not read; 57 (Everest) and 46 (Natura) in the samples, the same as the sequence's +0x44 |
0x14 | per ghost | Time (4: 16.16 seconds, equal to the summary's total in both samples), size (4), then that many bytes of the ghost's sequence |
The import reads the two words of each ghost (0x4a7969) and copies the sequence in pieces of at most 10,000 bytes (0x2710) into the work file Load.seq of the ghost folder (0x4e7e2a, 0x4eb9a8), then into a slot freed from the back (docs/systems/front-end.md, "Actions 194 and 195"). Nothing follows the last ghost in the samples.
The export (0x4a7d9c) writes the version, 16 bytes it fills later (its uninitialised header buffer), then for each chosen slot that holds a ghost the time from 0x4b3d80, the slot file's length and its bytes, and at the end the header over the 16 bytes at offset 4 (the magic words, the count written, the circuit's number); with no ghost written it deletes the file (observed).
The sequence
The recording itself; only the summary the menus read is decoded. It starts with 0x12345678, 0x78451236, 2, 0x13c, 0x3c (both samples). The game loads it through the file-system layer (0x4b5404, 0x4ae7e4) and walks a block B it gets from 0x4ae56c. Inferred from the samples: B is at the word +0x10 plus 4 (0x40).
| At | Content | Read by |
|---|---|---|
B + 0x00 | 3 | 0x4b3e40 (0x4b3eba) |
B + 0x20 | The summary record's size (0x48) | 0x4b4ec4 |
B + 0x24 | The summary record: 3 (0x4b3d2f), the total (+4, 16.16 s), a word (+8, unknown), the three laps (+0xc, +0x10, +0x14) | 0x4b3cec, 0x4b3c60 |
| after the record | A count and that many pairs of words; a count of entries; each entry an id, a length and that many bytes, a length and that many bytes, a count and that many 8-byte pairs | 0x4b4d6c, 0x4b4e4c |
The panel's row shows the total and the first entry's second string (0x4b3e40: 0x4b4d6c(B, 0) gives entry 0's id, 0x4b4e4c(B, id) the address of its second string). In both samples the first string is "shark" (32 bytes) and the second "Vortex" (64 bytes); which is the driver's name is unknown.
Times as the panel prints them
0x4b5860 (value in EAX, buffer in EDX): s = value >> 16 (arithmetic); hours = s / 3600, minutes = (s − hours × 3600) / 60, seconds the rest, hundredths = (value & 0xffff) × 100 >> 16 (truncated, unlike format_time's rounding). Each part goes through 0x4788f0 (an integer to text) and gets a leading 0 when it has one digit: hh:mm"ss'cc. The panel skips the first 3 characters when the text has at least 3 (mm"ss'cc).