Kit Structure Documentation - OpenKotOR/PyKotor GitHub Wiki

Kit structure Documentation

Kits are collections of reusable indoor map components for the Holocron Toolset. They contain room models, textures, lightmaps, doors, and other resources that can be assembled into complete game modules.

Table of Contents


Kit Overview

A kit is a self-contained collection of resources that can be used to build indoor maps. Kits are stored in Tools/HolocronToolset/src/toolset/kits/kits/ and consist of:

  • Components: Room models (MDL/MDX/WOK) with hook points for connecting to other rooms
  • textures: TGA texture files with optional TXI metadata
  • Lightmaps: TGA lightmap files with optional TXI metadata
  • Doors: UTD door templates (K1 and K2 versions) with DWK walkmeshes
  • Placeables: UTP placeable templates with PWK walkmeshes (optional)
  • Skyboxes: Optional MDL/MDX models for sky rendering
  • Always Resources: Static resources included in every generated module
  • models: Additional MDL/MDX models referenced by the module but not used as components

This layout mirrors the Holocron Toolset indoor-kit data model and loader package structure (toolset/data/indoorkit/).


Kit Directory structure

kits/
β”œβ”€β”€ {kit_id}/
β”‚   β”œβ”€β”€ {kit_id}.json          # Kit definition file
β”‚   β”œβ”€β”€ {component_id}.mdl     # Component model files
β”‚   β”œβ”€β”€ {component_id}.mdx
β”‚   β”œβ”€β”€ {component_id}.wok     # Component walkmesh (re-centered to origin)
β”‚   β”œβ”€β”€ {component_id}.png     # Component minimap image (top-down view)
β”‚   β”œβ”€β”€ textures/              # Texture files
β”‚   β”‚   β”œβ”€β”€ {texture_name}.tga
β”‚   β”‚   └── {texture_name}.txi
β”‚   β”œβ”€β”€ lightmaps/             # Lightmap files
β”‚   β”‚   β”œβ”€β”€ {lightmap_name}.tga
β”‚   β”‚   └── {lightmap_name}.txi
β”‚   β”œβ”€β”€ always/                # Always-included resources (optional)
β”‚   β”‚   └── {resource_name}.{ext}
β”‚   β”œβ”€β”€ skyboxes/              # Skybox models (optional)
β”‚   β”‚   β”œβ”€β”€ {skybox_name}.mdl
β”‚   β”‚   └── {skybox_name}.mdx
β”‚   β”œβ”€β”€ doorway/               # Door padding models (optional)
β”‚   β”‚   β”œβ”€β”€ {side|top}_{door_id}_size{size}.mdl
β”‚   β”‚   └── {side|top}_{door_id}_size{size}.mdx
β”‚   β”œβ”€β”€ models/                # Additional models (optional)
β”‚   β”‚   β”œβ”€β”€ {model_name}.mdl
β”‚   β”‚   └── {model_name}.mdx
β”‚   β”œβ”€β”€ {door_name}_k1.utd     # Door templates
β”‚   β”œβ”€β”€ {door_name}_k2.utd
β”‚   β”œβ”€β”€ {door_model}0.dwk      # Door walkmeshes (3 states: 0=closed, 1=open1, 2=open2)
β”‚   β”œβ”€β”€ {door_model}1.dwk
β”‚   β”œβ”€β”€ {door_model}2.dwk
β”‚   └── {placeable_model}.pwk  # Placeable walkmeshes (optional)

Example: jedienclave/ contains textures and lightmaps but no components (texture-only kit). enclavesurface/ contains full components with models, walkmeshes, and minimaps.


Kit JSON file

The kit JSON file ({kit_id}.json) defines the kit structure:

{
    "name": "Kit Display Name",
    "id": "kitid",
    "ht": "2.0.2",
    "version": 1,
    "components": [
        {
            "name": "Component Name",
            "id": "component_id",
            "native": 1,
            "doorhooks": [
                {
                    "x": 0.0,
                    "y": 0.0,
                    "z": 0.0,
                    "rotation": 90.0,
                    "door": 0,
                    "edge": 20
                }
            ]
        }
    ],
    "doors": [
        {
            "utd_k1": "door0_k1",
            "utd_k2": "door0_k2",
            "width": 2.0,
            "height": 3.0
        }
    ]
}

fields:

  • name: Display name for the kit
  • id: Unique kit identifier (matches folder name, must be lowercase, sanitized)
  • ht: Holocron Toolset version compatibility string
  • version: Kit version number (integer)
  • components: List of room components (can be empty for texture-only kits)
  • doors: List of door definitions

Component fields:

  • name: Display name for the component
  • id: Unique component identifier (matches MDL/WOK filename without extension)
  • native: Always 1 (legacy field, indicates native format)
  • doorhooks: List of door hook points extracted from BWM edges with transitions

Door Hook fields:

  • x, y, z: World-space position of the hook point (midpoint of BWM edge with transition)
  • rotation: rotation angle in degrees (0-360), calculated from edge direction in XY plane
  • door: index into the kit's doors array (mapped from BWM edge transition index)
  • edge: Global edge index in the BWM (face_index * 3 + local_edge_index)

Door fields:

  • utd_k1: ResRef of K1 door UTD file (without .utd extension)
  • utd_k2: ResRef of K2 door UTD file (without .utd extension)
  • width: Door width in world units (default: 2.0)
  • height: Door height in world units (default: 3.0)

All of these JSON fields are loaded and validated directly by the indoor-kit loader implementation (indoorkit_loader.py L23-L260).


Components

Components are reusable room models that can be placed and connected to build indoor maps. They are identified during kit extraction as MDL files that:

  1. Are listed as room models in the module's LYT file
  2. Have corresponding WOK (walkmesh) files
  3. Are not skyboxes (skyboxes have MDL/MDX but no WOK)

Component files:

  • {component_id}.mdl: 3D model geometry (BioWare MDL format)
  • {component_id}.mdx: material/lightmap index data (BioWare MDX format)
  • {component_id}.wok: walkmesh for pathfinding (BioWare BWM format, re-centered to origin)
  • {component_id}.png: Minimap image (top-down view of walkmesh, generated from BWM)

Component Identification Process:

  1. Load module LYT file to get list of room model names
  2. For each room model, resolve MDL/MDX/WOK using installation-wide resource resolution
  3. Components are room models that have both MDL and WOK files
  4. Component IDs are mapped from model names using _get_component_name_mapping() to create friendly names

Component JSON structure:

{
    "name": "Hall 1",
    "id": "hall_1",
    "native": 1,
    "doorhooks": [
        {
            "x": -4.476789474487305,
            "y": -17.959964752197266,
            "z": 3.8257503509521484,
            "rotation": 90.0,
            "door": 0,
            "edge": 25
        }
    ]
}

BWM Re-centering: Component WOK files are re-centered to origin (0, 0, 0) before saving. This is critical because:

  • The Indoor Map Builder draws preview images centered at room.position
  • The walkmesh is translated BY room.position from its original coordinates
  • For alignment, the BWM must be centered around (0, 0) so both image and walkmesh are at the same position after transformation

This re-centering behavior is implemented in the kit extraction flow that translates component walkmeshes and emits normalized component outputs (kit.py L1538-L1588).

Door Hooks:

  • x, y, z: World-space position of the hook point (midpoint of BWM edge with transition)
  • rotation: rotation angle in degrees (0-360), calculated from edge direction using atan2(dy, dx)
  • door: index into the kit's doors array (mapped from BWM edge transition index, clamped to valid range)
  • edge: Global edge index in the BWM (used for door placement during map generation)

Hook Extraction: Door hooks are extracted from BWM edges that have valid transitions (edge.transition >= 0). The transition index maps to the door index in the kit's doors array.

This edge-to-hook extraction and transition mapping logic is implemented in the same kit extraction path (kit.py L1467-L1535).

Hook Connection Logic: Components are connected when their hook points are within proximity. The toolset automatically links compatible hooks to form room connections.

Connection evaluation behavior is defined by the indoor-kit base model's hook compatibility logic (indoorkit_base.py L88-L106).


textures and Lightmaps

Kits contain all textures and lightmaps referenced by their component models, plus any additional shared resources found in the module or installation.

texture Extraction

textures are extracted from multiple sources using the game engine's resource resolution priority:

  1. Module RIM/ERF files: textures directly in the module archives
  2. Installation-wide Resolution: textures from:
    • override/ (user mods, highest priority)
    • modules/ (module-specific overrides, .mod files take precedence over .rim files)
    • textures_gui/ (GUI textures)
    • texturepacks/ (TPA/ERF texture packs)
    • chitin/ (base game BIF files, lowest priority)

texture Identification:

  • textures are identified by scanning all MDL files in the module using iterate_textures()
  • This extracts texture references from MDL material definitions
  • All models from module.models() are scanned, including those loaded from CHITIN

texture Naming:

  • textures: Standard names (e.g., lda_wall02, i_datapad)
  • Lightmaps: Suffixed with _lm or prefixed with l_ (e.g., m13aa_01a_lm0, l_sky01)

TPC to TGA Conversion: All textures are converted from TPC (BioWare's texture format) to TGA (Truevision Targa) format during extraction. The conversion process:

  1. Reads TPC file data
  2. Parses TPC structure (mipmaps, format, embedded TXI)
  3. Converts mipmaps to RGBA format if needed
  4. Writes TGA file with BGRA pixel order (TGA format requirement)

This conversion path is implemented directly in the kit extraction texture pipeline (kit.py L926-L948).

Resource Resolution Priority

The extraction process uses the same resource resolution priority as the game engine:

  1. OVERRIDE (priority 0 - highest): User mods in override/ folder
  2. MODULES (priority 1-2): Module files
    • .mod files (priority 1) - take precedence over .rim files
    • .rim and _s.rim files (priority 2)
  3. TEXTURES_GUI (priority 3): GUI textures
  4. TEXTURES_TPA (priority 4): texture packs
  5. CHITIN (priority 5 - lowest): Base game BIF files

The exact priority ordering used here comes from the resource-location ranking logic in the kit tooling (kit.py L74-L120).

Batch Processing: texture/lightmap lookups are batched for performance:

  • Single installation.locations() call for all textures/lightmaps
  • Results are pre-sorted by priority once to avoid repeated sorting
  • TPC/TGA files are grouped by filepath for batch I/O operations

This batching behavior is implemented in the texture/lightmap lookup and extraction routines used by kit generation (kit.py L814-L964).

TXI files

Each texture/lightmap can have an accompanying .txi file containing texture metadata (filtering, wrapping, etc.). TXI files are extracted from:

  1. Embedded TXI in TPC files: TPC files can contain embedded TXI data
  2. Standalone TXI files: TXI files in the installation (same resolution priority as textures)
  3. Empty TXI placeholders: If no TXI is found, an empty TXI file is created to match expected kit structure

TXI Extraction Process:

  1. Check for embedded TXI in TPC file during conversion
  2. If not found, lookup standalone TXI file using batch location results
  3. If still not found, create empty TXI placeholder

The embedded-first then standalone fallback flow is implemented in the same extraction block that writes texture assets for kits (kit.py L849-L1020).

Shared Resources

Some kits include textures/lightmaps from other modules that are not directly referenced by the kit's own models. These are typically:

  • Shared Lightmaps: Lightmaps from other modules stored in lightmaps3.bif (CHITIN) that may be used by multiple areas
    • Example: m03af_01a_lm13, m10aa_01a_lm13, m14ab_02a_lm13 from jedienclave kit
    • These are found in data/lightmaps3.bif as shared resources across multiple modules
    • They may be referenced by other modules that share resources with the kit's source module
  • Common textures: textures from swpc_tex_tpa.erf (texture packs) used across multiple modules
    • Example: lda_* textures (lda_bark04, lda_flr07, etc.) from texture packs
    • These are shared resources that may be used by multiple areas
  • Module Resources: textures/lightmaps found in the module's resource list but not directly referenced by models
    • Some resources may be included even if not directly referenced
    • These ensure the kit is self-contained and doesn't depend on external resources

Why Include Shared Resources?:

  • Self-Containment: Ensures the kit has all resources it might need
  • Compatibility: Some resources may be referenced indirectly or by other systems
  • Convenience: Manually curated collections of commonly used resources
  • Future-Proofing: Resources that might be needed when the kit is used in different contexts

Investigation using Installation.locations() shows these resources are found in the following locations:

  • data/lightmaps3.bif (CHITIN) for shared lightmaps
  • texturepacks/swpc_tex_tpa.erf (TEXTURES_TPA) for common textures

This has been verified via Installation.locations() lookups that resolve shared lightmaps to CHITIN BIFs and common kit textures to TPA texturepacks under the standard priority model.


Always Folder

The always/ folder contains resources that are always included in the generated module, regardless of which components are used.

Purpose:

  • Static Resources: Resources that should be included in every generated module using the kit
  • Common Assets: Shared textures, models, or other resources needed by all rooms
  • Override Resources: Resources that override base game files and should be included in every room
  • Non-Component Resources: Resources that don't belong to specific components but are needed for the kit to function

Usage: When a kit is used to generate a module, all files in the always/ folder are automatically added to the mod's resource list via add_static_resources(). These resources are included in every room, even if they're not directly referenced by component models.

Processing: Resources in the always/ folder are processed during indoor map generation:

  1. Each file in always/ is loaded into kit.always[filename] during kit loading
  2. When a room is processed, add_static_resources() extracts the resource name and type from the filename
  3. The resource is added to the mod with mod.set_data(resname, restype, data)
  4. This happens for every room, ensuring the resource is always available

Example: The sithbase kit includes:

  • CM_asith.tpc: Common texture used across all rooms
  • lsi_floor01b.tpc, lsi_flr03b.tpc, lsi_flr04b.tpc: Floor textures for all rooms
  • lsi_win01bmp.tpc: Window texture used throughout the base

These are added to every room when using the sithbase kit, ensuring consistent appearance across all generated rooms.

When to Use:

  • Resources that should be available in every room (e.g., common floor textures)
  • Override resources that replace base game files
  • Resources needed for kit functionality but not tied to specific components
  • Shared assets that multiple components might reference

Always-folder resource injection is handled by the indoor map generation path that calls add_static_resources() during room processing (indoormap.py L236-L256).


Doors

Doors are defined in the kit JSON and have corresponding UTD files. Doors connect adjacent rooms at component hook points.

Door files:

  • {door_name}_k1.utd: KotOR 1 door template (UTD format)
  • {door_name}_k2.utd: KotOR 2 door template (UTD format)
  • {door_model}0.dwk: Door walkmesh for closed state
  • {door_model}1.dwk: Door walkmesh for open1 state
  • {door_model}2.dwk: Door walkmesh for open2 state

Door JSON structure:

{
    "utd_k1": "door0_k1",
    "utd_k2": "door0_k2",
    "width": 2.0,
    "height": 3.0
}

Door Properties:

  • utd_k1, utd_k2: ResRefs of UTD files (without .utd extension)
  • width: Door width in world units (default: 2.0)
  • height: Door height in world units (default: 3.0)

Door Extraction Process:

  1. UTD files are extracted from module RIM/ERF archives
  2. Door model names are resolved from UTD files using genericdoors.2da
  3. DWK walkmeshes are extracted for each door model (3 states: 0=closed, 1=open1, 2=open2)
  4. Door dimensions are set to fast defaults (2.0x3.0) to avoid expensive extraction

This door extraction path is implemented in the kit builder's door processing routine (kit.py L1253-L1304).

Doors are placed at component hook points and connect adjacent rooms. The door templates define appearance, locking, scripts, and other properties.

Door template semantics are documented in the UTD section of the GFF format documentation (GFF UTD).

Door Walkmeshes (DWK)

Doors have 3 walkmesh states that define pathfinding behavior:

  • State 0 (closed): {door_model}0.dwk - Door is closed, blocks pathfinding
  • State 1 (open1): {door_model}1.dwk - Door is open in first direction
  • State 2 (open2): {door_model}2.dwk - Door is open in second direction

DWK Extraction Process:

  1. Load genericdoors.2da to map UTD files to door model names
  2. For each door, resolve model name from UTD using door_tools.get_model()
  3. Batch lookup all DWK files (3 states per door) using installation.locations()
  4. Extract DWK files from module first (fastest), then fall back to installation-wide resolution

This DWK lookup and extraction sequence is implemented in the door walkmesh path in the kit toolchain (kit.py L1090-L1174).

Game Engine Reference (reone): door.cpp L80–L94 β€” attaches closed/open DWK walkmeshes to the door model scene node


Placeables

Placeables are interactive objects (containers, terminals, etc.) that can be placed in rooms. They are optional and may not be present in all kits.

Placeable files:

  • {placeable_model}.pwk: Placeable walkmesh for pathfinding

Placeable Extraction Process:

  1. UTP files are extracted from module RIM/ERF archives
  2. Placeable model names are resolved from UTP files using placeables.2da
  3. PWK walkmeshes are extracted for each placeable model
  4. PWK files are written to kit directory root

This placeable extraction and PWK write flow is implemented in the corresponding kit extraction stage (kit.py L1176-L1251).

Placeable Walkmeshes (PWK)

Placeables have walkmeshes that define their collision boundaries for pathfinding.

PWK Extraction Process:

  1. Load placeables.2da to map UTP files to placeable model names
  2. For each placeable, resolve model name from UTP using placeable_tools.get_model()
  3. Batch lookup all PWK files using installation.locations()
  4. Extract PWK files from module first (fastest), then fall back to installation-wide resolution

These PWK resolution rules are implemented in the same placeable walkmesh extraction routine (kit.py L1176-L1251).

Game Engine Reference (reone): placeable.cpp L77–L80 β€” loads PWK for the resolved placeable model (ResType::Pwk)


Skyboxes

Skyboxes are optional MDL/MDX models used for outdoor area rendering. They are stored in the skyboxes/ folder.

Skybox Identification: Skyboxes are identified as MDL/MDX pairs that:

  1. Are NOT listed as room models in the LYT file
  2. Do NOT have corresponding WOK files
  3. Are found in the module's resource list

Skybox files:

Skyboxes are typically used for outdoor areas and provide the distant sky/background rendering. They are loaded separately from room components and don't have walkmeshes.

This skybox/component distinction is implemented by the model classification logic in the extraction pipeline (kit.py L740-L744).


Doorway Padding

The doorway/ folder contains padding models that fill gaps around doors:

Padding files:

  • side_{door_id}_size{size}.mdl: Side padding for horizontal doors
  • top_{door_id}_size{size}.mdl: Top padding for vertical doors
  • Corresponding .mdx files

Padding Purpose: When doors are inserted into walls, gaps may appear. Padding models fill these gaps to create seamless door transitions.

Naming Convention:

  • side_ or top_: Padding orientation
  • {door_id}: Door identifier (matches door index in JSON, extracted using get_nums())
  • size{size}: Padding size in world units (e.g., size650, size800)

The doorway filename parsing and loader conventions are implemented in the indoor-kit loader's doorway section (indoorkit_loader.py L127-L150).


models Directory

The models/ directory contains additional MDL/MDX models that are referenced by the module but are not used as components or skyboxes. These are typically:

models Directory structure:

models/
β”œβ”€β”€ {model_name}.mdl
└── {model_name}.mdx

This extra-model export stage is handled by the kit extraction logic for non-component, non-skybox models (kit.py L1311-L1324).


Resource Extraction

The kit extraction process (extract_kit()) extracts resources from module RIM or ERF files and generates a complete kit structure.

Archive file Support

The extraction process supports multiple archive formats:

  • RIM files: .rim (main module), _s.rim (supplementary data)
  • ERF files: .mod (module override), .erf (generic ERF), .hak (hakpak), .sav (savegame)

file Resolution Priority:

  1. .mod files take precedence over .rim files (as per KOTOR resolution order)
  2. If extension is specified, use that format directly
  3. If no extension, search for both RIM and ERF files, prioritizing .mod files

This archive handling and precedence logic is implemented in the extract_kit() archive resolution flow (kit.py L291-L550).

Component Identification

Components are identified using the following process:

  1. Load Module LYT files: Parse LYT file to get list of room model names
  2. Batch Resource Resolution: Batch lookup MDL/MDX/WOK for all room models using installation.locations()
  3. Component Criteria: A model is a component if:
  4. Component Name Mapping: Component IDs are mapped from model names using _get_component_name_mapping() to create friendly names (e.g., danm13_room01 -> room_01)

This component-identification sequence is implemented in the model classification and mapping pass in the kit extraction code (kit.py L600-L767).

texture and Lightmap Extraction

textures and lightmaps are extracted using a comprehensive process:

  1. model Scanning: Scan all MDL files from module.models() using iterate_textures() and iterate_lightmaps()
  2. Archive Scanning: Also extract TPC/TGA files directly from RIM/ERF archives
  3. Module Resource Scanning: Check module.resources for additional textures/lightmaps
  4. Batch Location Lookup: Single installation.locations() call for all textures/lightmaps with search order:
    • OVERRIDE
    • MODULES (.mod files take precedence over .rim files)
    • TEXTURES_GUI
    • TEXTURES_TPA
    • CHITIN
  5. Priority Sorting: Pre-sort all location results by priority once to avoid repeated sorting
  6. Batch I/O: Group TPC/TGA files by filepath for batch reading operations
  7. TPC to TGA Conversion: Convert all TPC files to TGA format during extraction
  8. TXI Extraction: Extract TXI files from embedded TPC data or standalone files

This full texture/lightmap extraction pipeline is implemented in the corresponding extraction stage in the kit tooling (kit.py L769-L1020).

Door Extraction

Doors are extracted from module UTD files:

  1. UTD Extraction: Extract all UTD files from module RIM/ERF archives
  2. Door model Resolution: Load genericdoors.2da once for all doors
  3. model Name Resolution: Resolve door model names from UTD files using door_tools.get_model()
  4. DWK Extraction: Extract door walkmeshes (3 states per door) using batch lookup
  5. Door JSON Generation: Generate door entries in kit JSON with fast defaults (2.0x3.0 dimensions)

This door extraction workflow and JSON emission are implemented in the door handling pass of the kit extractor (kit.py L1253-L1304).

walkmesh Extraction

walkmeshes are extracted for components, doors, and placeables:

  1. Component WOK: Extracted from module or installation-wide resolution
  2. Door DWK: Extracted using batch lookup (3 states: 0, 1, 2)
  3. Placeable PWK: Extracted using batch lookup

These walkmesh extraction paths are implemented across the door/placeable/component extraction routines (kit.py L1090-L1251).

BWM Re-centering

Component WOK files are re-centered to origin (0, 0, 0) before saving. This is critical for proper alignment in the Indoor Map Builder:

Problem: Without re-centering:

  • Preview image is drawn centered at room.position
  • walkmesh is translated BY room.position from original coordinates
  • If BWM center is at (100, 200) and room.position = (0, 0):
    • Image would be centered at (0, 0)
    • walkmesh would be centered at (100, 200) after translate
    • MISMATCH: Image and hitbox are in different places

Solution: After re-centering BWM to (0, 0):

  • Image is centered at room.position
  • walkmesh is centered at room.position after translate
  • MATCH: Image and hitbox overlap perfectly

Re-centering Process:

  1. Calculate BWM bounding box (min/max X, Y, Z)
  2. Calculate center point
  3. Translate all vertices by negative center to move BWM to origin
  4. Save re-centered WOK file

This recentering algorithm is implemented in the dedicated BWM translation helper in the kit tool (kit.py L1538-L1588).

Minimap Generation

Component minimap images are generated from re-centered BWM walkmeshes:

  1. Bounding Box Calculation: Calculate bounding box from BWM vertices
  2. Image Dimensions: scale to 10 pixels per world unit, minimum 256x256
  3. coordinate transformation: Transform world coordinates to image coordinates (flip Y-axis)
  4. face Rendering: Draw walkable faces in white, non-walkable in gray
  5. Image format: PNG format, saved as {component_id}.png

walkable face materials: faces with materials 1, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 16, 18, 20, 21, 22 are considered walkable.

This minimap generation and walkable-material filtering logic is implemented in the component image rendering code (kit.py L1348-L1465).

Doorhook Extraction

Door hooks are extracted from BWM edges that have valid transitions:

  1. edge Processing: Iterate through all BWM edges
  2. Transition Check: Skip edges without transitions (edge.transition < 0)
  3. Midpoint Calculation: Calculate midpoint of edge from vertices
  4. rotation Calculation: Calculate rotation angle from edge direction using atan2(dy, dx)
  5. Door index Mapping: Map transition index to door index (clamped to valid range)
  6. Hook Generation: Create doorhook entry with position, rotation, door index, and edge index

edge index Calculation: Global edge index = face_index * 3 + local_edge_index

This hook extraction and edge-index mapping are implemented in the doorhook generation routine (kit.py L1467-L1535).


Implementation Details

Kit Class structure

The Kit class structure in memory:

class Kit:
    name: str
    always: dict[Path, bytes]  # Static resources
    textures: CaseInsensitiveDict[bytes]  # Texture TGA data
    txis: CaseInsensitiveDict[bytes]  # TXI metadata (for textures and lightmaps)
    lightmaps: CaseInsensitiveDict[bytes]  # Lightmap TGA data
    skyboxes: CaseInsensitiveDict[MDLMDXTuple]  # Skybox models
    doors: list[KitDoor]  # Door definitions
    components: list[KitComponent]  # Room components
    side_padding: dict[int, dict[int, MDLMDXTuple]]  # Side padding by door_id and size
    top_padding: dict[int, dict[int, MDLMDXTuple]]  # Top padding by door_id and size

class KitComponent:
    kit: Kit
    name: str
    image: QImage  # Minimap image
    hooks: list[KitComponentHook]  # Door hook points
    bwm: BWM  # Walkmesh
    mdl: bytes  # Model geometry
    mdx: bytes  # Model extension

class KitComponentHook:
    position: Vector3  # Hook position
    rotation: float  # Rotation angle
    edge: str  # Edge identifier
    door: KitDoor  # Door reference

class KitDoor:
    utd_k1: UTD  # KotOR 1 door blueprint
    utd_k2: UTD  # KotOR 2 door blueprint
    width: float  # Door width
    height: float  # Door height
    utd: UTD  # Primary door blueprint alias (utd_k1)

This in-memory data model is defined by Holocron Toolset's indoor-kit base classes (indoorkit_base.py).

Kit Loading

Kits are loaded by load_kits() which:

  1. Scans Kits Directory: Iterates through all .json files in the kits directory
  2. Validates JSON: Skips invalid JSON files and non-dict structures
  3. Loads Kit Metadata: Extracts name and id from JSON
  4. Loads Always Resources: Loads all files from always/ folder into kit.always[filename]
  5. Loads textures: Loads TGA files from textures/ folder, extracts TXI files
  6. Loads Lightmaps: Loads TGA files from lightmaps/ folder, extracts TXI files
  7. Loads Skyboxes: Loads MDL/MDX pairs from skyboxes/ folder
  8. Loads Doorway Padding: Parses padding filenames to extract door_id and size, loads MDL/MDX pairs
  9. Loads Doors: Loads UTD files for K1 and K2, creates KitDoor instances
  10. Loads Components: Loads MDL/MDX/WOK files and PNG minimap images, creates KitComponent instances
  11. Populates Hooks: Extracts doorhook data from JSON and creates KitComponentHook instances
  12. Error Handling: Collects missing files instead of failing fast, returns list of missing files

This end-to-end load flow is implemented in the kit loader entry point and helper routines (indoorkit_loader.py L23-L260).

Indoor Map Generation

When generating an indoor map from kits:

  1. Component Placement: Components are placed at specified positions with rotations/flips
  2. Hook Connection: Hook points are matched to connect adjacent rooms
  3. model transformation: models are flipped, rotated, and transformed based on room properties
  4. texture/Lightmap Renaming: textures and lightmaps are renamed to module-specific names
  5. walkmesh Merging: Room walkmeshes are combined into a single area walkmesh
  6. Door Insertion: Doors are inserted at hook points with appropriate padding
  7. Resource Generation: are, GIT, LYT, VIS, IFO files are generated
  8. Minimap Generation: Minimap images are generated from component PNGs
  9. Static Resources: Always resources are added to every room via add_static_resources()

This generation pipeline is implemented in the indoor map builder's assembly and serialization flow (indoormap.py).

coordinate System

  • World coordinates: Meters in left-handed coordinate system (X=right, Y=forward, Z=up)
  • Hook positions: World-space coordinates relative to component origin (after re-centering to 0,0,0)
  • rotations: Degrees (0-360), counterclockwise from positive X-axis
  • Transforms: Components can be flipped on X/Y axes and rotated around Z-axis
  • BWM coordinates: Re-centered to origin (0, 0, 0) for proper alignment in Indoor Map Builder

These coordinate and transform conventions are consistent with the room/walkmesh definitions described in the BWM section and LYT format notes.


Kit types

Component-Based Kits

Kits with components array (e.g., enclavesurface, endarspire):

  • Contain reusable room models with walkmeshes
  • Have hook points for connecting rooms
  • Include textures/lightmaps referenced by components
  • Generate complete indoor maps with room layouts
  • Include minimap images for component selection

texture-Only Kits

Kits with empty components array (e.g., jedienclave):

  • Contain only textures and lightmaps
  • May include shared resources from multiple modules
  • Used for texture packs or shared resource collections
  • Don't generate room layouts (no components to place)
  • Useful for texture libraries or resource packs

Game Engine Compatibility

Kits are designed to be compatible with the KOTOR game engine's resource resolution and module structure:

Resource Resolution: Kits use the same resource resolution priority as the game engine:

  1. OVERRIDE (user mods)
  2. MODULES (.mod files take precedence over .rim files)
  3. TEXTURES_GUI
  4. TEXTURES_TPA
  5. CHITIN (base game)

Module structure: Generated modules follow the same structure as game modules:

  • are files for area definitions
  • GIT files for instance data
  • LYT files for room layouts
  • VIS files for visibility data
  • IFO files for module information

file formats: All kit resources use native game formats:

The shared resource-priority and format assumptions are implemented in the kit priority helpers (kit.py L74-L120).


Cross-reference: engine implementations

The kit extraction process is aligned with open-source engine code (reone, KotOR.js) and PyKotor’s kit tools. This section compares how those codebases handle the same operations and notes discrepancies.

Door Walkmesh (DWK) Extraction

reone (door.cpp L80–L98):

  • Doors load 3 walkmesh states: {modelName}0.dwk (closed), {modelName}1.dwk (open1), {modelName}2.dwk (open2)
  • walkmeshes are loaded via _services.resource.walkmeshes.get(modelName + "0", ResType::Dwk)
  • Each walkmesh state is stored as a separate WalkmeshSceneNode with enabled/disabled state based on door state
  • PyKotor Implementation: Matches reone exactly - extracts all 3 DWK states using the same naming convention

KotOR.js (ModuleDoor.ts L990–L1003):

  • Only loads the closed state walkmesh: ResourceLoader.loadResource(ResourceTypes['dwk'], resRef+'0')
  • Open states are handled dynamically through collision state updates, not separate walkmesh files
  • Discrepancy: KotOR.js only loads {modelName}0.dwk, while reone and PyKotor extract all 3 states
  • PyKotor Implementation: Extracts all 3 states to match reone's comprehensive approach

The PyKotor DWK extraction behavior described above is implemented in the dedicated door walkmesh extraction path (kit.py L1090-L1174).

Placeable Walkmesh (PWK) Extraction

reone (placeable.cpp L77–L80):

  • Placeables load a single walkmesh: _services.resource.walkmeshes.get(modelName, ResType::Pwk)
  • walkmesh is stored as a WalkmeshSceneNode attached to the placeable's scene node
  • PyKotor Implementation: Matches reone exactly - extracts PWK using model name directly

KotOR.js (ModulePlaceable.ts loadWalkmesh L665–L682):

  • Loads walkmesh: ResourceLoader.loadResource(ResourceTypes['pwk'], resRef)
  • Creates OdysseyWalkMesh from binary data and attaches to model
  • Falls back to empty walkmesh if loading fails
  • PyKotor Implementation: Matches KotOR.js approach - extracts PWK using model name

The PyKotor PWK extraction behavior described above is implemented in the placeable walkmesh extraction path (kit.py L1176-L1251).

room model and Component Identification

reone (area.cpp L305–L375):

  • Loads LYT file via _services.resource.layouts.get(_name)
  • Iterates through layout->rooms to get room model names
  • For each room, loads MDL model: _services.resource.models.get(lytRoom.name)
  • Loads WOK walkmesh: _services.resource.[walkmeshes](Level-Layout-Formats#bwm).get(lytRoom.name, ResType::Wok)
  • Rooms are identified as MDL models with corresponding WOK files from LYT
  • PyKotor Implementation: Matches reone exactly - uses LYT to identify room models, then resolves MDL/MDX/WOK

KotOR.js (ModuleRoom.ts L338–L342; loadWalkmesh L360–L367):

  • Loads walkmesh: ResourceLoader.loadResource(ResourceTypes['wok'], resRef)
  • Creates OdysseyWalkMesh from binary data and attaches to room model
  • Rooms are identified from LYT file room definitions
  • PyKotor Implementation: Matches KotOR.js approach - uses LYT room models to identify components

The corresponding PyKotor room/component detection logic is implemented in the component identification path (kit.py L545-L767).

Door model Resolution

reone (door.cpp loadFromBlueprint L59+):

  • Door models are resolved from UTD files using genericdoors.2da
  • The appearance_id field in UTD maps to a row in genericdoors.2da
  • The modelname column in that row provides the door model name
  • PyKotor Implementation: Matches reone exactly - uses door_tools.get_model() which reads genericdoors.2da

KotOR.js (ModuleDoor.ts L1062–L1063 β€” GenericType from UTD; see also getGenericType / appearance):

  • Door models are resolved similarly using [genericdoors.2da](2DA-File-Format#genericdoors2da)
  • The appearance ID from UTD is used to lookup model name
  • PyKotor Implementation: Matches KotOR.js approach

PyKotor's door model-resolution helper is implemented in the door utility module (door.py L25-L64).

Placeable model Resolution

reone (placeable.cpp loadFromBlueprint L50+):

  • Placeable models are resolved from UTP files using placeables.2da
  • The appearance_id field in UTP maps to a row in placeables.2da
  • The modelname column in that row provides the placeable model name
  • PyKotor Implementation: Matches reone exactly - uses placeable_tools.get_model() which reads placeables.2da

KotOR.js (ModulePlaceable.ts L729–L732 β€” Appearance -> placeableAppearance; L575 β€” modelname):

  • Placeable models are resolved similarly using placeables.2da
  • The appearance ID from UTP is used to lookup model name
  • PyKotor Implementation: Matches KotOR.js approach

PyKotor's placeable model-resolution helper is implemented in the placeable utility module (placeable.py L20-L50).

texture and Lightmap Extraction

PyKotor Implementation (Libraries/PyKotor/src/pykotor/tools/model.py):

  • Uses iterate_textures() and iterate_lightmaps() to extract texture/lightmap references from MDL files
  • Scans all MDL nodes (mesh, skin, emitter) for texture references
  • Lightmaps are identified by naming patterns (_lm suffix or l_ prefix)
  • Vendor Comparison: No direct equivalent in reone/KotOR.js - they load textures on-demand during rendering
  • Discrepancy: PyKotor proactively extracts all textures/lightmaps, while engines load them lazily during rendering
  • Rationale: Kit extraction needs all textures upfront for self-contained kit structure

This texture/lightmap reference scanning behavior is implemented in the model utility iterators (model.py L99-L887).

Resource Resolution Priority

reone (src/libs/resource/provider/):

  • Resource resolution follows KOTOR priority: Override -> Modules -> Chitin
  • .mod files take precedence over .rim files in Modules directory
  • PyKotor Implementation: Matches reone exactly - uses same priority order via _get_resource_priority()

KotOR.js (ResourceLoader.ts L22+):

  • Resource resolution follows similar priority order
  • Override folder checked first, then modules, then chitin
  • PyKotor Implementation: Matches KotOR.js approach

PyKotor's implementation of this resource-priority behavior is defined in its kit resource-priority helpers (kit.py L74-L120).

BWM/WOK walkmesh Handling

reone (walkmesh.cpp L1+):

  • walkmeshes are loaded from WOK/BWM files
  • face materials determine walkability (materials 1, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 16, 18, 20, 21, 22 are walkable)
  • edge transitions indicate door connections
  • PyKotor Implementation: Matches reone - uses same walkable material values for minimap generation

KotOR.js (OdysseyWalkMesh.ts L24+):

  • walkmeshes are loaded from WOK binary data
  • face materials and walk types determine walkability
  • edge transitions are stored in walkmesh structure
  • PyKotor Implementation: Matches KotOR.js - extracts doorhooks from BWM edges with transitions

PyKotor's corresponding minimap and doorhook logic is implemented in the relevant extraction helpers (kit.py L1348-L1465, kit.py L1467-L1535).

module archives Loading

reone (src/libs/resource/provider/):

  • Supports RIM and ERF file formats
  • .mod files (ERF format) take precedence over .rim files
  • PyKotor Implementation: Matches reone exactly - prioritizes .mod files over .rim files

KotOR.js (ResourceLoader.ts L22+):

  • Supports RIM and ERF file formats
  • Module loading follows same priority order
  • PyKotor Implementation: Matches KotOR.js approach

PyKotor's module-archive loading implementation for this flow is in the extraction archive resolution code (kit.py L291-L550).

Key Discrepancies and Rationale

  1. DWK Extraction:

    • KotOR.js: Only extracts closed state ({modelName}0.dwk)
    • reone/PyKotor: Extracts all 3 states (closed, open1, open2)
    • Rationale: PyKotor matches reone for comprehensive kit extraction
  2. texture Extraction:

    • Vendor Engines: Load textures lazily during rendering
    • PyKotor: Proactively extracts all textures/lightmaps upfront
    • Rationale: Kits need self-contained resource collections, not lazy loading
  3. BWM Re-centering:

    • Vendor Engines: Use BWMs in world coordinates as-is
    • PyKotor: Re-centers BWMs to origin (0, 0, 0)
    • Rationale: Indoor Map Builder requires centered BWMs for proper image/walkmesh alignment
  4. Component Name Mapping:

    • Vendor Engines: Use model names directly (e.g., m09aa_01a)
    • PyKotor: Maps to friendly names (e.g., hall_1) for better UX
    • Rationale: Kit components need human-readable identifiers for toolset UI

Test Comparison Precision

The kit generation tests (Tools/HolocronToolset/tests/data/test_kit_generation.py) use different comparison strategies depending on the data type:

Exact Matching (1:1 byte-for-byte)

Binary files (SHA256 hash comparison):

  • MDL/MDX: model geometry and animations - must be byte-for-byte identical
  • WOK/BWM: walkmesh data - must be byte-for-byte identical
  • DWK/PWK: Door and placeable walkmeshes - must be byte-for-byte identical
  • PNG: Minimap images - must be byte-for-byte identical
  • UTD: Door blueprints - must be byte-for-byte identical
  • TXI: texture metadata files - must be byte-for-byte identical

Rationale: These files contain critical game data that must match exactly for functional compatibility.

This exact-match behavior is asserted in the binary/hash comparison block of the kit generation tests (test_kit_generation.py L912-L970).

Approximate Matching (Tolerance-Based)

Image files (TGA/TPC - pixel-by-pixel comparison):

  • Dimensions: Must match exactly (width Γ— height)
  • Pixel data: Allows tolerance for compression artifacts:
    • Up to 2 levels difference per channel (R, G, B, A) per pixel
    • Up to 1% of pixels can differ by more than 2 levels
    • Accounts for DXT compression artifacts in TPC files

Rationale: TPC files use DXT compression which can introduce small pixel differences even for identical source images.

This tolerance-based image comparison is implemented in the image assertion block of the same test module (test_kit_generation.py L972-L1111).

structure-Only Verification (No value Comparison)

JSON Metadata (structure verification only):

  • Doorhook coordinates: Only verifies that x, y, z, rotation fields exist - does NOT compare actual values
  • Door Dimensions: Only verifies that width, height fields exist - does NOT compare actual values
  • Doorhook count: Verifies count matches exactly
  • Component count: Verifies count matches exactly
  • Door count: Verifies count matches exactly
  • field Presence: Verifies required fields exist (name, id, door, edge)

Current Test Behavior:

# Lines 1229-1234: Only checks field existence, not values
self.assertIn("x", gen_hook, f"Component {i} hook {j} missing x")
self.assertIn("y", gen_hook, f"Component {i} hook {j} missing y")
self.assertIn("z", gen_hook, f"Component {i} hook {j} missing z")
self.assertIn("rotation", gen_hook, f"Component {i} hook {j} missing rotation")
# NO assertEqual or assertAlmostEqual for coordinate values!

# Lines 1182-1185: Only checks field existence, not values
if "width" in exp_door:
    self.assertIn("width", gen_door, f"Door {i} missing width")
if "height" in exp_door:
    self.assertIn("height", gen_door, f"Door {i} missing height")
# NO assertEqual or assertAlmostEqual for dimension values!

Rationale: Tests were designed to be lenient during initial development when doorhook extraction and door dimension calculation were incomplete. The comment on line 1114 states: "handling differences in components/doorhooks that may not be fully extracted yet."

Implications:

  • Doorhook coordinates are NOT validated - tests will pass even if coordinates are completely wrong
  • Door dimensions are NOT validated - tests will pass even if dimensions are incorrect
  • High granularity matching is NOT enforced - coordinate precision is not verified
  • Error acceptability is currently 100% - any coordinate values are accepted as long as fields exist

This structure-only behavior is visible in the later hook/door assertions that check field presence but do not compare values (test_kit_generation.py L1113-L1234).

Recommended Test Improvements

To achieve high granularity coordinate matching, the tests should be enhanced to:

  1. Compare Doorhook coordinates:

    # Use assertAlmostEqual with appropriate tolerance for floating-point comparison
    self.assertAlmostEqual(gen_hook.get("x"), exp_hook.get("x"), places=6, 
                           msg=f"Component {i} hook {j} x coordinate differs")
    self.assertAlmostEqual(gen_hook.get("y"), exp_hook.get("y"), places=6,
                           msg=f"Component {i} hook {j} y coordinate differs")
    self.assertAlmostEqual(gen_hook.get("z"), exp_hook.get("z"), places=6,
                           msg=f"Component {i} hook {j} z coordinate differs")
    self.assertAlmostEqual(gen_hook.get("rotation"), exp_hook.get("rotation"), places=2,
                           msg=f"Component {i} hook {j} rotation differs")
    
  2. Compare Door Dimensions:

    # Use assertAlmostEqual with appropriate tolerance
    if "width" in exp_door:
        self.assertAlmostEqual(gen_door.get("width"), exp_door.get("width"), places=2,
                               msg=f"Door {i} width differs")
    if "height" in exp_door:
        self.assertAlmostEqual(gen_door.get("height"), exp_door.get("height"), places=2,
                               msg=f"Door {i} height differs")
    
  3. Tolerance Levels:

    • Coordinates (x, y, z): 6 decimal places (0.000001 units) - matches Python float precision
    • rotation: 2 decimal places (0.01 degrees) - sufficient for door placement
    • Dimensions (width, height): 2 decimal places (0.01 units) - sufficient for door sizing

Current Status: Tests are NOT performing 1:1 coordinate matching. They only verify structure, not values. This means tests can pass even if coordinates are incorrect, which may mask extraction bugs.


Best Practices

  1. Component Naming: Use descriptive, consistent naming (e.g., hall_1, junction_2)
  2. texture Organization: Group related textures logically
  3. Always Folder: Use sparingly for truly shared resources
  4. Door Definitions: Ensure door UTD files match JSON definitions
  5. Hook Placement: Place hooks at logical connection points (extracted from BWM edges with transitions)
  6. Minimap Images: Generate accurate top-down views for component selection
  7. BWM Re-centering: Always re-center BWMs to origin for proper alignment
  8. Resource Resolution: Respect game engine resource resolution priority
  9. Batch Processing: Use batch I/O operations for performance
  10. Error Handling: Collect missing files instead of failing fast
  11. Vendor Compatibility: Follow reone/KotOR.js patterns for walkmesh and model handling
  12. Comprehensive Extraction: Extract all DWK states and all referenced textures for complete kits
  13. Test Precision: Consider enhancing tests to verify coordinate values, not just structure

See also