载入中...
搜索中...
未找到
RuleEngine.cpp
浏览该文件的文档.
2
5
6#include "common/Capability.h"
7#include "common/Diagnostic.h"
8#include "common/IEconomy.h"
10
11#include <algorithm>
12#include <limits>
13#include <set>
14#include <utility>
15
16namespace eve::emergence {
17namespace {
18
19const eve::Value* objField(const eve::Value::Object& object, std::string_view name) {
20 const auto it = object.find(std::string(name));
21 return it == object.end() ? nullptr : &it->second;
22}
23
24bool exactFields(const eve::Value::Object& object, std::initializer_list<std::string_view> allowed) {
25 const std::set<std::string_view> fields(allowed.begin(), allowed.end());
26 for (const auto& [name, unused] : object) {
27 (void)unused;
28 if (!fields.contains(name)) return false;
29 }
30 return true;
31}
32
33eve::Result<std::int64_t> requireInt64(const eve::Value& value, const char* path) {
34 if (!value.isInt64())
36 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "value must be an integer", path ? path : "", {}, "emergence.rule_engine"));
38}
39
40eve::Result<int> requireInt(const eve::Value& value, const char* path) {
41 auto parsed = requireInt64(value, path);
42 if (!parsed) return eve::Result<int>::failure(parsed.status());
43 const auto raw = parsed.value();
44 if (raw < static_cast<std::int64_t>(std::numeric_limits<int>::min()) ||
45 raw > static_cast<std::int64_t>(std::numeric_limits<int>::max()))
47 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "integer out of int range", path ? path : "", {}, "emergence.rule_engine"));
48 return eve::Result<int>::success(static_cast<int>(raw));
49}
50
51eve::Result<std::uint64_t> requireNonNegativeU64(const eve::Value& value, const char* path) {
52 auto parsed = requireInt64(value, path);
53 if (!parsed) return eve::Result<std::uint64_t>::failure(parsed.status());
54 if (parsed.value() < 0)
56 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "value must be non-negative", path ? path : "", {}, "emergence.rule_engine"));
57 return eve::Result<std::uint64_t>::success(static_cast<std::uint64_t>(parsed.value()));
58}
59
60} // namespace
61
62eve::Result<RuleDefinition> RuleEngine::validateAndNormalize(RuleDefinition rule) {
63 if (rule.id.empty())
65 eve::DiagnosticCode::InvalidArgument, "rule id must be non-empty", "id", {}, "emergence.rule_engine"));
66 if (!rule.condition.isValid())
68 "rule condition is invalid", "condition", {},
69 "emergence.rule_engine"));
70 for (const auto& action : rule.actions) {
71 if (action.kind.empty())
73 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "action kind must be non-empty",
74 "actions.kind", {}, "emergence.rule_engine"));
75 }
76 if (rule.watchKeys.empty()) {
77 auto keys = collectWatchKeys(rule.condition);
78 if (!keys) return eve::Result<RuleDefinition>::failure(keys.status());
79 rule.watchKeys = std::move(keys).takeValue();
80 } else {
81 std::sort(rule.watchKeys.begin(), rule.watchKeys.end());
82 rule.watchKeys.erase(std::unique(rule.watchKeys.begin(), rule.watchKeys.end()), rule.watchKeys.end());
83 for (const auto& key : rule.watchKeys) {
84 auto parsed = parseFactKey(key);
85 if (!parsed) return eve::Result<RuleDefinition>::failure(parsed.status());
86 }
87 }
88 if (rule.watchKeys.empty())
90 "rule must declare at least one watch key",
91 "watchKeys", {}, "emergence.rule_engine"));
92 return eve::Result<RuleDefinition>::success(std::move(rule));
93}
94
95eve::Result<int> RuleEngine::replaceCatalogue(std::vector<RuleDefinition> rules) {
96 if (draining_)
98 "cannot replace catalogue during drain", {}, {},
99 "emergence.rule_engine"));
100 std::vector<RuleDefinition> next;
101 next.reserve(rules.size());
102 std::unordered_map<std::string, std::uint32_t> ids;
104 for (auto& rule : rules) {
105 auto normalized = validateAndNormalize(std::move(rule));
106 if (!normalized) return eve::Result<int>::failure(normalized.status());
107 auto value = std::move(normalized).takeValue();
108 if (ids.contains(value.id))
110 eve::DiagnosticCode::AlreadyExists, "duplicate rule id", "id", {}, "emergence.rule_engine"));
111 const auto slot = static_cast<std::uint32_t>(next.size());
112 ids.emplace(value.id, slot);
113 index.addRule(slot, value.watchKeys);
114 next.push_back(std::move(value));
115 }
116 rules_ = std::move(next);
117 runtime_.assign(rules_.size(), RuleRuntime{});
118 idToIndex_ = std::move(ids);
119 watchIndex_ = std::move(index);
120 dirty_.clear();
121 activations_.clear();
122 return eve::Result<int>::success(static_cast<int>(rules_.size()), eve::Status::success(eve::StatusCode::Applied));
123}
124
125eve::Result<int> RuleEngine::replaceCatalogueJson(std::string_view json) {
126 auto parsed = eve::Value::fromJson(json);
127 if (!parsed) return eve::Result<int>::failure(parsed.status());
128 const auto* root = parsed.value().getIf<eve::Value::Object>();
129 if (root == nullptr)
131 eve::DiagnosticCode::InvalidArgument, "rules document must be an object", {}, {}, "emergence.rule_engine"));
132 const auto* schema = objField(*root, "schema");
133 const auto* version = objField(*root, "version");
134 if (schema == nullptr || !schema->isString() || schema->asString() != "eve.emergence.rules")
136 eve::DiagnosticCode::InvalidArgument, "unexpected rules schema", "schema", {}, "emergence.rule_engine"));
137 if (version == nullptr || !version->isInt64() || version->asInt() != 1)
139 eve::DiagnosticCode::UnknownVersion, "unsupported rules version", "version", {}, "emergence.rule_engine"));
140 const auto* rulesValue = objField(*root, "rules");
141 const auto* rulesArray = rulesValue == nullptr ? nullptr : rulesValue->getIf<eve::Value::Array>();
142 if (rulesArray == nullptr)
144 eve::DiagnosticCode::InvalidArgument, "rules must be an array", "rules", {}, "emergence.rule_engine"));
145
146 std::vector<RuleDefinition> rules;
147 rules.reserve(rulesArray->size());
148 for (std::size_t i = 0; i < rulesArray->size(); ++i) {
149 const auto* object = (*rulesArray)[i].getIf<eve::Value::Object>();
150 if (object == nullptr)
152 "rule entry must be an object", "rules", {},
153 "emergence.rule_engine"));
154 RuleDefinition rule;
155 const auto* id = objField(*object, "id");
156 if (id == nullptr || !id->isString() || id->asString().empty())
158 "rule id must be a non-empty string", "rules.id",
159 {}, "emergence.rule_engine"));
160 rule.id = id->asString();
161 if (const auto* priority = objField(*object, "priority")) {
162 auto parsed = requireInt(*priority, "rules.priority");
163 if (!parsed) return eve::Result<int>::failure(parsed.status());
164 rule.priority = parsed.value();
165 }
166 if (const auto* once = objField(*object, "once")) {
167 if (!once->isBool())
169 "once must be a boolean", "rules.once", {},
170 "emergence.rule_engine"));
171 rule.once = once->asBool();
172 }
173 if (const auto* cooldown = objField(*object, "cooldownTicks")) {
174 auto parsed = requireNonNegativeU64(*cooldown, "rules.cooldownTicks");
175 if (!parsed) return eve::Result<int>::failure(parsed.status());
176 rule.cooldownTicks = parsed.value();
177 }
178 if (const auto* fireMode = objField(*object, "fireMode")) {
179 if (!fireMode->isString())
181 "fireMode must be a string", "rules.fireMode",
182 {}, "emergence.rule_engine"));
183 if (fireMode->asString() == "rising")
184 rule.fireMode = FireMode::Rising;
185 else if (fireMode->asString() == "level")
186 rule.fireMode = FireMode::Level;
187 else
189 "unknown fireMode", "rules.fireMode", {},
190 "emergence.rule_engine"));
191 }
192 const auto* conditionValue = objField(*object, "condition");
193 if (conditionValue == nullptr)
195 "rule requires condition", "rules.condition", {},
196 "emergence.rule_engine"));
197 auto condition = decodeCondition(*conditionValue);
198 if (!condition) return eve::Result<int>::failure(condition.status());
199 rule.condition = std::move(condition).takeValue();
200 if (const auto* watch = objField(*object, "watchKeys")) {
201 const auto* array = watch->getIf<eve::Value::Array>();
202 if (array == nullptr)
204 "watchKeys must be an array", "rules.watchKeys",
205 {}, "emergence.rule_engine"));
206 for (const auto& item : *array) {
207 if (!item.isString() || item.asString().empty())
209 eve::DiagnosticCode::InvalidArgument, "watch key must be a non-empty string", "rules.watchKeys",
210 {}, "emergence.rule_engine"));
211 rule.watchKeys.push_back(item.asString());
212 }
213 }
214 if (const auto* actions = objField(*object, "actions")) {
215 const auto* array = actions->getIf<eve::Value::Array>();
216 if (array == nullptr)
218 "actions must be an array", "rules.actions", {},
219 "emergence.rule_engine"));
220 for (const auto& item : *array) {
221 const auto* actionObject = item.getIf<eve::Value::Object>();
222 if (actionObject == nullptr)
224 "action must be an object", "rules.actions",
225 {}, "emergence.rule_engine"));
227 const auto* kind = objField(*actionObject, "kind");
228 if (kind == nullptr || !kind->isString() || kind->asString().empty())
230 eve::DiagnosticCode::InvalidArgument, "action kind must be a non-empty string",
231 "rules.actions.kind", {}, "emergence.rule_engine"));
232 action.kind = kind->asString();
233 if (const auto* args = objField(*actionObject, "args")) action.args = *args;
234 rule.actions.push_back(std::move(action));
235 }
236 }
237 rules.push_back(std::move(rule));
238 }
239 return replaceCatalogue(std::move(rules));
240}
241
242void RuleEngine::clear() {
243 facts_.clear();
244 watchIndex_.clear();
245 rules_.clear();
246 runtime_.clear();
247 idToIndex_.clear();
248 dirty_.clear();
249 activations_.clear();
250 nextSequence_ = 1;
251 lastDrainEvaluations_ = 0;
252 wakeKeyEvents_ = 0;
253}
254
255bool RuleEngine::contains(std::string_view ruleId) const { return idToIndex_.contains(std::string(ruleId)); }
256
257const RuleDefinition* RuleEngine::find(std::string_view ruleId) const {
258 auto it = idToIndex_.find(std::string(ruleId));
259 if (it == idToIndex_.end()) return nullptr;
260 return &rules_[it->second];
261}
262
263void RuleEngine::wakeKey(std::string_view key) {
264 std::vector<std::uint32_t> hit;
265 if (watchIndex_.collect(key, hit) == 0) return;
266 ++wakeKeyEvents_;
267 for (const auto index : hit) dirty_.insert(index);
268}
269
270eve::Result<bool> RuleEngine::setValue(std::string key, eve::Value value) {
271 if (key.empty()) return eve::Result<bool>::failure(
272 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "value key must be non-empty", "key", {}, "emergence.rule_engine"));
273 const bool changed = facts_.setValue(key, std::move(value)) == FactChange::Changed;
274 if (changed) wakeKey(makeFactKey(FactDomain::Value, key));
275 return eve::Result<bool>::success(changed,
277}
278
279eve::Result<bool> RuleEngine::setTag(std::string tag, bool present) {
280 if (tag.empty()) return eve::Result<bool>::failure(
281 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "tag must be non-empty", "tag", {}, "emergence.rule_engine"));
282 const bool changed = facts_.setTag(tag, present) == FactChange::Changed;
283 if (changed) wakeKey(makeFactKey(FactDomain::Tag, tag));
284 return eve::Result<bool>::success(changed,
286}
287
288eve::Result<bool> RuleEngine::setAttribute(std::string key, eve::Value value) {
289 if (key.empty()) return eve::Result<bool>::failure(
290 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "attribute key must be non-empty", "key", {}, "emergence.rule_engine"));
291 const bool changed = facts_.setAttribute(key, std::move(value)) == FactChange::Changed;
292 if (changed) wakeKey(makeFactKey(FactDomain::Attribute, key));
293 return eve::Result<bool>::success(changed,
295}
296
297eve::Result<bool> RuleEngine::setResource(std::string key, eve::Value value) {
298 if (key.empty()) return eve::Result<bool>::failure(
299 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "resource key must be non-empty", "key", {}, "emergence.rule_engine"));
300 const bool changed = facts_.setResource(key, std::move(value)) == FactChange::Changed;
301 if (changed) wakeKey(makeFactKey(FactDomain::Resource, key));
302 return eve::Result<bool>::success(changed,
304}
305
306eve::Result<bool> RuleEngine::setState(std::string key, eve::Value value) {
307 if (key.empty()) return eve::Result<bool>::failure(
308 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "state key must be non-empty", "key", {}, "emergence.rule_engine"));
309 const bool changed = facts_.setState(key, std::move(value)) == FactChange::Changed;
310 if (changed) wakeKey(makeFactKey(FactDomain::State, key));
311 return eve::Result<bool>::success(changed,
313}
314
315eve::Result<bool> RuleEngine::setAuthority(std::string scope, bool granted) {
316 if (scope.empty())
318 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "authority scope must be non-empty", "scope", {}, "emergence.rule_engine"));
319 const bool changed = facts_.setAuthority(scope, granted) == FactChange::Changed;
320 if (changed) wakeKey(makeFactKey(FactDomain::Authority, scope));
321 return eve::Result<bool>::success(changed,
323}
324
325eve::Result<bool> RuleEngine::setPolicy(std::string name, decision::ConditionResult result) {
326 if (name.empty()) return eve::Result<bool>::failure(
327 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "policy name must be non-empty", "name", {}, "emergence.rule_engine"));
328 const std::string wake = makeFactKey(FactDomain::Policy, name);
329 const bool changed = facts_.setPolicy(std::move(name), std::move(result)) == FactChange::Changed;
330 if (changed) wakeKey(wake);
331 return eve::Result<bool>::success(changed,
333}
334
335eve::Result<void> RuleEngine::executeOne(const EmergenceAction& action, std::uint64_t /*tick*/) {
336 if (action.kind == "fact.set") {
337 const auto* object = action.args.getIf<eve::Value::Object>();
338 if (object == nullptr)
340 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "fact.set args must be an object", "args", {}, "emergence.rule_engine"));
341 const auto* domain = objField(*object, "domain");
342 const auto* key = objField(*object, "key");
343 const auto* value = objField(*object, "value");
344 if (domain == nullptr || !domain->isString() || key == nullptr || !key->isString() || value == nullptr)
346 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "fact.set requires domain, key, value", "args", {}, "emergence.rule_engine"));
347 const auto& domainText = domain->asString();
348 const auto& keyText = key->asString();
349 if (domainText == "value")
350 return setValue(keyText, *value).andThen([](bool) { return eve::Result<void>::success(); });
351 if (domainText == "tag") {
352 if (!value->isBool()) return eve::Result<void>::failure(
353 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "tag value must be bool", "value", {}, "emergence.rule_engine"));
354 return setTag(keyText, value->asBool()).andThen([](bool) { return eve::Result<void>::success(); });
355 }
356 if (domainText == "attr")
357 return setAttribute(keyText, *value).andThen([](bool) { return eve::Result<void>::success(); });
358 if (domainText == "resource")
359 return setResource(keyText, *value).andThen([](bool) { return eve::Result<void>::success(); });
360 if (domainText == "state")
361 return setState(keyText, *value).andThen([](bool) { return eve::Result<void>::success(); });
362 if (domainText == "authority") {
363 if (!value->isBool())
365 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "authority value must be bool", "value", {}, "emergence.rule_engine"));
366 return setAuthority(keyText, value->asBool()).andThen([](bool) { return eve::Result<void>::success(); });
367 }
369 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "unknown fact.set domain", "domain", {}, "emergence.rule_engine"));
370 }
371 if (action.kind == "economy.credit" || action.kind == "economy.debit") {
372 auto* economy = eve::cap::query<eve::economy::IEconomy>();
373 if (economy == nullptr)
375 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "IEconomy provider is absent", action.kind.c_str() ? action.kind.c_str() : "", {}, "emergence.rule_engine"));
376 const auto* object = action.args.getIf<eve::Value::Object>();
377 if (object == nullptr)
379 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "economy action args must be an object", "args", {}, "emergence.rule_engine"));
380 const auto* player = objField(*object, "player");
381 const auto* type = objField(*object, "type");
382 const auto* amount = objField(*object, "amount");
383 if (player == nullptr || type == nullptr || !type->isString() || amount == nullptr)
385 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "economy action requires player, type, amount", "args", {}, "emergence.rule_engine"));
386 auto playerId = requireInt(*player, "player");
387 if (!playerId) return eve::Result<void>::failure(playerId.status());
388 auto qty = requireInt(*amount, "amount");
389 if (!qty) return eve::Result<void>::failure(qty.status());
390 if (qty.value() < 0) return eve::Result<void>::failure(
391 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "amount must be non-negative", "amount", {}, "emergence.rule_engine"));
392 if (action.kind == "economy.credit") {
393 economy->credit(playerId.value(), type->asString(), qty.value());
395 }
396 if (!economy->debit(playerId.value(), type->asString(), qty.value()))
398 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "economy debit rejected", "amount", {}, "emergence.rule_engine"));
400 }
401 if (action.kind == "emit") {
403 }
404
406 bool consumed = false;
407 eve::cap::forEach<eve::IEmergenceActionHandler>([&](eve::IEmergenceActionHandler* handler) {
408 if (consumed || !handlerFailure.ok()) return;
409 auto handled = handler->tryHandle(action.kind, action.args);
410 if (!handled) {
411 handlerFailure = eve::Result<void>::failure(handled.status());
412 return;
413 }
414 if (handled.value()) consumed = true;
415 });
416 if (!handlerFailure.ok()) return handlerFailure;
417 if (!consumed && action.kind != "emit") {
418 // Unhandled domain actions remain in the activation log for scripts.
420 }
422}
423
424eve::Result<void> RuleEngine::executeActions(const std::vector<EmergenceAction>& actions, std::uint64_t tick) {
425 for (const auto& action : actions) {
426 auto executed = executeOne(action, tick);
427 if (!executed) return executed;
428 }
430}
431
432eve::Result<int> RuleEngine::drain(std::uint64_t tick) {
433 if (draining_)
435 "nested drain is not allowed", {}, {},
436 "emergence.rule_engine"));
437 draining_ = true;
438 lastDrainEvaluations_ = 0;
439 int produced = 0;
440
441 struct Guard {
442 bool& flag;
443 ~Guard() { flag = false; }
444 } guard{draining_};
445
446 struct PendingFire {
447 Activation activation;
448 std::uint32_t index = 0;
449 bool nowPassed = false;
450 bool once = false;
451 std::uint64_t cooldownTicks = 0;
452 };
453
454 // Fixed-point: evaluate current dirty set, execute activations (which may dirty more), repeat.
455 constexpr int kMaxPasses = 64;
456 for (int pass = 0; pass < kMaxPasses; ++pass) {
457 if (dirty_.empty()) break;
458 std::vector<std::uint32_t> batch(dirty_.begin(), dirty_.end());
459 dirty_.clear();
460 std::sort(batch.begin(), batch.end(), [&](std::uint32_t left, std::uint32_t right) {
461 if (rules_[left].priority != rules_[right].priority) return rules_[left].priority > rules_[right].priority;
462 return rules_[left].id < rules_[right].id;
463 });
464
465 std::vector<PendingFire> pending;
466 for (const auto index : batch) {
467 if (index >= rules_.size()) continue;
468 auto& runtime = runtime_[index];
469 const auto& rule = rules_[index];
470 if (runtime.disabled) continue;
471 if (runtime.cooldownUntil > tick) continue;
472 ++lastDrainEvaluations_;
473 const auto result = rule.condition.evaluate(facts_);
474 const bool nowPassed = result.passed();
475 bool fire = false;
476 if (rule.fireMode == FireMode::Rising)
477 fire = !runtime.passed && nowPassed;
478 else
479 fire = nowPassed;
480 if (!fire) {
481 runtime.passed = nowPassed;
482 continue;
483 }
484 PendingFire entry;
485 entry.activation.sequence = nextSequence_++;
486 entry.activation.tick = tick;
487 entry.activation.ruleId = rule.id;
488 entry.activation.priority = rule.priority;
489 entry.activation.actions = rule.actions;
490 entry.index = index;
491 entry.nowPassed = nowPassed;
492 entry.once = rule.once;
493 entry.cooldownTicks = rule.cooldownTicks;
494 pending.push_back(std::move(entry));
495 }
496
497 for (std::size_t i = 0; i < pending.size(); ++i) {
498 auto& entry = pending[i];
499 auto executed = executeActions(entry.activation.actions, tick);
500 if (!executed) {
501 for (std::size_t j = i; j < pending.size(); ++j) dirty_.insert(pending[j].index);
502 return eve::Result<int>::failure(executed.status());
503 }
504 auto& runtime = runtime_[entry.index];
505 runtime.passed = entry.nowPassed;
506 runtime.fired = true;
507 if (entry.once) runtime.disabled = true;
508 if (entry.cooldownTicks > 0) runtime.cooldownUntil = tick + entry.cooldownTicks;
509 activations_.push_back(std::move(entry.activation));
510 ++produced;
511 }
512 }
513 if (!dirty_.empty())
515 "emergence drain exceeded cascade pass budget", {}, {},
516 "emergence.rule_engine"));
519}
520
521const Activation* RuleEngine::activationAt(int index) const {
522 if (index < 0 || index >= static_cast<int>(activations_.size())) return nullptr;
523 return &activations_[static_cast<std::size_t>(index)];
524}
525
526void RuleEngine::clearActivations() { activations_.clear(); }
527
528eve::Result<void> RuleEngine::resetRule(std::string_view ruleId) {
529 auto it = idToIndex_.find(std::string(ruleId));
530 if (it == idToIndex_.end()) return eve::Result<void>::failure(
531 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "rule id not found", "ruleId", {}, "emergence.rule_engine"));
532 auto& runtime = runtime_[it->second];
533 runtime.disabled = false;
534 runtime.fired = false;
535 runtime.passed = false;
536 runtime.cooldownUntil = 0;
537 dirty_.insert(it->second);
539}
540
541std::string RuleEngine::snapshotJson() const {
543 root.emplace("schema", eve::Value("eve.emergence.runtime"));
544 root.emplace("version", eve::Value(std::int64_t{1}));
545 auto factsJson = eve::Value::fromJson(facts_.snapshotJson());
546 if (factsJson) root.emplace("facts", std::move(factsJson).takeValue());
547 eve::Value::Array rules;
548 for (std::size_t i = 0; i < rules_.size(); ++i) {
550 row.emplace("id", eve::Value(rules_[i].id));
551 row.emplace("passed", eve::Value(runtime_[i].passed));
552 row.emplace("fired", eve::Value(runtime_[i].fired));
553 row.emplace("disabled", eve::Value(runtime_[i].disabled));
554 row.emplace("cooldownUntil", eve::Value(static_cast<std::int64_t>(runtime_[i].cooldownUntil)));
555 rules.push_back(eve::Value(std::move(row)));
556 }
557 root.emplace("rules", eve::Value(std::move(rules)));
558 auto encoded = eve::Value(std::move(root)).toJson();
559 return encoded ? std::move(encoded).takeValue() : std::string("{}");
560}
561
562eve::Result<void> RuleEngine::restoreJson(std::string_view json) {
563 auto parsed = eve::Value::fromJson(json);
564 if (!parsed) return eve::Result<void>::failure(parsed.status());
565 const auto* root = parsed.value().getIf<eve::Value::Object>();
566 if (root == nullptr) return eve::Result<void>::failure(
567 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "runtime snapshot must be an object", "", {}, "emergence.rule_engine"));
568 if (!exactFields(*root, {"schema", "version", "facts", "rules"}))
570 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "runtime snapshot contains an unknown field", "", {}, "emergence.rule_engine"));
571 const auto* schema = objField(*root, "schema");
572 const auto* version = objField(*root, "version");
573 if (schema == nullptr || !schema->isString() || schema->asString() != "eve.emergence.runtime")
575 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "unexpected runtime schema", "schema", {}, "emergence.rule_engine"));
576 if (version == nullptr || !version->isInt64() || version->asInt() != 1)
578 eve::Diagnostic::error(eve::DiagnosticCode::UnknownVersion, "unsupported runtime version", "version", {}, "emergence.rule_engine"));
579 const auto* factsValue = objField(*root, "facts");
580 const auto* rulesValue = objField(*root, "rules");
581 if (factsValue == nullptr)
583 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "runtime snapshot requires facts", "facts", {}, "emergence.rule_engine"));
584 if (rulesValue == nullptr)
586 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "runtime snapshot requires rules", "rules", {}, "emergence.rule_engine"));
587
588 FactStore nextFacts;
589 {
590 auto encoded = factsValue->toJson();
591 if (!encoded) return eve::Result<void>::failure(encoded.status());
592 auto restored = nextFacts.restoreJson(encoded.value());
593 if (!restored) return restored;
594 }
595
596 std::vector<RuleRuntime> nextRuntime(runtime_.size());
597 const auto* array = rulesValue->getIf<eve::Value::Array>();
598 if (array == nullptr) return eve::Result<void>::failure(
599 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "rules must be an array", "rules", {}, "emergence.rule_engine"));
600 std::unordered_set<std::string> seenIds;
601 for (const auto& item : *array) {
602 const auto* object = item.getIf<eve::Value::Object>();
603 if (object == nullptr) return eve::Result<void>::failure(
604 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "rule latch must be an object", "", {}, "emergence.rule_engine"));
605 if (!exactFields(*object, {"id", "passed", "fired", "disabled", "cooldownUntil"}))
607 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "rule latch contains an unknown field", "", {}, "emergence.rule_engine"));
608 const auto* id = objField(*object, "id");
609 if (id == nullptr || !id->isString() || id->asString().empty())
611 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "rule latch requires id", "id", {}, "emergence.rule_engine"));
612 if (!seenIds.insert(id->asString()).second)
614 eve::Diagnostic::error(eve::DiagnosticCode::AlreadyExists, "duplicate rule latch id", "id", {}, "emergence.rule_engine"));
615 auto it = idToIndex_.find(id->asString());
616 if (it == idToIndex_.end())
618 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "runtime rule id missing from catalogue", "id", {}, "emergence.rule_engine"));
619 auto& runtime = nextRuntime[it->second];
620 const auto* passed = objField(*object, "passed");
621 const auto* fired = objField(*object, "fired");
622 const auto* disabled = objField(*object, "disabled");
623 const auto* cooldown = objField(*object, "cooldownUntil");
624 if (passed == nullptr || !passed->isBool())
626 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "passed must be bool", "passed", {}, "emergence.rule_engine"));
627 if (fired == nullptr || !fired->isBool())
629 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "fired must be bool", "fired", {}, "emergence.rule_engine"));
630 if (disabled == nullptr || !disabled->isBool())
632 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "disabled must be bool", "disabled", {}, "emergence.rule_engine"));
633 if (cooldown == nullptr)
635 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "cooldownUntil is required", "cooldownUntil", {}, "emergence.rule_engine"));
636 auto cooldownValue = requireNonNegativeU64(*cooldown, "cooldownUntil");
637 if (!cooldownValue) return eve::Result<void>::failure(cooldownValue.status());
638 runtime.passed = passed->asBool();
639 runtime.fired = fired->asBool();
640 runtime.disabled = disabled->asBool();
641 runtime.cooldownUntil = cooldownValue.value();
642 }
643 if (seenIds.size() != rules_.size())
645 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "runtime snapshot must include every catalogue rule", "rules", {}, "emergence.rule_engine"));
646
647 facts_ = std::move(nextFacts);
648 runtime_ = std::move(nextRuntime);
649 dirty_.clear();
651}
652
653} // namespace eve::emergence
double value
int root
Definition AnimSmr.cpp:119
std::vector< QuestEvent > pending
int priority
Value/JSON codec for decision::Condition trees owned by emergence rules.
Derive inverted-index watch keys from a decision::Condition AST.
Stable, structured diagnostics shared by engine modules.
eve::Value condition
std::uint32_t key
glm::uvec4 ids
HexVec3 left
HexVec3 right
Optional multi-listener sink for emergence rule actions outside built-ins.
JobScope scope
TokenKind kind
HSQOBJECT handler
std::string name
const std::string * tag
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
bool hit
High-performance indexed rule engine for emergent gameplay triggers.
SimulationTick tick
Json object
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
Capability listener that may consume one emergence action kind.
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
auto andThen(Function &&function) &&-> std::invoke_result_t< Function, T && >
Compose a successful value with a function returning another Result.
Definition Result.h:365
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
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
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
Explain one condition evaluation in a UI- and log-friendly form.
Definition Condition.h:129
Mutable fact bag that doubles as a read-only Condition evaluation context.
Definition FactStore.h:60
eve::Result< void > restoreJson(std::string_view json)
Transactionally restore facts from a snapshot produced by snapshotJson().
Dense, sorted multi-map used to wake O(watchers) rules on a fact change.
Definition WatchIndex.h:23
eve::Result< std::vector< std::string > > collectWatchKeys(const decision::Condition &condition)
Collect the canonical fact keys a condition depends on.
eve::Result< std::pair< FactDomain, std::string > > parseFactKey(std::string_view key)
Parse a canonical watch key into domain and leaf name.
std::string makeFactKey(FactDomain domain, std::string_view name)
Build the canonical watch key for one fact domain.
eve::Result< decision::Condition > decodeCondition(const eve::Value &value)
Decode one validated condition tree from an owning Value.
One scheduled activation produced by drain().
Definition RuleTypes.h:50
One deferred side-effect descriptor; domains interpret kind.
Definition RuleTypes.h:27
One immutable rule definition owned by the catalogue.
Definition RuleTypes.h:38
std::vector< std::string > watchKeys
Definition RuleTypes.h:45
std::vector< EmergenceAction > actions
Definition RuleTypes.h:46
decision::Condition condition
Definition RuleTypes.h:44
bool consumed
Definition Graphics.cpp:184