Skip to content

The first mods ​

2026-10-23, eleventh session.

The race on Beltane with two mods: a chequered tunnel ceiling and "MOD 1/3" in place of "Tour 1/3"

Two mods in one frame: texture page 0 of Beltane redrawn as a chequerboard, and the lap counter's word changed in the text table. Both files were made by our writers and loaded by the unchanged WINPOD.EXE.

The project's bar for a finished format has three steps. Every file decodes, every file writes back byte for byte, and a changed file loads and shows the change in the game. Until today nothing had passed the third step.

Writers ​

The readers were built to show things, not to write them. They turned palettes into colours, skipped bytes nobody used, and sorted sections into a nice order. A writer needs the bytes as stored. So each reader now keeps them beside its friendly view: the raw palettes of the menu images, the text pool, each texture record's whole name field with the junk after its terminator, the unused tails of the blocks.

Two surprises:

  • The text tables' string pool is not in key order. Rebuilding it from the keys gives a different file. The writer keeps the pool and appends a changed string at its end.
  • The fonts end in a "leftover buffer": each byte is the byte one block earlier XORed with the key. The reader already checked that rule, so the writer makes the buffer again from it.

The track writer was the large one. It cuts a track at the reader's own boundaries: the header, the texture block, each sector, the props, the lights, the sky, the race data, and entries 1 to 8. Joining the parts gives the file back. When a part grows, the later entries move, and the entry table has to be written again in raw file offsets, which count the checksum words. The test adds a texture page to all 36 tracks, and each one still reads to the end of every entry.

The result, measured by writing every file of the disc back and comparing all bytes (crates/pod-formats/tests/writers.rs, disc.rs):

FormatFilesWriter
Menu images .IMG146new: image::write; Images::put draws into a picture
Text tables .BMD12new: text::Table; set appends a changed string
Video stills .BBM, .BVB6new: still::File::encode
Texture blocks (tracks, cars, chrome, fonts)371 pagesnew: texture::Block; put draws into a page
Fonts .BP3, effects .BI24 and 3new: Font::encode, Effect::encode
Tracks .BL3, .BL436new: track::layout
Cars .BV3, .BV418parameters (before), garage settings (new)
Container, names, sounds, best times, lists, car listsbefore this session

A changed copy of each also reads back changed. The track writer keeps most parts as the bytes it read; only its textures and sector vertices can be edited so far.

What went wrong on the way ​

  • A test that seemed to hang. The first run of the writer tests sat for minutes. It was a debug build: the image test looks up the nearest palette colour for every pixel of 146 pictures. In a release build the tests take 8 seconds.
  • A block that was not one. The texture test failed on TEST.BL4: the scan that finds texture blocks by their records guessed a page size for the .BL4 sky that runs past the end of the file. Viewers can live with a guess; a writer cannot. The track writer now uses the reader's own walk, and the scan marks that block as partial.
  • A string that read back as two. Text pools can end in leftover bytes with no zero after them, so an appended string ran into them. The writer now adds the terminator first.
  • Keys that did not arrive. In the emulated PC, xdotool moved the mouse but the menus ignored its clicks and keys. The game reads input through DirectInput; the Wine-side input tool (podinput, through inside.sh input) works.
  • A command that killed itself. pkill -f follow.py run over SSH matched the SSH shell's own command line and killed it, so the restart after it never ran. And inside.sh stop stops every Xvfb in the VM, which also stopped another session's display. Both are now know-how entries.

Where the game looks ​

How it is known: the path builder and its caller were read in the decompiled code (observed), then a changed file under the install folder was seen in the game (measured).

open_data_file (FUN_0047ba70) builds two names for every file from a folder number: one under the Directory set in Ubi.ini, one under the CD's root. It tries the first, then the CD. So a mod needs no patch and no new disc, just a file at the same path under C:\POD\. The full table of folders is in Modding.

In the game ​

All of this ran in the emulated PC, with the menus clicked by podinput and the race driven by the line follower. Each mod was made with make_mod (an example program over the writers) and copied under C:\POD\Data\... with run/vm/mods.sh. The first three loaded in one run: the menu image on the track screen, the lap label in the race, the texture in Beltane's tunnel.

The track screen: Beltane's map with a frame and MOD stamped on it

The menu image. The writer asked for yellow and got the nearest colour in the picture's own palette, a green.

The race HUD: MOD 1/3

The text table: "Tour" in the lap counter is now "MOD".

The chequered floor of Beltane's tunnel

The track texture: page 0 of BELTANE.BL3. The palette has no magenta, so those squares came out grey.

The grip that was not used ​

The fourth mod should have been easy. The car parameters already had a writer, so we cut GAMMA's grip on all four wheels from about 3.4 and 4.2 to 1.2 and drove a race. The race took 8:05.50 against 8:06.05 with the disc's file, and the best laps were 2:37.39 against 2:33.38: no change beyond the follower's own spread.

Reading the game's memory showed why. The changed values were in the car's slot, so the file had loaded. But the race car held 3.67 on every wheel, and so did a race with the disc's own file. The garage setup replaces the file's grip: setup_defaults (FUN_004436c8) computes one grip from the car's "adhérence" setting, and car_from_setup (FUN_00442f04) writes it over the parameters. A race with the disc's file confirmed it: the same 3.67, read from the same address (measured).

The five garage settings at the end of the car file turned out to be the five figures of the car screen, in order: acceleration, brakes, grip, steering, speed. So the mod became the third setting.

The car screen with Adhérence 5

Grip settingRace car's grip (from memory)Driven by the line follower
95 (the disc)3.67laps at 159, 317 and 472 s; again at 154, 323 and 482 s
502.50laps at 202 and 506 s (the game's own lap times 3:28.38 and 5:00.79), then wedged in lap 3
51.330.69 of a lap in 625 s, standing for 418 s of them

The race grip at each setting was read from the race car in memory (measured); the lap ends come from the follower's log, counted along the recorded line. The three points lie on one line: grip = 1.2 + 0.026 × setting. With half the grip the first lap took 202 seconds instead of about 157, and the second, with a slide into a wall, 304. At 5 the car slides into the walls and the follower cannot get it out.

Grip 50: lap times 3:28.38 and 5:00.79 on the HUD

For GameFrame ​

Four know-how entries in docs/knowhow/: CD-era games that look in their install folder first, writers proved by round trips on every file, a value replaced at run time, and pkill -f matching itself over SSH. The tools worth generalising (a round-trip harness, a container writer, a mod-folder installer, a memory read-back) are listed under "Proposed for GameFrame" in docs/gameframe-lessons.md.

Next ​

  • Write the track's other parts from their fields, so the editor can change props, lights and race paths and not only textures and vertices.
  • Trace how the garage setup builds the torque curve and the other values from the settings.
  • The 3D-card builds: check that they use the same folders.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.