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
widthandheightfields 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