Rose Online Asset File Format Technical Specification

This document will serve as a technical reference for the very old MMORPG Rose Online. The goal is that this information is never lost and may help decades later.

Table of Contents

1. Introduction

1.1 Rose Online Asset System Overview

Rose Online uses a sophisticated asset management system organized around a Virtual Filesystem (VFS) architecture. Assets are stored in proprietary binary formats optimized for real-time rendering in the game engine.

Key Characteristics:

  • Coordinate System: Y-up, centimeter-based (cm)
  • File Format Versions: Multiple format versions with backward compatibility
  • Asset Organization: Hierarchical structure with clear separation of concerns
  • Rendering Pipeline: GPU-accelerated with skinned mesh support

Asset File Extensions:

Core 3D Asset Formats:
Extension Description Primary Use Criticality
.zms Mesh geometry Character models, objects, terrain tiles Critical
.zmd Skeleton definition Bone hierarchies for skinned meshes Critical
.zmo Motion/animation Character animations, object animations Critical
.zon Zone definition Terrain composition, grid system Critical
.zsc Object composition Multi-part object assembly Critical
.ifo Zone object placement NPC spawns, objects, events Critical
.him Heightmap Terrain elevation data High
.til Tile index Terrain tile mapping High
.chr Character definition NPC and character asset references Critical
Data Table Formats:
Extension Description Primary Use Criticality
.stb String/Data tables Game data (items, skills, NPCs) Critical
.ltb Language tables UI text, dialogs, localization Critical
Effect Formats:
Extension Description Primary Use Criticality
.eft Effect definitions Visual effects, particles High
Texture Formats:
Extension Description Primary Use Criticality
.dds DirectDraw Surface Primary GPU texture format Critical
.tga Targa image Terrain/UI textures High
.bmp Bitmap image Screenshots Medium
.jpg JPEG image Screenshots, compressed images Medium
Audio Formats:
Extension Description Primary Use Criticality
.wav Wave audio Sound effects Critical
.ogg Ogg Vorbis audio Background music Critical
.mp3 MP3 audio Alternative music format Medium
Script Formats:
Extension Description Primary Use Criticality
.lua Lua scripts Game logic, UI, event handlers Critical
Archive Formats:
Extension Description Primary Use Criticality
.pkg Package/archive VFS container format Critical

1.2 File Format Relationships

graph TD
    ZON[ZON Zone File] -->|References| TIL[TIL Tile Index]
    ZON -->|Uses| HIM[HIM Heightmap]
    ZON -->|Contains| Tiles[Tiles Block]

    IFO[IFO Zone Object File] -->|Instantiates| ZSC[ZSC Object Files]
    IFO -->|References| CHR[CHR Character File]
    IFO -->|Places| Objects[Zone Objects]

    CHR -->|References| ZMD[ZMD Skeleton]
    CHR -->|References| ZSC
    CHR -->|References| ZMO[ZMO Animation]
    CHR -->|Maps| Motions[Motion Actions]

    ZSC -->|Contains| ZMS[ZMS Mesh]
    ZSC -->|Contains| Materials[ZSC Materials]
    ZSC -->|Defines| Parts[Object Parts]
    ZSC -->|Contains| Effects[Effect References]

    ZMS -->|Requires| ZMD
    ZMS -->|Contains| Vertices[Vertex Data]
    ZMS -->|Contains| Indices[Index Data]
    ZMS -->|Contains| BoneWeights[Bone Weights]

    ZMO -->|Targets| ZMD bones
    ZMO -->|Contains| Channels[Animation Channels]
    ZMO -->|Contains| FrameEvents[Frame Events]
    ZMO -->|Extended| EZMO[EZMO/3ZMO]

    ZMD -->|Contains| Bones[Regular Bones]
    ZMD -->|Contains| DummyBones[Dummy Bones]

    HIM -->|Provides| Heights[Terrain Heights]
    TIL -->|Provides| Indices[Tile Indices]

Complete Asset Loading Workflows

Zone Loading and Composition Workflow:
1. Load ZON File
   ├─ Parse ZoneInfo block (grid configuration)
   ├─ Parse EventPositions block (spawn points)
   ├─ Parse Textures block (texture paths)
   └─ Parse Tiles block (tile composition)

2. Load Terrain Data
   ├─ Load HIM file (heightmap)
   ├─ Load TIL file (tile indices)
   └─ Generate terrain mesh from ZON tiles + HIM heights

3. Load IFO File
   ├─ Parse object blocks (NPCs, monsters, decorations)
   ├─ Parse spawn points
   ├─ Parse warp positions
   └─ Parse water planes

4. Load Zone Objects
   ├─ For each IFO object:
   │  ├─ Load ZSC file (object composition)
   │  ├─ For each ZSC part:
   │  │  ├─ Load ZMS mesh file
   │  │  ├─ Load ZSC material
   │  │  ├─ Apply material to mesh
   │  │  └─ Parent to object entity
   │  └─ For each ZSC effect:
   │     └─ Load and spawn effect
   └─ Place object at IFO position with transform
Character/NPC Assembly Pipeline:
1. Load CHR File
   ├─ Parse skeleton file list
   ├─ Parse motion file list
   ├─ Parse effect file list
   └─ Parse character definitions

2. For Character/NPC:
   ├─ Load ZMD skeleton file
   │  ├─ Parse bone hierarchy
   │  ├─ Create bone entities
   │  ├─ Compute bind pose matrices
   │  ├─ Compute inverse bind matrices
   │  └─ Create SkinnedMesh component
   │
   ├─ Load ZSC object files (one per body part)
   │  ├─ For each ZSC object:
   │  │  ├─ Parse material definitions
   │  │  ├─ Parse object parts
   │  │  ├─ Parse effect definitions
   │  │  └─ Load ZMS mesh files
   │
   ├─ Load ZMS mesh files
   │  ├─ Parse vertex data (positions, normals, UVs)
   │  ├─ Parse bone weights and indices
   │  ├─ Parse index data
   │  └─ Create mesh entities
   │
   ├─ Bind meshes to skeleton
   │  ├─ Attach to bone entities (or dummy bones)
   │  ├─ Apply SkinnedMesh component
   │  └─ Set material properties
   │
   ├─ Load ZMO animation files
   │  ├─ Parse frame data
   │  ├─ Parse channel data (position, rotation, scale)
   │  ├─ Parse frame events
   │  └─ Create animation handles
   │
   └─ Attach effects to dummy bones
      ├─ Load effect files from CHR
      ├─ Spawn effect entities
      └─ Parent to dummy bone entities
Animation Application Pipeline:
1. Load ZMO Animation File
   ├─ Parse FPS and frame count
   ├─ Parse channel definitions
   ├─ Parse frame data per channel
   └─ Parse frame events

2. Create ZmoAsset
   ├─ Organize channels by bone ID
   ├─ Store translation vectors per frame
   ├─ Store rotation quaternions per frame
   ├─ Store scale values per frame
   └─ Store frame events

3. Animation Playback
   ├─ Calculate current frame from time
   ├─ Get frame fraction for interpolation
   ├─ Sample bone transforms:
   │  ├─ Position: Linear interpolation
   │  ├─ Rotation: Quaternion slerp
   │  └─ Scale: Linear interpolation
   ├─ Apply transforms to bone entities
   └─ Trigger frame events

4. Skinned Mesh Update
   ├─ GPU computes vertex positions
   ├─ Using bone weights and current pose
   ├─ Apply skinning matrices
   └─ Render final mesh

Detailed File Type Interactions:

ZON ↔ HIM/TIL Interaction:

Purpose: Generate terrain geometry from zone definition

Data Flow:

ZON File:
  ├─ Grid per patch (float)
  ├─ Grid size (float)
  ├─ Tile textures (array of paths)
  └─ Tiles (array of ZonTile)
     ├─ Layer1 texture index
     ├─ Layer2 texture index
     ├─ Offset1, Offset2 (UV offsets)
     ├─ Blend flag
     └─ Rotation enum

HIM File:
  ├─ Width, Height (grid dimensions)
  └─ Heights array (width × height floats)

TIL File:
  ├─ Width, Height (grid dimensions)
  └─ Tiles array (width × height indices)

Terrain Generation:
  For each grid cell (x, y):
    ├─ Get tile index from TIL[x][y]
    ├─ Get tile definition from ZON.tiles[tile_index]
    ├─ Get texture path from ZON.textures[tile.layer1]
    ├─ Get height from HIM[x][y]
    ├─ Apply tile rotation and offset
    ├─ Generate vertices with height
    └─ Apply texture coordinates

Client Implementation (observed in rendering code):

  • Terrain is rendered as a grid of tiles
  • Each tile is a quad with 4 vertices
  • Vertices are positioned using HIM heights
  • UVs are computed from TIL tile indices and ZON tile data
  • Layer blending is applied if tile.blend is true
IFO ↔ ZSC/ZMS/ZMD Interaction:

Purpose: Place objects in zone with correct models and transforms

Data Flow:

IFO File:
  ├─ Object type (Deco, NPC, MonsterSpawn, etc.)
  ├─ Object ID (references CHR or ZSC)
  ├─ Position (X, Y, Z)
  ├─ Rotation (quaternion XYZW)
  ├─ Scale (X, Y, Z)
  └─ Additional type-specific data

For NPC Objects:
  ├─ Load CHR file by NPC ID
  │  ├─ Get skeleton file path
  │  ├─ Get model IDs
  │  ├─ Get motion IDs
  │  └─ Get effect IDs
  │
  ├─ Load ZMD skeleton
  ├─ Load ZSC model files (from CHR.model_ids)
  │  └─ For each ZSC object:
  │     ├─ Load ZMS meshes
  │     ├─ Load materials
  │     └─ Create mesh entities
  │
  └─ Load ZMO animations (from CHR.motion_ids)
     └─ Create animation handles

For Decoration Objects:
  ├─ Load ZSC file directly (from IFO.object_id)
  └─ Same process as above
CHR ↔ ZMD/ZSC/ZMO Interaction:

Purpose: Define character/NPC asset composition and animations

Data Flow:

CHR File:
  ├─ Skeleton files (array of paths)
  ├─ Motion files (array of paths)
  ├─ Effect files (array of paths)
  └─ Character definitions (map: ID → NpcModelData)

NpcModelData:
  ├─ Skeleton index (into skeleton_files)
  ├─ Name
  ├─ Model IDs (array: into ZSC objects)
  ├─ Motion IDs (array of tuples: (motion_id, file_index))
  └─ Effect IDs (array of tuples: (motion_id, file_index))
ZSC ↔ ZMS/Material Interaction:

Purpose: Define multi-part objects with materials and transforms

Data Flow:

ZSC File:
  ├─ Meshes (array of paths to ZMS files)
  ├─ Materials (array of ZscMaterial)
  ├─ Effects (array of paths to effect files)
  └─ Objects (array of ZscObject)

ZscObject:
  └─ Parts (array of ZscObjectPart)

ZscObjectPart:
  ├─ Mesh ID (into meshes array)
  ├─ Material ID (into materials array)
  ├─ Transform (position, rotation, scale)
  ├─ Bone index (optional: bind to skeleton bone)
  ├─ Dummy index (optional: bind to dummy bone)
  ├─ Parent index (optional: parent part)
  └─ Animation path (optional: animation override)

ZscMaterial:
  ├─ Path (texture file)
  ├─ Alpha enabled, two-sided
  ├─ Alpha test (threshold)
  ├─ Z write/test enabled
  ├─ Blend mode
  ├─ Specular enabled
  ├─ Alpha value
  └─ Glow type and color
ZMO ↔ ZMD Interaction:

Purpose: Drive skeleton bones with animation data

Data Flow:

ZMO File:
  ├─ FPS (frames per second)
  ├─ Frame count
  ├─ Channels (array of (bone_id, channel_type))
  └─ Frame events (array of frame numbers)

ZmoChannel Types:
  ├─ Position: Vec3 per frame
  ├─ Rotation: Quat4 per frame
  ├─ Scale: f32 per frame
  ├─ Normal: Vec3 per frame (morph targets)
  ├─ UV1-UV4: Vec2 per frame (texture animation)
  ├─ Alpha: f32 per frame
  └─ Texture: f32 per frame
ZMS ↔ ZMD Interaction:

Purpose: Bind mesh vertices to skeleton bones for skinning

Data Flow:

ZMS File:
  ├─ Format flags (vertex attributes)
  ├─ Bone count
  ├─ Bone mapping (version 5/6 only)
  ├─ Vertex count
  ├─ Vertex data (based on flags):
  │  ├─ Positions (Vec3)
  │  ├─ Normals (Vec3)
  │  ├─ Colors (Vec4)
  │  ├─ Bone weights (Vec4: w1, w2, w3, w4)
  │  ├─ Bone indices (Vec4: i1, i2, i3, i4)
  │  ├─ Tangents (Vec3)
  │  └─ UV1-UV4 (Vec2)
  ├─ Triangle indices (u16)
  └─ Material face counts (u16)
Effect System Integration:

Purpose: Attach particle and mesh effects to objects

Data Flow:

CHR File:
  └─ Effect IDs (array of tuples: (motion_id, file_index))

ZSC File:
  └─ Effects (array of paths to effect files)



##### Vehicle Assembly Pipeline:

**Purpose**: Assemble vehicle models with skeleton and parts

**Data Flow**:

Vehicle Item Database:
└─ Vehicle item data
├─ Vehicle type (Cart/CastleGear)
├─ Base motion index
├─ Base avatar motion index
└─ Dummy effect file IDs

Client-Side Rendering Integration Summary:

The Rose Offline client integrates all file types through a hierarchical asset loading system:

  1. VFS Layer: Unified file access across packed archives and host filesystem
  2. Asset Server: Asynchronous loading and caching of assets
  3. ECS Components:
  • SkinnedMesh: Bone binding data (inverse bind matrices, joint entities)
  • CharacterModel: Character parts and animation handles
  • NpcModel: NPC-specific data
  • VehicleModel: Vehicle-specific data
  • ObjectMaterial: Material properties and textures
  1. Systems: Update bone poses, apply animations, render skinned meshes

Coordinate Transformations Applied in Client:

The Rose Online client applies coordinate transformations to convert from the game’s internal coordinate system to the rendering engine’s coordinate system:

  • Position Transformation: (x, y, z) → (x, z, -y) / 100.0

    • Swaps Y and Z axes
    • Negates the new Y (which was the original Z)
    • Divides by 100 to convert from centimeters to meters
  • Rotation Transformation: (w, x, y, z) → (x, z, -y, w)

    • Reorders quaternion components to match the new coordinate system
    • Applies the same Y-Z swap and negation as positions
  • Scale Factor: 100.0 (cm to m conversion)

    • Applied to all position data during loading

2. Core Systems

2.1 Virtual Filesystem (VFS) Abstraction Layer

Architecture Overview:

The VFS provides a unified interface for accessing game assets across multiple storage devices (packed archives, host filesystem).

Key Components:

The VFS is implemented as a C++ class hierarchy with protocol-based file access:

// Base VFS class
class zz_vfs {
public:
    typedef enum {
        ZZ_VFS_READ = (1 << 0),
        ZZ_VFS_WRITE = (1 << 1),
    } zz_vfs_mode;

    typedef enum {
        ZZ_VFS_LOCAL = 0,
        ZZ_VFS_HTTP,
        ZZ_VFS_FTP,
        ZZ_VFS_ZIP,
        ZZ_VFS_MEM,
        ZZ_VFS_PKG,  // TriggerVFS package file system (default)
    } zz_vfs_protocol;

    virtual bool open(const char* filename, zz_vfs_mode mode = ZZ_VFS_READ);
    virtual bool close(void);
    virtual uint32 read(char* buf, uint32 size);
    // ... other methods
};

// Derived filesystem implementations
class zz_vfs_local : public zz_vfs { /* Local filesystem */ };
class zz_vfs_pkg : public zz_vfs { /* Package archive filesystem */ };

Path Normalization:

All paths are normalized to:

  • Forward slashes (/)
  • Uppercase
  • No leading/trailing whitespace
  • No double slashes

Example: 3DDATA\AVATAR\MALE.ZMD3DDATA/AVATAR/MALE.ZMD

File Loading Mechanism:

// Open and read a file
zz_vfs file;
if (file.open("3DDATA/MODELS/CHARACTER.ZMS", zz_vfs::ZZ_VFS_READ)) {
    uint32 size = file.get_size();
    char* data = new char[size];
    file.read(data, size);
    // Process data...
    file.close();
}

// Using package filesystem directly
zz_vfs_pkg pkg_file;
if (pkg_file.open("DATA/ZONES/01.ZON")) {
    // Read file data from package
    uint32 size = pkg_file.get_size();
    // ...
}

Caching Strategy:

  • Assets are loaded on-demand
  • VFS devices are queried in order
  • First successful read wins
  • No built-in caching (handled by application layer)

2.2 Coordinate System Conversions

Rose Online Coordinate System:

  • Units: Centimeters (cm)
  • Up Axis: Y-up
  • Handedness: Right-handed
  • Forward: -Z direction
  • Right: +X direction

Coordinate System Mappings:

Rose Online Axis Notes
+X Same direction
+Y Client rotates Y to Z
+Z Client inverts Z to -Y

2.3 Bone Binding Matrices and Skinning

Bone Hierarchy Structure:

Bones form a tree structure where each bone has a parent index. The root bone is identified when a bone’s parent index equals its own index (parent_id == bone_index).

struct ZmdBone {
    uint32 parent_id;       // Parent bone index (parent_id == bone_index for root)
    char   name[];          // Null-terminated bone name
    float  position[3];     // X, Y, Z (cm, scaled to meters on load)
    float  rotation[4];     // W, X, Y, Z (quaternion)
};

Bone-to-Vertex Binding System:

Each vertex can be bound to up to 4 bones with weights.

// Vertex bone binding data
float bone_weights[4];    // Weight for each of 4 bones
uint16_t bone_indices[4]; // Bone indices (mapped to skeleton)

Weight Normalization: The 4 weights should sum to 1.0 for proper skinning.

Skinning Transformation Pipeline:

  1. Bind Pose: Bone positions at rest (from ZMD)
  2. Inverse Bind Matrix: Pre-computed for each bone
  3. Current Pose: Bone transformation from animation (ZMO)
  4. Skinning Matrix: CurrentPose × InverseBindMatrix
  5. Vertex Transformation: Σ (Weight_i × SkinningMatrix_i × VertexPosition)

Bone Pose Matrices and Bone Spaces:

Bone Spaces:

  • Local Space: Position/rotation relative to parent
  • Object Space: Position/rotation relative to mesh origin
  • World Space: Position/rotation in game world

2.4 Animation Track Interpolation

Frame-Based Animation System:

Animations are stored as discrete frames with fixed FPS.

class ZmoFile {
public:
    uint32 fps;              // Frames per second (typically 30)
    uint32 num_frames;       // Total frame count
    std::vector<std::pair<uint32, ZmoChannel>> channels;  // (bone_index, channel_data)
    std::vector<uint16> frame_events;  // Frame event triggers
    uint32 interpolation_interval_ms;  // For 3ZMO format (0 if not used)
};

Channel Types and Data Structures:

Channel Type Value Data Type Purpose
Empty 1 No animation data
Position 2 Vec3 Bone position
Rotation 4 Quat4 Bone rotation (WXYZ)
Normal 8 Vec3 Vertex normal
Alpha 16 f32 Transparency
UV1 32 Vec2 First UV set
UV2 64 Vec2 Second UV set
UV3 128 Vec2 Third UV set
UV4 256 Vec2 Fourth UV set
Texture 512 f32 Texture animation
Scale 1024 f32 Scale factor

Frame Events and Interpolation Intervals:

Frame Events:

  • Triggered at specific frames
  • Used for sound effects, particle spawns, etc.
  • Event IDs: 10, 20-28, 56-57, 66-67 (attack events)

3ZMO Extended Format:

  • Includes interpolation_interval_ms field
  • Specifies custom interpolation timing
  • Allows for variable frame rates

2.5 Material System

Material Properties and Flags:

class ZscMaterial {
public:
    std::string path;              // Material file path
    bool is_skin;                  // Skin/shader type
    bool alpha_enabled;            // Transparency enabled
    bool two_sided;                // Double-sided rendering
    float alpha_test;              // Alpha test threshold (0.0 if disabled)
    bool z_write_enabled;          // Depth write
    bool z_test_enabled;           // Depth test
    ZscMaterialBlend blend_mode;   // Blending mode
    bool specular_enabled;         // Specular highlights
    float alpha;                   // Global alpha
    ZscMaterialGlow glow;          // Glow effect
};

Blend Modes and Transparency:

Blend Mode Value Description
Normal 0 Standard alpha blending
Lighten 1 Additive blending

Alpha Test:

  • Threshold value (0-1) for pixel discard
  • Typical value: alpha_ref / 256.0
  • Pixels below threshold are discarded

Glow Effects and Shader Integration:

enum ZscMaterialGlowType {
    GLOW_NONE = 0,
    GLOW_SIMPLE = 2,
    GLOW_LIGHT = 3,
    GLOW_TEXTURE_LIGHT = 4,
    GLOW_ALPHA = 5
};

class ZscMaterialGlow {
public:
    ZscMaterialGlowType type;
    Vec3 color;  // Glow color (R, G, B)
};

Texture Referencing and UV Mapping:

UV Channels:

  • ZMS supports up to 4 UV sets (UV1-UV4)
  • UV1 is primary texture coordinates
  • UV2-UV4 used for multi-texturing or effects

Texture Paths:

  • Stored in ZSC material definitions
  • Resolved through VFS
  • File extensions: .dds, .tga, .jpg

2.6 Vertex Format Flags and Data Layout

ZmsFormatFlags Bitfield Structure:

enum ZmsFormatFlags {
    ZZ_VF_POSITION   = (1 << 1),  // 0x0002
    ZZ_VF_NORMAL     = (1 << 2),  // 0x0004
    ZZ_VF_COLOR      = (1 << 3),  // 0x0008
    ZZ_VF_SKIN       = (1 << 4),  // 0x0010 (bone weights + indices)
    ZZ_VF_TANGENT    = (1 << 6),  // 0x0040
    ZZ_VF_UV1        = (1 << 7),  // 0x0080
    ZZ_VF_UV2        = (1 << 8),  // 0x0100
    ZZ_VF_UV3        = (1 << 9),  // 0x0200
    ZZ_VF_UV4        = (1 << 10)  // 0x0400
};

class zz_vertex_format {
public:
    uint32 flags;
    
    bool use_position() const { return (flags & ZZ_VF_POSITION) != 0; }
    bool use_normal() const { return (flags & ZZ_VF_NORMAL) != 0; }
    bool use_color() const { return (flags & ZZ_VF_COLOR) != 0; }
    bool use_skin() const { return (flags & ZZ_VF_SKIN) != 0; }
    bool use_tangent() const { return (flags & ZZ_VF_TANGENT) != 0; }
    int get_num_mapchannel() const;
};

Vertex Attribute Variations:

Version 5/6 (Legacy):

  • Each vertex preceded by 32-bit vertex ID
  • Bone indices stored as 32-bit, mapped to 16-bit
  • Position scaled by 100.0 (cm to m conversion)

Version 7/8 (Modern):

  • No vertex IDs
  • Bone indices stored as 16-bit directly
  • No scaling (already in correct units)

Position, Normal, Color, Bone Weights:

class zz_mesh {
public:
    std::vector<Vec3> positions;      // X, Y, Z position
    std::vector<Vec3> normals;        // X, Y, Z normal vector
    std::vector<zz_color> colors;     // R, G, B, A vertex color
    std::vector<Vec4> bone_weights;   // Weight for 4 bones (W1, W2, W3, W4)
    std::vector<uivec4> bone_indices; // Bone index mapping (I1, I2, I3, I4)
    
    void set_pos(int index, const Vec3& pos);
    void set_normal(int index, const Vec3& normal);
    void set_color(int index, const zz_color& color);
};

UV Coordinate Sets (UV1-4):

class zz_mesh {
public:
    std::vector<Vec2> uv1;  // Primary UV coordinates
    std::vector<Vec2> uv2;  // Secondary UV coordinates
    std::vector<Vec2> uv3;  // Tertiary UV coordinates
    std::vector<Vec2> uv4;  // Quaternary UV coordinates
};

Tangent and Binormal Data:

class zz_mesh {
public:
    std::vector<Vec3> tangents;  // Tangent vector for normal mapping
};

Note: Binormals can be computed from tangent and normal:

Vec3 binormal = normal.cross(tangent);

Material Face Mapping:

class zz_mesh {
public:
    std::vector<uint16> material_num_faces;  // Face count per material
};

Faces are grouped by material index for efficient rendering.


3. File Format Specifications

3.1 ZMS Mesh Files (.zms)

File Header and Magic Number:

Offset  Type    Description
0x00    char[7]  Magic string ("ZMS0005", "ZMS0006", "ZMS0007", "ZMS0008")

Version Detection:

Magic String Version Notes
ZMS0005 5 Legacy format
ZMS0006 6 Legacy format with material mapping
ZMS0007 7 Modern format
ZMS0008 8 Modern format with strip indices

Binary Layout and Data Structures:

Version 5/6 Layout:

Offset  Type    Description
0x00    char[7]  Magic string
0x07    u32      Format flags (ZmsFormatFlags)
0x0B    Vec3     Bounding box minimum
0x17    Vec3     Bounding box maximum
0x23    u32      Bone count
0x27    u32[]    Bone mapping (bone_count × 4 bytes)
0x??    u32      Vertex count
0x??    Vertex[]  Vertex data (based on format flags)
0x??    u32      Triangle count
0x??    u16[]    Index data (triangle_count × 3)
0x??    u32      Material ID count (version 6+)
0x??    u16[]    Material face counts

Version 7/8 Layout:

Offset  Type    Description
0x00    char[7]  Magic string
0x07    u32      Format flags (ZmsFormatFlags)
0x0B    Vec3     Bounding box minimum
0x17    Vec3     Bounding box maximum
0x23    u16      Bone count
0x25    u16[]    Bone indices (bone_count × 2 bytes)
0x??    u16      Vertex count
0x??    Vertex[]  Vertex data (based on format flags)
0x??    u16      Triangle count
0x??    u16[]    Index data (triangle_count × 3)
0x??    u16      Material ID count
0x??    u16[]    Material face counts
0x??    u16      Strip index count (version 8)
0x??    u16[]    Strip indices
0x??    u16      Pool type (version 8)

Vertex Data Arrays:

Vertex Data Structure (per vertex):

struct Vertex {
    // Present if POSITION flag set
    float position[3];  // X, Y, Z

    // Present if NORMAL flag set
    float normal[3];    // X, Y, Z

    // Present if COLOR flag set
    float color[4];     // R, G, B, A

    // Present if BONE_WEIGHT and BONE_INDEX flags set
    float bone_weight[4];   // W1, W2, W3, W4
    uint32 bone_index[4];   // I1, I2, I3, I4 (v5/6)
    uint16 bone_index[4];   // I1, I2, I3, I4 (v7/8)

    // Present if TANGENT flag set
    float tangent[3];   // X, Y, Z

    // Present if UV1 flag set
    float uv1[2];      // U, V

    // Present if UV2 flag set
    float uv2[2];      // U, V

    // Present if UV3 flag set
    float uv3[2];      // U, V

    // Present if UV4 flag set
    float uv4[2];      // U, V
};

Version 5/6: Each vertex preceded by 32-bit vertex ID.

Material Face Mapping:

std::vector<uint16_t> material_num_faces;
  • Each entry specifies number of triangles for a material
  • Faces are grouped contiguously by material
  • Used for efficient multi-material rendering

Bone Index Mapping:

Version 5/6:

uint32_t bone_count = reader.read_u32();
std::vector<uint16_t> bones;
bones.reserve(bone_count);
for (uint32_t i = 0; i < bone_count; i++) {
    reader.read_u32();  // Unused
    bones.push_back(static_cast<uint16_t>(reader.read_u32()));
}

Bone indices in vertices are 32-bit, mapped through this table to 16-bit.

Version 7/8:

uint16_t bone_count = reader.read_u16();
std::vector<uint16_t> bones;
bones.reserve(bone_count);
for (uint16_t i = 0; i < bone_count; i++) {
    bones.push_back(reader.read_u16());
}

Bone indices in vertices are already 16-bit, no mapping needed.

Vertex Attribute Flags:

See Section 2.6 for flag definitions.

Scaling Factors:

Version 5/6: Position data is scaled by 100.0 (cm to m conversion).

for [x, y, z] in position.iter_mut() {
    *x /= 100.0;
    *y /= 100.0;
    *z /= 100.0;
}

Version 7/8: No scaling applied.

Parsing Algorithms and Edge Cases:

  1. Invalid Bone Index: Throw error if bone index >= bone_count
  2. Missing Attributes: Only read attributes present in format flags
  3. Empty Mesh: Handle vertex_count = 0 gracefully
  4. Material Count: May be 0 (single material mesh)

3.2 ZMD Skeleton Files (.zmd)

File Header and Magic Number:

Offset  Type    Description
0x00    char[7]  Magic string ("ZMD0002" or "ZMD0003")

Version Detection:

Magic String Version Notes
ZMD0002 2 Legacy format
ZMD0003 3 Modern format with dummy bone rotation

Binary Layout and Data Structures:

Offset  Type    Description
0x00    char[7]  Magic string
0x07    u32      Bone count
0x0B    Bone[]   Bone data
0x??    u32      Dummy bone count
0x??    DummyBone[] Dummy bone data

Bone Count and Hierarchy:

uint32_t bone_count = reader.read_u32();
std::vector<ZmdBone> bones;
bones.reserve(bone_count);

Bone Data Structure:

struct ZmdBone {
    uint32 parent_id;       // Parent bone index (parent_id == bone_index for root bone)
    char   name[];          // Null-terminated bone name
    float  position[3];     // X, Y, Z (cm, scaled to meters on load)
    float  rotation[4];     // W, X, Y, Z (quaternion)
};

Bone Name and ID Mapping:

Bone names are stored in the ZMD file but are not typically used directly in rendering. The bone ID (array index) is the primary reference mechanism used throughout the asset pipeline.

Name Storage Format:

Bone Name Storage:
  - Null-terminated ASCII string
  - Variable length (typically 1-32 characters)
  - Common naming conventions:
    - "Bip01" - Root bone
    - "Bip01 Spine" - Spine bone
    - "Bip01 L Hand" - Left hand
    - "Dummy_Weapon" - Weapon attachment point

Bone ID System:

Bone Index Mapping:
  ┌─────────┬──────────────────┬─────────────────┐
  │ ID      │ Bone Name        │ Parent ID       │
  ├─────────┼──────────────────┼─────────────────┤
  │ 0       │ Bip01            │ -1 (root)       │
  │ 1       │ Bip01 Pelvis     │ 0               │
  │ 2       │ Bip01 Spine      │ 1               │
  │ 3       │ Bip01 Spine1     │ 2               │
  │ 4       │ Bip01 Neck       │ 3               │
  │ 5       │ Bip01 Head       │ 4               │
  │ ...     │ ...              │ ...             │
  └─────────┴──────────────────┴─────────────────┘

Name-Based Lookup (Optional):

class ZmdSkeleton {
public:
    std::vector<ZmdBone> bones;
    std::map<std::string, uint32> bone_name_to_id;
    
    const ZmdBone* get_bone_by_name(const char* name) const {
        auto it = bone_name_to_id.find(name);
        if (it != bone_name_to_id.end()) {
            return &bones[it->second];
        }
        return nullptr;
    }
    
    int get_bone_id(const char* name) const {
        auto it = bone_name_to_id.find(name);
        if (it != bone_name_to_id.end()) {
            return it->second;
        }
        return -1;
    }
};

Cross-File Bone References:

Bone references in other files:
  - ZMS: bone_indices array references bone IDs
  - ZMO: channel bone_index references bone IDs
  - ZSC: bone_index field references bone IDs
  
All use the same bone ID from the ZMD skeleton.

Dummy Bone Support:

Dummy bones are special attachment points that don’t influence vertices directly but serve as mounts for weapons, effects, and other attachable objects.

Dummy Bone Purpose:

Attachment Point Types:
  ┌────────────────────┬─────────────────────────┐
  │ Dummy Name         │ Purpose                 │
  ├────────────────────┼─────────────────────────┤
  │ Dummy_Weapon_R     │ Right-hand weapon       │
  │ Dummy_Weapon_L     │ Left-hand weapon/shield │
  │ Dummy_Effect_Foot  │ Footstep effects        │
  │ Dummy_Effect_Hand  │ Skill effects           │
  │ Dummy_Head         │ Head attachments        │
  │ Dummy_Back         │ Backpack/wings          │
  │ Dummy_Mount        │ Vehicle mount point     │
  └────────────────────┴─────────────────────────┘

Dummy Bone Data Structure:

struct DummyBone {
    char   name[];      // Null-terminated name
    uint32 parent;      // Parent bone index
    float  position[3];  // X, Y, Z (cm)
    float  rotation[4];  // W, X, Y, Z (version 3 only)
};

Version Differences:

Version 2 (ZMD0002):
  - Dummy bones have identity rotation (0, 0, 0, 1)
  - Rotation not stored in file
  - Must use parent bone orientation

Version 3 (ZMD0003):
  - Full rotation quaternion stored
  - Allows arbitrary dummy bone orientation
  - Better control over attachment alignment

Dummy Bone Runtime Handling:

class DummyBone {
public:
    std::string name;
    uint32 parent_bone;
    Vec3 local_position;
    Quat local_rotation;  // Identity for v2, read from file for v3
    
    Transform get_world_transform(const Skeleton& skeleton) const {
        Transform parent_transform = skeleton.get_bone_world_transform(parent_bone);
        return parent_transform * Transform::from_pos_rot(local_position, local_rotation);
    }
    
    void attach_object(AttachedObject& object, const Skeleton& skeleton) const {
        Transform world_transform = get_world_transform(skeleton);
        object.set_transform(world_transform);
    }
};

Common Attachment Workflow:

1. Load ZMD skeleton with dummy bones
2. For each ZSC part with dummy_index > 0:
   a. Get dummy bone by index
   b. Compute world transform of dummy
   c. Attach mesh/effect to dummy transform
3. Update dummy transforms each frame:
   a. Traverse skeleton hierarchy
   b. Compute bone world matrices
   c. Apply dummy local transform
   d. Update attached objects

Pose Transformation Matrices:

Pose transformation matrices are not stored directly in the ZMD file. They are computed at runtime from the bone hierarchy data (parent indices, positions, rotations).

Matrix Types:

Transform Matrix Categories:
  ┌─────────────────────┬─────────────────────────────────┐
  │ Matrix Type         │ Description                     │
  ├─────────────────────┼─────────────────────────────────┤
  │ Local Matrix        │ Transform relative to parent    │
  │ Object/Bind Matrix  │ Transform in object space       │
  │ World Matrix        │ Transform in world space        │
  │ Inverse Bind Matrix │ Object space to bone space      │
  │ Skin Matrix         │ Current pose × inverse bind     │
  └─────────────────────┴─────────────────────────────────┘

Local Matrix Computation:

Mat4 compute_local_matrix(const ZmdBone& bone) {
    Mat4 translation = Mat4::from_translation(bone.position);
    Mat4 rotation = Mat4::from_quat(bone.rotation);
    return translation * rotation;
}

Object Space Matrix Computation:

void compute_object_space_matrices(const std::vector<ZmdBone>& bones, std::vector<Mat4>& matrices) {
    matrices.resize(bones.size());
    
    // Build matrices in order (parents before children)
    for (int i = 0; i < bones.size(); i++) {
        Mat4 local = compute_local_matrix(bones[i]);
        
        if (bones[i].parent_id == i) {  // Root bone: parent_id equals bone index
            matrices[i] = local;
        } else {
            matrices[i] = matrices[bones[i].parent_id] * local;
        }
    }
}

Inverse Bind Matrix:

void compute_inverse_bind_matrices(
    const std::vector<Mat4>& object_space,
    std::vector<Mat4>& inverse_bind_matrices
) {
    inverse_bind_matrices.resize(object_space.size());
    for (int i = 0; i < object_space.size(); i++) {
        inverse_bind_matrices[i] = object_space[i].inverse();
    }
}

Skinning Matrix (for GPU):

void compute_skinning_matrices(
    const std::vector<Mat4>& current_pose,  // Current animation pose in object space
    const std::vector<Mat4>& inverse_bind,  // Inverse bind pose matrices
    std::vector<Mat4>& skinning_matrices    // Output skinning matrices
) {
    skinning_matrices.resize(current_pose.size());
    for (int i = 0; i < current_pose.size(); i++) {
        skinning_matrices[i] = current_pose[i] * inverse_bind[i];
    }
}

Matrix Storage Optimization:

Runtime Storage:
  - Inverse bind matrices: Computed once at load time
  - Current pose matrices: Computed each animation frame
  - Skinning matrices: Computed each frame for GPU upload
  
Memory Layout (typical skeleton):
  - 100 bones × 64 bytes/matrix = 6.4 KB per matrix array
  - 3 arrays needed = ~20 KB per skeleton

Bone-to-Object Binding:

Bones in the ZMD file are stored in local space (relative to their parent bone). Object space matrices must be computed by traversing the bone hierarchy from root to each bone.

Coordinate Space Definitions:

Bone Coordinate Spaces:
  ┌─────────────────┬─────────────────────────────────────┐
  │ Space           │ Description                         │
  ├─────────────────┼─────────────────────────────────────┤
  │ Local Space     │ Position/rotation relative to parent│
  │ Object Space    │ Position/rotation from mesh origin  │
  │ Bone Space      │ Coordinates within bone's local frame│
  │ World Space     │ Position in game world coordinates  │
  └─────────────────┴─────────────────────────────────────┘

Hierarchy Traversal:

Bone Hierarchy Example:
  Bip01 (root)
    └── Bip01 Pelvis
          ├── Bip01 L Thigh
          │     └── Bip01 L Calf
          │           └── Bip01 L Foot
          └── Bip01 Spine
                └── Bip01 Spine1
                      └── Bip01 Neck
                            └── Bip01 Head

Matrix Accumulation:
  ObjectSpace(Head) = ObjectSpace(Root)
                    × Local(Pelvis)
                    × Local(Spine)
                    × Local(Spine1)
                    × Local(Neck)
                    × Local(Head)

Vertex Binding Process:

1. Vertex stored in bind pose (object space)
2. Bone indices and weights define influence
3. Skinning formula:
   FinalPos = Σ(weight_i × SkinMatrix_i × VertexPos)
   
Where:
   SkinMatrix_i = CurrentPose_i × InverseBind_i

Code Implementation:

struct BoneBinding {
    uint16 bone_indices[4];   // Up to 4 influencing bones
    float  bone_weights[4];   // Weights summing to 1.0
};

Vec3 skin_vertex(
    const Vec3& vertex,
    const BoneBinding& binding,
    const std::vector<Mat4>& skinning_matrices
) {
    Vec3 result = Vec3::ZERO;
    
    for (int i = 0; i < 4; i++) {
        float weight = binding.bone_weights[i];
        if (weight > 0.0f) {
            int bone_idx = binding.bone_indices[i];
            const Mat4& skin_matrix = skinning_matrices[bone_idx];
            result += skin_matrix.transform_point(vertex) * weight;
        }
    }
    
    return result;
}

Scaling from cm to m:

Bone positions in ZMD files are stored in centimeters (cm). Conversion to meters requires scaling by 0.01 for use in most rendering engines.

Scaling Conversion:

Position Scaling:
  - Storage: Centimeters (cm)
  - Rendering: Meters (m)
  - Conversion: meters = centimeters × 0.01

Example:
  - Stored position: (150.0, 100.0, 50.0) cm
  - Converted position: (1.5, 1.0, 0.5) m

Scaling Application Points:

When to Apply Scaling:
  ┌─────────────────────┬───────────────────────────────┐
  │ Location            │ Apply Scaling?                │
  ├─────────────────────┼───────────────────────────────┤
  │ ZMD bone position   │ Yes, at load time             │
  │ ZMS vertex position │ Yes (v5/6) / No (v7/8)        │
  │ ZMO position keys   │ Yes, at load time             │
  │ IFO object position │ Yes, at load time             │
  │ HIM height values   │ Yes, at load time             │
  └─────────────────────┴───────────────────────────────┘

Code Implementation:

const float CM_TO_M = 0.01f;

bool load_zmd_bone(zz_vfs& file, ZmdBone& bone) {
    // Read parent bone index
    file.read_uint32(bone.parent_id);
    
    // Read bone name (null-terminated string)
    char name[ZZ_MAX_STRING];
    file.read_string(name);
    bone.name = name;
    
    // Read position in cm, convert to meters
    float pos[3];
    file.read_float3(pos);
    bone.position.x = pos[0] * CM_TO_M;
    bone.position.y = pos[1] * CM_TO_M;
    bone.position.z = pos[2] * CM_TO_M;
    
    // Read rotation (quaternion: W, X, Y, Z)
    file.read_float(bone.rotation.w);
    file.read_float(bone.rotation.x);
    file.read_float(bone.rotation.y);
    file.read_float(bone.rotation.z);
    
    return true;
}

Consistency Requirements:

Important: All position data must use consistent units!

If skeleton uses meters:
  - Vertex positions: meters
  - Animation positions: meters
  - Object positions: meters
  
If skeleton uses centimeters:
  - All positions: centimeters
  - Convert at final rendering stage only

3.3 ZMO Animation Files (.zmo)

File Header and Magic Number:

Offset  Type    Description
0x00    char[7]  Magic string ("ZMO0002")

Channel Definitions and Types:

struct ChannelDef {
    uint32 channel_type;   // 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024
    uint32 bone_index;    // Target bone index
};

Frame Data Structures:

Frame data is stored per channel, not per frame.

Channel 0:
  Frame 0: value
  Frame 1: value
  ...
  Frame N: value

Channel 1:
  Frame 0: value
  ...

Extended Format Support (EZMO/3ZMO):

Extended data at end of file:

End-4:  char[4]  Magic ("EZMO" or "3ZMO")
End-8:  u32      Extended data offset
Offset: u16      Frame event count
Offset+2: u16[]   Frame events (event_count)
...

3ZMO Only:

  • After frame events: u32 interpolation_interval_ms

Frame Events and Interpolation Intervals:

Frame Events:

std::vector<uint16_t> frame_events;

Triggered at specific frames. Attack events: 10, 20-28, 56-57, 66-67.

Interpolation Interval:

std::optional<uint32_t> interpolation_interval_ms;

Custom interpolation timing for 3ZMO format.

Channel Interpolation Methods:

  • Position: Linear interpolation
  • Rotation: Quaternion Slerp
  • UV/Scale/Alpha: Linear interpolation

3.4 ZON Zone Files (.zon)

Block-Based File Structure:

Offset  Type    Description
0x00    u32      Block count
0x04    BlockHeader[] Block headers (count × 8 bytes)
...
Block Data...

Block Header:

struct BlockHeader {
    uint32 block_type;   // Block type enum
    uint32 block_offset;  // Offset to block data
};

Block Type Enumeration:

enum BlockType {
    ZoneInfo = 0,
    EventPositions = 1,
    Textures = 2,
    Tiles = 3,
    Economy = 4,
};

ZoneInfo Block – Grid Configuration:

Offset  Type    Description
0x00    u32[3]   Unknown (12 bytes)
0x0C    u32      Grid per patch
0x10    f32      Grid size
0x14    u32[2]   Unknown (8 bytes)

EventPositions Block – Spawn Points:

struct EventPosition {
    float  position[3];  // X, Y, Z
    uint8  name_len;     // Name length
    char   name[name_len]; // Event name
};

Textures Block – Texture References:

struct TextureRef {
    uint8  path_len;     // Path length
    char   path[path_len]; // Texture path
};

Tiles Block – Tile Composition:

struct ZonTile {
    uint32 layer1;       // Primary layer texture index
    uint32 layer2;       // Secondary layer texture index
    uint32 offset1;      // Primary layer offset
    uint32 offset2;      // Secondary layer offset
    uint32 blend;        // Blend flag (0/1)
    uint32 rotation;     // Rotation enum
    uint32 padding;      // 4 bytes padding
};

ZonTile Structure and Layer Composition:

Rotation Enum:

enum class ZonTileRotation : uint8_t {
    Unknown = 0,
    None = 1,
    FlipHorizontal = 2,
    FlipVertical = 3,
    Flip = 4,
    Clockwise90 = 5,
    CounterClockwise90 = 6,
};

Tile Offset and Blend Modes:

Tile offset and blend modes control how textures are applied to terrain tiles, enabling texture variation, UV tiling, and multi-layer blending.

Offset System:

UV Offset Configuration:
  - offset1: UV offset for primary layer (layer1)
  - offset2: UV offset for secondary layer (layer2)
  - Stored as integer, converted to UV space

UV Calculation:
  uv_u = (tile_x + offset_value / 1000.0) * uv_scale
  uv_v = (tile_y + offset_value / 1000.0) * uv_scale

Example:
  - offset1 = 500
  - UV offset = 500 / 1000.0 = 0.5
  - Result: Texture shifted by half a tile

Blend Mode System:

Blend Flag Values:
  - 0: Single layer (layer1 only)
  - 1: Dual layer blending (layer1 + layer2)

Blending Formula:
  final_color = lerp(layer1_color, layer2_color, blend_factor)

Blend Factor Sources:
  - Vertex alpha from terrain mesh
  - Pre-computed blend mask texture
  - Height-based blending

Rotation Transformations:

Rotation Enum Values:
  ┌──────────────────┬─────┬─────────────────────────────┐
  │ Rotation         │ Val │ UV Transform                │
  ├──────────────────┼─────┼─────────────────────────────┤
  │ Unknown          │ 0   │ Identity (no change)        │
  │ None             │ 1   │ Identity (no change)        │
  │ FlipHorizontal   │ 2   │ (1-u, v)                    │
  │ FlipVertical     │ 3   │ (u, 1-v)                    │
  │ Flip             │ 4   │ (1-u, 1-v)                  │
  │ Clockwise90      │ 5   │ (v, 1-u)                    │
  │ CounterClockwise90│ 6  │ (1-v, u)                    │
  └──────────────────┴─────┴─────────────────────────────┘

UV Transformation Code:

Vec2 apply_tile_transform(const Vec2& uv, const ZonTile& tile) {
    float offset = tile.offset1 / 1000.0f;
    Vec2 result = uv + Vec2(offset, offset);
    
    switch (tile.rotation) {
        case ZON_TILE_ROTATION_NONE:
            return result;
        case ZON_TILE_ROTATION_FLIP_HORIZONTAL:
            return Vec2(1.0f - result.x, result.y);
        case ZON_TILE_ROTATION_FLIP_VERTICAL:
            return Vec2(result.x, 1.0f - result.y);
        case ZON_TILE_ROTATION_FLIP:
            return Vec2(1.0f - result.x, 1.0f - result.y);
        case ZON_TILE_ROTATION_CLOCKWISE90:
            return Vec2(result.y, 1.0f - result.x);
        case ZON_TILE_ROTATION_COUNTER_CLOCKWISE90:
            return Vec2(1.0f - result.y, result.x);
        default:
            return result;
    }
}

Practical Usage Examples:

Texture Variation Techniques:
  1. Same texture, different offsets:
     - Tile A: grass.dds, offset=0
     - Tile B: grass.dds, offset=500
     - Result: Same texture, varied UVs (breaks repetition)
     
  2. Layer blending for transitions:
     - Tile: layer1=grass, layer2=dirt, blend=1
     - Result: Smooth transition between grass and dirt
     
  3. Rotation for variety:
     - 4 adjacent tiles with same texture
     - Apply different rotations to each
     - Result: Non-repeating pattern from single texture

Spatial Partitioning and Grid System:

The zone is divided into a grid of tiles where each tile references textures from the Textures block. This spatial partitioning enables efficient terrain rendering and culling.

Grid System Architecture:

Zone Grid Structure:
  ┌─────────────────────────────────────────────────┐
  │ Zone (entire map area)                          │
  │ ┌─────────────────────────────────────────────┐ │
  │ │ Tile Grid (width × height tiles)            │ │
  │ │ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐      │ │
  │ │ │0,0│1,0│2,0│...│   │   │   │   │W-1│      │ │
  │ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤      │ │
  │ │ │0,1│1,1│2,1│...│   │   │   │   │   │      │ │
  │ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤      │ │
  │ │ │...│...│...│...│   │   │   │   │   │      │ │
  │ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤      │ │
  │ │ │0,H│...│...│...│   │   │   │   │W-1│      │ │
  │ │ └───┴───┴───┴───┴───┴───┴───┴───┴H-1┘      │ │
  │ └─────────────────────────────────────────────┘ │
  └─────────────────────────────────────────────────┘

Grid Configuration from ZoneInfo:

ZoneInfo Block Parameters:
  - Grid per patch: Grid cells per terrain patch (typically 1)
  - Grid size (patch_size): World units per grid cell (e.g., 100.0 cm)

Calculated Values:
  - Total world width = grid_width × patch_size
  - Total world height = grid_height × patch_size
  - Tile count = grid_width × grid_height

Spatial Queries:

class ZoneGrid {
public:
    uint32 width;
    uint32 height;
    float patch_size;
    std::vector<ZonTile> tiles;
    
    void world_to_grid(float world_x, float world_z, uint32& grid_x, uint32& grid_y) const {
        grid_x = static_cast<uint32>(std::floor(world_x / patch_size));
        grid_y = static_cast<uint32>(std::floor(world_z / patch_size));
        grid_x = std::min(grid_x, width - 1);
        grid_y = std::min(grid_y, height - 1);
    }
    
    void grid_to_world(uint32 grid_x, uint32 grid_y, float& world_x, float& world_z) const {
        world_x = grid_x * patch_size;
        world_z = grid_y * patch_size;
    }
    
    const ZonTile* get_tile(uint32 grid_x, uint32 grid_y) const {
        if (grid_x < width && grid_y < height) {
            return &tiles[grid_y * width + grid_x];
        }
        return nullptr;
    }
    
    std::vector<const ZonTile*> get_tiles_in_radius(float center_x, float center_z, float radius) const {
        uint32 cx, cy;
        world_to_grid(center_x, center_z, cx, cy);
        int r = static_cast<int>(std::ceil(radius / patch_size));
        
        std::vector<const ZonTile*> result;
        for (int dy = -r; dy <= r; dy++) {
            for (int dx = -r; dx <= r; dx++) {
                uint32 x = static_cast<uint32>(static_cast<int>(cx) + dx);
                uint32 y = static_cast<uint32>(static_cast<int>(cy) + dy);
                const ZonTile* tile = get_tile(x, y);
                if (tile) {
                    result.push_back(tile);
                }
            }
        }
        return result;
    }
};

Frustum Culling Integration:

Terrain Rendering Optimization:
  1. Calculate visible grid range from camera frustum
  2. Only render tiles within visible range
  3. Use LOD based on distance from camera

Visible Range Calculation:
  min_x = floor((frustum_min_x) / patch_size)
  max_x = ceil((frustum_max_x) / patch_size)
  min_y = floor((frustum_min_z) / patch_size)
  max_y = ceil((frustum_max_z) / patch_size)

Memory Layout:

Tile Data Storage:
  - ZonTile size: 28 bytes (7 × u32)
  - Typical zone: 64 × 64 = 4096 tiles
  - Memory: 4096 × 28 = 114,688 bytes (~112 KB)
  
Texture Reference Storage:
  - Path length: Variable (typically 20-60 chars)
  - Texture count: 10-50 per zone
  - Memory: ~2-5 KB for texture paths

3.5 ZSC Object Composition Files (.zsc)

Material List Structure:

struct MaterialList {
    uint16 material_count;
    MaterialEntry materials[material_count];
};

ZscMaterial Properties:

struct ZscMaterial {
    char   path[];        // Null-terminated path
    uint16 is_skin;       // 0/1
    uint16 alpha_enabled; // 0/1
    uint16 two_sided;    // 0/1
    uint16 alpha_test_enabled; // 0/1
    uint16 alpha_ref;     // Alpha reference (0-255)
    uint16 z_test_enabled; // 0/1
    uint16 z_write_enabled; // 0/1
    uint16 blend_mode;    // 0=Normal, 1=Lighten
    uint16 specular_enabled; // 0/1
    float  alpha;         // Global alpha
    uint16 glow_type;     // 0=None, 2=Simple, 3=Light, 4=TextureLight, 5=Alpha
    float  glow_color[3]; // R, G, B
};

Object Part Definitions:

struct ObjectPart {
    uint16 mesh_id;      // Mesh index
    uint16 material_id;   // Material index
    // Properties follow (variable length)
};

Mesh and Material References:

The ZSC file maintains three primary reference arrays that define the visual appearance of objects: mesh paths, material definitions, and effect paths.

Mesh List Structure:

Mesh Reference Array:
  ┌─────────────────────────────────────────┐
  │ mesh_count: uint16                       │
  │ mesh_paths: string[]                     │
  └─────────────────────────────────────────┘

Example:
  [0] = "3DDATA/OBJECTS/TREE_A.ZMS"
  [1] = "3DDATA/OBJECTS/TREE_B.ZMS"
  [2] = "3DDATA/OBJECTS/ROCK.ZMS"

Material List Structure:

Material Definition Array:
  ┌─────────────────────────────────────────┐
  │ material_count: uint16                   │
  │ materials: ZscMaterial[]                 │
  └─────────────────────────────────────────┘

Each ZscMaterial contains:
  - Texture path
  - Blend mode
  - Alpha settings
  - Glow properties

Effect List Structure:

Effect Reference Array:
  ┌─────────────────────────────────────────┐
  │ effect_count: uint16                     │
  │ effect_paths: string[]                   │
  └─────────────────────────────────────────┘

Example:
  [0] = "EFFECT/FIRE_TORCH.EFT"
  [1] = "EFFECT/SMOKE.EFT"

Reference Resolution:

class ZscFile {
public:
    std::vector<std::string> meshes;
    std::vector<ZscMaterial> materials;
    std::vector<std::string> effects;
    std::vector<ZscObject> objects;
    
    const std::string* get_part_mesh(const ZscObjectPart& part) const {
        if (part.mesh_id < meshes.size()) {
            return &meshes[part.mesh_id];
        }
        return nullptr;
    }
    
    const ZscMaterial* get_part_material(const ZscObjectPart& part) const {
        if (part.material_id < materials.size()) {
            return &materials[part.material_id];
        }
        return nullptr;
    }
};

Part Transforms (Position, Rotation, Scale):

Each object part has its own transform that positions it relative to the object origin or parent part. This enables complex multi-part objects with articulated structures.

Transform Data Structure:

struct Transform {
    float position[3];   // X, Y, Z in centimeters
    float rotation[4];   // W, X, Y, Z (quaternion)
    float scale[3];      // X, Y, Z scale factors
};

Transform Application Order:

Final Vertex Position:
  1. Apply scale:     v' = v * scale
  2. Apply rotation:  v'' = rotation * v'
  3. Apply position:  v''' = v'' + position

Matrix Form:
  transform_matrix = T * R * S
  where:
    T = translation matrix
    R = rotation matrix (from quaternion)
    S = scale matrix

Common Transform Examples:

Object Part Transform Examples:
  ┌─────────────────┬────────────────────┬──────────────────────┐
  │ Part            │ Position           │ Rotation             │
  ├─────────────────┼────────────────────┼──────────────────────┤
  │ Door (closed)   │ (0, 0, 0)          │ (1, 0, 0, 0)         │
  │ Door (open 90°) │ (0, 0, 0)          │ (0.707, 0, 0.707, 0) │
  │ Wheel rotating  │ (0, 50, 0)         │ (varies)             │
  │ Weapon mount    │ (10, 0, 5)         │ (1, 0, 0, 0)         │
  └─────────────────┴────────────────────┴──────────────────────┘

Transform Code Implementation:

Mat4 compute_transform_matrix(const Transform& t) {
    Mat4 translation = Mat4::from_translation(t.position);
    Mat4 rotation = Mat4::from_quat(t.rotation);
    Mat4 scale = Mat4::from_scale(t.scale);
    return translation * rotation * scale;
}

Vec3 apply_transform(const Vec3& vertex, const Transform& t) {
    Vec3 scaled = vertex * t.scale;
    Vec3 rotated = t.rotation * scaled;
    return rotated + t.position;
}

Bone Index and Dummy Bone Binding:

Parts can be attached to skeleton bones or dummy bones for skinned objects. This enables weapons, armor, and other equipment to follow character animations.

Bone Binding Structure:

struct BoneBinding {
    uint16 bone_index;    // Regular bone index (0xFFFF = none)
    uint16 dummy_index;   // Dummy bone index (0xFFFF = none)
};

Binding Priority:

Attachment Logic:
  1. If dummy_index != 0xFFFF:
     - Attach to dummy bone
  2. Else if bone_index != 0xFFFF:
     - Attach to regular bone
  3. Else:
     - Use part transform (static attachment)

Common Binding Configurations:

┌─────────────────┬─────────────┬─────────────┬────────────────┐
│ Part Type       │ bone_index  │ dummy_index │ Behavior       │
├─────────────────┼─────────────┼─────────────┼────────────────┤
│ Body armor      │ spine_index │ 0xFFFF      │ Follows spine  │
│ Right weapon    │ 0xFFFF      │ weapon_r    │ Follows dummy  │
│ Left shield     │ 0xFFFF      │ weapon_l    │ Follows dummy  │
│ Helmet          │ head_index  │ 0xFFFF      │ Follows head   │
│ Backpack        │ 0xFFFF      │ back_dummy  │ Follows dummy  │
│ Static deco     │ 0xFFFF      │ 0xFFFF      │ No bone binding│
└─────────────────┴─────────────┴─────────────┴────────────────┘

Runtime Attachment Code:

Mat4 attach_part_to_skeleton(
    ZscObjectPart& part,
    const Skeleton& skeleton,
    const Mat4& parent_transform
) {
    Mat4 bone_transform;
    
    if (part.dummy_index != 0xFFFF) {
        // Attach to dummy bone
        bone_transform = skeleton.get_dummy_transform(part.dummy_index);
    } else if (part.bone_index != 0xFFFF) {
        // Attach to regular bone
        bone_transform = skeleton.get_bone_transform(part.bone_index);
    } else {
        // No bone binding, use part transform
        bone_transform = Mat4::IDENTITY;
    }
    
    Mat4 part_transform = compute_transform_matrix(part.transform);
    return parent_transform * bone_transform * part_transform;
}

Parent-Child Relationships:

Parts can form parent-child hierarchies, allowing complex articulated objects where child parts inherit transforms from their parents.

Parent Reference Structure:

struct ParentRef {
    uint16 parent_id;    // Parent part index (0 = none)
};

Parent ID Interpretation:

Parent ID System:
  - 0: No parent (root part)
  - 1-N: Parent is part at index (parent_id - 1)
  
Note: Parent IDs are 1-indexed!
      Array index = parent_id - 1

Hierarchy Example:

Object Part Hierarchy:
  Part 0 (Torso)      - parent_id = 0 (root)
    Part 1 (Head)     - parent_id = 1 (parent = Part 0)
    Part 2 (L Arm)    - parent_id = 1 (parent = Part 0)
    Part 3 (R Arm)    - parent_id = 1 (parent = Part 0)
      Part 4 (R Hand) - parent_id = 4 (parent = Part 3)
        Part 5 (Weapon) - parent_id = 5 (parent = Part 4)

Transform Inheritance:

void compute_hierarchy_transforms(
    const std::vector<ZscObjectPart>& parts,
    const Mat4& base_transform,
    std::vector<Mat4>& transforms
) {
    transforms.resize(parts.size());
    
    // Process in order (parents before children)
    for (int i = 0; i < parts.size(); i++) {
        Mat4 local = compute_transform_matrix(parts[i].transform);
        
        if (parts[i].parent_id == 0) {
            transforms[i] = base_transform * local;
        } else {
            int parent_idx = parts[i].parent_id - 1;
            transforms[i] = transforms[parent_idx] * local;
        }
    }
}

Collision Shape and Flags:

Objects can have collision data that defines how they interact with physics, camera, and player movement.

Collision Data Structure:

struct CollisionData {
    uint16 flags;        // Collision flags
    // Bits 0-2: Shape (0=None, 1=Sphere, 2=AABB, 3=OBB, 4=Polygon)
    // Bit 3: NOT_MOVEABLE
    // Bit 4: NOT_PICKABLE
    // Bit 5: HEIGHT_ONLY
    // Bit 6: NOT_CAMERA_COLLISION
    // Bit 7: PASSTHROUGH
};

Collision Shape Types:

┌──────┬─────────────┬─────────────────────────────────────┐
│ Type │ Shape       │ Description                         │
├──────┼─────────────┼─────────────────────────────────────┤
│ 0    │ None        │ No collision                        │
│ 1    │ Sphere      │ Bounding sphere (center + radius)   │
│ 2    │ AABB        │ Axis-aligned bounding box           │
│ 3    │ OBB         │ Oriented bounding box               │
│ 4    │ Polygon     │ Triangle mesh collision             │
└──────┴─────────────┴─────────────────────────────────────┘

Collision Flag Meanings:

Flag Bit Interpretation:
  Bit 3 (NOT_MOVEABLE):     Object cannot be pushed/moved
  Bit 4 (NOT_PICKABLE):     Mouse click-through enabled
  Bit 5 (HEIGHT_ONLY):      Only affects terrain height
  Bit 6 (NOT_CAMERA_COLL):  Camera passes through
  Bit 7 (PASSTHROUGH):      Player can walk through

Common Combinations:
  Building:    Shape=OBB, NOT_MOVEABLE
  Grass:       Shape=None, PASSTHROUGH
  Water:       Shape=None, HEIGHT_ONLY
  Trigger:     Shape=AABB, PASSTHROUGH, NOT_CAMERA_COLL

Physics Integration:

CollisionShape create_collision_shape(const CollisionData& collision, const BoundingBox& bounds) {
    int shape_type = collision.flags & 0x7;
    
    switch (shape_type) {
        case 1:
            return CollisionShape::Sphere(bounds.radius());
        case 2:
            return CollisionShape::Aabb(bounds.half_extents());
        case 3:
            return CollisionShape::Obb(bounds.half_extents(), bounds.rotation());
        case 4:
            return CollisionShape::Mesh(bounds.triangles());
        default:
            return CollisionShape::None();
    }
}

Animation Path References:

Parts can have associated animation files (ZMO) that override or supplement the main object animation.

Animation Reference Structure:

struct AnimationRef {
    uint8  property_id;  // Must be 30
    uint8  size;         // Path length
    char   path[size];   // Animation path
};

Property ID System:

Property IDs in ZSC:
  ┌──────┬────────────────────────┐
  │ ID   │ Property               │
  ├──────┼────────────────────────┤
  │ 30   │ Animation path         │
  │ ...  │ Other properties       │
  └──────┴────────────────────────┘

Animation references use property_id = 30

Animation Application:

Part Animation Workflow:
  1. Check if part has animation reference
  2. If property_id == 30:
     - Load ZMO file from path
     - Apply to part's transform each frame
  3. Animation affects only this part
     (not inherited to children)

Code Implementation:

ZmoFile* load_part_animation(Vfs& vfs, const ZscObjectPart& part) {
    for (const auto& prop : part.properties) {
        if (prop.property_id == 30) {
            return vfs.load_zmo(prop.path.c_str());
        }
    }
    return nullptr;
}

void apply_part_animation(ZscObjectPart& part, const ZmoFile& zmo, float frame) {
    // Sample animation at current frame
    Vec3 pos;
    Quat rot;
    float scale;
    
    if (zmo.sample_position(0, frame, pos)) {
        part.transform.position = pos;
    }
    if (zmo.sample_rotation(0, frame, rot)) {
        part.transform.rotation = rot;
    }
    if (zmo.sample_scale(0, frame, scale)) {
        part.transform.scale = Vec3(scale, scale, scale);
    }
}

Object Effects and Properties:

struct ObjectEffect {
    uint16 effect_id;    // Effect index
    uint16 effect_type;  // 0=Normal, 1=DayNight, 2=LightContainer
    Transform transform;   // Position, rotation, scale
    uint16 parent;       // Parent part index
};

3.6 IFO Zone Object Files (.ifo)

Object Type Enumeration:

enum BlockType {
    DeprecatedMapInfo = 0,
    DecoObject = 1,
    Npc = 2,
    CnstObject = 3,
    SoundObject = 4,
    EffectObject = 5,
    AnimatedObject = 6,
    DeprecatedWater = 7,
    MonsterSpawn = 8,
    WaterPlanes = 9,
    Warp = 10,
    CollisionObject = 11,
    EventObject = 12,
};

Object Placement Data:

struct IfoObject {
    uint8  name_len;       // Name length
    char   name[name_len]; // Object name
    uint16 warp_id;        // Warp ID
    uint16 event_id;       // Event ID
    uint32 object_type;    // Object type enum
    uint32 object_id;      // Object ID
    uint32 minimap_x;     // Minimap X position
    uint32 minimap_y;     // Minimap Y position
    float  rotation[4];    // X, Y, Z, W (quaternion)
    float  position[3];    // X, Y, Z
    float  scale[3];       // X, Y, Z
};

Minimap Position and ID:

The minimap position fields (minimap_x and minimap_y) are used to display objects on the in-game minimap and for object identification purposes.

Minimap Coordinate System:

Minimap Space:
  - Origin: Top-left corner of minimap
  - X-axis: Increases to the right
  - Y-axis: Increases downward
  - Range: 0 to minimap_texture_size

Coordinate Conversion:
  minimap_x = world_x / zone_width * minimap_width
  minimap_y = world_z / zone_height * minimap_height

Usage Examples:

Object Type        | Minimap Display
-------------------|-----------------
NPC                | Yellow dot with icon
Warp               | Blue portal icon
Monster Spawn      | Not shown (hidden)
Quest Target       | Pulsing highlight
Vendor NPC         | Bag/shop icon

Client-Side Processing:

void draw_minimap_object(const IfoObject& object, uint32 minimap_size) {
    // Convert to minimap coordinates
    float mm_x = object.minimap_x / 1000.0f * minimap_size;
    float mm_y = object.minimap_y / 1000.0f * minimap_size;
    
    // Select icon based on object type
    MinimapIcon icon;
    switch (object.object_type) {
        case OBJECT_TYPE_NPC:
            icon = npc_icon;
            break;
        case OBJECT_TYPE_WARP:
            icon = warp_icon;
            break;
        case OBJECT_TYPE_EVENT:
            icon = event_icon;
            break;
        default:
            return; // Don't draw other types
    }
    
    minimap.draw_icon(icon, mm_x, mm_y);
}

Warp and Event References:

Warp and Event IDs provide links to external game systems that handle zone transitions and scripted events.

Warp ID System:

Warp Configuration:
  - Warp ID: Index into warp definition table (STB or similar)
  - Each warp defines:
    - Destination zone ID
    - Destination position (x, y, z)
    - Required conditions (level, items, quests)
    - Warp animation/effect

Warp Activation Flow:
  1. Player enters warp trigger area
  2. Client sends warp request with warp_id
  3. Server validates conditions
  4. Player teleported to destination
  5. Loading screen shown during zone change

Event ID System:

Event Configuration:
  - Event ID: Index into event definition table
  - Events can trigger:
    - Quest dialogs
    - Scripted sequences
    - NPC interactions
    - Cutscenes
    - Special encounters

Event Trigger Flow:
  1. Player interacts with object (click/collision)
  2. Client checks event_id
  3. If event_id > 0:
     - Load event script from Lua
     - Execute event handler
     - Display appropriate UI

Code Reference:

struct WarpInfo {
    uint32 warp_id;
    uint32 dest_zone;
    Vec3 dest_position;
    uint32 required_level;
    uint32 required_quest;  // 0 if no quest requirement
};

struct EventInfo {
    uint32 event_id;
    std::string script_path;
    EventTriggerType trigger_type;
    bool repeatable;
};

void handle_object_interaction(const IfoObject& object) {
    if (object.warp_id > 0) {
        const WarpInfo* warp = warp_table.get(object.warp_id);
        if (warp) {
            initiate_warp(*warp);
        }
    }
    
    if (object.event_id > 0) {
        const EventInfo* event = event_table.get(object.event_id);
        if (event) {
            trigger_event(*event);
        }
    }
}

Spawn Point Definitions:

struct MonsterSpawnPoint {
    IfoObject object;          // Spawn position
    char     spawn_name[];     // Spawn name
    uint32   basic_count;      // Basic spawn count
    BasicSpawn basic_spawns[basic_count];
    uint32   tactic_count;     // Tactic spawn count
    TacticSpawn tactic_spawns[tactic_count];
    uint32   interval;        // Spawn interval (seconds)
    uint32   limit_count;     // Max monsters
    uint32   range;           // Spawn range
    uint32   tactic_points;   // Tactic points
};

struct BasicSpawn {
    char   monster_name[];
    uint32 monster_id;
    uint32 count;
};

Object-Specific Properties:

NPC:

struct IfoNpc {
    IfoObject object;
    uint32 ai_id;               // AI behavior ID
    char   quest_file_name[];   // Quest file
};

Effect Object:

struct IfoEffect {
    IfoObject object;
    char   effect_path[];       // Effect file path
};

Sound Object:

struct IfoSound {
    IfoObject object;
    char   sound_path[];        // Sound file path
    uint32 range;              // Sound range
    uint32 interval;           // Play interval (seconds)
};

Monster Spawn Configuration:

Monster spawn points define where and how monsters appear in the game world. The spawn system controls monster population, respawn timing, and tactical behavior.

Spawn Point Components:

MonsterSpawnPoint Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - position, rotation, scale           │
│   - object_type = MonsterSpawn (8)      │
├─────────────────────────────────────────┤
│ spawn_name: "Forest_Spawn_01"           │
│ basic_spawns: [                         │
│   { monster_id: 101, count: 3 },        │
│   { monster_id: 102, count: 2 }         │
│ ]                                       │
│ interval: 60 (seconds)                  │
│ limit_count: 5 (max alive)              │
│ range: 500 (spawn radius in cm)         │
└─────────────────────────────────────────┘

Spawn Timing Configuration:

Spawn Timing Parameters:
  - interval: Time between spawn checks (seconds)
  - limit_count: Maximum monsters alive from this spawn
  - range: Radius around spawn point for monster placement

Spawn Algorithm:
  1. Check if current_alive < limit_count
  2. If yes and interval elapsed:
     a. Select monster type from basic_spawns
     b. Generate random position within range
     c. Instantiate monster at position
     d. Reset spawn timer

Tactical Spawn System:

TacticSpawn Structure (advanced spawns):
  - Triggered by specific conditions
  - Can spawn different monsters based on:
    - Time of day
    - Player count in area
    - Quest progress
    - Previous spawn deaths

Example Tactical Configuration:
  TacticSpawn {
    condition: TIME_NIGHT,
    monster_id: 201,  // Night-only monster
    count: 2,
    tactic_points: 100
  }

Code Implementation:

class SpawnManager {
public:
    std::vector<MonsterSpawnPoint> spawn_points;
    std::map<uint32, std::vector<EntityId>> active_monsters;
    std::map<uint32, float> spawn_timers;
    
    void update(float delta_time, Zone& zone) {
        for (int spawn_id = 0; spawn_id < spawn_points.size(); spawn_id++) {
            const MonsterSpawnPoint& spawn = spawn_points[spawn_id];
            
            int alive_count = 0;
            auto it = active_monsters.find(spawn_id);
            if (it != active_monsters.end()) {
                alive_count = it->second.size();
            }
            
            if (alive_count < spawn.limit_count) {
                float& timer = spawn_timers[spawn_id];
                timer += delta_time;
                
                if (timer >= spawn.interval) {
                    spawn_monster(spawn_id, spawn);
                    timer = 0.0f;
                }
            }
        }
    }
    
    void spawn_monster(int spawn_id, const MonsterSpawnPoint& spawn) {
        // Select random monster from basic_spawns
        int index = rand() % spawn.basic_spawns.size();
        const BasicSpawn& basic = spawn.basic_spawns[index];
        
        // Generate random position within range
        float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
        float dist = static_cast<float>(rand()) / RAND_MAX * spawn.range;
        Vec3 offset(cos(angle) * dist, 0.0f, sin(angle) * dist);
        Vec3 spawn_pos = spawn.object.position + offset;
        
        // Create monster entity
        EntityId entity = create_monster(basic.monster_id, spawn_pos);
        active_monsters[spawn_id].push_back(entity);
    }
};

NPC and Character Placement:

NPCs and characters are placed in zones using the IfoNpc structure, which extends the base IfoObject with NPC-specific properties.

NPC Placement Configuration:

IfoNpc Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - position: (x, y, z) in cm           │
│   - rotation: quaternion (facing dir)   │
│   - scale: (1, 1, 1) typically          │
│   - object_type = Npc (2)               │
│   - object_id: NPC definition ID        │
├─────────────────────────────────────────┤
│ ai_id: 5 (AI behavior script)           │
│ quest_file_name: "quest_001.lua"        │
└─────────────────────────────────────────┘

NPC ID Resolution:

NPC Loading Pipeline:
  1. Read IfoNpc from IFO file
  2. Look up NPC definition by object_id:
     - From LIST_NPC.STB (NPC data table)
     - From CHR file (character model)
  3. Load NPC resources:
     - Skeleton (ZMD)
     - Models (ZSC)
     - Animations (ZMO)
  4. Apply AI behavior from ai_id
  5. Attach quest script if specified

AI Behavior System:

AI ID Reference:
  ┌───────┬────────────────────────┐
  │ ai_id │ Behavior               │
  ├───────┼────────────────────────┤
  │ 0     │ Idle (no movement)     │
  │ 1     │ Wander (random walk)   │
  │ 2     │ Vendor (shop interface)│
  │ 3     │ Quest NPC (dialog)     │
  │ 4     │ Guard (stationary)     │
  │ 5     │ Patrol (waypoints)     │
  └───────┴────────────────────────┘

Quest File Integration:

class NpcEntity {
public:
    IfoObject base;
    uint32 npc_id;
    AiBehavior ai_behavior;
    std::unique_ptr<QuestScript> quest_script;
    
    static std::unique_ptr<NpcEntity> from_ifo(const IfoNpc& ifo, const NpcDatabase& npc_db) {
        const NpcDef* npc_def = npc_db.get(ifo.object.object_id);
        if (!npc_def) {
            ZZ_LOG("NpcEntity::from_ifo: NPC %d not found\n", ifo.object.object_id);
            return nullptr;
        }
        
        auto entity = std::make_unique<NpcEntity>();
        entity->base = ifo.object;
        entity->npc_id = ifo.object.object_id;
        entity->ai_behavior = AiBehavior::from_id(ifo.ai_id);
        
        if (!ifo.quest_file_name.empty()) {
            entity->quest_script = QuestScript::load(ifo.quest_file_name);
        }
        
        return entity;
    }
};

Animated Object Definitions:

Animated objects are decorations or interactive elements that have associated animations. They use the standard IfoObject structure with object_type = AnimatedObject (6).

Animated Object Configuration:

AnimatedObject Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - object_type = AnimatedObject (6)    │
│   - object_id: ZSC object reference     │
│   - position, rotation, scale           │
└─────────────────────────────────────────┘

Animation Sources:
  - ZSC file contains animation references
  - ZMO files define the actual animation data
  - Animations can be looped or triggered

Common Animated Object Types:

Object Type          | Animation Type        | Example
---------------------|----------------------|------------------
Windmill             | Continuous loop      | Rotating blades
Water wheel          | Continuous loop      | Rotating wheel
Flag                 | Continuous loop      | Waving flag
Door                 | Triggered            | Open/close on click
Chest                | Triggered            | Open on loot
Campfire             | Particle + animation | Flickering flames

Animation Playback Configuration:

class AnimatedObject {
public:
    IfoObject base;
    ZscObject zsc_object;
    std::string active_animation;
    AnimationState animation_state;
    
    void update(float delta_time) {
        if (!active_animation.empty()) {
            animation_state.advance(delta_time);
            
            // Apply animation to all parts
            for (auto& part : zsc_object.parts) {
                if (part.animation) {
                    float frame = animation_state.current_frame;
                    part.apply_animation_frame(*part.animation, frame);
                }
            }
        }
    }
};

enum AnimationPlayMode {
    ANIMATION_LOOP,           // Continuous playback
    ANIMATION_ONCE,           // Play once and stop
    ANIMATION_PING_PONG,      // Forward then reverse
    ANIMATION_TRIGGERED       // Play on event
};

Collision Object Properties:

Collision objects define invisible or visible geometry that participates in physics collision detection. They use object_type = CollisionObject (11).

Collision Object Types:

Collision Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - object_type = CollisionObject (11)  │
│   - position, rotation, scale           │
│   - object_id: ZSC collision mesh       │
├─────────────────────────────────────────┤
│ Collision Shape (from ZSC):             │
│   - None (0): No collision              │
│   - Sphere (1): Bounding sphere         │
│   - AABB (2): Axis-aligned box          │
│   - OBB (3): Oriented bounding box      │
│   - Polygon (4): Triangle mesh          │
└─────────────────────────────────────────┘

Collision Flags (from ZSC):

Collision Flag Bits:
  Bit 0-2: Shape type
  Bit 3:   NOT_MOVEABLE (static object)
  Bit 4:   NOT_PICKABLE (no mouse selection)
  Bit 5:   HEIGHT_ONLY (terrain collision only)
  Bit 6:   NOT_CAMERA_COLLISION (camera passes through)
  Bit 7:   PASSTHROUGH (player can walk through)

Common Configurations:
  ┌──────────────────┬────────────────────────┐
  │ Object           │ Collision Flags        │
  ├──────────────────┼────────────────────────┤
  │ Building wall    │ OBB, NOT_MOVEABLE      │
  │ Fence            │ OBB, NOT_CAMERA_COLL   │
  │ Water            │ HEIGHT_ONLY            │
  │ Trigger zone     │ PASSTHROUGH            │
  │ Invisible wall   │ NOT_PICKABLE           │
  └──────────────────┴────────────────────────┘

Physics Integration:

class CollisionObject {
public:
    IfoObject base;
    CollisionShape collision_shape;
    CollisionFlags collision_flags;
    PhysicsHandle physics_handle;
    
    void add_to_physics(PhysicsWorld& physics) {
        Transform transform = Transform::from_pos_rot_scale(
            base.position,
            base.rotation,
            base.scale
        );
        
        switch (collision_shape.type) {
            case COLLISION_SHAPE_SPHERE:
                physics_handle = physics.add_sphere(
                    transform, collision_shape.radius, collision_flags
                );
                break;
            case COLLISION_SHAPE_AABB:
                physics_handle = physics.add_box(
                    transform, collision_shape.half_extents, collision_flags
                );
                break;
            case COLLISION_SHAPE_POLYGON:
                physics_handle = physics.add_mesh(
                    transform, collision_shape.mesh, collision_flags
                );
                break;
            default:
                break;
        }
    }
};

Deco and CNST Object Types:

Decoration (Deco) and Construction (Cnst) objects are the two primary static object types placed in zones, differing primarily in their collision behavior.

Deco Objects (type 1):

Characteristics:
  - object_type = DecoObject (1)
  - No physics collision (player can walk through)
  - Used for visual-only elements
  - Lower rendering priority

Common Uses:
  - Grass and small plants
  - Hanging banners/cloth
  - Small rocks and debris
  - Background decorations
  - Particle effect emitters

CNST Objects (type 3):

Characteristics:
  - object_type = CnstObject (3)
  - Has physics collision
  - Blocks player movement
  - Higher rendering priority
  - Camera collision enabled

Common Uses:
  - Buildings and walls
  - Large rocks and boulders
  - Fences and barriers
  - Bridges and platforms
  - Furniture and large props

Comparison Table:

┌─────────────────┬─────────────────┬─────────────────┐
│ Property        │ Deco            │ Cnst            │
├─────────────────┼─────────────────┼─────────────────┤
│ Collision       │ None            │ Full            │
│ Player blocking │ No              │ Yes             │
│ Camera blocking │ No              │ Yes             │
│ Mouse picking   │ Optional        │ Yes             │
│ Shadow casting  │ Optional        │ Yes             │
│ LOD distance    │ Shorter         │ Longer          │
│ Memory priority │ Lower           │ Higher          │
└─────────────────┴─────────────────┴─────────────────┘

Code Implementation:

Entity* load_zone_object(const IfoObject& obj) {
    auto zsc = vfs.load_zsc(obj.object_id);
    if (!zsc) {
        ZZ_LOG("load_zone_object: failed to load ZSC %d\n", obj.object_id);
        return nullptr;
    }
    
    switch (obj.object_type) {
        case OBJECT_TYPE_DECO:
            // Decoration: visual only, no collision
            return create_decoration_entity(obj, zsc);
        case OBJECT_TYPE_CNST:
            // Construction: has collision
            Entity* entity = create_decoration_entity(obj, zsc);
            if (entity) {
                add_collision_to_entity(*entity, *zsc);
            }
            return entity;
        default:
            ZZ_LOG("load_zone_object: unknown object type %d\n", obj.object_type);
            return nullptr;
    }
}

Effect and Sound Object Types:

Effect and Sound objects are ambient elements that enhance the atmosphere of a zone without physical presence.

Effect Objects (type 5):

IfoEffect Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - object_type = EffectObject (5)      │
│   - position: Effect spawn point        │
├─────────────────────────────────────────┤
│ effect_path: "EFFECT/FIRE_TORCH.EFT"    │
└─────────────────────────────────────────┘

Common Effect Types:
  - Torches and fires
  - Water fountains
  - Magic auras
  - Smoke and steam
  - Light beams

Sound Objects (type 4):

IfoSound Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base)                        │
│   - object_type = SoundObject (4)       │
│   - position: Sound emitter location    │
├─────────────────────────────────────────┤
│ sound_path: "SOUND/AMBIENT/FOREST.WAV"  │
│ range: 1000 (audible radius in cm)      │
│ interval: 30 (play every 30 seconds)    │
└─────────────────────────────────────────┘

Sound Playback Modes:
  - Loop: Continuous playback
  - Interval: Play every N seconds
  - Triggered: Play on event
  - Random: Random intervals with variance

Ambient System Integration:

class AmbientManager {
public:
    std::vector<EffectEmitter> effects;
    std::vector<SoundEmitter> sounds;
    
    void load_from_ifo(const IfoFile& ifo) {
        for (const auto& obj : ifo.objects) {
            switch (obj.object_type) {
                case OBJECT_TYPE_EFFECT: {
                    IfoEffect effect = IfoEffect::from_object(obj);
                    effects.push_back(EffectEmitter{
                        effect.object.position,
                        effect.effect_path,
                        true
                    });
                    break;
                }
                case OBJECT_TYPE_SOUND: {
                    IfoSound sound = IfoSound::from_object(obj);
                    sounds.push_back(SoundEmitter{
                        sound.object.position,
                        sound.sound_path,
                        static_cast<float>(sound.range),
                        static_cast<float>(sound.interval),
                        0.0f
                    });
                    break;
                }
                default:
                    break;
            }
        }
    }
    
    void update(float delta_time, const Vec3& listener_pos) {
        // Update sound emitters
        for (auto& sound : sounds) {
            float distance = (sound.position - listener_pos).length();
            
            if (distance <= sound.range) {
                sound.timer += delta_time;
                if (sound.timer >= sound.interval) {
                    audio::play_3d_sound(sound.sound_path, sound.position);
                    sound.timer = 0.0f;
                }
            }
        }
    }
};

3.7 HIM Heightmap Files (.him)

File Header and Magic Number:

No magic string. Direct data layout.

Offset  Type    Description
0x00    u32      Width
0x04    u32      Height
0x08    u32[2]   Unknown (8 bytes)
0x10    f32[]    Height data (width × height)

Heightmap Grid Structure:

The heightmap is organized as a 2D grid of floating-point height values that define the terrain elevation at each grid point. The grid forms the foundation for terrain mesh generation.

Grid Organization:

HIM Grid Layout:
┌─────────────────────────────────────┐
│  [0][0]    [1][0]    ...  [W-1][0]  │  ← Row 0 (North edge)
│  [0][1]    [1][1]    ...  [W-1][1]  │
│   ...       ...      ...    ...     │
│  [0][H-1] [1][H-1] ... [W-1][H-1]   │  ← Row H-1 (South edge)
└─────────────────────────────────────┘
   ↑ Column 0                      ↑ Column W-1
   (West edge)                     (East edge)

Key Relationships:

  • Grid dimensions are defined by width and height fields in the header
  • Each grid cell represents a single height sample point
  • Grid resolution typically matches the TIL tile grid (1:1 mapping)
  • Common sizes: 65×65, 129×129, 257×257 (power of 2 + 1 for edge vertices)

Terrain Mesh Generation:

For each grid cell (x, y):
  - Vertex position: (x * patch_size, height[x][y], y * patch_size)
  - Normal: computed from neighboring heights
  - UV: (x / width, y / height)

Height Data Encoding:

Each grid cell stores a 32-bit IEEE 754 floating-point value representing the terrain elevation at that point.

Data Layout:

Offset Calculation:
  offset = 0x10 + (y * width + x) * 4

Value Range:
  - Typical terrain: -500.0 to +2000.0 (centimeters)
  - Sea level reference: 0.0
  - Underground areas: negative values
  - Mountains: positive values up to ~2000.0

Example Height Values:
  [100.5, 102.3, 105.0, 108.2, ...]  ← Row of heights in cm

Encoding Characteristics:

  • Little-endian byte order (x86 native)
  • No compression applied
  • Precision: ~7 significant decimal digits (32-bit float)
  • Special values: 0.0 commonly used for flat/sea-level terrain

Code Reference:

bool read_height_data(zz_vfs& file, uint32 width, uint32 height, std::vector<float>& heights) {
    uint32 count = width * height;
    heights.resize(count);
    
    for (uint32 i = 0; i < count; i++) {
        if (!file.read_float(heights[i])) {
            ZZ_LOG("read_height_data: failed to read height at index %d\n", i);
            return false;
        }
    }
    
    return true;
}

Grid Size and Resolution:

The grid size determines the spatial resolution of the terrain height data. Width and height are specified in grid cells (number of sample points).

Typical Grid Configurations:

Zone Type Grid Size Patch Count World Coverage
Small Zone 65 × 65 64 × 64 patches ~64m × 64m
Standard Zone 129 × 129 128 × 128 patches ~128m × 128m
Large Zone 257 × 257 256 × 256 patches ~256m × 256m
City/Indoor 33 × 33 32 × 32 patches ~32m × 32m

Resolution Relationships:

  • Grid cells per patch: Typically 1:1 (one height sample per terrain tile)
  • Patch size: Defined in ZON file (e.g., 100.0 cm = 1 meter)
  • Total world size = grid_size × patch_size

Example Calculation:

Zone Configuration:
  - HIM grid: 129 × 129 cells
  - Patch size: 100.0 cm (from ZON)
  - World coverage: 129 × 100.0 = 12,900 cm = 129 meters per axis

Coordinate Mapping:

Grid coordinates in the HIM file map directly to world coordinates through a linear transformation with scaling applied.

Coordinate Transformation:

World Position from Grid Coordinates:
  world_x = grid_x * patch_size
  world_y = height[grid_x][grid_y]  // Y is up in Rose
  world_z = grid_y * patch_size

Grid Coordinates from World Position:
  grid_x = floor(world_x / patch_size)
  grid_y = floor(world_z / patch_size)
  height = heights[grid_x][grid_y]  // Clamp to valid range

Coordinate System Details:

HIM Coordinate World Coordinate Notes
grid_x world_x / patch_size X axis mapping
grid_y world_z / patch_size Z axis (forward direction)
height[x][y] world_y Y is up axis
Origin (0,0) Zone origin Bottom-left corner

Height Lookup Example:

float get_height_at_world(
    const std::vector<float>& heights,
    uint32 grid_width,
    uint32 grid_height,
    float world_x,
    float world_z,
    float patch_size
) {
    int grid_x = static_cast<int>(std::floor(world_x / patch_size));
    int grid_y = static_cast<int>(std::floor(world_z / patch_size));
    
    // Clamp to valid range
    int x = std::clamp(grid_x, 0, static_cast<int>(grid_width) - 1);
    int y = std::clamp(grid_y, 0, static_cast<int>(grid_height) - 1);
    
    return heights[y * grid_width + x];
}

Heightmap Scaling Factors:

Height values are stored in centimeters (cm) in the HIM file. Conversion to meters requires scaling by 0.01.

Scaling Conversions:

Storage Format (HIM file):
  - Units: Centimeters (cm)
  - Typical range: -500.0 to +2000.0 cm
  
Rendering Format (Client):
  - Units: Meters (m)
  - Conversion: height_meters = height_cm * 0.01
  - Converted range: -5.0 to +20.0 m

Practical Examples:

Height Value (cm)  |  Height (m)  |  Terrain Feature
-------------------|--------------|------------------
     -100.0        |    -1.0      |  Shallow water
       0.0         |     0.0      |  Sea level / Flat ground
      50.0         |     0.5      |  Small bump
     200.0         |     2.0      |  Hill
    1000.0         |    10.0      |  Mountain
    2000.0         |    20.0      |  Peak

Code Implementation:

void convert_heights_to_meters(std::vector<float>& heights) {
    for (float& height : heights) {
        height *= 0.01f;  // cm to m conversion
    }
}

// Or during terrain mesh generation:
float vertex_y = him_heights[y * width + x] * CM_TO_M;

Client-Side Application:

  • Heights are typically converted during terrain mesh building
  • Physics engine uses meter-based coordinates
  • Rendering pipeline expects meter-based vertex positions

3.8 TIL Tile Index Files (.til)

Tile Index Structure:

Offset  Type    Description
0x00    u32      Width
0x04    u32      Height
0x08    Tile[]   Tile data (width × height)

Tile Entry:

Offset  Type    Description
0x00    u8[3]    Unknown (3 bytes)
0x03    u32      Tile index

Texture Mapping:

Tile indices in the TIL file reference textures defined in the ZON file’s Textures block. This indirection allows multiple tiles to share the same texture while enabling per-tile customization.

Texture Reference Chain:

TIL File                    ZON File                    Texture File
┌─────────┐                ┌─────────────┐              ┌──────────┐
│Tile[x,y]│──index──►      │Textures[i]  │───path──►    │.DDS/.TGA │
│ = 5     │                │ = "grass"   │              │file      │
└─────────┘                └─────────────┘              └──────────┘

Texture Index Resolution:

1. Read tile_index from TIL[x][y]
2. Look up ZON.tile_textures[tile_index]
3. Get texture path string
4. Load texture via VFS

Example Mapping:

TIL Data:
  tile[10][20] = 3

ZON Textures Block:
  [0] = "sand.dds"
  [1] = "rock.dds"
  [2] = "water.dds"
  [3] = "grass.dds"    ← Referenced by tile[10][20]
  [4] = "dirt.dds"

Result: tile[10][20] uses "grass.dds"

Tile Coordinate System:

The tile coordinate system uses a 2D grid that directly corresponds to the HIM heightmap resolution. Each tile position (tileX, tileY) maps to a specific patch in the heightmap grid.

Coordinate System Properties:

Property Value Description
Origin (0, 0) Bottom-left corner of zone
X-Axis tileX East direction (increasing)
Y-Axis tileY North direction (increasing)
Max X width – 1 Eastern boundary
Max Y height – 1 Northern boundary

Coordinate Relationships:

Tile Grid Layout:
     Y
     ↑
  H-1├──────────────────────┤
     │                      │
     │    Tile Coordinate   │
     │    System            │
     │                      │
   0 ├──────────────────────┴──→ X
     0                     W-1

Key Relationships:
- 1 tile = 1 grid cell in HIM heightmap
- 1 tile = 1 patch in terrain mesh
- Tile (0,0) is at the bottom-left corner of the zone
- Maximum tiles per zone: defined by HIM dimensions

World Position to Tile Coordinate Conversion:

Given:
  - World position (worldX, worldZ)
  - Patch size from ZON (e.g., 100.0 cm)

Calculation:
  tileX = floor(worldX / patch_size)
  tileY = floor(worldZ / patch_size)

Example:
  - World position: (1500.0, 2300.0) cm
  - Patch size: 100.0 cm
  - tileX = floor(1500.0 / 100.0) = 15
  - tileY = floor(2300.0 / 100.0) = 23
  - References tile[15][23] in the TIL file

Code Reference:

void world_to_tile(float world_x, float world_z, float patch_size, uint32& tile_x, uint32& tile_y) {
    tile_x = static_cast<uint32>(std::floor(world_x / patch_size));
    tile_y = static_cast<uint32>(std::floor(world_z / patch_size));
}

void tile_to_world(uint32 tile_x, uint32 tile_y, float patch_size, float& world_x, float& world_z) {
    world_x = tile_x * patch_size;
    world_z = tile_y * patch_size;
}

Tile Size and Resolution:

Each tile covers a fixed area of terrain defined by the patch size specified in the ZON file. The tile resolution determines the level of detail in terrain texturing.

Tile Size Configuration:

From ZON ZoneInfo Block:
  - Grid per patch: Number of grid cells per patch (typically 1)
  - Grid size (patch_size): Size of each patch in centimeters

Common Configurations:
  ┌─────────────────┬────────────┬─────────────────┐
  │ Patch Size (cm) │ Size (m)   │ Use Case        │
  ├─────────────────┼────────────┼─────────────────┤
  │ 100.0           │ 1.0 m      │ Standard zones  │
  │ 200.0           │ 2.0 m      │ Large outdoor   │
  │ 50.0            │ 0.5 m      │ Indoor/dungeons │
  └─────────────────┴────────────┴─────────────────┘

Resolution Calculations:

Zone Coverage Example:
  - TIL dimensions: 64 × 64 tiles
  - Patch size: 100.0 cm (1 meter)
  - Total coverage: 64m × 64m

Tile Density:
  - Higher density (smaller patches): More detailed texturing
  - Lower density (larger patches): Larger terrain areas, less detail
  - Memory impact: 7 bytes per tile entry × width × height

Terrain Mesh Generation:

For each tile (x, y):
  1. Get tile_index from TIL[x][y]
  2. Get tile definition from ZON.tiles[tile_index]
  3. Get height from HIM[x][y]
  4. Generate 4 vertices for tile corners:
     - v0: (x * patch_size, height, y * patch_size)
     - v1: ((x+1) * patch_size, height, y * patch_size)
     - v2: ((x+1) * patch_size, height, (y+1) * patch_size)
     - v3: (x * patch_size, height, (y+1) * patch_size)
  5. Apply texture coordinates from ZON tile rotation/offset

Texture Atlas Organization:

Textures in Rose Online are stored as individual files rather than combined into a single texture atlas. This approach has specific implications for rendering and asset management.

Individual Texture Architecture:

Texture File Organization:
  3DDATA/
  └── TEXTURES/
      ├── terrain/
      │   ├── grass_01.dds
      │   ├── grass_02.dds
      │   ├── sand_01.dds
      │   ├── rock_01.dds
      │   └── water_01.dds
      └── special/
          ├── lava.dds
          └── snow.dds

Advantages of Individual Files:

  • Easy texture replacement and modding
  • Selective loading (only load textures in use)
  • No wasted texture memory for unused tiles
  • Straightforward asset pipeline

Disadvantages:

  • More draw calls (texture binding per material)
  • Higher file count in VFS
  • Potential texture cache thrashing

Texture Dimensions:

Common Texture Sizes:
  ┌────────────┬─────────────┬─────────────────┐
  │ Size       │ Memory      │ Use Case        │
  ├────────────┼─────────────┼─────────────────┤
  │ 256 × 256  │ ~64 KB DXT1  │ Standard tiles  │
  │ 512 × 512  │ ~256 KB DXT1 │ High-detail     │
  │ 128 × 128  │ ~16 KB DXT1  │ Low-detail      │
  └────────────┴─────────────┴─────────────────┘

Runtime Texture Management:

class TextureManager {
public:
    std::map<std::string, GpuTexture> loaded_textures;
    size_t max_cache_size;
    
    GpuTexture* get_texture(const char* path) {
        std::string path_str(path);
        
        auto it = loaded_textures.find(path_str);
        if (it == loaded_textures.end()) {
            GpuTexture texture = load_texture_from_vfs(path);
            auto result = loaded_textures.emplace(path_str, texture);
            return &result.first->second;
        }
        
        return &it->second;
    }
};

Tile-to-Texture Mapping Algorithms:

The complete algorithm for resolving a tile’s texture involves multiple steps and handles edge cases like out-of-bounds coordinates.

Complete Mapping Algorithm:

Algorithm: GetTileTexture
Input: til_file, zon_file, world_x, world_z, patch_size
Output: Texture path and tile properties

1. Convert world coordinates to tile coordinates:
   tile_x = floor(world_x / patch_size)
   tile_y = floor(world_z / patch_size)

2. Clamp tile coordinates to valid range:
   tile_x = clamp(tile_x, 0, til_file.width - 1)
   tile_y = clamp(tile_y, 0, til_file.height - 1)

3. Read tile index from TIL file:
   tile_entry = til_file.tiles[tile_y][tile_x]
   tile_index = tile_entry.tile_index

4. Get tile definition from ZON:
   tile_def = zon_file.tiles[tile_index]

5. Resolve texture paths:
   layer1_path = zon_file.textures[tile_def.layer1]
   layer2_path = zon_file.textures[tile_def.layer2] (if blending enabled)

6. Return texture information:
   return (layer1_path, layer2_path, tile_def)

Python Implementation:

def get_tile_texture_info(til_file, zon_file, x, y):
    """Get complete texture information for a tile."""
    # Clamp coordinates
    x = max(0, min(x, til_file.width - 1))
    y = max(0, min(y, til_file.height - 1))
    
    # Get tile index
    tile_index = til_file.get_clamped(x, y)
    
    # Get tile definition
    tile_def = zon_file.tiles[tile_index]
    
    # Resolve textures
    texture1_path = zon_file.textures[tile_def.layer1]
    texture2_path = None
    if tile_def.blend and tile_def.layer2 < len(zon_file.textures):
        texture2_path = zon_file.textures[tile_def.layer2]
    
    return {
        'primary_texture': texture1_path,
        'secondary_texture': texture2_path,
        'blend': tile_def.blend,
        'rotation': tile_def.rotation,
        'offset1': tile_def.offset1,
        'offset2': tile_def.offset2
    }

C++ Implementation with Error Handling:

bool get_tile_texture(
    const TilFile& til,
    const ZonFile& zon,
    uint32 x,
    uint32 y,
    TileTextureInfo& info
) {
    // Validate coordinates
    if (x >= til.width || y >= til.height) {
        ZZ_LOG("get_tile_texture: coordinates out of bounds (%d, %d)\n", x, y);
        return false;
    }
    
    // Get tile entry
    const TilTileEntry& tile_entry = til.tiles[y][x];
    uint32 tile_index = tile_entry.tile_index;
    
    // Validate tile index
    if (tile_index >= zon.tiles.size()) {
        ZZ_LOG("get_tile_texture: invalid tile index %d\n", tile_index);
        return false;
    }
    
    const ZonTile& tile_def = zon.tiles[tile_index];
    
    // Resolve texture paths
    if (tile_def.layer1 >= zon.textures.size()) {
        ZZ_LOG("get_tile_texture: invalid layer1 texture index %d\n", tile_def.layer1);
        return false;
    }
    
    info.primary_texture = zon.textures[tile_def.layer1];
    
    if (tile_def.blend != 0 && tile_def.layer2 < zon.textures.size()) {
        info.secondary_texture = zon.textures[tile_def.layer2];
    } else {
        info.secondary_texture.clear();
    }
    
    info.blend = (tile_def.blend != 0);
    info.rotation = static_cast<ZonTileRotation>(tile_def.rotation);
    info.uv_offset1 = tile_def.offset1 / 1000.0f;
    info.uv_offset2 = tile_def.offset2 / 1000.0f;
    
    return true;
}

UV Coordinate Calculation with Rotation:

Vec2 apply_tile_rotation(const Vec2& uv, ZonTileRotation rotation) {
    switch (rotation) {
        case ZON_TILE_ROTATION_NONE:
            return uv;
        case ZON_TILE_ROTATION_FLIP_HORIZONTAL:
            return Vec2(1.0f - uv.x, uv.y);
        case ZON_TILE_ROTATION_FLIP_VERTICAL:
            return Vec2(uv.x, 1.0f - uv.y);
        case ZON_TILE_ROTATION_FLIP:
            return Vec2(1.0f - uv.x, 1.0f - uv.y);
        case ZON_TILE_ROTATION_CLOCKWISE90:
            return Vec2(uv.y, 1.0f - uv.x);
        case ZON_TILE_ROTATION_COUNTER_CLOCKWISE90:
            return Vec2(1.0f - uv.y, uv.x);
        default:
            return uv;
    }
}

3.9 CHR Character Files (.chr)

Character Definition Structure:

struct ChrFile {
    uint16 skeleton_count;
    char   skeleton_files[skeleton_count][];

    uint16 motion_count;
    char   motion_files[motion_count][];

    uint16 effect_count;
    char   effect_files[effect_count][];

    uint16 character_count;
    CharacterData characters[character_count];
};

NPC Data Organization:

Characters in the CHR file are indexed by NPC ID, providing a central lookup mechanism for all character-related assets. This organization enables efficient asset loading when spawning NPCs or loading player characters.

NPC ID Mapping System:

CHR File Structure:
┌─────────────────────────────────────────┐
│ Header                                   │
│   - skeleton_count, motion_count, etc.  │
├─────────────────────────────────────────┤
│ File Reference Tables:                   │
│   - skeleton_files[]  → ZMD paths       │
│   - motion_files[]    → ZMO paths       │
│   - effect_files[]    → EFT paths       │
├─────────────────────────────────────────┤
│ Character Definitions:                   │
│   characters: Map<NPC_ID, NpcModelData> │
│   - ID 1: Town NPC                       │
│   - ID 100: Monster                      │
│   - ID 500: Boss                         │
└─────────────────────────────────────────┘

NPC ID Categories:

ID Range      | Type           | Description
--------------|----------------|---------------------------
1-99          | Town NPCs      | Vendors, quest givers, etc.
100-999       | Regular mobs   | Standard monsters
1000-1999     | Elite mobs     | Stronger monsters
2000-2999     | Bosses         | Boss monsters
3000+         | Special        | Event NPCs, etc.

Lookup Example:

bool get_npc_assets(const ChrFile& chr, uint32 npc_id, NpcAssets& assets) {
    auto it = chr.characters.find(npc_id);
    if (it == chr.characters.end()) {
        ZZ_LOG("get_npc_assets: NPC %d not found\n", npc_id);
        return false;
    }
    
    const NpcModelData& model_data = it->second;
    
    // Get skeleton
    if (model_data.skeleton_index >= chr.skeleton_files.size()) {
        ZZ_LOG("get_npc_assets: invalid skeleton index %d\n", model_data.skeleton_index);
        return false;
    }
    assets.skeleton = chr.skeleton_files[model_data.skeleton_index];
    
    // Get models
    assets.models.clear();
    for (uint16 model_id : model_data.model_ids) {
        if (model_id >= chr.model_paths.size()) {
            ZZ_LOG("get_npc_assets: invalid model_id %d\n", model_id);
            continue;
        }
        assets.models.push_back(chr.model_paths[model_id]);
    }
    
    // Get motions
    assets.motions.clear();
    for (const auto& motion : model_data.motions) {
        if (motion.file_index >= chr.motion_files.size()) {
            ZZ_LOG("get_npc_assets: invalid motion file_index %d\n", motion.file_index);
            continue;
        }
        assets.motions.push_back(chr.motion_files[motion.file_index]);
    }
    
    return true;
}

Skeleton, Motion, and Effect References:

The CHR file maintains arrays of file paths for skeletons, motions, and effects. Character definitions reference these by index, allowing multiple characters to share common assets.

Skeleton Reference System:

Skeleton Files Array:
  [0] = "3DDATA/AVATAR/MALE.ZMD"
  [1] = "3DDATA/AVATAR/FEMALE.ZMD"
  [2] = "3DDATA/MONSTER/GOBLIN.ZMD"
  ...

Character → Skeleton Mapping:
  NpcModelData.skeleton_index → skeleton_files[index]
  
Example:
  NPC ID 100 (Goblin):
    skeleton_index = 2
    → Uses "3DDATA/MONSTER/GOBLIN.ZMD"

Motion Reference System:

Motion Files Array:
  [0] = "3DDATA/MOTION/WALK.ZMO"
  [1] = "3DDATA/MOTION/RUN.ZMO"
  [2] = "3DDATA/MOTION/ATTACK1.ZMO"
  ...

Motion Reference Structure:
  struct MotionRef {
      uint16 motion_id;     // Action type (walk=1, run=2, attack=10, etc.)
      uint16 file_index;    // Index into motion_files[]
  }
  
Example:
  NPC ID 100 (Goblin) motions:
    [(1, 0), (2, 1), (10, 2)]
    → Walk uses motion_files[0]
    → Run uses motion_files[1]
    → Attack uses motion_files[2]

Effect Reference System:

Effect Files Array:
  [0] = "EFFECT/FIRE_HIT.EFT"
  [1] = "EFFECT/LEVEL_UP.EFT"
  ...

Effect Reference Structure:
  struct EffectRef {
      uint16 motion_id;     // Trigger motion (when to play)
      uint16 file_index;    // Index into effect_files[]
  }
  
Example:
  NPC ID 100 (Goblin) effects:
    [(10, 0)]  // Play FIRE_HIT.EFT on attack motion

Code Implementation:

class ChrFile {
public:
    std::vector<std::string> skeleton_files;
    std::vector<std::string> motion_files;
    std::vector<std::string> effect_files;
    std::map<uint32, NpcModelData> characters;
    
    const std::string* resolve_skeleton(uint32 npc_id) const {
        auto it = characters.find(npc_id);
        if (it != characters.end()) {
            const NpcModelData& model = it->second;
            if (model.skeleton_index < skeleton_files.size()) {
                return &skeleton_files[model.skeleton_index];
            }
        }
        return nullptr;
    }
    
    std::vector<std::pair<uint16, const std::string*>> resolve_motions(uint32 npc_id) const {
        std::vector<std::pair<uint16, const std::string*>> result;
        auto it = characters.find(npc_id);
        if (it == characters.end()) {
            return result;
        }
        
        const NpcModelData& model = it->second;
        for (const auto& motion : model.motions) {
            if (motion.file_index < motion_files.size()) {
                result.push_back({motion.motion_id, &motion_files[motion.file_index]});
            }
        }
        return result;
    }
};

Character Type Classification:

Characters are classified into different types based on their behavior, rendering requirements, and gameplay role.

Character Type Categories:

┌─────────────────┬─────────────────────────────────────────────┐
│ Type            │ Description                                 │
├─────────────────┼─────────────────────────────────────────────┤
│ Player Character│ Humanoid, full skeleton, many animations    │
│ Town NPC        │ Humanoid, limited animations, stationary    │
│ Monster         │ Various skeletons, combat animations        │
│ Boss            │ Complex monster, special effects            │
│ Pet             │ Simple skeleton, follow animations          │
│ Vehicle         │ Special skeleton, mount animations          │
└─────────────────┴─────────────────────────────────────────────┘

Type-Specific Properties:

Player Characters:
  - Full skeleton (100+ bones)
  - 20+ animation types
  - Equipment skinning
  - Face/emotion animations

Monsters:
  - Variable skeleton (10-50 bones)
  - Combat animations
  - Hit/damage reactions
  - Death animations

NPCs:
  - Simplified skeleton
  - Idle/talk animations
  - Quest interaction triggers

Classification by ID Convention:

enum class CharacterType {
    Player,
    TownNpc,
    Monster,
    EliteMonster,
    Boss,
    Pet,
    Vehicle,
};

CharacterType from_npc_id(uint32_t npc_id) {
    if (npc_id == 0) return CharacterType::Player;
    if (npc_id <= 99) return CharacterType::TownNpc;
    if (npc_id <= 999) return CharacterType::Monster;
    if (npc_id <= 1999) return CharacterType::EliteMonster;
    if (npc_id <= 2999) return CharacterType::Boss;
    if (npc_id >= 5000 && npc_id <= 5999) return CharacterType::Pet;
    if (npc_id >= 6000 && npc_id <= 6999) return CharacterType::Vehicle;
    return CharacterType::Monster;
}

Asset Path Resolution:

All asset paths in the CHR file are resolved through the Virtual Filesystem (VFS), allowing assets to be loaded from packed archives or loose files.

Path Resolution Flow:

1. CHR file contains relative paths:
   "3DDATA/AVATAR/MALE.ZMD"
   
2. VFS normalizes path:
   - Convert to uppercase
   - Forward slashes
   - "3DDATA/AVATAR/MALE.ZMD"
   
3. VFS searches mounted devices:
   a. Check host filesystem first
   b. Check update.pkg
   c. Check data.pkg
   
4. Return file handle or error

Path Resolution Code:

bool load_character_assets(zz_vfs& vfs, const ChrFile& chr, uint32 npc_id, CharacterAssets& assets) {
    auto it = chr.characters.find(npc_id);
    if (it == chr.characters.end()) {
        ZZ_LOG("load_character_assets: NPC %d not found\n", npc_id);
        return false;
    }
    
    const NpcModelData& model_data = it->second;
    
    // Resolve skeleton
    if (model_data.skeleton_index >= chr.skeleton_files.size()) {
        ZZ_LOG("load_character_assets: invalid skeleton index %d\n", model_data.skeleton_index);
        return false;
    }
    
    const std::string& skeleton_path = chr.skeleton_files[model_data.skeleton_index];
    assets.skeleton = load_zmd(skeleton_path);
    if (!assets.skeleton) {
        ZZ_LOG("load_character_assets: failed to load skeleton %s\n", skeleton_path.c_str());
        return false;
    }
    
    // Resolve models (ZSC files)
    assets.models.clear();
    for (uint16 model_id : model_data.model_ids) {
        if (model_id >= chr.model_paths.size()) {
            ZZ_LOG("load_character_assets: invalid model_id %d\n", model_id);
            continue;
        }
        
        const std::string& model_path = chr.model_paths[model_id];
        auto zsc = load_zsc(model_path);
        if (zsc) {
            assets.models.push_back(zsc);
        }
    }
    
    // Resolve motions
    assets.motions.clear();
    for (const auto& motion_ref : model_data.motions) {
        if (motion_ref.file_index >= chr.motion_files.size()) {
            ZZ_LOG("load_character_assets: invalid motion file_index %d\n", motion_ref.file_index);
            continue;
        }
        
        const std::string& motion_path = chr.motion_files[motion_ref.file_index];
        auto zmo = load_zmo(motion_path);
        if (zmo) {
            assets.motions[motion_ref.motion_id] = zmo;
        }
    }
    
    return true;
}

VFS Integration Example:

class Vfs {
public:
    ZmdFile* load_zmd(const char* path) {
        std::string normalized = normalize_path(path);
        
        for (auto& device : devices) {
            if (device->exists(normalized.c_str())) {
                zz_vfs file;
                if (file.open(normalized.c_str(), zz_vfs::ZZ_VFS_READ)) {
                    uint32 size = file.get_size();
                    std::vector<char> data(size);
                    file.read(data.data(), size);
                    file.close();
                    return ZmdFile::parse(data.data(), size);
                }
            }
        }
        
        ZZ_LOG("Vfs::load_zmd: file not found: %s\n", path);
        return nullptr;
    }
    
private:
    std::string normalize_path(const char* path) {
        std::string result(path);
        // Convert to uppercase and forward slashes
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        std::replace(result.begin(), result.end(), '\\', '/');
        return result;
    }
    
    std::vector<std::unique_ptr<zz_vfs>> devices;
};

Character-Specific Properties:

struct NpcModelData {
    uint16 skeleton_index;              // Skeleton file index
    char   name[];                    // NPC name
    uint16 mesh_count;                // Number of mesh parts
    uint16 model_ids[mesh_count];     // ZSC model IDs
    uint16 motion_count;              // Number of motions
    MotionRef motions[motion_count];    // (motion_id, file_index)
    uint16 effect_count;              // Number of effects
    EffectRef effects[effect_count];    // (motion_id, file_index)
};

struct MotionRef {
    uint16 motion_id;     // Action ID
    uint16 file_index;    // Motion file index
};

struct EffectRef {
    uint16 motion_id;     // Trigger motion ID
    uint16 file_index;    // Effect file index
};

3.10 STB/LTB Data Table Files (.stb, .ltb)

STB (String Table) and LTB (Language Table) files are critical data files used throughout Rose Online for storing game configuration, item definitions, and localized text.

Purpose and Usage:

  • STB Files: Store structured game data (items, skills, NPCs, quests, etc.)
  • LTB Files: Store localized text strings for UI, dialogs, and messages

File Header and Structure:

STB File Structure:
Offset  Type    Description
0x00    u16     Row count
0x02    u16     Column count
0x04    u16     Row offset (first data row)
0x06    u16[]   Column offsets (column_count × 2 bytes)
0x??    Row[]   Data rows

Data Row Format:

struct StbRow {
    u16     string_length;      // Length of string data
    char    string[string_length]; // String data (UTF-8 or codepage)
    u32     unknown1;           // Additional data (format dependent)
    // Additional fields based on STB type
};

Common STB File Types:

File Name Purpose Criticality
LIST_STB.STB Item definitions Critical
LIST_SKILL.STB Skill definitions Critical
LIST_NPC.STB NPC definitions Critical
LIST_QUEST.STB Quest definitions High
LIST_STATUS.STB Status effect definitions High
LIST_DROP.STB Drop table definitions High

LTB Language Table Structure:

LTB File Structure:
Offset  Type    Description
0x00    u32     String count
0x04    u32     Language ID
0x08    Entry[] String entries

LTB Entry Format:

struct LtbEntry {
    u16     text_length;        // Length of text
    char    text[text_length];  // Localized text string
};

Relationship to Other Files:

  • CHR Files: Reference NPC IDs defined in LIST_NPC.STB
  • IFO Files: Use spawn definitions from STB tables
  • UI System: Loads all display text from LTB files
  • Quest System: References LIST_QUEST.STB for quest data

Source Code References:

  • STB/LTB parsing is typically handled by dedicated table loader classes
  • Data is cached in memory for fast lookup by ID
  • String IDs are used throughout the codebase for text references

3.11 EFT Effect Files (.eft)

EFT files define visual effects including particles, billboards, and mesh-based effects used for skills, environmental effects, and UI elements.

Purpose and Usage:

  • Skill visual effects (attacks, spells, healing)
  • Environmental effects (fire, smoke, water)
  • Character attachment effects (glows, auras)
  • UI effects (highlights, transitions)

File Structure Overview:

EFT files are binary format containing:

struct EftFile {
    char    magic[4];           // "EFT\0" or similar
    u32     version;            // Format version
    u32     emitter_count;      // Number of particle emitters
    Emitter emitters[emitter_count];
    u32     timeline_count;     // Animation timeline data
    Timeline timelines[timeline_count];
};

Emitter Data Structure:

struct Emitter {
    char    name[64];           // Emitter name
    u32     emitter_type;       // Type: Point, Box, Sphere, Mesh
    float   duration;           // Effect duration in seconds
    float   emission_rate;      // Particles per second
    u32     max_particles;      // Maximum particle count
    
    // Particle properties
    float   start_size[2];      // Min/max start size
    float   end_size[2];        // Min/max end size
    float   start_color[4];     // RGBA start color
    float   end_color[4];       // RGBA end color
    float   velocity[3];        // Initial velocity
    float   gravity;            // Gravity modifier
    float   lifetime[2];        // Min/max particle lifetime
    
    // Texture references
    u16     texture_path_len;
    char    texture_path[texture_path_len];
    
    // Blend mode
    u32     blend_mode;         // 0=Normal, 1=Additive, 2=Subtractive
};

Effect Types:

Type ID Description Use Case
0 Point Emitter Simple particle spray
1 Box Emitter Area-based emission
2 Sphere Emitter Radial emission
3 Mesh Emitter Mesh surface emission
4 Trail Effect Trailing particles
5 Beam Effect Linear beam effects

Relationship to Other Files:

  • ZSC Files: Reference EFT files for object effects
  • CHR Files: Reference EFT files for character effects
  • DDS/TGA Files: Provide textures for particles
  • IFO Files: Place effect objects in zones

Particle System Integration:

Effect Loading Pipeline:
1. Load EFT file from VFS
2. Parse emitter definitions
3. Load referenced textures
4. Create particle system instances
5. Attach to parent object or bone

3.12 Texture/Image Files (.dds, .tga, .bmp, .jpg)

Texture files provide the visual surfaces for all 3D models and UI elements in Rose Online.

DDS – DirectDraw Surface (.dds)

Primary GPU texture format – Most textures in Rose Online use DDS format.

Advantages:
  • Native GPU compression (DXT1, DXT3, DXT5)
  • Hardware-decoded for fast loading
  • Supports mipmaps for level-of-detail
  • Smaller memory footprint
DDS File Structure:
DDS File Structure:
Offset  Type    Description
0x00    char[4]  Magic "DDS "
0x04    u32      Header size (124)
0x08    u32      Flags
0x0C    u32      Height
0x10    u32      Width
0x14    u32      Pitch/linear size
0x18    u32      Depth
0x1C    u32      Mipmap count
0x20    u32[11]  Reserved
0x4C    u32      Pixel format size
0x50    u32      Pixel format flags
0x54    char[4]  FourCC (DXT1, DXT3, DXT5)
0x58    u32      RGB bit count
0x5C    u32      Red mask
0x60    u32      Green mask
0x64    u32      Blue mask
0x68    u32      Alpha mask
0x6C    u32      Caps
0x70    u32      Caps2
0x74    u32[3]   Reserved
0x80    u8[]     Texture data
Compression Formats:
FourCC Format Compression Ratio Use Case
DXT1 BC1 6:1 Opaque textures
DXT3 BC2 4:1 Sharp alpha
DXT5 BC3 4:1 Smooth alpha

TGA – Targa Image Format (.tga)

Secondary texture format – Used for terrain tiles and UI elements.

TGA File Structure:
TGA File Structure:
Offset  Type    Description
0x00    u8      ID length
0x01    u8      Color map type
0x02    u8      Image type (2 = uncompressed RGB, 10 = RLE RGB)
0x03    u16[5]  Color map specification
0x08    u16     X origin
0x0A    u16     Y origin
0x0C    u16     Width
0x0E    u16     Height
0x10    u8      Pixel depth (16, 24, 32)
0x11    u8      Image descriptor
Common Usage:
  • Terrain tile textures
  • UI elements and icons
  • Uncompressed textures requiring exact color

BMP – Bitmap Images (.bmp)

Screenshot format – Used primarily for screenshots and simple images.

BMP File Structure:
BMP File Structure:
Offset  Type    Description
0x00    char[2]  Magic "BM"
0x02    u32      File size
0x06    u16[2]   Reserved
0x0A    u32      Pixel data offset
0x0E    u32      Header size (40 for BITMAPINFOHEADER)
0x12    u32      Width
0x16    u32      Height
0x1A    u16      Planes (1)
0x1C    u16      Bits per pixel (16, 24, 32)
0x1E    u32      Compression (0 = none)
0x22    u32      Image size
0x26    u32      X pixels per meter
0x2A    u32      Y pixels per meter
0x2E    u32      Colors used
0x32    u32      Important colors
0x36    u8[]     Pixel data

JPG – JPEG Images (.jpg)

Alternative screenshot format – Compressed screenshots and web-compatible images.

Usage:
  • Compressed screenshots
  • Web-compatible image exports
  • Lower quality but smaller file size

Texture Loading Pipeline:

1. VFS receives texture path request
2. Determine format from extension
3. Load appropriate decoder:
   - DDS: Direct GPU upload (native format)
   - TGA: Decode to RGBA, upload to GPU
   - BMP: Decode to RGBA, upload to GPU
   - JPG: JPEG decode, upload to GPU
4. Generate mipmaps if not present
5. Apply to material/shader

Relationship to Other Files:

  • ZSC Materials: Reference texture file paths
  • ZON Textures: Reference terrain texture paths
  • EFT Files: Reference particle texture paths

3.13 Audio Files (.wav, .ogg, .mp3)

Audio files provide sound effects and music for the game.

WAV – Wave Audio (.wav)

Primary sound effect format – Used for all game sound effects.

WAV File Structure:
WAV File Structure (RIFF format):
Offset  Type    Description
0x00    char[4]  "RIFF"
0x04    u32      File size - 8
0x08    char[4]  "WAVE"
0x0C    char[4]  "fmt " (format chunk)
0x10    u32      Format chunk size (16)
0x14    u16      Audio format (1 = PCM)
0x16    u16      Channel count
0x18    u32      Sample rate
0x1C    u32      Byte rate
0x20    u16      Block align
0x22    u16      Bits per sample
0x24    char[4]  "data" (data chunk)
0x28    u32      Data size
0x2C    u8[]     Audio samples
Usage:
  • UI sounds (clicks, notifications)
  • Character sounds (footsteps, attacks)
  • Environmental sounds (ambient, weather)
  • Skill sound effects

OGG – Ogg Vorbis Audio (.ogg)

Primary music format – Background music and long audio tracks.

Advantages:
  • High quality compression
  • Smaller file size than MP3 at equivalent quality
  • Open source, royalty-free
  • Streaming support
OGG Structure:

Ogg Vorbis uses a container format with multiple pages:

Ogg Page Structure:
Offset  Type    Description
0x00    char[4]  "OggS" (capture pattern)
0x04    u8       Version (0)
0x05    u8       Header type
0x06    u64      Granule position
0x0E    u32      Serial number
0x12    u32      Page sequence
0x16    u32      CRC checksum
0x1A    u8       Segment count
0x1B    u8[]     Segment table
0x??    u8[]     Segment data
Usage:
  • Background music (BGM)
  • Zone music
  • Long environmental audio loops

MP3 – MPEG Audio Layer 3 (.mp3)

Alternative music format – Alternative compressed audio format.

Usage:
  • Alternative BGM format
  • Imported audio assets
  • Lower priority than OGG in asset loading

Audio Loading Pipeline:

1. VFS receives audio path request
2. Determine format from extension
3. Load appropriate decoder:
   - WAV: Direct PCM playback
   - OGG: Ogg Vorbis streaming decode
   - MP3: MPEG audio decode
4. Create audio buffer (sound effects) or stream (music)
5. Register with audio system

Relationship to Other Files:

  • IFO Sound Objects: Reference sound file paths
  • ZMO Frame Events: Trigger sound effects at animation frames
  • UI Definitions: Reference UI sound effects

3.14 LUA Script Files (.lua)

Lua scripts provide game logic, UI behavior, and event handling for Rose Online.

Purpose and Usage:

  • Game Logic: Quest scripts, NPC behavior, event handlers
  • UI System: Window definitions, button handlers, layout scripts
  • Configuration: Game settings, difficulty tuning, drop rates
  • Event System: Trigger handlers, timed events, conditional logic

Lua Integration Architecture:

Lua Integration:
┌─────────────────────────────────────────┐
│           Game Engine (C++)              │
├─────────────────────────────────────────┤
│         Lua Script Engine                │
│  ┌─────────────────────────────────┐    │
│  │   Standard Lua Libraries        │    │
│  │   - base, math, string, table   │    │
│  └─────────────────────────────────┘    │
│  ┌─────────────────────────────────┐    │
│  │   Rose Online API Bindings      │    │
│  │   - Character functions         │    │
│  │   - Item functions              │    │
│  │   - Quest functions             │    │
│  │   - UI functions                │    │
│  │   - Network functions           │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

Common Script Categories:

Category Purpose Example Files
Quest Quest logic and conditions quest_*.lua
NPC NPC behavior and dialogs npc_*.lua
UI Window and widget scripts ui_.lua, window_.lua
Skill Skill effect scripts skill_*.lua
Event Game event handlers event_*.lua
Config Configuration settings config_*.lua

Script Loading Pipeline:

1. VFS receives Lua script path request
2. Load script file as text
3. Compile Lua chunk
4. Execute in sandboxed environment
5. Register functions and callbacks
6. Link to game events

Example Script Structure:

-- Quest script example
function Quest_Start(player, quest_id)
    -- Initialize quest state
    player:SetQuestVar(quest_id, "stage", 1)
    player:SetQuestVar(quest_id, "kills", 0)
    return true
end

function Quest_Update(player, quest_id, event_type, event_data)
    if event_type == "KILL_MONSTER" then
        local kills = player:GetQuestVar(quest_id, "kills")
        player:SetQuestVar(quest_id, "kills", kills + 1)
        
        if kills >= 10 then
            player:SetQuestVar(quest_id, "stage", 2)
        end
    end
end

function Quest_Complete(player, quest_id)
    player:GiveItem(1001, 1)  -- Give reward item
    player:GiveExp(1000)      -- Give experience
    return true
end

Relationship to Other Files:

  • STB/LTB Files: Quest data referenced by scripts
  • IFO Files: Event triggers that call Lua scripts
  • CHR Files: NPC scripts for behavior
  • UI Files: UI scripts for interface behavior

3.15 PKG Archive Files (.pkg)

PKG files are the Virtual File System (VFS) container format used to package game assets into compressed archives.

Purpose and Usage:

  • Asset Distribution: All game assets distributed in PKG format
  • File Organization: Hierarchical directory structure within archives
  • Compression: Reduce disk space and speed up loading
  • Protection: Basic obfuscation of game assets

PKG File Structure:

PKG File Structure:
Offset  Type    Description
0x00    char[4]  Magic "PKG" + version
0x04    u32      Header size
0x08    u32      File count
0x0C    u32      Directory count
0x10    u32      String table offset
0x14    u32      String table size
0x18    u32      Entry table offset
0x1C    u32      Data offset

Directory Entry Structure:

struct PkgDirectory {
    u32     name_offset;       // Offset in string table
    u32     parent_index;      // Parent directory index
    u32     first_child;       // First child directory
    u32     next_sibling;      // Next sibling directory
    u32     first_file;        // First file in directory
};

File Entry Structure:

struct PkgFileEntry {
    u32     name_offset;       // Offset in string table
    u32     directory_index;   // Parent directory index
    u32     data_offset;       // Offset in data section
    u32     uncompressed_size; // Original file size
    u32     compressed_size;   // Compressed size (0 = uncompressed)
    u32     crc32;             // CRC32 checksum
    u16     compression_type;  // 0 = none, 1 = zlib
    u16     flags;             // Additional flags
};

Compression Types:

Type ID Compression Description
0 None Uncompressed data
1 Zlib Standard zlib/deflate
2 LZ77 LZ77 variant (rare)

VFS Integration:

VFS Device Stack:
┌─────────────────────────────────────────┐
│           Application Layer             │
├─────────────────────────────────────────┤
│         Virtual Filesystem              │
│  ┌─────────────────────────────────┐    │
│  │   Host Filesystem Device        │    │
│  │   - Direct file access          │    │
│  └─────────────────────────────────┘    │
│  ┌─────────────────────────────────┐    │
│  │   PKG Archive Device 1          │    │
│  │   - data.pkg                    │    │
│  └─────────────────────────────────┘    │
│  ┌─────────────────────────────────┐    │
│  │   PKG Archive Device 2          │    │
│  │   - update.pkg                  │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

File Lookup Process:

1. Normalize path (uppercase, forward slashes)
2. Query each VFS device in order:
   a. Check if path exists in device
   b. If PKG device:
      - Look up directory in directory table
      - Find file in file entry table
      - Read data offset and size
      - Decompress if needed
   c. Return first successful result
3. Cache result if applicable

Common PKG Files:

File Name Contents Priority
data.pkg Core game assets Low
update.pkg Patch/update assets High
event.pkg Event-specific assets Medium
ui.pkg UI assets Low

Relationship to Other Files:

  • All Asset Types: All file types (.zms, .zmd, .zmo, etc.) are stored within PKG archives
  • VFS Layer: PKG files are mounted as VFS devices
  • Loading Order: Later PKG files override earlier ones (patch system)

4. Edge Cases and Format Variations

Version Differences and Compatibility:

  • ZMS v5/6: Vertex IDs, bone index mapping, scaling
  • ZMS v7/8: No vertex IDs, direct bone indices, no scaling
  • ZMD v2: Dummy bones have identity rotation
  • ZMD v3: Dummy bones have explicit rotation

Missing or Optional Data Handling:

  • Use default values for missing attributes
  • Skip empty arrays gracefully
  • Validate data bounds before access

Invalid or Corrupted File Detection:

  • Check magic strings
  • Validate version numbers
  • Verify data bounds
  • Check for read errors

Format Extensions and Customizations:

  • EZMO/3ZMO extended animation format
  • Custom collision shapes in ZSC
  • Additional property IDs in ZSC

5. Quick Reference

5.1 File Format Summary Table

Core Asset Formats:

Format Magic Versions Key Features Complexity Criticality
ZMS ZMS000X 5-8 Mesh, skinning, materials High Critical
ZMD ZMD000X 2-3 Skeleton, dummy bones Medium Critical
ZMO ZMO0002 2 Animation, frame events Medium Critical
ZON None Zone, tiles, events High Critical
ZSC None Objects, materials, effects High Critical
IFO None Object placement, spawns High Critical
HIM None Heightmap Low High
TIL None Tile indices Low High
CHR None Character definitions Medium Critical

Data Table Formats:

Format Magic Versions Key Features Complexity Criticality
STB None Game data tables Low Critical
LTB None Localization strings Low Critical

Effect Formats:

Format Magic Versions Key Features Complexity Criticality
EFT EFT 1+ Particle effects Medium High

Texture Formats:

Format Magic Versions Key Features Complexity Criticality
DDS DDS GPU textures, DXT Low Critical
TGA None Terrain/UI textures Low High
BMP BM Screenshots Low Medium
JPG 0xFFD8 Compressed images Low Medium

Audio Formats:

Format Magic Versions Key Features Complexity Criticality
WAV RIFF Sound effects, PCM Low Critical
OGG OggS Background music Medium Critical
MP3 ID3 Alternative music Medium Medium

Script Formats:

Format Magic Versions Key Features Complexity Criticality
LUA None 5.x Game logic, UI Medium Critical

Archive Formats:

Format Magic Versions Key Features Complexity Criticality
PKG PKG 1+ VFS container, zlib Medium Critical

5.2 Key Constants and Conversions

Scaling Factors:

MESH_SCALE = 100.0      # ZMS v5/6 position scaling
CM_TO_M = 0.01         # Centimeters to meters

Binary Data Type Sizes:

Type Size
u8 1 byte
u16 2 bytes
u32 4 bytes
f32 4 bytes
Vec3 12 bytes
Quat4 16 bytes

5.3 Troubleshooting Guide

Common Import Errors:

Error Cause Solution
Invalid magic Wrong file type Check file extension
Version mismatch Unsupported version Update parser
Bone index out of range Corrupted data Validate indices
Missing vertex data Format flags wrong Check flags

Asset Loading Workflow:

    participant App
    participant VFS
    participant ZON
    participant ZMS
    participant ZMD
    participant ZMO

    App->>VFS: Load Zone
    VFS->>ZON: Read ZON file
    ZON-->>VFS: Zone data

    App->>VFS: Load Mesh
    VFS->>ZMS: Read ZMS file
    ZMS-->>VFS: Mesh data

    App->>VFS: Load Skeleton
    VFS->>ZMD: Read ZMD file
    ZMD-->>VFS: Skeleton data

    App->>VFS: Load Animation
    VFS->>ZMO: Read ZMO file
    ZMO-->>VFS: Animation data

Bone Hierarchy Structure:

    Root[Bone 0: Root] --> Bone1[Bone 1]
    Root --> Bone2[Bone 2]
    Bone1 --> Bone3[Bone 3]
    Bone1 --> Bone4[Bone 4]
    Bone2 --> Bone5[Bone 5]

Animation Interpolation Pipeline:

Frame1[Frame 1] -->|t=0.0| Interp[Interpolation]
Frame2[Frame 2] -->|t=1.0| Interp
Interp -->|Linear/Slerp| Result[Animated Pose]

Zone Composition System:

ZON[ZON File] –> Tiles[Tiles Block] ZON –> Textures[Textures Block] ZON –> Events[EventPositions Block]

Tiles –> HIM[HIM Heightmap] Tiles –> TIL[TIL Tile Index]

Textures –> TextureFiles[.DDS/.TGA Files]

Events –> Spawns[Spawn Points]




### 6. Glossary

#### Technical Terminology:

*   **VFS**: Virtual Filesystem – Abstracted file access layer
*   **ECS**: Entity Component System – Architecture pattern
*   **WGSL**: WebGPU Shading Language – Shader language
*   **Skinned Mesh**: Mesh with bone-based vertex deformation
*   **Inverse Bind Matrix**: Matrix transforming from bind pose to bone space

#### Rose Online-Specific Terms:

*   **ZMS**: Z-Model-Structure – Mesh file format
*   **ZMD**: Z-Model-Definition – Skeleton file format
*   **ZMO**: Z-Motion-Object – Animation file format
*   **ZON**: Z-One – Zone file format
*   **ZSC**: Z-Structure-Composition – Object composition format
*   **IFO**: Information File Object – Zone object placement format
*   **HIM**: Height Map – Terrain elevation format
*   **TIL**: Tile – Terrain tile index format
*   **CHR**: Character – Character definition format