Technical Overview
How the names, flags and portraits are stored, why five cells hold fewer than sixteen tiles, and the two ways a face can disappear into the background.
Two text encodings
The game does not use ASCII. It uses two different linear character tables, one for menu and roster strings and one for the prose on the advice screens, offset from each other. Neither is documented anywhere, and neither can be found by searching for a known word, because no byte value matches any letter.
They fall out of a relative search instead: the difference between two letters survives any linear encoding, so searching for the gaps in a known name finds the string regardless of what the table's base value is. Once one string is located the whole table follows from it.
The prose table also carries its own punctuation - space, full stop and hyphen sit at values unrelated to their ASCII positions. The hyphen is what makes CARL-UWE STEEB possible; it was confirmed by finding it already in use in the game's own tip text.
The roster table
The sixteen players are a flat array of structures, 0x8A bytes each, beginning at 0x00687C. Each structure opens with three little-endian pointers to its own strings, followed by the rating and style data.
Because the names are reached through pointers rather than sitting at fixed offsets, a replacement name does not have to be the same length as the one it replaces. Names are written into whatever space the region affords and the pointer is repointed at them, so MICHAEL STICH can replace KRAUSE without padding or truncation. Where a longer name would not fit at its own slot, the unused tails of other slots are collected and the displaced name placed in the first gap large enough for it.
The nationality flag is a 16-bit pointer at structure offset +0x13. Changing slot 16 from Japan to USA is therefore a two-byte edit - repointing that field at the USA flag's data - and not a graphics change at all.
Portrait graphics
The portraits are not stored plainly. They live in a compressed block at 0x060AE6, identified by a four-byte TPWM magic and a sixteen-bit decompressed size, which unpacks to 20,544 bytes.
The format is LZSS. A flag byte is consumed most-significant-bit first; a clear bit emits one literal byte, a set bit emits a back-reference whose length is the low nibble of the first byte plus three and whose twelve-bit offset is built from the high nibble and the byte after it. Matches are copied one byte at a time, so a reference may overlap what it is still writing and extend itself - which is how the format encodes runs.
Re-inserting artwork means decompressing the block, replacing tile data, and compressing it again small enough to fit the space the original occupied - 14,639 bytes, with no padding after it; the next data begins immediately at 0x064415.
Photographs are much harder to compress than pixel art, and a greedy encoder - even with lazy matching - overshot that limit by 48 bytes. The fix is not to touch the artwork but to parse it properly: in this format every token costs a fixed number of bits, nine for a literal and seventeen for a match, and a match's length does not change its cost. The smallest possible stream is therefore a shortest path over the positions, which one backwards pass finds exactly. That brought the block to 14,522 of the 14,639 bytes available.
The portrait cell
Each portrait is a 4×4 grid of 8×8 tiles, 4 bits per pixel - 32×32 pixels, 512 bytes. The cell is not all portrait:
| Region | Contents |
|---|---|
| Row 0, row 30, column 0, column 30 | Frame |
| Row 31, column 31 | Drop shadow |
| x 1-29, y 1-29 | The portrait itself - 29×29 pixels |
Eleven colours, and two ways to disappear
All sixteen portraits share one eleven-entry palette: a single tan ramp with no other hue in it at all. There is no white, no blue, no red. A coloured headband or collar in a reference photograph can only be rendered as a tone, never as a colour. The real values, read out of CGRAM rather than guessed:
| Index | RGB | Role |
|---|---|---|
| 1 | 255, 238, 172 | lightest tone |
| 2-8 | 230, 213, 148 … 98, 82, 41 | the ramp |
| 9 | 74, 57, 24 | darkest tone |
| 3 | 213, 189, 123 | also the flat backdrop behind the head |
| 10 | 255, 255, 189 | the frame |
Two entries in that list are traps, and both of them make parts of a face vanish:
- Index 3 is the backdrop and a light skin tone at once. A pale highlight that reaches the silhouette punches a hole straight through the outline.
- Index 1 is all but the frame colour -
255,238,172against255,255,189. A patch of the lightest tone reads as a hole in the head even in the middle of the cell.
The consequence is a rule the original artists clearly worked to: where the head meets the backdrop it must be at least one step darker than it. Every portrait here carries that rim. Without it a brightly lit forehead runs straight into the background and the top of the skull simply goes missing - which is exactly what happened, and what the rim fixed.
One more difference from a photograph: the original art is not dithered. Tones are quantised flat, because a checkerboard reads as obviously non-native next to the rest of the game.
Why five cells did not own all their tiles
All sixteen portraits can be found. Each cell's top-left tile carries an unmistakable signature - frame colour along its whole top row and down its whole left column - and sweeping the decompressed block for that signature returns exactly sixteen hits and no false positives.
Eleven of them are stored as contiguous sixteen-tile runs. The other five store only fourteen or fifteen tiles, and the positions they are missing are always plain frame-and-background corners: the game reads those from another portrait's copy of the same tile. Seven 8×8 tiles in total are shared this way.
| Cell | Player | Tiles stored | Positions read from another cell |
|---|---|---|---|
| 0x01420 | Hlasek | 15 | 12 (bottom left) |
| 0x01E00 | Wilander | 15 | 12 (bottom left) |
| 0x021E0 | McEnroe | 14 | 12, 15 (both bottom corners) |
| 0x023A0 | Krajicek | 14 | 3, 15 (top right, bottom right) |
| 0x045E0 | Stich | 15 | 15 (bottom right) |
This is not deduplication of identical data - every one of the 641 tiles in the block is distinct. It is the packer saving seven tiles on corners that happened to be blank in the original art, and it has a consequence for new artwork: anything drawn in those seven positions is simply not displayed. Writing over them is worse than useless, because the tile belongs to a second player's portrait as well.
The table lists tile numbers explicitly, though, so this is a data choice rather than a hardware limit - and that cuts both ways. If the game names every tile, then which slot holds which tile is ours to decide. LZSS only matches backwards within 4,095 bytes, so tiles that resemble each other compress far better stored near each other than scattered across the block one player at a time. Ordering the 256 portrait tiles by similarity instead of by player saves 203 bytes - more than the seven extra corner tiles cost.
So all five cells now own all sixteen of their tiles: seven tiles appended, seven tilemap words repointed, the artwork untouched and the ROM still 512 KB. The block occupies 14,522 of 14,639 bytes, with 117 to spare. It is worth recording what this fixes, because the artefact is in the unmodified game too - read the original block as sixteen consecutive tiles from those five bases and the developers' own Hlasek, Wilander, McEnroe, Krajicek and Stich each show a neighbour's corner.
Verification
Every build is checked against the ROM rather than against the tool that produced it: the graphics block is decompressed again from the finished file and compared tile by tile with the intended artwork, the recompressed stream is decoded back to confirm it reproduces the intended bytes exactly, and every byte outside the block is required to be unchanged.
| Tiles written | 256 of 256 (the seven corner tiles are appended, so every cell owns all of its own) |
| Tiles differing from the artwork | 0 |
| Compressed block | 14,522 of 14,639 bytes |
| Bytes changed outside the block and the header checksum | 0 |
| File size | 524,288, unchanged |