载入中...
搜索中...
未找到
ClimbingSelection.cpp
浏览该文件的文档.
3
4#include "physics/World3D.h"
5
6#include <algorithm>
7#include <cmath>
8#include <limits>
9#include <utility>
10
11namespace eve::climbing {
12namespace {
13
14bool activePhase(ClimbingPhase phase) {
17}
18
19bool commandMatches(ClimbingCommandRequirement requirement, ClimbingCommand command) {
20 if (requirement == ClimbingCommandRequirement::Any) return true;
21 switch (command) {
27 }
28 return false;
29}
30
31const ClimbingActionDefinition* findAction(const ClimbingProfileDefinition& profile, std::string_view id) {
32 for (const ClimbingActionDefinition& action : profile.actions)
33 if (action.id == id) return &action;
34 return nullptr;
35}
36
37Vec3 subtract(Vec3 lhs, Vec3 rhs) { return {lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z}; }
38float length(Vec3 value) { return std::sqrt(value.x * value.x + value.y * value.y + value.z * value.z); }
39
40eve::Result<void> validateFullCapsulePath(physics::World3D& world, Vec3 start, const ClimbingCandidate& candidate,
41 const ClimbingActionDefinition& action,
42 const ClimbingProfileDefinition& profile, std::uint32_t& queryCount,
43 std::uint32_t queryBudget, bool& budgetExceeded,
44 std::uint32_t& moverIterations) {
45 physics::QueryFilter3D filter = profile.queryFilter;
46 filter.ignoredBodyId = candidate.ignoredBodyId;
48 const std::uint32_t remainingBudget = queryCount < queryBudget ? queryBudget - queryCount : 0;
49 const std::uint32_t segmentCount = std::min(profile.pathValidationSegments, remainingBudget);
50 if (segmentCount == 0) {
51 budgetExceeded = true;
53 "climbing.query_budget.exceeded", "candidate.path", {},
54 "climbing.selection"));
55 }
56 if (segmentCount < profile.pathValidationSegments) budgetExceeded = true;
57 for (std::uint32_t segment = 1; segment <= segmentCount; ++segment) {
58 const float t = static_cast<float>(segment) / static_cast<float>(segmentCount);
59 const Vec3 target = detail::trajectoryPoint(start, candidate, action, profile, t);
60 const Vec3 desired = subtract(target, current);
61 const float lowerY = current.y + profile.capsuleRadius;
62 const float collisionHeight = action.kind == ClimbingActionKind::Slide
63 ? profile.compactCapsuleHeight
64 : profile.capsuleHeight;
65 const float upperY = current.y + collisionHeight - profile.capsuleRadius;
66 auto moved = world.moveCapsuleOwned(current.x, lowerY, current.z, current.x, upperY, current.z,
67 profile.capsuleRadius, desired.x, desired.y, desired.z, filter);
68 ++queryCount;
69 if (!moved) return eve::Result<void>::failure(moved.status());
70 moverIterations += static_cast<std::uint32_t>(std::max(0, moved.value().iterations));
71 const Vec3 actual{moved.value().deltaX, moved.value().deltaY, moved.value().deltaZ};
72 if (length(subtract(desired, actual)) > profile.skin + 0.001f)
75 "candidate.path." + std::to_string(segment), {}, "climbing.selection"));
77 }
79}
80
81} // namespace
82
83eve::Result<ClimbingRuntime::PreparedBegin> ClimbingRuntime::prepareBegin(physics::World3D& world,
84 const ClimbingPose& pose,
86 if (activePhase(phase_))
88 "a climbing execution is already active",
89 "runtime.phase", {}, "climbing.selection"));
90 if (nextExecutionId_ == 0 || nextExecutionId_ == std::numeric_limits<std::uint64_t>::max())
92 eve::DiagnosticCode::PreconditionViolation, "climbing execution id space is exhausted",
93 "runtime.nextExecutionId", {}, "climbing.selection"));
94
95 auto candidates = probe(world, pose);
96 if (!candidates) return eve::Result<PreparedBegin>::failure(candidates.status());
97 ClimbingCandidateSet values = std::move(candidates).takeValue();
98 if (values.empty())
100 "no valid climbing candidate was found",
101 "candidate", {}, "climbing.selection"));
102 return prepareBeginCandidate(world, pose, tick, values.takeFront());
103}
104
105eve::Result<ClimbingRuntime::PreparedBegin> ClimbingRuntime::prepareBeginCandidate(
106 physics::World3D& world, const ClimbingPose& pose, eve::SimulationTick tick, ClimbingCandidate candidate) {
107 if (activePhase(phase_))
109 "a climbing execution is already active",
110 "runtime.phase", {}, "climbing.selection"));
111 if (nextExecutionId_ == 0 || nextExecutionId_ == std::numeric_limits<std::uint64_t>::max())
113 eve::DiagnosticCode::PreconditionViolation, "climbing execution id space is exhausted",
114 "runtime.nextExecutionId", {}, "climbing.selection"));
115 const ClimbingActionDefinition* action = findAction(profile_, candidate.actionId);
116 if (!action)
118 eve::Diagnostic::error(eve::DiagnosticCode::InvariantViolation, "selected action definition disappeared",
119 "candidate.actionId", {}, "climbing.selection"));
120 if (!action->requiredNotifies.empty() && !validatedAnimationActions_.contains(action->id))
123 "climbing.animation.notify_missing: action clip contract was not validated",
124 "candidate.actionId", {}, "climbing.selection"));
125 bool budgetExceeded = false;
126 std::uint32_t moverIterations = 0;
127 auto path = validateFullCapsulePath(world, pose.feet, candidate, *action, profile_, lastQueryCount_,
128 ClimbingQueryBudgets::CandidateProbe, budgetExceeded, moverIterations);
129 lastCounters_.queryCount = lastQueryCount_;
130 lastCounters_.moverIterations += moverIterations;
131 if (budgetExceeded) lastCounters_.budgetState = ClimbingQueryBudgetState::Exceeded;
132 telemetry_.updateLatestCounters(lastCounters_);
133 if (!path) return eve::Result<PreparedBegin>::failure(path.status());
134
135 PreparedBegin prepared;
136 prepared.executionId = ClimbingExecutionId(nextExecutionId_);
137 prepared.candidate = std::move(candidate);
138 prepared.action = *action;
139 prepared.startFeet = pose.feet;
140 prepared.tick = tick;
141 prepared.conditionsSatisfied = action->requiredConditionTags.empty();
142 return eve::Result<PreparedBegin>::success(std::move(prepared));
143}
144
145eve::Result<ClimbingStart> ClimbingRuntime::commitBegin(PreparedBegin prepared) {
146 if (activePhase(phase_) || execution_)
148 "climbing state changed before commit",
149 "runtime.phase", {}, "climbing.selection"));
150 if (prepared.executionId.isZero() || prepared.executionId.value() != nextExecutionId_)
152 "prepared climbing execution is stale",
153 "executionId", {}, "climbing.selection"));
154 if (!prepared.conditionsSatisfied)
156 eve::DiagnosticCode::Unsupported, "required climbing conditions were not evaluated by a service authority",
157 "action.requiredConditionTags", {}, "climbing.selection"));
158 auto eventCapacity = requireEventCapacity(1, prepared.tick);
159 if (!eventCapacity) return eve::Result<ClimbingStart>::failure(eventCapacity.status());
160
161 Execution execution;
162 execution.executionId = prepared.executionId;
163 execution.definitionGeneration = prepared.candidate.definitionGeneration;
164 execution.candidate = prepared.candidate;
165 execution.action = std::move(prepared.action);
166 execution.startFeet = prepared.startFeet;
167 execution.currentFeet = prepared.startFeet;
168 execution.lastPlannedFeet = prepared.startFeet;
169 execution.duration = execution.action.duration;
170 execution.lastTick = prepared.tick;
171
172 ClimbingStart started{prepared.executionId, prepared.candidate};
173 execution_ = std::move(execution);
174 previousActionId_ = started.candidate.actionId;
175 if (debugCapture_ == ClimbingDebugCapture::Enabled) motionEvidence_.clear();
177 terminalCode_.clear();
178 ++nextExecutionId_;
179 enqueueEvent({ClimbingEventKind::Started, started.candidate.actionId, prepared.tick, started.executionId});
181}
182
185 auto prepared = prepareBegin(world, pose, tick);
186 if (!prepared) return eve::Result<ClimbingCandidate>::failure(prepared.status());
187 auto committed = commitBegin(std::move(prepared).takeValue());
188 if (!committed) return eve::Result<ClimbingCandidate>::failure(committed.status());
189 return eve::Result<ClimbingCandidate>::success(std::move(committed).takeValue().candidate,
191}
192
194 const ClimbingPose& pose, ClimbingIntent& intent,
196 eve::SimulationTick lastGroundedTick) {
197 if (!ClimbingInputSystem::peek(intent, command, tick))
199 eve::DiagnosticCode::NotFound, "climbing.input.no_match", "intent.commands", {}, "climbing.selection"));
200
201 ClimbingPose effectivePose = pose;
202 if (!effectivePose.grounded && command != ClimbingCommand::Drop &&
203 ClimbingInputSystem::coyoteWindowState(tick, lastGroundedTick, runtime.profile_.coyoteTicks) ==
205 effectivePose.grounded = true;
206
207 auto prepared = runtime.prepareBegin(world, effectivePose, tick);
208 if (!prepared) return eve::Result<ClimbingStart>::failure(prepared.status());
209 if (!commandMatches(prepared.value().action.requiredCommand, command))
211 eve::DiagnosticCode::NotFound, "selected climbing action does not accept the buffered command",
212 "action.requiredCommand", {}, "climbing.selection"));
213
214 const ClimbingIntent originalIntent = intent;
215 auto consumed = ClimbingInputSystem::consume(intent, command, tick, prepared.value().executionId);
216 if (!consumed || !consumed.value()) {
217 intent = originalIntent;
220 eve::DiagnosticCode::Conflict, "eligible climbing input changed before transaction commit",
221 "intent.commands", {}, "climbing.selection"));
222 }
223
224 auto committed = runtime.commitBegin(std::move(prepared).takeValue());
225 if (!committed) {
226 intent = originalIntent;
227 return eve::Result<ClimbingStart>::failure(committed.status());
228 }
229 return committed;
230}
231
232} // namespace eve::climbing
LogicalId target
double value
Duration start
eve::EntitySpatialPose pose
float phase
Definition CaveMesh.cpp:58
float length
Definition CaveMesh.cpp:94
Tick-addressed command buffering for deterministic climbing input.
std::map< std::string, Var > values
World3D * world
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
float t
std::string filter
double current
SimulationTick tick
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
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static ClimbingCoyoteState coyoteWindowState(eve::SimulationTick currentTick, eve::SimulationTick lastGroundedTick, std::uint64_t coyoteTicks) noexcept
Evaluate a grounded grace window without reading wall clock.
static eve::Result< std::optional< BufferedClimbingCommand > > consume(ClimbingIntent &intent, ClimbingCommand command, eve::SimulationTick currentTick, ClimbingExecutionId executionId)
Atomically mark the oldest eligible command consumed by one committed execution.
static std::optional< BufferedClimbingCommand > peek(const ClimbingIntent &intent, ClimbingCommand command, eve::SimulationTick currentTick) noexcept
Observe the oldest eligible command without consuming or pruning intent.
Owner-thread climbing planner and executor for one character.
Definition Climbing.h:653
eve::Result< ClimbingCandidateSet > probe(physics::World3D &world, const ClimbingPose &pose) const
Probes, validates, and deterministically sorts all matching candidates.
Definition Climbing.cpp:166
eve::Result< ClimbingCandidate > tryBegin(physics::World3D &world, const ClimbingPose &pose, eve::SimulationTick tick)
Begins the best current candidate atomically, or returns NotFound without mutation.
static eve::Result< ClimbingStart > tryStart(ClimbingRuntime &runtime, physics::World3D &world, const ClimbingPose &pose, ClimbingIntent &intent, ClimbingCommand command, eve::SimulationTick tick, eve::SimulationTick lastGroundedTick)
Start the best candidate and consume its input edge exactly once.
void updateLatestCounters(ClimbingRuntimeCounters counters) noexcept
Replace counters on the most recent sample after a caller completes deferred hard validation.
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...
eve::detail::StrongUint64< detail::ClimbingExecutionIdTag > ClimbingExecutionId
Stable non-zero identity assigned when a climbing selection transaction commits.
ClimbingPhase
Execution lifecycle visible to gameplay and animation adapters.
Definition Climbing.h:470
ClimbingCommandRequirement
Input edge required by an action definition; Any keeps legacy automatic selection.
Definition Climbing.h:79
ClimbingCommand
Edge and held commands understood by the climbing input phase.
Hot authoritative intent component; contains no device or callback pointers.
Input pose used for one deterministic candidate probe.
Definition Climbing.h:275
static constexpr std::uint32_t CandidateProbe
bool started
Definition Graphics.cpp:183
bool consumed
Definition Graphics.cpp:184