载入中...
搜索中...
未找到
ClimbingPrediction.cpp
浏览该文件的文档.
1#include "climbing/Climbing.h"
2
3#include <algorithm>
4#include <charconv>
5#include <cmath>
6#include <limits>
7#include <string_view>
8#include <type_traits>
9#include <unordered_set>
10#include <utility>
11
12namespace eve::climbing {
13namespace {
14
15constexpr std::uint64_t fnvOffset = 14695981039346656037ull;
16constexpr std::uint64_t fnvPrime = 1099511628211ull;
17
18void appendByte(std::uint64_t& hash, std::uint8_t value) noexcept {
19 hash ^= value;
20 hash *= fnvPrime;
21}
22
23template <class T>
24void appendInteger(std::uint64_t& hash, T value) noexcept {
25 using Unsigned = std::make_unsigned_t<T>;
26 const Unsigned bits = static_cast<Unsigned>(value);
27 for (std::size_t index = 0; index < sizeof(T); ++index)
28 appendByte(hash, static_cast<std::uint8_t>((bits >> (index * 8)) & static_cast<Unsigned>(0xff)));
29}
30
31void appendString(std::uint64_t& hash, std::string_view value) noexcept {
32 appendInteger(hash, static_cast<std::uint64_t>(value.size()));
33 for (const unsigned char character : value) appendByte(hash, character);
34}
35
36std::int64_t quantize(float value) noexcept {
37 if (!std::isfinite(value)) return std::numeric_limits<std::int64_t>::min();
38 const double scaled = std::round(static_cast<double>(value) * 1000.0);
39 if (scaled <= static_cast<double>(std::numeric_limits<std::int64_t>::min()))
40 return std::numeric_limits<std::int64_t>::min();
41 if (scaled >= static_cast<double>(std::numeric_limits<std::int64_t>::max()))
42 return std::numeric_limits<std::int64_t>::max();
43 return static_cast<std::int64_t>(scaled);
44}
45
46void appendVec(std::uint64_t& hash, Vec3 value) noexcept {
47 appendInteger(hash, quantize(value.x));
48 appendInteger(hash, quantize(value.y));
49 appendInteger(hash, quantize(value.z));
50}
51
52const eve::Value* field(const eve::Value::Object& object, std::string_view name) {
53 const auto found = object.find(std::string(name));
54 return found == object.end() ? nullptr : &found->second;
55}
56
57bool readString(const eve::Value::Object& object, std::string_view name, std::string& output) {
58 const auto* value = field(object, name);
59 const auto* text = value ? value->getIf<std::string>() : nullptr;
60 if (!text) return false;
61 output = *text;
62 return true;
63}
64
65bool parseUint64(std::string_view text, std::uint64_t& output, int base = 10) {
66 if (text.empty()) return false;
67 const char* first = text.data();
68 const char* last = first + text.size();
69 const auto parsed = std::from_chars(first, last, output, base);
70 return parsed.ec == std::errc{} && parsed.ptr == last;
71}
72
73bool readUint64String(const eve::Value::Object& object, std::string_view name, std::uint64_t& output,
74 int base = 10) {
75 std::string text;
76 return readString(object, name, text) && parseUint64(text, output, base);
77}
78
79bool readInt64(const eve::Value::Object& object, std::string_view name, std::int64_t& output) {
80 const auto* value = field(object, name);
81 const auto* integer = value ? value->getIf<std::int64_t>() : nullptr;
82 if (!integer) return false;
83 output = *integer;
84 return true;
85}
86
87eve::Value::Object unknownFields(const eve::Value::Object& object,
88 std::initializer_list<std::string_view> known) {
89 const std::unordered_set<std::string_view> names(known.begin(), known.end());
90 eve::Value::Object result;
91 for (const auto& [name, value] : object)
92 if (!names.contains(name)) result.emplace(name, value);
93 return result;
94}
95
96std::string_view dispositionName(ClimbingPredictionDisposition value) {
97 switch (value) {
98 case ClimbingPredictionDisposition::Accepted: return "accepted";
99 case ClimbingPredictionDisposition::Corrected: return "corrected";
100 case ClimbingPredictionDisposition::Rejected: return "rejected";
101 }
102 return "rejected";
103}
104
105bool readDisposition(std::string_view text, ClimbingPredictionDisposition& output) {
107 else if (text == "corrected") output = ClimbingPredictionDisposition::Corrected;
108 else if (text == "rejected") output = ClimbingPredictionDisposition::Rejected;
109 else return false;
110 return true;
111}
112
113std::string_view reasonName(ClimbingPredictionReason value) {
114 switch (value) {
115 case ClimbingPredictionReason::None: return "none";
116 case ClimbingPredictionReason::CandidateMismatch: return "candidate_mismatch";
117 case ClimbingPredictionReason::NoCandidate: return "no_candidate";
118 case ClimbingPredictionReason::TickTooFarAhead: return "tick_too_far_ahead";
119 case ClimbingPredictionReason::RuntimeBusy: return "runtime_busy";
120 case ClimbingPredictionReason::InvalidRequest: return "invalid_request";
121 }
122 return "invalid_request";
123}
124
125bool readReason(std::string_view text, ClimbingPredictionReason& output) {
127 else if (text == "candidate_mismatch") output = ClimbingPredictionReason::CandidateMismatch;
128 else if (text == "no_candidate") output = ClimbingPredictionReason::NoCandidate;
129 else if (text == "tick_too_far_ahead") output = ClimbingPredictionReason::TickTooFarAhead;
130 else if (text == "runtime_busy") output = ClimbingPredictionReason::RuntimeBusy;
131 else if (text == "invalid_request") output = ClimbingPredictionReason::InvalidRequest;
132 else return false;
133 return true;
134}
135
136eve::Value encodeKey(const ClimbingCandidateKey& key) {
137 return eve::Value::object({
138 {"actionId", key.actionId},
139 {"kind", static_cast<std::int64_t>(key.kind)},
140 {"definitionGeneration", std::to_string(key.definitionGeneration)},
141 {"sortedRank", static_cast<std::int64_t>(key.sortedRank)},
142 {"fingerprintHex", [&key] {
143 char buffer[17]{};
144 const auto converted = std::to_chars(buffer, buffer + 16, key.fingerprint, 16);
145 return std::string(buffer, converted.ptr);
146 }()},
147 });
148}
149
150bool decodeKey(const eve::Value& value, ClimbingCandidateKey& key) {
151 const auto* object = value.getIf<eve::Value::Object>();
152 std::int64_t kind = 0;
153 std::int64_t rank = 0;
154 if (!object || !readString(*object, "actionId", key.actionId) ||
155 !readInt64(*object, "kind", kind) || kind < 0 ||
156 kind > static_cast<std::int64_t>(ClimbingActionKind::BarSwing) ||
157 !readUint64String(*object, "definitionGeneration", key.definitionGeneration) ||
158 !readInt64(*object, "sortedRank", rank) || rank < 0 ||
159 rank > std::numeric_limits<std::uint32_t>::max() ||
160 !readUint64String(*object, "fingerprintHex", key.fingerprint, 16))
161 return false;
162 key.kind = static_cast<ClimbingActionKind>(kind);
163 key.sortedRank = static_cast<std::uint32_t>(rank);
164 return true;
165}
166
167bool validKey(const ClimbingCandidateKey& key) {
168 return !key.actionId.empty() && key.definitionGeneration != 0 && key.fingerprint != 0;
169}
170
171eve::Result<void> validateSnapshotEnvelope(const eve::Value& value) {
172 const auto* object = value.getIf<eve::Value::Object>();
173 std::string schemaId;
174 std::int64_t schemaVersion = -1;
175 if (!object || !readString(*object, "schemaId", schemaId) ||
177 !readInt64(*object, "schemaVersion", schemaVersion) || schemaVersion < 0)
179 eve::DiagnosticCode::ParseError, "authoritative snapshot has an invalid climbing runtime envelope",
180 "authoritativeSnapshot", {}, "climbing.prediction"));
181 if (schemaVersion > ClimbingRuntime::SnapshotSchemaVersion)
183 eve::DiagnosticCode::UnknownVersion, "authoritative snapshot version is newer than this runtime",
184 "authoritativeSnapshot.schemaVersion", {}, "climbing.prediction"));
186}
187
188bool consistentDecision(const ClimbingPredictionDecision& decision) {
189 if (decision.sequence.isZero()) return false;
190 if (decision.disposition == ClimbingPredictionDisposition::Accepted)
191 return validKey(decision.authoritativeCandidate) && decision.reason == ClimbingPredictionReason::None;
192 if (decision.reason == ClimbingPredictionReason::None) return false;
193 return decision.disposition != ClimbingPredictionDisposition::Corrected ||
194 validKey(decision.authoritativeCandidate);
195}
196
197eve::Result<eve::Value::Object> readEnvelope(const eve::Value& value, std::string_view schemaId) {
198 const auto* object = value.getIf<eve::Value::Object>();
199 if (!object)
201 eve::DiagnosticCode::ParseError, "prediction payload must be an object", {}, {}, "climbing.prediction"));
202 std::string actualSchema;
203 std::int64_t version = 0;
204 if (!readString(*object, "schemaId", actualSchema) || actualSchema != schemaId)
206 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "prediction schemaId is missing or mismatched",
207 "schemaId", {}, "climbing.prediction"));
208 if (!readInt64(*object, "schemaVersion", version))
210 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "prediction schemaVersion must be an integer",
211 "schemaVersion", {}, "climbing.prediction"));
212 if (version != 1)
214 eve::Diagnostic::error(eve::DiagnosticCode::UnknownVersion, "prediction schema version is unsupported",
215 "schemaVersion", {}, "climbing.prediction"));
217}
218
219bool activePhase(ClimbingPhase phase) {
222}
223
224} // namespace
225
227 std::uint32_t sortedRank) noexcept {
228 std::uint64_t hash = fnvOffset;
229 appendString(hash, candidate.actionId);
230 appendInteger(hash, static_cast<std::uint8_t>(candidate.kind));
231 appendInteger(hash, candidate.definitionGeneration);
232 appendInteger(hash, sortedRank);
233 appendInteger(hash, candidate.obstacleBodyId);
234 appendInteger(hash, candidate.obstacleShapeId);
235 appendVec(hash, candidate.frontPoint);
236 appendVec(hash, candidate.bodyLocalTop);
237 appendVec(hash, candidate.bodyLocalLanding);
238 appendVec(hash, candidate.surfaceNormal);
239 appendVec(hash, candidate.surfaceTangent);
240 appendInteger(hash, quantize(candidate.obstacleHeight));
241 appendInteger(hash, quantize(candidate.obstacleDepth));
242 appendInteger(hash, quantize(candidate.gapDistance));
243 appendInteger(hash, quantize(candidate.clearanceHeight));
244 appendInteger(hash, quantize(candidate.slopeRadians));
245 appendInteger(hash, quantize(candidate.curvature));
246 appendInteger(hash, candidate.supportShapeTag);
247 appendInteger(hash, candidate.supportMaterialId);
248 appendInteger(hash, static_cast<std::uint8_t>(candidate.probeRecipe));
249 appendInteger(hash, candidate.score);
250 appendInteger(hash, static_cast<std::uint8_t>(candidate.support));
251 if (hash == 0) hash = 1;
252 return {candidate.actionId, candidate.kind, candidate.definitionGeneration, sortedRank, hash};
253}
254
256 if (request.sequence.isZero() || !validKey(request.candidate))
258 eve::DiagnosticCode::InvalidArgument, "prediction request requires a non-zero sequence and candidate key",
259 "request", {}, "climbing.prediction"));
260 eve::Value::Object object = request.extensionMetadata;
261 object["schemaId"] = eve::Value(std::string(ClimbingPredictionRequest::SchemaId));
262 object["schemaVersion"] = eve::Value(ClimbingPredictionRequest::SchemaVersion);
263 object["sequence"] = eve::Value(std::to_string(request.sequence.value()));
264 object["clientTick"] = eve::Value(std::to_string(request.clientTick.value()));
265 object["candidate"] = encodeKey(request.candidate);
266 return eve::Result<eve::Value>::success(eve::Value(std::move(object)));
267}
268
270 auto root = readEnvelope(value, ClimbingPredictionRequest::SchemaId);
272 std::uint64_t sequence = 0;
273 std::uint64_t clientTick = 0;
275 const eve::Value* candidate = field(root.value(), "candidate");
276 if (!readUint64String(root.value(), "sequence", sequence) || sequence == 0 ||
277 !readUint64String(root.value(), "clientTick", clientTick) || !candidate ||
278 !decodeKey(*candidate, request.candidate) || !validKey(request.candidate))
280 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "prediction request has missing or invalid fields",
281 "request", {}, "climbing.prediction"));
283 request.clientTick = eve::SimulationTick(clientTick);
284 request.extensionMetadata = unknownFields(root.value(),
285 {"schemaId", "schemaVersion", "sequence", "clientTick", "candidate"});
287}
288
290 if (!consistentDecision(decision))
292 "prediction decision fields are inconsistent",
293 "decision", {}, "climbing.prediction"));
294 auto snapshotEnvelope = validateSnapshotEnvelope(decision.authoritativeSnapshot);
295 if (!snapshotEnvelope) return eve::Result<eve::Value>::failure(snapshotEnvelope.status());
296 eve::Value::Object object = decision.extensionMetadata;
297 object["schemaId"] = eve::Value(std::string(ClimbingPredictionDecision::SchemaId));
298 object["schemaVersion"] = eve::Value(ClimbingPredictionDecision::SchemaVersion);
299 object["sequence"] = eve::Value(std::to_string(decision.sequence.value()));
300 object["clientTick"] = eve::Value(std::to_string(decision.clientTick.value()));
301 object["serverTick"] = eve::Value(std::to_string(decision.serverTick.value()));
302 object["disposition"] = eve::Value(std::string(dispositionName(decision.disposition)));
303 object["reason"] = eve::Value(std::string(reasonName(decision.reason)));
304 object["authoritativeCandidate"] = encodeKey(decision.authoritativeCandidate);
305 object["authoritativeSnapshot"] = decision.authoritativeSnapshot;
306 return eve::Result<eve::Value>::success(eve::Value(std::move(object)));
307}
308
310 auto root = readEnvelope(value, ClimbingPredictionDecision::SchemaId);
312 std::uint64_t sequence = 0;
313 std::uint64_t clientTick = 0;
314 std::uint64_t serverTick = 0;
315 std::string disposition;
316 std::string reason;
318 const eve::Value* key = field(root.value(), "authoritativeCandidate");
319 const eve::Value* snapshot = field(root.value(), "authoritativeSnapshot");
320 if (!readUint64String(root.value(), "sequence", sequence) || sequence == 0 ||
321 !readUint64String(root.value(), "clientTick", clientTick) ||
322 !readUint64String(root.value(), "serverTick", serverTick) ||
323 !readString(root.value(), "disposition", disposition) ||
324 !readDisposition(disposition, decision.disposition) || !readString(root.value(), "reason", reason) ||
325 !readReason(reason, decision.reason) || !key || !decodeKey(*key, decision.authoritativeCandidate) ||
326 !snapshot)
328 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "prediction decision has missing or invalid fields",
329 "decision", {}, "climbing.prediction"));
331 decision.clientTick = eve::SimulationTick(clientTick);
332 decision.serverTick = eve::SimulationTick(serverTick);
333 if (!consistentDecision(decision))
335 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "prediction decision fields are inconsistent",
336 "decision", {}, "climbing.prediction"));
337 decision.authoritativeSnapshot = *snapshot;
338 auto snapshotEnvelope = validateSnapshotEnvelope(decision.authoritativeSnapshot);
339 if (!snapshotEnvelope)
340 return eve::Result<ClimbingPredictionDecision>::failure(snapshotEnvelope.status());
341 decision.extensionMetadata = unknownFields(root.value(),
342 {"schemaId", "schemaVersion", "sequence", "clientTick", "serverTick", "disposition", "reason",
343 "authoritativeCandidate", "authoritativeSnapshot"});
344 return eve::Result<ClimbingPredictionDecision>::success(std::move(decision));
345}
346
349 eve::SimulationTick serverTick, std::uint64_t maxClientTickLead) {
351 decision.sequence = request.sequence;
352 decision.clientTick = request.clientTick;
353 decision.serverTick = serverTick;
354 const auto finish = [this](ClimbingPredictionDecision value)
356 auto state = snapshot();
358 value.authoritativeSnapshot = std::move(state).takeValue();
360 };
361
362 if (request.sequence.isZero() || !validKey(request.candidate)) {
365 return finish(std::move(decision));
366 }
367 if (request.clientTick > serverTick &&
368 request.clientTick.value() - serverTick.value() > maxClientTickLead) {
371 return finish(std::move(decision));
372 }
373 if (activePhase(phase_)) {
376 return finish(std::move(decision));
377 }
378
379 auto candidates = probe(world, pose);
380 if (!candidates) return eve::Result<ClimbingPredictionDecision>::failure(candidates.status());
381 if (candidates.value().empty()) {
384 return finish(std::move(decision));
385 }
386 decision.authoritativeCandidate = makeClimbingCandidateKey(candidates.value().front(), 0);
387 std::size_t match = candidates.value().size();
388 for (std::size_t index = 0; index < candidates.value().size(); ++index) {
389 if (index > std::numeric_limits<std::uint32_t>::max()) break;
390 if (makeClimbingCandidateKey(candidates.value()[index], static_cast<std::uint32_t>(index)) ==
391 request.candidate) {
392 match = index;
393 break;
394 }
395 }
396 if (match == candidates.value().size()) {
399 return finish(std::move(decision));
400 }
401
402 decision.authoritativeCandidate = request.candidate;
403 auto prepared = prepareBeginCandidate(world, pose, serverTick, candidates.value()[match]);
404 if (!prepared) return eve::Result<ClimbingPredictionDecision>::failure(prepared.status());
405 auto committed = commitBegin(std::move(prepared).takeValue());
406 if (!committed) return eve::Result<ClimbingPredictionDecision>::failure(committed.status());
409 return finish(std::move(decision));
410}
411
412} // namespace eve::climbing
double value
int root
Definition AnimSmr.cpp:119
std::string output
eve::EntitySpatialPose pose
float phase
Definition CaveMesh.cpp:58
Deterministic climbing/parkour planning and capsule-constrained execution.
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
const GltfImportRequest & request
std::uint32_t key
std::int32_t first
std::string text
TokenKind kind
std::string name
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
World3D * world
bool found
Json object
float size
Definition TreeMesh.cpp:156
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
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
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< ClimbingCandidateSet > probe(physics::World3D &world, const ClimbingPose &pose) const
Probes, validates, and deterministically sorts all matching candidates.
Definition Climbing.cpp:166
eve::Result< ClimbingPredictionDecision > tryBeginPredicted(physics::World3D &world, const ClimbingPose &pose, const ClimbingPredictionRequest &request, eve::SimulationTick serverTick, std::uint64_t maxClientTickLead)
Re-probes authoritative state and commits only the exact server-matched predicted candidate.
static constexpr std::string_view SnapshotSchemaId
Stable schema id for owning runtime snapshots.
Definition Climbing.h:656
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
EVENGINE_API_DOMAINS eve::Result< eve::Value > encodeClimbingPredictionRequest(const ClimbingPredictionRequest &request)
Encodes a validated prediction request to its canonical owning Value representation.
ClimbingPhase
Execution lifecycle visible to gameplay and animation adapters.
Definition Climbing.h:470
EVENGINE_API_DOMAINS eve::Result< eve::Value > encodeClimbingPredictionDecision(const ClimbingPredictionDecision &decision)
Encodes a validated authoritative decision to its canonical owning Value representation.
ClimbingPredictionReason
Stable reason for a non-accepted prediction decision.
Definition Climbing.h:421
ClimbingPredictionDisposition
Named server-authoritative outcome of one prediction request.
Definition Climbing.h:418
EVENGINE_API_DOMAINS eve::Result< ClimbingPredictionDecision > decodeClimbingPredictionDecision(const eve::Value &value)
Decodes a complete authoritative decision transactionally.
eve::detail::StrongUint64< detail::ClimbingPredictionSequenceTag > ClimbingPredictionSequence
Non-zero client command sequence used to pair one prediction decision.
Definition Climbing.h:380
ClimbingActionKind
Data-driven action family; stable action identity remains the definition id.
Definition Climbing.h:46
EVENGINE_API_DOMAINS eve::Result< ClimbingPredictionRequest > decodeClimbingPredictionRequest(const eve::Value &value)
Decodes a complete request and rejects unknown schema versions transactionally.
EVENGINE_API_DOMAINS ClimbingCandidateKey makeClimbingCandidateKey(const ClimbingCandidate &candidate, std::uint32_t sortedRank) noexcept
Creates the opaque key for one candidate at its deterministic sorted rank.
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)
Result< std::vector< uint8_t > > quantize(std::span< const float > input, float scale, int zero, bool sign)
Affine quantization to int8/uint8 bytes, saturating and rounding ties to even.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
Opaque deterministic identity of one sorted candidate; contains no client-authored position.
Definition Climbing.h:389
Owning, replayable climbing candidate; contains no borrowed query pointers.
Definition Climbing.h:290
Input pose used for one deterministic candidate probe.
Definition Climbing.h:275
Server-to-client decision carrying the authoritative runtime snapshot for reconciliation.
Definition Climbing.h:431
ClimbingPredictionSequence sequence
Definition Climbing.h:437
eve::Value authoritativeSnapshot
Canonical server runtime state after the decision, used for rollback/replay reconciliation.
Definition Climbing.h:445
static constexpr std::int64_t SchemaVersion
Current decision schema version.
Definition Climbing.h:435
ClimbingPredictionReason reason
Definition Climbing.h:441
eve::Value::Object extensionMetadata
Unknown schema fields retained across decode and encode.
Definition Climbing.h:447
ClimbingPredictionDisposition disposition
Definition Climbing.h:440
ClimbingCandidateKey authoritativeCandidate
Server-selected key; fingerprint zero means no authoritative candidate was available.
Definition Climbing.h:443
static constexpr std::string_view SchemaId
Canonical cross-process schema id.
Definition Climbing.h:433
Client-to-server request containing selection identity only, never a world-space target.
Definition Climbing.h:404
static constexpr std::int64_t SchemaVersion
Current request schema version.
Definition Climbing.h:408
static constexpr std::string_view SchemaId
Canonical cross-process schema id.
Definition Climbing.h:406