mkds — Mario Kart DS NKM & KCL Readers

Installable parser library for Mario Kart DS course files.
Read NKM (course map) and KCL (collision) files with clean Python APIs. Designed for tooling, editors, analytics, and research.

  • PyPI: pip install mkds

  • Modules: mkds.nkm, mkds.kcl

  • Status: Focused on correctness and readability. Torch extensions live outside this package (see Torch Extensions (Optional)).


Table of Contents


Overview

The mkds package provides binary readers for Mario Kart DS course files:

  • NKM — Course map logic and gameplay data (objects, paths, checkpoints, cameras, etc.).

  • KCL — Collision meshes using triangular prisms with an octree index.

This library focuses on structured access to course data with minimal surprises:

  • Sections are parsed into Python objects with typed lists/fields.

  • Iteration is predictable via a base Section class for fixed-stride sections.

  • Field readers (read_u16, read_fx32, etc.) hide fixed-point details.


Installation1

pip install mkds

Python ≥3.10 is recommended.


Getting Started

from mkds.nkm import NKM
from mkds.kcl import KCL

# NKM
nkm = NKM.from_file("my_course.nkm")
print(len(nkm._OBJI))           # number of object instances
print(nkm._STAG.amt_of_laps)    # lap count

# KCL
kcl = KCL.from_file("course_collision.kcl")
print(len(kcl.prisms))          # number of prisms
print(kcl.positions[0])         # first vertex position

Design & Conventions

Binary Layouts

  • NKM contains a header with relative offsets to each section. NKM adds the fixed header length (0x4C) to compute absolute file positions and slices the byte array to create each section object.

  • KCL contains offsets to positions, normals, prisms, and the octree block region. KCLBase parses positions/normals and exposes prism attributes as Python lists.

Fixed-Point Formats

  • Many numeric fields are fixed-point: Fx16 and Fx32. Readers in mkds.utils convert to Python floats transparently.

  • Vector readers (e.g., read_vector_3d_fx32) return (x, y, z) tuples in floating-point.

Section Iteration Model

  • Most NKM sections (except STAG) implement:

    • An 8-byte header (magic, entry_count).

    • Fixed-size entries (a stride).

  • The Section base class:

    • Parses entry_count from the header.

    • Provides __len__ and __iter__ to iterate over raw entry bytes.

    • Concrete subclasses decode per-entry fields into Python lists.


NKM — Course Map

Quickstart

from mkds.nkm import NKM

nkm = NKM.from_file("my_course.nkm")

# Example: iterate object IDs
for oid in nkm._OBJI.object_id:
    print("Object ID:", oid)

# Example: checkpoint segment endpoints (2D vectors)
p1 = nkm._CPOI.position1[0]
p2 = nkm._CPOI.position2[0]

Header & Section Offsets

  • NKM._header_offset = 0x4C (header length).

  • Section offsets in the header (at 0x08..0x48) are relative to the end of the header; the parser adds 0x4C to get absolute positions.

  • The canonical order in this implementation: OBJI, PATH, POIT, STAG, KTPS, KTPJ, KTP2, KTPC, KTPM, CPOI, CPAT, IPOI, IPAT, EPOI, EPAT, AREA, CAME.

Sections API

Below, fields are Python lists with per-entry values unless noted.

Section (base)

  • Purpose: Common scaffolding for fixed-stride NKM sections.

  • Attributes:

    • data: Raw section bytes (header + entries)

    • stride: Entry size in bytes

    • entry_count: Count parsed from header (UInt32 at offset 0x04)

  • Iteration: Iterates entry slices (data[8 + i*stride : 8 + (i+1)*stride]).


OBJI — Object Instances

  • Stride: 0x3C bytes

  • Purpose: Placement of map objects (decorations, interactives, item boxes, etc.).

  • Fields:

    • rot_vec1: list[tuple[float,float,float]] — position (X,Y,Z) @0x00 (VecFx32)

    • rot_vec2: list[tuple[float,float,float]] — rotation vector @0x0C

    • scale_vec: list[tuple[float,float,float]] — scale vector @0x18

    • object_id: list[int] — @0x24

    • route_id: list[int] — @0x26 (0xFFFF ⇒ no route)

    • object_settings: list[list[int]] — four UInt32s @0x28..0x37

    • show_in_time_trials: list[int] — UInt32 @0x38

  • Notes: Settings meaning depends on object_id. Routes link to PATH/POIT.


PATH — Path Metadata

  • Stride: 0x04 bytes

  • Purpose: Route descriptors pointing into POIT point streams.

  • Fields:

    • route_id: list[int] — Byte @0x00

    • has_loop: list[bool] — Byte @0x01 (implementation uses != 1; canonical spec is == 1 for loop)

    • point_count: list[int] — UInt16 @0x02


POIT — Path Points

  • Stride: 0x14 bytes

  • Purpose: 3D points forming the routes.

  • Fields:

    • position: list[tuple[float,float,float]] — VecFx32 @0x00

    • point_index: list[int] — Byte @0x0C

    • unknown1: list[int] — Byte @0x0D

    • point_duration: list[int] — Int16 @0x0E

    • unknown2: list[int] — UInt32 @0x10


STAG — Stage Info

  • Unique: No section header. Fixed-size 0x2C struct.

  • Fields (high level):

    • track_id: int, amt_of_laps: int, fog_enabled: bool, fog params, KCL colors (placeholders), etc.

  • Notes: Color fields (GXRgb) are left as None in this implementation.


KTPS — Kart/Start Positions

  • Stride: 0x1C bytes

  • Fields: position, rot_vec, padding, start_position_index.


KTPJ — Respawn Positions

  • Stride: 0x20 bytes

  • Fields: position, rot_vec, enemy_position_id (EPOI), item_position_id (IPOI), respawn_id (may be absent in very old versions).


KTP2 — Lap Checkpoint Points

  • Stride: 0x1C bytes

  • Fields: position, rot_vec, padding, index (often 0xFFFF).


KTPC — Cannon/Pipe Destinations

  • Stride: 0x1C bytes

  • Fields: position, rot_vec, unknown, cannon_index.


KTPM — Mission Points

  • Stride: 0x1C bytes

  • Fields: position, rot_vec, padding, index.


CPOI — Checkpoints

  • Stride: 0x24 bytes

  • Fields:

    • position1, position2: 2D vectors (VecFx32) @0x00, 0x08

    • sinus, cosinus, distance: Fx32 @0x10..0x18

    • section_data1, section_data2: UInt16 @0x1C..0x1E

    • key_id: UInt16 @0x20 (0=lap, 0xFFFF=none, otherwise keyed checkpoint)

    • respawn_id: Byte @0x22

    • unknown: Byte @0x23


CPAT — Checkpoint Groups

  • Stride: 0x0C bytes

  • Fields: point_start, point_length, next_group[3], prev_group[3], section_order.


IPOI — Item Points

  • Stride: 0x14 bytes

  • Fields: position, point_scale (Fx32), unknown (UInt32).


IPAT — Item Groups

  • Stride: 0x0C bytes

  • Fields: point_start, point_length, next_group[3], prev_group[3], section_order.


EPOI — CPU Path Points

  • Stride: 0x18 bytes

  • Fields: position, point_scale (Fx32), drifting (Int16), unknown1 (UInt16), unknown2 (UInt32).


EPAT — CPU Grouping

  • Stride: 0x0C bytes

  • Fields: point_start, point_length, next_group[3], prev_group[3], section_order.


MEPO — Minigame Enemy Points

  • Stride: 0x18 bytes

  • Fields: position, point_scale, drifting (Int32), unknown (UInt32).


MEPA — Minigame Grouping

  • Stride: 0x14 bytes

  • Fields: point_start, point_length, next_group[8], prev_group[8].


AREA — Areas / Zones

  • Stride: 0x48 bytes

  • Fields: position (center), length_vec, x_vec, y_vec, z_vec, camera linkage, and several unknowns. area_type is left as None placeholder.


CAME — Cameras

  • Stride: 0x4C bytes

  • Fields: 3D positions, rotation, FOV begin/end (+ sine/cosine), zoom, type, linked route, speeds, duration, next camera, intro-pan indicator, etc.

  • Notes: Several precomputed fields (sin/cos) are preserved as-is.


NKM — Top-Level Parser

from mkds.nkm import NKM

nkm = NKM.from_file("my_course.nkm")
# Access parsed sections:
nkm._OBJI, nkm._PATH, nkm._POIT, nkm._STAG, nkm._KTPS, nkm._KTPJ, nkm._KTP2, \
nkm._KTPC, nkm._KTPM, nkm._CPOI, nkm._CPAT, nkm._IPOI, nkm._IPAT, nkm._EPOI, \
nkm._EPAT, nkm._AREA, nkm._CAME
  • Construction: Slices raw file by computed offsets to build section objects.

  • Classmethod: NKM.from_file(path) opens and parses bytes in one call.


KCL — Collision

Quickstart

from mkds.kcl import KCL

kcl = KCL.from_file("course_collision.kcl")
print("Prisms:", len(kcl.prisms))
print("First prism vertex index:", kcl.prisms.pos_i[0])

Header Layout

Offset

Type

Name

Description

0x00

u32

positions_offset

Start of position array

0x04

u32

normals_offset

Start of normal array

0x08

u32

prisms_offset

Start of prism array

0x0C

u32

block_data_offset

Start of octree blocks

0x10

f32

prism_thickness

Depth of triangular prism along normal

0x14

Vec3

area_min_pos

Min corner of model bounding box

0x20

u32

area_x_width_mask

X-axis mask for octree

0x24

u32

area_y_width_mask

Y-axis mask for octree

0x28

u32

area_z_width_mask

Z-axis mask for octree

0x2C

u32

block_width_shift

Leaf block size (shift)

0x30

u32

area_x_blocks_shift

Root child index shift (Y)

0x34

u32

area_xy_blocks_shift

Root child index shift (Z)

0x38

f32?

sphere_radius

Optional, not parsed here

API

PrismsBase

Represents the raw prism table. Each entry (stride 0x10) has:

  • height: list[float]

  • pos_i: list[int]

  • fnrm_i, enrm1_i, enrm2_i, enrm3_i: list[int] — normal indices

  • attributes: list[list[int]] — parsed bitfields via parse_attributes(bits):

    • [ map_2d_shadow, light_id(1-3), ignore_drivers, collision_variant, collision_type, ignore_items, is_wall, is_floor ]

Constructors:

  • PrismsBase.from_bytes(data: bytes) -> PrismsBase — parse a contiguous prism region.

  • PrismsBase.parse_attributes(bits: int) -> list[int] — helper for attribute bit slicing.

KCLBase

Top-level parser for collision files (bytes-level). Responsibilities:

  • Parse header offsets and metadata.

  • Parse positions (VecFx32) and normals (VecFx16) based on max indices found in prisms.

  • Build a prisms instance (class attribute prism_cls controls which concrete class is used).

Helpers:

  • _parse_positions(data, prisms, positions_offset)list[tuple[float,float,float]]

  • _parse_normals(data, prisms, normals_offset)list[tuple[float,float,float]]

  • search_block(point)leaf block offset within block_data containing point, or None if outside masks.

Leaf encoding note: In octree leaves, prism indices are stored as 1-based UInt16 values terminated by 0x0000. When reading, subtract 1; stop at -1 sentinel.

Prisms

A convenience subclass of PrismsBase that adds:

  • __len__(), __iter__()

  • __getitem__(idx) → returns all attributes for prism idx

KCL

A convenience subclass of KCLBase that:

  • Uses Prisms as prism_cls.

  • Adds a human-readable __str__ summary.

  • Provides from_file(path) to open+parse in one call.


Torch Extensions (Optional)

This package focuses on I/O. If you need tensor-native processing (GPU/MPS), consider separate extensions (not part of mkds):

  • PrismsTensor — wraps prism arrays as torch.Tensor with convenience boolean masks (is_wall, is_floor, etc.).

  • KCLTensor — adds triangle reconstruction and batched queries (e.g., nearest triangles, point→triangle distances).

  • CPOITensor / NKMTensor — expose NKM checkpoint positions as tensors for geometric operations.

Example import paths may vary in your codebase (e.g., utils.kcl_torch / utils.nkm_torch). These are not included in the mkds PyPI package.


Edge Cases & Version Notes

  • NKM PATH.has_loop: The implementation reads has_loop = (byte != 1) to preserve historical behavior. Canonical spec treats 1 as loop enabled. Harmonize across your tools if needed.

  • NKM STAG: Uses placeholders (None) for colors. If you need colors, implement GXRgb decoding and assign real values.

  • Old Beta Tracks: Some fields (e.g., KTPJ.respawn_id) may not exist; handle absent bytes when writing broad-compat parsers.

  • KCL Leaf Blocks: Outside the octree masks (area_*_width_mask) return None in search_block.

  • Fixed-Point Precision: Fx16/Fx32 values are converted to floats, which is adequate for tooling; games may use integer arithmetic internally.


FAQ

Q: How do I list all cameras that follow a route?
A: In nkm._CAME, filter entries where linked_route[i] != 0xFFFF.

Q: How do I determine if a prism is a wall or floor?
A: Use prisms.attributes[i][6] and [7] respectively (or named tensor properties in your Torch extension).

Q: Can I rebuild triangles directly from KCL?
A: Yes, but mkds itself doesn’t compute them. Use a tensor extension (see Torch Extensions) or reconstruct from positions, normals, and prism height/normals like in the example code in your project.

Q: Are indices 0-based?
A: Prism indices in leaves are 1-based and terminated by 0; everywhere else indices into arrays are 0-based in the parsed lists.


License

This documentation accompanies the mkds parsing library. See the repository’s LICENSE file for terms.