Rose Online Asset File Format Technical Specification Part 2

This is part 2 of the Rose Online Asset File Format Technical Specification document. Part 1 can be found here.

In part 1 we stopped right before getting into the IFO file format.

Table of Contents

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 TypeGrid SizePatch CountWorld Coverage
Small Zone65 × 6564 × 64 patches~64m × 64m
Standard Zone129 × 129128 × 128 patches~128m × 128m
Large Zone257 × 257256 × 256 patches~256m × 256m
City/Indoor33 × 3332 × 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 CoordinateWorld CoordinateNotes
grid_xworld_x / patch_sizeX axis mapping
grid_yworld_z / patch_sizeZ axis (forward direction)
height[x][y]world_yY is up axis
Origin (0,0)Zone originBottom-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:

PropertyValueDescription
Origin(0, 0)Bottom-left corner of zone
X-AxistileXEast direction (increasing)
Y-AxistileYNorth direction (increasing)
Max Xwidth – 1Eastern boundary
Max Yheight – 1Northern 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 NamePurposeCriticality
LIST_STB.STBItem definitionsCritical
LIST_SKILL.STBSkill definitionsCritical
LIST_NPC.STBNPC definitionsCritical
LIST_QUEST.STBQuest definitionsHigh
LIST_STATUS.STBStatus effect definitionsHigh
LIST_DROP.STBDrop table definitionsHigh

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 IDDescriptionUse Case
0Point EmitterSimple particle spray
1Box EmitterArea-based emission
2Sphere EmitterRadial emission
3Mesh EmitterMesh surface emission
4Trail EffectTrailing particles
5Beam EffectLinear 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:
FourCCFormatCompression RatioUse Case
DXT1BC16:1Opaque textures
DXT3BC24:1Sharp alpha
DXT5BC34:1Smooth 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:

CategoryPurposeExample Files
QuestQuest logic and conditionsquest_*.lua
NPCNPC behavior and dialogsnpc_*.lua
UIWindow and widget scriptsui_.lua, window_.lua
SkillSkill effect scriptsskill_*.lua
EventGame event handlersevent_*.lua
ConfigConfiguration settingsconfig_*.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 IDCompressionDescription
0NoneUncompressed data
1ZlibStandard zlib/deflate
2LZ77LZ77 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 NameContentsPriority
data.pkgCore game assetsLow
update.pkgPatch/update assetsHigh
event.pkgEvent-specific assetsMedium
ui.pkgUI assetsLow

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:

FormatMagicVersionsKey FeaturesComplexityCriticality
ZMSZMS000X5-8Mesh, skinning, materialsHighCritical
ZMDZMD000X2-3Skeleton, dummy bonesMediumCritical
ZMOZMO00022Animation, frame eventsMediumCritical
ZONNoneZone, tiles, eventsHighCritical
ZSCNoneObjects, materials, effectsHighCritical
IFONoneObject placement, spawnsHighCritical
HIMNoneHeightmapLowHigh
TILNoneTile indicesLowHigh
CHRNoneCharacter definitionsMediumCritical

Data Table Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
STBNoneGame data tablesLowCritical
LTBNoneLocalization stringsLowCritical

Effect Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
EFTEFT1+Particle effectsMediumHigh

Texture Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
DDSDDSGPU textures, DXTLowCritical
TGANoneTerrain/UI texturesLowHigh
BMPBMScreenshotsLowMedium
JPG0xFFD8Compressed imagesLowMedium

Audio Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
WAVRIFFSound effects, PCMLowCritical
OGGOggSBackground musicMediumCritical
MP3ID3Alternative musicMediumMedium

Script Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
LUANone5.xGame logic, UIMediumCritical

Archive Formats:

FormatMagicVersionsKey FeaturesComplexityCriticality
PKGPKG1+VFS container, zlibMediumCritical

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:

TypeSize
u81 byte
u162 bytes
u324 bytes
f324 bytes
Vec312 bytes
Quat416 bytes

5.3 Troubleshooting Guide

Common Import Errors:

ErrorCauseSolution
Invalid magicWrong file typeCheck file extension
Version mismatchUnsupported versionUpdate parser
Bone index out of rangeCorrupted dataValidate indices
Missing vertex dataFormat flags wrongCheck 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