载入中...
搜索中...
未找到
SettlementReplay.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <charconv>
5#include <cmath>
6#include <limits>
7#include <utility>
8
9namespace eve::settlement {
10namespace {
11
12template <class T>
13eve::Result<T> failure(eve::DiagnosticCode code, std::string message, std::string path = {}) {
15}
16
17Value::Array strings(const std::vector<std::string>& values) {
18 Value::Array encoded;
19 encoded.reserve(values.size());
20 for (const auto& value : values) encoded.emplace_back(value);
21 return encoded;
22}
23
24Value encodeDecision(const SettlementRandomDecision& decision) {
25 return Value::Object{{"accepted", decision.accepted},
26 {"sample", decision.sample},
27 {"sequence", std::to_string(decision.sequence)},
28 {"stream", decision.stream.format()},
29 {"threshold", decision.threshold}};
30}
31
32Value encodeRequest(const SettlementRequest& request) {
33 Value::Array decisions;
34 decisions.reserve(request.decisions.size());
35 for (const auto& decision : request.decisions) decisions.push_back(encodeDecision(decision));
36
37 return Value::Object{
38 {"causation", request.causation.format()},
39 {"causationKind", static_cast<std::int64_t>(request.causation.kind())},
40 {"chain",
41 Value::Object{{"depth", static_cast<std::int64_t>(request.chain.depth)},
42 {"emittedCount", static_cast<std::int64_t>(request.chain.emittedCount)},
43 {"triggerPath", strings(request.chain.triggerPath)}}},
44 {"context", request.context},
45 {"correlation", request.correlation.format()},
46 {"correlationKind", static_cast<std::int64_t>(request.correlation.kind())},
47 {"decisions", std::move(decisions)},
48 {"kind", request.kind},
49 {"magnitude", request.magnitude},
50 {"resource", request.resource},
51 {"source", request.source.format()},
52 {"tags", strings(request.tags)},
53 {"target", request.target.format()},
54 {"tick", std::to_string(request.tick.value())},
55 {"trace", static_cast<std::int64_t>(request.trace)},
56 {"trigger", request.trigger},
57 };
58}
59
60Value encodeStage(const SettlementStageResult& stage) {
61 return Value::Object{{"after", stage.after},
62 {"before", stage.before},
63 {"details", stage.details},
64 {"kind", stageKindName(stage.kind)},
65 {"name", stage.name},
66 {"status", static_cast<std::int64_t>(stage.status)}};
67}
68
69eve::Result<Value> encodeEvent(const game_event::GameEvent& event) {
70 auto payload = Value::fromJson(event.payload);
71 if (!payload) return eve::Result<Value>::failure(payload.status());
73 {"causation", event.causation.format()},
74 {"causationKind", static_cast<std::int64_t>(event.causation.kind())},
75 {"correlation", event.correlation.format()},
76 {"correlationKind", static_cast<std::int64_t>(event.correlation.kind())},
77 {"flags", static_cast<std::int64_t>(event.flags)},
78 {"payload", std::move(payload).takeValue()},
79 {"schema", event.schemaId.format()},
80 {"schemaVersion", std::to_string(event.schemaVersion.value())},
81 {"source", event.source},
82 {"subject", event.subject},
83 {"tick", std::to_string(event.tick.value())},
84 {"type", event.type},
85 });
86}
87
88eve::Result<Value> encodeResult(const SettlementResult& result) {
89 Value::Array stages;
90 stages.reserve(result.stages.size());
91 for (const auto& stage : result.stages) stages.push_back(encodeStage(stage));
92
93 Value event;
94 if (result.event) {
95 auto encoded = encodeEvent(*result.event);
96 if (!encoded) return eve::Result<Value>::failure(encoded.status());
97 event = std::move(encoded).takeValue();
98 }
99
100 Value::Array derived;
101 derived.reserve(result.derived.size());
102 for (const auto& request : result.derived) derived.push_back(encodeRequest(request));
103
105 {"payload",
106 Value::Object{{"absorbed", result.absorbed},
107 {"applied", result.applied},
108 {"clamped", result.clamped},
109 {"critical", result.critical},
110 {"derived", std::move(derived)},
111 {"disposition", settlementDispositionName(result.disposition)},
112 {"event", std::move(event)},
113 {"requested", result.requested},
114 {"resisted", result.resisted},
115 {"stages", std::move(stages)},
116 {"tick", std::to_string(result.tick.value())}}},
117 {"schema", "settlement.result"},
118 {"version", 1},
119 });
120}
121
122template <class T>
123void compareField(const T& expected, const T& actual, std::string path, std::vector<std::string>& differences) {
124 if (expected != actual) differences.push_back(std::move(path));
125}
126
127eve::Result<const Value::Object*> strictObject(const Value& value, std::initializer_list<std::string_view> keys,
128 const std::string& path) {
129 const auto* object = value.getIf<Value::Object>();
130 if (object == nullptr)
131 return failure<const Value::Object*>(eve::DiagnosticCode::TypeMismatch, "expected object", path);
132 if (object->size() != keys.size())
133 return failure<const Value::Object*>(eve::DiagnosticCode::InvalidArgument,
134 "object contains missing or unknown fields", path);
135 for (const auto key : keys)
136 if (!object->contains(std::string(key)))
137 return failure<const Value::Object*>(eve::DiagnosticCode::InvalidArgument,
138 "object contains missing or unknown fields", path);
140}
141
142eve::Result<std::string> stringField(const Value::Object& object, std::string_view key, const std::string& path) {
143 const auto* value = object.at(std::string(key)).getIf<std::string>();
144 if (value == nullptr)
145 return failure<std::string>(eve::DiagnosticCode::TypeMismatch, "expected string",
146 path + "." + std::string(key));
148}
149
150eve::Result<std::int64_t> integerField(const Value::Object& object, std::string_view key,
151 const std::string& path) {
152 const auto* value = object.at(std::string(key)).getIf<std::int64_t>();
153 if (value == nullptr)
154 return failure<std::int64_t>(eve::DiagnosticCode::TypeMismatch, "expected integer",
155 path + "." + std::string(key));
157}
158
159eve::Result<double> numberField(const Value::Object& object, std::string_view key, const std::string& path) {
160 const auto& field = object.at(std::string(key));
161 if (const auto* value = field.getIf<double>()) return eve::Result<double>::success(*value);
162 if (const auto* value = field.getIf<std::int64_t>())
163 return eve::Result<double>::success(static_cast<double>(*value));
164 return failure<double>(eve::DiagnosticCode::TypeMismatch, "expected number", path + "." + std::string(key));
165}
166
167eve::Result<std::uint64_t> unsignedText(std::string_view text, const std::string& path) {
168 std::uint64_t value = 0;
169 const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
170 if (error != std::errc{} || end != text.data() + text.size())
171 return failure<std::uint64_t>(eve::DiagnosticCode::ParseError, "expected unsigned decimal text", path);
173}
174
175eve::Result<std::vector<std::string>> stringArray(const Value& value, const std::string& path) {
176 const auto* array = value.getIf<Value::Array>();
177 if (array == nullptr)
178 return failure<std::vector<std::string>>(eve::DiagnosticCode::TypeMismatch, "expected string array", path);
179 std::vector<std::string> decoded;
180 decoded.reserve(array->size());
181 for (std::size_t index = 0; index < array->size(); ++index) {
182 const auto* item = (*array)[index].getIf<std::string>();
183 if (item == nullptr)
184 return failure<std::vector<std::string>>(eve::DiagnosticCode::TypeMismatch, "expected string",
185 path + "[" + std::to_string(index) + "]");
186 decoded.push_back(*item);
187 }
188 return eve::Result<std::vector<std::string>>::success(std::move(decoded));
189}
190
191eve::Status locateRequestStatus(const eve::Status& status, const std::string& root) {
192 std::vector<eve::Diagnostic> diagnostics;
193 diagnostics.reserve(status.diagnostics().size());
194 for (const auto& diagnostic : status.diagnostics()) {
195 std::string path = root;
196 if (!diagnostic.path().empty()) path += "." + diagnostic.path();
197 diagnostics.emplace_back(diagnostic.code(), diagnostic.severity(), diagnostic.message(), std::move(path),
198 diagnostic.details(), diagnostic.source());
199 }
200 return eve::Status(status.code(), std::move(diagnostics));
201}
202
203bool knownStageKind(std::string_view value) noexcept {
204 return value == "validate" || value == "decision" || value == "source_modifiers" ||
205 value == "target_mitigation" || value == "armor_shield" || value == "clamp" || value == "apply" ||
206 value == "trigger" || value == "event";
207}
208
209bool knownDisposition(std::string_view value) noexcept {
210 return value == "applied" || value == "no_op" || value == "immune" || value == "resisted" ||
211 value == "partially_applied" || value == "blocked" || value == "invalid_target";
212}
213
214eve::Result<SettlementRequest> decodeRequest(const Value& value, const std::string& path,
215 bool emittedTemplate = false) {
216 auto checked = strictObject(value,
217 {"causation", "causationKind", "chain", "context", "correlation",
218 "correlationKind", "decisions", "kind", "magnitude", "resource", "source",
219 "tags", "target", "tick", "trace", "trigger"},
220 path);
221 if (!checked) return eve::Result<SettlementRequest>::failure(checked.status());
222 const auto& object = *checked.value();
223
224 SettlementRequest request;
225 auto sourceText = stringField(object, "source", path);
226 auto targetText = stringField(object, "target", path);
227 auto kind = stringField(object, "kind", path);
228 auto resource = stringField(object, "resource", path);
229 auto trigger = stringField(object, "trigger", path);
230 auto tickText = stringField(object, "tick", path);
231 auto magnitude = numberField(object, "magnitude", path);
232 auto trace = integerField(object, "trace", path);
233 if (!sourceText) return eve::Result<SettlementRequest>::failure(sourceText.status());
234 if (!targetText) return eve::Result<SettlementRequest>::failure(targetText.status());
236 if (!resource) return eve::Result<SettlementRequest>::failure(resource.status());
237 if (!trigger) return eve::Result<SettlementRequest>::failure(trigger.status());
238 if (!tickText) return eve::Result<SettlementRequest>::failure(tickText.status());
239 if (!magnitude) return eve::Result<SettlementRequest>::failure(magnitude.status());
241
242 const auto sourceId = eve::PersistentId::parse(sourceText.value());
243 const auto targetId = eve::PersistentId::parse(targetText.value());
244 if (!sourceId)
245 return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid source subject id",
246 path + ".source");
247 if (!targetId)
248 return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid target subject id",
249 path + ".target");
252 request.kind = std::move(kind).takeValue();
253 request.resource = std::move(resource).takeValue();
254 request.trigger = std::move(trigger).takeValue();
255 request.magnitude = magnitude.value();
256 if (trace.value() < 0 || trace.value() > static_cast<std::int64_t>(SettlementTraceLevel::Full))
257 return failure<SettlementRequest>(eve::DiagnosticCode::InvalidArgument, "unknown trace level",
258 path + ".trace");
259 request.trace = static_cast<SettlementTraceLevel>(trace.value());
260 auto tick = unsignedText(tickText.value(), path + ".tick");
262 request.tick = SimulationTick(tick.value());
263 request.context = object.at("context");
264
265 auto tags = stringArray(object.at("tags"), path + ".tags");
266 if (!tags) return eve::Result<SettlementRequest>::failure(tags.status());
267 request.tags = std::move(tags).takeValue();
268
269 auto chainObject = strictObject(object.at("chain"), {"depth", "emittedCount", "triggerPath"}, path + ".chain");
270 if (!chainObject) return eve::Result<SettlementRequest>::failure(chainObject.status());
271 auto depth = integerField(*chainObject.value(), "depth", path + ".chain");
272 auto emitted = integerField(*chainObject.value(), "emittedCount", path + ".chain");
274 if (!emitted) return eve::Result<SettlementRequest>::failure(emitted.status());
275 if (depth.value() < 0 || depth.value() > std::numeric_limits<std::uint32_t>::max() || emitted.value() < 0 ||
276 emitted.value() > std::numeric_limits<std::uint32_t>::max())
277 return failure<SettlementRequest>(eve::DiagnosticCode::InvalidArgument, "chain counter is out of range",
278 path + ".chain");
279 request.chain.depth = static_cast<std::uint32_t>(depth.value());
280 request.chain.emittedCount = static_cast<std::uint32_t>(emitted.value());
281 auto triggerPath = stringArray(chainObject.value()->at("triggerPath"), path + ".chain.triggerPath");
282 if (!triggerPath) return eve::Result<SettlementRequest>::failure(triggerPath.status());
283 request.chain.triggerPath = std::move(triggerPath).takeValue();
284
285 const auto* decisions = object.at("decisions").getIf<Value::Array>();
286 if (decisions == nullptr)
287 return failure<SettlementRequest>(eve::DiagnosticCode::TypeMismatch, "expected decision array",
288 path + ".decisions");
289 request.decisions.reserve(decisions->size());
290 for (std::size_t index = 0; index < decisions->size(); ++index) {
291 const auto itemPath = path + ".decisions[" + std::to_string(index) + "]";
292 auto decisionObject = strictObject((*decisions)[index], {"accepted", "sample", "sequence", "stream",
293 "threshold"}, itemPath);
294 if (!decisionObject) return eve::Result<SettlementRequest>::failure(decisionObject.status());
295 auto streamText = stringField(*decisionObject.value(), "stream", itemPath);
296 auto sequenceText = stringField(*decisionObject.value(), "sequence", itemPath);
297 auto sample = numberField(*decisionObject.value(), "sample", itemPath);
298 auto threshold = numberField(*decisionObject.value(), "threshold", itemPath);
299 const auto* accepted = decisionObject.value()->at("accepted").getIf<bool>();
300 if (!streamText) return eve::Result<SettlementRequest>::failure(streamText.status());
301 if (!sequenceText) return eve::Result<SettlementRequest>::failure(sequenceText.status());
302 if (!sample) return eve::Result<SettlementRequest>::failure(sample.status());
303 if (!threshold) return eve::Result<SettlementRequest>::failure(threshold.status());
304 if (accepted == nullptr)
305 return failure<SettlementRequest>(eve::DiagnosticCode::TypeMismatch, "expected boolean",
306 itemPath + ".accepted");
307 const auto stream = LogicalId::parse(streamText.value());
308 if (!stream)
309 return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid decision stream id",
310 itemPath + ".stream");
311 auto sequence = unsignedText(sequenceText.value(), itemPath + ".sequence");
313 request.decisions.push_back({*stream, sequence.value(), sample.value(), threshold.value(), *accepted});
314 }
315
316 auto causationText = stringField(object, "causation", path);
317 auto causationKind = integerField(object, "causationKind", path);
318 auto correlationText = stringField(object, "correlation", path);
319 auto correlationKind = integerField(object, "correlationKind", path);
320 if (!causationText) return eve::Result<SettlementRequest>::failure(causationText.status());
321 if (!causationKind) return eve::Result<SettlementRequest>::failure(causationKind.status());
322 if (!correlationText) return eve::Result<SettlementRequest>::failure(correlationText.status());
323 if (!correlationKind) return eve::Result<SettlementRequest>::failure(correlationKind.status());
324 if (causationKind.value() == static_cast<std::int64_t>(game_event::CausationRef::Kind::Event)) {
325 const auto id = game_event::EventId::parse(causationText.value());
326 if (!id) return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid causation event id",
327 path + ".causation");
329 } else if (causationKind.value() == static_cast<std::int64_t>(game_event::CausationRef::Kind::Command)) {
330 const auto id = game_event::CommandId::parse(causationText.value());
331 if (!id) return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid causation command id",
332 path + ".causation");
334 } else if (causationKind.value() != static_cast<std::int64_t>(game_event::CausationRef::Kind::None) ||
335 !causationText.value().empty()) {
336 return failure<SettlementRequest>(eve::DiagnosticCode::InvalidArgument, "unsupported causation form",
337 path + ".causationKind");
338 }
339 if (correlationKind.value() == static_cast<std::int64_t>(game_event::CorrelationId::Kind::Id)) {
340 const auto id = game_event::EventId::parse(correlationText.value());
341 if (!id) return failure<SettlementRequest>(eve::DiagnosticCode::ParseError, "invalid correlation id",
342 path + ".correlation");
344 } else if (correlationKind.value() != static_cast<std::int64_t>(game_event::CorrelationId::Kind::None) ||
345 !correlationText.value().empty()) {
346 return failure<SettlementRequest>(eve::DiagnosticCode::InvalidArgument, "unsupported correlation form",
347 path + ".correlationKind");
348 }
349 auto executable = request;
350 if (emittedTemplate && executable.chain.depth == 0 && !executable.trigger.empty()) {
351 executable.chain.depth = 1;
352 executable.chain.emittedCount = 1;
353 executable.chain.triggerPath = {executable.trigger};
354 }
355 auto valid = validateSettlementRequest(executable);
356 if (!valid) return eve::Result<SettlementRequest>::failure(locateRequestStatus(valid.status(), path));
358}
359
360eve::Result<void> validateResultShape(const Value& value, const std::string& path) {
361 auto result = strictObject(value, {"payload", "schema", "version"}, path);
362 if (!result) return eve::Result<void>::failure(result.status());
363 auto schema = stringField(*result.value(), "schema", path);
364 auto version = integerField(*result.value(), "version", path);
365 if (!schema) return eve::Result<void>::failure(schema.status());
366 if (!version) return eve::Result<void>::failure(version.status());
367 if (schema.value() != "settlement.result")
368 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unexpected settlement result schema",
369 path + ".schema");
370 if (version.value() != 1)
371 return failure<void>(eve::DiagnosticCode::UnknownVersion, "unsupported settlement result version",
372 path + ".version");
373
374 auto payload = strictObject(result.value()->at("payload"),
375 {"absorbed", "applied", "clamped", "critical", "derived", "disposition", "event",
376 "requested", "resisted", "stages", "tick"},
377 path + ".payload");
378 if (!payload) return eve::Result<void>::failure(payload.status());
379 for (const auto field : {"requested", "applied", "absorbed", "resisted", "clamped"}) {
380 auto number = numberField(*payload.value(), field, path + ".payload");
381 if (!number) return eve::Result<void>::failure(number.status());
382 if (!std::isfinite(number.value()) || number.value() < 0.0)
383 return failure<void>(eve::DiagnosticCode::InvalidArgument, "expected finite non-negative number",
384 path + ".payload." + field);
385 }
386 if (payload.value()->at("critical").getIf<bool>() == nullptr)
387 return failure<void>(eve::DiagnosticCode::TypeMismatch, "expected boolean", path + ".payload.critical");
388 auto disposition = stringField(*payload.value(), "disposition", path + ".payload");
389 if (!disposition) return eve::Result<void>::failure(disposition.status());
390 if (!knownDisposition(disposition.value()))
391 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unknown settlement disposition",
392 path + ".payload.disposition");
393 auto tickText = stringField(*payload.value(), "tick", path + ".payload");
394 if (!tickText) return eve::Result<void>::failure(tickText.status());
395 auto tick = unsignedText(tickText.value(), path + ".payload.tick");
396 if (!tick) return eve::Result<void>::failure(tick.status());
397 const auto* stages = payload.value()->at("stages").getIf<Value::Array>();
398 if (stages == nullptr)
399 return failure<void>(eve::DiagnosticCode::TypeMismatch, "expected stage array", path + ".payload.stages");
400 for (std::size_t index = 0; index < stages->size(); ++index) {
401 auto stage = strictObject((*stages)[index], {"after", "before", "details", "kind", "name", "status"},
402 path + ".payload.stages[" + std::to_string(index) + "]");
403 if (!stage) return eve::Result<void>::failure(stage.status());
404 const auto stagePath = path + ".payload.stages[" + std::to_string(index) + "]";
405 auto kind = stringField(*stage.value(), "kind", stagePath);
406 auto name = stringField(*stage.value(), "name", stagePath);
407 auto status = integerField(*stage.value(), "status", stagePath);
408 auto before = numberField(*stage.value(), "before", stagePath);
409 auto after = numberField(*stage.value(), "after", stagePath);
410 if (!kind) return eve::Result<void>::failure(kind.status());
411 if (!name) return eve::Result<void>::failure(name.status());
412 if (!status) return eve::Result<void>::failure(status.status());
413 if (!before) return eve::Result<void>::failure(before.status());
414 if (!after) return eve::Result<void>::failure(after.status());
415 if (!knownStageKind(kind.value()))
416 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unknown settlement stage kind",
417 stagePath + ".kind");
418 if (status.value() < 0 || status.value() > static_cast<std::int64_t>(eve::StatusCode::Failed))
419 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unknown settlement stage status",
420 stagePath + ".status");
421 if (!std::isfinite(before.value()) || before.value() < 0.0 || !std::isfinite(after.value()) ||
422 after.value() < 0.0)
423 return failure<void>(eve::DiagnosticCode::InvalidArgument,
424 "stage magnitudes must be finite and non-negative", stagePath);
425 if (stage.value()->at("details").getIf<Value::Object>() == nullptr)
426 return failure<void>(eve::DiagnosticCode::TypeMismatch, "expected details object",
427 stagePath + ".details");
428 }
429 const auto* derived = payload.value()->at("derived").getIf<Value::Array>();
430 if (derived == nullptr)
431 return failure<void>(eve::DiagnosticCode::TypeMismatch, "expected derived request array",
432 path + ".payload.derived");
433 for (std::size_t index = 0; index < derived->size(); ++index) {
434 auto request = decodeRequest((*derived)[index], path + ".payload.derived[" + std::to_string(index) + "]",
435 true);
436 if (!request) return eve::Result<void>::failure(request.status());
437 }
438 const auto& event = payload.value()->at("event");
439 if (!event.isNull()) {
440 auto checked = strictObject(event,
441 {"causation", "causationKind", "correlation", "correlationKind", "flags",
442 "payload", "schema", "schemaVersion", "source", "subject", "tick", "type"},
443 path + ".payload.event");
444 if (!checked) return eve::Result<void>::failure(checked.status());
445 const auto eventPath = path + ".payload.event";
446 for (const auto field : {"causation", "correlation", "schema", "schemaVersion", "source", "subject",
447 "tick", "type"}) {
448 auto text = stringField(*checked.value(), field, eventPath);
449 if (!text) return eve::Result<void>::failure(text.status());
450 }
451 for (const auto field : {"causationKind", "correlationKind", "flags"}) {
452 auto integer = integerField(*checked.value(), field, eventPath);
453 if (!integer) return eve::Result<void>::failure(integer.status());
454 if (integer.value() < 0)
455 return failure<void>(eve::DiagnosticCode::InvalidArgument, "expected non-negative integer",
456 eventPath + "." + field);
457 }
458 const auto causationKind = checked.value()->at("causationKind").asInt();
459 const auto correlationKind = checked.value()->at("correlationKind").asInt();
460 if (causationKind > static_cast<std::int64_t>(game_event::CausationRef::Kind::Command))
461 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unknown event causation kind",
462 eventPath + ".causationKind");
463 if (correlationKind > static_cast<std::int64_t>(game_event::CorrelationId::Kind::Id))
464 return failure<void>(eve::DiagnosticCode::InvalidArgument, "unknown event correlation kind",
465 eventPath + ".correlationKind");
466 for (const auto field : {"schemaVersion", "tick"}) {
467 auto text = stringField(*checked.value(), field, eventPath);
468 auto number = unsignedText(text.value(), eventPath + "." + field);
469 if (!number) return eve::Result<void>::failure(number.status());
470 }
471 if (checked.value()->at("payload").getIf<Value::Object>() == nullptr)
472 return failure<void>(eve::DiagnosticCode::TypeMismatch, "expected event payload object",
473 eventPath + ".payload");
474 }
476}
477
478eve::Result<Value> parseReplayRecord(std::string_view json) {
479 auto parsed = Value::fromJson(json);
480 if (!parsed) return eve::Result<Value>::failure(parsed.status());
481 auto root = std::move(parsed).takeValue();
482 auto checked = strictObject(root, {"request", "result", "resultDigest", "ruleDigest", "schema", "version"},
483 "record");
484 if (!checked) return eve::Result<Value>::failure(checked.status());
485 auto schema = stringField(*checked.value(), "schema", "record");
486 auto version = integerField(*checked.value(), "version", "record");
487 if (!schema) return eve::Result<Value>::failure(schema.status());
488 if (!version) return eve::Result<Value>::failure(version.status());
489 if (schema.value() != "settlement.replay")
490 return failure<Value>(eve::DiagnosticCode::InvalidArgument, "unexpected replay schema", "record.schema");
491 if (version.value() != 1)
492 return failure<Value>(eve::DiagnosticCode::UnknownVersion, "unsupported settlement replay version",
493 "record.version");
494 for (const auto field : {"ruleDigest", "resultDigest"}) {
495 auto digestText = stringField(*checked.value(), field, "record");
496 if (!digestText) return eve::Result<Value>::failure(digestText.status());
497 const auto digest = eve::ContentId::parse(digestText.value());
498 if (!digest || digest->isNil())
499 return failure<Value>(eve::DiagnosticCode::ParseError, "invalid content digest",
500 "record." + std::string(field));
501 }
502 auto request = decodeRequest(checked.value()->at("request"), "record.request");
503 if (!request) return eve::Result<Value>::failure(request.status());
504 auto result = validateResultShape(checked.value()->at("result"), "record.result");
505 if (!result) return eve::Result<Value>::failure(result.status());
506 return eve::Result<Value>::success(std::move(root));
507}
508
509bool numericEquivalent(const Value& left, const Value& right) {
510 if (!left.isNumeric() || !right.isNumeric()) return false;
511 const auto number = [](const Value& value) {
512 if (const auto* integer = value.getIf<std::int64_t>()) return static_cast<double>(*integer);
513 return *value.getIf<double>();
514 };
515 return number(left) == number(right);
516}
517
518void diffValues(const Value& expected, const Value& actual, const std::string& path,
519 std::vector<std::string>& differences) {
520 if (numericEquivalent(expected, actual)) return;
521 if (expected.type() != actual.type()) {
522 differences.push_back(path);
523 return;
524 }
525 if (const auto* expectedObject = expected.getIf<Value::Object>()) {
526 const auto* actualObject = actual.getIf<Value::Object>();
527 for (const auto& [key, value] : *expectedObject) {
528 const auto found = actualObject->find(key);
529 if (found == actualObject->end()) differences.push_back(path + "." + key);
530 else diffValues(value, found->second, path + "." + key, differences);
531 }
532 for (const auto& [key, value] : *actualObject)
533 if (!expectedObject->contains(key)) differences.push_back(path + "." + key);
534 return;
535 }
536 if (const auto* expectedArray = expected.getIf<Value::Array>()) {
537 const auto* actualArray = actual.getIf<Value::Array>();
538 if (expectedArray->size() != actualArray->size()) differences.push_back(path + ".size");
539 const auto count = std::min(expectedArray->size(), actualArray->size());
540 for (std::size_t index = 0; index < count; ++index)
541 diffValues((*expectedArray)[index], (*actualArray)[index],
542 path + "[" + std::to_string(index) + "]", differences);
543 return;
544 }
545 if (expected != actual) differences.push_back(path);
546}
547
548} // namespace
549
551 auto encoded = encodeResult(result);
552 if (!encoded) return eve::Result<std::string>::failure(encoded.status());
553 return std::move(encoded).takeValue().toJson();
554}
555
557 const eve::SnapshotHashProvider& hashProvider) {
558 if (!hashProvider)
559 return failure<eve::ContentId>(eve::DiagnosticCode::PreconditionViolation,
560 "settlement result digest requires a hash provider", "hashProvider");
561 auto canonical = settlementResultCanonicalJson(result);
562 if (!canonical) return eve::Result<eve::ContentId>::failure(canonical.status());
563 return hashProvider(canonical.value());
564}
565
567 const SettlementResult& actual) {
568 std::vector<std::string> differences;
569 compareField(expected.requested, actual.requested, "requested", differences);
570 compareField(expected.applied, actual.applied, "applied", differences);
571 compareField(expected.absorbed, actual.absorbed, "absorbed", differences);
572 compareField(expected.resisted, actual.resisted, "resisted", differences);
573 compareField(expected.clamped, actual.clamped, "clamped", differences);
574 compareField(expected.critical, actual.critical, "critical", differences);
575 compareField(expected.disposition, actual.disposition, "disposition", differences);
576 compareField(expected.tick, actual.tick, "tick", differences);
577
578 if (expected.stages.size() != actual.stages.size()) differences.emplace_back("stages.size");
579 const auto stageCount = std::min(expected.stages.size(), actual.stages.size());
580 for (std::size_t index = 0; index < stageCount; ++index) {
581 const auto path = "stages[" + std::to_string(index) + "]";
582 compareField(expected.stages[index].kind, actual.stages[index].kind, path + ".kind", differences);
583 compareField(expected.stages[index].name, actual.stages[index].name, path + ".name", differences);
584 compareField(expected.stages[index].status, actual.stages[index].status, path + ".status", differences);
585 compareField(expected.stages[index].before, actual.stages[index].before, path + ".before", differences);
586 compareField(expected.stages[index].after, actual.stages[index].after, path + ".after", differences);
587 compareField(expected.stages[index].details, actual.stages[index].details, path + ".details", differences);
588 }
589
590 if (expected.event.has_value() != actual.event.has_value()) {
591 differences.emplace_back("event");
592 } else if (expected.event) {
593 auto expectedEvent = encodeEvent(*expected.event);
594 if (!expectedEvent) return eve::Result<std::vector<std::string>>::failure(expectedEvent.status());
595 auto actualEvent = encodeEvent(*actual.event);
596 if (!actualEvent) return eve::Result<std::vector<std::string>>::failure(actualEvent.status());
597 if (expectedEvent.value() != actualEvent.value()) differences.emplace_back("event");
598 }
599
600 if (expected.derived.size() != actual.derived.size()) differences.emplace_back("derived.size");
601 const auto derivedCount = std::min(expected.derived.size(), actual.derived.size());
602 for (std::size_t index = 0; index < derivedCount; ++index)
603 if (encodeRequest(expected.derived[index]) != encodeRequest(actual.derived[index]))
604 differences.push_back("derived[" + std::to_string(index) + "]");
605
606 const auto code = differences.empty() ? eve::StatusCode::NoOp : eve::StatusCode::Applied;
607 return eve::Result<std::vector<std::string>>::success(std::move(differences), eve::Status::success(code));
608}
609
611 const SettlementResult& result,
612 const eve::SnapshotHashProvider& hashProvider) {
613 if (ruleDigest.isNil())
614 return failure<std::string>(eve::DiagnosticCode::InvalidArgument,
615 "settlement replay requires a non-nil rule digest", "ruleDigest");
616 if (!hashProvider)
617 return failure<std::string>(eve::DiagnosticCode::PreconditionViolation,
618 "settlement replay requires a hash provider", "hashProvider");
619 auto encodedResult = encodeResult(result);
620 if (!encodedResult) return eve::Result<std::string>::failure(encodedResult.status());
621 auto resultJson = encodedResult.value().toJson();
622 if (!resultJson) return eve::Result<std::string>::failure(resultJson.status());
623 auto resultDigest = hashProvider(resultJson.value());
624 if (!resultDigest) return eve::Result<std::string>::failure(resultDigest.status());
625 return Value(Value::Object{
626 {"request", encodeRequest(request)},
627 {"result", std::move(encodedResult).takeValue()},
628 {"resultDigest", resultDigest.value().format()},
629 {"ruleDigest", ruleDigest.format()},
630 {"schema", "settlement.replay"},
631 {"version", 1},
632 })
633 .toJson();
634}
635
637 auto record = parseReplayRecord(recordJson);
638 if (!record) return eve::Result<SettlementRequest>::failure(record.status());
639 const auto& root = *record.value().getIf<Value::Object>();
640 return decodeRequest(root.at("request"), "record.request");
641}
642
644 std::string_view recordJson, eve::ContentId ruleDigest, const SettlementResult& actual,
645 const eve::SnapshotHashProvider& hashProvider) {
646 if (!hashProvider)
647 return failure<std::vector<std::string>>(eve::DiagnosticCode::PreconditionViolation,
648 "settlement replay verification requires a hash provider",
649 "hashProvider");
650 auto record = parseReplayRecord(recordJson);
651 if (!record) return eve::Result<std::vector<std::string>>::failure(record.status());
652 const auto& root = *record.value().getIf<Value::Object>();
653
654 auto storedRuleText = stringField(root, "ruleDigest", "record");
655 auto storedResultText = stringField(root, "resultDigest", "record");
656 if (!storedRuleText) return eve::Result<std::vector<std::string>>::failure(storedRuleText.status());
657 if (!storedResultText) return eve::Result<std::vector<std::string>>::failure(storedResultText.status());
658 const auto storedRule = eve::ContentId::parse(storedRuleText.value());
659 const auto storedResult = eve::ContentId::parse(storedResultText.value());
660 if (!storedRule)
661 return failure<std::vector<std::string>>(eve::DiagnosticCode::ParseError, "invalid rule digest",
662 "record.ruleDigest");
663 if (!storedResult)
664 return failure<std::vector<std::string>>(eve::DiagnosticCode::ParseError, "invalid result digest",
665 "record.resultDigest");
666
667 auto expectedJson = root.at("result").toJson();
668 if (!expectedJson) return eve::Result<std::vector<std::string>>::failure(expectedJson.status());
669 auto verifiedDigest = hashProvider(expectedJson.value());
670 if (!verifiedDigest) return eve::Result<std::vector<std::string>>::failure(verifiedDigest.status());
671 if (verifiedDigest.value() != *storedResult)
672 return failure<std::vector<std::string>>(eve::DiagnosticCode::HashMismatch,
673 "settlement replay result payload digest mismatch",
674 "record.resultDigest");
675
676 auto encodedActual = encodeResult(actual);
677 if (!encodedActual) return eve::Result<std::vector<std::string>>::failure(encodedActual.status());
678 std::vector<std::string> differences;
679 if (*storedRule != ruleDigest) differences.emplace_back("ruleDigest");
680 diffValues(root.at("result"), encodedActual.value(), "result", differences);
681 const auto code = differences.empty() ? eve::StatusCode::NoOp : eve::StatusCode::Applied;
682 return eve::Result<std::vector<std::string>>::success(std::move(differences), eve::Status::success(code));
683}
684
685} // namespace eve::settlement
double value
Value::Object payload
Trace trace
Definition Agent.cpp:49
int root
Definition AnimSmr.cpp:119
std::map< std::string, Var > values
std::uint64_t sourceId
std::string message
DiagnosticCode code
const GltfImportRequest & request
std::uint32_t key
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::string text
TokenKind kind
std::string name
bool valid
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::uint32_t targetId
double number
bool found
std::string resource
std::string digest
std::string string
std::uint32_t count
SimulationTick tick
Json object
uint32_t index
std::uint32_t depth
std::size_t at
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 std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
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
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static SubjectRef fromPersistentId(PersistentId id) noexcept
Wrap a persistent identity without changing its bytes.
Definition SubjectRef.h:32
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Id128 public API.
Definition Identity.h:112
std::string format() const
Formats the ID in lower-case canonical UUID text.
Definition Identity.h:173
constexpr bool isNil() const noexcept
Returns whether this value is the all-zero nil ID.
Definition Identity.h:176
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
Definition Identity.h:151
const T * getIf() const
Return an immediately borrowed typed value.
static CausationRef fromEventId(EventId value)
Creates a causation reference to an event.
Definition GameEvent.h:105
static CausationRef fromCommandId(CommandId value)
Creates a causation reference to a command.
Definition GameEvent.h:115
static CorrelationId fromEventId(EventId value)
Creates a canonical correlation ID from an EventId.
Definition GameEvent.h:58
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
double sample(const Heightmap &map, double u, double v)
Sample.
const EditorValue * field(const EditorValue &value, const char *name)
const char * settlementDispositionName(SettlementDisposition disposition) noexcept
Return the stable lowercase spelling of a settlement disposition.
EVENGINE_API_FOUNDATION eve::Result< SettlementRequest > settlementReplayRequest(std::string_view recordJson)
Decode the request from a strict version-1 replay record.
EVENGINE_API_FOUNDATION eve::Result< std::vector< std::string > > verifySettlementResult(const SettlementResult &expected, const SettlementResult &actual)
Compare two settlement results and report stable field paths that differ.
const char * stageKindName(StageKind kind) noexcept
Return the stable lowercase spelling of a stage kind.
SettlementTraceLevel
Amount of stage explanation retained by one settlement request.
Definition Settlement.h:60
EVENGINE_API_FOUNDATION eve::Result< std::string > createSettlementReplayRecord(const SettlementRequest &request, eve::ContentId ruleDigest, const SettlementResult &result, const eve::SnapshotHashProvider &hashProvider)
Serialize one replayable request with its rule and expected-result identities.
eve::Result< void > validateSettlementRequest(const SettlementRequest &request)
Validate the canonical semantic invariants of a settlement request.
EVENGINE_API_FOUNDATION eve::Result< std::string > settlementResultCanonicalJson(const SettlementResult &result)
Serialize the stable semantic fields of a settlement result as canonical JSON.
EVENGINE_API_FOUNDATION eve::Result< std::vector< std::string > > verifySettlementReplayRecord(std::string_view recordJson, eve::ContentId ruleDigest, const SettlementResult &actual, const eve::SnapshotHashProvider &hashProvider)
Verify current rules and outcome against a strict replay record.
EVENGINE_API_FOUNDATION eve::Result< eve::ContentId > settlementResultDigest(const SettlementResult &result, const eve::SnapshotHashProvider &hashProvider)
Hash the canonical semantic representation of a settlement result.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
std::function< Result< ContentId >(std::string_view canonicalInput)> SnapshotHashProvider
Injected content-digest implementation used by snapshots.
Definition Snapshot.h:36
Domain-neutral, deterministic settlement pipeline.
Domain-neutral input to one settlement operation.
Definition Settlement.h:107
Complete observable outcome of one successful settlement.
Definition Settlement.h:163
std::vector< SettlementRequest > derived
Owning derived requests emitted by the policy in deterministic order.
Definition Settlement.h:175
std::vector< SettlementStageResult > stages
Definition Settlement.h:172
std::optional< game_event::GameEvent > event
Definition Settlement.h:173
SettlementDisposition disposition
Definition Settlement.h:170