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 structure Documentation
- Table of Contents
- Kit Overview
- Kit Directory structure
- Kit JSON file
- Components
- textures and Lightmaps
- Always Folder
- Doors
- Placeables
- Skyboxes
- Doorway Padding
- models Directory
- Resource Extraction
- Implementation Details
- Kit types
- Game Engine Compatibility
- Cross-reference: engine implementations
- Test Comparison Precision
- Best Practices
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 kitid: Unique kit identifier (matches folder name, must be lowercase, sanitized)ht: Holocron Toolset version compatibility stringversion: 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 componentid: 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 planedoor: index into the kit'sdoorsarray (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.utdextension)utd_k2: ResRef of K2 door UTD file (without.utdextension)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:
- Are listed as room models in the module's LYT file
- Have corresponding WOK (walkmesh) files
- 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:
- Load module LYT file to get list of room model names
- For each room model, resolve MDL/MDX/WOK using installation-wide resource resolution
- Components are room models that have both MDL and WOK files
- 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.positionfrom 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 usingatan2(dy, dx)door: index into the kit'sdoorsarray (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:
- Module RIM/ERF files: textures directly in the module archives
- Installation-wide Resolution: textures from:
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
_lmor prefixed withl_(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:
- Reads TPC file data
- Parses TPC structure (mipmaps, format, embedded TXI)
- Converts mipmaps to RGBA format if needed
- 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:
- OVERRIDE (priority 0 - highest): User mods in
override/folder - MODULES (priority 1-2): Module files
.modfiles (priority 1) - take precedence over.rimfiles.rimand_s.rimfiles (priority 2)
- TEXTURES_GUI (priority 3): GUI textures
- TEXTURES_TPA (priority 4): texture packs
- 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:
- Embedded TXI in TPC files: TPC files can contain embedded TXI data
- Standalone TXI files: TXI files in the installation (same resolution priority as textures)
- Empty TXI placeholders: If no TXI is found, an empty TXI file is created to match expected kit structure
TXI Extraction Process:
- Check for embedded TXI in TPC file during conversion
- If not found, lookup standalone TXI file using batch location results
- 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_lm13fromjedienclavekit - These are found in
data/lightmaps3.bifas shared resources across multiple modules - They may be referenced by other modules that share resources with the kit's source module
- Example:
- 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
- Example:
- 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 lightmapstexturepacks/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:
- Each file in
always/is loaded intokit.always[filename]during kit loading - When a room is processed,
add_static_resources()extracts the resource name and type from the filename - The resource is added to the mod with
mod.set_data(resname, restype, data) - This happens for every room, ensuring the resource is always available
Example: The sithbase kit includes:
CM_asith.tpc: Common texture used across all roomslsi_floor01b.tpc,lsi_flr03b.tpc,lsi_flr04b.tpc: Floor textures for all roomslsi_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.utdextension)width: Door width in world units (default: 2.0)height: Door height in world units (default: 3.0)
Door Extraction Process:
- UTD files are extracted from module RIM/ERF archives
- Door model names are resolved from UTD files using
genericdoors.2da - DWK walkmeshes are extracted for each door model (3 states: 0=closed, 1=open1, 2=open2)
- 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:
- Load
genericdoors.2dato map UTD files to door model names - For each door, resolve model name from UTD using
door_tools.get_model() - Batch lookup all DWK files (3 states per door) using
installation.locations() - 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:
- UTP files are extracted from module RIM/ERF archives
- Placeable model names are resolved from UTP files using
placeables.2da - PWK walkmeshes are extracted for each placeable model
- 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:
- Load
placeables.2dato map UTP files to placeable model names - For each placeable, resolve model name from UTP using
placeable_tools.get_model() - Batch lookup all PWK files using
installation.locations() - 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:
- Are NOT listed as room models in the LYT file
- Do NOT have corresponding WOK files
- 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 doorstop_{door_id}_size{size}.mdl: Top padding for vertical doors- Corresponding
.mdxfiles
Padding Purpose: When doors are inserted into walls, gaps may appear. Padding models fill these gaps to create seamless door transitions.
Naming Convention:
side_ortop_: Padding orientation{door_id}: Door identifier (matches door index in JSON, extracted usingget_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:
- Decorative models: models used for decoration or atmosphere
- Non-room models: models that don't have walkmeshes and aren't room components
- Referenced models: models that are referenced by scripts or other systems
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:
.modfiles take precedence over.rimfiles (as per KOTOR resolution order)- If extension is specified, use that format directly
- If no extension, search for both RIM and ERF files, prioritizing
.modfiles
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:
- Load Module LYT files: Parse LYT file to get list of room model names
- Batch Resource Resolution: Batch lookup MDL/MDX/WOK for all room models using
installation.locations() - Component Criteria: A model is a component if:
- 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:
- model Scanning: Scan all MDL files from
module.models()usingiterate_textures()anditerate_lightmaps() - Archive Scanning: Also extract TPC/TGA files directly from RIM/ERF archives
- Module Resource Scanning: Check
module.resourcesfor additional textures/lightmaps - Batch Location Lookup: Single
installation.locations()call for all textures/lightmaps with search order:- OVERRIDE
- MODULES (
.modfiles take precedence over.rimfiles) - TEXTURES_GUI
- TEXTURES_TPA
- CHITIN
- Priority Sorting: Pre-sort all location results by priority once to avoid repeated sorting
- Batch I/O: Group TPC/TGA files by filepath for batch reading operations
- TPC to TGA Conversion: Convert all TPC files to TGA format during extraction
- 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:
- UTD Extraction: Extract all UTD files from module RIM/ERF archives
- Door model Resolution: Load
genericdoors.2daonce for all doors - model Name Resolution: Resolve door model names from UTD files using
door_tools.get_model() - DWK Extraction: Extract door walkmeshes (3 states per door) using batch lookup
- 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:
- Component WOK: Extracted from module or installation-wide resolution
- Door DWK: Extracted using batch lookup (3 states: 0, 1, 2)
- 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.positionfrom 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.positionafter translate - MATCH: Image and hitbox overlap perfectly
Re-centering Process:
- Calculate BWM bounding box (min/max X, Y, Z)
- Calculate center point
- Translate all vertices by negative center to move BWM to origin
- 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:
- Bounding Box Calculation: Calculate bounding box from BWM vertices
- Image Dimensions: scale to 10 pixels per world unit, minimum 256x256
- coordinate transformation: Transform world coordinates to image coordinates (flip Y-axis)
- face Rendering: Draw walkable faces in white, non-walkable in gray
- 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:
- edge Processing: Iterate through all BWM edges
- Transition Check: Skip edges without transitions (
edge.transition < 0) - Midpoint Calculation: Calculate midpoint of edge from vertices
- rotation Calculation: Calculate rotation angle from edge direction using
atan2(dy, dx) - Door index Mapping: Map transition index to door index (clamped to valid range)
- 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:
- Scans Kits Directory: Iterates through all
.jsonfiles in the kits directory - Validates JSON: Skips invalid JSON files and non-dict structures
- Loads Kit Metadata: Extracts
nameandidfrom JSON - Loads Always Resources: Loads all files from
always/folder intokit.always[filename] - Loads textures: Loads TGA files from
textures/folder, extracts TXI files - Loads Lightmaps: Loads TGA files from
lightmaps/folder, extracts TXI files - Loads Skyboxes: Loads MDL/MDX pairs from
skyboxes/folder - Loads Doorway Padding: Parses padding filenames to extract door_id and size, loads MDL/MDX pairs
- Loads Doors: Loads UTD files for K1 and K2, creates
KitDoorinstances - Loads Components: Loads MDL/MDX/WOK files and PNG minimap images, creates
KitComponentinstances - Populates Hooks: Extracts doorhook data from JSON and creates
KitComponentHookinstances - 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:
- Component Placement: Components are placed at specified positions with rotations/flips
- Hook Connection: Hook points are matched to connect adjacent rooms
- model transformation: models are flipped, rotated, and transformed based on room properties
- texture/Lightmap Renaming: textures and lightmaps are renamed to module-specific names
- walkmesh Merging: Room walkmeshes are combined into a single area walkmesh
- Door Insertion: Doors are inserted at hook points with appropriate padding
- Resource Generation: are, GIT, LYT, VIS, IFO files are generated
- Minimap Generation: Minimap images are generated from component PNGs
- 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:
- OVERRIDE (user mods)
- MODULES (
.modfiles take precedence over.rimfiles) - TEXTURES_GUI
- TEXTURES_TPA
- 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:
- MDL/MDX for 3D models
- WOK/BWM for walkmeshes
- TGA for textures (converted from TPC during extraction)
- TXI for texture metadata
- UTD for door blueprints
- DWK/PWK for walkmeshes
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
WalkmeshSceneNodewith 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
WalkmeshSceneNodeattached 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
OdysseyWalkMeshfrom 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->roomsto 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
OdysseyWalkMeshfrom 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_idfield in UTD maps to a row ingenericdoors.2da - The
modelnamecolumn in that row provides the door model name - PyKotor Implementation: Matches reone exactly - uses
door_tools.get_model()which readsgenericdoors.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_idfield in UTP maps to a row inplaceables.2da - The
modelnamecolumn in that row provides the placeable model name - PyKotor Implementation: Matches reone exactly - uses
placeable_tools.get_model()which readsplaceables.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()anditerate_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 (
_lmsuffix orl_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
.modfiles take precedence over.rimfiles 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
.modfiles (ERF format) take precedence over.rimfiles- PyKotor Implementation: Matches reone exactly - prioritizes
.modfiles over.rimfiles
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
-
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
- KotOR.js: Only extracts closed state (
-
texture Extraction:
-
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
-
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
- Vendor Engines: Use model names directly (e.g.,
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,rotationfields exist - does NOT compare actual values - Door Dimensions: Only verifies that
width,heightfields 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:
-
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") -
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") -
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
- Component Naming: Use descriptive, consistent naming (e.g.,
hall_1,junction_2) - texture Organization: Group related textures logically
- Always Folder: Use sparingly for truly shared resources
- Door Definitions: Ensure door UTD files match JSON definitions
- Hook Placement: Place hooks at logical connection points (extracted from BWM edges with transitions)
- Minimap Images: Generate accurate top-down views for component selection
- BWM Re-centering: Always re-center BWMs to origin for proper alignment
- Resource Resolution: Respect game engine resource resolution priority
- Batch Processing: Use batch I/O operations for performance
- Error Handling: Collect missing files instead of failing fast
- Vendor Compatibility: Follow reone/KotOR.js patterns for walkmesh and model handling
- Comprehensive Extraction: Extract all DWK states and all referenced textures for complete kits
- Test Precision: Consider enhancing tests to verify coordinate values, not just structure
See also
- Indoor Map Builder User Guide -- End-user guide for building indoor areas
- Indoor Map Builder Implementation Guide -- Code-level implementation details
- Indoor Area Room Layout and Walkmesh Guide -- LYT, VIS, BWM workflow
- Level Layout Formats -- LYT, VIS, BWM binary format reference
- Holocron Toolset Map Builder -- Map Builder tool documentation
- Texture Formats -- TPC, TGA, DDS texture handling