载入中...
搜索中...
未找到
ClimbingAnchorExecution.cpp
浏览该文件的文档.
1#include "climbing/Climbing.h"
2
4#include "physics/Body3D.h"
5#include "physics/World3D.h"
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10#include <utility>
11
12namespace eve::climbing {
13namespace {
14
15constexpr float epsilon = 1e-5f;
16
17Vec3 subtract(Vec3 lhs, Vec3 rhs) { return {lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z}; }
18Vec3 add(Vec3 lhs, Vec3 rhs) { return {lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z}; }
19Vec3 scale(Vec3 value, float amount) { return {value.x * amount, value.y * amount, value.z * amount}; }
20float length(Vec3 value) { return std::sqrt(value.x * value.x + value.y * value.y + value.z * value.z); }
21
22bool activePhase(ClimbingPhase phase) {
25}
26
27const ClimbingActionDefinition* findAction(const ClimbingProfileDefinition& profile, std::string_view id) {
28 const auto found = std::lower_bound(profile.actions.begin(), profile.actions.end(), id,
29 [](const auto& action, std::string_view value) { return action.id < value; });
30 return found != profile.actions.end() && found->id == id ? &*found : nullptr;
31}
32
33bool startKindMatches(ClimbingAnchorKind nodeKind, ClimbingActionKind actionKind) {
34 if (nodeKind == ClimbingAnchorKind::LadderRung)
35 return actionKind == ClimbingActionKind::LadderMount || actionKind == ClimbingActionKind::LedgeGrab;
36 if (nodeKind == ClimbingAnchorKind::Beam) return actionKind == ClimbingActionKind::BeamBalance;
37 if (nodeKind == ClimbingAnchorKind::Pole) return actionKind == ClimbingActionKind::PoleSwing;
38 if (nodeKind == ClimbingAnchorKind::Bar) return actionKind == ClimbingActionKind::BarSwing;
39 return actionKind == ClimbingActionKind::LedgeGrab || actionKind == ClimbingActionKind::ClimbDown;
40}
41
42bool edgeKindMatches(ClimbingAnchorEdgeKind edgeKind, ClimbingActionKind actionKind) {
43 switch (edgeKind) {
46 return actionKind == ClimbingActionKind::CornerInner || actionKind == ClimbingActionKind::CornerOuter;
54 return actionKind == ClimbingActionKind::PoleSwing || actionKind == ClimbingActionKind::BarSwing;
55 }
56 return false;
57}
58
59bool tagsContain(const std::vector<std::string>& values, const std::vector<std::string>& required) {
60 return std::all_of(required.begin(), required.end(), [&](const auto& tag) {
61 return std::find(values.begin(), values.end(), tag) != values.end();
62 });
63}
64
65Vec3 hangingFeet(const ResolvedClimbingAnchorNode& node, const ClimbingActionDefinition& action,
66 const ClimbingProfileDefinition& profile) {
67 Vec3 result = add(node.position,
68 scale(node.normal, profile.capsuleRadius + profile.skin + action.hangBodyOffset));
69 result.y = node.position.y - action.hangFeetBelowLedge;
70 return result;
71}
72
73eve::Result<void> validatePath(physics::World3D& world, Vec3 start, const ClimbingCandidate& candidate,
74 const ClimbingActionDefinition& action, const ClimbingProfileDefinition& profile,
75 std::uint32_t& queryCount) {
76 physics::QueryFilter3D filter = profile.queryFilter;
77 filter.ignoredBodyId = candidate.ignoredBodyId;
79 for (std::uint32_t segment = 1; segment <= profile.pathValidationSegments; ++segment) {
80 const float t = static_cast<float>(segment) / static_cast<float>(profile.pathValidationSegments);
81 const Vec3 target = detail::trajectoryPoint(start, candidate, action, profile, t);
82 const Vec3 desired = subtract(target, current);
83 const float lowerY = current.y + profile.capsuleRadius;
84 const float upperY = current.y + profile.capsuleHeight - profile.capsuleRadius;
85 auto moved = world.moveCapsuleOwned(current.x, lowerY, current.z, current.x, upperY, current.z,
86 profile.capsuleRadius, desired.x, desired.y, desired.z, filter);
87 ++queryCount;
88 if (!moved) return eve::Result<void>::failure(moved.status());
89 const Vec3 actual{moved.value().deltaX, moved.value().deltaY, moved.value().deltaZ};
90 if (length(subtract(desired, actual)) > profile.skin + 0.001f)
93 "candidate.path." + std::to_string(segment), {}, "climbing.anchor_execution"));
95 }
97}
98
99eve::Result<ClimbingCandidate> makeCandidate(physics::World3D& world, ClimbingAnchorGraphInstance& graph,
100 const ResolvedClimbingAnchorNode& node,
101 const ClimbingActionDefinition& action,
102 const ClimbingProfileDefinition& profile,
103 const ClimbingPose& pose, std::uint64_t definitionGeneration) {
104 physics::Body3D* body = world.findBody(graph.body());
105 if (!body)
107 eve::Diagnostic::error(eve::DiagnosticCode::StaleHandle, "anchor graph target body is stale", "graph.body",
108 {}, "climbing.anchor_execution"));
109 ClimbingCandidate candidate;
110 candidate.actionId = action.id;
111 candidate.definitionGeneration = definitionGeneration;
112 candidate.world = world.runtimeHandle();
113 candidate.obstacleBody = graph.body();
114 candidate.obstacleBodyId = body->getId();
115 candidate.ignoredBodyId = pose.ignoredBodyId;
116 candidate.frontPoint = node.position;
117 candidate.topPoint = node.position;
118 candidate.surfaceNormal = node.normal;
119 candidate.surfaceTangent = node.tangent;
120 candidate.leftHandAnchor = node.leftHandSocket;
121 candidate.rightHandAnchor = node.rightHandSocket;
122 candidate.landingFeet = action.kind == ClimbingActionKind::LadderDismount ||
124 ? node.position
125 : hangingFeet(node, action, profile);
126 candidate.obstacleHeight = length(subtract(candidate.landingFeet, pose.feet));
127 candidate.kind = action.kind;
128 candidate.support = action.kind == ClimbingActionKind::LadderDismount ||
132 auto localTop = body->worldToLocalPointOwned(node.position.x, node.position.y, node.position.z);
133 if (!localTop) return eve::Result<ClimbingCandidate>::failure(localTop.status());
134 auto localLanding = body->worldToLocalPointOwned(candidate.landingFeet.x, candidate.landingFeet.y,
135 candidate.landingFeet.z);
136 if (!localLanding) return eve::Result<ClimbingCandidate>::failure(localLanding.status());
137 candidate.bodyLocalTop = {localTop.value().x, localTop.value().y, localTop.value().z};
138 candidate.bodyLocalLanding = {localLanding.value().x, localLanding.value().y, localLanding.value().z};
139 return eve::Result<ClimbingCandidate>::success(std::move(candidate));
140}
141
142} // namespace
143
145 auto released = releaseAnchorReservation();
146 released.ignore("climbing runtime destruction releases graph occupancy");
147}
148
149eve::Result<void> ClimbingRuntime::releaseAnchorReservation() {
150 if (!execution_ || execution_->anchorReservation.id.isZero())
152 auto graph = Climbing::resolveAnchorGraph(execution_->anchorGraph);
153 if (!graph.isBound()) {
154 execution_->anchorReservation = {};
155 execution_->anchorGraph = {};
156 execution_->anchorNode = {};
158 eve::DiagnosticCode::StaleHandle, "anchor graph instance is stale while releasing occupancy",
159 "execution.anchorGraph", {}, "climbing.anchor_execution"));
160 }
161 auto released = graph->release(execution_->anchorReservation);
162 if (released || (released.error() &&
163 (released.error()->code() == eve::DiagnosticCode::StaleHandle ||
164 released.error()->code() == eve::DiagnosticCode::NotFound ||
165 released.error()->code() == eve::DiagnosticCode::Conflict))) {
166 execution_->anchorReservation = {};
167 execution_->anchorGraph = {};
168 execution_->anchorNode = {};
169 }
170 if (!released && released.error() && released.error()->code() == eve::DiagnosticCode::Conflict)
172 return released;
173}
174
176 if (!execution_ || !execution_->anchorGraph.isValid() || execution_->anchorReservation.id.isZero())
178 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "active execution is not bound to an anchor graph",
179 "execution.anchor", {}, "climbing.anchor_execution"));
180 return eve::Result<ClimbingAnchorNodeRef>::success(execution_->anchorNode);
181}
182
184 physics::World3D& world, ClimbingAnchorGraphHandleRef graphReference, const ClimbingAnchorNodeRef& nodeReference,
185 ClimbingAnchorAgentId agentId, std::string_view actionId, const ClimbingPose& pose, eve::SimulationTick tick) {
186 if (activePhase(phase_) || execution_)
188 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "a climbing execution is already active",
189 "runtime.phase", {}, "climbing.anchor_execution"));
190 if (nextExecutionId_ == 0 || nextExecutionId_ == std::numeric_limits<std::uint64_t>::max())
192 eve::DiagnosticCode::PreconditionViolation, "climbing execution id space is exhausted",
193 "runtime.nextExecutionId", {}, "climbing.anchor_execution"));
194 if (agentId.isZero())
196 eve::DiagnosticCode::InvalidArgument, "anchor execution requires a stable non-zero agent id", "agentId", {},
197 "climbing.anchor_execution"));
198 auto graph = Climbing::resolveAnchorGraph(graphReference);
199 if (!graph.isBound())
201 eve::Diagnostic::error(eve::DiagnosticCode::StaleHandle, "anchor graph instance handle is stale", "graph",
202 {}, "climbing.anchor_execution"));
203 auto resolved = graph->resolveNode(world, nodeReference);
204 if (!resolved) return eve::Result<ClimbingCandidate>::failure(resolved.status());
205 const ClimbingActionDefinition* action = findAction(profile_, actionId);
206 if (!action || !startKindMatches(resolved.value().kind, action->kind))
208 eve::DiagnosticCode::InvalidArgument, "action kind cannot approach the requested anchor node", "actionId",
209 {}, "climbing.anchor_execution"));
210 if (!action->requiredNotifies.empty() && !validatedAnimationActions_.contains(action->id))
213 "climbing.animation.notify_missing: action clip contract was not validated",
214 "actionId", {}, "climbing.anchor_execution"));
215 auto candidate = makeCandidate(world, *graph, resolved.value(), *action, profile_, pose, definitionGeneration_);
216 if (!candidate) return candidate;
217 if (candidate.value().obstacleHeight + epsilon < action->minHeight ||
218 candidate.value().obstacleHeight - epsilon > action->maxHeight)
220 eve::DiagnosticCode::PreconditionViolation, "anchor approach lies outside the action geometry range",
221 "action.height", {}, "climbing.anchor_execution"));
222 auto path = validatePath(world, pose.feet, candidate.value(), *action, profile_, lastQueryCount_);
224 auto eventCapacity = requireEventCapacity(1, tick);
225 if (!eventCapacity) return eve::Result<ClimbingCandidate>::failure(eventCapacity.status());
226 const ClimbingExecutionId executionId(nextExecutionId_);
227 auto reservation = graph->reserve(nodeReference, {agentId, executionId});
228 if (!reservation) return eve::Result<ClimbingCandidate>::failure(reservation.status());
229
230 PreparedBegin prepared;
231 prepared.executionId = executionId;
232 prepared.candidate = candidate.value();
233 prepared.action = *action;
234 prepared.startFeet = pose.feet;
235 prepared.tick = tick;
236 prepared.conditionsSatisfied = action->requiredConditionTags.empty();
237 auto committed = commitBegin(std::move(prepared));
238 if (!committed) {
239 auto rollback = graph->release(reservation.value());
240 rollback.ignore("rollback graph reservation after climbing begin commit failure");
241 return eve::Result<ClimbingCandidate>::failure(committed.status());
242 }
243 execution_->anchorGraph = graphReference;
244 execution_->anchorNode = nodeReference;
245 execution_->anchorReservation = std::move(reservation).takeValue();
246 return eve::Result<ClimbingCandidate>::success(std::move(committed).takeValue().candidate,
248}
249
252 ClimbingAnchorEdgeKind edgeKind,
253 std::string_view actionId,
255 if ((phase_ != ClimbingPhase::Hanging && phase_ != ClimbingPhase::Balanced &&
256 phase_ != ClimbingPhase::Swinging) || !execution_ || !execution_->anchorGraph.isValid() ||
257 execution_->anchorReservation.id.isZero())
259 eve::DiagnosticCode::PreconditionViolation, "anchor transition requires a graph-bound hanging execution",
260 "runtime.phase", {}, "climbing.anchor_execution"));
261 if (!execution_->branchWindowOpen)
263 eve::DiagnosticCode::PreconditionViolation, "anchor transition branch window is closed",
264 "execution.branchWindow", {}, "climbing.anchor_execution"));
265 if (tick <= execution_->lastTick)
267 eve::DiagnosticCode::Conflict, "anchor transition tick must be newer than the last execution tick", "tick",
268 {}, "climbing.anchor_execution"));
269 auto graph = Climbing::resolveAnchorGraph(execution_->anchorGraph);
270 if (!graph.isBound())
272 eve::Diagnostic::error(eve::DiagnosticCode::StaleHandle, "anchor graph instance handle is stale",
273 "execution.anchorGraph", {}, "climbing.anchor_execution"));
274 auto reservationValid = graph->validateReservation(execution_->anchorReservation);
275 if (!reservationValid) return reservationValid;
276 auto edges = graph->edgesFrom(execution_->anchorNode);
277 if (!edges) return eve::Result<void>::failure(edges.status());
278 const auto edge = std::find_if(edges.value().begin(), edges.value().end(), [&](const auto& candidate) {
279 return candidate.to == target.nodeId && candidate.kind == edgeKind;
280 });
281 if (edge == edges.value().end())
283 eve::DiagnosticCode::NotFound, "no authored edge connects the current and requested anchors", "target", {},
284 "climbing.anchor_execution"));
285 auto resolved = graph->resolveNode(world, target);
286 if (!resolved) return eve::Result<void>::failure(resolved.status());
287 if (!tagsContain(resolved.value().tags, edge->requiredTags))
289 "target anchor does not satisfy the edge tag contract",
290 "target.tags", {}, "climbing.anchor_execution"));
291 const ClimbingActionDefinition* action = findAction(profile_, actionId);
292 if (!action || !edgeKindMatches(edgeKind, action->kind))
294 "action kind does not match the authored anchor edge",
295 "actionId", {}, "climbing.anchor_execution"));
296 if (!action->requiredNotifies.empty() && !validatedAnimationActions_.contains(action->id))
299 "climbing.animation.notify_missing: action clip contract was not validated",
300 "actionId", {}, "climbing.anchor_execution"));
302 pose.feet = execution_->currentFeet;
303 pose.forward = scale(execution_->candidate.surfaceNormal, -1.f);
304 pose.ignoredBodyId = execution_->candidate.ignoredBodyId;
305 pose.grounded = false;
306 auto candidate = makeCandidate(world, *graph, resolved.value(), *action, profile_, pose, definitionGeneration_);
307 if (!candidate) return eve::Result<void>::failure(candidate.status());
308 const float span = length(subtract(candidate.value().landingFeet, execution_->currentFeet));
309 candidate.value().obstacleHeight = span;
310 if (span + epsilon < action->minHeight || span - epsilon > action->maxHeight)
312 eve::DiagnosticCode::PreconditionViolation, "anchor transition lies outside the action geometry range",
313 "action.height", {}, "climbing.anchor_execution"));
314 auto path = validatePath(world, execution_->currentFeet, candidate.value(), *action, profile_, lastQueryCount_);
315 if (!path) return eve::Result<void>::failure(path.status());
316 auto eventCapacity = requireEventCapacity(1, tick);
317 if (!eventCapacity) return eventCapacity;
318 auto nextReservation = graph->reserve(target, execution_->anchorReservation.occupant);
319 if (!nextReservation) return eve::Result<void>::failure(nextReservation.status());
320 auto released = graph->release(execution_->anchorReservation);
321 if (!released) {
322 auto rollback = graph->release(nextReservation.value());
323 rollback.ignore("rollback target reservation after source release failure");
324 return released;
325 }
326
327 execution_->candidate = std::move(candidate).takeValue();
328 execution_->action = *action;
329 execution_->definitionGeneration = definitionGeneration_;
330 execution_->startFeet = execution_->currentFeet;
331 execution_->lastPlannedFeet = execution_->currentFeet;
332 execution_->elapsed = eve::Duration::zero();
333 execution_->duration = action->duration;
334 execution_->lastTick = tick;
335 execution_->accumulatedResidual = {};
336 execution_->horizontalWarpUsed = 0.f;
337 execution_->verticalWarpUsed = 0.f;
338 execution_->facingWarpUsed = 0.f;
339 execution_->leftContactEmitted = false;
340 execution_->rightContactEmitted = false;
341 execution_->landContactReleased = false;
342 execution_->compactCollisionActive = false;
343 execution_->branchWindowOpen = false;
344 execution_->anchorNode = target;
345 execution_->anchorReservation = std::move(nextReservation).takeValue();
347 terminalCode_.clear();
348 enqueueEvent({ClimbingEventKind::AnchorTransitionStarted, execution_->candidate.actionId, tick,
349 execution_->executionId});
351}
352
353} // namespace eve::climbing
LogicalId target
double value
Duration start
eve::EntitySpatialPose pose
float phase
Definition CaveMesh.cpp:58
float length
Definition CaveMesh.cpp:94
Deterministic climbing/parkour planning and capsule-constrained execution.
std::map< std::string, Var > values
std::array< float, 3 > scale
bool required
const std::string * tag
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
World3D * world
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
float t
const RoadNode * node
const RoadEdge * edge
bool found
std::string filter
double current
SimulationTick tick
std::string body
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
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
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 Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
eve::Result< void > transitionAnchor(physics::World3D &world, const ClimbingAnchorNodeRef &target, ClimbingAnchorEdgeKind edgeKind, std::string_view actionId, eve::SimulationTick tick)
Atomically moves a hanging graph execution to a reserved adjacent node.
~ClimbingRuntime() noexcept
Releases any live anchor reservation before runtime storage is destroyed.
eve::Result< ClimbingCandidate > tryBeginAnchor(physics::World3D &world, ClimbingAnchorGraphHandleRef graph, const ClimbingAnchorNodeRef &node, ClimbingAnchorAgentId agentId, std::string_view actionId, const ClimbingPose &pose, eve::SimulationTick tick)
Begins an authored graph anchor using the same authoritative execution lifecycle.
eve::Result< ClimbingAnchorNodeRef > currentAnchor() const
Returns the current graph node reference, or NotFound when execution is probe-only.
ClimbingExecutionId executionId() const noexcept
Current execution identity, or zero when no execution is active or retained.
Definition Climbing.h:805
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.
constexpr bool isZero() const noexcept
Returns whether this value is zero.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
Vec3 trajectoryPoint(Vec3 start, const ClimbingCandidate &candidate, const ClimbingActionDefinition &action, const ClimbingProfileDefinition &profile, float normalizedTime) noexcept
Trajectory point.
const ClimbingActionDefinition * findAction(const ClimbingProfile &profile, std::string_view id)
Optional action. @borrowed From profile; lifetime ends when the caller-owned profile changes or is de...
ClimbingAnchorEdgeKind
Allowed authored transition between two explicit anchors.
ClimbingAnchorKind
Authored semantic of one explicit climbing anchor.
ClimbingPhase
Execution lifecycle visible to gameplay and animation adapters.
Definition Climbing.h:470
ClimbingActionKind
Data-driven action family; stable action identity remains the definition id.
Definition Climbing.h:46
Definition of one geometry-compatible climbing action.
Definition Climbing.h:160
Generation-qualified reference to an authored anchor node.
Input pose used for one deterministic candidate probe.
Definition Climbing.h:275