载入中...
搜索中...
未找到
ClimbingSnapshot.cpp
浏览该文件的文档.
1#include "climbing/Climbing.h"
2
4#include "physics/World3D.h"
5
6#include <charconv>
7#include <cmath>
8#include <limits>
9#include <optional>
10#include <utility>
11
12namespace eve::climbing {
13namespace {
14
15const eve::Value* field(const eve::Value::Object& object, std::string_view name) {
16 const auto found = object.find(std::string(name));
17 return found == object.end() ? nullptr : &found->second;
18}
19
20bool readString(const eve::Value::Object& object, std::string_view name, std::string& output) {
21 const eve::Value* value = field(object, name);
22 const auto* text = value ? value->getIf<std::string>() : nullptr;
23 if (!text) return false;
24 output = *text;
25 return true;
26}
27
28bool readInt64(const eve::Value::Object& object, std::string_view name, std::int64_t& output) {
29 const eve::Value* value = field(object, name);
30 const auto* number = value ? value->getIf<std::int64_t>() : nullptr;
31 if (!number) return false;
32 output = *number;
33 return true;
34}
35
36bool readBool(const eve::Value::Object& object, std::string_view name, bool& output, bool defaultValue, bool required) {
37 const eve::Value* value = field(object, name);
38 if (!value) {
40 return !required;
41 }
42 const auto* boolean = value->getIf<bool>();
43 if (!boolean) return false;
44 output = *boolean;
45 return true;
46}
47
48bool readUint64String(const eve::Value::Object& object, std::string_view name, std::uint64_t& output) {
49 std::string text;
50 if (!readString(object, name, text) || text.empty()) return false;
51 const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), output);
52 return error == std::errc{} && end == text.data() + text.size();
53}
54
55eve::Value vecValue(Vec3 value) {
57}
58
59bool numericFloat(const eve::Value& value, float& output) {
60 double number = 0.0;
61 if (const auto* integer = value.getIf<std::int64_t>())
62 number = static_cast<double>(*integer);
63 else if (const auto* real = value.getIf<double>())
64 number = *real;
65 else
66 return false;
67 if (!std::isfinite(number) || number < -static_cast<double>(std::numeric_limits<float>::max()) ||
68 number > static_cast<double>(std::numeric_limits<float>::max()))
69 return false;
70 output = static_cast<float>(number);
71 return true;
72}
73
74bool readVec(const eve::Value::Object& object, std::string_view name, Vec3& output) {
75 const eve::Value* value = field(object, name);
76 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
77 return array && array->size() == 3 && numericFloat((*array)[0], output.x) && numericFloat((*array)[1], output.y) &&
78 numericFloat((*array)[2], output.z);
79}
80
81bool readOptionalFloat(const eve::Value::Object& object, std::string_view name, float& output) {
82 const eve::Value* value = field(object, name);
83 return !value || numericFloat(*value, output);
84}
85
86std::string_view phaseName(ClimbingPhase phase) {
87 switch (phase) {
88 case ClimbingPhase::Idle: return "idle";
89 case ClimbingPhase::Requested: return "requested";
90 case ClimbingPhase::Aligning: return "aligning";
91 case ClimbingPhase::Launching: return "launching";
92 case ClimbingPhase::Climbing: return "climbing";
93 case ClimbingPhase::Landing: return "landing";
94 case ClimbingPhase::Recovering: return "recovering";
95 case ClimbingPhase::Hanging: return "hanging";
96 case ClimbingPhase::Dropping: return "dropping";
97 case ClimbingPhase::Completed: return "completed";
98 case ClimbingPhase::Cancelled: return "cancelled";
99 case ClimbingPhase::Failed: return "failed";
100 case ClimbingPhase::Balanced: return "balanced";
101 case ClimbingPhase::Swinging: return "swinging";
102 }
103 return "unknown";
104}
105
106bool readPhase(std::string_view value, ClimbingPhase& output) {
107 if (value == "idle")
109 else if (value == "requested")
111 else if (value == "aligning")
113 else if (value == "launching")
115 else if (value == "climbing" || value == "traversing")
116 // "traversing" is accepted only to migrate snapshots emitted before the
117 // public climbing terminology was finalized.
119 else if (value == "landing")
121 else if (value == "recovering")
123 else if (value == "hanging")
125 else if (value == "dropping")
127 else if (value == "completed")
129 else if (value == "cancelled")
131 else if (value == "failed")
133 else if (value == "balanced")
135 else if (value == "swinging")
137 else
138 return false;
139 return true;
140}
141
142std::string_view eventKindName(ClimbingEventKind kind) {
143 switch (kind) {
144 case ClimbingEventKind::Started: return "started";
145 case ClimbingEventKind::AnchorTransitionStarted: return "anchor_transition_started";
146 case ClimbingEventKind::AnchorReached: return "anchor_reached";
147 case ClimbingEventKind::ContactLeftHand: return "contact_left_hand";
148 case ClimbingEventKind::ContactRightHand: return "contact_right_hand";
149 case ClimbingEventKind::Landed: return "landed";
150 case ClimbingEventKind::Hanging: return "hanging";
151 case ClimbingEventKind::Dropped: return "dropped";
152 case ClimbingEventKind::Completed: return "completed";
153 case ClimbingEventKind::Cancelled: return "cancelled";
154 case ClimbingEventKind::Failed: return "failed";
155 }
156 return "failed";
157}
158
159bool readEventKind(std::string_view value, ClimbingEventKind& output) {
160 if (value == "started")
162 else if (value == "anchor_transition_started")
164 else if (value == "anchor_reached")
166 else if (value == "contact_left_hand")
168 else if (value == "contact_right_hand")
170 else if (value == "landed")
172 else if (value == "hanging")
174 else if (value == "dropped")
176 else if (value == "completed")
178 else if (value == "cancelled")
180 else if (value == "failed")
182 else
183 return false;
184 return true;
185}
186
187eve::Value eventValue(const ClimbingEvent& event) {
188 eve::Value::Array metadata;
189 metadata.reserve(event.metadata.size());
190 for (const std::string& value : event.metadata) metadata.emplace_back(value);
191 return eve::Value::object({
192 {"kind", std::string(eventKindName(event.kind))},
193 {"actionId", event.actionId},
194 {"tick", std::to_string(event.tick.value())},
195 {"executionId", std::to_string(event.executionId.value())},
196 {"metadata", eve::Value(std::move(metadata))},
197 });
198}
199
200eve::Value eventArrayValue(const std::vector<ClimbingEvent>& events) {
202 values.reserve(events.size());
203 for (const auto& event : events) values.push_back(eventValue(event));
204 return eve::Value(std::move(values));
205}
206
207eve::Result<std::vector<ClimbingEvent>> readEvents(const eve::Value& value, std::uint64_t nextExecutionId) {
208 const auto* array = value.getIf<eve::Value::Array>();
209 if (!array || array->size() > ClimbingRuntime::PendingEventCapacity)
210 return eve::Result<std::vector<ClimbingEvent>>::failure(
211 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "pending climbing events must be a bounded array",
212 "pendingEvents", {}, "climbing.snapshot"));
213 std::vector<ClimbingEvent> events;
214 events.reserve(array->size());
216 bool first = true;
217 for (std::size_t index = 0; index < array->size(); ++index) {
218 const auto* object = (*array)[index].getIf<eve::Value::Object>();
219 ClimbingEvent event;
220 std::string kind;
221 std::uint64_t tick = 0;
222 std::uint64_t executionId = 0;
223 if (!object || !readString(*object, "kind", kind) || !readEventKind(kind, event.kind) ||
224 !readString(*object, "actionId", event.actionId) || event.actionId.empty() ||
225 !readUint64String(*object, "tick", tick) || !readUint64String(*object, "executionId", executionId) ||
226 executionId == 0 || executionId >= nextExecutionId)
227 return eve::Result<std::vector<ClimbingEvent>>::failure(eve::Diagnostic::error(
228 eve::DiagnosticCode::ParseError, "pending climbing event is missing or inconsistent",
229 "pendingEvents." + std::to_string(index), {}, "climbing.snapshot"));
230 if (const eve::Value* metadataValue = field(*object, "metadata")) {
231 const auto* metadata = metadataValue->getIf<eve::Value::Array>();
232 if (!metadata)
234 eve::DiagnosticCode::ParseError, "climbing event metadata must be an array",
235 "pendingEvents." + std::to_string(index) + ".metadata", {}, "climbing.snapshot"));
236 event.metadata.reserve(metadata->size());
237 for (const eve::Value& item : *metadata) {
238 const auto* text = item.getIf<std::string>();
239 if (!text || text->empty())
240 return eve::Result<std::vector<ClimbingEvent>>::failure(eve::Diagnostic::error(
241 eve::DiagnosticCode::ParseError, "climbing event metadata must contain non-empty strings",
242 "pendingEvents." + std::to_string(index) + ".metadata", {}, "climbing.snapshot"));
243 event.metadata.push_back(*text);
244 }
245 }
246 event.tick = eve::SimulationTick(tick);
247 event.executionId = ClimbingExecutionId(executionId);
248 if (!first && event.tick < previousTick)
250 eve::DiagnosticCode::InvariantViolation, "pending climbing events must be ordered by tick",
251 "pendingEvents." + std::to_string(index) + ".tick", {}, "climbing.snapshot"));
252 first = false;
253 previousTick = event.tick;
254 events.push_back(std::move(event));
255 }
256 return eve::Result<std::vector<ClimbingEvent>>::success(std::move(events));
257}
258
259eve::Value candidateValue(const ClimbingCandidate& value) {
260 return eve::Value::object({
261 {"actionId", value.actionId},
262 {"definitionGeneration", std::to_string(value.definitionGeneration)},
263 {"world", std::to_string(value.world.packed())},
264 {"obstacleBody", std::to_string(value.obstacleBody.packed())},
265 {"obstacleShape", std::to_string(value.obstacleShape.packed())},
266 {"obstacleBodyId", value.obstacleBodyId},
267 {"obstacleShapeId", value.obstacleShapeId},
268 {"ignoredBodyId", value.ignoredBodyId},
269 {"frontPoint", vecValue(value.frontPoint)},
270 {"topPoint", vecValue(value.topPoint)},
271 {"landingFeet", vecValue(value.landingFeet)},
272 {"surfaceNormal", vecValue(value.surfaceNormal)},
273 {"surfaceTangent", vecValue(value.surfaceTangent)},
274 {"leftHandAnchor", vecValue(value.leftHandAnchor)},
275 {"rightHandAnchor", vecValue(value.rightHandAnchor)},
276 {"bodyLocalTop", vecValue(value.bodyLocalTop)},
277 {"bodyLocalLanding", vecValue(value.bodyLocalLanding)},
278 {"obstacleHeight", value.obstacleHeight},
279 {"obstacleDepth", value.obstacleDepth},
280 {"gapDistance", value.gapDistance},
281 {"clearanceHeight", value.clearanceHeight},
282 {"slopeRadians", value.slopeRadians},
283 {"curvature", value.curvature},
284 {"supportShapeTag", value.supportShapeTag},
285 {"supportMaterialId", value.supportMaterialId},
286 {"probeRecipe", static_cast<std::int64_t>(value.probeRecipe)},
287 {"score", value.score},
288 {"kind", static_cast<std::int64_t>(value.kind)},
289 {"support", static_cast<std::int64_t>(value.support)},
290 });
291}
292
293eve::Value anchorValue(ClimbingAnchorGraphHandleRef graph, const ClimbingAnchorNodeRef& node,
294 const ClimbingAnchorReservation& reservation) {
295 if (!graph.isValid() || reservation.id.isZero()) return eve::Value();
296 return eve::Value::object({
297 {"graphHandle", std::to_string(graph.packed())},
298 {"graphOwnerEpoch", std::to_string(graph.ownerEpoch)},
299 {"graphId", node.graphId},
300 {"nodeId", node.nodeId},
301 {"nodeGraphGeneration", std::to_string(node.graphGeneration)},
302 {"reservationId", std::to_string(reservation.id.value())},
303 {"reservationGraphGeneration", std::to_string(reservation.graphGeneration)},
304 {"claimGeneration", std::to_string(reservation.claimGeneration)},
305 {"slot", static_cast<std::int64_t>(reservation.slot)},
306 {"agentId", std::to_string(reservation.occupant.agentId.value())},
307 {"executionId", std::to_string(reservation.occupant.executionId.value())},
308 });
309}
310
311eve::Result<ClimbingCandidate> readCandidate(const eve::Value& value, std::int64_t snapshotVersion) {
312 const auto* object = value.getIf<eve::Value::Object>();
313 if (!object)
315 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "runtime candidate must be an object",
316 "execution.candidate", {}, "climbing.snapshot"));
317 ClimbingCandidate result;
318 std::uint64_t world = 0, body = 0, shape = 0, definitionGeneration = 1;
319 std::int64_t bodyId = 0, shapeId = 0, ignoredBodyId = 0, score = 0, kind = 0, support = 0;
320 std::int64_t supportShapeTag = 0, supportMaterialId = 0, probeRecipe = 0;
321 const eve::Value* heightValue = field(*object, "obstacleHeight");
322 const eve::Value* depthValue = field(*object, "obstacleDepth");
323 const eve::Value* gapValue = field(*object, "gapDistance");
324 const eve::Value* clearanceValue = field(*object, "clearanceHeight");
325 const eve::Value* slopeValue = field(*object, "slopeRadians");
326 const eve::Value* curvatureValue = field(*object, "curvature");
327 if (!readString(*object, "actionId", result.actionId) ||
328 (field(*object, "definitionGeneration") &&
329 !readUint64String(*object, "definitionGeneration", definitionGeneration)) ||
330 definitionGeneration == 0 || !readUint64String(*object, "world", world) ||
331 !readUint64String(*object, "obstacleBody", body) || !readUint64String(*object, "obstacleShape", shape) ||
332 !readInt64(*object, "obstacleBodyId", bodyId) || bodyId < std::numeric_limits<int>::min() ||
333 bodyId > std::numeric_limits<int>::max() || !readInt64(*object, "obstacleShapeId", shapeId) ||
334 shapeId < std::numeric_limits<int>::min() || shapeId > std::numeric_limits<int>::max() ||
335 !readInt64(*object, "ignoredBodyId", ignoredBodyId) || ignoredBodyId < std::numeric_limits<int>::min() ||
336 ignoredBodyId > std::numeric_limits<int>::max() || !readVec(*object, "frontPoint", result.frontPoint) ||
337 !readVec(*object, "topPoint", result.topPoint) || !readVec(*object, "landingFeet", result.landingFeet) ||
338 !readVec(*object, "surfaceNormal", result.surfaceNormal) ||
339 !readVec(*object, "surfaceTangent", result.surfaceTangent) ||
340 !readVec(*object, "leftHandAnchor", result.leftHandAnchor) ||
341 !readVec(*object, "rightHandAnchor", result.rightHandAnchor) ||
342 !readVec(*object, "bodyLocalTop", result.bodyLocalTop) ||
343 !readVec(*object, "bodyLocalLanding", result.bodyLocalLanding) || !heightValue ||
344 !numericFloat(*heightValue, result.obstacleHeight) || !readInt64(*object, "score", score) ||
345 !readInt64(*object, "kind", kind) || kind < 0 ||
346 kind > static_cast<std::int64_t>(ClimbingActionKind::BarSwing) ||
347 !readInt64(*object, "support", support) ||
348 support < 0 || support > static_cast<std::int64_t>(HangSupport::Free))
350 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "runtime candidate has missing or invalid fields",
351 "execution.candidate", {}, "climbing.snapshot"));
352 if ((snapshotVersion >= 4 && (!depthValue || !gapValue || !clearanceValue || !slopeValue || !curvatureValue ||
353 !field(*object, "supportShapeTag") || !field(*object, "supportMaterialId") ||
354 !field(*object, "probeRecipe"))) ||
355 (depthValue && !numericFloat(*depthValue, result.obstacleDepth)) ||
356 (gapValue && !numericFloat(*gapValue, result.gapDistance)) ||
357 (clearanceValue && !numericFloat(*clearanceValue, result.clearanceHeight)) ||
358 (slopeValue && !numericFloat(*slopeValue, result.slopeRadians)) ||
359 (curvatureValue && !numericFloat(*curvatureValue, result.curvature)) ||
360 (field(*object, "supportShapeTag") && !readInt64(*object, "supportShapeTag", supportShapeTag)) ||
361 supportShapeTag < std::numeric_limits<int>::min() || supportShapeTag > std::numeric_limits<int>::max() ||
362 (field(*object, "supportMaterialId") && !readInt64(*object, "supportMaterialId", supportMaterialId)) ||
363 supportMaterialId < std::numeric_limits<int>::min() ||
364 supportMaterialId > std::numeric_limits<int>::max() ||
365 (field(*object, "probeRecipe") && !readInt64(*object, "probeRecipe", probeRecipe)) || probeRecipe < 0 ||
366 probeRecipe > static_cast<std::int64_t>(ClimbingProbeRecipe::AnchorGraph))
368 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "runtime candidate metrics are missing or invalid",
369 "execution.candidate", {}, "climbing.snapshot"));
370 result.definitionGeneration = definitionGeneration;
372 result.obstacleBody = physics::PhysicsBodyHandle::fromPacked(body);
373 result.obstacleShape = physics::PhysicsShapeHandle::fromPacked(shape);
374 result.obstacleBodyId = static_cast<int>(bodyId);
375 result.obstacleShapeId = static_cast<int>(shapeId);
376 result.ignoredBodyId = static_cast<int>(ignoredBodyId);
377 result.score = score;
378 result.kind = static_cast<ClimbingActionKind>(kind);
379 result.supportShapeTag = static_cast<int>(supportShapeTag);
380 result.supportMaterialId = static_cast<int>(supportMaterialId);
381 result.probeRecipe = static_cast<ClimbingProbeRecipe>(probeRecipe);
382 result.support = static_cast<HangSupport>(support);
383 return eve::Result<ClimbingCandidate>::success(std::move(result));
384}
385
386bool activePhase(ClimbingPhase phase) {
389}
390
391} // namespace
392
394 auto profile = encodeClimbingProfileDefinition(profile_);
395 if (!profile) return eve::Result<eve::Value>::failure(profile.status());
396 eve::Value::Object root{{"schemaId", eve::Value(std::string(SnapshotSchemaId))},
397 {"schemaVersion", eve::Value(SnapshotSchemaVersion)},
398 {"phase", eve::Value(std::string(phaseName(phase_)))},
399 {"terminalCode", eve::Value(terminalCode_)},
400 {"definitionGeneration", eve::Value(std::to_string(definitionGeneration_))},
401 {"profile", std::move(profile).takeValue()},
402 {"previousActionId", eve::Value(previousActionId_)},
403 {"nextExecutionId", eve::Value(std::to_string(nextExecutionId_))},
404 {"pendingEvents", eventArrayValue(pendingEvents_)}};
405 if (!execution_) {
406 root.emplace("execution", eve::Value());
408 }
409 auto action = encodeClimbingActionDefinition(execution_->action);
410 if (!action) return eve::Result<eve::Value>::failure(action.status());
412 {"executionId", std::to_string(execution_->executionId.value())},
413 {"definitionGeneration", std::to_string(execution_->definitionGeneration)},
414 {"action", std::move(action).takeValue()},
415 {"candidate", candidateValue(execution_->candidate)},
416 {"startFeet", vecValue(execution_->startFeet)},
417 {"currentFeet", vecValue(execution_->currentFeet)},
418 {"lastPlannedFeet", vecValue(execution_->lastPlannedFeet)},
419 {"elapsedNs", execution_->elapsed.nanoseconds()},
420 {"durationNs", execution_->duration.nanoseconds()},
421 {"lastTick", std::to_string(execution_->lastTick.value())},
422 {"velocity", vecValue(execution_->velocity)},
423 {"accumulatedResidual", vecValue(execution_->accumulatedResidual)},
424 {"horizontalWarpUsed", execution_->horizontalWarpUsed},
425 {"verticalWarpUsed", execution_->verticalWarpUsed},
426 {"facingWarpUsed", execution_->facingWarpUsed},
427 {"leftContactEmitted", execution_->leftContactEmitted},
428 {"rightContactEmitted", execution_->rightContactEmitted},
429 {"landContactReleased", execution_->landContactReleased},
430 {"compactCollisionActive", execution_->compactCollisionActive},
431 {"branchWindowOpen", execution_->branchWindowOpen},
432 {"anchor", anchorValue(execution_->anchorGraph, execution_->anchorNode,
433 execution_->anchorReservation)},
434 };
435 root.emplace("execution", eve::Value(std::move(state)));
437}
438
440 const auto* root = value.getIf<eve::Value::Object>();
441 if (!root)
443 eve::DiagnosticCode::ParseError, "runtime snapshot must be an object", {}, {}, "climbing.snapshot"));
444 std::string schemaId;
445 std::int64_t version = -1;
446 std::string phaseText;
447 std::string terminalCode;
448 std::string candidatePreviousActionId;
449 std::uint64_t candidateNextExecutionId = 1;
450 std::uint64_t candidateDefinitionGeneration = definitionGeneration_;
451 if (!readString(*root, "schemaId", schemaId) || schemaId != SnapshotSchemaId ||
452 !readInt64(*root, "schemaVersion", version) || version < 0 || version > SnapshotSchemaVersion ||
453 !readString(*root, "phase", phaseText) ||
454 (version >= 1 && !readString(*root, "terminalCode", terminalCode))) {
455 if (version > SnapshotSchemaVersion)
457 "climbing.restore.version_unsupported",
458 "schemaVersion", {}, "climbing.snapshot"));
460 "invalid climbing runtime snapshot envelope", {}, {},
461 "climbing.snapshot"));
462 }
463 if (version >= 4 && !readString(*root, "previousActionId", candidatePreviousActionId))
465 "invalid previous climbing action id",
466 "previousActionId", {}, "climbing.snapshot"));
467 if (field(*root, "nextExecutionId") && !readUint64String(*root, "nextExecutionId", candidateNextExecutionId))
469 "invalid next climbing execution id",
470 "nextExecutionId", {}, "climbing.snapshot"));
471 if (field(*root, "definitionGeneration") &&
472 (!readUint64String(*root, "definitionGeneration", candidateDefinitionGeneration) ||
473 candidateDefinitionGeneration == 0))
475 "invalid climbing definition generation",
476 "definitionGeneration", {}, "climbing.snapshot"));
477 ClimbingProfile candidateProfile = profile_;
478 if (const eve::Value* profileValue = field(*root, "profile")) {
479 auto decodedProfile = decodeClimbingProfileDefinition(*profileValue);
480 if (!decodedProfile) return eve::Result<void>::failure(decodedProfile.status());
481 candidateProfile = std::move(decodedProfile).takeValue();
482 }
483 ClimbingPhase candidatePhase;
484 if (!readPhase(phaseText, candidatePhase))
486 eve::DiagnosticCode::ParseError, "invalid climbing runtime phase", "phase", {}, "climbing.snapshot"));
487 const eve::Value* executionValue = field(*root, "execution");
488 if (!executionValue)
490 "runtime snapshot execution field is required",
491 "execution", {}, "climbing.snapshot"));
492 std::optional<Execution> candidateExecution;
493 if (!executionValue->isNull()) {
494 const auto* state = executionValue->getIf<eve::Value::Object>();
495 const eve::Value* actionValue = state ? field(*state, "action") : nullptr;
496 const eve::Value* candidateValueField = state ? field(*state, "candidate") : nullptr;
497 if (!state || !actionValue || !candidateValueField)
499 "invalid climbing execution object", "execution",
500 {}, "climbing.snapshot"));
501 auto action = decodeClimbingActionDefinition(*actionValue);
502 if (!action) return eve::Result<void>::failure(action.status());
503 auto candidate = readCandidate(*candidateValueField, version);
504 if (!candidate) return eve::Result<void>::failure(candidate.status());
505 Execution parsed;
506 std::uint64_t executionId = 1;
507 std::uint64_t executionDefinitionGeneration = candidateDefinitionGeneration;
508 if (field(*state, "executionId") && !readUint64String(*state, "executionId", executionId))
510 "invalid climbing execution id",
511 "execution.executionId", {}, "climbing.snapshot"));
512 parsed.executionId = ClimbingExecutionId(executionId);
513 if (field(*state, "definitionGeneration") &&
514 (!readUint64String(*state, "definitionGeneration", executionDefinitionGeneration) ||
515 executionDefinitionGeneration == 0))
517 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid execution definition generation",
518 "execution.definitionGeneration", {}, "climbing.snapshot"));
519 parsed.definitionGeneration = executionDefinitionGeneration;
520 parsed.action = std::move(action).takeValue();
521 parsed.candidate = std::move(candidate).takeValue();
522 std::int64_t elapsedNs = 0, durationNs = 0;
523 std::uint64_t lastTick = 0;
524 if (parsed.action.id != parsed.candidate.actionId ||
525 (field(*state, "definitionGeneration") &&
526 parsed.candidate.definitionGeneration != parsed.definitionGeneration) ||
527 !readVec(*state, "startFeet", parsed.startFeet) ||
528 !readVec(*state, "currentFeet", parsed.currentFeet) ||
529 !readVec(*state, "lastPlannedFeet", parsed.lastPlannedFeet) || !readInt64(*state, "elapsedNs", elapsedNs) ||
530 elapsedNs < 0 || !readInt64(*state, "durationNs", durationNs) || durationNs <= 0 ||
531 !readUint64String(*state, "lastTick", lastTick) || !readVec(*state, "velocity", parsed.velocity) ||
532 !readVec(*state, "accumulatedResidual", parsed.accumulatedResidual) ||
533 !readOptionalFloat(*state, "horizontalWarpUsed", parsed.horizontalWarpUsed) ||
534 !readOptionalFloat(*state, "verticalWarpUsed", parsed.verticalWarpUsed) ||
535 !readOptionalFloat(*state, "facingWarpUsed", parsed.facingWarpUsed) || parsed.horizontalWarpUsed < 0.f ||
536 parsed.verticalWarpUsed < 0.f || parsed.facingWarpUsed < 0.f ||
537 !readBool(*state, "leftContactEmitted", parsed.leftContactEmitted, false, version != 0) ||
538 !readBool(*state, "rightContactEmitted", parsed.rightContactEmitted, false, version != 0) ||
539 !readBool(*state, "landContactReleased", parsed.landContactReleased, false, false) ||
540 !readBool(*state, "compactCollisionActive", parsed.compactCollisionActive, false, false) ||
541 !readBool(*state, "branchWindowOpen", parsed.branchWindowOpen, false, false))
543 eve::DiagnosticCode::ParseError, "climbing execution has missing or inconsistent fields", "execution",
544 {}, "climbing.snapshot"));
545 const eve::Value* anchorField = field(*state, "anchor");
546 if (version >= 3 && !anchorField)
548 "runtime snapshot anchor field is required",
549 "execution.anchor", {}, "climbing.snapshot"));
550 if (anchorField && !anchorField->isNull()) {
551 if (version < 3)
553 eve::DiagnosticCode::ParseError, "legacy runtime snapshot cannot contain an anchor claim",
554 "execution.anchor", {}, "climbing.snapshot"));
555 const auto* anchor = anchorField->getIf<eve::Value::Object>();
556 std::uint64_t graphHandle = 0, graphOwnerEpoch = 0, nodeGraphGeneration = 0;
557 std::uint64_t reservationId = 0, reservationGraphGeneration = 0, claimGeneration = 0;
558 std::uint64_t agentId = 0, occupantExecutionId = 0;
559 std::int64_t slot = -1;
560 std::string graphId, nodeId;
561 if (!anchor || !readUint64String(*anchor, "graphHandle", graphHandle) ||
562 !readUint64String(*anchor, "graphOwnerEpoch", graphOwnerEpoch) || graphOwnerEpoch == 0 ||
563 !readString(*anchor, "graphId", graphId) || graphId.empty() ||
564 !readString(*anchor, "nodeId", nodeId) || nodeId.empty() ||
565 !readUint64String(*anchor, "nodeGraphGeneration", nodeGraphGeneration) ||
566 nodeGraphGeneration == 0 || !readUint64String(*anchor, "reservationId", reservationId) ||
567 reservationId == 0 ||
568 !readUint64String(*anchor, "reservationGraphGeneration", reservationGraphGeneration) ||
569 reservationGraphGeneration == 0 ||
570 !readUint64String(*anchor, "claimGeneration", claimGeneration) || claimGeneration == 0 ||
571 !readInt64(*anchor, "slot", slot) || slot < 0 ||
572 slot > static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max()) ||
573 !readUint64String(*anchor, "agentId", agentId) || agentId == 0 ||
574 !readUint64String(*anchor, "executionId", occupantExecutionId) || occupantExecutionId == 0)
576 eve::DiagnosticCode::ParseError, "runtime anchor claim has missing or invalid fields",
577 "execution.anchor", {}, "climbing.snapshot"));
578 parsed.anchorGraph.handle = eve::RuntimeHandle<ClimbingAnchorGraphHandleTag>::fromPacked(graphHandle);
579 parsed.anchorGraph.ownerEpoch = graphOwnerEpoch;
580 parsed.anchorNode = {std::move(graphId), std::move(nodeId), nodeGraphGeneration};
581 parsed.anchorReservation = {ClimbingAnchorReservationId(reservationId), reservationGraphGeneration,
582 claimGeneration, parsed.anchorNode.nodeId, static_cast<std::uint32_t>(slot),
583 {ClimbingAnchorAgentId(agentId), ClimbingExecutionId(occupantExecutionId)}};
584 if (!parsed.anchorGraph.isValid() || nodeGraphGeneration != reservationGraphGeneration ||
585 parsed.executionId != parsed.anchorReservation.occupant.executionId)
587 "runtime anchor identities are inconsistent",
588 "execution.anchor", {}, "climbing.snapshot"));
589 }
590 parsed.elapsed = eve::Duration::fromNanoseconds(elapsedNs);
591 parsed.duration = eve::Duration::fromNanoseconds(durationNs);
592 parsed.lastTick = eve::SimulationTick(lastTick);
593 if (parsed.duration != parsed.action.duration || parsed.elapsed > parsed.duration)
595 "execution timing disagrees with pinned action",
596 "execution.durationNs", {}, "climbing.snapshot"));
597 const bool graphBound = parsed.anchorGraph.isValid();
598 if (world.runtimeHandle() != parsed.candidate.world || !world.findBody(parsed.candidate.obstacleBody) ||
599 (!graphBound && !world.findShape(parsed.candidate.obstacleShape)))
601 "climbing restore target link is stale",
602 "execution.candidate", {}, "climbing.snapshot"));
603 if (graphBound) {
604 auto graph = Climbing::resolveAnchorGraph(parsed.anchorGraph);
605 if (!graph.isBound() || graph->body() != parsed.candidate.obstacleBody)
607 eve::DiagnosticCode::StaleHandle, "climbing restore anchor graph link is stale",
608 "execution.anchor.graph", {}, "climbing.snapshot"));
609 auto resolved = graph->resolveNode(world, parsed.anchorNode);
610 if (!resolved)
611 return eve::Result<void>::failure(resolved.status());
612 auto reservation = graph->validateReservation(parsed.anchorReservation);
613 if (!reservation && (!reservation.error() || reservation.error()->code() != eve::DiagnosticCode::NotFound))
614 return eve::Result<void>::failure(reservation.status());
615 }
616 candidateExecution = std::move(parsed);
617 }
618 if (activePhase(candidatePhase) != candidateExecution.has_value())
620 "active phase and execution presence disagree",
621 "execution", {}, "climbing.snapshot"));
622 if (!field(*root, "nextExecutionId")) {
623 candidateNextExecutionId = candidateExecution ? candidateExecution->executionId.value() + 1 : 1;
624 }
625 if (candidateNextExecutionId == 0 ||
626 (candidateExecution && (candidateExecution->executionId.isZero() ||
627 candidateNextExecutionId <= candidateExecution->executionId.value())))
629 "next execution id must follow the restored execution",
630 "nextExecutionId", {}, "climbing.snapshot"));
631 std::vector<ClimbingEvent> candidateEvents;
632 if (const eve::Value* eventValue = field(*root, "pendingEvents")) {
633 auto parsedEvents = readEvents(*eventValue, candidateNextExecutionId);
634 if (!parsedEvents) return eve::Result<void>::failure(parsedEvents.status());
635 candidateEvents = std::move(parsedEvents).takeValue();
636 } else if (version >= 2) {
638 "runtime snapshot pendingEvents field is required",
639 "pendingEvents", {}, "climbing.snapshot"));
640 }
641
642 if (execution_ && execution_->anchorGraph.isValid() &&
643 (!candidateExecution || execution_->anchorGraph != candidateExecution->anchorGraph ||
644 execution_->anchorReservation.id != candidateExecution->anchorReservation.id ||
645 execution_->anchorReservation.claimGeneration != candidateExecution->anchorReservation.claimGeneration))
647 eve::DiagnosticCode::Conflict, "restore cannot overwrite a different live anchor claim", "execution.anchor",
648 {}, "climbing.snapshot"));
649 if (candidateExecution && candidateExecution->anchorGraph.isValid()) {
650 const bool alreadyOwned = execution_ && execution_->anchorGraph == candidateExecution->anchorGraph &&
651 execution_->anchorReservation.id == candidateExecution->anchorReservation.id &&
652 execution_->anchorReservation.claimGeneration ==
653 candidateExecution->anchorReservation.claimGeneration;
654 if (!alreadyOwned) {
655 auto graph = Climbing::resolveAnchorGraph(candidateExecution->anchorGraph);
656 if (!graph.isBound())
658 eve::DiagnosticCode::StaleHandle, "climbing restore anchor graph link became stale",
659 "execution.anchor.graph", {}, "climbing.snapshot"));
660 auto claimed = graph->restoreReservation(candidateExecution->anchorReservation);
661 if (!claimed) return eve::Result<void>::failure(claimed.status());
662 candidateExecution->anchorReservation = std::move(claimed).takeValue();
663 }
664 }
665
666 // Publish only after the whole owning candidate and all cross-domain links have passed validation.
667 phase_ = candidatePhase;
668 profile_ = std::move(candidateProfile);
669 terminalCode_ = std::move(terminalCode);
670 execution_ = std::move(candidateExecution);
671 previousActionId_ = std::move(candidatePreviousActionId);
672 nextExecutionId_ = candidateNextExecutionId;
673 definitionGeneration_ = candidateDefinitionGeneration;
674 pendingEvents_ = std::move(candidateEvents);
675 validatedAnimationActions_.clear();
676 lastCandidates_.clear();
677 lastDebugQueries_.clear();
678 lastEvidence_.clear();
679 motionEvidence_.clear();
680 lastQueryCount_ = 0;
682}
683
684} // namespace eve::climbing
double value
double score
Definition Agent.cpp:50
int root
Definition AnimSmr.cpp:119
std::string output
eve::action::ActionExecutionId executionId
float phase
Definition CaveMesh.cpp:58
Vec3 anchor
Definition CaveMesh.cpp:90
Versioned, lossless codecs for climbing definitions.
Deterministic climbing/parkour planning and capsule-constrained execution.
std::string nodeId
std::map< std::string, Var > values
ShaderImageInput shape
std::int32_t first
std::string text
TokenKind kind
bool required
std::string name
std::string error
Definition Package.cpp:60
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
World3D * world
std::string action
Definition PlayHost.cpp:117
const RoadNode * node
double number
bool found
bool boolean
Battle::Events events
SimulationTick tick
std::string body
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
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
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
static constexpr RuntimeHandle fromPacked(std::uint64_t value) noexcept
Reconstructs a handle from an explicit packed boundary value.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
static Value array(Array value)
Compatibility factory for an array value.
Definition Value.h:112
std::vector< Value > Array
Definition Value.h:33
bool isNull() const noexcept
Return true when this value is null.
Definition Value.h:87
static Value object(Object value)
Compatibility factory for an object value.
Definition Value.h:114
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
static constexpr std::int64_t SnapshotSchemaVersion
Current runtime snapshot version; v3 is N-1 and historical v2/v1/v0 remain migratable.
Definition Climbing.h:658
eve::Result< eve::Value > snapshot() const
Captures deterministic runtime state; debug candidates and final bone poses are excluded.
eve::Result< void > restore(const eve::Value &value, physics::World3D &world)
Restores a fully parsed candidate atomically after revalidating all process-local Physics links.
ClimbingExecutionId executionId() const noexcept
Current execution identity, or zero when no execution is active or retained.
Definition Climbing.h:805
static constexpr std::string_view SnapshotSchemaId
Stable schema id for owning runtime snapshots.
Definition Climbing.h:656
static constexpr std::size_t PendingEventCapacity
Hard bound for undelivered post-simulation events owned by one runtime.
Definition Climbing.h:660
static eve::script::Borrowed< ClimbingAnchorGraphInstance > resolveAnchorGraph(ClimbingAnchorGraphHandleRef reference) noexcept
Resolves a live graph instance as a synchronous non-owning observation.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
static constexpr StrongUint64 zero() noexcept
Returns the zero value for this strong type.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
const char * defaultValue
std::string eventKindName(EventKind kind)
Returns the stable lowercase name of an event kind.
eve::detail::StrongUint64< detail::ClimbingExecutionIdTag > ClimbingExecutionId
Stable non-zero identity assigned when a climbing selection transaction commits.
eve::Result< ClimbingActionDefinition > decodeClimbingActionDefinition(const eve::Value &value)
Decode and validate one action without partially mutating a registry.
eve::Result< eve::Value > encodeClimbingProfileDefinition(const ClimbingProfileDefinition &profile)
Encode one validated profile and all nested actions into canonical schema v4 data.
eve::Result< ClimbingProfileDefinition > decodeClimbingProfileDefinition(const eve::Value &value)
Decode and validate an owning profile candidate transactionally.
ClimbingPhase
Execution lifecycle visible to gameplay and animation adapters.
Definition Climbing.h:470
eve::detail::StrongUint64< detail::ClimbingAnchorReservationIdTag > ClimbingAnchorReservationId
Stable non-zero identity of one live anchor-slot reservation.
eve::Result< eve::Value > encodeClimbingActionDefinition(const ClimbingActionDefinition &action)
Encode one validated action into canonical schema v4 data.
HangSupport
Evidence describing whether a hanging pose has foot support.
Definition Climbing.h:137
eve::script::RuntimeHandleRef< ClimbingAnchorGraphHandleTag > ClimbingAnchorGraphHandleRef
Generation- and module-epoch-qualified anchor graph instance reference.
ClimbingEventKind
Stable semantic event emitted by the authoritative climbing lifecycle.
Definition Climbing.h:488
ClimbingActionKind
Data-driven action family; stable action identity remains the definition id.
Definition Climbing.h:46
eve::detail::StrongUint64< detail::ClimbingAnchorAgentIdTag > ClimbingAnchorAgentId
Stable non-zero identity of the character/agent that owns graph occupancy.
ClimbingProbeRecipe
Semantic query family used to produce action candidates.
Definition Climbing.h:82
bool readString(const eve::Value::Object &object, const char *name, std::string &output)
const EditorValue * field(const EditorValue &value, const char *name)
bool readUint64String(const eve::Value::Object &object, const char *name, std::uint64_t &output)
std::string_view phaseName(BattlePhase phase) noexcept
Return the stable protocol spelling of a battle phase.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
Runtime profile controlling capsule geometry and probe limits.
Definition Climbing.h:218