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 mkdsModules:
mkds.nkm,mkds.kclStatus: 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
Sectionclass 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.
NKMadds 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.
KCLBaseparses positions/normals and exposes prism attributes as Python lists.
Fixed-Point Formats¶
Many numeric fields are fixed-point: Fx16 and Fx32. Readers in
mkds.utilsconvert 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
Sectionbase class:Parses
entry_countfrom 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 bytesentry_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:
0x3CbytesPurpose: 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 @0x0Cscale_vec: list[tuple[float,float,float]]— scale vector @0x18object_id: list[int]— @0x24route_id: list[int]— @0x26 (0xFFFF ⇒ no route)object_settings: list[list[int]]— four UInt32s @0x28..0x37show_in_time_trials: list[int]— UInt32 @0x38
Notes: Settings meaning depends on
object_id. Routes link toPATH/POIT.
PATH — Path Metadata¶
Stride:
0x04bytesPurpose: Route descriptors pointing into
POITpoint streams.Fields:
route_id: list[int]— Byte @0x00has_loop: list[bool]— Byte @0x01 (implementation uses!= 1; canonical spec is== 1for loop)point_count: list[int]— UInt16 @0x02
POIT — Path Points¶
Stride:
0x14bytesPurpose: 3D points forming the routes.
Fields:
position: list[tuple[float,float,float]]— VecFx32 @0x00point_index: list[int]— Byte @0x0Cunknown1: list[int]— Byte @0x0Dpoint_duration: list[int]— Int16 @0x0Eunknown2: 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
Nonein this implementation.
KTPS — Kart/Start Positions¶
Stride:
0x1CbytesFields:
position,rot_vec,padding,start_position_index.
KTPJ — Respawn Positions¶
Stride:
0x20bytesFields:
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:
0x1CbytesFields:
position,rot_vec,padding,index(often 0xFFFF).
KTPC — Cannon/Pipe Destinations¶
Stride:
0x1CbytesFields:
position,rot_vec,unknown,cannon_index.
KTPM — Mission Points¶
Stride:
0x1CbytesFields:
position,rot_vec,padding,index.
CPOI — Checkpoints¶
Stride:
0x24bytesFields:
position1,position2: 2D vectors (VecFx32) @0x00, 0x08sinus,cosinus,distance: Fx32 @0x10..0x18section_data1,section_data2: UInt16 @0x1C..0x1Ekey_id: UInt16 @0x20 (0=lap, 0xFFFF=none, otherwise keyed checkpoint)respawn_id: Byte @0x22unknown: Byte @0x23
CPAT — Checkpoint Groups¶
Stride:
0x0CbytesFields:
point_start,point_length,next_group[3],prev_group[3],section_order.
IPOI — Item Points¶
Stride:
0x14bytesFields:
position,point_scale(Fx32),unknown(UInt32).
IPAT — Item Groups¶
Stride:
0x0CbytesFields:
point_start,point_length,next_group[3],prev_group[3],section_order.
EPOI — CPU Path Points¶
Stride:
0x18bytesFields:
position,point_scale(Fx32),drifting(Int16),unknown1(UInt16),unknown2(UInt32).
EPAT — CPU Grouping¶
Stride:
0x0CbytesFields:
point_start,point_length,next_group[3],prev_group[3],section_order.
MEPO — Minigame Enemy Points¶
Stride:
0x18bytesFields:
position,point_scale,drifting(Int32),unknown(UInt32).
MEPA — Minigame Grouping¶
Stride:
0x14bytesFields:
point_start,point_length,next_group[8],prev_group[8].
AREA — Areas / Zones¶
Stride:
0x48bytesFields:
position(center),length_vec,x_vec,y_vec,z_vec, camera linkage, and several unknowns.area_typeis left asNoneplaceholder.
CAME — Cameras¶
Stride:
0x4CbytesFields: 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 indicesattributes: list[list[int]]— parsed bitfields viaparse_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
prismsinstance (class attributeprism_clscontrols 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 withinblock_datacontainingpoint, orNoneif 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 prismidx
KCL¶
A convenience subclass of KCLBase that:
Uses
Prismsasprism_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 astorch.Tensorwith 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 themkdsPyPI package.
Edge Cases & Version Notes¶
NKM
PATH.has_loop: The implementation readshas_loop = (byte != 1)to preserve historical behavior. Canonical spec treats1as 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) returnNoneinsearch_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.