载入中...
搜索中...
未找到
GameplayControlJson.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
5
6#include <algorithm>
7#include <limits>
8#include <set>
9
10namespace eve {
11namespace {
12
13Result<const Value::Object*> asObject(const Value& value, std::string path) {
14 const auto* object = value.getIf<Value::Object>();
15 if (!object)
17 Diagnostic::error(DiagnosticCode::ParseError, "gameplay control value must be an object", path));
19}
20
21Result<const Value*> member(const Value::Object& object, std::string_view name, Value::Type type,
22 std::string_view path) {
23 const auto found = object.find(std::string(name));
24 if (found == object.end() || found->second.type() != type)
26 "missing or invalid gameplay control field",
27 std::string(path) + "." + std::string(name)));
28 return Result<const Value*>::success(&found->second);
29}
30
31Result<std::string> stringMember(const Value::Object& object, std::string_view name,
32 std::string_view path) {
33 auto found = member(object, name, Value::Type::String, path);
34 if (!found) return Result<std::string>::failure(found.status());
35 return Result<std::string>::success(found.value()->asString());
36}
37
38Result<std::uint64_t> uintMember(const Value::Object& object, std::string_view name,
39 std::string_view path) {
40 auto found = member(object, name, Value::Type::Int64, path);
41 if (!found) return Result<std::uint64_t>::failure(found.status());
42 if (found.value()->asInt() < 0)
44 "gameplay control integer must be non-negative",
45 std::string(path) + "." + std::string(name)));
46 return Result<std::uint64_t>::success(static_cast<std::uint64_t>(found.value()->asInt()));
47}
48
49Result<void> knownFields(const Value::Object& object, const std::set<std::string>& allowed,
50 std::string_view path) {
51 for (const auto& [name, value] : object) {
52 (void)value;
53 if (!allowed.contains(name))
54 return Result<void>::failure(Diagnostic::error(DiagnosticCode::ParseError, "unknown gameplay control field",
55 std::string(path) + "." + name));
56 }
57 return Result<void>::success();
58}
59
60Result<SubjectRef> subject(std::string_view text, std::string path) {
61 const auto parsed = PersistentId::parse(text);
62 if (!parsed)
64 DiagnosticCode::ParseError, "gameplay subject must be a canonical UUID", path));
66}
67
68Result<GameplaySession> session(const Value::Object& root) {
69 const auto found = root.find("session");
70 if (found == root.end())
72 Diagnostic::error(DiagnosticCode::ParseError, "gameplay request requires a session", "request.session"));
73 auto object = asObject(found->second, "request.session");
74 if (!object) return Result<GameplaySession>::failure(object.status());
75 auto known = knownFields(*object.value(), {"access", "controlledSubjects", "id"},
76 "request.session");
77 if (!known) return Result<GameplaySession>::failure(known.status());
78 auto id = stringMember(*object.value(), "id", "request.session");
79 auto access = stringMember(*object.value(), "access", "request.session");
80 auto subjects = member(*object.value(), "controlledSubjects", Value::Type::Array, "request.session");
81 if (!id) return Result<GameplaySession>::failure(id.status());
82 if (!access) return Result<GameplaySession>::failure(access.status());
83 if (!subjects) return Result<GameplaySession>::failure(subjects.status());
84 GameplaySession result;
85 result.id = id.value();
86 if (result.id.empty())
88 DiagnosticCode::ParseError, "gameplay session id must not be empty", "request.session.id"));
89 if (access.value() == "player") result.access = GameplayAccess::PlayerEquivalent;
90 else if (access.value() == "test-driver") result.access = GameplayAccess::TestDriver;
91 else if (access.value() == "developer-cheat") result.access = GameplayAccess::DeveloperCheat;
92 else
94 Diagnostic::error(DiagnosticCode::ParseError, "unknown gameplay access profile", "request.session.access"));
95 const auto* values = subjects.value()->getIf<Value::Array>();
96 for (std::size_t index = 0; index < values->size(); ++index) {
97 if (!(*values)[index].isString())
99 Diagnostic::error(DiagnosticCode::ParseError, "controlled subject must be a UUID string",
100 "request.session.controlledSubjects." + std::to_string(index)));
101 auto parsed = subject((*values)[index].asString(),
102 "request.session.controlledSubjects." + std::to_string(index));
103 if (!parsed) return Result<GameplaySession>::failure(parsed.status());
104 result.controlledSubjects.push_back(parsed.value());
105 }
106 return Result<GameplaySession>::success(std::move(result));
107}
108
120Result<IGameplayControlProvider*> provider(std::string_view domain, SubjectRef instance) {
121 std::vector<IGameplayControlProvider*> matches;
122 cap::forEach<IGameplayControlProvider>([&](auto* candidate) {
123 if (candidate && candidate->gameplayDomain() == domain) matches.push_back(candidate);
124 });
125 if (matches.empty())
127 Diagnostic::error(DiagnosticCode::NotFound, "gameplay provider domain was not found", "request.domain"));
128 if (matches.size() == 1) return Result<IGameplayControlProvider*>::success(matches.front());
129
130 IGameplayControlProvider* claimant = nullptr;
131 int claimants = 0;
132 for (auto* candidate : matches) {
133 auto* catalog = dynamic_cast<IGameplayInstanceCatalog*>(candidate);
134 if (catalog == nullptr) continue;
135 const auto instances = catalog->gameplayInstances();
136 if (std::find(instances.begin(), instances.end(), instance) == instances.end()) continue;
137 claimant = candidate;
138 ++claimants;
139 }
140 if (claimants == 1) return Result<IGameplayControlProvider*>::success(claimant);
141 if (claimants > 1)
143 DiagnosticCode::Conflict, "several gameplay providers claim that instance", "request.instance"));
145 DiagnosticCode::Conflict, "multiple gameplay providers publish the same domain and none claims that instance",
146 "request.domain"));
147}
148
149Value encodeObservation(GameplayObservation observation) {
150 return Value(Value::Object{{"domain", Value(observation.domain.format())},
151 {"instance", Value(observation.instance.format())},
152 {"revision", Value(static_cast<std::int64_t>(observation.revision))},
153 {"state", std::move(observation.state)},
154 {"tick", Value(static_cast<std::int64_t>(observation.tick.value()))}});
155}
156
157Value encodeActions(std::vector<GameplayActionDescriptor> actions) {
159 for (auto& action : actions)
160 output.emplace_back(Value::Object{{"id", Value(action.id.format())},
161 {"parameterSchema", std::move(action.parameterSchema)}});
162 return Value(std::move(output));
163}
164
165Value encodeReceipt(GameplayCommandReceipt receipt) {
166 return Value(Value::Object{{"acceptedTick", Value(static_cast<std::int64_t>(receipt.acceptedTick.value()))},
167 {"commandId", Value(std::move(receipt.commandId))},
168 {"details", std::move(receipt.details)},
169 {"executionId", Value(std::move(receipt.executionId))},
170 {"resultingRevision", Value(static_cast<std::int64_t>(receipt.resultingRevision))}});
171}
172
173Value encodeInstances(const IGameplayInstanceCatalog* catalog) {
175 if (catalog == nullptr) return Value(std::move(output));
176 for (const auto& instance : catalog->gameplayInstances()) output.emplace_back(instance.format());
177 return Value(std::move(output));
178}
179
180Value encodeEvents(std::vector<GameplayEvent> events) {
182 for (auto& event : events)
183 output.emplace_back(Value::Object{
184 {"causationCommandId", Value(std::move(event.causationCommandId))},
185 {"correlationId", Value(std::move(event.correlationId))},
186 {"payload", std::move(event.payload)},
187 {"sequence", Value(static_cast<std::int64_t>(event.sequence))},
188 {"subject", Value(event.subject.format())},
189 {"tick", Value(static_cast<std::int64_t>(event.tick.value()))},
190 {"type", Value(std::move(event.type))},
191 });
192 return Value(std::move(output));
193}
194
195Result<void> validateRoot(const Value::Object& root) {
196 static const std::set<std::string> allowed{"afterSequence", "command", "deltaNanoseconds", "domain",
197 "instance", "op", "schemaId", "schemaVersion", "session",
198 "subject", "tick"};
199 auto known = knownFields(root, allowed, "request");
200 if (!known) return known;
201 auto schemaId = stringMember(root, "schemaId", "request");
202 auto version = member(root, "schemaVersion", Value::Type::Int64, "request");
203 if (!schemaId) return Result<void>::failure(schemaId.status());
204 if (!version) return Result<void>::failure(version.status());
205 if (schemaId.value() != "evengine.gameplay-control-request" || version.value()->asInt() != 1)
206 return Result<void>::failure(Diagnostic::error(
207 DiagnosticCode::Unsupported, "unsupported gameplay control request schema", "request.schemaVersion"));
208 return Result<void>::success();
209}
210
211Result<SubjectRef> rootSubject(const Value::Object& root, std::string_view name) {
212 auto text = stringMember(root, name, "request");
213 if (!text) return Result<SubjectRef>::failure(text.status());
214 return subject(text.value(), "request." + std::string(name));
215}
216
217} // namespace
218
220 auto root = asObject(request, "request");
221 if (!root) return Result<Value>::failure(root.status());
222 auto valid = validateRoot(*root.value());
223 if (!valid) return Result<Value>::failure(valid.status());
224 auto operation = stringMember(*root.value(), "op", "request");
225 if (!operation) return Result<Value>::failure(operation.status());
226 if (operation.value() == "domains") {
227 std::vector<std::string> domains;
228 cap::forEach<IGameplayControlProvider>([&](auto* candidate) {
229 if (candidate) domains.emplace_back(candidate->gameplayDomain());
230 });
231 std::sort(domains.begin(), domains.end());
232 // A per-instance adapter registers once per instance, so the same domain
233 // can appear several times; discovery lists it once while an operation on
234 // that domain still reports the duplicate-provider conflict.
235 domains.erase(std::unique(domains.begin(), domains.end()), domains.end());
237 for (auto& domain : domains) output.emplace_back(std::move(domain));
238 return Result<Value>::success(Value(Value::Object{{"domains", Value(std::move(output))}}));
239 }
240 if (operation.value() == "instances") {
241 // Discovery needs neither a session nor an authority check: it reports
242 // what exists, not what the caller may do with it.
243 const auto domainField = root.value()->find("domain");
244 if (domainField != root.value()->end()) {
245 if (!domainField->second.isString())
247 Diagnostic::error(DiagnosticCode::ParseError, "domain must be a string", "request.domain"));
248 const std::string wanted = domainField->second.asString();
249 IGameplayInstanceCatalog* match = nullptr;
250 cap::forEach<IGameplayInstanceCatalog>([&](auto* candidate) {
251 if (candidate && candidate->gameplayDomain() == wanted) match = candidate;
252 });
253 if (match == nullptr)
254 return Result<Value>::failure(Diagnostic::error(DiagnosticCode::Unsupported,
255 "that gameplay domain cannot enumerate its instances",
256 "request.domain"));
258 Value(Value::Object{{"domain", Value(wanted)}, {"instances", encodeInstances(match)}}));
259 }
260 std::vector<std::pair<std::string, Value>> catalogs;
261 std::vector<std::string> catalogDomains;
262 cap::forEach<IGameplayInstanceCatalog>([&](auto* candidate) {
263 if (!candidate) return;
264 const std::string domain(candidate->gameplayDomain());
265 catalogDomains.push_back(domain);
266 catalogs.emplace_back(domain, encodeInstances(candidate));
267 });
268 std::sort(catalogs.begin(), catalogs.end(),
269 [](const auto& left, const auto& right) { return left.first < right.first; });
270 Value::Array encoded;
271 for (auto& entry : catalogs)
272 encoded.emplace_back(
273 Value::Object{{"domain", Value(std::move(entry.first))}, {"instances", std::move(entry.second)}});
274 std::vector<std::string> unenumerable;
275 cap::forEach<IGameplayControlProvider>([&](auto* candidate) {
276 if (!candidate) return;
277 const std::string domain(candidate->gameplayDomain());
278 if (std::find(catalogDomains.begin(), catalogDomains.end(), domain) == catalogDomains.end())
279 unenumerable.push_back(domain);
280 });
281 std::sort(unenumerable.begin(), unenumerable.end());
282 Value::Array missing;
283 for (auto& domain : unenumerable) missing.emplace_back(std::move(domain));
284 return Result<Value>::success(
285 Value(Value::Object{{"catalogs", Value(std::move(encoded))}, {"unenumerable", Value(std::move(missing))}}));
286 }
287 auto domain = stringMember(*root.value(), "domain", "request");
288 if (!domain) return Result<Value>::failure(domain.status());
289 auto access = session(*root.value());
290 if (!access) return Result<Value>::failure(access.status());
291 auto instance = rootSubject(*root.value(), "instance");
292 if (!instance) return Result<Value>::failure(instance.status());
293 // Routing needs the instance, so it resolves after the instance is parsed: when
294 // several providers publish one domain, the owning catalogue decides.
295 auto target = provider(domain.value(), instance.value());
296 if (!target) return Result<Value>::failure(target.status());
297
298 if (operation.value() == "observe") {
299 auto result = target.value()->observeGameplay(access.value(), instance.value());
300 if (!result) return Result<Value>::failure(result.status());
301 return Result<Value>::success(encodeObservation(std::move(result).takeValue()));
302 }
303 if (operation.value() == "actions") {
304 auto actor = rootSubject(*root.value(), "subject");
305 if (!actor) return Result<Value>::failure(actor.status());
306 auto result = target.value()->availableGameplayActions(access.value(), instance.value(), actor.value());
307 if (!result) return Result<Value>::failure(result.status());
308 return Result<Value>::success(Value(Value::Object{{"actions", encodeActions(std::move(result).takeValue())}}));
309 }
310 if (operation.value() == "submit") {
311 const auto found = root.value()->find("command");
312 if (found == root.value()->end())
313 return Result<Value>::failure(
314 Diagnostic::error(DiagnosticCode::ParseError, "submit requires a command", "request.command"));
315 auto commandObject = asObject(found->second, "request.command");
316 if (!commandObject) return Result<Value>::failure(commandObject.status());
317 auto known = knownFields(*commandObject.value(),
318 {"action", "expectedRevision", "id", "observedTick", "parameters", "subject"},
319 "request.command");
320 if (!known) return Result<Value>::failure(known.status());
321 auto commandId = stringMember(*commandObject.value(), "id", "request.command");
322 auto actionText = stringMember(*commandObject.value(), "action", "request.command");
323 auto actorText = stringMember(*commandObject.value(), "subject", "request.command");
324 auto observedTick = uintMember(*commandObject.value(), "observedTick", "request.command");
325 auto expectedRevision = uintMember(*commandObject.value(), "expectedRevision", "request.command");
326 const auto parameters = commandObject.value()->find("parameters");
327 if (!commandId) return Result<Value>::failure(commandId.status());
328 if (!actionText) return Result<Value>::failure(actionText.status());
329 if (!actorText) return Result<Value>::failure(actorText.status());
330 if (!observedTick) return Result<Value>::failure(observedTick.status());
331 if (!expectedRevision) return Result<Value>::failure(expectedRevision.status());
332 if (parameters == commandObject.value()->end())
333 return Result<Value>::failure(Diagnostic::error(
334 DiagnosticCode::ParseError, "submit command requires parameters", "request.command.parameters"));
335 const auto parsedAction = LogicalId::parse(actionText.value());
336 if (!parsedAction)
337 return Result<Value>::failure(Diagnostic::error(
338 DiagnosticCode::ParseError, "command action must be a logical id", "request.command.action"));
339 auto actor = subject(actorText.value(), "request.command.subject");
340 if (!actor) return Result<Value>::failure(actor.status());
341 GameplayCommand command;
342 command.id = commandId.value();
343 command.action = *parsedAction;
344 command.subject = actor.value();
345 command.observedTick = SimulationTick(observedTick.value());
346 command.expectedRevision = expectedRevision.value();
347 command.parameters = parameters->second;
348 auto result = target.value()->submitGameplay(access.value(), instance.value(), command);
349 if (!result) return Result<Value>::failure(result.status());
350 return Result<Value>::success(Value(Value::Object{{"receipt", encodeReceipt(std::move(result).takeValue())}}));
351 }
352 if (operation.value() == "advance") {
353 auto tick = uintMember(*root.value(), "tick", "request");
354 auto delta = member(*root.value(), "deltaNanoseconds", Value::Type::Int64, "request");
355 if (!tick) return Result<Value>::failure(tick.status());
356 if (!delta) return Result<Value>::failure(delta.status());
357 if (delta.value()->asInt() < 0)
358 return Result<Value>::failure(Diagnostic::error(DiagnosticCode::ParseError, "delta must be non-negative",
359 "request.deltaNanoseconds"));
360 SimulationStep step{SimulationTick(tick.value()), Duration::fromNanoseconds(delta.value()->asInt())};
361 auto result = target.value()->advanceGameplay(access.value(), instance.value(), step);
362 if (!result) return Result<Value>::failure(result.status());
363 return Result<Value>::success(encodeObservation(std::move(result).takeValue()));
364 }
365 if (operation.value() == "events") {
366 auto cursor = uintMember(*root.value(), "afterSequence", "request");
367 if (!cursor) return Result<Value>::failure(cursor.status());
368 auto result = target.value()->gameplayEvents(access.value(), instance.value(), cursor.value());
369 if (!result) return Result<Value>::failure(result.status());
370 return Result<Value>::success(Value(Value::Object{{"events", encodeEvents(std::move(result).takeValue())}}));
371 }
372 return Result<Value>::failure(
373 Diagnostic::error(DiagnosticCode::Unsupported, "unsupported gameplay control operation", "request.op"));
374}
375
376Result<std::string> executeGameplayControlJson(std::string_view requestJson) {
377 auto request = Value::fromJson(requestJson);
378 if (!request) return Result<std::string>::failure(request.status());
379 auto response = executeGameplayControlRequest(request.value());
380 if (!response) return Result<std::string>::failure(response.status());
381 return response.value().toJson();
382}
383
384} // namespace eve
LogicalId target
ActionParameterOperation operation
double value
int root
Definition AnimSmr.cpp:119
int subject
Definition AnimSmr.cpp:163
std::string output
std::vector< BuildingInstanceSnapshot > instances
std::map< std::string, Var > values
Versioned JSON facade shared by scripting, MCP and scenario tooling.
Optional companion surface for gameplay providers that can enumerate instances.
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::string text
std::string name
bool valid
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
bool found
Battle::Events events
SimulationTick tick
std::size_t cursor
std::optional< InteractionSession > session
Definition Tactics.cpp:39
Json object
float step
Definition TreeMesh.cpp:314
float(ui::Theme::* member)[4]
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
Optional read-only discovery surface implemented beside a gameplay provider.
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 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
std::map< std::string, Value > Object
Definition Value.h:34
Type
Runtime kind of the owned value.
Definition Value.h:38
std::vector< Value > Array
Definition Value.h:33
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
Definition Identity.h:151
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
eve::SimulationStep SimulationStep
Compatibility alias for the common injected simulation step.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Result< Value > executeGameplayControlRequest(const Value &request)
Routes one versioned owning request to the uniquely matching gameplay provider.
Result< std::string > executeGameplayControlJson(std::string_view requestJson)
Parse, execute and serialize one gameplay-control JSON request.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31