The patch

A hundred and thirty entries, 1,396 bytes, none outside the table and none shifted: every patch is exactly as long as what it replaces, so no address in the game moves.

They come from two places: 27 entries written by hand — 548 bytes of code, pointers and text — and 103 that fall out of the canvases — 848 bytes, the 122 repainted map tiles. Byte by byte: 848 of tiles, 376 of text, 137 of new code, 23 of hooks and trampolines, 10 of pointers and 2 loose ones — the planting loop's limit and the text's paper. The tool draws no distinction between them: every one carries the bytes it expects to find, and every one is as long as what it replaces.

The table

addressblockbyteswhat
0x7FD1middle1the planting loop's limit, 0x78 → 0x00
0x708Amiddle3trampoline to the attribute routine
0x6600middle76MUESTRA_LOS_VALORES
0x664Cmiddle61planting by side, drawing by side, and the deadline
0x7FC9middle5planting hook
0x770Amiddle10drawing hook
0x6F77middle5ring hook
0x763Fmiddle1the text's paper, 0x78 → 0x70
0x7DE6middle9Habil → Firme, in its own slot
0x6689middle25 Virtuoso, Valiente and Fuerte
0x6FF0, 0x701F, 0x7036middle2 eachthose three adjectives' pointers
0x66A2middle64the four whole phrases of the sheet's last line
0x7074, 0x707Amiddle2 eachthat list's pointer and its column
0x7A79…0x7BC7middle180the ten place names on the map
0x6BA5middle9Brand III → Bardo III
0x7D07middle45the race names in the plural
0x7D3Amiddle44the race names in the singular
0x9E02…0xA280high848the 122 repainted tiles, in 103 entries

The addresses are execution addresses. The "middle" block runs from 0x5E00 and the "high" one from 0x9E00. The ten place names come in eight entries: seven on their own — 0x7A79, 0x7B28, 0x7B34, 0x7B4B, 0x7B5D, 0x7B7F and 0x7BC7 — and one of 112 bytes that rewrites in one go the stretch of nine records starting at 0x7AA1, because there Rivendell gives up a letter to lend it to Dale, and shrinking one record shifts every record behind it.

The four text formats

Nothing shifts, so where a string lives is what decides whether it can change. And each format squeezes differently:

The fourth list, the one for the sheet's last line (0x7D6A), moved whole to 0x66A2. That line used to be composed from a template plus the list's word, and now each entry carries the complete phrase, written from column 0: it was the only way to turn Aliado a la Sociedad into Aliado a la Comunidad without touching the other three.

How it is applied

The disassembly is never touched. It starts from the block bodies that make extract pulls out of your tape (work/*.raw, the same bytes without the Spectrum format's wrapper), applies the table in tools/parchea.py, and rebuilds the cassette by re-wrapping each block with its flag in front and its XOR behind.

Every entry in the table carries the bytes it expects to find. If they are not there, make parche aborts: wrong tape. And when it finishes it checks that, outside the table's ranges, the body is identical to the original.

Where the new code lives

The 137 bytes of new code — 76 for the attribute routine and 61 for the second round — are written over the ZX Spectrum's beeper engine, at 0x6600-0x6688. Behind them go 89 more bytes that are no longer code but text: the three long adjectives (0x6689-0x66A1) and the four phrases of the sheet's last line (0x66A2-0x66E1).

The conversion brought that engine across whole and nothing ever calls it: not one instruction in the five listings points at 0x6600, and the four places that ask for a sound effect call 0x65FF, which is a bare ret. Of the MSX's PSG only two registers are ever written, 7 and 14, and both are for reading the joystick. This game is silent, and its silence gives us 276 bytes of room, of which the patch spends 226 and leaves 50 free. A check makes sure no entry runs past that stretch or steps on another.

The graphics, in three PNGs

All the artwork in the high block is edited with an image editor. It lives in src/parche/, at full size and with no gaps: one pixel of the PNG is one pixel of the game.

canvaswhat it holdswhere it livessize
tiles_del_mapa.pngthe 128 map tiles, 8 × 80x9E00128 × 64
sprites_de_batalla.pngthe 176 battle sprites, 16 × 8 with a mask0xA2E8176 × 128
fuente.pngthe 128 characters, 8 × 80xC800128 × 64

Repaint whatever you like and make parche does the rest: it compares the canvases with the cassette and every drawing that changed comes out on its own as one more entry in the table, in the graficos group, with its orig and its nuevo the same length, exactly like the hand-written ones. Leave them alone and not a single extra entry appears.

Today 122 of the 128 tiles come out of there, repainted: 103 entries and 848 bytes, more than half the patch. The six that do not move are 97 to 102, where only the background changed — the ZX's white and the MSX's are the same colour 15, so the drawing that comes back is byte for byte the one that was there. The Eye of Sauron is in that group now too: it used to be 36 hand-written bytes and it is drawn on the canvas like everything else.

commandwhat it does
make graficossays which drawings differ from the cassette, without building anything
make parcheturns them into patch entries and builds the cassette
make lienzosredraws the PNGs from the cassette, wiping whatever was painted on top; it has to be forced with --rehaz

How each sheet is painted

What is left alone

A drawing that looks exactly like the one on the cassette is handed back with its original bytes, not re-encoded. That is why opening a PNG and saving it unchanged does not move a single byte, not even in the ones that cannot be rebuilt from the picture: a tile with white ink on white paper hides eight bytes of bitmap underneath, and so does one that is black on black. Without that rule every round trip would dirty the patch with changes nobody asked for — and it is what leaves tiles 97 to 102 out of the patch today.

Each sheet's palette travels inside its PNG, so an editor in indexed mode offers it ready-made. If a colour from outside still gets in - by working in true colour, say - the nearest one is taken and the screen reports which it was, how many pixels, and what it was changed to.

The IPS

make ips produces war_parche.ips: 1,718 bytes in 39 records, holding only what changes. Verified on the spot — and in the tests — that applied to war.tsx it gives back the patched cassette byte for byte.

That is what gets shared. Not the game.

The checks

make test is 79 of them, and they are not decoration. Among others: