载入中...
搜索中...
未找到
ActionTimeline.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <tuple>
7#include <utility>
8
9namespace eve::action {
10namespace {
11
12const Value* field(const Value& value, std::string_view name) { return value.find(std::string(name)); }
13
14Result<std::string> stringField(const Value& value, std::string_view name, std::string path) {
15 const Value* member = field(value, name);
16 if (!member || !member->isString())
18 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected string field", path));
19 return Result<std::string>::success(member->asString());
20}
21
22Result<std::int64_t> intField(const Value& value, std::string_view name, std::string path) {
23 const Value* member = field(value, name);
24 if (!member || !member->isInt64())
26 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected integer field", path));
28}
29
30Result<bool> boolField(const Value& value, std::string_view name, std::string path) {
31 const Value* member = field(value, name);
32 if (!member || !member->isBool())
34 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected boolean field", path));
35 return Result<bool>::success(member->asBool());
36}
37
38Result<LogicalId> logicalIdField(const Value& value, std::string_view name, std::string path) {
39 auto text = stringField(value, name, path);
40 if (!text) return Result<LogicalId>::failure(text.status());
41 auto parsed = LogicalId::parse(text.value());
42 if (!parsed)
44 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected namespace:name logical id", path));
45 return Result<LogicalId>::success(std::move(*parsed));
46}
47
48Value payloadValue(const Value::Object& payload) { return Value(payload); }
49
50Result<Value::Object> payloadField(const Value& value, std::string path) {
51 const Value* payload = field(value, "payload");
52 if (!payload || !payload->isObject())
54 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected object payload", path));
56}
57
58Value encodeNotify(const ActionNotify& notify) {
60 object["id"] = notify.id.format();
61 object["type"] = notify.type.format();
62 object["timeNs"] = notify.time.nanoseconds();
63 object["payload"] = payloadValue(notify.payload);
64 object["enabled"] = notify.enabled;
65 return Value(std::move(object));
66}
67
68Value encodeState(const ActionNotifyState& state) {
70 object["id"] = state.id.format();
71 object["type"] = state.type.format();
72 object["startNs"] = state.start.nanoseconds();
73 object["endNs"] = state.end.nanoseconds();
74 object["payload"] = payloadValue(state.payload);
75 object["enabled"] = state.enabled;
76 return Value(std::move(object));
77}
78
79Value encodeAnimationSection(const ActionAnimationSection& section) {
81 object["id"] = section.id.format();
82 object["animationUri"] = section.animationUri;
83 object["startNs"] = section.start.nanoseconds();
84 object["endNs"] = section.end.nanoseconds();
85 object["blendInNs"] = section.blendIn.nanoseconds();
86 object["sourceStartNs"] = section.sourceStart.nanoseconds();
87 object["sourceEndNs"] = section.sourceEnd.nanoseconds();
88 object["blendCurve"] = std::string(actionBlendCurveName(section.blendCurve));
89 return Value(std::move(object));
90}
91
92Result<ActionAnimationSection> decodeAnimationSection(const Value& value, const std::string& path,
93 std::int64_t schemaVersion) {
94 if (!value.isObject())
96 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected animation section object", path));
97 auto id = logicalIdField(value, "id", path + ".id");
99 auto uri = stringField(value, "animationUri", path + ".animationUri");
100 if (!uri) return Result<ActionAnimationSection>::failure(uri.status());
101 auto start = intField(value, "startNs", path + ".startNs");
103 auto end = intField(value, "endNs", path + ".endNs");
104 if (!end) return Result<ActionAnimationSection>::failure(end.status());
105 auto blend = intField(value, "blendInNs", path + ".blendInNs");
107 ActionAnimationSection section{std::move(id).takeValue(), std::move(uri).takeValue(),
110 if (schemaVersion >= 3) {
111 auto sourceStart = intField(value, "sourceStartNs", path + ".sourceStartNs");
112 if (!sourceStart) return Result<ActionAnimationSection>::failure(sourceStart.status());
113 auto sourceEnd = intField(value, "sourceEndNs", path + ".sourceEndNs");
114 if (!sourceEnd) return Result<ActionAnimationSection>::failure(sourceEnd.status());
115 auto blendCurve = stringField(value, "blendCurve", path + ".blendCurve");
116 if (!blendCurve) return Result<ActionAnimationSection>::failure(blendCurve.status());
117 auto parsedCurve = actionBlendCurveFromName(blendCurve.value());
118 if (!parsedCurve)
120 DiagnosticCode::InvalidArgument, "unknown animation blend curve", path + ".blendCurve"));
121 section.sourceStart = Duration::fromNanoseconds(sourceStart.value());
122 section.sourceEnd = Duration::fromNanoseconds(sourceEnd.value());
123 section.blendCurve = *parsedCurve;
124 }
125 return Result<ActionAnimationSection>::success(std::move(section));
126}
127
128Result<ActionNotify> decodeNotify(const Value& value, const std::string& path, std::int64_t schemaVersion) {
129 if (!value.isObject())
131 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected notify object", path));
132 auto id = logicalIdField(value, "id", path + ".id");
133 if (!id) return Result<ActionNotify>::failure(id.status());
134 auto type = logicalIdField(value, "type", path + ".type");
135 if (!type) return Result<ActionNotify>::failure(type.status());
136 auto time = intField(value, "timeNs", path + ".timeNs");
137 if (!time) return Result<ActionNotify>::failure(time.status());
138 auto payload = payloadField(value, path + ".payload");
139 if (!payload) return Result<ActionNotify>::failure(payload.status());
140 bool enabled = true;
141 if (schemaVersion >= 4) {
142 auto decodedEnabled = boolField(value, "enabled", path + ".enabled");
143 if (!decodedEnabled) return Result<ActionNotify>::failure(decodedEnabled.status());
144 enabled = decodedEnabled.value();
145 }
146 return Result<ActionNotify>::success(ActionNotify{std::move(id).takeValue(), std::move(type).takeValue(),
148 std::move(payload).takeValue(), enabled});
149}
150
151Result<ActionNotifyState> decodeState(const Value& value, const std::string& path, std::int64_t schemaVersion) {
152 if (!value.isObject())
154 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected notify-state object", path));
155 auto id = logicalIdField(value, "id", path + ".id");
156 if (!id) return Result<ActionNotifyState>::failure(id.status());
157 auto type = logicalIdField(value, "type", path + ".type");
158 if (!type) return Result<ActionNotifyState>::failure(type.status());
159 auto start = intField(value, "startNs", path + ".startNs");
160 if (!start) return Result<ActionNotifyState>::failure(start.status());
161 auto end = intField(value, "endNs", path + ".endNs");
162 if (!end) return Result<ActionNotifyState>::failure(end.status());
163 auto payload = payloadField(value, path + ".payload");
165 bool enabled = true;
166 if (schemaVersion >= 4) {
167 auto decodedEnabled = boolField(value, "enabled", path + ".enabled");
168 if (!decodedEnabled) return Result<ActionNotifyState>::failure(decodedEnabled.status());
169 enabled = decodedEnabled.value();
170 }
171 return Result<ActionNotifyState>::success(ActionNotifyState{
172 std::move(id).takeValue(), std::move(type).takeValue(), Duration::fromNanoseconds(start.value()),
173 Duration::fromNanoseconds(end.value()), std::move(payload).takeValue(), enabled});
174}
175
176bool inWindow(Duration value, Duration previous, Duration current, bool includePrevious) {
177 return includePrevious ? value >= previous && value <= current : value > previous && value <= current;
178}
179
180} // namespace
181
182std::string_view actionBlendCurveName(ActionBlendCurve curve) noexcept {
183 switch (curve) {
184 case ActionBlendCurve::Linear: return "linear";
185 case ActionBlendCurve::EaseInOut: return "ease-in-out";
186 }
187 return "ease-in-out";
188}
189
190std::optional<ActionBlendCurve> actionBlendCurveFromName(std::string_view name) noexcept {
191 if (name == "linear") return ActionBlendCurve::Linear;
192 if (name == "ease-in-out") return ActionBlendCurve::EaseInOut;
193 return std::nullopt;
194}
195
196std::string_view actionTrackKindName(ActionTrackKind kind) noexcept {
197 switch (kind) {
198 case ActionTrackKind::Animation: return "animation";
199 case ActionTrackKind::Gameplay: return "gameplay";
200 case ActionTrackKind::Effect: return "effect";
201 case ActionTrackKind::Audio: return "audio";
202 case ActionTrackKind::Camera: return "camera";
203 case ActionTrackKind::Movement: return "movement";
204 case ActionTrackKind::Tag: return "tag";
205 case ActionTrackKind::Custom: return "custom";
206 }
207 return "custom";
208}
209
218
222 Diagnostic::error(DiagnosticCode::InvalidArgument, "unsupported action timeline schema version", "schemaVersion"));
224 Diagnostic::error(DiagnosticCode::InvalidArgument, "action timeline requires a valid action id", "actionId"));
226 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline duration must be non-negative", "durationNs"));
227 if (!std::isfinite(montage.basePlayRate) || montage.basePlayRate <= 0.0)
229 Diagnostic::error(DiagnosticCode::InvalidArgument, "montage base play rate must be positive and finite", "montage.basePlayRate"));
233 Diagnostic::error(DiagnosticCode::InvalidArgument, "montage blend durations must be non-negative", "montage"));
234 for (std::size_t index = 0; index < splitTimestamps.size(); ++index) {
238 Diagnostic::error(DiagnosticCode::InvalidArgument, "physical section splits must be strictly ordered inside the timeline", "splitTimestampsNs[" + std::to_string(index) + "]"));
239 }
240
241 std::set<std::string> ids;
242 for (std::size_t index = 0; index < animationSections.size(); ++index) {
243 const auto& section = animationSections[index];
244 const std::string path = "animationSections[" + std::to_string(index) + "]";
245 if (!section.id.isValid()) return Result<void>::failure(
246 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation section id is invalid", path + ".id"));
247 if (!ids.insert(section.id.format()).second) return Result<void>::failure(
248 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline item ids must be unique", path + ".id"));
249 if (section.animationUri.empty()) return Result<void>::failure(
250 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation section requires a URI", path + ".animationUri"));
251 if (section.start < Duration::zero() || section.end <= section.start || section.end > duration)
253 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation section range is invalid", path));
254 if (section.blendIn < Duration::zero() ||
255 section.blendIn.nanoseconds() > section.end.nanoseconds() - section.start.nanoseconds())
257 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation section blend-in is outside its range", path + ".blendInNs"));
258 if (section.sourceStart < Duration::zero() || section.sourceEnd < Duration::zero() ||
259 (!section.sourceEnd.isZero() && section.sourceEnd <= section.sourceStart))
261 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation section source trim is invalid", path + ".sourceStartNs"));
262 if (index > 0) {
263 const auto& previous = animationSections[index - 1];
264 if (previous.start > section.start)
266 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation sections must be sorted by start time", path + ".startNs"));
267 if (previous.end > section.start) return Result<void>::failure(
268 Diagnostic::error(DiagnosticCode::InvalidArgument, "animation sections must not overlap", path + ".startNs"));
269 }
270 }
271 for (std::size_t trackIndex = 0; trackIndex < tracks.size(); ++trackIndex) {
272 const auto& track = tracks[trackIndex];
273 const std::string path = "tracks[" + std::to_string(trackIndex) + "]";
274 if (!track.id.isValid()) return Result<void>::failure(
275 Diagnostic::error(DiagnosticCode::InvalidArgument, "track id is invalid", path + ".id"));
276 if (!ids.insert(track.id.format()).second) return Result<void>::failure(
277 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline item ids must be unique", path + ".id"));
278 for (std::size_t index = 0; index < track.notifies.size(); ++index) {
279 const auto& notify = track.notifies[index];
280 const std::string itemPath = path + ".notifies[" + std::to_string(index) + "]";
281 if (!notify.id.isValid() || !notify.type.isValid()) return Result<void>::failure(
282 Diagnostic::error(DiagnosticCode::InvalidArgument, "notify ids are invalid", itemPath));
283 if (!ids.insert(notify.id.format()).second)
285 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline item ids must be unique", itemPath + ".id"));
286 if (notify.time < Duration::zero() || notify.time > duration)
288 Diagnostic::error(DiagnosticCode::InvalidArgument, "notify time is outside the timeline", itemPath + ".timeNs"));
289 if (index > 0 && track.notifies[index - 1].time > notify.time)
291 Diagnostic::error(DiagnosticCode::InvalidArgument, "notifies must be sorted by time", itemPath + ".timeNs"));
292 }
293 for (std::size_t index = 0; index < track.states.size(); ++index) {
294 const auto& state = track.states[index];
295 const std::string itemPath = path + ".states[" + std::to_string(index) + "]";
296 if (!state.id.isValid() || !state.type.isValid()) return Result<void>::failure(
297 Diagnostic::error(DiagnosticCode::InvalidArgument, "notify-state ids are invalid", itemPath));
298 if (!ids.insert(state.id.format()).second)
300 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline item ids must be unique", itemPath + ".id"));
301 if (state.start < Duration::zero() || state.end <= state.start || state.end > duration)
303 Diagnostic::error(DiagnosticCode::InvalidArgument, "notify-state range is invalid", itemPath));
304 if (index > 0 && track.states[index - 1].start > state.start)
306 Diagnostic::error(DiagnosticCode::InvalidArgument, "notify states must be sorted by start time", itemPath + ".startNs"));
307 }
308 }
309 return Result<void>::success();
310}
311
313 bool includePrevious) const {
314 auto valid = validate();
315 if (!valid) return Result<std::vector<ActionTimelineEvent>>::failure(valid.status());
316 if (previous < Duration::zero() || current < previous || current > duration)
318 Diagnostic::error(DiagnosticCode::InvalidArgument, "sample range is outside the timeline", "sample"));
319
320 std::vector<ActionTimelineEvent> out;
321 for (const auto& track : tracks) {
322 if (track.muted) continue;
323 for (const auto& notify : track.notifies) {
324 if (!notify.enabled) continue;
325 if (inWindow(notify.time, previous, current, includePrevious))
326 out.push_back(
327 {ActionTimelineEventKind::Notify, track.id, notify.id, notify.type, notify.time, notify.payload});
328 }
329 for (const auto& state : track.states) {
330 if (!state.enabled) continue;
331 if (inWindow(state.start, previous, current, includePrevious))
332 out.push_back(
333 {ActionTimelineEventKind::StateEnter, track.id, state.id, state.type, state.start, state.payload});
334 if (inWindow(state.end, previous, current, includePrevious))
335 out.push_back(
336 {ActionTimelineEventKind::StateExit, track.id, state.id, state.type, state.end, state.payload});
337 }
338 }
339 std::stable_sort(out.begin(), out.end(), [](const auto& left, const auto& right) {
340 if (left.time != right.time) return left.time < right.time;
341 if (left.trackId != right.trackId) return left.trackId.format() < right.trackId.format();
342 if (left.itemId != right.itemId) return left.itemId.format() < right.itemId.format();
343 return static_cast<std::uint8_t>(left.kind) < static_cast<std::uint8_t>(right.kind);
344 });
345 return Result<std::vector<ActionTimelineEvent>>::success(std::move(out));
346}
347
348Result<std::vector<ActionActiveBlock>> ActionTimeline::activeBlocks(Duration time) const {
349 auto valid = validate();
350 if (!valid) return Result<std::vector<ActionActiveBlock>>::failure(valid.status());
351 if (time < Duration::zero() || time > duration)
353 Diagnostic::error(DiagnosticCode::InvalidArgument, "active-block sample is outside the timeline", "time"));
354
355 std::vector<ActionActiveBlock> out;
356 for (const auto& track : tracks) {
357 if (track.muted) continue;
358 for (const auto& state : track.states) {
359 if (!state.enabled) continue;
360 if (time < state.start || time >= state.end) continue;
361 out.push_back({track.id,
362 state.id,
363 state.type,
364 Duration::fromNanoseconds(time.nanoseconds() - state.start.nanoseconds()),
365 Duration::fromNanoseconds(state.end.nanoseconds() - state.start.nanoseconds()),
366 state.payload});
367 }
368 }
369 std::stable_sort(out.begin(), out.end(), [](const auto& left, const auto& right) {
370 if (left.trackId != right.trackId) return left.trackId.format() < right.trackId.format();
371 return left.itemId.format() < right.itemId.format();
372 });
373 return Result<std::vector<ActionActiveBlock>>::success(std::move(out));
374}
375
376Result<Value> ActionTimeline::toValue() const {
377 auto valid = validate();
378 if (!valid) return Result<Value>::failure(valid.status());
379 Value::Array encodedTracks;
380 Value::Array encodedSections;
381 Value::Array encodedSplits;
382 encodedSections.reserve(animationSections.size());
383 for (const auto& section : animationSections) encodedSections.push_back(encodeAnimationSection(section));
384 encodedSplits.reserve(splitTimestamps.size());
385 for (const Duration split : splitTimestamps) encodedSplits.emplace_back(split.nanoseconds());
386 encodedTracks.reserve(tracks.size());
387 for (const auto& track : tracks) {
388 Value::Array notifies;
389 Value::Array states;
390 for (const auto& notify : track.notifies) notifies.push_back(encodeNotify(notify));
391 for (const auto& state : track.states) states.push_back(encodeState(state));
393 object["id"] = track.id.format();
394 object["label"] = track.label;
395 object["kind"] = std::string(actionTrackKindName(track.kind));
396 object["muted"] = track.muted;
397 object["locked"] = track.locked;
398 object["notifies"] = Value(std::move(notifies));
399 object["states"] = Value(std::move(states));
400 encodedTracks.emplace_back(std::move(object));
401 }
403 root["schema"] = std::string(kActionTimelineSchemaId);
404 root["schemaVersion"] = static_cast<std::int64_t>(schemaVersion.value());
405 root["actionId"] = actionId.format();
406 root["durationNs"] = duration.nanoseconds();
407 root["animationUri"] = animationSections.empty() ? animationUri : animationSections.front().animationUri;
408 root["animationSections"] = Value(std::move(encodedSections));
409 root["splitTimestampsNs"] = Value(std::move(encodedSplits));
410 root["montage"] = Value::Object{{"basePlayRate", montage.basePlayRate},
411 {"looping", montage.looping},
412 {"footIk", montage.footIk},
413 {"animationLayer", static_cast<std::int64_t>(montage.animationLayer)},
414 {"defaultBlendInNs", montage.defaultBlendIn.nanoseconds()},
415 {"defaultBlendOutNs", montage.defaultBlendOut.nanoseconds()},
416 {"blendOutOffsetNs", montage.blendOutOffset.nanoseconds()},
417 {"rootMotionHorizontal", montage.rootMotionHorizontal},
418 {"rootMotionVertical", montage.rootMotionVertical},
419 {"rootMotionRotation", montage.rootMotionRotation}};
420 root["tracks"] = Value(std::move(encodedTracks));
421 root["metadata"] = Value(metadata);
422 return Result<Value>::success(Value(std::move(root)));
423}
424
425Result<ActionTimeline> ActionTimeline::fromValue(const Value& value) {
426 if (!value.isObject())
428 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected action timeline object", "timeline"));
429 auto schema = stringField(value, "schema", "schema");
430 if (!schema) return Result<ActionTimeline>::failure(schema.status());
431 if (schema.value() != kActionTimelineSchemaId)
433 Diagnostic::error(DiagnosticCode::InvalidArgument, "unexpected action timeline schema", "schema"));
434 auto version = intField(value, "schemaVersion", "schemaVersion");
435 if (!version) return Result<ActionTimeline>::failure(version.status());
436 auto actionId = logicalIdField(value, "actionId", "actionId");
437 if (!actionId) return Result<ActionTimeline>::failure(actionId.status());
438 auto duration = intField(value, "durationNs", "durationNs");
440 auto animationUri = stringField(value, "animationUri", "animationUri");
441 if (!animationUri) return Result<ActionTimeline>::failure(animationUri.status());
442 const Value* tracksValue = field(value, "tracks");
443 const Value* metadataValue = field(value, "metadata");
444 if (!tracksValue || !tracksValue->isArray())
446 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected tracks array", "tracks"));
447 if (!metadataValue || !metadataValue->isObject())
449 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected metadata object", "metadata"));
450
451 ActionTimeline candidate;
452 if (version.value() < 1 || version.value() > static_cast<std::int64_t>(kActionTimelineSchemaVersion))
454 DiagnosticCode::InvalidArgument, "unsupported action timeline schema version", "schemaVersion"));
456 candidate.actionId = std::move(actionId).takeValue();
457 candidate.duration = Duration::fromNanoseconds(duration.value());
458 candidate.animationUri = std::move(animationUri).takeValue();
459 candidate.metadata = *metadataValue->getIf<Value::Object>();
460 if (version.value() == 1) {
461 if (!candidate.animationUri.empty() && !candidate.duration.isZero()) {
462 candidate.animationSections.push_back({*LogicalId::fromParts("animation-section", "legacy"),
463 candidate.animationUri, Duration::zero(), candidate.duration,
464 Duration::zero()});
465 }
466 } else {
467 const Value* sectionsValue = field(value, "animationSections");
468 if (!sectionsValue || !sectionsValue->isArray())
470 DiagnosticCode::InvalidArgument, "expected animationSections array", "animationSections"));
471 const auto& sections = *sectionsValue->getIf<Value::Array>();
472 for (std::size_t index = 0; index < sections.size(); ++index) {
473 auto decoded = decodeAnimationSection(sections[index], "animationSections[" + std::to_string(index) + "]",
474 version.value());
475 if (!decoded) return Result<ActionTimeline>::failure(decoded.status());
476 candidate.animationSections.push_back(std::move(decoded).takeValue());
477 }
478 if (!candidate.animationSections.empty())
479 candidate.animationUri = candidate.animationSections.front().animationUri;
480 if (version.value() >= 3) {
481 const Value* splitsValue = field(value, "splitTimestampsNs");
482 if (!splitsValue || !splitsValue->isArray())
484 DiagnosticCode::InvalidArgument, "expected splitTimestampsNs array", "splitTimestampsNs"));
485 const auto& splits = *splitsValue->getIf<Value::Array>();
486 for (std::size_t index = 0; index < splits.size(); ++index) {
487 if (!splits[index].isInt64())
489 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected physical section timestamp",
490 "splitTimestampsNs[" + std::to_string(index) + "]"));
491 candidate.splitTimestamps.push_back(Duration::fromNanoseconds(splits[index].asInt()));
492 }
493 const Value* montageValue = field(value, "montage");
494 if (!montageValue || !montageValue->isObject())
496 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected montage settings object", "montage"));
497 const Value* basePlayRate = field(*montageValue, "basePlayRate");
498 auto looping = boolField(*montageValue, "looping", "montage.looping");
499 auto footIk = boolField(*montageValue, "footIk", "montage.footIk");
500 auto layer = intField(*montageValue, "animationLayer", "montage.animationLayer");
501 auto blendIn = intField(*montageValue, "defaultBlendInNs", "montage.defaultBlendInNs");
502 auto blendOut = intField(*montageValue, "defaultBlendOutNs", "montage.defaultBlendOutNs");
503 auto blendOutOffset = intField(*montageValue, "blendOutOffsetNs", "montage.blendOutOffsetNs");
504 auto rootHorizontal = boolField(*montageValue, "rootMotionHorizontal", "montage.rootMotionHorizontal");
505 auto rootVertical = boolField(*montageValue, "rootMotionVertical", "montage.rootMotionVertical");
506 auto rootRotation = boolField(*montageValue, "rootMotionRotation", "montage.rootMotionRotation");
507 if (!basePlayRate || (!basePlayRate->isDouble() && !basePlayRate->isInt64()) || !looping || !footIk ||
508 !layer || !blendIn || !blendOut || !blendOutOffset || !rootHorizontal || !rootVertical ||
509 !rootRotation || layer.value() < 0)
511 Diagnostic::error(DiagnosticCode::InvalidArgument, "montage settings are invalid", "montage"));
512 candidate.montage.basePlayRate =
513 basePlayRate->isDouble() ? basePlayRate->asDouble() : static_cast<double>(basePlayRate->asInt());
514 candidate.montage.looping = looping.value();
515 candidate.montage.footIk = footIk.value();
516 candidate.montage.animationLayer = static_cast<std::uint32_t>(layer.value());
517 candidate.montage.defaultBlendIn = Duration::fromNanoseconds(blendIn.value());
518 candidate.montage.defaultBlendOut = Duration::fromNanoseconds(blendOut.value());
519 candidate.montage.blendOutOffset = Duration::fromNanoseconds(blendOutOffset.value());
520 candidate.montage.rootMotionHorizontal = rootHorizontal.value();
521 candidate.montage.rootMotionVertical = rootVertical.value();
522 candidate.montage.rootMotionRotation = rootRotation.value();
523 }
524 }
525 const auto& tracks = *tracksValue->getIf<Value::Array>();
526 for (std::size_t trackIndex = 0; trackIndex < tracks.size(); ++trackIndex) {
527 const Value& trackValue = tracks[trackIndex];
528 const std::string path = "tracks[" + std::to_string(trackIndex) + "]";
529 if (!trackValue.isObject())
531 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected track object", path));
532 auto id = logicalIdField(trackValue, "id", path + ".id");
533 auto label = stringField(trackValue, "label", path + ".label");
534 auto kindText = stringField(trackValue, "kind", path + ".kind");
535 auto muted = boolField(trackValue, "muted", path + ".muted");
536 auto locked = boolField(trackValue, "locked", path + ".locked");
537 if (!id) return Result<ActionTimeline>::failure(id.status());
538 if (!label) return Result<ActionTimeline>::failure(label.status());
539 if (!kindText) return Result<ActionTimeline>::failure(kindText.status());
540 if (!muted) return Result<ActionTimeline>::failure(muted.status());
541 if (!locked) return Result<ActionTimeline>::failure(locked.status());
542 auto kind = parseActionTrackKind(kindText.value());
543 if (!kind) return Result<ActionTimeline>::failure(kind.status());
544 const Value* notifies = field(trackValue, "notifies");
545 const Value* states = field(trackValue, "states");
546 if (!notifies || !notifies->isArray())
548 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected notifies array", path + ".notifies"));
549 if (!states || !states->isArray())
551 Diagnostic::error(DiagnosticCode::InvalidArgument, "expected states array", path + ".states"));
552 ActionTrack track;
553 track.id = std::move(id).takeValue();
554 track.label = std::move(label).takeValue();
555 track.kind = std::move(kind).takeValue();
556 track.muted = muted.value();
557 track.locked = locked.value();
558 const auto& notifyValues = *notifies->getIf<Value::Array>();
559 for (std::size_t index = 0; index < notifyValues.size(); ++index) {
560 auto decoded = decodeNotify(notifyValues[index], path + ".notifies[" + std::to_string(index) + "]",
561 version.value());
562 if (!decoded) return Result<ActionTimeline>::failure(decoded.status());
563 track.notifies.push_back(std::move(decoded).takeValue());
564 }
565 const auto& stateValues = *states->getIf<Value::Array>();
566 for (std::size_t index = 0; index < stateValues.size(); ++index) {
567 auto decoded = decodeState(stateValues[index], path + ".states[" + std::to_string(index) + "]",
568 version.value());
569 if (!decoded) return Result<ActionTimeline>::failure(decoded.status());
570 track.states.push_back(std::move(decoded).takeValue());
571 }
572 candidate.tracks.push_back(std::move(track));
573 }
574 auto valid = candidate.validate();
575 if (!valid) return Result<ActionTimeline>::failure(valid.status());
576 return Result<ActionTimeline>::success(std::move(candidate));
577}
578
579Result<std::pair<Duration, Duration>> ActionTimeline::sectionRange(std::size_t index) const {
580 if (index >= sectionCount())
582 DiagnosticCode::InvalidArgument, "physical section index is outside the timeline", "index"));
583 const Duration start = index == 0 ? Duration::zero() : splitTimestamps[index - 1];
584 const Duration end = index < splitTimestamps.size() ? splitTimestamps[index] : duration;
586}
587
588} // namespace eve::action
double value
Value::Object payload
bool locked
Duration start
Versioned, deterministic authoring timeline for gameplay actions.
float duration
int root
Definition AnimSmr.cpp:119
bool split
Definition CaveMesh.cpp:123
std::string label
wgpu::PopErrorScopeStatus status
glm::uvec4 ids
HexVec3 left
HexVec3 right
float blend
std::string text
TokenKind kind
std::vector< Track > tracks
std::string name
bool valid
graphics::Canvas * previous
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
std::string uri
double current
Heightmap curve
Json object
float(ui::Theme::* member)[4]
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
constexpr std::int64_t nanoseconds() const noexcept
Return the exact signed nanosecond representation.
Definition Time.h:69
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
constexpr bool isZero() const noexcept
Whether this duration is exactly zero.
Definition Time.h:75
static std::optional< LogicalId > fromParts(std::string_view namespaceName, std::string_view name)
Builds a logical ID from separate namespace and name components.
Definition Identity.cpp:48
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
bool isValid() const noexcept
Returns whether this value contains a valid namespace and name.
Definition Identity.h:433
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
std::map< std::string, Value > Object
Definition Value.h:34
bool isArray() const noexcept
Return true when this value is an array.
Definition Value.h:97
double asDouble() const
Return the Double payload; the caller must have checked the kind.
Definition Value.cpp:75
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
std::vector< Value > Array
Definition Value.h:33
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
bool isObject() const noexcept
Return true when this value is an object.
Definition Value.h:99
bool isDouble() const noexcept
Return true when this value is a Double.
Definition Value.h:93
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
std::string_view actionTrackKindName(ActionTrackKind kind) noexcept
Stable lowercase protocol spelling for a track kind.
std::optional< ActionBlendCurve > actionBlendCurveFromName(std::string_view name) noexcept
Parse a persisted montage cross-fade curve name.
constexpr std::uint64_t kActionTimelineSchemaVersion
Current action timeline schema version.
constexpr std::string_view kActionTimelineSchemaId
Canonical schema identifier for persisted action timelines.
Result< ActionTrackKind > parseActionTrackKind(std::string_view text)
Parse a stable track kind spelling.
std::string_view actionBlendCurveName(ActionBlendCurve curve) noexcept
Stable persisted name for a montage cross-fade curve.
ActionTrackKind
Semantic lane kind; hosts choose presentation without changing data.
ActionBlendCurve
Built-in deterministic cross-fade curve evaluated without asset callbacks.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
const EditorValue * field(const EditorValue &value, const char *name)
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
bool enabled
Canonical action timeline asset shared by runtime and editor.
ActionMontageSettings montage
Result< std::vector< ActionTimelineEvent > > sample(Duration previous, Duration current, bool includePrevious=false) const
Sample all boundaries in (previous,current] in stable order.
Result< void > validate() const
Validate ids, ranges, uniqueness and deterministic ordering.
std::vector< ActionAnimationSection > animationSections
std::string animationUri
Compatibility-only single-clip projection; v2 runtime uses animationSections.
std::vector< ActionTrack > tracks
std::vector< Duration > splitTimestamps
Sorted objective split timestamps forming N+1 physical sections.
Ordered semantic lane in one action timeline.
std::vector< ActionNotifyState > states
std::vector< ActionNotify > notifies