Source code for mkds.nkm

from .utils import (
    read_u8,
    read_u16,
    read_u32,
    read_s16,
    read_s32,
    read_fx16,
    read_fx32,
    read_vector_2d_fx32,
    read_vector_3d_fx32
)


[docs] class Section: """ Base class for sections in an NKM file. Overview ======== Most NKM sections (all except STAG) begin with an 8-byte section header: - 0x00 (4 bytes): Section magic (ASCII, e.g., "OBJI"). - 0x04 (4 bytes): Number of entries in the section (UInt32). Immediately after the header the section contains `entry_count` entries, each with a fixed stride (size) which is specified by the concrete Section subclass. This base class encapsulates: - raw `data` for the whole section (header + entries) - `stride` (size in bytes of a single entry) - `entry_count` parsed from the header - an iterator over each fixed-size entry slice Notes / Caveats ---------------- * This class assumes the `data` passed includes the section header. * If a section contains variable-length entries (rare in NKM), a custom parser should be used instead of relying on a fixed stride. * Many sections have fields where 0xFFFF or 0xFF indicates "unused" or "none" — callers should treat those sentinel values accordingly. """ def __init__(self, data, size): self.data = data self.iter = range(0x08, len(data), size) self.stride = size # Number of entries (UInt32 at offset 0x04 of the section header). # If `data` is not the whole section starting with the header, this # will be incorrect — ensure `data` includes the 8-byte section header. self.entry_count = read_u32(data, 0x04) def __len__(self): return self.entry_count def __iter__(self): for i in self.iter: yield self.data[i:i+self.stride]
[docs] class OBJI(Section): """ OBJI — Object Instances section. Stride: 0x3C bytes per entry. Purpose ------- Describes every object placed in the track: visual decorations, interactive objects, obstacle instances, item boxes, etc. These objects are instantiated by the game engine and can be linked to PATH routes. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-obji: :end-before: .. _nkm-table-path: Gameplay Context ---------------- - The object ID decides both model and in some cases runtime logic (collision, activatable behaviors). - The route_id links the object to a PATH. If present, the object will be moved/animated by the PATH's POIT points at runtime (e.g., moving platforms, cameras). - The object settings are used by many object types to control behavior: rotation speed, spawn flags, timers, random seeds, or other per-object parameters. Different object IDs require different decoding logic. Reverse-Engineering Notes / Tips -------------------------------- - The four 32-bit settings differ radically between object types — community wikis often contain per-object decode rules (use object ID to branch). - The presence of a non-0 or non-0xFFFFFFFF route_id often means the object will be animated along that route; route indices are bytes in PATH entries but stored here as a 16-bit value (keep an eye on sign/width). - Show-in-time-trials: historically some editors used 0/1 inverse; verify against known tracks when in doubt. Parsing Caveat -------------- We read `object_id` as `read_u16` and `route_id` as `read_u16`. Consumers of this class should treat 0xFFFF in `route_id` as "no route". """ def __init__(self, data): super().__init__(data, 0x3C) self.rot_vec1 = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec2 = [read_vector_3d_fx32(d, 0x0C) for d in self] self.scale_vec = [read_vector_3d_fx32(d, 0x18) for d in self] self.object_id = [read_u16(d, 0x24) for d in self] self.route_id = [read_u16(d, 0x26) for d in self] self.object_settings = [ [read_u32(d, j) for j in range(0x28, 0x38, 0x04)] for d in self ] self.show_in_time_trials = [read_u32(d, 0x38) for d in self]
[docs] class PATH(Section): """ PATH — Path metadata. Stride: 0x04 bytes per entry. Purpose ------- Describes metadata for routes used by objects and cameras. Each PATH entry points to a sequence of POIT entries (control points) that define the route. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-path: :end-before: .. _nkm-table-poit: Gameplay Context ---------------- - Objects or cameras that reference a route will move along the POIT points in order; if the loop flag is set, the route repeats. - Route ID is commonly a small integer; the PATH list enumerates all routes in use on the track. Reverse-Engineering Notes / Caveats ----------------------------------- * The canonical spec states: 0x01 == 1 if the route loops, 0 otherwise. In the code you originally used `read_u8(d, 0x01) != 1` (which inverts the meaning). I have NOT altered your logic — but be aware of the discrepancy: callers should expect `True` when the route loops. Consider changing to `read_u8(... ) == 1` for clarity. * `point_count` enumerates POIT entries but the mapping from global POIT index to route is (index offset + length) — consumers should reconstruct the actual POIT index ranges using CPAT/EPAT/IPAT/MEPA grouping sections when applicable (these groupings partition points). """ def __init__(self, data): super().__init__(data, 0x04) self.route_id = [read_u8(d, 0x00) for d in self] # Warning: original code used inverted logic. The spec: 1 means loop. # We preserve original behavior here; consider flipping if you want literal. self.has_loop = [read_u8(d, 0x01) != 1 for d in self] self.point_count = [read_u16(d, 0x02) for d in self]
[docs] class POIT(Section): """ POIT — Path points (control points). Stride: 0x14 bytes per entry. Purpose ------- Stores the actual 3D points used by PATH routes. Points are grouped by route using the counts stored in PATH plus the various *PAT grouping sections. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-poit: :end-before: .. _nkm-table-stag: Gameplay Context ---------------- - Moving objects and cameras read these points sequentially to interpolate positions. The "point_index" normally indicates position in the route's ordering (0,1,2,...). - Some cameras or scripted objects use `point_duration` to wait between points, enabling non-linear motion. - POIT order in the file is important: group membership is often determined by successive ranges; use PAT sections to map ranges to specific routes. Reverse-Engineering Notes ------------------------- - The unknown fields often show consistent patterns per track editor — check community resources to decode them for special behaviors. - Some older track versions or beta files encode rotation differently; when in doubt, cross-check with KTPJ notes for version-dependent behavior. """ def __init__(self, data): super().__init__(data, 0x14) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.point_index = [read_u8(d, 0x0C) for d in self] self.unknown1 = [read_u8(d, 0x0D) for d in self] self.point_duration = [read_s16(d, 0x0E) for d in self] self.unknown2 = [read_u32(d, 0x10) for d in self]
[docs] class STAG: """ STAG — Stage (track) information. Fixed-size: The STAG section is unique in NKM: it does NOT have a section header and is a single 0x2C-byte structure placed directly in the file (after POIT in the canonical header ordering). Purpose ------- Contains global track settings: track ID, default lap count, fog settings, colors used by KCL (collision visual palettes), and other miscellaneous bytes. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-stag: :end-before: .. _nkm-table-ktps: Gameplay Context ---------------- - Amount of laps controls how many laps the race uses by default for the stage; some tracks use lap_count = 0 for special cases (verify per track). - Fog parameters influence rendering: enabling fog can hide distant objects and alter perceived depth; fog color & distance control atmosphere. - KCL colors are the default palette for collision visualization (useful for editors and collision debugging). Implementation Notes -------------------- * This class sets placeholders for color fields (GXRgb) — you can implement `GXRgb` decoding (usually 2 bytes per color or platform-specific) and populate these fields for richer output. * The unknown arrays are repeated in code (unknown2 and unknown3) — that mirrors the spec layout but may be redundant; keep one copy or rename for clarity if desired. """ def __init__(self, data): self.track_id = read_u16(data, 0x04) self.amt_of_laps = read_u16(data, 0x06) self.unknown1 = read_u8(data, 0x08) self.fog_enabled = read_u8(data, 0x09) != 0 self.fog_table_generation_mode = read_u8(data, 0x0A) self.fog_slope = read_u8(data, 0x0B) self.unknown2 = [read_u8(data, 0x0C + i) for i in range(0, 0x14, 0x01)] self.fog_distance = read_fx32(data, 0x14) self.fog_color = None # TODO: Implement GXRgb parsing & set this. self.fog_alpha = read_u16(data, 0x1A) self.kcl_color1 = None # TODO: Implement GXRgb self.kcl_color2 = None self.kcl_color3 = None self.kcl_color4 = None self.unknown3 = [read_u8(data, 0x0C + i) for i in range(0, 0x14, 0x01)]
[docs] class KTPS(Section): """ KTPS — Kart/Start Positions (Start points for racers). Stride: 0x1C bytes per entry. Purpose ------- Defines start positions (spawn/starting grid) for players/racers. Typically used for the main race starts; can also be used in battle or mission modes. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ktps: :end-before: .. _nkm-table-ktpj: Gameplay Context ---------------- - On race start, the game picks starting positions from this section. - `start_position_index` is relevant for battle stages or mission mode where start ordering differs from main racing. - The rotation vector might be stored differently in beta versions — the canonical community notes indicate the Y-rotation sometimes needs to be computed via Atan2 on rotation vector components for older versions. Notes / Community Tips ---------------------- - Typically the number of KTPS entries equals the number of players or more (some tracks list more possible starts than vehicles). - If you want to reposition start locations, modify positions and write the file back in FX32 format. """ def __init__(self, data): super().__init__(data, 0x1C) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.padding = [read_u16(d, 0x18) for d in self] self.start_position_index = [read_u16(d, 0x1A) for d in self]
[docs] class KTPJ(Section): """ KTPJ — Respawn positions (kart respawn). Stride: 0x20 bytes per entry. Purpose ------- Positions to which a kart (player) can respawn after falling off or during certain scripted events. Contains references to enemy/item points (EPOI/IPOI) to determine nearby behavior or AI context. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ktpj: :end-before: .. _nkm-table-ktp2: Gameplay Context ---------------- - When a kart falls off the track or hits a severe collision, the engine picks a KTPJ respawn that matches the current lap and nearby conditions. - The enemy/item IDs let respawn logic pick nearby AI/item spawn points for smoother reintroduction. Version Notes ------------- - For version 0x1E (older beta), the final Respawn ID (0x1C) may not exist. - The rotation encoding changed for early beta versions; if reading older tracks, compute Y-rotation via atan2(Rx, Rz) to convert to degrees. Remarks for Parser ------------------ * We read `respawn_id` as a 32-bit value; if parsing older tracks that omit it, ensure you guard read beyond section length. """ def __init__(self, data): super().__init__(data, 0x20) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.enemy_position_id = [read_u16(d, 0x18) for d in self] # EPOI reference self.item_position_id = [read_u16(d, 0x1A) for d in self] # IPOI reference # Respawn ID may not exist in very old versions; this read assumes it does. self.respawn_id = [read_u32(d, 0x1C) for d in self]
[docs] class KTP2(Section): """ KTP2 — Lap checkpoints (points to pass to count lap progress). Stride: 0x1C bytes per entry. Purpose ------- Defines the "lap gate" points that the engine checks to determine whether a player completed a lap. Usually combined with timing/ordering checks. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ktp2: :end-before: .. _nkm-table-ktpc: Gameplay Context ---------------- - The race logic queries these points as canonical lap markers. - Often unused fields are set to 0xFFFF; do not treat them as valid indices. Notes ----- - The "Index" is usually unused (set to 0xFFFF); if present, it may participate in specialized lap logic or developer tools. """ def __init__(self, data): super().__init__(data, 0x1C) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.padding = [read_u16(d, 0x18) for d in self] self.index = [read_u16(d, 0x1A) for d in self]
[docs] class KTPC(Section): """ KTPC — Cannon / Pipe destination points. Stride: 0x1C bytes per entry. Purpose ------- Describes destinations for cannons/pipes used in certain stages (mostly in battle stages). Cannon indices are used by specialized collision types. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ktpc: :end-before: .. _nkm-table-ktpm: Gameplay Context ---------------- - Cannon/pipe logic teleports an object/player to the KTPC destination. - The cannon_index can be used as a link between activator and destination. Notes ----- - In many tracks this section is small or absent; treat missing sections gracefully when writing tools that modify KTPC entries. """ def __init__(self, data): super().__init__(data, 0x1C) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.unknown = [read_u16(d, 0x18) for d in self] self.cannon_index = [read_u16(d, 0x1A) for d in self]
[docs] class KTPM(Section): """ KTPM — Mission points. Stride: 0x1C bytes per entry. Purpose ------- Points used by mission objectives (mission mode). They often resemble KTPS/KTP2 entries but have mission-specific indexing. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ktpm: :end-before: .. _nkm-table-cpoi: Gameplay Context ---------------- - Missions may use these points to define target positions or spawns. - `index` can be used in mission scripts to select a specific point out of the KTPM array. Notes ----- - If the track is not a mission type, these often default to 0xFFFF. """ def __init__(self, data): super().__init__(data, 0x1C) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.padding = [read_u16(d, 0x18) for d in self] self.index = [read_u16(d, 0x1A) for d in self]
[docs] class CPOI(Section): """ CPOI — Checkpoints (2D oriented). Stride: 0x24 bytes per entry. Purpose ------- Defines checkpoint segments used for lap counting, key handling, and respawn logic. CPOI includes two 2D positions and precomputed trig/distance values used by the engine to determine crossing and checkpoint ordering. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-cpoi: :end-before: .. _nkm-table-cpat: Gameplay Context ---------------- - CPOIs are the canonical gate the engine checks for lap progress. - The Key ID allows some checkpoints to behave as keys (for gates, or race logic). If Key ID == 0x0000 the point counts as the lap marker. - The precomputed sinus/cosinus/distance allow the engine to quickly test whether a player crossed the checkpoint along the correct orientation. Reverse-Engineering Notes ------------------------- - Community docs note the `section_data` fields are partially decoded for special track logic. If you need precise behavior, compare original tracks and observe in-engine behavior. """ def __init__(self, data): super().__init__(data, 0x24) self.position1 = [read_vector_2d_fx32(d, 0x00) for d in self] self.position2 = [read_vector_2d_fx32(d, 0x08) for d in self] self.sinus = [read_fx32(d, 0x10) for d in self] self.cosinus = [read_fx32(d, 0x14) for d in self] self.distance = [read_fx32(d, 0x18) for d in self] self.section_data1 = [read_u16(d, 0x1C) for d in self] self.section_data2 = [read_u16(d, 0x1E) for d in self] self.key_id = [read_u16(d, 0x20) for d in self] self.respawn_id = [read_u8(d, 0x22) for d in self] self.unknown = [read_u8(d, 0x23) for d in self]
[docs] class CPAT(Section): """ CPAT — CPOI grouping (checkpoint groups). Stride: 0x0C bytes per entry. Purpose ------- Groups CPOI entries into logical sequences (routes/sections). Each CPAT entry points to a contiguous block of CPOI points and describes adjacency (previous/next groups) and section order. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-cpat: :end-before: .. _nkm-table-ipoi: Gameplay Context ---------------- - CPAT allows complex checkpoint graphs (non-linear courses) by connecting multiple CPOI groups. - Useful when a single lap uses multiple discontiguous checkpoint regions. Implementation Notes -------------------- - The `next_group` and `prev_group` arrays use 0xFF as "unused"; treat sentinel values accordingly. """ def __init__(self, data): super().__init__(data, 0x0C) self.point_start = [read_u16(d, 0x00) for d in self] self.point_length = [read_u16(d, 0x02) for d in self] self.next_group = [ [read_u8(d, i) for i in range(0x04, 0x07, 0x01)] for d in self ] self.prev_group = [ [read_u8(d, i) for i in range(0x07, 0x0A, 0x01)] for d in self ] self.section_order = [read_s16(d, 0x0A) for d in self]
[docs] class IPOI(Section): """ IPOI — Item spawn points. Stride: 0x14 bytes per entry. Purpose ------- Describes where items (like red shells, bananas) may spawn or where items follow a path along a route. IPOI entries are often referenced by KTPJ (respawn) or object logic. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ipoi: :end-before: .. _nkm-table-ipat: Gameplay Context ---------------- - Items may spawn at IPOI locations or be used for scripted item routes. - `point_scale` may modify spawn probability or area radius. Notes ----- - The unknown 32-bit value is often zero; it may contain bitflags in certain custom tracks. """ def __init__(self, data): super().__init__(data, 0x14) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.point_scale = [read_fx32(d, 0x0C) for d in self] self.unknown = [read_u32(d, 0x10) for d in self]
[docs] class IPAT(Section): """ IPAT — IPOI grouping. Stride: 0x0C bytes per entry. Purpose ------- Group IPOI entries into contiguous point ranges and define adjacency, much like CPAT but for item points. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-ipat: :end-before: .. _nkm-table-epoi: Gameplay Context ---------------- - Used by item routing and by respawn selection to find nearby item points. """ def __init__(self, data): super().__init__(data, 0x0C) self.point_start = [read_u16(d, 0x00) for d in self] self.point_length = [read_u16(d, 0x02) for d in self] self.next_group = [ [read_u8(d, i) for i in range(0x04, 0x07, 0x01)] for d in self ] self.prev_group = [ [read_u8(d, i) for i in range(0x07, 0x0A, 0x01)] for d in self ] self.section_order = [read_s16(d, 0x0A) for d in self]
[docs] class EPOI(Section): """ EPOI — Enemy/CPU path points. Stride: 0x18 bytes per entry. Purpose ------- Defines points that the CPU opponents use for their routing (AI paths). These influence how CPUs drive the course (lines, drifting behavior, etc). .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-epoi: :end-before: .. _nkm-table-epat: Gameplay Context ---------------- - CPU behavior heavily depends on EPOI positions and the drifting parameter — altering these can change how tight/loose CPUs corner. - EPOI groups are linked via EPAT entries (below). Notes ----- - The meaning of the 0x14 32-bit word is partially unknown in community docs; experiments suggest it can contain flags for AI behavior. """ def __init__(self, data): super().__init__(data, 0x18) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.point_scale = [read_fx32(d, 0x0C) for d in self] self.drifting = [read_u16(d, 0x10) for d in self] self.unknown1 = [read_u16(d, 0x12) for d in self] self.unknown2 = [read_u32(d, 0x14) for d in self]
[docs] class EPAT(Section): """ EPAT — EPOI grouping. Stride: 0x0C bytes per entry. Purpose ------- Groups EPOI points into contiguous blocks and defines adjacency (next/prev groups) and section ordering. Used by CPU pathing code to navigate routes. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-epat: :end-before: .. _nkm-table-mepo: Gameplay Context ---------------- - EPAT partitions EPOI arrays into logical AI routes; enabling multiple CPU strategies per segment. """ def __init__(self, data): super().__init__(data, 0x0C) self.point_start = [read_u16(d, 0x00) for d in self] self.point_length = [read_u16(d, 0x02) for d in self] self.next_group = [ [read_u8(d, i) for i in range(0x04, 0x07, 0x01)] for d in self ] self.prev_group = [ [read_u8(d, i) for i in range(0x07, 0x0A, 0x01)] for d in self ] self.section_order = [read_s16(d, 0x0A) for d in self]
[docs] class MEPO(Section): """ MEPO — Mini-game enemy points. Stride: 0x18 bytes per entry. Purpose ------- Similar to EPOI but used by specific mini-games; describes where minigame entities spawn/move. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-mepo: :end-before: .. _nkm-table-mepa: Gameplay Context ---------------- - MEPO entries are used only in special mini-game contexts and may allow more variety (hence Int32 for drifting). """ def __init__(self, data): super().__init__(data, 0x18) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.point_scale = [read_fx32(d, 0x0C) for d in self] self.drifting = [read_u32(d, 0x10) for d in self] self.unknown = [read_u32(d, 0x14) for d in self]
[docs] class MEPA(Section): """ MEPA — MEPO grouping (for mini-games). Stride: 0x14 bytes per entry. Purpose ------- Groups MEPO points into sequences. MEPA entries support up to 8 next and 8 previous groups (Byte[8]) because mini-games may have richer topology. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-mepa: :end-before: .. _nkm-table-nkm: Gameplay Context ---------------- - Use MEPA to create complex mini-game movement graphs (multiple branching). """ def __init__(self, data): super().__init__(data, 0x14) self.point_start = [read_u16(d, 0x00) for d in self] self.point_length = [read_u16(d, 0x02) for d in self] self.next_group = [ [read_u8(d, i) for i in range(0x04, 0x0C, 0x01)] for d in self ] self.prev_group = [ [read_u8(d, i) for i in range(0x0C, 0x14, 0x01)] for d in self ]
[docs] class AREA(Section): """ AREA — Camera/zone areas. Stride: 0x48 bytes per entry. Purpose ------- Defines 3D regions used by the engine for camera selection and environmental triggers (sounds like waterfalls). Each area contains a center position, axes/length vectors (defining an oriented bounding box), and metadata such as area type and camera ID. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-area: :end-before: .. _nkm-table-came: Gameplay Context ---------------- - AREA sections usually determine which camera the engine should switch to when the player is inside the region. - Camera ID links to CAME entries; Area type allows special behavior (e.g., waterfall sound triggers). - Good for editors to preview camera transitions. Notes ----- - Several fields are still undocumented; their meaning varies by track. """ def __init__(self, data): super().__init__(data, 0x48) self.position = [read_vector_3d_fx32(d, 0x00) for d in self] self.length_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.x_vec = [read_vector_3d_fx32(d, 0x18) for d in self] self.y_vec = [read_vector_3d_fx32(d, 0x24) for d in self] self.z_vec = [read_vector_3d_fx32(d, 0x30) for d in self] self.unknown1 = [read_u16(d, 0x3C) for d in self] self.unknown2 = [read_u16(d, 0x3E) for d in self] self.unknown3 = [read_u16(d, 0x40) for d in self] self.unknown4 = [read_u8(d, 0x42) for d in self] self.camera_id = [read_u8(d, 0x43) for d in self] self.area_type = None # TODO: parse as Byte at 0x44 if desired self.unknown5 = [read_s16(d, 0x45) for d in self] self.unknown6 = [read_u8(d, 0x47) for d in self]
[docs] class CAME(Section): """ CAME — Camera definitions. Stride: 0x4C bytes per entry. Purpose ------- Defines camera motions and static camera positions used in cutscenes, intros, and dynamic in-game camera triggers. Camera entries are often linked by AREA zones or object routes. .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-came: :end-before: .. _nkm-table-came-again: .. include:: /_includes/nkm_tables.rst :start-after: .. _nkm-table-came-again: :end-before: .. _nkm-table-end: Gameplay Context ---------------- - CAME entries drive cinematic camera movement during intros, cutscenes, and special camera modes. - Linked route + point speed define how the camera follows a PATH. - Camera duration uses 1/60th-second units — helpful for exact timing. Notes & Community Tips ---------------------- - Many of these fields are precomputed (sine/cosine) for faster in-engine interpolation. Editors can either preserve or recompute them. - The "next camera" field allows building camera chains for sequences. - When building camera editors, expose both positions and FOV values to enable previewing transitions correctly. """ def __init__(self, data): super().__init__(data, 0x4C) self.position1 = [read_vector_3d_fx32(d, 0x00) for d in self] self.rot_vec = [read_vector_3d_fx32(d, 0x0C) for d in self] self.position2 = [read_vector_3d_fx32(d, 0x18) for d in self] self.position3 = [read_vector_3d_fx32(d, 0x24) for d in self] self.fov_begin = [read_u16(d, 0x30) for d in self] self.fov_begin_sine = [read_fx16(d, 0x32) for d in self] self.fov_begin_cosine = [read_fx16(d, 0x34) for d in self] self.fov_end = [read_u16(d, 0x36) for d in self] self.fov_end_sine = [read_fx16(d, 0x38) for d in self] self.fov_end_cosine = [read_fx16(d, 0x3A) for d in self] self.camera_zoom = [read_u16(d, 0x3C) for d in self] self.camera_type = None # TODO: parse if you want the raw value at 0x3E self.linked_route = [read_u16(d, 0x40) for d in self] self.route_speed = [read_u16(d, 0x42) for d in self] self.point_speed = [read_u16(d, 0x44) for d in self] self.camera_duration = [read_u16(d, 0x46) for d in self] self.next_camera = [read_u16(d, 0x48) for d in self] self.intro_pan_first_camera_indicator = [read_u8(d, 0x4A) for d in self] self.unknown = [read_u8(d, 0x4B) for d in self]
[docs] class NKM: """ NKM — Mario Kart DS Course Map parser. Purpose ------- Top-level container that parses the NKM file header and initializes objects representing each section (OBJI, PATH, POIT, STAG, KTPS, ... , CAME). Usage Example ---------------- >>> nkm = NKM.from_file("my_course.nkm") >>> print(len(nkm._OBJI)) # number of object instances >>> print(nkm._STAG.amt_of_laps) # global lap count for the stage Implementation Details ---------------------- * The typical header length is 0x4C and contains offsets (UInt32) to 17 canonical sections in standard order. Offsets are relative to the end of the header (H), so they are added to `_header_offset` to compute absolute positions inside the file. * This parser assumes the "typical" header layout. If an NKM contains additional special sections (like NKMI) or a different header length, additional handling will be required. * Parsing is defensive but assumes the file is well-formed; for robust tools consider adding bounds checks when slicing `self._data[...]`. Known Limitations ----------------- - Some fields (GXRgb, camera_type, some unknown bytes) are left as None or unknown placeholders. Implementing GXRgb and Fx16/Fx32 conversions will enable richer output. - The code currently uses `PATH.has_loop = read_u8(...) != 1` which inverts the canonical meaning (spec: 1 means loop). Consider normalizing this if you rely on literal interpretation elsewhere. See Also -------- The official NKM spec (community wiki) for exhaustive explanation of flags and object-specific settings. """ _header_offset = 0x4C def __init__(self, data): self._data = data self._h = 0x4C # Header contains offsets relative to header_end; add header_offset self._OBJI_offset = read_u32(data, 0x08) + NKM._header_offset self._PATH_offset = read_u32(data, 0x0C) + NKM._header_offset self._POIT_offset = read_u32(data, 0x10) + NKM._header_offset self._STAG_offset = read_u32(data, 0x14) + NKM._header_offset self._KTPS_offset = read_u32(data, 0x18) + NKM._header_offset self._KTPJ_offset = read_u32(data, 0x1C) + NKM._header_offset self._KTP2_offset = read_u32(data, 0x20) + NKM._header_offset self._KTPC_offset = read_u32(data, 0x24) + NKM._header_offset self._KTPM_offset = read_u32(data, 0x28) + NKM._header_offset self._CPOI_offset = read_u32(data, 0x2C) + NKM._header_offset self._CPAT_offset = read_u32(data, 0x30) + NKM._header_offset self._IPOI_offset = read_u32(data, 0x34) + NKM._header_offset self._IPAT_offset = read_u32(data, 0x38) + NKM._header_offset self._EPOI_offset = read_u32(data, 0x3C) + NKM._header_offset self._EPAT_offset = read_u32(data, 0x40) + NKM._header_offset self._AREA_offset = read_u32(data, 0x44) + NKM._header_offset self._CAME_offset = read_u32(data, 0x48) + NKM._header_offset # Instantiate section objects. Slicing uses computed offsets. self._OBJI = OBJI(self._data[self._OBJI_offset:self._PATH_offset]) self._PATH = PATH(self._data[self._PATH_offset:self._POIT_offset]) self._POIT = POIT(self._data[self._POIT_offset:self._STAG_offset]) self._STAG = STAG(self._data[self._STAG_offset:self._KTPS_offset]) self._KTPS = KTPS(self._data[self._KTPS_offset:self._KTPJ_offset]) self._KTPJ = KTPJ(self._data[self._KTPJ_offset:self._KTP2_offset]) self._KTP2 = KTP2(self._data[self._KTP2_offset:self._KTPC_offset]) self._KTPC = KTPC(self._data[self._KTPC_offset:self._KTPM_offset]) self._KTPM = KTPM(self._data[self._KTPM_offset:self._CPOI_offset]) self._CPOI = CPOI(self._data[self._CPOI_offset:self._CPAT_offset]) self._CPAT = CPAT(self._data[self._CPAT_offset:self._IPOI_offset]) self._IPOI = IPOI(self._data[self._IPOI_offset:self._IPAT_offset]) self._IPAT = IPAT(self._data[self._IPAT_offset:self._EPOI_offset]) self._EPOI = EPOI(self._data[self._EPOI_offset:self._EPAT_offset]) self._EPAT = EPAT(self._data[self._EPAT_offset:self._AREA_offset]) self._AREA = AREA(self._data[self._AREA_offset:self._CAME_offset]) self._CAME = CAME(self._data[self._CAME_offset:])
[docs] @classmethod def from_file(cls, path: str, **kwargs): """ Load an NKM file from disk and parse its sections. Args: path (str): Path to the `.nkm` file. Defaults to DEFAULT_NKM_PATH. Returns: NKM: The parsed NKM object. """ import os path = os.path.abspath(path) if not os.path.exists(path): raise FileNotFoundError(f"NKM file not found at {path}") with open(path, 'rb') as f: return cls(f.read(), **kwargs)