This document will serve as a technical reference for the very old MMORPG Rose Online. The goal is that this information is never lost and may help decades later.
Table of Contents
1. Introduction
1.1 Rose Online Asset System Overview
Rose Online uses a sophisticated asset management system organized around a Virtual Filesystem (VFS) architecture. Assets are stored in proprietary binary formats optimized for real-time rendering in the game engine.
Key Characteristics:
- Coordinate System: Y-up, centimeter-based (cm)
- File Format Versions: Multiple format versions with backward compatibility
- Asset Organization: Hierarchical structure with clear separation of concerns
- Rendering Pipeline: GPU-accelerated with skinned mesh support
Asset File Extensions:
Core 3D Asset Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .zms | Mesh geometry | Character models, objects, terrain tiles | Critical |
| .zmd | Skeleton definition | Bone hierarchies for skinned meshes | Critical |
| .zmo | Motion/animation | Character animations, object animations | Critical |
| .zon | Zone definition | Terrain composition, grid system | Critical |
| .zsc | Object composition | Multi-part object assembly | Critical |
| .ifo | Zone object placement | NPC spawns, objects, events | Critical |
| .him | Heightmap | Terrain elevation data | High |
| .til | Tile index | Terrain tile mapping | High |
| .chr | Character definition | NPC and character asset references | Critical |
Data Table Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .stb | String/Data tables | Game data (items, skills, NPCs) | Critical |
| .ltb | Language tables | UI text, dialogs, localization | Critical |
Effect Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .eft | Effect definitions | Visual effects, particles | High |
Texture Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .dds | DirectDraw Surface | Primary GPU texture format | Critical |
| .tga | Targa image | Terrain/UI textures | High |
| .bmp | Bitmap image | Screenshots | Medium |
| .jpg | JPEG image | Screenshots, compressed images | Medium |
Audio Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .wav | Wave audio | Sound effects | Critical |
| .ogg | Ogg Vorbis audio | Background music | Critical |
| .mp3 | MP3 audio | Alternative music format | Medium |
Script Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .lua | Lua scripts | Game logic, UI, event handlers | Critical |
Archive Formats:
| Extension | Description | Primary Use | Criticality |
|---|---|---|---|
| .pkg | Package/archive | VFS container format | Critical |
1.2 File Format Relationships
graph TD
ZON[ZON Zone File] -->|References| TIL[TIL Tile Index]
ZON -->|Uses| HIM[HIM Heightmap]
ZON -->|Contains| Tiles[Tiles Block]
IFO[IFO Zone Object File] -->|Instantiates| ZSC[ZSC Object Files]
IFO -->|References| CHR[CHR Character File]
IFO -->|Places| Objects[Zone Objects]
CHR -->|References| ZMD[ZMD Skeleton]
CHR -->|References| ZSC
CHR -->|References| ZMO[ZMO Animation]
CHR -->|Maps| Motions[Motion Actions]
ZSC -->|Contains| ZMS[ZMS Mesh]
ZSC -->|Contains| Materials[ZSC Materials]
ZSC -->|Defines| Parts[Object Parts]
ZSC -->|Contains| Effects[Effect References]
ZMS -->|Requires| ZMD
ZMS -->|Contains| Vertices[Vertex Data]
ZMS -->|Contains| Indices[Index Data]
ZMS -->|Contains| BoneWeights[Bone Weights]
ZMO -->|Targets| ZMD bones
ZMO -->|Contains| Channels[Animation Channels]
ZMO -->|Contains| FrameEvents[Frame Events]
ZMO -->|Extended| EZMO[EZMO/3ZMO]
ZMD -->|Contains| Bones[Regular Bones]
ZMD -->|Contains| DummyBones[Dummy Bones]
HIM -->|Provides| Heights[Terrain Heights]
TIL -->|Provides| Indices[Tile Indices]
Complete Asset Loading Workflows
Zone Loading and Composition Workflow:
1. Load ZON File
├─ Parse ZoneInfo block (grid configuration)
├─ Parse EventPositions block (spawn points)
├─ Parse Textures block (texture paths)
└─ Parse Tiles block (tile composition)
2. Load Terrain Data
├─ Load HIM file (heightmap)
├─ Load TIL file (tile indices)
└─ Generate terrain mesh from ZON tiles + HIM heights
3. Load IFO File
├─ Parse object blocks (NPCs, monsters, decorations)
├─ Parse spawn points
├─ Parse warp positions
└─ Parse water planes
4. Load Zone Objects
├─ For each IFO object:
│ ├─ Load ZSC file (object composition)
│ ├─ For each ZSC part:
│ │ ├─ Load ZMS mesh file
│ │ ├─ Load ZSC material
│ │ ├─ Apply material to mesh
│ │ └─ Parent to object entity
│ └─ For each ZSC effect:
│ └─ Load and spawn effect
└─ Place object at IFO position with transform
Character/NPC Assembly Pipeline:
1. Load CHR File
├─ Parse skeleton file list
├─ Parse motion file list
├─ Parse effect file list
└─ Parse character definitions
2. For Character/NPC:
├─ Load ZMD skeleton file
│ ├─ Parse bone hierarchy
│ ├─ Create bone entities
│ ├─ Compute bind pose matrices
│ ├─ Compute inverse bind matrices
│ └─ Create SkinnedMesh component
│
├─ Load ZSC object files (one per body part)
│ ├─ For each ZSC object:
│ │ ├─ Parse material definitions
│ │ ├─ Parse object parts
│ │ ├─ Parse effect definitions
│ │ └─ Load ZMS mesh files
│
├─ Load ZMS mesh files
│ ├─ Parse vertex data (positions, normals, UVs)
│ ├─ Parse bone weights and indices
│ ├─ Parse index data
│ └─ Create mesh entities
│
├─ Bind meshes to skeleton
│ ├─ Attach to bone entities (or dummy bones)
│ ├─ Apply SkinnedMesh component
│ └─ Set material properties
│
├─ Load ZMO animation files
│ ├─ Parse frame data
│ ├─ Parse channel data (position, rotation, scale)
│ ├─ Parse frame events
│ └─ Create animation handles
│
└─ Attach effects to dummy bones
├─ Load effect files from CHR
├─ Spawn effect entities
└─ Parent to dummy bone entities
Animation Application Pipeline:
1. Load ZMO Animation File
├─ Parse FPS and frame count
├─ Parse channel definitions
├─ Parse frame data per channel
└─ Parse frame events
2. Create ZmoAsset
├─ Organize channels by bone ID
├─ Store translation vectors per frame
├─ Store rotation quaternions per frame
├─ Store scale values per frame
└─ Store frame events
3. Animation Playback
├─ Calculate current frame from time
├─ Get frame fraction for interpolation
├─ Sample bone transforms:
│ ├─ Position: Linear interpolation
│ ├─ Rotation: Quaternion slerp
│ └─ Scale: Linear interpolation
├─ Apply transforms to bone entities
└─ Trigger frame events
4. Skinned Mesh Update
├─ GPU computes vertex positions
├─ Using bone weights and current pose
├─ Apply skinning matrices
└─ Render final mesh
Detailed File Type Interactions:
ZON ↔ HIM/TIL Interaction:
Purpose: Generate terrain geometry from zone definition
Data Flow:
ZON File:
├─ Grid per patch (float)
├─ Grid size (float)
├─ Tile textures (array of paths)
└─ Tiles (array of ZonTile)
├─ Layer1 texture index
├─ Layer2 texture index
├─ Offset1, Offset2 (UV offsets)
├─ Blend flag
└─ Rotation enum
HIM File:
├─ Width, Height (grid dimensions)
└─ Heights array (width × height floats)
TIL File:
├─ Width, Height (grid dimensions)
└─ Tiles array (width × height indices)
Terrain Generation:
For each grid cell (x, y):
├─ Get tile index from TIL[x][y]
├─ Get tile definition from ZON.tiles[tile_index]
├─ Get texture path from ZON.textures[tile.layer1]
├─ Get height from HIM[x][y]
├─ Apply tile rotation and offset
├─ Generate vertices with height
└─ Apply texture coordinates
Client Implementation (observed in rendering code):
- Terrain is rendered as a grid of tiles
- Each tile is a quad with 4 vertices
- Vertices are positioned using HIM heights
- UVs are computed from TIL tile indices and ZON tile data
- Layer blending is applied if tile.blend is true
IFO ↔ ZSC/ZMS/ZMD Interaction:
Purpose: Place objects in zone with correct models and transforms
Data Flow:
IFO File:
├─ Object type (Deco, NPC, MonsterSpawn, etc.)
├─ Object ID (references CHR or ZSC)
├─ Position (X, Y, Z)
├─ Rotation (quaternion XYZW)
├─ Scale (X, Y, Z)
└─ Additional type-specific data
For NPC Objects:
├─ Load CHR file by NPC ID
│ ├─ Get skeleton file path
│ ├─ Get model IDs
│ ├─ Get motion IDs
│ └─ Get effect IDs
│
├─ Load ZMD skeleton
├─ Load ZSC model files (from CHR.model_ids)
│ └─ For each ZSC object:
│ ├─ Load ZMS meshes
│ ├─ Load materials
│ └─ Create mesh entities
│
└─ Load ZMO animations (from CHR.motion_ids)
└─ Create animation handles
For Decoration Objects:
├─ Load ZSC file directly (from IFO.object_id)
└─ Same process as above
CHR ↔ ZMD/ZSC/ZMO Interaction:
Purpose: Define character/NPC asset composition and animations
Data Flow:
CHR File:
├─ Skeleton files (array of paths)
├─ Motion files (array of paths)
├─ Effect files (array of paths)
└─ Character definitions (map: ID → NpcModelData)
NpcModelData:
├─ Skeleton index (into skeleton_files)
├─ Name
├─ Model IDs (array: into ZSC objects)
├─ Motion IDs (array of tuples: (motion_id, file_index))
└─ Effect IDs (array of tuples: (motion_id, file_index))
ZSC ↔ ZMS/Material Interaction:
Purpose: Define multi-part objects with materials and transforms
Data Flow:
ZSC File:
├─ Meshes (array of paths to ZMS files)
├─ Materials (array of ZscMaterial)
├─ Effects (array of paths to effect files)
└─ Objects (array of ZscObject)
ZscObject:
└─ Parts (array of ZscObjectPart)
ZscObjectPart:
├─ Mesh ID (into meshes array)
├─ Material ID (into materials array)
├─ Transform (position, rotation, scale)
├─ Bone index (optional: bind to skeleton bone)
├─ Dummy index (optional: bind to dummy bone)
├─ Parent index (optional: parent part)
└─ Animation path (optional: animation override)
ZscMaterial:
├─ Path (texture file)
├─ Alpha enabled, two-sided
├─ Alpha test (threshold)
├─ Z write/test enabled
├─ Blend mode
├─ Specular enabled
├─ Alpha value
└─ Glow type and color
ZMO ↔ ZMD Interaction:
Purpose: Drive skeleton bones with animation data
Data Flow:
ZMO File:
├─ FPS (frames per second)
├─ Frame count
├─ Channels (array of (bone_id, channel_type))
└─ Frame events (array of frame numbers)
ZmoChannel Types:
├─ Position: Vec3 per frame
├─ Rotation: Quat4 per frame
├─ Scale: f32 per frame
├─ Normal: Vec3 per frame (morph targets)
├─ UV1-UV4: Vec2 per frame (texture animation)
├─ Alpha: f32 per frame
└─ Texture: f32 per frame
ZMS ↔ ZMD Interaction:
Purpose: Bind mesh vertices to skeleton bones for skinning
Data Flow:
ZMS File:
├─ Format flags (vertex attributes)
├─ Bone count
├─ Bone mapping (version 5/6 only)
├─ Vertex count
├─ Vertex data (based on flags):
│ ├─ Positions (Vec3)
│ ├─ Normals (Vec3)
│ ├─ Colors (Vec4)
│ ├─ Bone weights (Vec4: w1, w2, w3, w4)
│ ├─ Bone indices (Vec4: i1, i2, i3, i4)
│ ├─ Tangents (Vec3)
│ └─ UV1-UV4 (Vec2)
├─ Triangle indices (u16)
└─ Material face counts (u16)
Effect System Integration:
Purpose: Attach particle and mesh effects to objects
Data Flow:
CHR File:
└─ Effect IDs (array of tuples: (motion_id, file_index))
ZSC File:
└─ Effects (array of paths to effect files)
##### Vehicle Assembly Pipeline:
**Purpose**: Assemble vehicle models with skeleton and parts
**Data Flow**:
Vehicle Item Database:
└─ Vehicle item data
├─ Vehicle type (Cart/CastleGear)
├─ Base motion index
├─ Base avatar motion index
└─ Dummy effect file IDs
Client-Side Rendering Integration Summary:
The Rose Offline client integrates all file types through a hierarchical asset loading system:
- VFS Layer: Unified file access across packed archives and host filesystem
- Asset Server: Asynchronous loading and caching of assets
- ECS Components:
SkinnedMesh: Bone binding data (inverse bind matrices, joint entities)CharacterModel: Character parts and animation handlesNpcModel: NPC-specific dataVehicleModel: Vehicle-specific dataObjectMaterial: Material properties and textures
- Systems: Update bone poses, apply animations, render skinned meshes
Coordinate Transformations Applied in Client:
The Rose Online client applies coordinate transformations to convert from the game’s internal coordinate system to the rendering engine’s coordinate system:
-
Position Transformation:
(x, y, z) → (x, z, -y) / 100.0- Swaps Y and Z axes
- Negates the new Y (which was the original Z)
- Divides by 100 to convert from centimeters to meters
-
Rotation Transformation:
(w, x, y, z) → (x, z, -y, w)- Reorders quaternion components to match the new coordinate system
- Applies the same Y-Z swap and negation as positions
-
Scale Factor: 100.0 (cm to m conversion)
- Applied to all position data during loading
2. Core Systems
2.1 Virtual Filesystem (VFS) Abstraction Layer
Architecture Overview:
The VFS provides a unified interface for accessing game assets across multiple storage devices (packed archives, host filesystem).
Key Components:
The VFS is implemented as a C++ class hierarchy with protocol-based file access:
// Base VFS class
class zz_vfs {
public:
typedef enum {
ZZ_VFS_READ = (1 << 0),
ZZ_VFS_WRITE = (1 << 1),
} zz_vfs_mode;
typedef enum {
ZZ_VFS_LOCAL = 0,
ZZ_VFS_HTTP,
ZZ_VFS_FTP,
ZZ_VFS_ZIP,
ZZ_VFS_MEM,
ZZ_VFS_PKG, // TriggerVFS package file system (default)
} zz_vfs_protocol;
virtual bool open(const char* filename, zz_vfs_mode mode = ZZ_VFS_READ);
virtual bool close(void);
virtual uint32 read(char* buf, uint32 size);
// ... other methods
};
// Derived filesystem implementations
class zz_vfs_local : public zz_vfs { /* Local filesystem */ };
class zz_vfs_pkg : public zz_vfs { /* Package archive filesystem */ };
Path Normalization:
All paths are normalized to:
- Forward slashes (
/) - Uppercase
- No leading/trailing whitespace
- No double slashes
Example: 3DDATA\AVATAR\MALE.ZMD → 3DDATA/AVATAR/MALE.ZMD
File Loading Mechanism:
// Open and read a file
zz_vfs file;
if (file.open("3DDATA/MODELS/CHARACTER.ZMS", zz_vfs::ZZ_VFS_READ)) {
uint32 size = file.get_size();
char* data = new char[size];
file.read(data, size);
// Process data...
file.close();
}
// Using package filesystem directly
zz_vfs_pkg pkg_file;
if (pkg_file.open("DATA/ZONES/01.ZON")) {
// Read file data from package
uint32 size = pkg_file.get_size();
// ...
}
Caching Strategy:
- Assets are loaded on-demand
- VFS devices are queried in order
- First successful read wins
- No built-in caching (handled by application layer)
2.2 Coordinate System Conversions
Rose Online Coordinate System:
- Units: Centimeters (cm)
- Up Axis: Y-up
- Handedness: Right-handed
- Forward: -Z direction
- Right: +X direction
Coordinate System Mappings:
| Rose Online Axis | Notes |
|---|---|
| +X | Same direction |
| +Y | Client rotates Y to Z |
| +Z | Client inverts Z to -Y |
2.3 Bone Binding Matrices and Skinning
Bone Hierarchy Structure:
Bones form a tree structure where each bone has a parent index. The root bone is identified when a bone’s parent index equals its own index (parent_id == bone_index).
struct ZmdBone {
uint32 parent_id; // Parent bone index (parent_id == bone_index for root)
char name[]; // Null-terminated bone name
float position[3]; // X, Y, Z (cm, scaled to meters on load)
float rotation[4]; // W, X, Y, Z (quaternion)
};
Bone-to-Vertex Binding System:
Each vertex can be bound to up to 4 bones with weights.
// Vertex bone binding data
float bone_weights[4]; // Weight for each of 4 bones
uint16_t bone_indices[4]; // Bone indices (mapped to skeleton)
Weight Normalization: The 4 weights should sum to 1.0 for proper skinning.
Skinning Transformation Pipeline:
- Bind Pose: Bone positions at rest (from ZMD)
- Inverse Bind Matrix: Pre-computed for each bone
- Current Pose: Bone transformation from animation (ZMO)
- Skinning Matrix:
CurrentPose × InverseBindMatrix - Vertex Transformation:
Σ (Weight_i × SkinningMatrix_i × VertexPosition)
Bone Pose Matrices and Bone Spaces:
Bone Spaces:
- Local Space: Position/rotation relative to parent
- Object Space: Position/rotation relative to mesh origin
- World Space: Position/rotation in game world
2.4 Animation Track Interpolation
Frame-Based Animation System:
Animations are stored as discrete frames with fixed FPS.
class ZmoFile {
public:
uint32 fps; // Frames per second (typically 30)
uint32 num_frames; // Total frame count
std::vector<std::pair<uint32, ZmoChannel>> channels; // (bone_index, channel_data)
std::vector<uint16> frame_events; // Frame event triggers
uint32 interpolation_interval_ms; // For 3ZMO format (0 if not used)
};
Channel Types and Data Structures:
| Channel Type | Value | Data Type | Purpose |
|---|---|---|---|
| Empty | 1 | – | No animation data |
| Position | 2 | Vec3 | Bone position |
| Rotation | 4 | Quat4 | Bone rotation (WXYZ) |
| Normal | 8 | Vec3 | Vertex normal |
| Alpha | 16 | f32 | Transparency |
| UV1 | 32 | Vec2 | First UV set |
| UV2 | 64 | Vec2 | Second UV set |
| UV3 | 128 | Vec2 | Third UV set |
| UV4 | 256 | Vec2 | Fourth UV set |
| Texture | 512 | f32 | Texture animation |
| Scale | 1024 | f32 | Scale factor |
Frame Events and Interpolation Intervals:
Frame Events:
- Triggered at specific frames
- Used for sound effects, particle spawns, etc.
- Event IDs: 10, 20-28, 56-57, 66-67 (attack events)
3ZMO Extended Format:
- Includes
interpolation_interval_msfield - Specifies custom interpolation timing
- Allows for variable frame rates
2.5 Material System
Material Properties and Flags:
class ZscMaterial {
public:
std::string path; // Material file path
bool is_skin; // Skin/shader type
bool alpha_enabled; // Transparency enabled
bool two_sided; // Double-sided rendering
float alpha_test; // Alpha test threshold (0.0 if disabled)
bool z_write_enabled; // Depth write
bool z_test_enabled; // Depth test
ZscMaterialBlend blend_mode; // Blending mode
bool specular_enabled; // Specular highlights
float alpha; // Global alpha
ZscMaterialGlow glow; // Glow effect
};
Blend Modes and Transparency:
| Blend Mode | Value | Description |
|---|---|---|
| Normal | 0 | Standard alpha blending |
| Lighten | 1 | Additive blending |
Alpha Test:
- Threshold value (0-1) for pixel discard
- Typical value:
alpha_ref / 256.0 - Pixels below threshold are discarded
Glow Effects and Shader Integration:
enum ZscMaterialGlowType {
GLOW_NONE = 0,
GLOW_SIMPLE = 2,
GLOW_LIGHT = 3,
GLOW_TEXTURE_LIGHT = 4,
GLOW_ALPHA = 5
};
class ZscMaterialGlow {
public:
ZscMaterialGlowType type;
Vec3 color; // Glow color (R, G, B)
};
Texture Referencing and UV Mapping:
UV Channels:
- ZMS supports up to 4 UV sets (UV1-UV4)
- UV1 is primary texture coordinates
- UV2-UV4 used for multi-texturing or effects
Texture Paths:
- Stored in ZSC material definitions
- Resolved through VFS
- File extensions:
.dds,.tga,.jpg
2.6 Vertex Format Flags and Data Layout
ZmsFormatFlags Bitfield Structure:
enum ZmsFormatFlags {
ZZ_VF_POSITION = (1 << 1), // 0x0002
ZZ_VF_NORMAL = (1 << 2), // 0x0004
ZZ_VF_COLOR = (1 << 3), // 0x0008
ZZ_VF_SKIN = (1 << 4), // 0x0010 (bone weights + indices)
ZZ_VF_TANGENT = (1 << 6), // 0x0040
ZZ_VF_UV1 = (1 << 7), // 0x0080
ZZ_VF_UV2 = (1 << 8), // 0x0100
ZZ_VF_UV3 = (1 << 9), // 0x0200
ZZ_VF_UV4 = (1 << 10) // 0x0400
};
class zz_vertex_format {
public:
uint32 flags;
bool use_position() const { return (flags & ZZ_VF_POSITION) != 0; }
bool use_normal() const { return (flags & ZZ_VF_NORMAL) != 0; }
bool use_color() const { return (flags & ZZ_VF_COLOR) != 0; }
bool use_skin() const { return (flags & ZZ_VF_SKIN) != 0; }
bool use_tangent() const { return (flags & ZZ_VF_TANGENT) != 0; }
int get_num_mapchannel() const;
};
Vertex Attribute Variations:
Version 5/6 (Legacy):
- Each vertex preceded by 32-bit vertex ID
- Bone indices stored as 32-bit, mapped to 16-bit
- Position scaled by 100.0 (cm to m conversion)
Version 7/8 (Modern):
- No vertex IDs
- Bone indices stored as 16-bit directly
- No scaling (already in correct units)
Position, Normal, Color, Bone Weights:
class zz_mesh {
public:
std::vector<Vec3> positions; // X, Y, Z position
std::vector<Vec3> normals; // X, Y, Z normal vector
std::vector<zz_color> colors; // R, G, B, A vertex color
std::vector<Vec4> bone_weights; // Weight for 4 bones (W1, W2, W3, W4)
std::vector<uivec4> bone_indices; // Bone index mapping (I1, I2, I3, I4)
void set_pos(int index, const Vec3& pos);
void set_normal(int index, const Vec3& normal);
void set_color(int index, const zz_color& color);
};
UV Coordinate Sets (UV1-4):
class zz_mesh {
public:
std::vector<Vec2> uv1; // Primary UV coordinates
std::vector<Vec2> uv2; // Secondary UV coordinates
std::vector<Vec2> uv3; // Tertiary UV coordinates
std::vector<Vec2> uv4; // Quaternary UV coordinates
};
Tangent and Binormal Data:
class zz_mesh {
public:
std::vector<Vec3> tangents; // Tangent vector for normal mapping
};
Note: Binormals can be computed from tangent and normal:
Vec3 binormal = normal.cross(tangent);
Material Face Mapping:
class zz_mesh {
public:
std::vector<uint16> material_num_faces; // Face count per material
};
Faces are grouped by material index for efficient rendering.
3. File Format Specifications
3.1 ZMS Mesh Files (.zms)
File Header and Magic Number:
Offset Type Description
0x00 char[7] Magic string ("ZMS0005", "ZMS0006", "ZMS0007", "ZMS0008")
Version Detection:
| Magic String | Version | Notes |
|---|---|---|
| ZMS0005 | 5 | Legacy format |
| ZMS0006 | 6 | Legacy format with material mapping |
| ZMS0007 | 7 | Modern format |
| ZMS0008 | 8 | Modern format with strip indices |
Binary Layout and Data Structures:
Version 5/6 Layout:
Offset Type Description
0x00 char[7] Magic string
0x07 u32 Format flags (ZmsFormatFlags)
0x0B Vec3 Bounding box minimum
0x17 Vec3 Bounding box maximum
0x23 u32 Bone count
0x27 u32[] Bone mapping (bone_count × 4 bytes)
0x?? u32 Vertex count
0x?? Vertex[] Vertex data (based on format flags)
0x?? u32 Triangle count
0x?? u16[] Index data (triangle_count × 3)
0x?? u32 Material ID count (version 6+)
0x?? u16[] Material face counts
Version 7/8 Layout:
Offset Type Description
0x00 char[7] Magic string
0x07 u32 Format flags (ZmsFormatFlags)
0x0B Vec3 Bounding box minimum
0x17 Vec3 Bounding box maximum
0x23 u16 Bone count
0x25 u16[] Bone indices (bone_count × 2 bytes)
0x?? u16 Vertex count
0x?? Vertex[] Vertex data (based on format flags)
0x?? u16 Triangle count
0x?? u16[] Index data (triangle_count × 3)
0x?? u16 Material ID count
0x?? u16[] Material face counts
0x?? u16 Strip index count (version 8)
0x?? u16[] Strip indices
0x?? u16 Pool type (version 8)
Vertex Data Arrays:
Vertex Data Structure (per vertex):
struct Vertex {
// Present if POSITION flag set
float position[3]; // X, Y, Z
// Present if NORMAL flag set
float normal[3]; // X, Y, Z
// Present if COLOR flag set
float color[4]; // R, G, B, A
// Present if BONE_WEIGHT and BONE_INDEX flags set
float bone_weight[4]; // W1, W2, W3, W4
uint32 bone_index[4]; // I1, I2, I3, I4 (v5/6)
uint16 bone_index[4]; // I1, I2, I3, I4 (v7/8)
// Present if TANGENT flag set
float tangent[3]; // X, Y, Z
// Present if UV1 flag set
float uv1[2]; // U, V
// Present if UV2 flag set
float uv2[2]; // U, V
// Present if UV3 flag set
float uv3[2]; // U, V
// Present if UV4 flag set
float uv4[2]; // U, V
};
Version 5/6: Each vertex preceded by 32-bit vertex ID.
Material Face Mapping:
std::vector<uint16_t> material_num_faces;
- Each entry specifies number of triangles for a material
- Faces are grouped contiguously by material
- Used for efficient multi-material rendering
Bone Index Mapping:
Version 5/6:
uint32_t bone_count = reader.read_u32();
std::vector<uint16_t> bones;
bones.reserve(bone_count);
for (uint32_t i = 0; i < bone_count; i++) {
reader.read_u32(); // Unused
bones.push_back(static_cast<uint16_t>(reader.read_u32()));
}
Bone indices in vertices are 32-bit, mapped through this table to 16-bit.
Version 7/8:
uint16_t bone_count = reader.read_u16();
std::vector<uint16_t> bones;
bones.reserve(bone_count);
for (uint16_t i = 0; i < bone_count; i++) {
bones.push_back(reader.read_u16());
}
Bone indices in vertices are already 16-bit, no mapping needed.
Vertex Attribute Flags:
See Section 2.6 for flag definitions.
Scaling Factors:
Version 5/6: Position data is scaled by 100.0 (cm to m conversion).
for [x, y, z] in position.iter_mut() {
*x /= 100.0;
*y /= 100.0;
*z /= 100.0;
}
Version 7/8: No scaling applied.
Parsing Algorithms and Edge Cases:
- Invalid Bone Index: Throw error if bone index >= bone_count
- Missing Attributes: Only read attributes present in format flags
- Empty Mesh: Handle vertex_count = 0 gracefully
- Material Count: May be 0 (single material mesh)
3.2 ZMD Skeleton Files (.zmd)
File Header and Magic Number:
Offset Type Description
0x00 char[7] Magic string ("ZMD0002" or "ZMD0003")
Version Detection:
| Magic String | Version | Notes |
|---|---|---|
| ZMD0002 | 2 | Legacy format |
| ZMD0003 | 3 | Modern format with dummy bone rotation |
Binary Layout and Data Structures:
Offset Type Description
0x00 char[7] Magic string
0x07 u32 Bone count
0x0B Bone[] Bone data
0x?? u32 Dummy bone count
0x?? DummyBone[] Dummy bone data
Bone Count and Hierarchy:
uint32_t bone_count = reader.read_u32();
std::vector<ZmdBone> bones;
bones.reserve(bone_count);
Bone Data Structure:
struct ZmdBone {
uint32 parent_id; // Parent bone index (parent_id == bone_index for root bone)
char name[]; // Null-terminated bone name
float position[3]; // X, Y, Z (cm, scaled to meters on load)
float rotation[4]; // W, X, Y, Z (quaternion)
};
Bone Name and ID Mapping:
Bone names are stored in the ZMD file but are not typically used directly in rendering. The bone ID (array index) is the primary reference mechanism used throughout the asset pipeline.
Name Storage Format:
Bone Name Storage:
- Null-terminated ASCII string
- Variable length (typically 1-32 characters)
- Common naming conventions:
- "Bip01" - Root bone
- "Bip01 Spine" - Spine bone
- "Bip01 L Hand" - Left hand
- "Dummy_Weapon" - Weapon attachment point
Bone ID System:
Bone Index Mapping:
┌─────────┬──────────────────┬─────────────────┐
│ ID │ Bone Name │ Parent ID │
├─────────┼──────────────────┼─────────────────┤
│ 0 │ Bip01 │ -1 (root) │
│ 1 │ Bip01 Pelvis │ 0 │
│ 2 │ Bip01 Spine │ 1 │
│ 3 │ Bip01 Spine1 │ 2 │
│ 4 │ Bip01 Neck │ 3 │
│ 5 │ Bip01 Head │ 4 │
│ ... │ ... │ ... │
└─────────┴──────────────────┴─────────────────┘
Name-Based Lookup (Optional):
class ZmdSkeleton {
public:
std::vector<ZmdBone> bones;
std::map<std::string, uint32> bone_name_to_id;
const ZmdBone* get_bone_by_name(const char* name) const {
auto it = bone_name_to_id.find(name);
if (it != bone_name_to_id.end()) {
return &bones[it->second];
}
return nullptr;
}
int get_bone_id(const char* name) const {
auto it = bone_name_to_id.find(name);
if (it != bone_name_to_id.end()) {
return it->second;
}
return -1;
}
};
Cross-File Bone References:
Bone references in other files:
- ZMS: bone_indices array references bone IDs
- ZMO: channel bone_index references bone IDs
- ZSC: bone_index field references bone IDs
All use the same bone ID from the ZMD skeleton.
Dummy Bone Support:
Dummy bones are special attachment points that don’t influence vertices directly but serve as mounts for weapons, effects, and other attachable objects.
Dummy Bone Purpose:
Attachment Point Types:
┌────────────────────┬─────────────────────────┐
│ Dummy Name │ Purpose │
├────────────────────┼─────────────────────────┤
│ Dummy_Weapon_R │ Right-hand weapon │
│ Dummy_Weapon_L │ Left-hand weapon/shield │
│ Dummy_Effect_Foot │ Footstep effects │
│ Dummy_Effect_Hand │ Skill effects │
│ Dummy_Head │ Head attachments │
│ Dummy_Back │ Backpack/wings │
│ Dummy_Mount │ Vehicle mount point │
└────────────────────┴─────────────────────────┘
Dummy Bone Data Structure:
struct DummyBone {
char name[]; // Null-terminated name
uint32 parent; // Parent bone index
float position[3]; // X, Y, Z (cm)
float rotation[4]; // W, X, Y, Z (version 3 only)
};
Version Differences:
Version 2 (ZMD0002):
- Dummy bones have identity rotation (0, 0, 0, 1)
- Rotation not stored in file
- Must use parent bone orientation
Version 3 (ZMD0003):
- Full rotation quaternion stored
- Allows arbitrary dummy bone orientation
- Better control over attachment alignment
Dummy Bone Runtime Handling:
class DummyBone {
public:
std::string name;
uint32 parent_bone;
Vec3 local_position;
Quat local_rotation; // Identity for v2, read from file for v3
Transform get_world_transform(const Skeleton& skeleton) const {
Transform parent_transform = skeleton.get_bone_world_transform(parent_bone);
return parent_transform * Transform::from_pos_rot(local_position, local_rotation);
}
void attach_object(AttachedObject& object, const Skeleton& skeleton) const {
Transform world_transform = get_world_transform(skeleton);
object.set_transform(world_transform);
}
};
Common Attachment Workflow:
1. Load ZMD skeleton with dummy bones
2. For each ZSC part with dummy_index > 0:
a. Get dummy bone by index
b. Compute world transform of dummy
c. Attach mesh/effect to dummy transform
3. Update dummy transforms each frame:
a. Traverse skeleton hierarchy
b. Compute bone world matrices
c. Apply dummy local transform
d. Update attached objects
Pose Transformation Matrices:
Pose transformation matrices are not stored directly in the ZMD file. They are computed at runtime from the bone hierarchy data (parent indices, positions, rotations).
Matrix Types:
Transform Matrix Categories:
┌─────────────────────┬─────────────────────────────────┐
│ Matrix Type │ Description │
├─────────────────────┼─────────────────────────────────┤
│ Local Matrix │ Transform relative to parent │
│ Object/Bind Matrix │ Transform in object space │
│ World Matrix │ Transform in world space │
│ Inverse Bind Matrix │ Object space to bone space │
│ Skin Matrix │ Current pose × inverse bind │
└─────────────────────┴─────────────────────────────────┘
Local Matrix Computation:
Mat4 compute_local_matrix(const ZmdBone& bone) {
Mat4 translation = Mat4::from_translation(bone.position);
Mat4 rotation = Mat4::from_quat(bone.rotation);
return translation * rotation;
}
Object Space Matrix Computation:
void compute_object_space_matrices(const std::vector<ZmdBone>& bones, std::vector<Mat4>& matrices) {
matrices.resize(bones.size());
// Build matrices in order (parents before children)
for (int i = 0; i < bones.size(); i++) {
Mat4 local = compute_local_matrix(bones[i]);
if (bones[i].parent_id == i) { // Root bone: parent_id equals bone index
matrices[i] = local;
} else {
matrices[i] = matrices[bones[i].parent_id] * local;
}
}
}
Inverse Bind Matrix:
void compute_inverse_bind_matrices(
const std::vector<Mat4>& object_space,
std::vector<Mat4>& inverse_bind_matrices
) {
inverse_bind_matrices.resize(object_space.size());
for (int i = 0; i < object_space.size(); i++) {
inverse_bind_matrices[i] = object_space[i].inverse();
}
}
Skinning Matrix (for GPU):
void compute_skinning_matrices(
const std::vector<Mat4>& current_pose, // Current animation pose in object space
const std::vector<Mat4>& inverse_bind, // Inverse bind pose matrices
std::vector<Mat4>& skinning_matrices // Output skinning matrices
) {
skinning_matrices.resize(current_pose.size());
for (int i = 0; i < current_pose.size(); i++) {
skinning_matrices[i] = current_pose[i] * inverse_bind[i];
}
}
Matrix Storage Optimization:
Runtime Storage:
- Inverse bind matrices: Computed once at load time
- Current pose matrices: Computed each animation frame
- Skinning matrices: Computed each frame for GPU upload
Memory Layout (typical skeleton):
- 100 bones × 64 bytes/matrix = 6.4 KB per matrix array
- 3 arrays needed = ~20 KB per skeleton
Bone-to-Object Binding:
Bones in the ZMD file are stored in local space (relative to their parent bone). Object space matrices must be computed by traversing the bone hierarchy from root to each bone.
Coordinate Space Definitions:
Bone Coordinate Spaces:
┌─────────────────┬─────────────────────────────────────┐
│ Space │ Description │
├─────────────────┼─────────────────────────────────────┤
│ Local Space │ Position/rotation relative to parent│
│ Object Space │ Position/rotation from mesh origin │
│ Bone Space │ Coordinates within bone's local frame│
│ World Space │ Position in game world coordinates │
└─────────────────┴─────────────────────────────────────┘
Hierarchy Traversal:
Bone Hierarchy Example:
Bip01 (root)
└── Bip01 Pelvis
├── Bip01 L Thigh
│ └── Bip01 L Calf
│ └── Bip01 L Foot
└── Bip01 Spine
└── Bip01 Spine1
└── Bip01 Neck
└── Bip01 Head
Matrix Accumulation:
ObjectSpace(Head) = ObjectSpace(Root)
× Local(Pelvis)
× Local(Spine)
× Local(Spine1)
× Local(Neck)
× Local(Head)
Vertex Binding Process:
1. Vertex stored in bind pose (object space)
2. Bone indices and weights define influence
3. Skinning formula:
FinalPos = Σ(weight_i × SkinMatrix_i × VertexPos)
Where:
SkinMatrix_i = CurrentPose_i × InverseBind_i
Code Implementation:
struct BoneBinding {
uint16 bone_indices[4]; // Up to 4 influencing bones
float bone_weights[4]; // Weights summing to 1.0
};
Vec3 skin_vertex(
const Vec3& vertex,
const BoneBinding& binding,
const std::vector<Mat4>& skinning_matrices
) {
Vec3 result = Vec3::ZERO;
for (int i = 0; i < 4; i++) {
float weight = binding.bone_weights[i];
if (weight > 0.0f) {
int bone_idx = binding.bone_indices[i];
const Mat4& skin_matrix = skinning_matrices[bone_idx];
result += skin_matrix.transform_point(vertex) * weight;
}
}
return result;
}
Scaling from cm to m:
Bone positions in ZMD files are stored in centimeters (cm). Conversion to meters requires scaling by 0.01 for use in most rendering engines.
Scaling Conversion:
Position Scaling:
- Storage: Centimeters (cm)
- Rendering: Meters (m)
- Conversion: meters = centimeters × 0.01
Example:
- Stored position: (150.0, 100.0, 50.0) cm
- Converted position: (1.5, 1.0, 0.5) m
Scaling Application Points:
When to Apply Scaling:
┌─────────────────────┬───────────────────────────────┐
│ Location │ Apply Scaling? │
├─────────────────────┼───────────────────────────────┤
│ ZMD bone position │ Yes, at load time │
│ ZMS vertex position │ Yes (v5/6) / No (v7/8) │
│ ZMO position keys │ Yes, at load time │
│ IFO object position │ Yes, at load time │
│ HIM height values │ Yes, at load time │
└─────────────────────┴───────────────────────────────┘
Code Implementation:
const float CM_TO_M = 0.01f;
bool load_zmd_bone(zz_vfs& file, ZmdBone& bone) {
// Read parent bone index
file.read_uint32(bone.parent_id);
// Read bone name (null-terminated string)
char name[ZZ_MAX_STRING];
file.read_string(name);
bone.name = name;
// Read position in cm, convert to meters
float pos[3];
file.read_float3(pos);
bone.position.x = pos[0] * CM_TO_M;
bone.position.y = pos[1] * CM_TO_M;
bone.position.z = pos[2] * CM_TO_M;
// Read rotation (quaternion: W, X, Y, Z)
file.read_float(bone.rotation.w);
file.read_float(bone.rotation.x);
file.read_float(bone.rotation.y);
file.read_float(bone.rotation.z);
return true;
}
Consistency Requirements:
Important: All position data must use consistent units!
If skeleton uses meters:
- Vertex positions: meters
- Animation positions: meters
- Object positions: meters
If skeleton uses centimeters:
- All positions: centimeters
- Convert at final rendering stage only
3.3 ZMO Animation Files (.zmo)
File Header and Magic Number:
Offset Type Description
0x00 char[7] Magic string ("ZMO0002")
Channel Definitions and Types:
struct ChannelDef {
uint32 channel_type; // 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024
uint32 bone_index; // Target bone index
};
Frame Data Structures:
Frame data is stored per channel, not per frame.
Channel 0:
Frame 0: value
Frame 1: value
...
Frame N: value
Channel 1:
Frame 0: value
...
Extended Format Support (EZMO/3ZMO):
Extended data at end of file:
End-4: char[4] Magic ("EZMO" or "3ZMO")
End-8: u32 Extended data offset
Offset: u16 Frame event count
Offset+2: u16[] Frame events (event_count)
...
3ZMO Only:
- After frame events:
u32 interpolation_interval_ms
Frame Events and Interpolation Intervals:
Frame Events:
std::vector<uint16_t> frame_events;
Triggered at specific frames. Attack events: 10, 20-28, 56-57, 66-67.
Interpolation Interval:
std::optional<uint32_t> interpolation_interval_ms;
Custom interpolation timing for 3ZMO format.
Channel Interpolation Methods:
- Position: Linear interpolation
- Rotation: Quaternion Slerp
- UV/Scale/Alpha: Linear interpolation
3.4 ZON Zone Files (.zon)
Block-Based File Structure:
Offset Type Description
0x00 u32 Block count
0x04 BlockHeader[] Block headers (count × 8 bytes)
...
Block Data...
Block Header:
struct BlockHeader {
uint32 block_type; // Block type enum
uint32 block_offset; // Offset to block data
};
Block Type Enumeration:
enum BlockType {
ZoneInfo = 0,
EventPositions = 1,
Textures = 2,
Tiles = 3,
Economy = 4,
};
ZoneInfo Block – Grid Configuration:
Offset Type Description
0x00 u32[3] Unknown (12 bytes)
0x0C u32 Grid per patch
0x10 f32 Grid size
0x14 u32[2] Unknown (8 bytes)
EventPositions Block – Spawn Points:
struct EventPosition {
float position[3]; // X, Y, Z
uint8 name_len; // Name length
char name[name_len]; // Event name
};
Textures Block – Texture References:
struct TextureRef {
uint8 path_len; // Path length
char path[path_len]; // Texture path
};
Tiles Block – Tile Composition:
struct ZonTile {
uint32 layer1; // Primary layer texture index
uint32 layer2; // Secondary layer texture index
uint32 offset1; // Primary layer offset
uint32 offset2; // Secondary layer offset
uint32 blend; // Blend flag (0/1)
uint32 rotation; // Rotation enum
uint32 padding; // 4 bytes padding
};
ZonTile Structure and Layer Composition:
Rotation Enum:
enum class ZonTileRotation : uint8_t {
Unknown = 0,
None = 1,
FlipHorizontal = 2,
FlipVertical = 3,
Flip = 4,
Clockwise90 = 5,
CounterClockwise90 = 6,
};
Tile Offset and Blend Modes:
Tile offset and blend modes control how textures are applied to terrain tiles, enabling texture variation, UV tiling, and multi-layer blending.
Offset System:
UV Offset Configuration:
- offset1: UV offset for primary layer (layer1)
- offset2: UV offset for secondary layer (layer2)
- Stored as integer, converted to UV space
UV Calculation:
uv_u = (tile_x + offset_value / 1000.0) * uv_scale
uv_v = (tile_y + offset_value / 1000.0) * uv_scale
Example:
- offset1 = 500
- UV offset = 500 / 1000.0 = 0.5
- Result: Texture shifted by half a tile
Blend Mode System:
Blend Flag Values:
- 0: Single layer (layer1 only)
- 1: Dual layer blending (layer1 + layer2)
Blending Formula:
final_color = lerp(layer1_color, layer2_color, blend_factor)
Blend Factor Sources:
- Vertex alpha from terrain mesh
- Pre-computed blend mask texture
- Height-based blending
Rotation Transformations:
Rotation Enum Values:
┌──────────────────┬─────┬─────────────────────────────┐
│ Rotation │ Val │ UV Transform │
├──────────────────┼─────┼─────────────────────────────┤
│ Unknown │ 0 │ Identity (no change) │
│ None │ 1 │ Identity (no change) │
│ FlipHorizontal │ 2 │ (1-u, v) │
│ FlipVertical │ 3 │ (u, 1-v) │
│ Flip │ 4 │ (1-u, 1-v) │
│ Clockwise90 │ 5 │ (v, 1-u) │
│ CounterClockwise90│ 6 │ (1-v, u) │
└──────────────────┴─────┴─────────────────────────────┘
UV Transformation Code:
Vec2 apply_tile_transform(const Vec2& uv, const ZonTile& tile) {
float offset = tile.offset1 / 1000.0f;
Vec2 result = uv + Vec2(offset, offset);
switch (tile.rotation) {
case ZON_TILE_ROTATION_NONE:
return result;
case ZON_TILE_ROTATION_FLIP_HORIZONTAL:
return Vec2(1.0f - result.x, result.y);
case ZON_TILE_ROTATION_FLIP_VERTICAL:
return Vec2(result.x, 1.0f - result.y);
case ZON_TILE_ROTATION_FLIP:
return Vec2(1.0f - result.x, 1.0f - result.y);
case ZON_TILE_ROTATION_CLOCKWISE90:
return Vec2(result.y, 1.0f - result.x);
case ZON_TILE_ROTATION_COUNTER_CLOCKWISE90:
return Vec2(1.0f - result.y, result.x);
default:
return result;
}
}
Practical Usage Examples:
Texture Variation Techniques:
1. Same texture, different offsets:
- Tile A: grass.dds, offset=0
- Tile B: grass.dds, offset=500
- Result: Same texture, varied UVs (breaks repetition)
2. Layer blending for transitions:
- Tile: layer1=grass, layer2=dirt, blend=1
- Result: Smooth transition between grass and dirt
3. Rotation for variety:
- 4 adjacent tiles with same texture
- Apply different rotations to each
- Result: Non-repeating pattern from single texture
Spatial Partitioning and Grid System:
The zone is divided into a grid of tiles where each tile references textures from the Textures block. This spatial partitioning enables efficient terrain rendering and culling.
Grid System Architecture:
Zone Grid Structure:
┌─────────────────────────────────────────────────┐
│ Zone (entire map area) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Tile Grid (width × height tiles) │ │
│ │ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ │
│ │ │0,0│1,0│2,0│...│ │ │ │ │W-1│ │ │
│ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤ │ │
│ │ │0,1│1,1│2,1│...│ │ │ │ │ │ │ │
│ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤ │ │
│ │ │...│...│...│...│ │ │ │ │ │ │ │
│ │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┤ │ │
│ │ │0,H│...│...│...│ │ │ │ │W-1│ │ │
│ │ └───┴───┴───┴───┴───┴───┴───┴───┴H-1┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Grid Configuration from ZoneInfo:
ZoneInfo Block Parameters:
- Grid per patch: Grid cells per terrain patch (typically 1)
- Grid size (patch_size): World units per grid cell (e.g., 100.0 cm)
Calculated Values:
- Total world width = grid_width × patch_size
- Total world height = grid_height × patch_size
- Tile count = grid_width × grid_height
Spatial Queries:
class ZoneGrid {
public:
uint32 width;
uint32 height;
float patch_size;
std::vector<ZonTile> tiles;
void world_to_grid(float world_x, float world_z, uint32& grid_x, uint32& grid_y) const {
grid_x = static_cast<uint32>(std::floor(world_x / patch_size));
grid_y = static_cast<uint32>(std::floor(world_z / patch_size));
grid_x = std::min(grid_x, width - 1);
grid_y = std::min(grid_y, height - 1);
}
void grid_to_world(uint32 grid_x, uint32 grid_y, float& world_x, float& world_z) const {
world_x = grid_x * patch_size;
world_z = grid_y * patch_size;
}
const ZonTile* get_tile(uint32 grid_x, uint32 grid_y) const {
if (grid_x < width && grid_y < height) {
return &tiles[grid_y * width + grid_x];
}
return nullptr;
}
std::vector<const ZonTile*> get_tiles_in_radius(float center_x, float center_z, float radius) const {
uint32 cx, cy;
world_to_grid(center_x, center_z, cx, cy);
int r = static_cast<int>(std::ceil(radius / patch_size));
std::vector<const ZonTile*> result;
for (int dy = -r; dy <= r; dy++) {
for (int dx = -r; dx <= r; dx++) {
uint32 x = static_cast<uint32>(static_cast<int>(cx) + dx);
uint32 y = static_cast<uint32>(static_cast<int>(cy) + dy);
const ZonTile* tile = get_tile(x, y);
if (tile) {
result.push_back(tile);
}
}
}
return result;
}
};
Frustum Culling Integration:
Terrain Rendering Optimization:
1. Calculate visible grid range from camera frustum
2. Only render tiles within visible range
3. Use LOD based on distance from camera
Visible Range Calculation:
min_x = floor((frustum_min_x) / patch_size)
max_x = ceil((frustum_max_x) / patch_size)
min_y = floor((frustum_min_z) / patch_size)
max_y = ceil((frustum_max_z) / patch_size)
Memory Layout:
Tile Data Storage:
- ZonTile size: 28 bytes (7 × u32)
- Typical zone: 64 × 64 = 4096 tiles
- Memory: 4096 × 28 = 114,688 bytes (~112 KB)
Texture Reference Storage:
- Path length: Variable (typically 20-60 chars)
- Texture count: 10-50 per zone
- Memory: ~2-5 KB for texture paths
3.5 ZSC Object Composition Files (.zsc)
Material List Structure:
struct MaterialList {
uint16 material_count;
MaterialEntry materials[material_count];
};
ZscMaterial Properties:
struct ZscMaterial {
char path[]; // Null-terminated path
uint16 is_skin; // 0/1
uint16 alpha_enabled; // 0/1
uint16 two_sided; // 0/1
uint16 alpha_test_enabled; // 0/1
uint16 alpha_ref; // Alpha reference (0-255)
uint16 z_test_enabled; // 0/1
uint16 z_write_enabled; // 0/1
uint16 blend_mode; // 0=Normal, 1=Lighten
uint16 specular_enabled; // 0/1
float alpha; // Global alpha
uint16 glow_type; // 0=None, 2=Simple, 3=Light, 4=TextureLight, 5=Alpha
float glow_color[3]; // R, G, B
};
Object Part Definitions:
struct ObjectPart {
uint16 mesh_id; // Mesh index
uint16 material_id; // Material index
// Properties follow (variable length)
};
Mesh and Material References:
The ZSC file maintains three primary reference arrays that define the visual appearance of objects: mesh paths, material definitions, and effect paths.
Mesh List Structure:
Mesh Reference Array:
┌─────────────────────────────────────────┐
│ mesh_count: uint16 │
│ mesh_paths: string[] │
└─────────────────────────────────────────┘
Example:
[0] = "3DDATA/OBJECTS/TREE_A.ZMS"
[1] = "3DDATA/OBJECTS/TREE_B.ZMS"
[2] = "3DDATA/OBJECTS/ROCK.ZMS"
Material List Structure:
Material Definition Array:
┌─────────────────────────────────────────┐
│ material_count: uint16 │
│ materials: ZscMaterial[] │
└─────────────────────────────────────────┘
Each ZscMaterial contains:
- Texture path
- Blend mode
- Alpha settings
- Glow properties
Effect List Structure:
Effect Reference Array:
┌─────────────────────────────────────────┐
│ effect_count: uint16 │
│ effect_paths: string[] │
└─────────────────────────────────────────┘
Example:
[0] = "EFFECT/FIRE_TORCH.EFT"
[1] = "EFFECT/SMOKE.EFT"
Reference Resolution:
class ZscFile {
public:
std::vector<std::string> meshes;
std::vector<ZscMaterial> materials;
std::vector<std::string> effects;
std::vector<ZscObject> objects;
const std::string* get_part_mesh(const ZscObjectPart& part) const {
if (part.mesh_id < meshes.size()) {
return &meshes[part.mesh_id];
}
return nullptr;
}
const ZscMaterial* get_part_material(const ZscObjectPart& part) const {
if (part.material_id < materials.size()) {
return &materials[part.material_id];
}
return nullptr;
}
};
Part Transforms (Position, Rotation, Scale):
Each object part has its own transform that positions it relative to the object origin or parent part. This enables complex multi-part objects with articulated structures.
Transform Data Structure:
struct Transform {
float position[3]; // X, Y, Z in centimeters
float rotation[4]; // W, X, Y, Z (quaternion)
float scale[3]; // X, Y, Z scale factors
};
Transform Application Order:
Final Vertex Position:
1. Apply scale: v' = v * scale
2. Apply rotation: v'' = rotation * v'
3. Apply position: v''' = v'' + position
Matrix Form:
transform_matrix = T * R * S
where:
T = translation matrix
R = rotation matrix (from quaternion)
S = scale matrix
Common Transform Examples:
Object Part Transform Examples:
┌─────────────────┬────────────────────┬──────────────────────┐
│ Part │ Position │ Rotation │
├─────────────────┼────────────────────┼──────────────────────┤
│ Door (closed) │ (0, 0, 0) │ (1, 0, 0, 0) │
│ Door (open 90°) │ (0, 0, 0) │ (0.707, 0, 0.707, 0) │
│ Wheel rotating │ (0, 50, 0) │ (varies) │
│ Weapon mount │ (10, 0, 5) │ (1, 0, 0, 0) │
└─────────────────┴────────────────────┴──────────────────────┘
Transform Code Implementation:
Mat4 compute_transform_matrix(const Transform& t) {
Mat4 translation = Mat4::from_translation(t.position);
Mat4 rotation = Mat4::from_quat(t.rotation);
Mat4 scale = Mat4::from_scale(t.scale);
return translation * rotation * scale;
}
Vec3 apply_transform(const Vec3& vertex, const Transform& t) {
Vec3 scaled = vertex * t.scale;
Vec3 rotated = t.rotation * scaled;
return rotated + t.position;
}
Bone Index and Dummy Bone Binding:
Parts can be attached to skeleton bones or dummy bones for skinned objects. This enables weapons, armor, and other equipment to follow character animations.
Bone Binding Structure:
struct BoneBinding {
uint16 bone_index; // Regular bone index (0xFFFF = none)
uint16 dummy_index; // Dummy bone index (0xFFFF = none)
};
Binding Priority:
Attachment Logic:
1. If dummy_index != 0xFFFF:
- Attach to dummy bone
2. Else if bone_index != 0xFFFF:
- Attach to regular bone
3. Else:
- Use part transform (static attachment)
Common Binding Configurations:
┌─────────────────┬─────────────┬─────────────┬────────────────┐
│ Part Type │ bone_index │ dummy_index │ Behavior │
├─────────────────┼─────────────┼─────────────┼────────────────┤
│ Body armor │ spine_index │ 0xFFFF │ Follows spine │
│ Right weapon │ 0xFFFF │ weapon_r │ Follows dummy │
│ Left shield │ 0xFFFF │ weapon_l │ Follows dummy │
│ Helmet │ head_index │ 0xFFFF │ Follows head │
│ Backpack │ 0xFFFF │ back_dummy │ Follows dummy │
│ Static deco │ 0xFFFF │ 0xFFFF │ No bone binding│
└─────────────────┴─────────────┴─────────────┴────────────────┘
Runtime Attachment Code:
Mat4 attach_part_to_skeleton(
ZscObjectPart& part,
const Skeleton& skeleton,
const Mat4& parent_transform
) {
Mat4 bone_transform;
if (part.dummy_index != 0xFFFF) {
// Attach to dummy bone
bone_transform = skeleton.get_dummy_transform(part.dummy_index);
} else if (part.bone_index != 0xFFFF) {
// Attach to regular bone
bone_transform = skeleton.get_bone_transform(part.bone_index);
} else {
// No bone binding, use part transform
bone_transform = Mat4::IDENTITY;
}
Mat4 part_transform = compute_transform_matrix(part.transform);
return parent_transform * bone_transform * part_transform;
}
Parent-Child Relationships:
Parts can form parent-child hierarchies, allowing complex articulated objects where child parts inherit transforms from their parents.
Parent Reference Structure:
struct ParentRef {
uint16 parent_id; // Parent part index (0 = none)
};
Parent ID Interpretation:
Parent ID System:
- 0: No parent (root part)
- 1-N: Parent is part at index (parent_id - 1)
Note: Parent IDs are 1-indexed!
Array index = parent_id - 1
Hierarchy Example:
Object Part Hierarchy:
Part 0 (Torso) - parent_id = 0 (root)
Part 1 (Head) - parent_id = 1 (parent = Part 0)
Part 2 (L Arm) - parent_id = 1 (parent = Part 0)
Part 3 (R Arm) - parent_id = 1 (parent = Part 0)
Part 4 (R Hand) - parent_id = 4 (parent = Part 3)
Part 5 (Weapon) - parent_id = 5 (parent = Part 4)
Transform Inheritance:
void compute_hierarchy_transforms(
const std::vector<ZscObjectPart>& parts,
const Mat4& base_transform,
std::vector<Mat4>& transforms
) {
transforms.resize(parts.size());
// Process in order (parents before children)
for (int i = 0; i < parts.size(); i++) {
Mat4 local = compute_transform_matrix(parts[i].transform);
if (parts[i].parent_id == 0) {
transforms[i] = base_transform * local;
} else {
int parent_idx = parts[i].parent_id - 1;
transforms[i] = transforms[parent_idx] * local;
}
}
}
Collision Shape and Flags:
Objects can have collision data that defines how they interact with physics, camera, and player movement.
Collision Data Structure:
struct CollisionData {
uint16 flags; // Collision flags
// Bits 0-2: Shape (0=None, 1=Sphere, 2=AABB, 3=OBB, 4=Polygon)
// Bit 3: NOT_MOVEABLE
// Bit 4: NOT_PICKABLE
// Bit 5: HEIGHT_ONLY
// Bit 6: NOT_CAMERA_COLLISION
// Bit 7: PASSTHROUGH
};
Collision Shape Types:
┌──────┬─────────────┬─────────────────────────────────────┐
│ Type │ Shape │ Description │
├──────┼─────────────┼─────────────────────────────────────┤
│ 0 │ None │ No collision │
│ 1 │ Sphere │ Bounding sphere (center + radius) │
│ 2 │ AABB │ Axis-aligned bounding box │
│ 3 │ OBB │ Oriented bounding box │
│ 4 │ Polygon │ Triangle mesh collision │
└──────┴─────────────┴─────────────────────────────────────┘
Collision Flag Meanings:
Flag Bit Interpretation:
Bit 3 (NOT_MOVEABLE): Object cannot be pushed/moved
Bit 4 (NOT_PICKABLE): Mouse click-through enabled
Bit 5 (HEIGHT_ONLY): Only affects terrain height
Bit 6 (NOT_CAMERA_COLL): Camera passes through
Bit 7 (PASSTHROUGH): Player can walk through
Common Combinations:
Building: Shape=OBB, NOT_MOVEABLE
Grass: Shape=None, PASSTHROUGH
Water: Shape=None, HEIGHT_ONLY
Trigger: Shape=AABB, PASSTHROUGH, NOT_CAMERA_COLL
Physics Integration:
CollisionShape create_collision_shape(const CollisionData& collision, const BoundingBox& bounds) {
int shape_type = collision.flags & 0x7;
switch (shape_type) {
case 1:
return CollisionShape::Sphere(bounds.radius());
case 2:
return CollisionShape::Aabb(bounds.half_extents());
case 3:
return CollisionShape::Obb(bounds.half_extents(), bounds.rotation());
case 4:
return CollisionShape::Mesh(bounds.triangles());
default:
return CollisionShape::None();
}
}
Animation Path References:
Parts can have associated animation files (ZMO) that override or supplement the main object animation.
Animation Reference Structure:
struct AnimationRef {
uint8 property_id; // Must be 30
uint8 size; // Path length
char path[size]; // Animation path
};
Property ID System:
Property IDs in ZSC:
┌──────┬────────────────────────┐
│ ID │ Property │
├──────┼────────────────────────┤
│ 30 │ Animation path │
│ ... │ Other properties │
└──────┴────────────────────────┘
Animation references use property_id = 30
Animation Application:
Part Animation Workflow:
1. Check if part has animation reference
2. If property_id == 30:
- Load ZMO file from path
- Apply to part's transform each frame
3. Animation affects only this part
(not inherited to children)
Code Implementation:
ZmoFile* load_part_animation(Vfs& vfs, const ZscObjectPart& part) {
for (const auto& prop : part.properties) {
if (prop.property_id == 30) {
return vfs.load_zmo(prop.path.c_str());
}
}
return nullptr;
}
void apply_part_animation(ZscObjectPart& part, const ZmoFile& zmo, float frame) {
// Sample animation at current frame
Vec3 pos;
Quat rot;
float scale;
if (zmo.sample_position(0, frame, pos)) {
part.transform.position = pos;
}
if (zmo.sample_rotation(0, frame, rot)) {
part.transform.rotation = rot;
}
if (zmo.sample_scale(0, frame, scale)) {
part.transform.scale = Vec3(scale, scale, scale);
}
}
Object Effects and Properties:
struct ObjectEffect {
uint16 effect_id; // Effect index
uint16 effect_type; // 0=Normal, 1=DayNight, 2=LightContainer
Transform transform; // Position, rotation, scale
uint16 parent; // Parent part index
};
3.6 IFO Zone Object Files (.ifo)
Object Type Enumeration:
enum BlockType {
DeprecatedMapInfo = 0,
DecoObject = 1,
Npc = 2,
CnstObject = 3,
SoundObject = 4,
EffectObject = 5,
AnimatedObject = 6,
DeprecatedWater = 7,
MonsterSpawn = 8,
WaterPlanes = 9,
Warp = 10,
CollisionObject = 11,
EventObject = 12,
};
Object Placement Data:
struct IfoObject {
uint8 name_len; // Name length
char name[name_len]; // Object name
uint16 warp_id; // Warp ID
uint16 event_id; // Event ID
uint32 object_type; // Object type enum
uint32 object_id; // Object ID
uint32 minimap_x; // Minimap X position
uint32 minimap_y; // Minimap Y position
float rotation[4]; // X, Y, Z, W (quaternion)
float position[3]; // X, Y, Z
float scale[3]; // X, Y, Z
};
Minimap Position and ID:
The minimap position fields (minimap_x and minimap_y) are used to display objects on the in-game minimap and for object identification purposes.
Minimap Coordinate System:
Minimap Space:
- Origin: Top-left corner of minimap
- X-axis: Increases to the right
- Y-axis: Increases downward
- Range: 0 to minimap_texture_size
Coordinate Conversion:
minimap_x = world_x / zone_width * minimap_width
minimap_y = world_z / zone_height * minimap_height
Usage Examples:
Object Type | Minimap Display
-------------------|-----------------
NPC | Yellow dot with icon
Warp | Blue portal icon
Monster Spawn | Not shown (hidden)
Quest Target | Pulsing highlight
Vendor NPC | Bag/shop icon
Client-Side Processing:
void draw_minimap_object(const IfoObject& object, uint32 minimap_size) {
// Convert to minimap coordinates
float mm_x = object.minimap_x / 1000.0f * minimap_size;
float mm_y = object.minimap_y / 1000.0f * minimap_size;
// Select icon based on object type
MinimapIcon icon;
switch (object.object_type) {
case OBJECT_TYPE_NPC:
icon = npc_icon;
break;
case OBJECT_TYPE_WARP:
icon = warp_icon;
break;
case OBJECT_TYPE_EVENT:
icon = event_icon;
break;
default:
return; // Don't draw other types
}
minimap.draw_icon(icon, mm_x, mm_y);
}
Warp and Event References:
Warp and Event IDs provide links to external game systems that handle zone transitions and scripted events.
Warp ID System:
Warp Configuration:
- Warp ID: Index into warp definition table (STB or similar)
- Each warp defines:
- Destination zone ID
- Destination position (x, y, z)
- Required conditions (level, items, quests)
- Warp animation/effect
Warp Activation Flow:
1. Player enters warp trigger area
2. Client sends warp request with warp_id
3. Server validates conditions
4. Player teleported to destination
5. Loading screen shown during zone change
Event ID System:
Event Configuration:
- Event ID: Index into event definition table
- Events can trigger:
- Quest dialogs
- Scripted sequences
- NPC interactions
- Cutscenes
- Special encounters
Event Trigger Flow:
1. Player interacts with object (click/collision)
2. Client checks event_id
3. If event_id > 0:
- Load event script from Lua
- Execute event handler
- Display appropriate UI
Code Reference:
struct WarpInfo {
uint32 warp_id;
uint32 dest_zone;
Vec3 dest_position;
uint32 required_level;
uint32 required_quest; // 0 if no quest requirement
};
struct EventInfo {
uint32 event_id;
std::string script_path;
EventTriggerType trigger_type;
bool repeatable;
};
void handle_object_interaction(const IfoObject& object) {
if (object.warp_id > 0) {
const WarpInfo* warp = warp_table.get(object.warp_id);
if (warp) {
initiate_warp(*warp);
}
}
if (object.event_id > 0) {
const EventInfo* event = event_table.get(object.event_id);
if (event) {
trigger_event(*event);
}
}
}
Spawn Point Definitions:
struct MonsterSpawnPoint {
IfoObject object; // Spawn position
char spawn_name[]; // Spawn name
uint32 basic_count; // Basic spawn count
BasicSpawn basic_spawns[basic_count];
uint32 tactic_count; // Tactic spawn count
TacticSpawn tactic_spawns[tactic_count];
uint32 interval; // Spawn interval (seconds)
uint32 limit_count; // Max monsters
uint32 range; // Spawn range
uint32 tactic_points; // Tactic points
};
struct BasicSpawn {
char monster_name[];
uint32 monster_id;
uint32 count;
};
Object-Specific Properties:
NPC:
struct IfoNpc {
IfoObject object;
uint32 ai_id; // AI behavior ID
char quest_file_name[]; // Quest file
};
Effect Object:
struct IfoEffect {
IfoObject object;
char effect_path[]; // Effect file path
};
Sound Object:
struct IfoSound {
IfoObject object;
char sound_path[]; // Sound file path
uint32 range; // Sound range
uint32 interval; // Play interval (seconds)
};
Monster Spawn Configuration:
Monster spawn points define where and how monsters appear in the game world. The spawn system controls monster population, respawn timing, and tactical behavior.
Spawn Point Components:
MonsterSpawnPoint Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - position, rotation, scale │
│ - object_type = MonsterSpawn (8) │
├─────────────────────────────────────────┤
│ spawn_name: "Forest_Spawn_01" │
│ basic_spawns: [ │
│ { monster_id: 101, count: 3 }, │
│ { monster_id: 102, count: 2 } │
│ ] │
│ interval: 60 (seconds) │
│ limit_count: 5 (max alive) │
│ range: 500 (spawn radius in cm) │
└─────────────────────────────────────────┘
Spawn Timing Configuration:
Spawn Timing Parameters:
- interval: Time between spawn checks (seconds)
- limit_count: Maximum monsters alive from this spawn
- range: Radius around spawn point for monster placement
Spawn Algorithm:
1. Check if current_alive < limit_count
2. If yes and interval elapsed:
a. Select monster type from basic_spawns
b. Generate random position within range
c. Instantiate monster at position
d. Reset spawn timer
Tactical Spawn System:
TacticSpawn Structure (advanced spawns):
- Triggered by specific conditions
- Can spawn different monsters based on:
- Time of day
- Player count in area
- Quest progress
- Previous spawn deaths
Example Tactical Configuration:
TacticSpawn {
condition: TIME_NIGHT,
monster_id: 201, // Night-only monster
count: 2,
tactic_points: 100
}
Code Implementation:
class SpawnManager {
public:
std::vector<MonsterSpawnPoint> spawn_points;
std::map<uint32, std::vector<EntityId>> active_monsters;
std::map<uint32, float> spawn_timers;
void update(float delta_time, Zone& zone) {
for (int spawn_id = 0; spawn_id < spawn_points.size(); spawn_id++) {
const MonsterSpawnPoint& spawn = spawn_points[spawn_id];
int alive_count = 0;
auto it = active_monsters.find(spawn_id);
if (it != active_monsters.end()) {
alive_count = it->second.size();
}
if (alive_count < spawn.limit_count) {
float& timer = spawn_timers[spawn_id];
timer += delta_time;
if (timer >= spawn.interval) {
spawn_monster(spawn_id, spawn);
timer = 0.0f;
}
}
}
}
void spawn_monster(int spawn_id, const MonsterSpawnPoint& spawn) {
// Select random monster from basic_spawns
int index = rand() % spawn.basic_spawns.size();
const BasicSpawn& basic = spawn.basic_spawns[index];
// Generate random position within range
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
float dist = static_cast<float>(rand()) / RAND_MAX * spawn.range;
Vec3 offset(cos(angle) * dist, 0.0f, sin(angle) * dist);
Vec3 spawn_pos = spawn.object.position + offset;
// Create monster entity
EntityId entity = create_monster(basic.monster_id, spawn_pos);
active_monsters[spawn_id].push_back(entity);
}
};
NPC and Character Placement:
NPCs and characters are placed in zones using the IfoNpc structure, which extends the base IfoObject with NPC-specific properties.
NPC Placement Configuration:
IfoNpc Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - position: (x, y, z) in cm │
│ - rotation: quaternion (facing dir) │
│ - scale: (1, 1, 1) typically │
│ - object_type = Npc (2) │
│ - object_id: NPC definition ID │
├─────────────────────────────────────────┤
│ ai_id: 5 (AI behavior script) │
│ quest_file_name: "quest_001.lua" │
└─────────────────────────────────────────┘
NPC ID Resolution:
NPC Loading Pipeline:
1. Read IfoNpc from IFO file
2. Look up NPC definition by object_id:
- From LIST_NPC.STB (NPC data table)
- From CHR file (character model)
3. Load NPC resources:
- Skeleton (ZMD)
- Models (ZSC)
- Animations (ZMO)
4. Apply AI behavior from ai_id
5. Attach quest script if specified
AI Behavior System:
AI ID Reference:
┌───────┬────────────────────────┐
│ ai_id │ Behavior │
├───────┼────────────────────────┤
│ 0 │ Idle (no movement) │
│ 1 │ Wander (random walk) │
│ 2 │ Vendor (shop interface)│
│ 3 │ Quest NPC (dialog) │
│ 4 │ Guard (stationary) │
│ 5 │ Patrol (waypoints) │
└───────┴────────────────────────┘
Quest File Integration:
class NpcEntity {
public:
IfoObject base;
uint32 npc_id;
AiBehavior ai_behavior;
std::unique_ptr<QuestScript> quest_script;
static std::unique_ptr<NpcEntity> from_ifo(const IfoNpc& ifo, const NpcDatabase& npc_db) {
const NpcDef* npc_def = npc_db.get(ifo.object.object_id);
if (!npc_def) {
ZZ_LOG("NpcEntity::from_ifo: NPC %d not found\n", ifo.object.object_id);
return nullptr;
}
auto entity = std::make_unique<NpcEntity>();
entity->base = ifo.object;
entity->npc_id = ifo.object.object_id;
entity->ai_behavior = AiBehavior::from_id(ifo.ai_id);
if (!ifo.quest_file_name.empty()) {
entity->quest_script = QuestScript::load(ifo.quest_file_name);
}
return entity;
}
};
Animated Object Definitions:
Animated objects are decorations or interactive elements that have associated animations. They use the standard IfoObject structure with object_type = AnimatedObject (6).
Animated Object Configuration:
AnimatedObject Structure:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - object_type = AnimatedObject (6) │
│ - object_id: ZSC object reference │
│ - position, rotation, scale │
└─────────────────────────────────────────┘
Animation Sources:
- ZSC file contains animation references
- ZMO files define the actual animation data
- Animations can be looped or triggered
Common Animated Object Types:
Object Type | Animation Type | Example
---------------------|----------------------|------------------
Windmill | Continuous loop | Rotating blades
Water wheel | Continuous loop | Rotating wheel
Flag | Continuous loop | Waving flag
Door | Triggered | Open/close on click
Chest | Triggered | Open on loot
Campfire | Particle + animation | Flickering flames
Animation Playback Configuration:
class AnimatedObject {
public:
IfoObject base;
ZscObject zsc_object;
std::string active_animation;
AnimationState animation_state;
void update(float delta_time) {
if (!active_animation.empty()) {
animation_state.advance(delta_time);
// Apply animation to all parts
for (auto& part : zsc_object.parts) {
if (part.animation) {
float frame = animation_state.current_frame;
part.apply_animation_frame(*part.animation, frame);
}
}
}
}
};
enum AnimationPlayMode {
ANIMATION_LOOP, // Continuous playback
ANIMATION_ONCE, // Play once and stop
ANIMATION_PING_PONG, // Forward then reverse
ANIMATION_TRIGGERED // Play on event
};
Collision Object Properties:
Collision objects define invisible or visible geometry that participates in physics collision detection. They use object_type = CollisionObject (11).
Collision Object Types:
Collision Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - object_type = CollisionObject (11) │
│ - position, rotation, scale │
│ - object_id: ZSC collision mesh │
├─────────────────────────────────────────┤
│ Collision Shape (from ZSC): │
│ - None (0): No collision │
│ - Sphere (1): Bounding sphere │
│ - AABB (2): Axis-aligned box │
│ - OBB (3): Oriented bounding box │
│ - Polygon (4): Triangle mesh │
└─────────────────────────────────────────┘
Collision Flags (from ZSC):
Collision Flag Bits:
Bit 0-2: Shape type
Bit 3: NOT_MOVEABLE (static object)
Bit 4: NOT_PICKABLE (no mouse selection)
Bit 5: HEIGHT_ONLY (terrain collision only)
Bit 6: NOT_CAMERA_COLLISION (camera passes through)
Bit 7: PASSTHROUGH (player can walk through)
Common Configurations:
┌──────────────────┬────────────────────────┐
│ Object │ Collision Flags │
├──────────────────┼────────────────────────┤
│ Building wall │ OBB, NOT_MOVEABLE │
│ Fence │ OBB, NOT_CAMERA_COLL │
│ Water │ HEIGHT_ONLY │
│ Trigger zone │ PASSTHROUGH │
│ Invisible wall │ NOT_PICKABLE │
└──────────────────┴────────────────────────┘
Physics Integration:
class CollisionObject {
public:
IfoObject base;
CollisionShape collision_shape;
CollisionFlags collision_flags;
PhysicsHandle physics_handle;
void add_to_physics(PhysicsWorld& physics) {
Transform transform = Transform::from_pos_rot_scale(
base.position,
base.rotation,
base.scale
);
switch (collision_shape.type) {
case COLLISION_SHAPE_SPHERE:
physics_handle = physics.add_sphere(
transform, collision_shape.radius, collision_flags
);
break;
case COLLISION_SHAPE_AABB:
physics_handle = physics.add_box(
transform, collision_shape.half_extents, collision_flags
);
break;
case COLLISION_SHAPE_POLYGON:
physics_handle = physics.add_mesh(
transform, collision_shape.mesh, collision_flags
);
break;
default:
break;
}
}
};
Deco and CNST Object Types:
Decoration (Deco) and Construction (Cnst) objects are the two primary static object types placed in zones, differing primarily in their collision behavior.
Deco Objects (type 1):
Characteristics:
- object_type = DecoObject (1)
- No physics collision (player can walk through)
- Used for visual-only elements
- Lower rendering priority
Common Uses:
- Grass and small plants
- Hanging banners/cloth
- Small rocks and debris
- Background decorations
- Particle effect emitters
CNST Objects (type 3):
Characteristics:
- object_type = CnstObject (3)
- Has physics collision
- Blocks player movement
- Higher rendering priority
- Camera collision enabled
Common Uses:
- Buildings and walls
- Large rocks and boulders
- Fences and barriers
- Bridges and platforms
- Furniture and large props
Comparison Table:
┌─────────────────┬─────────────────┬─────────────────┐
│ Property │ Deco │ Cnst │
├─────────────────┼─────────────────┼─────────────────┤
│ Collision │ None │ Full │
│ Player blocking │ No │ Yes │
│ Camera blocking │ No │ Yes │
│ Mouse picking │ Optional │ Yes │
│ Shadow casting │ Optional │ Yes │
│ LOD distance │ Shorter │ Longer │
│ Memory priority │ Lower │ Higher │
└─────────────────┴─────────────────┴─────────────────┘
Code Implementation:
Entity* load_zone_object(const IfoObject& obj) {
auto zsc = vfs.load_zsc(obj.object_id);
if (!zsc) {
ZZ_LOG("load_zone_object: failed to load ZSC %d\n", obj.object_id);
return nullptr;
}
switch (obj.object_type) {
case OBJECT_TYPE_DECO:
// Decoration: visual only, no collision
return create_decoration_entity(obj, zsc);
case OBJECT_TYPE_CNST:
// Construction: has collision
Entity* entity = create_decoration_entity(obj, zsc);
if (entity) {
add_collision_to_entity(*entity, *zsc);
}
return entity;
default:
ZZ_LOG("load_zone_object: unknown object type %d\n", obj.object_type);
return nullptr;
}
}
Effect and Sound Object Types:
Effect and Sound objects are ambient elements that enhance the atmosphere of a zone without physical presence.
Effect Objects (type 5):
IfoEffect Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - object_type = EffectObject (5) │
│ - position: Effect spawn point │
├─────────────────────────────────────────┤
│ effect_path: "EFFECT/FIRE_TORCH.EFT" │
└─────────────────────────────────────────┘
Common Effect Types:
- Torches and fires
- Water fountains
- Magic auras
- Smoke and steam
- Light beams
Sound Objects (type 4):
IfoSound Configuration:
┌─────────────────────────────────────────┐
│ IfoObject (base) │
│ - object_type = SoundObject (4) │
│ - position: Sound emitter location │
├─────────────────────────────────────────┤
│ sound_path: "SOUND/AMBIENT/FOREST.WAV" │
│ range: 1000 (audible radius in cm) │
│ interval: 30 (play every 30 seconds) │
└─────────────────────────────────────────┘
Sound Playback Modes:
- Loop: Continuous playback
- Interval: Play every N seconds
- Triggered: Play on event
- Random: Random intervals with variance
Ambient System Integration:
class AmbientManager {
public:
std::vector<EffectEmitter> effects;
std::vector<SoundEmitter> sounds;
void load_from_ifo(const IfoFile& ifo) {
for (const auto& obj : ifo.objects) {
switch (obj.object_type) {
case OBJECT_TYPE_EFFECT: {
IfoEffect effect = IfoEffect::from_object(obj);
effects.push_back(EffectEmitter{
effect.object.position,
effect.effect_path,
true
});
break;
}
case OBJECT_TYPE_SOUND: {
IfoSound sound = IfoSound::from_object(obj);
sounds.push_back(SoundEmitter{
sound.object.position,
sound.sound_path,
static_cast<float>(sound.range),
static_cast<float>(sound.interval),
0.0f
});
break;
}
default:
break;
}
}
}
void update(float delta_time, const Vec3& listener_pos) {
// Update sound emitters
for (auto& sound : sounds) {
float distance = (sound.position - listener_pos).length();
if (distance <= sound.range) {
sound.timer += delta_time;
if (sound.timer >= sound.interval) {
audio::play_3d_sound(sound.sound_path, sound.position);
sound.timer = 0.0f;
}
}
}
}
};
3.7 HIM Heightmap Files (.him)
File Header and Magic Number:
No magic string. Direct data layout.
Offset Type Description
0x00 u32 Width
0x04 u32 Height
0x08 u32[2] Unknown (8 bytes)
0x10 f32[] Height data (width × height)
Heightmap Grid Structure:
The heightmap is organized as a 2D grid of floating-point height values that define the terrain elevation at each grid point. The grid forms the foundation for terrain mesh generation.
Grid Organization:
HIM Grid Layout:
┌─────────────────────────────────────┐
│ [0][0] [1][0] ... [W-1][0] │ ← Row 0 (North edge)
│ [0][1] [1][1] ... [W-1][1] │
│ ... ... ... ... │
│ [0][H-1] [1][H-1] ... [W-1][H-1] │ ← Row H-1 (South edge)
└─────────────────────────────────────┘
↑ Column 0 ↑ Column W-1
(West edge) (East edge)
Key Relationships:
- Grid dimensions are defined by
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