Source code for mkds.kcl

from .utils import (
    read_u16,
    read_vector_3d_fx16,
    read_vector_3d_fx32,
    read_u32,
    read_fx32,
    read_s32,
    slice_bits
)
from typing import Sequence, Self


[docs] class PrismsBase: """ Represents the triangular prisms section of the KCL file. .. include:: /_includes/kcl_tables.rst :start-after: .. _kcl-table-prisms: :end-before: .. _kcl-table: Attributes ---------- _height : list[float] Prism heights _pos_i : list[int] Vertex indices _fnrm_i : list[int] Face normal indices _enrm1_i : list[int] Edge normal 1 indices _enrm2_i : list[int] Edge normal 2 indices _enrm3_i : list[int] Edge normal 3 indices _attributes : list[int] Collision attribute flags """ def __init__(self, _height: Sequence[float], _pos_i: Sequence[int], _fnrm_i: Sequence[int], _enrm1_i: Sequence[int], _enrm2_i: Sequence[int], _enrm3_i: Sequence[int], _attributes: Sequence[Sequence[int]] ): self.height = _height self.pos_i = _pos_i self.fnrm_i = _fnrm_i self.enrm1_i = _enrm1_i self.enrm2_i = _enrm2_i self.enrm3_i = _enrm3_i self.attributes = _attributes
[docs] @classmethod def from_bytes(cls, data: bytes) -> Self: iter = range(0, len(data), 0x10) return cls( [read_fx32(data, i + 0x00) for i in iter], [read_u16(data, i + 0x04) for i in iter], [read_u16(data, i + 0x06) for i in iter], [read_u16(data, i + 0x08) for i in iter], [read_u16(data, i + 0x0A) for i in iter], [read_u16(data, i + 0x0C) for i in iter], [PrismsBase.parse_attributes(read_u16(data, i + 0x0E)) for i in iter] )
[docs] @staticmethod def parse_attributes(bits: int) -> list[int]: return [ slice_bits(bits, 0, 1), # map 2d shadow slice_bits(bits, 1, 4), # light id (bit 1-3) slice_bits(bits, 4, 5), # ignore drivers slice_bits(bits, 5, 8), # collision variant slice_bits(bits, 8, 13), # collision type slice_bits(bits, 13, 14), # ignore items slice_bits(bits, 14, 15), # is wall slice_bits(bits, 15, 16) # is floor ]
[docs] class KCLBase: """ Represents a KCL (collision) file. KCL files store simplified model data for collision detection in games such as Mario Kart Wii / DS. They consist of a header, positions, normals, triangular prisms, and octree blocks. .. include:: /_includes/kcl_tables.rst :start-after: .. _kcl-table: :end-before: .. _kcl-end: Attributes ---------- _height : list[float] Prism heights _pos_i : list[int] Vertex indices _fnrm_i : list[int] Face normal indices _enrm1_i : list[int] Edge normal 1 indices _enrm2_i : list[int] Edge normal 2 indices _enrm3_i : list[int] Edge normal 3 indices _attributes : list[int] Collision attribute flags """ prism_cls = PrismsBase def __init__(self, data: bytes, prisms: PrismsBase, positions: Sequence[Sequence[float]], normals: Sequence[Sequence[float]], _positions_offset: int, _normals_offset: int, _prisms_offset: int, _block_data_offset: int, _prism_thickness: float, _area_min_pos: tuple[float, float, float], _area_x_width_mask: int, _area_y_width_mask: int, _area_z_width_mask: int, _block_width_shift: int, _area_x_blocks_shift: int, _area_xy_blocks_shift: int, _sphere_radius: int | None, ): self.data = data self.prisms = prisms self.positions = positions self.normals = normals self.positions_offset = _positions_offset self.normals_offset = _normals_offset self.prisms_offset = _prisms_offset self.block_data_offset = _block_data_offset self.prism_thickness = _prism_thickness self.area_min_pos = _area_min_pos self.area_x_width_mask = _area_x_width_mask self.area_y_width_mask = _area_y_width_mask self.area_z_width_mask = _area_z_width_mask self.block_width_shift = _block_width_shift self.area_x_blocks_shift = _area_x_blocks_shift self.area_xy_blocks_shift = _area_xy_blocks_shift self.sphere_radius = _sphere_radius
[docs] @classmethod def from_bytes(cls, data: bytes, **kwargs) -> Self: _positions_offset = read_u32(data, 0x00) _normals_offset = read_u32(data, 0x04) _prisms_offset = read_u32(data, 0x08) _block_data_offset = read_u32(data, 0x0C) _prism_thickness = read_fx32(data, 0x10) _area_min_pos = read_vector_3d_fx32(data, 0x14) _area_x_width_mask = read_u32(data, 0x20) _area_y_width_mask = read_u32(data, 0x24) _area_z_width_mask = read_u32(data, 0x28) _block_width_shift = read_u32(data, 0x2C) _area_x_blocks_shift = read_u32(data, 0x30) _area_xy_blocks_shift = read_u32(data, 0x34) _sphere_radius = None#read_f32(data, 0x38) prisms = PrismsBase.from_bytes(data[_prisms_offset+0x10:_block_data_offset]) positions = KCLBase._parse_positions(data, prisms, _positions_offset) normals = KCLBase._parse_normals(data, prisms, _normals_offset) prisms = cls.prism_cls( prisms.height, prisms.pos_i, prisms.fnrm_i, prisms.enrm1_i, prisms.enrm2_i, prisms.enrm3_i, prisms.attributes, **kwargs ) return cls( data, prisms, positions, normals, _positions_offset, _normals_offset, _prisms_offset, _block_data_offset, _prism_thickness, _area_min_pos, _area_x_width_mask, _area_y_width_mask, _area_z_width_mask, _block_width_shift, _area_x_blocks_shift, _area_xy_blocks_shift, _sphere_radius, **kwargs )
@staticmethod def _parse_positions(data: bytes, prisms: PrismsBase, positions_offset: int): """ Parse position vectors from the file. Each position vector consists of 3 consecutive floats (X, Y, Z). The number of positions is determined from the prisms indices. Returns ------- list List of 3D vectors (tuples of floats) """ position_size = 0x0C start = positions_offset section_size = max(prisms.pos_i) end = (section_size + 1) * position_size + start positions = [] for offset in range(start, end, position_size): positions.append(read_vector_3d_fx32(data, offset)) return positions @staticmethod def _parse_normals(data: bytes, prisms: PrismsBase, normals_offset: int): """ Parse normal vectors from the file. Each normal vector consists of 3 consecutive floats (X, Y, Z). The number of normals is determined from all prism normal indices. Returns ------- list List of 3D normal vectors (tuples of floats) """ normal_size = 0x06 start = normals_offset section_size = max([ *prisms.fnrm_i, *prisms.enrm1_i, *prisms.enrm2_i, *prisms.enrm3_i, ]) end = (section_size + 1) * normal_size + start normals = [] for offset in range(start, end, normal_size): normals.append(read_vector_3d_fx16(data, offset)) return normals
[docs] def search_block(self, point: tuple[float, float, float] | list[float, float, float]): """ Return the offset of the leaf node containing a queried point """ block_start = self.block_data_offset block_data = self.data[block_start:] px, py, pz = point minx, miny, minz = self.area_min_pos x = int(px - minx) if (x & self.area_x_width_mask) != 0: return None y = int(py - miny) if (y & self.area_y_width_mask) != 0: return None z = int(pz - minz) if (z & self.area_z_width_mask) != 0: return None # initialize root shift = self.block_width_shift cur_block_offset = 0 # root at start of block_data index = 4 * (((z >> shift) << self.area_xy_blocks_shift) | ((y >> shift) << self.area_x_blocks_shift) | (x >> shift)) while True: offset = read_u32(block_data, cur_block_offset + index) if (offset & 0x80000000) != 0: # negative flag = leaf node break shift -= 1 cur_block_offset += offset # initialize next index index = 4 * (((z >> shift) & 1) << 2 | ((y >> shift) & 1) << 1 | ((x >> shift) & 1)) # leaf = return pointer into block_data (as slice) leaf_offset = cur_block_offset + (offset & 0x7FFFFFFF) return leaf_offset
[docs] class Prisms(PrismsBase): """ Represents the triangular prisms section of the KCL file. .. include:: /_includes/kcl_tables.rst :start-after: .. _kcl-table-prisms: :end-before: .. _kcl-table: Attributes ---------- _height : list[float] Prism heights _pos_i : list[int] Vertex indices _fnrm_i : list[int] Face normal indices _enrm1_i : list[int] Edge normal 1 indices _enrm2_i : list[int] Edge normal 2 indices _enrm3_i : list[int] Edge normal 3 indices _attributes : list[int] Collision attribute flags """ def __init__(self, _height: list[float], _pos_i: list[int], _fnrm_i: list[int], _enrm1_i: list[int], _enrm2_i: list[int], _enrm3_i: list[int], _attributes: list[list[int]] ): super().__init__( _height, _pos_i, _fnrm_i, _enrm1_i, _enrm2_i, _enrm3_i, _attributes ) self.height = _height self.pos_i = _pos_i self.fnrm_i = _fnrm_i self.enrm1_i = _enrm1_i self.enrm2_i = _enrm2_i self.enrm3_i = _enrm3_i self.attributes = _attributes def __getitem__(self, idx): """ Return all attributes of the prism at index ``idx``. """ if idx >= len(self): raise IndexError("Index out of range") return [arr[idx] for arr in self.__dict__.values()] def __len__(self): """ Return the number of prisms in the section. """ return len(self.pos_i) def __iter__(self): """ Iterate over all prisms, yielding full attribute lists. """ for i in range(len(self)): yield self[i // 0x10]
[docs] class KCL(KCLBase): """ Represents a KCL (collision) file. KCL files store simplified model data for collision detection in games such as Mario Kart Wii / DS. They consist of a header, positions, normals, triangular prisms, and octree blocks. .. include:: /_includes/kcl_tables.rst :start-after: .. _kcl-table: :end-before: .. _kcl-end: Attributes ---------- _positions_offset : int File offset to position vectors _normals_offset : int File offset to normal vectors _prisms_offset : int File offset to prism data _block_data_offset : int File offset to octree blocks _prism_thickness : float Depth of each prism _area_min_pos : list[float] Minimum coordinates of the collision area _area_x_width_mask : int X-axis mask for octree _area_y_width_mask : int Y-axis mask for octree _area_z_width_mask : int Z-axis mask for octree _block_width_shift : int Octree leaf size shift _area_x_blocks_shift : int Root block child index shift (Y) _area_xy_blocks_shift : int Root block child index shift (Z) _sphere_radius : float or None Optional maximum sphere radius for collisions _prisms : Prisms Parsed prism objects _positions : list List of vertex positions _normals : list List of normal vectors """ prism_cls = Prisms def __init__( self, data: bytes, prisms: Prisms, positions: list[list[float]], normals: list[list[float]], _positions_offset: int, _normals_offset: int, _prisms_offset: int, _block_data_offset: int, _prism_thickness: float, _area_min_pos: tuple[float, float, float], _area_x_width_mask: int, _area_y_width_mask: int, _area_z_width_mask: int, _block_width_shift: int, _area_x_blocks_shift: int, _area_xy_blocks_shift: int, _sphere_radius: int | None, ): super().__init__( data, prisms, positions, normals, _positions_offset, _normals_offset, _prisms_offset, _block_data_offset, _prism_thickness, _area_min_pos, _area_x_width_mask, _area_y_width_mask, _area_z_width_mask, _block_width_shift, _area_x_blocks_shift, _area_xy_blocks_shift, _sphere_radius ) self.data = data self.prisms = prisms self.positions = positions self.normals = normals self.positions_offset = _positions_offset self.normals_offset = _normals_offset self.prisms_offset = _prisms_offset self.block_data_offset = _block_data_offset self.prism_thickness = _prism_thickness self.area_min_pos = _area_min_pos self.area_x_width_mask = _area_x_width_mask self.area_y_width_mask = _area_y_width_mask self.area_z_width_mask = _area_z_width_mask self.block_width_shift = _block_width_shift self.area_x_blocks_shift = _area_x_blocks_shift self.area_xy_blocks_shift = _area_xy_blocks_shift self.sphere_radius = _sphere_radius def __str__(self): """ Returns a human-readable summary of the KCL file. Output includes: - Number of positions and a preview of first and last vectors - Number of normals and a preview of first and last vectors - Number of prisms """ def str_vec(l): return f""" {l[0]} ... {len(l)} vectors ... {l[-1]} """ return f""" Positions: {str_vec(self.positions) if len(self.positions) != 0 else "No entries"} Normals: {str_vec(self.normals) if len(self.normals) != 0 else "No entries"} Prisms: {len(self.prisms) if len(self.prisms) != 0 else "No entries"}\n """
[docs] @classmethod def from_file(cls, path: str): data = None with open(path, 'rb') as f: data = f.read() assert data is not None return cls.from_bytes(data)