1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
#pragma once
#include <whiteout/common_types.h>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace whiteout {
namespace mdx {
// ─── MDL Parse Tree ──────────────────────────────────────────────────────────
// A generic, format-agnostic AST for the MDL text format. The parser knows
// only syntax (blocks, properties, animation tracks, vectors) — semantic
// meaning is left to higher-level consumers.
//
// Grammar summary (both v800 and v1000):
//
// file ::= block*
// block ::= IDENT [STRING] [NUMBER*] '{' (block | property | keyframe_track)* '}'
// property ::= ['static'] IDENT value+ ','
// | IDENT ',' (bare flag)
// value ::= NUMBER | STRING | IDENT | '{' value (',' value)* '}'
// keyframe_track ::= IDENT NUMBER '{' IDENT ',' keyframe* '}'
// keyframe ::= NUMBER ':' value ','
// ['InTan' value ',']
// ['OutTan' value ',']
// Forward declarations
struct MdlNode;
// ─── Value types ─────────────────────────────────────────────────────────────
// An atomic value: number, string, identifier, or a curly-brace-delimited
// list of atomic values (used for vectors like { 1, 2, 3 }).
struct MdlValue {
using Array = std::vector<MdlValue>;
std::variant<f64, std::string, Array> data;
bool isNumber() const {
return std::holds_alternative<f64>(data);
}
bool isString() const {
return std::holds_alternative<std::string>(data);
}
bool isArray() const {
return std::holds_alternative<Array>(data);
}
f64 asNumber() const {
return std::get<f64>(data);
}
const std::string& asString() const {
return std::get<std::string>(data);
}
const Array& asArray() const {
return std::get<Array>(data);
}
};
// ─── Keyframe ────────────────────────────────────────────────────────────────
struct MdlKeyframe {
i32 time;
MdlValue value; // scalar or { vector }
MdlValue inTan; // empty (no data member) when not present
MdlValue outTan;
bool hasTangents = false;
};
// ─── Property ────────────────────────────────────────────────────────────────
// A named property with zero or more values, terminated by comma.
// Examples:
// MinimumExtent { -1, -2, -3 }, → name="MinimumExtent", values=[{-1,-2,-3}]
// static TextureID 5, → name="TextureID", values=[5], isStatic=true
// NonLooping, → name="NonLooping", values=[] (bare flag)
// Image "foo.blp", → name="Image", values=["foo.blp"]
struct MdlProperty {
std::string name;
std::vector<MdlValue> values;
bool isStatic = false;
/// Engine-style slot designator from "<= N" suffix (HD-texture sub-slot
/// in `static TextureID 5 <= 1,`). Set only when explicitly written.
std::optional<i32> slot;
};
// ─── Animation Track ─────────────────────────────────────────────────────────
// A named track with an interpolation type and a list of keyframes.
// Examples:
// Translation 402 { Linear, 300: { 0.2, 0.9, -2.3 }, ... }
// EventTrack 1 { 0, } (special: no interp type, bare values)
struct MdlAnimTrack {
std::string name;
u32 count = 0;
std::string interpolation; // "Linear", "Hermite", "Bezier", "DontInterp", or "" for EventTrack
u32 globalSequenceId = 0xFFFFFFFF; ///< 0xFFFFFFFF if track is not driven by a global sequence
/// Engine-style HD-texture slot designator from a "<= N" suffix on the
/// track header (e.g. `TextureID 3 <= 2 { ... }`). Set only when present.
std::optional<i32> slot;
std::vector<MdlKeyframe> keyframes;
};
// ─── Node ────────────────────────────────────────────────────────────────────
// A named block that may carry string/number header parameters and contains
// an ordered list of children: properties, animation tracks, or nested blocks.
//
// Examples:
// Version { ... } → name="Version", headerParams=[]
// Anim "Stand 1" { ... } → name="Anim", headerParams=["Stand 1"]
// Sequences 8 { ... } → name="Sequences", headerParams=[8]
// Model "HeroPaladinBoss" { ... } → name="Model", headerParams=["Hero..."]
// Faces 1 10572 { ... } → name="Faces", headerParams=[1, 10572]
// Bone "bone_turret" { ... } → name="Bone", headerParams=["bone_turret"]
using MdlChild = std::variant<MdlProperty, MdlAnimTrack, MdlNode>;
struct MdlNode {
std::string name;
std::vector<MdlValue> headerParams; // string/number params before the '{'
std::vector<MdlChild> children;
};
// ─── Parse Result ────────────────────────────────────────────────────────────
struct MdlParseError {
std::string message;
u32 line = 0;
u32 column = 0;
};
struct MdlDocument {
std::vector<MdlNode> roots; // top-level blocks (Version, Model, Sequences, ...)
std::vector<MdlParseError> errors;
bool hasErrors() const {
return !errors.empty();
}
};
// ─── Parser ──────────────────────────────────────────────────────────────────
class MdlParser {
public:
/// Parse an MDL source string into a document tree.
/// The source data must remain valid for the duration of parsing
/// (string views in tokens reference it), but the returned MdlDocument
/// owns all its data and is independent of the source buffer.
static MdlDocument parse(std::string_view source);
};
} // namespace mdx
} // namespace whiteout