载入中...
搜索中...
未找到
AbilityAsset.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <limits>
7#include <set>
8
9namespace eve::action {
10namespace {
11
12template <typename T>
13Result<T> invalid(std::string message, std::string path) {
14 return Result<T>::failure(
16}
17
18const Value* field(const Value& value, std::string_view name) {
19 if (!value.isObject()) return nullptr;
20 const auto& object = *value.getIf<Value::Object>();
21 const auto found = object.find(std::string(name));
22 return found == object.end() ? nullptr : &found->second;
23}
24
25Result<std::string> text(const Value& value, std::string_view name, std::string path) {
26 const auto* item = field(value, name);
27 if (!item || !item->isString()) return invalid<std::string>("expected text field", std::move(path));
28 return Result<std::string>::success(item->asString());
29}
30
31Result<std::int64_t> integer(const Value& value, std::string_view name, std::string path) {
32 const auto* item = field(value, name);
33 if (!item || !item->isInt64()) return invalid<std::int64_t>("expected integer field", std::move(path));
34 return Result<std::int64_t>::success(item->asInt());
35}
36
37Result<bool> boolean(const Value& value, std::string_view name, std::string path) {
38 const auto* item = field(value, name);
39 if (!item || !item->isBool()) return invalid<bool>("expected boolean field", std::move(path));
40 return Result<bool>::success(item->asBool());
41}
42
43Result<double> number(const Value& value, std::string_view name, std::string path) {
44 const auto* item = field(value, name);
45 if (!item || (!item->isDouble() && !item->isInt64()))
46 return invalid<double>("expected numeric field", std::move(path));
47 const double result = item->isDouble() ? item->asDouble() : static_cast<double>(item->asInt());
48 if (!std::isfinite(result)) return invalid<double>("numeric field must be finite", std::move(path));
49 return Result<double>::success(result);
50}
51
52Result<void> rejectUnknown(const Value& value, const std::set<std::string, std::less<>>& known,
53 std::string path) {
54 if (!value.isObject()) return invalid<void>("expected object", std::move(path));
55 for (const auto& [name, item] : *value.getIf<Value::Object>()) {
56 (void)item;
57 if (!known.contains(name)) return invalid<void>("unknown field", path + "." + name);
58 }
59 return Result<void>::success();
60}
61
62std::string_view instancingName(AbilityInstancingPolicy value) {
63 switch (value) {
64 case AbilityInstancingPolicy::NonInstanced: return "non_instanced";
65 case AbilityInstancingPolicy::PerOwner: return "per_owner";
66 case AbilityInstancingPolicy::PerExecution: return "per_execution";
67 }
68 return "unknown";
69}
70
71Result<AbilityInstancingPolicy> parseInstancing(std::string_view value) {
72 if (value == "non_instanced")
75 if (value == "per_execution")
77 return invalid<AbilityInstancingPolicy>("unknown ability instancing policy", "instancing");
78}
79
80std::string_view groupName(AbilityActivationGroup value) {
81 switch (value) {
82 case AbilityActivationGroup::Independent: return "independent";
83 case AbilityActivationGroup::ExclusiveReplaceable: return "exclusive_replaceable";
84 case AbilityActivationGroup::ExclusiveBlocking: return "exclusive_blocking";
85 }
86 return "unknown";
87}
88
89Result<AbilityActivationGroup> parseGroup(std::string_view value) {
91 if (value == "exclusive_replaceable")
93 if (value == "exclusive_blocking")
95 return invalid<AbilityActivationGroup>("unknown ability activation group", "activationGroup");
96}
97
98std::string_view targetingModeName(TargetingMode value) {
99 switch (value) {
100 case TargetingMode::None: return "none";
101 case TargetingMode::Explicit: return "explicit";
102 case TargetingMode::Query: return "query";
103 }
104 return "unknown";
105}
106
107Result<TargetingMode> parseTargetingMode(std::string_view value) {
111 return invalid<TargetingMode>("unknown targeting mode", "action.targetingMode");
112}
113
114Value encodeCondition(const decision::Condition& condition) {
115 Value::Object object{{"kind", std::string(decision::conditionKindName(condition.kind()))}};
120 for (const auto& child : condition.children()) children.push_back(encodeCondition(child));
121 object["children"] = std::move(children);
122 }
123 switch (condition.kind()) {
125 object["key"] = condition.key();
126 object["operator"] = std::string(decision::compareOperatorName(condition.compareOperator()));
127 object["expected"] = condition.expected();
128 break;
130 object["key"] = condition.key();
131 object["expected"] = condition.expected();
132 break;
136 case decision::ConditionKind::AuthorityCheck: object["key"] = condition.key(); break;
138 object["key"] = condition.key();
139 object["arguments"] = condition.arguments();
140 if (condition.scriptDeclaration()) {
141 Value::Array dependencies;
142 for (const auto& dependency : condition.scriptDeclaration()->dependencies)
143 dependencies.emplace_back(dependency);
144 object["script"] = Value::Object{
145 {"name", condition.scriptDeclaration()->name},
146 {"dependencies", std::move(dependencies)},
147 {"determinism", static_cast<std::int64_t>(condition.scriptDeclaration()->determinism)}};
148 }
149 break;
150 }
154 }
155 return Value(std::move(object));
156}
157
158Result<std::vector<std::string>> decodeStrings(const Value& parent, std::string_view name, std::string path);
159
160Result<decision::Condition> decodeCondition(const Value& value, const std::string& path) {
161 auto kind = text(value, "kind", path + ".kind");
162 if (!kind) return Result<decision::Condition>::failure(kind.status());
163 auto children = [&]() -> Result<std::vector<decision::Condition>> {
164 const auto* source = field(value, "children");
165 if (!source || !source->isArray())
166 return invalid<std::vector<decision::Condition>>("expected children array", path + ".children");
167 std::vector<decision::Condition> result;
168 const auto& array = *source->getIf<Value::Array>();
169 result.reserve(array.size());
170 for (std::size_t index = 0; index < array.size(); ++index) {
171 auto child = decodeCondition(array[index], path + ".children[" + std::to_string(index) + "]");
172 if (!child) return Result<std::vector<decision::Condition>>::failure(child.status());
173 result.push_back(std::move(child).takeValue());
174 }
175 return Result<std::vector<decision::Condition>>::success(std::move(result));
176 };
177 if (kind.value() == "all" || kind.value() == "any" || kind.value() == "not") {
178 auto decoded = children();
179 if (!decoded) return Result<decision::Condition>::failure(decoded.status());
180 if (kind.value() == "all")
181 return Result<decision::Condition>::success(decision::Condition::all(std::move(decoded).takeValue()));
182 if (kind.value() == "any")
183 return Result<decision::Condition>::success(decision::Condition::any(std::move(decoded).takeValue()));
184 if (decoded.value().size() != 1)
185 return invalid<decision::Condition>("not condition requires one child", path + ".children");
186 return Result<decision::Condition>::success(decision::Condition::not_(std::move(decoded.value().front())));
187 }
188 auto key = text(value, "key", path + ".key");
189 if (!key) return Result<decision::Condition>::failure(key.status());
190 if (kind.value() == "compare") {
191 auto op = text(value, "operator", path + ".operator");
192 const auto* expected = field(value, "expected");
193 if (!op || !expected) return invalid<decision::Condition>("compare condition is incomplete", path);
194 const std::pair<std::string_view, decision::CompareOperator> operators[] = {
198 for (const auto& [name, parsed] : operators)
199 if (op.value() == name)
201 decision::Condition::compare(key.value(), parsed, *expected));
202 return invalid<decision::Condition>("unknown comparison operator", path + ".operator");
203 }
204 if (kind.value() == "state_equals") {
205 const auto* expected = field(value, "expected");
206 if (!expected) return invalid<decision::Condition>("state condition is incomplete", path);
208 }
209 if (kind.value() == "has_tag")
211 if (kind.value() == "has_attribute")
213 if (kind.value() == "has_resource")
215 if (kind.value() == "authority_check")
217 if (kind.value() == "policy_call") {
218 const auto* arguments = field(value, "arguments");
219 if (!arguments) return invalid<decision::Condition>("policy arguments are missing", path + ".arguments");
220 std::optional<decision::ScriptConditionDeclaration> declaration;
221 if (const auto* script = field(value, "script")) {
222 auto name = text(*script, "name", path + ".script.name");
223 if (!name) return Result<decision::Condition>::failure(name.status());
224 auto dependencies = decodeStrings(*script, "dependencies", path + ".script.dependencies");
225 if (!dependencies) return Result<decision::Condition>::failure(dependencies.status());
226 auto determinism = integer(*script, "determinism", path + ".script.determinism");
227 if (!determinism) return Result<decision::Condition>::failure(determinism.status());
228 if (determinism.value() < 0 || determinism.value() >
229 static_cast<std::int64_t>(
231 return invalid<decision::Condition>("script condition declaration is invalid", path + ".script");
233 name.value(), std::move(dependencies).takeValue(),
234 static_cast<decision::DeterminismLevel>(determinism.value())};
235 }
237 decision::Condition::policyCall(key.value(), *arguments, std::move(declaration)));
238 }
239 return invalid<decision::Condition>("unknown condition kind", path + ".kind");
240}
241
242Value::Array strings(const std::vector<std::string>& values) {
243 Value::Array result;
244 result.reserve(values.size());
245 for (const auto& value : values) result.emplace_back(value);
246 return result;
247}
248
249Result<std::vector<std::string>> decodeStrings(const Value& parent, std::string_view name, std::string path) {
250 const auto* source = field(parent, name);
251 if (!source || !source->isArray()) return invalid<std::vector<std::string>>("expected text array", path);
252 std::vector<std::string> result;
253 for (std::size_t index = 0; index < source->getIf<Value::Array>()->size(); ++index) {
254 const auto& item = source->getIf<Value::Array>()->at(index);
255 if (!item.isString())
256 return invalid<std::vector<std::string>>("expected text item",
257 path + "[" + std::to_string(index) + "]");
258 result.push_back(item.asString());
259 }
260 return Result<std::vector<std::string>>::success(std::move(result));
261}
262
263Value encodeTargetingSpec(const sensing::TargetingSpec& spec) {
264 Value::Object result{{"space", std::string(sensing::coordinateSpaceName(spec.space))},
265 {"domain", static_cast<std::int64_t>(spec.domain)},
266 {"minCount", static_cast<std::int64_t>(spec.minCount)},
267 {"maxCount", static_cast<std::int64_t>(spec.maxCount)},
268 {"minRange", spec.minRange},
269 {"maxRange", std::isfinite(spec.maxRange) ? Value(spec.maxRange) : Value()},
270 {"requiredTags", strings(spec.requiredTags)},
271 {"excludedTags", strings(spec.excludedTags)},
272 {"lineOfSight", spec.lineOfSight == sensing::LineOfSightMode::Required}};
273 if (spec.zone) result["zone"] = spec.zone->logicalId().format();
274 if (spec.worldArea) {
275 const auto first = spec.worldArea->first();
276 const auto second = spec.worldArea->second();
277 result["worldArea"] = Value::Object{
278 {"shape", static_cast<std::int64_t>(spec.worldArea->shape())},
279 {"first", Value::Array{first.x(), first.y(), first.z()}},
280 {"second", Value::Array{second.x(), second.y(), second.z()}},
281 {"radius", spec.worldArea->radius()}};
282 }
283 if (spec.gridArea) {
284 const auto minimum = spec.gridArea->minimum();
285 const auto maximum = spec.gridArea->maximum();
286 result["gridArea"] = Value::Object{
287 {"shape", static_cast<std::int64_t>(spec.gridArea->shape())},
288 {"minimum", Value::Array{minimum.x(), minimum.y(), minimum.z()}},
289 {"maximum", Value::Array{maximum.x(), maximum.y(), maximum.z()}}};
290 }
291 return Value(std::move(result));
292}
293
294Result<std::array<double, 3>> vector3(const Value& value, std::string_view name, std::string path) {
295 const auto* item = field(value, name);
296 if (!item || !item->isArray() || item->getIf<Value::Array>()->size() != 3)
297 return invalid<std::array<double, 3>>("expected three-component array", std::move(path));
298 std::array<double, 3> result{};
299 for (std::size_t index = 0; index < result.size(); ++index) {
300 const auto& component = item->getIf<Value::Array>()->at(index);
301 if (!component.isDouble() && !component.isInt64())
302 return invalid<std::array<double, 3>>("vector component must be numeric", path);
303 result[index] = component.isDouble() ? component.asDouble() : static_cast<double>(component.asInt());
304 if (!std::isfinite(result[index]))
305 return invalid<std::array<double, 3>>("vector component must be finite", path);
306 }
307 return Result<std::array<double, 3>>::success(result);
308}
309
310Result<std::array<std::int32_t, 3>> gridVector3(const Value& value, std::string_view name,
311 std::string path) {
312 auto decoded = vector3(value, name, path);
313 if (!decoded) return Result<std::array<std::int32_t, 3>>::failure(decoded.status());
314 std::array<std::int32_t, 3> result{};
315 for (std::size_t index = 0; index < result.size(); ++index) {
316 const double component = decoded.value()[index];
317 if (std::trunc(component) != component ||
318 component < static_cast<double>(std::numeric_limits<std::int32_t>::min()) ||
319 component > static_cast<double>(std::numeric_limits<std::int32_t>::max()))
320 return invalid<std::array<std::int32_t, 3>>("grid component must be an int32", path);
321 result[index] = static_cast<std::int32_t>(component);
322 }
323 return Result<std::array<std::int32_t, 3>>::success(result);
324}
325
326Result<sensing::TargetingSpec> decodeTargetingSpec(const Value& value) {
328 auto space = text(value, "space", "action.targetingSpec.space");
329 if (!space) return Result<sensing::TargetingSpec>::failure(space.status());
330 auto domain = integer(value, "domain", "action.targetingSpec.domain");
331 if (!domain) return Result<sensing::TargetingSpec>::failure(domain.status());
332 auto minCount = integer(value, "minCount", "action.targetingSpec.minCount");
333 if (!minCount) return Result<sensing::TargetingSpec>::failure(minCount.status());
334 auto maxCount = integer(value, "maxCount", "action.targetingSpec.maxCount");
335 if (!maxCount) return Result<sensing::TargetingSpec>::failure(maxCount.status());
336 auto minRange = number(value, "minRange", "action.targetingSpec.minRange");
337 if (!minRange) return Result<sensing::TargetingSpec>::failure(minRange.status());
338 auto required = decodeStrings(value, "requiredTags", "action.targetingSpec.requiredTags");
340 auto excluded = decodeStrings(value, "excludedTags", "action.targetingSpec.excludedTags");
341 if (!excluded) return Result<sensing::TargetingSpec>::failure(excluded.status());
342 auto lineOfSight = boolean(value, "lineOfSight", "action.targetingSpec.lineOfSight");
343 if (!lineOfSight) return Result<sensing::TargetingSpec>::failure(lineOfSight.status());
344 const std::pair<std::string_view, sensing::CoordinateSpace> spaces[] = {
347 bool spaceFound = false;
348 for (const auto& [name, parsed] : spaces)
349 if (space.value() == name) { result.space = parsed; spaceFound = true; break; }
350 if (!spaceFound || domain.value() < 0 ||
351 domain.value() > static_cast<std::int64_t>(sensing::TargetDomain::Neutral) ||
352 minCount.value() < 0 || maxCount.value() < 0 ||
353 minCount.value() > std::numeric_limits<std::uint32_t>::max() ||
354 maxCount.value() > std::numeric_limits<std::uint32_t>::max())
355 return invalid<sensing::TargetingSpec>("targeting specification enum or count is invalid",
356 "action.targetingSpec");
357 result.domain = static_cast<sensing::TargetDomain>(domain.value());
358 result.minCount = static_cast<std::uint32_t>(minCount.value());
359 result.maxCount = static_cast<std::uint32_t>(maxCount.value());
360 result.minRange = static_cast<float>(minRange.value());
361 const auto* maxRange = field(value, "maxRange");
362 if (!maxRange) return invalid<sensing::TargetingSpec>("maximum range is missing", "action.targetingSpec.maxRange");
363 if (maxRange->isNull()) result.maxRange = std::numeric_limits<float>::infinity();
364 else if (maxRange->isDouble() || maxRange->isInt64())
365 result.maxRange = static_cast<float>(maxRange->isDouble() ? maxRange->asDouble() : maxRange->asInt());
366 else
367 return invalid<sensing::TargetingSpec>("maximum range must be numeric or null",
368 "action.targetingSpec.maxRange");
369 result.requiredTags = std::move(required).takeValue();
370 result.excludedTags = std::move(excluded).takeValue();
371 result.lineOfSight = lineOfSight.value() ? sensing::LineOfSightMode::Required
373 if (const auto* zone = field(value, "zone")) {
374 if (!zone->isString()) return invalid<sensing::TargetingSpec>("zone must be text", "action.targetingSpec.zone");
375 auto id = LogicalId::parse(zone->asString());
376 if (!id) return invalid<sensing::TargetingSpec>("zone id is invalid", "action.targetingSpec.zone");
378 }
379 if (const auto* area = field(value, "worldArea")) {
380 auto shape = integer(*area, "shape", "action.targetingSpec.worldArea.shape");
382 auto first = vector3(*area, "first", "action.targetingSpec.worldArea.first");
384 auto second = vector3(*area, "second", "action.targetingSpec.worldArea.second");
386 auto radius = number(*area, "radius", "action.targetingSpec.worldArea.radius");
388 if (shape.value() < 0 || shape.value() > 3)
389 return invalid<sensing::TargetingSpec>("world area is invalid", "action.targetingSpec.worldArea");
390 const auto makePoint = [&](const std::array<double, 3>& point) {
392 ? sensing::WorldPoint::world2D(static_cast<float>(point[0]), static_cast<float>(point[1]))
393 : sensing::WorldPoint::world3D(static_cast<float>(point[0]), static_cast<float>(point[1]),
394 static_cast<float>(point[2]));
395 };
396 auto a = makePoint(first.value());
397 if (!a) return Result<sensing::TargetingSpec>::failure(a.status());
398 auto b = makePoint(second.value());
399 if (!b) return Result<sensing::TargetingSpec>::failure(b.status());
401 shape.value() == 0
402 ? sensing::WorldArea::circle2D(a.value(), static_cast<float>(radius.value()))
403 : shape.value() == 1
404 ? sensing::WorldArea::box2D(a.value(), b.value())
405 : shape.value() == 2
406 ? sensing::WorldArea::sphere3D(a.value(), static_cast<float>(radius.value()))
407 : sensing::WorldArea::box3D(a.value(), b.value());
408 if (!decoded) return Result<sensing::TargetingSpec>::failure(decoded.status());
409 result.worldArea = std::move(decoded).takeValue();
410 }
411 if (const auto* area = field(value, "gridArea")) {
412 auto shape = integer(*area, "shape", "action.targetingSpec.gridArea.shape");
414 auto minimum = gridVector3(*area, "minimum", "action.targetingSpec.gridArea.minimum");
416 auto maximum = gridVector3(*area, "maximum", "action.targetingSpec.gridArea.maximum");
418 if (shape.value() < 0 || shape.value() > 1)
419 return invalid<sensing::TargetingSpec>("grid area is invalid", "action.targetingSpec.gridArea");
420 const auto makePoint = [&](const std::array<std::int32_t, 3>& point) {
424 };
425 const auto a = makePoint(minimum.value());
426 const auto b = makePoint(maximum.value());
427 auto decoded = shape.value() == 0 ? sensing::GridArea::box2D(a, b) : sensing::GridArea::box3D(a, b);
428 if (!decoded) return Result<sensing::TargetingSpec>::failure(decoded.status());
429 result.gridArea = std::move(decoded).takeValue();
430 }
431 auto valid = result.validate();
433 return Result<sensing::TargetingSpec>::success(std::move(result));
434}
435
436Result<Duration> durationField(const Value& value, std::string_view nsName, std::string_view msName,
437 std::uint64_t version, std::string path) {
438 if (field(value, version == 1 ? nsName : msName))
439 return invalid<Duration>("duration field does not match schema version", std::move(path));
440 auto raw = integer(value, version == 1 ? msName : nsName, path);
441 if (!raw) return Result<Duration>::failure(raw.status());
442 if (raw.value() < 0) return invalid<Duration>("duration must be non-negative", std::move(path));
443 if (version == 1 && raw.value() > std::numeric_limits<std::int64_t>::max() / 1'000'000)
444 return invalid<Duration>("millisecond duration overflows nanoseconds", std::move(path));
445 return Result<Duration>::success(Duration::fromNanoseconds(version == 1 ? raw.value() * 1'000'000 : raw.value()));
446}
447
448} // namespace
449
451 auto valid = definition.validate();
452 if (!valid) return Result<Value>::failure(valid.status());
453 Value::Array costs;
454 if (definition.action.cost)
455 for (const auto& item : definition.action.cost->items())
456 costs.emplace_back(Value::Object{{"resource", item.resource.value()}, {"amount", item.amount.value()}});
457 Value::Array triggers;
458 for (const auto& trigger : definition.triggers)
459 triggers.emplace_back(Value::Object{
460 {"gameplayTag", trigger.gameplayTag},
461 {"match", trigger.match == tags::GameplayTagMatch::Exact ? "exact" : "include_descendants"}});
462 Value timeline;
463 if (definition.action.timeline) {
464 auto encoded = definition.action.timeline->toValue();
465 if (!encoded) return Result<Value>::failure(encoded.status());
466 timeline = std::move(encoded).takeValue();
467 }
468 Value::Object action{{"id", definition.action.id.format()},
469 {"timing", Value::Object{{"windupNs", definition.action.timing.windup.nanoseconds()},
470 {"activeNs", definition.action.timing.active.nanoseconds()},
471 {"recoverNs", definition.action.timing.recover.nanoseconds()}}},
472 {"condition", encodeCondition(definition.action.condition)},
473 {"targetingMode", std::string(targetingModeName(definition.action.targetingMode))},
474 {"targetingSpec", definition.action.targetingSpec
475 ? encodeTargetingSpec(*definition.action.targetingSpec)
476 : Value()},
477 {"cost", definition.action.cost ? Value(std::move(costs)) : Value()},
478 {"effectIds", strings(definition.action.effectIds)},
479 {"activeExecutionRequired", definition.action.activeExecutionRequired},
480 {"metadata", definition.action.metadata},
481 {"timeline", std::move(timeline)}};
482 Value::Object root{{"schema", std::string(kAbilityAssetSchemaId)},
483 {"schemaVersion", static_cast<std::int64_t>(kAbilityAssetSchemaVersion)},
484 {"id", definition.id.format()},
485 {"action", std::move(action)},
486 {"cooldownNs", definition.cooldown.nanoseconds()},
487 {"instancing", std::string(instancingName(definition.instancing))},
488 {"activationGroup", std::string(groupName(definition.activationGroup))},
489 {"triggers", std::move(triggers)}};
490 return Result<Value>::success(Value(std::move(root)));
491}
492
494 auto rootFields = rejectUnknown(value, {"schema", "schemaVersion", "id", "action", "cooldownNs", "cooldownMs",
495 "instancing", "activationGroup", "triggers"}, "ability");
496 if (!rootFields) return Result<AbilityDefinition>::failure(rootFields.status());
497 auto schema = text(value, "schema", "schema");
498 if (!schema) return Result<AbilityDefinition>::failure(schema.status());
499 auto versionRaw = integer(value, "schemaVersion", "schemaVersion");
500 if (!versionRaw) return Result<AbilityDefinition>::failure(versionRaw.status());
501 if (schema.value() != kAbilityAssetSchemaId)
502 return invalid<AbilityDefinition>("unexpected ability asset schema", "schema");
503 if (versionRaw.value() < 1 || versionRaw.value() > static_cast<std::int64_t>(kAbilityAssetSchemaVersion))
504 return invalid<AbilityDefinition>("unsupported ability asset schema version", "schemaVersion");
505 const auto version = static_cast<std::uint64_t>(versionRaw.value());
506 auto idText = text(value, "id", "id");
507 if (!idText) return Result<AbilityDefinition>::failure(idText.status());
508 auto cooldown = durationField(value, "cooldownNs", "cooldownMs", version, "cooldown");
509 if (!cooldown) return Result<AbilityDefinition>::failure(cooldown.status());
510 auto instancingText = text(value, "instancing", "instancing");
511 if (!instancingText) return Result<AbilityDefinition>::failure(instancingText.status());
512 auto groupText = text(value, "activationGroup", "activationGroup");
513 if (!groupText) return Result<AbilityDefinition>::failure(groupText.status());
514 const auto* actionValue = field(value, "action");
515 const auto* triggerValue = field(value, "triggers");
516 if (!actionValue || !triggerValue || !triggerValue->isArray())
517 return invalid<AbilityDefinition>("ability asset is incomplete", "ability");
518 auto id = LogicalId::parse(idText.value());
519 auto instancing = parseInstancing(instancingText.value());
521 auto group = parseGroup(groupText.value());
522 if (!group) return Result<AbilityDefinition>::failure(group.status());
523 if (!id || !actionValue->isObject())
524 return invalid<AbilityDefinition>("ability identity or policy is invalid", "ability");
525
526 AbilityDefinition result;
527 result.id = *id;
528 result.cooldown = cooldown.value();
529 result.instancing = instancing.value();
530 result.activationGroup = group.value();
531 auto actionIdText = text(*actionValue, "id", "action.id");
532 if (!actionIdText) return Result<AbilityDefinition>::failure(actionIdText.status());
533 const auto* timing = field(*actionValue, "timing");
534 auto modeText = text(*actionValue, "targetingMode", "action.targetingMode");
535 if (!modeText) return Result<AbilityDefinition>::failure(modeText.status());
536 auto activeRequired = boolean(*actionValue, "activeExecutionRequired", "action.activeExecutionRequired");
537 if (!activeRequired) return Result<AbilityDefinition>::failure(activeRequired.status());
538 auto effects = decodeStrings(*actionValue, "effectIds", "action.effectIds");
539 if (!effects) return Result<AbilityDefinition>::failure(effects.status());
540 const auto* metadata = field(*actionValue, "metadata");
541 const auto* condition = field(*actionValue, "condition");
542 if (!timing || !timing->isObject() || !metadata || !metadata->isObject() || !condition)
543 return invalid<AbilityDefinition>("action definition is incomplete", "action");
544 auto actionId = LogicalId::parse(actionIdText.value());
545 auto windup = durationField(*timing, "windupNs", "windupMs", version, "action.timing.windup");
546 if (!windup) return Result<AbilityDefinition>::failure(windup.status());
547 auto active = durationField(*timing, "activeNs", "activeMs", version, "action.timing.active");
548 if (!active) return Result<AbilityDefinition>::failure(active.status());
549 auto recover = durationField(*timing, "recoverNs", "recoverMs", version, "action.timing.recover");
550 if (!recover) return Result<AbilityDefinition>::failure(recover.status());
551 auto mode = parseTargetingMode(modeText.value());
552 if (!mode) return Result<AbilityDefinition>::failure(mode.status());
553 auto decodedCondition = decodeCondition(*condition, "action.condition");
554 if (!decodedCondition) return Result<AbilityDefinition>::failure(decodedCondition.status());
555 if (!actionId) return invalid<AbilityDefinition>("action identity is invalid", "action.id");
556 result.action.id = *actionId;
557 result.action.timing = {windup.value(), active.value(), recover.value()};
558 result.action.condition = std::move(decodedCondition).takeValue();
559 result.action.targetingMode = mode.value();
560 result.action.activeExecutionRequired = activeRequired.value();
561 result.action.effectIds = std::move(effects).takeValue();
562 result.action.metadata = *metadata->getIf<Value::Object>();
563 if (const auto* spec = field(*actionValue, "targetingSpec"); spec && !spec->isNull()) {
564 auto decoded = decodeTargetingSpec(*spec);
565 if (!decoded) return Result<AbilityDefinition>::failure(decoded.status());
566 result.action.targetingSpec = std::move(decoded).takeValue();
567 }
568 if (const auto* cost = field(*actionValue, "cost"); cost && !cost->isNull()) {
569 if (!cost->isArray()) return invalid<AbilityDefinition>("action cost must be an array or null", "action.cost");
570 std::vector<resource::ResourceCost> items;
571 const auto& array = *cost->getIf<Value::Array>();
572 for (std::size_t index = 0; index < array.size(); ++index) {
573 auto resourceName = text(array[index], "resource", "action.cost[" + std::to_string(index) + "].resource");
574 if (!resourceName) return Result<AbilityDefinition>::failure(resourceName.status());
575 auto amount = integer(array[index], "amount", "action.cost[" + std::to_string(index) + "].amount");
576 if (!amount) return Result<AbilityDefinition>::failure(amount.status());
577 auto item = resource::ResourceCost::create(resourceName.value(), amount.value());
578 if (!item) return Result<AbilityDefinition>::failure(item.status());
579 items.push_back(std::move(item).takeValue());
580 }
581 auto costSpec = resource::CostSpec::create(std::move(items));
582 if (!costSpec) return Result<AbilityDefinition>::failure(costSpec.status());
583 result.action.cost = std::move(costSpec).takeValue();
584 }
585 if (const auto* timeline = field(*actionValue, "timeline"); timeline && !timeline->isNull()) {
586 auto decoded = ActionTimeline::fromValue(*timeline);
587 if (!decoded) return Result<AbilityDefinition>::failure(decoded.status());
588 result.action.timeline = std::move(decoded).takeValue();
589 }
590 for (std::size_t index = 0; index < triggerValue->getIf<Value::Array>()->size(); ++index) {
591 const auto& item = triggerValue->getIf<Value::Array>()->at(index);
592 auto tag = text(item, "gameplayTag", "triggers[" + std::to_string(index) + "].gameplayTag");
593 if (!tag) return Result<AbilityDefinition>::failure(tag.status());
594 auto match = text(item, "match", "triggers[" + std::to_string(index) + "].match");
595 if (!match) return Result<AbilityDefinition>::failure(match.status());
596 if (match.value() != "exact" && match.value() != "include_descendants")
597 return invalid<AbilityDefinition>("ability trigger is invalid", "triggers[" + std::to_string(index) + "]");
598 result.triggers.push_back({tag.value(), match.value() == "exact" ? tags::GameplayTagMatch::Exact
599 : tags::GameplayTagMatch::IncludeDescendants});
600 }
601 auto valid = result.validate();
602 if (!valid) return Result<AbilityDefinition>::failure(valid.status());
603 return Result<AbilityDefinition>::success(std::move(result));
604}
605
606} // namespace eve::action
Versioned persistent codec for ability definitions.
double value
bool & active
int root
Definition AnimSmr.cpp:119
building::EdgeCurveGroup group
eve::resource::CostSpec cost
eve::Value condition
std::map< std::string, Var > values
std::string message
float maximum[3]
float minimum[3]
std::uint32_t key
ShaderImageInput shape
float area
Definition Grass.cpp:62
std::int32_t second
std::int32_t first
std::string text
TokenKind kind
std::int32_t parent
bool required
std::string name
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
const std::string * tag
float radius
std::string action
Definition PlayHost.cpp:117
std::string script
Definition PlayHost.cpp:116
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
double number
bool found
bool boolean
AbilityInstancingPolicy instancing
LogicalId definition
int children
Definition TreeMesh.cpp:295
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
std::size_t at
glm::vec3 point
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
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
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:269
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
Type kind() const noexcept
Compatibility spelling for the former dialogue data tree.
Definition Value.h:85
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Immutable condition AST node.
Definition Condition.h:195
static Condition hasAttribute(std::string attribute)
Require an attribute to exist.
static Condition all(std::vector< Condition > children)
Construct a conjunction of child conditions.
static Condition not_(Condition child)
Construct a logical negation of one child condition.
static Condition hasTag(std::string tag)
Require a tag to be present.
static Condition stateEquals(std::string key, Value expected)
Require a named state value to equal the expected value.
static Condition policyCall(std::string name, Value arguments=Value::Object{}, std::optional< ScriptConditionDeclaration > scriptDeclaration=std::nullopt)
Call a named read-only policy and use its ConditionResult.
static Condition compare(std::string key, CompareOperator op, Value expected)
Compare a context value against an expected scalar value.
static Condition authorityCheck(std::string scope)
Require read-only authority for a scope.
static Condition any(std::vector< Condition > children)
Construct a disjunction of child conditions.
static Condition hasResource(std::string resource)
Require a resource to exist.
const T * getIf() const
Return an immediately borrowed typed value.
static Result< GridArea > box2D(GridPoint minimum, GridPoint maximum)
Creates an inclusive 2D grid rectangle.
static Result< GridArea > box3D(GridPoint minimum, GridPoint maximum)
Creates an inclusive 3D grid box.
static GridPoint grid3D(std::int32_t x, std::int32_t y, std::int32_t z) noexcept
Creates a 3D integer grid point.
static GridPoint grid2D(std::int32_t x, std::int32_t y) noexcept
Creates a 2D integer grid point.
static Result< WorldArea > circle2D(WorldPoint center, float radius)
Creates a finite non-negative 2D world circle.
static Result< WorldArea > box3D(WorldPoint minimum, WorldPoint maximum)
Creates an axis-aligned 3D world box from inclusive corners.
static Result< WorldArea > sphere3D(WorldPoint center, float radius)
Creates a finite non-negative 3D world sphere.
static Result< WorldArea > box2D(WorldPoint minimum, WorldPoint maximum)
Creates an axis-aligned 2D world box from inclusive corners.
static Result< WorldPoint > world3D(float x, float y, float z)
Creates a finite 3D world point.
static Result< WorldPoint > world2D(float x, float y)
Creates a finite 2D world point.
static std::optional< ZoneRef > fromLogicalId(LogicalId id)
Creates a zone reference from a valid logical ID.
constexpr std::uint64_t kAbilityAssetSchemaVersion
constexpr std::string_view kAbilityAssetSchemaId
AbilityActivationGroup
Concurrency policy modeled after independent and exclusive ability groups.
Result< Value > encodeAbilityAsset(const AbilityDefinition &definition)
Encode one validated ability definition as canonical schema version 2 data.
Result< AbilityDefinition > decodeAbilityAsset(const Value &value)
Decode schema version 2 or migrate version 1 millisecond data to the current definition.
TargetingMode
Selects how an action obtains its target set.
Definition Action.h:59
@ Query
The request supplies a sensing query resolved during validation.
@ None
The action has no target selection.
@ Explicit
The request already owns the selected ECS target handles.
AbilityInstancingPolicy
Controls where mutable ability instance state is allocated.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
const char * conditionKindName(ConditionKind kind) noexcept
Return the stable lowercase spelling of a condition node kind. @ownership Borrowed process-static tex...
DeterminismLevel
Determinism contract declared by a script-backed condition policy.
Definition Condition.h:46
@ ExplicitlyNondeterministic
Result is intentionally not suitable for replay or lockstep.
const char * compareOperatorName(CompareOperator op) noexcept
Return the stable lowercase spelling of a comparison operator. @ownership Borrowed process-static tex...
std::string_view coordinateSpaceName(CoordinateSpace space) noexcept
Returns the stable protocol name for a coordinate space.
TargetDomain
Stable relationship class used by generic domain constraints.
Definition Targeting.h:54
Canonical immutable-by-convention definition of a grantable ability.
AbilityActivationGroup activationGroup
std::vector< AbilityTrigger > triggers
AbilityInstancingPolicy instancing
Result< void > validate() const
Validate identity, action, cooldown and gameplay-event trigger invariants.
bool activeExecutionRequired
Whether Active must call the injected executor even with no effect ids.
Definition Action.h:106
TargetingMode targetingMode
Target acquisition policy for this action.
Definition Action.h:98
std::vector< std::string > effectIds
Stable effect-definition ids interpreted by the active executor.
Definition Action.h:104
decision::Condition condition
Side-effect-free precondition tree.
Definition Action.h:96
std::optional< ActionTimeline > timeline
Optional versioned choreography authored by the action editor.
Definition Action.h:110
std::optional< resource::CostSpec > cost
Optional canonical cost reserved and committed by the pipeline.
Definition Action.h:102
ActionTiming timing
Timed phase policy in simulation durations.
Definition Action.h:94
LogicalId id
Scoped logical definition identity, not a runtime execution id.
Definition Action.h:92
Value::Object metadata
Deterministic extension data owned by this definition.
Definition Action.h:108
std::optional< sensing::TargetingSpec > targetingSpec
Constraints used when targetingMode is Query.
Definition Action.h:100
Metadata a script policy must declare before it is used by a condition.
Definition Condition.h:64
A checked target-count and spatial constraint description.
Definition Targeting.h:310
LineOfSightMode lineOfSight
Whether an injected LOS provider must confirm each candidate.
Definition Targeting.h:340
std::optional< GridArea > gridArea
Optional Grid2D/Grid3D area constraint.
Definition Targeting.h:338
Result< void > validate() const
Validates count, range, tags, zone and coordinate-space invariants.
std::vector< std::string > excludedTags
Tags that exclude a candidate when present.
Definition Targeting.h:332
float maxRange
Maximum distance, in world units or grid cells; positive infinity means unbounded.
Definition Targeting.h:328
float minRange
Minimum distance, in world units or grid cells according to space.
Definition Targeting.h:326
std::uint32_t maxCount
Maximum number of candidates accepted.
Definition Targeting.h:318
std::optional< ZoneRef > zone
Optional logical zone membership required from candidates.
Definition Targeting.h:334
std::vector< std::string > requiredTags
All tags that each candidate must contain.
Definition Targeting.h:330
CoordinateSpace space
Required coordinate space for origin, candidates and area.
Definition Targeting.h:312
std::optional< WorldArea > worldArea
Optional World2D/World3D area constraint.
Definition Targeting.h:336
std::uint32_t minCount
Minimum number of candidates required in the resolved set.
Definition Targeting.h:316
TargetDomain domain
Relationship class accepted from candidate facts.
Definition Targeting.h:314