19constexpr std::size_t kMaximumConditionSource = 4096;
20constexpr std::size_t kMaximumConditionNodes = 256;
21constexpr std::size_t kMaximumConditionDepth = 32;
22constexpr std::size_t kMaximumEvaluationSteps = 1024;
26enum class ExpressionKind : std::uint8_t {
50struct ExpressionNode {
51 ExpressionKind
kind = ExpressionKind::Literal;
52 ExpressionType
type = ExpressionType::Boolean;
60struct ExpressionProgram {
61 std::vector<ExpressionNode>
nodes;
65enum class TokenKind : std::uint8_t {
End, Identifier,
Number,
String, Symbol };
68 TokenKind
kind = TokenKind::End;
74const char* typeName(ExpressionType
type)
noexcept {
76 case ExpressionType::Unknown:
return "Unknown";
77 case ExpressionType::Boolean:
return "Boolean";
78 case ExpressionType::Number:
return "Number";
79 case ExpressionType::String:
return "String";
85 const Token&
token, std::string expected = {}, std::string actual = {}) {
87 if (!expected.empty()) details.emplace_back(
"expected", std::move(expected));
88 if (!actual.empty()) details.emplace_back(
"actual", std::move(actual));
90 "settlement.condition");
98 auto advance = [&](
char value) {
116 if (std::isalpha(
static_cast<unsigned char>(
value)) ||
value ==
'_') {
117 token.kind = TokenKind::Identifier;
120 if (!std::isalnum(
static_cast<unsigned char>(next)) && next !=
'_' && next !=
'.')
break;
121 token.text.push_back(next);
127 if (std::isdigit(
static_cast<unsigned char>(
value)) ||
129 std::isdigit(
static_cast<unsigned char>(
source[
offset + 1])))) {
130 token.kind = TokenKind::Number;
134 const double number = std::strtod(remaining.c_str(), &
end);
135 if (
end == remaining.c_str() || !std::isfinite(
number))
139 const auto length =
static_cast<std::size_t
>(
end - remaining.c_str());
146 token.kind = TokenKind::String;
156 if (next ==
'\n' || next ==
'\r')
break;
162 case 'n':
token.text.push_back(
'\n');
break;
163 case 'r':
token.text.push_back(
'\r');
break;
164 case 't':
token.text.push_back(
'\t');
break;
165 case '"':
token.text.push_back(
'"');
break;
166 case '\\':
token.text.push_back(
'\\');
break;
170 path,
token,
"supported escape", std::string(
"\\") + escaped));
175 token.text.push_back(next);
181 token,
"closing quote"));
185 token.kind = TokenKind::Symbol;
189 if (pair ==
"&&" || pair ==
"||" || pair ==
"==" || pair ==
"!=" || pair ==
"<=" || pair ==
">=") {
197 if (std::string_view(
"!<>+-*/(),").find(
value) == std::string_view::npos)
208class ExpressionParser {
210 ExpressionParser(std::vector<Token>
tokens, std::string
path)
214 const auto root = parseOr(0);
217 fail(
"condition contains trailing input",
"end of expression",
current().
text);
220 if (nodes_[
root].
type != ExpressionType::Boolean) {
221 fail(
"condition root must be Boolean",
"Boolean", typeName(nodes_[
root].
type));
228 const Token&
current() const noexcept {
return tokens_[position_]; }
230 bool accept(std::string_view
text) {
236 void expect(std::string_view
text) {
240 void fail(std::string
message, std::string expected = {}, std::string actual = {}) {
243 std::move(expected), std::move(actual));
246 std::uint16_t add(ExpressionNode
node) {
247 if (nodes_.size() >= kMaximumConditionNodes) {
248 fail(
"condition exceeds the node limit", std::to_string(kMaximumConditionNodes));
251 nodes_.push_back(std::move(
node));
252 return static_cast<std::uint16_t
>(nodes_.size() - 1);
255 std::uint16_t binary(ExpressionKind
kind, ExpressionType result, std::uint16_t
left, std::uint16_t
right,
256 ExpressionType operand,
const Token&
operation) {
257 if (error_)
return 0;
259 bind(
right, operand);
261 error_ = expressionDiagnostic(
263 typeName(operand), std::string(typeName(nodes_[
left].
type)) +
"," + typeName(nodes_[
right].
type));
269 void bind(std::uint16_t
index, ExpressionType
type) {
270 if (nodes_[
index].
type == ExpressionType::Unknown && nodes_[
index].
kind == ExpressionKind::Field)
274 std::uint16_t parseOr(std::size_t
depth) {
279 left = binary(ExpressionKind::Or, ExpressionType::Boolean,
left, parseAnd(
depth + 1),
285 std::uint16_t parseAnd(std::size_t
depth) {
290 left = binary(ExpressionKind::And, ExpressionType::Boolean,
left, parseEquality(
depth + 1),
296 std::uint16_t parseEquality(std::size_t
depth) {
301 const auto right = parseComparison(
depth + 1);
302 if (!error_ && nodes_[
left].
type == ExpressionType::Unknown &&
303 nodes_[
right].
type != ExpressionType::Unknown)
305 if (!error_ && nodes_[
right].
type == ExpressionType::Unknown &&
306 nodes_[
left].
type != ExpressionType::Unknown)
308 if (!error_ && nodes_[
left].
type == ExpressionType::Unknown &&
309 nodes_[
right].
type == ExpressionType::Unknown) {
311 "condition cannot infer either equality operand type", path_,
operation,
312 "one typed operand",
"Unknown,Unknown");
317 "condition equality operands must have the same type", path_,
operation,
321 left = add(ExpressionNode{
operation.text ==
"==" ? ExpressionKind::Equal : ExpressionKind::NotEqual,
327 std::uint16_t parseComparison(std::size_t
depth) {
334 ExpressionKind
kind = ExpressionKind::Less;
336 kind = ExpressionKind::LessEqual;
338 kind = ExpressionKind::Greater;
340 kind = ExpressionKind::GreaterEqual;
346 std::uint16_t parseAdditive(std::size_t
depth) {
347 auto left = parseMultiplicative(
depth + 1);
358 std::uint16_t parseMultiplicative(std::size_t
depth) {
364 if (!error_ &&
operation.text ==
"/" && nodes_[
right].kind == ExpressionKind::Literal) {
367 "condition contains constant division by zero", path_,
operation,
368 "non-zero Number",
"0");
378 std::uint16_t parseUnary(std::size_t
depth) {
379 if (
depth > kMaximumConditionDepth) {
380 fail(
"condition exceeds the nesting limit", std::to_string(kMaximumConditionDepth));
387 const auto expected =
operation.text ==
"!" ? ExpressionType::Boolean : ExpressionType::Number;
388 if (!error_) bind(child, expected);
389 if (!error_ && nodes_[child].
type != expected) {
391 "condition unary operator has an incompatible operand", path_,
operation,
392 typeName(expected), typeName(nodes_[child].
type));
396 ExpressionNode{
operation.text ==
"!" ? ExpressionKind::Not : ExpressionKind::Negate, expected,
child});
398 return parsePrimary(
depth + 1);
401 std::uint16_t parseFunction(std::string
name,
const Token&
token, std::size_t
depth) {
403 std::vector<std::uint16_t> arguments;
406 arguments.push_back(parseOr(
depth + 1));
407 }
while (!error_ && accept(
","));
410 if (error_)
return 0;
411 if (
name ==
"has_tag") {
412 if (arguments.size() != 1 || nodes_[arguments[0]].kind != ExpressionKind::Literal ||
413 nodes_[arguments[0]].type != ExpressionType::String) {
415 path_,
token,
"has_tag(String literal)");
418 ExpressionNode
node{ExpressionKind::HasTag, ExpressionType::Boolean};
419 node.name = *nodes_[arguments[0]].literal.getIf<std::string>();
420 return add(std::move(
node));
422 const std::size_t arity =
name ==
"clamp" ? 3 : (
name ==
"abs" ? 1 : 2);
423 if ((
name !=
"min" &&
name !=
"max" &&
name !=
"clamp" &&
name !=
"abs") || arguments.size() != arity) {
425 "condition uses an unknown function or invalid arity", path_,
token,
426 "min/max(2), clamp(3), abs(1), or has_tag(1)",
name);
429 for (
const auto argument : arguments) {
430 bind(argument, ExpressionType::Number);
431 if (nodes_[argument].
type != ExpressionType::Number) {
434 path_,
token,
"Number", typeName(nodes_[argument].
type));
439 node.type = ExpressionType::Number;
440 node.left = arguments[0];
441 node.right = arguments.size() > 1 ? arguments[1] : 0;
442 node.third = arguments.size() > 2 ? arguments[2] : 0;
444 node.kind = ExpressionKind::Minimum;
445 else if (
name ==
"max")
446 node.kind = ExpressionKind::Maximum;
447 else if (
name ==
"clamp")
448 node.kind = ExpressionKind::Clamp;
450 node.kind = ExpressionKind::Absolute;
451 return add(std::move(
node));
454 std::uint16_t parsePrimary(std::size_t
depth) {
455 if (
depth > kMaximumConditionDepth) {
456 fail(
"condition exceeds the nesting limit", std::to_string(kMaximumConditionDepth));
461 const auto expression = parseOr(
depth + 1);
465 if (
token.kind == TokenKind::Number) {
467 ExpressionNode
node{ExpressionKind::Literal, ExpressionType::Number};
469 return add(std::move(
node));
471 if (
token.kind == TokenKind::String) {
473 ExpressionNode
node{ExpressionKind::Literal, ExpressionType::String};
475 return add(std::move(
node));
477 if (
token.kind != TokenKind::Identifier) {
478 fail(
"condition expected a literal, field, function, or parenthesized expression",
"expression",
483 if (
token.text ==
"true" ||
token.text ==
"false") {
484 ExpressionNode
node{ExpressionKind::Literal, ExpressionType::Boolean};
486 return add(std::move(
node));
489 ExpressionNode
node{ExpressionKind::Field, ExpressionType::Unknown};
491 if (
token.text ==
"kind" ||
token.text ==
"resource")
492 node.type = ExpressionType::String;
493 else if (
token.text ==
"magnitude" ||
token.text ==
"tick")
494 node.type = ExpressionType::Number;
495 else if (!
token.text.starts_with(
"context.") ||
token.text.size() == std::string_view(
"context.").size()) {
497 token,
"kind, resource, magnitude, tick, or context.<name>",
token.text);
500 return add(std::move(
node));
503 std::vector<Token> tokens_;
505 std::size_t position_ = 0;
506 std::vector<ExpressionNode> nodes_;
507 std::optional<eve::Diagnostic> error_;
510struct EvaluatedValue {
511 ExpressionType
type = ExpressionType::Boolean;
512 bool boolean =
false;
524 while (!
path.empty()) {
527 if (part.empty() || !
current->isObject())
return nullptr;
530 if (
split == std::string_view::npos)
break;
538 if (++
steps > kMaximumEvaluationSteps)
540 "condition exceeded the evaluation step limit",
"when");
542 if (
node.kind == ExpressionKind::Literal) {
543 EvaluatedValue result;
544 result.type =
node.type;
545 if (
const auto*
value =
node.literal.getIf<
bool>())
546 result.boolean = *
value;
547 else if (
const auto*
value =
node.literal.getIf<
double>())
548 result.number = *
value;
549 else if (
const auto*
value =
node.literal.getIf<std::string>())
550 result.string = *
value;
553 if (
node.kind == ExpressionKind::HasTag) {
555 EvaluatedValue result;
556 result.type = ExpressionType::Boolean;
557 result.boolean = present;
560 if (
node.kind == ExpressionKind::Field) {
561 EvaluatedValue result;
562 result.type =
node.type;
563 if (
node.name ==
"kind")
565 else if (
node.name ==
"resource")
566 result.string =
request.resource;
567 else if (
node.name ==
"magnitude")
568 result.number =
request.magnitude;
569 else if (
node.name ==
"tick")
570 result.number =
static_cast<double>(
request.tick.value());
572 auto contextPath = std::string_view(
node.name).substr(std::string_view(
"context.").
size());
573 const auto*
value = findContextValue(
request.context, contextPath);
577 if (
node.type == ExpressionType::Boolean) {
578 const auto* typed =
value->getIf<
bool>();
581 "condition context field is not Boolean",
node.name);
582 result.boolean = *typed;
583 }
else if (
node.type == ExpressionType::String) {
584 const auto* typed =
value->getIf<std::string>();
588 result.string = *typed;
589 }
else if (
const auto* integer =
value->getIf<std::int64_t>()) {
590 result.number =
static_cast<double>(*integer);
591 }
else if (
const auto* real =
value->getIf<
double>(); real && std::isfinite(*real)) {
592 result.number = *real;
595 "condition context field is not a finite Number",
node.name);
601 if (!leftResult)
return leftResult;
602 auto left = std::move(leftResult).takeValue();
603 if (
node.kind == ExpressionKind::Not) {
607 if (
node.kind == ExpressionKind::Negate) {
611 if (
node.kind == ExpressionKind::Absolute) {
612 left.number = std::abs(
left.number);
618 if (!rightResult)
return rightResult;
619 auto right = std::move(rightResult).takeValue();
620 EvaluatedValue result;
622 case ExpressionKind::Add:
623 result.type = ExpressionType::Number;
624 result.number =
left.number +
right.number;
626 case ExpressionKind::Subtract:
627 result.type = ExpressionType::Number;
628 result.number =
left.number -
right.number;
630 case ExpressionKind::Multiply:
631 result.type = ExpressionType::Number;
632 result.number =
left.number *
right.number;
634 case ExpressionKind::Divide:
635 if (
right.number == 0.0)
638 result.type = ExpressionType::Number;
639 result.number =
left.number /
right.number;
641 case ExpressionKind::Minimum:
642 result.type = ExpressionType::Number;
643 result.number = std::min(
left.number,
right.number);
645 case ExpressionKind::Maximum:
646 result.type = ExpressionType::Number;
647 result.number = std::max(
left.number,
right.number);
649 case ExpressionKind::Clamp: {
651 if (!thirdResult)
return thirdResult;
652 const auto third = std::move(thirdResult).takeValue();
655 "condition clamp minimum exceeds maximum",
"when");
656 result.type = ExpressionType::Number;
657 result.number = std::clamp(
left.number,
right.number,
third.number);
660 case ExpressionKind::Equal:
661 case ExpressionKind::NotEqual: {
662 result.type = ExpressionType::Boolean;
664 if (
left.type == ExpressionType::Boolean)
666 else if (
left.type == ExpressionType::Number)
670 result.boolean =
node.kind == ExpressionKind::Equal ? equal : !equal;
673 case ExpressionKind::Less:
674 result.type = ExpressionType::Boolean;
675 result.boolean =
left.number <
right.number;
677 case ExpressionKind::LessEqual:
678 result.type = ExpressionType::Boolean;
679 result.boolean =
left.number <=
right.number;
681 case ExpressionKind::Greater:
682 result.type = ExpressionType::Boolean;
683 result.boolean =
left.number >
right.number;
685 case ExpressionKind::GreaterEqual:
686 result.type = ExpressionType::Boolean;
687 result.boolean =
left.number >=
right.number;
689 case ExpressionKind::And:
690 result.type = ExpressionType::Boolean;
691 result.boolean =
left.boolean &&
right.boolean;
693 case ExpressionKind::Or:
694 result.type = ExpressionType::Boolean;
695 result.boolean =
left.boolean ||
right.boolean;
699 "condition contains an invalid instruction",
"when");
701 if (result.type == ExpressionType::Number && !std::isfinite(result.number))
707std::string digestCondition(std::string_view
source) {
708 std::uint64_t
hash = 1469598103934665603ull;
709 constexpr std::string_view version =
"settlement-condition-v1\n";
710 for (
const char value : version) {
711 hash ^=
static_cast<unsigned char>(
value);
712 hash *= 1099511628211ull;
715 hash ^=
static_cast<unsigned char>(
value);
716 hash *= 1099511628211ull;
718 std::ostringstream stream;
719 stream << std::hex << std::setfill(
'0') << std::setw(16) <<
hash;
723struct CompiledExpression {
724 std::shared_ptr<const ExpressionProgram>
program;
729 if (
source.size() > kMaximumConditionSource) {
733 path,
token, std::to_string(kMaximumConditionSource), std::to_string(
source.size())));
737 ExpressionParser parser(std::move(tokenResult).takeValue(),
path);
738 auto programResult = parser.parse();
740 CompiledExpression compiled;
741 compiled.program = std::make_shared<ExpressionProgram>(std::move(programResult).takeValue());
742 compiled.digest = digestCondition(
source);
746bool contains(
const std::vector<std::string>&
values,
const std::string&
value) {
750bool matches(
const RuleFilter&
filter,
const SettlementRequest&
request) {
752 for (
const auto&
tag :
filter.requiredTags)
753 if (!contains(
request.tags,
tag)) return false;
754 for (
const auto&
tag :
filter.excludedTags)
755 if (contains(
request.tags,
tag)) return false;
761 const std::string& conditionDigest, SettlementContext&
context) {
762 if (!matches(rule.filter,
context.request()))
769 const double operand = rule.value + rule.valuePerExtraStack *
static_cast<double>(rule.stacks - 1);
770 const double before =
context.magnitude();
773 switch (rule.operation) {
777 removed = std::min(before, operand);
778 if (
auto changed =
context.setMagnitude(before -
removed); !changed)
return changed;
782 if (
auto changed =
context.setMagnitude(before -
removed); !changed)
return changed;
785 removed = std::min(before, operand);
786 if (
auto changed =
context.setMagnitude(before -
removed); !changed)
return changed;
793 if (!
context.request().source.isValid() ||
context.projectedResult().applied == 0.0)
795 const double magnitude =
context.projectedResult().applied * operand;
796 if (!std::isfinite(magnitude))
800 SettlementRequest derived;
803 derived.target =
context.request().source;
805 derived.resource =
context.request().resource;
806 derived.magnitude = magnitude;
807 derived.tick =
context.request().tick;
808 derived.correlation =
context.request().correlation;
809 derived.trigger =
"rule:" + rule.id;
811 :
"trigger:lifesteal");
812 return context.emitDerived(std::move(derived));
816 "unknown settlement rule operation",
"operation"));
818 if (!outcome)
return outcome;
822 context.setStageDetail(
"stacks",
eve::Value(
static_cast<std::int64_t
>(rule.stacks)));
824 if (!conditionDigest.empty())
context.setStageDetail(
"condition_digest",
eve::Value(conditionDigest));
832 std::set<std::string>
ids;
833 std::vector<ConditionFunction> conditions;
834 std::vector<std::string> conditionDigests;
835 conditions.reserve(rules.size());
836 conditionDigests.reserve(rules.size());
838 const auto& rule = rules[
index];
839 const auto path =
"rules[" + std::to_string(
index) +
"]";
846 if (!std::isfinite(rule.value) || !std::isfinite(rule.valuePerExtraStack))
858 const bool triggerOperation =
869 (rule.value < 0.0 || rule.valuePerExtraStack < 0.0))
873 (rule.value < 0.0 || rule.value > 1.0 || rule.valuePerExtraStack < 0.0 ||
874 rule.value + rule.valuePerExtraStack *
static_cast<double>(rule.stacks - 1) > 1.0))
877 if (rule.when.empty()) {
878 conditions.emplace_back();
879 conditionDigests.emplace_back();
881 auto compiled = compileExpression(rule.when,
path +
".when");
883 auto expression = std::move(compiled).takeValue();
884 auto program = std::move(expression.program);
886 std::size_t
steps = 0;
891 conditionDigests.push_back(std::move(expression.digest));
894 rules_ = std::move(rules);
895 conditions_ = std::move(conditions);
896 conditionDigests_ = std::move(conditionDigests);
901 struct InstalledRule {
906 std::vector<InstalledRule> rules;
907 rules.reserve(rules_.size());
909 rules.push_back(InstalledRule{rules_[index], conditions_[index], conditionDigests_[index]});
910 std::sort(rules.begin(), rules.end(), [](
const auto&
left,
const auto&
right) {
911 const auto leftStage = static_cast<std::uint8_t>(left.rule.stage);
912 const auto rightStage = static_cast<std::uint8_t>(right.rule.stage);
913 if (leftStage != rightStage) return leftStage < rightStage;
914 if (left.rule.priority != right.rule.priority) return left.rule.priority < right.rule.priority;
915 return left.rule.id < right.rule.id;
917 for (
auto& installed : rules) {
918 const auto stage = installed.rule.stage;
922 const auto name = std::string(
"zz_rule.") + installed.rule.id;
923 const auto priority = installed.rule.priority;
925 [rule = std::move(installed.rule),
condition = std::move(installed.condition),
927 return applyRule(rule, condition, digest, context);
929 if (!added)
return added;
ActionParameterOperation operation
std::map< std::string, Var > values
std::array< std::uint8_t, 32 > hash
const GltfImportRequest & request
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::shared_ptr< const ExpressionProgram > program
Declarative, domain-neutral rules for buffs and gameplay effects.
const UnitySourceAsset & source
const VegetationPresetContext & context
A structured explanation of a failed, degraded, or noteworthy result.
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
The canonical owning dynamic value used by data-facing protocols.
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Mutable calculation frame visible to policy and custom stage code.
Deterministically ordered, composable settlement pipeline.
eve::Result< void > addStage(StageKind kind, std::string name, int priority, StageFunction function)
Add one named custom stage; duplicate names are rejected.
eve::Result< void > configure(std::vector< SettlementRule > rules)
Atomically replace all rules after validating ids, operands and phases.
eve::Result< void > install(SettlementPipeline &pipeline) const
Install a stable snapshot as custom stages on a pipeline.
@ Number
Unsigned numeric literal; sign handling belongs to the parser.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
ModelData parse(std::span< const uint8_t > bytes)
Parse.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
std::vector< DiagnosticDetail > DiagnosticDetails
Owning collection of diagnostic details with stable insertion order.
DiagnosticCode
Stable machine-readable diagnostic codes.
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
enum EVENGINE_API_FOUNDATION String
OT_STRING.
Domain-neutral input to one settlement operation.
One owning, serializable projection of a buff/effect contribution.