载入中...
搜索中...
未找到
ClimbingAnchorGraph.cpp
浏览该文件的文档.
2
3#include "physics/Body3D.h"
4#include "physics/World3D.h"
5
6#include <algorithm>
7#include <cmath>
8#include <deque>
9#include <limits>
10#include <unordered_set>
11#include <utility>
12
13namespace eve::climbing {
14namespace {
15
16constexpr std::size_t maxGraphNodes = 4096;
17constexpr std::size_t maxGraphEdges = 16384;
18constexpr std::uint32_t maxOccupancySlots = 8;
19constexpr float frameTolerance = 0.02f;
20
21bool finite(float value) { return std::isfinite(value); }
22bool finite(Vec3 value) { return finite(value.x) && finite(value.y) && finite(value.z); }
23float lengthSquared(Vec3 value) { return value.x * value.x + value.y * value.y + value.z * value.z; }
24float dot(Vec3 lhs, Vec3 rhs) { return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; }
25
26const eve::Value* field(const eve::Value::Object& object, std::string_view name) {
27 const auto found = object.find(std::string(name));
28 return found == object.end() ? nullptr : &found->second;
29}
30
31bool readString(const eve::Value::Object& object, std::string_view name, std::string& output) {
32 const eve::Value* value = field(object, name);
33 const auto* text = value ? value->getIf<std::string>() : nullptr;
34 if (!text) return false;
35 output = *text;
36 return true;
37}
38
39bool readInt64(const eve::Value::Object& object, std::string_view name, std::int64_t& output) {
40 const eve::Value* value = field(object, name);
41 const auto* number = value ? value->getIf<std::int64_t>() : nullptr;
42 if (!number) return false;
43 output = *number;
44 return true;
45}
46
47bool readBool(const eve::Value::Object& object, std::string_view name, bool& output) {
48 const eve::Value* value = field(object, name);
49 const auto* boolean = value ? value->getIf<bool>() : nullptr;
50 if (!boolean) return false;
51 output = *boolean;
52 return true;
53}
54
55bool numericFloat(const eve::Value& value, float& output) {
56 double number = 0.0;
57 if (const auto* integer = value.getIf<std::int64_t>())
58 number = static_cast<double>(*integer);
59 else if (const auto* real = value.getIf<double>())
60 number = *real;
61 else
62 return false;
63 if (!std::isfinite(number) || number < -static_cast<double>(std::numeric_limits<float>::max()) ||
64 number > static_cast<double>(std::numeric_limits<float>::max()))
65 return false;
66 output = static_cast<float>(number);
67 return true;
68}
69
70bool readVec(const eve::Value::Object& object, std::string_view name, Vec3& output) {
71 const eve::Value* value = field(object, name);
72 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
73 return array && array->size() == 3 && numericFloat((*array)[0], output.x) &&
74 numericFloat((*array)[1], output.y) && numericFloat((*array)[2], output.z);
75}
76
77eve::Value vecValue(Vec3 value) {
79}
80
81std::string_view nodeKindName(ClimbingAnchorKind kind) {
82 switch (kind) {
83 case ClimbingAnchorKind::Ledge: return "ledge";
84 case ClimbingAnchorKind::CornerInner: return "corner_inner";
85 case ClimbingAnchorKind::CornerOuter: return "corner_outer";
86 case ClimbingAnchorKind::LadderRung: return "ladder_rung";
87 case ClimbingAnchorKind::Pole: return "pole";
88 case ClimbingAnchorKind::Beam: return "beam";
89 case ClimbingAnchorKind::Bar: return "bar";
90 }
91 return "unknown";
92}
93
94bool readNodeKind(std::string_view value, ClimbingAnchorKind& output) {
95 if (value == "ledge")
97 else if (value == "corner_inner")
99 else if (value == "corner_outer")
101 else if (value == "ladder_rung")
103 else if (value == "pole")
105 else if (value == "beam")
107 else if (value == "bar")
109 else
110 return false;
111 return true;
112}
113
114std::string_view edgeKindName(ClimbingAnchorEdgeKind kind) {
115 switch (kind) {
116 case ClimbingAnchorEdgeKind::Shimmy: return "shimmy";
117 case ClimbingAnchorEdgeKind::Corner: return "corner";
118 case ClimbingAnchorEdgeKind::Jump: return "jump";
119 case ClimbingAnchorEdgeKind::Drop: return "drop";
120 case ClimbingAnchorEdgeKind::Mount: return "mount";
121 case ClimbingAnchorEdgeKind::Dismount: return "dismount";
122 case ClimbingAnchorEdgeKind::Climb: return "climb";
123 case ClimbingAnchorEdgeKind::Balance: return "balance";
124 case ClimbingAnchorEdgeKind::Swing: return "swing";
125 }
126 return "unknown";
127}
128
129bool readEdgeKind(std::string_view value, ClimbingAnchorEdgeKind& output) {
130 if (value == "shimmy")
132 else if (value == "corner")
134 else if (value == "jump")
136 else if (value == "drop")
138 else if (value == "mount")
140 else if (value == "dismount")
142 else if (value == "climb")
144 else if (value == "balance")
146 else if (value == "swing")
148 else
149 return false;
150 return true;
151}
152
153eve::Value::Array stringArray(const std::vector<std::string>& strings) {
155 values.reserve(strings.size());
156 for (const auto& value : strings) values.emplace_back(value);
157 return values;
158}
159
160bool readStringArray(const eve::Value::Object& object, std::string_view name, std::vector<std::string>& output) {
161 const eve::Value* value = field(object, name);
162 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
163 if (!array) return false;
164 output.reserve(array->size());
165 for (const auto& item : *array) {
166 const auto* text = item.getIf<std::string>();
167 if (!text) return false;
168 output.push_back(*text);
169 }
170 return true;
171}
172
173eve::Value::Object unknownFields(const eve::Value::Object& object,
174 std::initializer_list<std::string_view> knownNames) {
175 std::unordered_set<std::string_view> known(knownNames.begin(), knownNames.end());
176 eve::Value::Object result;
177 for (const auto& [name, value] : object)
178 if (!known.contains(name)) result.emplace(name, value);
179 return result;
180}
181
182bool validUniqueStrings(std::vector<std::string> values) {
183 std::sort(values.begin(), values.end());
184 return !std::any_of(values.begin(), values.end(), [](const auto& value) { return value.empty(); }) &&
185 std::adjacent_find(values.begin(), values.end()) == values.end();
186}
187
188void canonicalize(ClimbingAnchorGraphDefinition& graph) {
189 for (auto& node : graph.nodes) std::sort(node.tags.begin(), node.tags.end());
190 for (auto& edge : graph.edges) std::sort(edge.requiredTags.begin(), edge.requiredTags.end());
191 std::sort(graph.nodes.begin(), graph.nodes.end(), [](const auto& lhs, const auto& rhs) { return lhs.id < rhs.id; });
192 std::sort(graph.edges.begin(), graph.edges.end(), [](const auto& lhs, const auto& rhs) {
193 if (lhs.from != rhs.from) return lhs.from < rhs.from;
194 if (lhs.to != rhs.to) return lhs.to < rhs.to;
195 if (lhs.kind != rhs.kind) return lhs.kind < rhs.kind;
196 return lhs.bidirectional < rhs.bidirectional;
197 });
198}
199
200bool sameEdgeIdentity(const ClimbingAnchorEdgeDefinition& lhs, const ClimbingAnchorEdgeDefinition& rhs) {
201 return lhs.from == rhs.from && lhs.to == rhs.to && lhs.kind == rhs.kind;
202}
203
204Vec3 vectorFrom(physics::PhysicsVector3D value) { return {value.x, value.y, value.z}; }
205
206} // namespace
207
209 if (graph.id.empty() || graph.sourceGeometryContentId.empty() || graph.buildSettingsHash.empty())
212 "graph id, source geometry content id, and build settings hash are required",
213 "graph.identity", {}, "climbing.anchor_graph"));
214 if (graph.nodes.empty() || graph.nodes.size() > maxGraphNodes || graph.edges.size() > maxGraphEdges)
216 eve::DiagnosticCode::InvalidArgument, "anchor graph node and edge counts exceed supported bounds",
217 "graph.topology", {}, "climbing.anchor_graph"));
218 std::unordered_set<std::string> nodeIds;
219 for (std::size_t index = 0; index < graph.nodes.size(); ++index) {
220 const auto& node = graph.nodes[index];
221 const std::string path = "nodes." + std::to_string(index);
222 if (node.id.empty() || !nodeIds.emplace(node.id).second)
224 "anchor node ids must be non-empty and unique",
225 path + ".id", {}, "climbing.anchor_graph"));
226 if (!finite(node.localPosition) || !finite(node.localNormal) || !finite(node.localTangent) ||
227 !finite(node.leftHandSocket) || !finite(node.rightHandSocket) || !finite(node.feetSocket))
229 "anchor frame and sockets must be finite", path,
230 {}, "climbing.anchor_graph"));
231 const float normalLength = lengthSquared(node.localNormal);
232 const float tangentLength = lengthSquared(node.localTangent);
233 if (std::fabs(normalLength - 1.f) > frameTolerance || std::fabs(tangentLength - 1.f) > frameTolerance ||
234 std::fabs(dot(node.localNormal, node.localTangent)) > frameTolerance)
236 eve::DiagnosticCode::InvalidArgument, "anchor normal and tangent must form an orthonormal local frame",
237 path + ".frame", {}, "climbing.anchor_graph"));
238 if (node.occupancySlots == 0 || node.occupancySlots > maxOccupancySlots)
240 eve::DiagnosticCode::InvalidArgument, "anchor occupancy slots must be between one and eight",
241 path + ".occupancySlots", {}, "climbing.anchor_graph"));
242 if (!validUniqueStrings(node.tags))
244 "anchor tags must be non-empty and unique",
245 path + ".tags", {}, "climbing.anchor_graph"));
246 }
247 std::vector<ClimbingAnchorEdgeDefinition> edges = graph.edges;
248 std::sort(edges.begin(), edges.end(), [](const auto& lhs, const auto& rhs) {
249 if (lhs.from != rhs.from) return lhs.from < rhs.from;
250 if (lhs.to != rhs.to) return lhs.to < rhs.to;
251 return lhs.kind < rhs.kind;
252 });
253 for (std::size_t index = 0; index < edges.size(); ++index) {
254 const auto& edge = edges[index];
255 const std::string path = "edges." + std::to_string(index);
256 if (edge.from.empty() || edge.to.empty() || edge.from == edge.to || !nodeIds.contains(edge.from) ||
257 !nodeIds.contains(edge.to))
259 eve::DiagnosticCode::InvalidArgument, "anchor edge endpoints must name two distinct existing nodes",
260 path, {}, "climbing.anchor_graph"));
261 if (index != 0 && sameEdgeIdentity(edges[index - 1], edge))
263 "anchor edge identity must be unique", path, {},
264 "climbing.anchor_graph"));
265 if (!validUniqueStrings(edge.requiredTags))
267 eve::DiagnosticCode::InvalidArgument, "edge required tags must be non-empty and unique",
268 path + ".requiredTags", {}, "climbing.anchor_graph"));
269 }
271}
272
275 if (!valid) return eve::Result<eve::Value>::failure(valid.status());
277 canonicalize(canonical);
279 nodes.reserve(canonical.nodes.size());
280 for (const auto& node : canonical.nodes) {
281 eve::Value::Object object = node.extensionMetadata;
282 object["id"] = eve::Value(node.id);
283 object["kind"] = eve::Value(std::string(nodeKindName(node.kind)));
284 object["localPosition"] = vecValue(node.localPosition);
285 object["localNormal"] = vecValue(node.localNormal);
286 object["localTangent"] = vecValue(node.localTangent);
287 object["leftHandSocket"] = vecValue(node.leftHandSocket);
288 object["rightHandSocket"] = vecValue(node.rightHandSocket);
289 object["feetSocket"] = vecValue(node.feetSocket);
290 object["occupancySlots"] = eve::Value(static_cast<std::int64_t>(node.occupancySlots));
291 object["tags"] = eve::Value(stringArray(node.tags));
292 nodes.emplace_back(std::move(object));
293 }
295 edges.reserve(canonical.edges.size());
296 for (const auto& edge : canonical.edges) {
297 eve::Value::Object object = edge.extensionMetadata;
298 object["from"] = eve::Value(edge.from);
299 object["to"] = eve::Value(edge.to);
300 object["kind"] = eve::Value(std::string(edgeKindName(edge.kind)));
301 object["bidirectional"] = eve::Value(edge.bidirectional);
302 object["requiredTags"] = eve::Value(stringArray(edge.requiredTags));
303 edges.emplace_back(std::move(object));
304 }
306 root["schemaId"] = eve::Value(std::string(ClimbingAnchorGraphDefinition::SchemaId));
308 root["id"] = eve::Value(canonical.id);
309 root["sourceGeometryContentId"] = eve::Value(canonical.sourceGeometryContentId);
310 root["buildSettingsHash"] = eve::Value(canonical.buildSettingsHash);
311 root["nodes"] = eve::Value(std::move(nodes));
312 root["edges"] = eve::Value(std::move(edges));
314}
315
317 const auto* root = value.getIf<eve::Value::Object>();
318 if (!root)
320 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "anchor graph definition must be an object", {}, {},
321 "climbing.anchor_graph"));
323 std::string schemaId;
324 std::int64_t version = -1;
325 const eve::Value* nodesValue = field(*root, "nodes");
326 const auto* nodes = nodesValue ? nodesValue->getIf<eve::Value::Array>() : nullptr;
327 const eve::Value* edgesValue = field(*root, "edges");
328 const auto* edges = edgesValue ? edgesValue->getIf<eve::Value::Array>() : nullptr;
329 if (!readString(*root, "schemaId", schemaId) || schemaId != ClimbingAnchorGraphDefinition::SchemaId ||
330 !readInt64(*root, "schemaVersion", version) ||
332 version != ClimbingAnchorGraphDefinition::SchemaVersion - 1) || !readString(*root, "id", graph.id) ||
333 !readString(*root, "sourceGeometryContentId", graph.sourceGeometryContentId) ||
334 !readString(*root, "buildSettingsHash", graph.buildSettingsHash) || !nodes || !edges) {
337 eve::DiagnosticCode::UnknownVersion, "anchor graph schema version is unsupported", "schemaVersion", {},
338 "climbing.anchor_graph"));
340 eve::DiagnosticCode::ParseError, "anchor graph envelope has missing or invalid known fields", {}, {},
341 "climbing.anchor_graph"));
342 }
343 graph.nodes.reserve(nodes->size());
344 for (std::size_t index = 0; index < nodes->size(); ++index) {
345 const auto* object = (*nodes)[index].getIf<eve::Value::Object>();
347 std::string kind;
348 std::int64_t occupancySlots = 0;
349 if (!object || !readString(*object, "id", node.id) || !readString(*object, "kind", kind) ||
350 !readNodeKind(kind, node.kind) || !readVec(*object, "localPosition", node.localPosition) ||
351 !readVec(*object, "localNormal", node.localNormal) ||
352 !readVec(*object, "localTangent", node.localTangent) ||
353 !readVec(*object, "leftHandSocket", node.leftHandSocket) ||
354 !readVec(*object, "rightHandSocket", node.rightHandSocket) ||
355 !readVec(*object, "feetSocket", node.feetSocket) ||
356 !readInt64(*object, "occupancySlots", occupancySlots) || occupancySlots < 0 ||
357 occupancySlots > std::numeric_limits<std::uint32_t>::max() ||
358 !readStringArray(*object, "tags", node.tags))
360 eve::DiagnosticCode::ParseError, "anchor node has missing or invalid known fields",
361 "nodes." + std::to_string(index), {}, "climbing.anchor_graph"));
362 node.occupancySlots = static_cast<std::uint32_t>(occupancySlots);
363 node.extensionMetadata = unknownFields(*object, {"id", "kind", "localPosition", "localNormal",
364 "localTangent", "leftHandSocket", "rightHandSocket",
365 "feetSocket", "occupancySlots", "tags"});
366 graph.nodes.push_back(std::move(node));
367 }
368 graph.edges.reserve(edges->size());
369 for (std::size_t index = 0; index < edges->size(); ++index) {
370 const auto* object = (*edges)[index].getIf<eve::Value::Object>();
372 std::string kind;
373 if (!object || !readString(*object, "from", edge.from) || !readString(*object, "to", edge.to) ||
374 !readString(*object, "kind", kind) || !readEdgeKind(kind, edge.kind) ||
375 !readBool(*object, "bidirectional", edge.bidirectional) ||
376 !readStringArray(*object, "requiredTags", edge.requiredTags))
378 eve::DiagnosticCode::ParseError, "anchor edge has missing or invalid known fields",
379 "edges." + std::to_string(index), {}, "climbing.anchor_graph"));
380 edge.extensionMetadata = unknownFields(*object, {"from", "to", "kind", "bidirectional", "requiredTags"});
381 graph.edges.push_back(std::move(edge));
382 }
383 graph.extensionMetadata = unknownFields(*root, {"schemaId", "schemaVersion", "id",
384 "sourceGeometryContentId", "buildSettingsHash", "nodes", "edges"});
387 canonicalize(graph);
389}
390
396 if (!body.isValid() || !world.findBody(body))
398 eve::Diagnostic::error(eve::DiagnosticCode::StaleHandle, "anchor graph target body handle is stale", "body",
399 {}, "climbing.anchor_graph"));
400 canonicalize(graph);
402 instance.graph_ = std::move(graph);
403 instance.world_ = world.runtimeHandle();
404 instance.body_ = body;
405 return eve::Result<ClimbingAnchorGraphInstance>::success(std::move(instance));
406}
407
408const ClimbingAnchorNodeDefinition* ClimbingAnchorGraphInstance::findNode(std::string_view nodeId) const noexcept {
409 const auto found = std::lower_bound(graph_.nodes.begin(), graph_.nodes.end(), nodeId,
410 [](const auto& node, std::string_view id) { return node.id < id; });
411 return found != graph_.nodes.end() && found->id == nodeId ? &*found : nullptr;
412}
413
415 if (!findNode(nodeId))
417 eve::DiagnosticCode::NotFound, "anchor node was not found", "nodeId", {}, "climbing.anchor_graph"));
418 return eve::Result<ClimbingAnchorNodeRef>::success({graph_.id, std::string(nodeId), generation_});
419}
420
423 auto resolved = resolveNodeKinematics(world, reference);
424 if (!resolved) return resolved;
425 const auto* node = findNode(reference.nodeId);
426 EV_ASSERT(node != nullptr, "kinematics resolution validated the anchor node");
427 resolved.value().tags = node->tags;
428 return resolved;
429}
430
433 if (reference.graphId != graph_.id)
435 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "anchor node reference belongs to another graph",
436 "reference.graphId", {}, "climbing.anchor_graph"));
437 if (reference.graphGeneration != generation_)
439 eve::DiagnosticCode::StaleHandle, "anchor node reference belongs to a stale graph generation",
440 "reference.graphGeneration", {}, "climbing.anchor_graph"));
441 const auto* node = findNode(reference.nodeId);
442 if (!node)
444 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "anchor node was not found", "reference.nodeId", {},
445 "climbing.anchor_graph"));
446 if (world.runtimeHandle() != world_)
448 eve::DiagnosticCode::StaleHandle, "anchor graph belongs to another or stale Physics world", "world", {},
449 "climbing.anchor_graph"));
450 physics::Body3D* body = world.findBody(body_);
451 if (!body)
453 eve::Diagnostic::error(eve::DiagnosticCode::StaleHandle, "anchor graph target body handle is stale", "body",
454 {}, "climbing.anchor_graph"));
456 result.reference = reference;
457 result.kind = node->kind;
458 result.body = body_;
459 auto position = body->localToWorldPointOwned(node->localPosition.x, node->localPosition.y,
460 node->localPosition.z);
462 auto normal = body->localToWorldVectorOwned(node->localNormal.x, node->localNormal.y,
463 node->localNormal.z);
465 auto tangent = body->localToWorldVectorOwned(node->localTangent.x, node->localTangent.y,
466 node->localTangent.z);
468 auto leftHand = body->localToWorldPointOwned(node->leftHandSocket.x, node->leftHandSocket.y,
469 node->leftHandSocket.z);
470 if (!leftHand) return eve::Result<ResolvedClimbingAnchorNode>::failure(leftHand.status());
471 auto rightHand = body->localToWorldPointOwned(node->rightHandSocket.x, node->rightHandSocket.y,
472 node->rightHandSocket.z);
473 if (!rightHand) return eve::Result<ResolvedClimbingAnchorNode>::failure(rightHand.status());
474 auto feet = body->localToWorldPointOwned(node->feetSocket.x, node->feetSocket.y, node->feetSocket.z);
475 if (!feet) return eve::Result<ResolvedClimbingAnchorNode>::failure(feet.status());
476 auto velocity = body->getLocalPointVelocityOwned(node->localPosition.x, node->localPosition.y,
477 node->localPosition.z);
478 if (!velocity) return eve::Result<ResolvedClimbingAnchorNode>::failure(velocity.status());
479 result.position = vectorFrom(position.value());
480 result.normal = vectorFrom(normal.value());
481 result.tangent = vectorFrom(tangent.value());
482 result.leftHandSocket = vectorFrom(leftHand.value());
483 result.rightHandSocket = vectorFrom(rightHand.value());
484 result.feetSocket = vectorFrom(feet.value());
485 result.pointVelocity = vectorFrom(velocity.value());
486 return eve::Result<ResolvedClimbingAnchorNode>::success(std::move(result));
487}
488
490 const ClimbingAnchorNodeRef& reference) const {
491 if (reference.graphId != graph_.id)
493 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "anchor node reference belongs to another graph",
494 "reference.graphId", {}, "climbing.anchor_graph"));
495 if (reference.graphGeneration != generation_)
497 eve::DiagnosticCode::StaleHandle, "anchor node reference belongs to a stale graph generation",
498 "reference.graphGeneration", {}, "climbing.anchor_graph"));
499 if (!findNode(reference.nodeId))
501 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "anchor node was not found", "reference.nodeId", {},
502 "climbing.anchor_graph"));
503 std::vector<ClimbingAnchorEdgeDefinition> result;
504 for (const auto& edge : graph_.edges) {
505 if (edge.from == reference.nodeId)
506 result.push_back(edge);
507 else if (edge.bidirectional && edge.to == reference.nodeId) {
509 std::swap(reverse.from, reverse.to);
510 result.push_back(std::move(reverse));
511 }
512 }
513 std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs) {
514 if (lhs.kind != rhs.kind) return lhs.kind < rhs.kind;
515 return lhs.to < rhs.to;
516 });
517 return eve::Result<std::vector<ClimbingAnchorEdgeDefinition>>::success(std::move(result));
518}
519
521 const ClimbingAnchorRouteRequest& request) const {
522 if (request.start.graphId != graph_.id || request.goal.graphId != graph_.id)
524 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "route endpoints belong to another anchor graph",
525 "request", {}, "climbing.anchor_graph"));
526 if (request.start.graphGeneration != generation_ || request.goal.graphGeneration != generation_)
528 eve::DiagnosticCode::StaleHandle, "route endpoints belong to a stale graph generation", "request", {},
529 "climbing.anchor_graph"));
530 if (!findNode(request.start.nodeId) || !findNode(request.goal.nodeId))
532 eve::DiagnosticCode::NotFound, "route endpoint was not found", "request", {}, "climbing.anchor_graph"));
533 if (request.maxVisitedNodes == 0 || request.maxVisitedNodes > 65536)
535 eve::DiagnosticCode::InvalidArgument, "route maxVisitedNodes must be between one and 65536",
536 "request.maxVisitedNodes", {}, "climbing.anchor_graph"));
537 const bool requesterHasAgent = !request.requester.agentId.isZero();
538 const bool requesterHasExecution = !request.requester.executionId.isZero();
539 if (requesterHasAgent != requesterHasExecution)
541 eve::DiagnosticCode::InvalidArgument, "route requester must provide both agent and execution ids",
542 "request.requester", {}, "climbing.anchor_graph"));
543
544 std::vector<ClimbingAnchorEdgeKind> allowed = request.allowedEdgeKinds;
545 std::sort(allowed.begin(), allowed.end());
546 allowed.erase(std::unique(allowed.begin(), allowed.end()), allowed.end());
547 if (!validUniqueStrings(request.availableTags))
549 eve::DiagnosticCode::InvalidArgument, "route capability tags must be non-empty and unique",
550 "request.availableTags", {}, "climbing.anchor_graph"));
551 std::vector<std::string> availableTags = request.availableTags;
552 std::sort(availableTags.begin(), availableTags.end());
553 const auto permits = [&allowed](ClimbingAnchorEdgeKind kind) {
554 return allowed.empty() || std::binary_search(allowed.begin(), allowed.end(), kind);
555 };
556 const auto isFull = [this, &request](std::string_view nodeId) {
557 if (request.occupancyPolicy == ClimbingRouteOccupancyPolicy::Ignore) return false;
558 const auto* node = findNode(nodeId);
559 if (!node) return true;
560 std::uint32_t occupied = 0;
561 for (const auto& [id, record] : reservations_) {
562 (void)id;
563 if (record.nodeId != nodeId) continue;
564 if (record.occupant == request.requester) return false;
565 ++occupied;
566 }
567 return occupied >= node->occupancySlots;
568 };
569
570 struct Previous {
571 std::string from;
572 ClimbingAnchorEdgeKind kind = ClimbingAnchorEdgeKind::Shimmy;
573 };
574 std::deque<std::string> queue;
575 std::unordered_map<std::string, Previous> previous;
576 previous.emplace(request.start.nodeId, Previous{});
577 queue.push_back(request.start.nodeId);
578 bool found = request.start.nodeId == request.goal.nodeId;
579 while (!queue.empty() && !found) {
580 if (previous.size() > request.maxVisitedNodes)
582 eve::DiagnosticCode::PreconditionViolation, "route planning exceeded maxVisitedNodes",
583 "request.maxVisitedNodes", {}, "climbing.anchor_graph"));
584 const std::string current = std::move(queue.front());
585 queue.pop_front();
586 auto reference = nodeRef(current);
588 auto outgoing = edgesFrom(reference.value());
589 if (!outgoing) return eve::Result<ClimbingAnchorRoute>::failure(outgoing.status());
590 for (const auto& edge : outgoing.value()) {
591 const bool hasRequiredTags = std::all_of(
592 edge.requiredTags.begin(), edge.requiredTags.end(), [&availableTags](const std::string& tag) {
593 return std::binary_search(availableTags.begin(), availableTags.end(), tag);
594 });
595 if (!permits(edge.kind) || !hasRequiredTags || previous.contains(edge.to) || isFull(edge.to))
596 continue;
597 if (previous.size() >= request.maxVisitedNodes)
599 eve::DiagnosticCode::PreconditionViolation, "route planning exceeded maxVisitedNodes",
600 "request.maxVisitedNodes", {}, "climbing.anchor_graph"));
601 previous.emplace(edge.to, Previous{current, edge.kind});
602 if (edge.to == request.goal.nodeId) {
603 found = true;
604 break;
605 }
606 queue.push_back(edge.to);
607 }
608 }
609 if (!found)
611 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "no permitted anchor route reaches the goal",
612 "request.goal", {}, "climbing.anchor_graph"));
613
614 std::vector<std::string> reversedNodes;
615 std::vector<ClimbingAnchorEdgeKind> reversedKinds;
616 for (std::string cursor = request.goal.nodeId;;) {
617 reversedNodes.push_back(cursor);
618 if (cursor == request.start.nodeId) break;
619 const auto foundPrevious = previous.find(cursor);
620 if (foundPrevious == previous.end())
622 eve::Diagnostic::error(eve::DiagnosticCode::InvariantViolation, "route predecessor chain is incomplete",
623 "route", {}, "climbing.anchor_graph"));
624 reversedKinds.push_back(foundPrevious->second.kind);
625 cursor = foundPrevious->second.from;
626 }
627 std::reverse(reversedNodes.begin(), reversedNodes.end());
628 std::reverse(reversedKinds.begin(), reversedKinds.end());
629
630 ClimbingAnchorRoute route;
631 route.graphId = graph_.id;
632 route.graphGeneration = generation_;
633 route.nodes.reserve(reversedNodes.size());
634 for (const auto& nodeId : reversedNodes) route.nodes.push_back({graph_.id, nodeId, generation_});
635 route.steps.reserve(reversedKinds.size());
636 for (std::size_t index = 0; index < reversedKinds.size(); ++index)
637 route.steps.push_back({route.nodes[index], route.nodes[index + 1], reversedKinds[index]});
638 return eve::Result<ClimbingAnchorRoute>::success(std::move(route));
639}
640
641eve::Result<ClimbingAnchorReservation> ClimbingAnchorGraphInstance::reserve(
643 if (reference.graphId != graph_.id)
645 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "anchor node reference belongs to another graph",
646 "reference.graphId", {}, "climbing.anchor_graph"));
647 if (reference.graphGeneration != generation_)
649 eve::DiagnosticCode::StaleHandle, "anchor node reference belongs to a stale graph generation",
650 "reference.graphGeneration", {}, "climbing.anchor_graph"));
651 const auto* node = findNode(reference.nodeId);
652 if (!node)
654 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "anchor node was not found", "reference.nodeId", {},
655 "climbing.anchor_graph"));
656 if (occupant.agentId.isZero() || occupant.executionId.isZero())
658 eve::DiagnosticCode::InvalidArgument, "anchor reservation requires non-zero agent and execution ids",
659 "occupant", {}, "climbing.anchor_graph"));
660 std::vector<bool> occupied(node->occupancySlots, false);
661 for (const auto& [id, record] : reservations_) {
662 (void)id;
663 if (record.nodeId != node->id) continue;
664 if (record.occupant == occupant)
666 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "occupant already owns a slot on this anchor",
667 "occupant", {}, "climbing.anchor_graph"));
668 if (record.slot < occupied.size()) occupied[record.slot] = true;
669 }
670 const auto free = std::find(occupied.begin(), occupied.end(), false);
671 if (free == occupied.end())
673 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "all anchor occupancy slots are reserved",
674 "reference.nodeId", {}, "climbing.anchor_graph"));
675 const auto next = nextReservationId_.incremented();
676 if (!next)
678 eve::DiagnosticCode::PreconditionViolation, "anchor reservation identity is exhausted", "reservationId", {},
679 "climbing.anchor_graph"));
680 const ClimbingAnchorReservationId id = nextReservationId_;
681 const std::uint32_t slot = static_cast<std::uint32_t>(std::distance(occupied.begin(), free));
682 reservations_.emplace(id, ReservationRecord{node->id, slot, occupant, 1});
683 nextReservationId_ = *next;
685 {id, generation_, 1, node->id, slot, occupant}, eve::Status::success(eve::StatusCode::Applied));
686}
687
688eve::Result<void> ClimbingAnchorGraphInstance::release(const ClimbingAnchorReservation& reservation) {
689 auto valid = validateReservation(reservation);
690 if (!valid) return valid;
691 reservations_.erase(reservation.id);
693}
694
695eve::Result<void> ClimbingAnchorGraphInstance::validateReservation(
696 const ClimbingAnchorReservation& reservation) const {
697 if (reservation.graphGeneration != generation_)
699 eve::DiagnosticCode::StaleHandle, "anchor reservation belongs to a stale graph generation",
700 "reservation.graphGeneration", {}, "climbing.anchor_graph"));
701 const auto found = reservations_.find(reservation.id);
702 if (found == reservations_.end())
704 "anchor reservation is not live", "reservation.id", {},
705 "climbing.anchor_graph"));
706 const auto& record = found->second;
707 if (record.nodeId != reservation.nodeId || record.slot != reservation.slot ||
708 record.occupant != reservation.occupant || record.claimGeneration != reservation.claimGeneration)
710 eve::DiagnosticCode::Conflict, "anchor reservation credential does not match the live record",
711 "reservation", {}, "climbing.anchor_graph"));
713}
714
715eve::Result<ClimbingAnchorReservation> ClimbingAnchorGraphInstance::transferReservation(
716 const ClimbingAnchorReservation& reservation) {
717 auto valid = validateReservation(reservation);
719 auto found = reservations_.find(reservation.id);
720 if (found->second.claimGeneration == std::numeric_limits<std::uint64_t>::max())
722 eve::DiagnosticCode::PreconditionViolation, "anchor reservation claim generation is exhausted",
723 "reservation.claimGeneration", {}, "climbing.anchor_graph"));
724 ++found->second.claimGeneration;
725 ClimbingAnchorReservation transferred = reservation;
726 transferred.claimGeneration = found->second.claimGeneration;
728 std::move(transferred), eve::Status::success(eve::StatusCode::Applied));
729}
730
731eve::Result<ClimbingAnchorReservation> ClimbingAnchorGraphInstance::restoreReservation(
732 const ClimbingAnchorReservation& reservation) {
733 if (reservation.graphGeneration != generation_)
735 eve::DiagnosticCode::StaleHandle, "snapshot reservation belongs to a stale graph generation",
736 "reservation.graphGeneration", {}, "climbing.anchor_graph"));
737 const auto* node = findNode(reservation.nodeId);
738 if (!node)
740 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "snapshot reservation node was not found",
741 "reservation.nodeId", {}, "climbing.anchor_graph"));
742 if (reservation.id.isZero() || reservation.claimGeneration == 0 || reservation.occupant.agentId.isZero() ||
743 reservation.occupant.executionId.isZero() || reservation.slot >= node->occupancySlots)
745 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "snapshot reservation credential is invalid",
746 "reservation", {}, "climbing.anchor_graph"));
747 if (const auto live = reservations_.find(reservation.id); live != reservations_.end()) {
748 auto valid = validateReservation(reservation);
750 return transferReservation(reservation);
751 }
752 for (const auto& [id, record] : reservations_) {
753 (void)id;
754 if (record.nodeId == reservation.nodeId &&
755 (record.slot == reservation.slot || record.occupant == reservation.occupant))
757 eve::DiagnosticCode::Conflict, "snapshot anchor slot or occupant is already reserved",
758 "reservation.slot", {}, "climbing.anchor_graph"));
759 }
760 const auto next = nextReservationId_.incremented();
761 if (!next)
763 eve::DiagnosticCode::PreconditionViolation, "anchor reservation identity is exhausted", "reservationId", {},
764 "climbing.anchor_graph"));
765 const ClimbingAnchorReservationId id = nextReservationId_;
766 reservations_.emplace(id, ReservationRecord{reservation.nodeId, reservation.slot,
767 reservation.occupant, 1});
768 nextReservationId_ = *next;
769 ClimbingAnchorReservation restored{id, generation_, 1, reservation.nodeId, reservation.slot,
770 reservation.occupant};
772 std::move(restored), eve::Status::success(eve::StatusCode::Applied));
773}
774
775eve::Result<std::uint32_t> ClimbingAnchorGraphInstance::releaseOccupant(ClimbingAnchorOccupant occupant) {
776 if (occupant.agentId.isZero() || occupant.executionId.isZero())
778 eve::DiagnosticCode::InvalidArgument, "release requires non-zero agent and execution ids", "occupant", {},
779 "climbing.anchor_graph"));
780 std::uint32_t count = 0;
781 for (auto iterator = reservations_.begin(); iterator != reservations_.end();) {
782 if (iterator->second.occupant == occupant) {
783 iterator = reservations_.erase(iterator);
784 ++count;
785 } else {
786 ++iterator;
787 }
788 }
791}
792
796 if (generation_ == std::numeric_limits<std::uint64_t>::max())
798 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "anchor graph generation is exhausted",
799 "generation", {}, "climbing.anchor_graph"));
800 canonicalize(graph);
802 result.oldGeneration = generation_;
803 result.newGeneration = generation_ + 1;
804 result.invalidatedOccupants.reserve(reservations_.size());
805 for (const auto& [id, record] : reservations_) {
806 (void)id;
807 result.invalidatedOccupants.push_back(record.occupant);
808 }
809 std::sort(result.invalidatedOccupants.begin(), result.invalidatedOccupants.end());
810 result.invalidatedOccupants.erase(
811 std::unique(result.invalidatedOccupants.begin(), result.invalidatedOccupants.end()),
812 result.invalidatedOccupants.end());
813 graph_ = std::move(graph);
814 generation_ = result.newGeneration;
815 reservations_.clear();
818}
819
820} // namespace eve::climbing
double value
int root
Definition AnimSmr.cpp:119
#define EV_ASSERT(cond,...)
Assert an internal engine invariant (state that must always hold).
Definition Assert.h:37
std::string output
std::string from
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
Vec3 tangent
Definition CaveMesh.cpp:80
Versioned body-local climbing topology, dynamic resolution, and deterministic occupancy.
std::string nodeId
std::map< std::string, Var > values
DiagnosticCode code
const GltfImportRequest & request
std::string text
TokenKind kind
std::array< float, 3 > position
std::string name
bool valid
std::vector< BvhNode > nodes
Texture * normal
const std::string * tag
graphics::Canvas * previous
bool finite
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
World3D * world
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float begin
const RoadNode * node
const RoadEdge * edge
double number
bool found
double current
bool boolean
bool occupied
std::uint32_t count
std::size_t cursor
Json object
std::string body
uint32_t index
std::vector< int > edges
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
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
constexpr bool isValid() const noexcept
Returns whether both coordinates are usable handle values.
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
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Runtime instance binding one immutable graph generation to one Physics body.
eve::Result< std::vector< ClimbingAnchorEdgeDefinition > > edgesFrom(const ClimbingAnchorNodeRef &reference) const
Returns deterministic outgoing edges for a current-generation node reference.
eve::Result< ClimbingAnchorNodeRef > nodeRef(std::string_view nodeId) const
Returns a current-generation reference for a stable node id.
physics::PhysicsBodyHandle body() const noexcept
Target Physics body handle; stale detection occurs during resolve.
static eve::Result< ClimbingAnchorGraphInstance > bind(ClimbingAnchorGraphDefinition graph, physics::World3D &world, physics::PhysicsBodyHandle body)
Validates and binds a graph snapshot to an existing Physics body.
eve::Result< ResolvedClimbingAnchorNode > resolveNodeKinematics(physics::World3D &world, const ClimbingAnchorNodeRef &reference) const
Resolves per-tick anchor transforms without copying cold tag metadata.
eve::Result< ResolvedClimbingAnchorNode > resolveNode(physics::World3D &world, const ClimbingAnchorNodeRef &reference) const
Resolves a current node from body-local to world space after validating world/body generation.
eve::Result< ClimbingAnchorRoute > planRoute(const ClimbingAnchorRouteRequest &request) const
Finds the shortest occupancy-aware route using stable edge-kind/node-id tie breaking.
constexpr bool isZero() const noexcept
Returns whether this value is zero.
3D rigid body (Box3D) in meter-space coordinates (+Y up by convention). Owned by a World3D; create pr...
Definition Body3D.h:24
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
const char * nodeKindName(NodeKind kind) noexcept
Stable wire name for a node kind. @ownership Non-owning pointer to a static string literal; do not fr...
float lengthSquared(Vec3 value)
Length squared.
ClimbingAnchorEdgeKind
Allowed authored transition between two explicit anchors.
eve::Result< ClimbingAnchorGraphDefinition > decodeClimbingAnchorGraphDefinition(const eve::Value &value)
Decodes and validates a complete owning graph candidate transactionally.
ClimbingAnchorKind
Authored semantic of one explicit climbing anchor.
eve::Result< void > validateClimbingAnchorGraphDefinition(const ClimbingAnchorGraphDefinition &graph)
Validates all graph identities, frames, slots, tags, and edge endpoints.
eve::Result< eve::Value > encodeClimbingAnchorGraphDefinition(const ClimbingAnchorGraphDefinition &graph)
Encodes a validated graph to its canonical owning Value representation.
bool readString(const eve::Value::Object &object, const char *name, std::string &output)
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
const EditorValue * field(const EditorValue &value, const char *name)
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
Directed authored transition in a climbing anchor graph.
Versioned graph asset; topology is authoritative while collision remains owned by Physics.
eve::Value::Object extensionMetadata
Unknown schema fields retained across decode and encode.
std::vector< ClimbingAnchorNodeDefinition > nodes
std::vector< ClimbingAnchorEdgeDefinition > edges
static constexpr std::string_view SchemaId
Canonical schema id.
static constexpr std::int64_t SchemaVersion
Current schema version.
Atomic graph-reload outcome and owners whose old-generation reservations were invalidated.
std::vector< ClimbingAnchorOccupant > invalidatedOccupants
One body-local anchor node with deterministic occupancy slots.
Generation-qualified reference to an authored anchor node.
Compound occupancy owner; execution ids are only unique inside one agent/runtime.
Owning reservation credential; release validates every field.
Read-only route request over one current anchor-graph generation.
World-space projection of a body-local anchor at one synchronous resolve point.