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 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:
0x3C
bytesPurpose: 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:
0x04
bytesPurpose: Route descriptors pointing into
POIT
point streams.Fields:
route_id: list[int]
— Byte @0x00has_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
bytesPurpose: 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
None
in this implementation.
KTPS — Kart/Start Positions¶
Stride:
0x1C
bytesFields:
position
,rot_vec
,padding
,start_position_index
.
KTPJ — Respawn Positions¶
Stride:
0x20
bytesFields:
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
bytesFields:
position
,rot_vec
,padding
,index
(often 0xFFFF).
KTPC — Cannon/Pipe Destinations¶
Stride:
0x1C
bytesFields:
position
,rot_vec
,unknown
,cannon_index
.
KTPM — Mission Points¶
Stride:
0x1C
bytesFields:
position
,rot_vec
,padding
,index
.
CPOI — Checkpoints¶
Stride:
0x24
bytesFields:
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:
0x0C
bytesFields:
point_start
,point_length
,next_group[3]
,prev_group[3]
,section_order
.
IPOI — Item Points¶
Stride:
0x14
bytesFields:
position
,point_scale
(Fx32),unknown
(UInt32).
IPAT — Item Groups¶
Stride:
0x0C
bytesFields:
point_start
,point_length
,next_group[3]
,prev_group[3]
,section_order
.
EPOI — CPU Path Points¶
Stride:
0x18
bytesFields:
position
,point_scale
(Fx32),drifting
(Int16),unknown1
(UInt16),unknown2
(UInt32).
EPAT — CPU Grouping¶
Stride:
0x0C
bytesFields:
point_start
,point_length
,next_group[3]
,prev_group[3]
,section_order
.
MEPO
— Minigame Enemy Points¶
Stride:
0x18
bytesFields:
position
,point_scale
,drifting
(Int32),unknown
(UInt32).
MEPA — Minigame Grouping¶
Stride:
0x14
bytesFields:
point_start
,point_length
,next_group[8]
,prev_group[8]
.
AREA — Areas / Zones¶
Stride:
0x48
bytesFields:
position
(center),length_vec
,x_vec
,y_vec
,z_vec
, camera linkage, and several unknowns.area_type
is left asNone
placeholder.
CAME — Cameras¶
Stride:
0x4C
bytesFields: 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
prisms
instance (class attributeprism_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 withinblock_data
containingpoint
, orNone
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 prismidx
KCL¶
A convenience subclass of KCLBase
that:
Uses
Prisms
asprism_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.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 themkds
PyPI package.
Edge Cases & Version Notes¶
NKM
PATH.has_loop
: The implementation readshas_loop = (byte != 1)
to preserve historical behavior. Canonical spec treats1
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
) returnNone
insearch_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.