Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@
Steam Deck Wi-Fi and separate-machine acceptance remain necessary. See the
release validation record for exact final-build coverage.

- Custom tracks can carry up to 127 checkpoints per vehicle set, up from
retail's 60, for finer AI racing lines. Two recomp-policy instruction patches
grow the checkpoint table and its load bound in both revisions.
- Track Lab boss races start and end: a `.dkrmap` boss level no longer loads an
unrelated level in place of the retail intro and outro scenes, whose lookup
table holds only retail level ids.
- Shadows can no longer overflow the game's fixed shadow arrays. A custom floor
cut into many small faces under a racer used to draw stray triangles and then
crash; the shadow now drops the pieces that do not fit.
- A hub's exits can lead to other `.dkrmap` tracks. The package names the
destination track in `levelLinks`, and the runtime writes its level id into
the exit when the hub's object map loads.

## 1.0.14 — 2026-11-04

- Manual Play now fades into the shared Starting Game screen with inline mod
Expand Down
97 changes: 97 additions & 0 deletions docs/CUSTOM_TRACKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,53 @@ A hub gate is two separate objects in a level's object map, not one:

`destinationMapId` is a `u8`. Retail uses 34 level ids, so roughly 220 remain.

An exit fires for a player inside its radius (the raw byte, in units; at least
5) who has crossed the plane through the exit facing its `angleY`
(`obj_loop_exit`, `object_functions.c:2441-2457`). Its bytes become the next
level's settings as they are (`func_8006D968`, `thread3_main.c:790-806`):
`destinationMapId` the level, `overworldSpawnIndex` the entrance used when that
level is a hub, and `returnSpawnIndex` the entrance of *this* hub the player
comes back to after a race (255: not here). A hub needs a start position for
every entrance something returns to, and entrance 0, which a new game uses.
`LevelObjectEntry_Door.levelID` (`+0x14`) is also a level id, but only for the
door's look: `obj_loop_door` reads that level's cleared and silver-coin flags
to pick the door model. It never warps anywhere.

### Exits to other custom tracks: `levelLinks`

A custom track's level id is assigned at load from the installed set, so a hub
cannot store it in `destinationMapId` when it is exported. Track Lab lets an
exit name a destination track by its id instead. The exit is exported with
level 0 (the Central Area hub) as a placeholder, and the manifest says which
byte to patch:

```json
"levelLinks": [
{ "slot": "structure", "offset": 382, "track": "frosty-ridge" },
{ "slot": "collectables", "offset": 40, "track": "sunset-bay" }
]
```

- `slot` is the object map that holds the exit, `structure` or `collectables`.
- `offset` is the position of the exit's `destinationMapId` byte in that map
**as the game holds it after inflating it**. That buffer is the 16-byte map
header (a big-endian `u32` with the size of the entries, then 12 zero bytes),
followed by the entries in order. `track_spawn_objects` starts spawning at
`gObjectMap[index] + sizeof(uintptr_t)` on an `s32 *` (`objects.c:980`),
which is 16 bytes in. So
`offset = 16 + (bytes of the entries before the exit) + 8`, and 8 is
`destinationMapId`'s place in `LevelObjectEntry_Exit`. It always falls in
`[16, 16 + entry bytes)`.
- `track` is the destination's manifest `id`.

When the map loads, before the exit spawns, the runtime writes the level id it
resolved for `track` into that byte. If that track is not installed, it logs
the link and keeps the placeholder, so the exit leads to the Central Area hub
and not to some other level. Only exits carry links. The source glTF keeps the
destination as `destinationTrack` in the exit's extras, so a re-import keeps
the link. The encoder writes only the entry's own fields, so the extra never
reaches the game's bytes.

## Progression

The retail EEPROM is 512 bytes, bit-packed with per-slot checksums, and fixed
Expand Down Expand Up @@ -439,6 +486,37 @@ appended level ID. The native menu supports IDs below 128; higher IDs remain
available through Track Lab. Hub, boss and special level types are not added
to the normal-race grid.

Challenge (battle, bananas, eggs), boss and hub levels play from a `.dkrmap`
through Track Lab's auto boot or a hub exit; retail decides everything from the
header's race type. One retail lookup needs help. The first time a boss level
loads, `level_load` swaps it for the boss's introduction scene, found by
scanning `ASSET_MISC_67` for `{boss level, intro level}` pairs until the level
id matches, and `racer_boss_finish` repeats that scan for the closing scene
unless the course is cleared. An appended level id is in no pair, so both scans
run past the table and load an unrelated level (the camera froze in one). The
`level_load` hook therefore marks a `.dkrmap` boss course visited and cleared
before the header is read: retail's rematch path, which races at once and ends
with a plain transition back to the hub - the one the player came from, or for
a test boot the hub of the world the game is served (Dino Domain for
`WORLD_CUSTOM_TRACKS`). A boss level never runs a silver coin race, so the
cleared bit unlocks nothing; the log shows
`boss race runs without the retail intro and outro scenes`.

**Shadows on busy floors.** Every racer's shadow is built from the floor
triangles under it (`shadow_generate`, `func_8002E904` in `tracks.c`) into
fixed arrays with no bound checks: 32 vertices (`D_8011B120`), 32 polygons
(`D_8011C238`), and the points clipping creates, kept in four groups of 32
(`D_8011B330`) - which retail declares with 120 entries, so the fourth group's
last eight land on the polygon count. Retail floors never come close. A custom
floor cut into many small faces can: a plane flying under the covered plaza of
an Eggs arena overflowed them, drew stray triangles and then wrote outside
RDRAM. Two hooks in `func_8002E904` keep each polygon inside the arrays - one
gives the clip no vertices when the fourth group or the polygon slots are
full, one drops a clipped polygon whose vertices might not fit - so the shadow
loses a piece instead. The log says so once per session
(`a shadow crosses more floor edges…` / `a shadow covers more floor
polygons…`).

World `6` only files the course; the game never sees it. Retail indexes
five-world arrays with `header->world - 1`. The post-race mosaic read past
`gTracksMenuBgTextureIndices`, and `bgdraw_texture` then tiled a non-texture
Expand Down Expand Up @@ -797,6 +875,25 @@ python scripts/generate_legacy_menu_qualification.py --characters --character-me
Point `DKR_GENERATED_SOURCE_V77` / `_V80` at `generated-v77` / `generated-v80`
in that folder. The presentation step accepts only the reviewed ELF images.

**A vehicle set holds up to 127 checkpoints.** `checkpoint_update_all` loads
one set's gates (those whose `vehicleType` matches the header's `unk4F` entry
for the vehicle), main and alternate route together, into `gTrackCheckpoints`
in spawn order, and silently skips any past `MAX_CHECKPOINTS` (`objects.c`).
Retail's is 60, which caps how finely a course can steer its AI racing line.
Two `instructionPatches` in each versioned recomp policy raise it to 127: the
table's allocation in `allocate_object_pools` (`addiu a0, zero, 0xE10` becomes
`0x1DC4`, 127 × the 0x3C-byte `CheckpointNode`) and the load bound in
`checkpoint_update_all` (`slti at, a3, 60` becomes `127`). 127 is the real
ceiling: a gate's place in the table lives in signed bytes
(`Object_Racer.nextCheckpoint` at 0x192, `Object_Weapon.checkpoint`,
`CheckpointNode.altRouteID`), and the racer code increments `nextCheckpoint`
before comparing it with the count, so the count may not exceed 127. Nothing
else in the game or the runtime sizes anything by checkpoint count: the AI and
homing-rocket splines take four or five consecutive gates and wrap their index,
`courseCheckpoint` is an `s16`, and the netplay state hash hashes racer fields,
not the table. Track Lab reports a set over 127 as an error and one over 60 as
a warning, since older DKR-R builds and original hardware still load only 60.

**`mods/` belongs to librecomp.** N64ModernRuntime ships its own mod system,
it is live in DKR-R, and it scans `mods/` for its `.nrm` format. A `.dkrmap`
placed there is reported as `Mod is missing a mod.json` to the user. Custom
Expand Down
Loading
Loading