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:
- The 15 retail racers, in each layout's reading order (the order retail's tables place them).
- N. Oxide, once unlocked (
GAME_UNLOCK_BIT_OXIDE: beat him in Adventure, or the Spyro 2 demo code). - 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.
- In 1P/2P, with nothing in the last row (no secret or extra racer), the grid moves down 0x20 under the "Select Character" title: retail's unexpanded layout.
- In 3P/4P the title is drawn when the page's top row is empty.
- Icons slide in with retail's stagger. A turned page slides in from the side it was turned toward.
Navigation
- Left and right move along the row. Up and down go to the nearest row with an icon at most half a column aside; on a tie, down goes right and up goes left. Retail's own links follow these rules.
- Icons taken by another player are passed over.
- At a dead end (every icon that way taken, or none), the cursor first steps one place across, then moves from there. Across is down then up for left and right, and left then right for up and down, like retail's fallback directions.
- Off the end of a row, left and right turn the page; so do L1 and R1. The page is shared: any player who has not chosen can turn it. A cursor whose racer is on another page is hidden until its next d-pad press brings it onto the shown 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": "..."}
}
}
nameandshort_name: printable ASCII, drawn with the game's font (under 32 and 16 bytes).template: the retail racer whose character ID it borrows:crash cortex tiny coco ngin dingodile polar pura pinstripe papu roo komodo tropy penta fakecrash oxide.engine:balanced,accel,speedorturn.wheels: whether the game draws its tires.model.ctr: a racer model as the BIGFILE stores one (a u32 pointer-map offset, the model, the pointer map), at most 512 KiB. The character select draws a racer with wheels at race size, since the menu's wheels sit where the race's do; one without wheels is scaled 5/8 to fit, like N. Oxide's hovercraft.- Converted models have no animations and can use all 256 vertex slots of a command list. Retail models stay under the 88 that fit the scratchpad's vertex cache, so slots past it live in their own array (
RenderBucket_PackedVertexScratch,game/RenderBucket/RenderBucket_QueueExecute.c). Past the scratchpad lies the first draw buffer, so a slot written there cuts its draw list short (the HUD and loading flag flicker every other frame). portrait.bin:CDP1, u16 width 44, u16 height 26, 16 palette words, then 26 rows of 11 words of 4-bit texels. PS1 15-bit colours, word 0 transparent.- Every file's size and SHA-256 must match. The id is unique: a second folder with the same id is skipped.
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:
- The Mods screen's track picture, at (512, 256) in the second page row (CUSTOM_TRACKS.md).
- The title screen's Roo (
ap/ap_title_roo.c, Archipelago build): his podium textures at (512, 0), 384×256. He dances in Crash's place using his podium dance model from the player's disc, with texture layouts tagged to sample the bank, so the main menu's VRAM is untouched.
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:
- The model is the patched
BIGFILEracer model (high detail). - The portrait is cut from the patched shared VRM at the template's portrait (44×26, 4-bit; positions in the script).
- The names come from the patched English language file.
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.
- A listed racer's portrait shows before install. When the selection rests on it,
NativeRacerCatalogue_StartPreviewfetches itsracer.json(checked against the index), then the portrait (checked againstracer.json), and keeps it inassets/racers/previews/<id>-<first 16 of the racer.json hash>.bin, so a seen racer shows offline and an updated one is fetched again. It is drawn from the slot the track pictures use, so one hides the other (game/230/MM_CustomPicture.c). - Downloads come from
raw.githubusercontent.com, the catalogue'smainbranch, through the same HTTPS code as Project Saphi (ap/ap_custom_track_download.cpp,NativeRacerCatalogue_*). The index pins eachracer.json(size and SHA-256) andracer.jsonpins its files, so the catalogue's.gitattributeskeepsracer.jsonbyte for byte (eol=lf). NativeRacerLibrary_Installchecks the files against each other, writes them to a hidden.<folder>.partfolder, removes any installed folder with the same id, then renames the staging folder into place.NativeRacerLibrary_Removedeletes the racer's folder.- After either,
NativeRacers_Reloadrereads the library so the character select has the change at once, and clears the players' racer picks, since indices change.
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:
VehBirth_NonGhostbirths it with the racer's model, at every split-screen size. In Time Trial,MainInit_Driversthen gives player 1 the template's high-detail model but leaves a custom racer's alone.VehBirth_TireSpritesgives it wheels or not bywheels.VehBirth_SetConstsreads the racer's engine class (VEH_BIRTH_ENGINE_ID,include/common.h). Engine sound stays the template's.- The HUD portraits use the racer's (
VEH_DRIVER_ICON): race rank list, cup standings, race results, VS and battle standings.
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.