CTR Decrypted

Character select and custom racers

The native character select is built from a roster list instead of retail's hand-made icon tables, so it can hold N. Oxide and any number of custom racers. With only the retail racers, it draws, animates and navigates exactly like retail.

Code: game/230/MM_CharacterGrid.c (included from MM_Characters.c; the PS1 build keeps the retail functions there). Racers: platform/native_racers.c (models, portraits, the players' picks) and platform/native_racer_library.cpp (installed packages).

The roster

In this order:

  1. The 15 retail racers, in each layout's reading order (the order retail's tables place them).
  2. N. Oxide, once unlocked (GAME_UNLOCK_BIT_OXIDE: beat him in Adventure, or the Spyro 2 demo code).
  3. The custom racers, in library order.

A locked retail racer keeps an empty place, as in retail, except in the last retail row, where gaps would push extra racers off-centre.

The layout

Each player count has a list of rows in fill order, each with a height and a capacity:

Players Rows (fill order) Centre
1P/2P three rows of 6, top down x 0xE0
3P three rows of 4, then the top row of 4 x 0x80
4P two rows of 6 between the windows, 2 under them, 2 above x 0xE0

Entries flow into the rows, and every row is centred, so 15 racers land exactly on retail's positions and Oxide makes the 1P bottom row four wide. A full page spills onto more pages; arrows beside the grid show another page.

Players are tracked by roster entry. GAME_CHARACTER_IDS gets the entry's character ID, and NativeRacers_SetPlayer its racer index (-1 for a retail racer).

Racer packages

A racer is a folder under assets/racers/ (any name) holding racer.json and the files it lists. Package builds only (CTR_CUSTOM_PACKAGES, with the Mods screen). The library reads every folder once, when the character select first needs it. A folder whose manifest or files don't check out is skipped with a [Racers] log line.

{
  "schema_version": 1,
  "id": "uuid",
  "name": "CRUNCH BANDICOOT",
  "short_name": "CRUNCH",
  "version": "1",
  "authors": ["robthefryman", "Angel123/Booz"],
  "credits": "",
  "source": {"file": "ctr-u_CrunchKart.xdelta", "sha256": "...", "url": "https://gamebanana.com/mods/575350"},
  "template": "crash",
  "engine": "balanced",
  "wheels": true,
  "files": {
    "model": {"path": "model.ctr", "bytes": 14536, "sha256": "..."},
    "portrait": {"path": "portrait.bin", "bytes": 612, "sha256": "..."}
  }
}

Portraits: the racer VRAM bank

Portraits go to a second VRAM, a GL texture with the PS1 VRAM's size and format (NativeRenderer_CreateVramBank). A primitive whose tpage word has both bits 14 and 15 set (NATIVE_TPAGE_RACER_BANK, include/platform/native_gpu.h) samples the bank instead of VRAM, with its page, palette, depth and blending unchanged, so the paletted shaders and every icon draw path work as is. Either bit alone keeps its Archipelago meaning (box atlas, trial letters).

Portrait i sits in a 64×32-texel slot, four across and eight down per 4-bit page, from page 0 of the first page row (64 racers fill two pages). Its palette goes in page 15, four per row. Each racer gets an Icon whose layout points there (platform/native_racers.c).

Other users of the bank, which anything new must stay clear of:

Converting PS1 racer mods

Racer mods for the disc are xdelta patches that replace a retail racer. tools/racers/convert_racer.py applies each patch to a clean disc and writes one package per racer it replaces:

A racer whose mod kept the retail name or portrait is refused until sources.json names it; nothing from the retail disc goes into a package. sources.json also holds authors, credits and the source URL, keyed by patch file name. Two mods with the same model give one racer. The script prints what it wrote, skipped and refused.

gcc -O2 -Iinclude tools/racers/xdelta_apply.c -o tools/racers/xdelta_apply.exe
python tools/racers/convert_racer.py --disc "CTR (USA).bin" --sources sources.json --out racers/ mods/*.xdelta

The catalogue, Decryptu/ctr-decrypted-racers, keeps the converted racers under racers/ with its sources.json. tools/racers/build_index.py <catalogue> checks every racer folder and writes index.json (each racer's folder, authors, and its racer.json size and SHA-256) and CREDITS.md. It refuses a racer with no authors. A racer that reuses a look from another release says so in credits (the Japanese Crash credits the Japanese release).

Converted racers are static, vertex-coloured meshes with no textures or animations; a textured racer would use the same VRAM bank. Their kart textures, voices and code patches are not carried over.

The Racers tab

The Mods screen's Racers tab (game/230/MM_CustomRacers.c) lists every racer in the catalogue's index.json and every installed racer it doesn't list, by name, with OWNED on installed ones. The panel shows the portrait, the authors and the template racer. Cross downloads a racer, or asks to remove an installed one. L2 reloads the catalogue; R2 searches names and authors. The Installed tab lists installed racers after the tracks, with the same panel and Cross.

CTR_DEV_MODS=2 opens the tab (DEV-TOOLS.md); CTR_DEV_PAD presses then drive it. The catalogue loads within about 300 frames of a fast-forwarded run.

In a race

A driver races as a custom racer when it is a human player who picked one, outside Adventure and the menus, and its character ID is still the racer's template (NativeRacers_Driver). Anything that changed the ID since (Archipelago's character swap, the garage) changed the racer too. Then:

Everything else keyed by character ID uses the template: voice, podium, records, ghosts, Archipelago. Bots never pick a custom racer.

CTR_DEV_RACER=n with CTR_DEV_RACE puts player 1 in a race as the nth installed racer. Through the menus: CTR_DEV_CHARACTER_SELECT=1 with CTR_DEV_MODE=tt opens the Time Trial character select, and CTR_DEV_PAD picks the racer, track and ghost.

Checking

CTR_DEV_CHARACTER_SELECT=1 opens the screen (DEV-TOOLS.md). CTR_DEV_FAKE_RACERS=n replaces the installed racers with n placeholders that borrow the first eight retail racers' looks, to fill pages. To check retail is unchanged, A/B the screen against an older build with CTR_DEV_UNLOCKS=0 and =1 at 1 to 4 players, with CTR_DEV_PAD scripts that hit taken icons and row ends. Every frame should match except the exit slide-out, which older builds did not animate.