Skip to content

Expression Engine

The expression engine is based on the exp4j library, providing math expression evaluation capabilities for EmakiCoreLib with support for built-in functions, variable substitution, and various random distribution configurations.

Basic Usage

The expression engine accepts standard math expression strings and returns a double result:

java
ExpressionService exprService = EmakiServiceRegistry.get(ExpressionService.class);

double result = exprService.evaluate("2 + 3 * 4");       // 14.0
double result2 = exprService.evaluate("ceil(3.2)");       // 4.0
double result3 = exprService.evaluate("max(10, 20)");     // 20.0

Built-in Functions

FunctionDescriptionExampleResult
ceil(x)Ceiling (round up)ceil(3.2)4.0
floor(x)Floor (round down)floor(3.8)3.0
round(x)Round to nearestround(3.5)4.0
log10(x)Base-10 logarithmlog10(100)2.0
min(a, b)Minimum valuemin(3, 7)3.0
max(a, b)Maximum valuemax(3, 7)7.0
pow(a, b)Exponentiationpow(2, 10)1024.0

Additionally, exp4j's standard math functions are supported: abs, sqrt, sin, cos, tan, asin, acos, atan, exp, log (natural logarithm), etc.

Variable Syntax

Use the {varName} format to reference variables in expressions:

java
Map<String, Double> variables = Map.of(
    "base_damage", 10.0,
    "level", 5.0,
    "multiplier", 1.5
);

double result = exprService.evaluate(
    "{base_damage} + {level} * {multiplier}",
    variables
);
// Result: 10.0 + 5.0 * 1.5 = 17.5

Using variables in YAML configuration:

yaml
damage_formula: "{base_damage} + {level} * 2 + pow({strength}, 0.5)"
heal_formula: "max({base_heal}, {level} * 0.8)"
crit_chance: "min(0.75, {base_crit} + {agility} * 0.01)"

Tip

Variable names support letters, numbers, and underscores, and are case-sensitive. Variable values must be numeric types.

Random Distribution Configuration

The expression engine supports multiple random distributions for defining random values in configuration. Each distribution can be configured in YAML using different formats.

constant — Constant

A fixed value with no randomness.

yaml
# Writing a number directly is a constant
drop_count: 5

# Or explicitly declared
drop_count:
  type: constant
  value: 5

range — Range Shorthand

A shorthand using the min~max format, equivalent to a uniform distribution.

yaml
# Shorthand format: random integer between 5 and 15
drop_count: "5~15"

# Supports decimals
damage_bonus: "1.5~3.0"

uniform — Uniform Distribution

Uniformly random within the [min, max] range.

yaml
drop_count:
  type: uniform
  min: 5
  max: 15

gaussian — Gaussian (Normal) Distribution

A normal distribution centered on mean with standard deviation stddev. Optional min / max truncation.

yaml
damage:
  type: gaussian
  mean: 100
  stddev: 15
  min: 50       # Optional, minimum truncation value
  max: 150      # Optional, maximum truncation value

skew_normal — Skew Normal Distribution

Adds a skewness parameter skew on top of the Gaussian distribution. Positive values skew right, negative values skew left.

yaml
rare_drop_quality:
  type: skew_normal
  mean: 50
  stddev: 20
  skew: 3.0     # Positive: right-skewed (higher values are rarer)
  min: 0
  max: 100

triangle — Triangular Distribution

A triangular distribution defined by min, max, and mode (the most likely value).

yaml
crafting_quality:
  type: triangle
  min: 0
  max: 100
  mode: 70      # Most likely value

expression — Expression Distribution

Uses a math expression to calculate random values. The {random} variable (a uniform random number from 0.0 to 1.0) can be used in the expression.

yaml
custom_damage:
  type: expression
  expression: "floor({base_damage} * (1 + {random} * 0.5))"
  variables:
    base_damage: 100

String Configuration Types

In addition to numeric distributions, the expression engine supports evaluating "string configuration objects". This is used in Lore rendering, skill parameters, and other scenarios where dynamic text is needed.

random_text — Random Text

Randomly selects a specified number of text lines from a candidate list. Ideal for random affixes, random descriptions, etc.

yaml
random_bonus_lines:
  type: "random_text"
  rolls: 2                        # Number of draws (supports numeric config like range/expression)
  allow_duplicates: false          # Whether the same line can be drawn multiple times
  lines:                           # Candidate line list
    - "Physical Attack: +{phys_atk}"
    - "Physical Defense: +{phys_def}"
  variables:                       # Local variables (supports numeric config types)
    phys_atk:
      type: "range"
      min: 2
      max: 6
    phys_def:
      type: "uniform"
      min: 1
      max: 4
FieldRequiredTypeDefaultDescription
typeYesstringMust be random_text (aliases: random_text_lines, random_lines, random_line, text_lines)
rollsNonumeric config1Number of draws, supports constant, range, expression, etc.
allow_duplicatesNobooleanfalseWhether the same line can be drawn more than once
linesYeslistCandidate text line list (aliases: values, options, texts)
variablesNomapLocal variable definitions, each supporting numeric config types
separatorNostring\nSeparator when joining multiple lines into a single string

Tip

The rolls field itself supports numeric configuration types. For example, you can write rolls: "1~3" to randomly draw 1 to 3 lines, or use rolls: { type: expression, expression: "{level}" } to scale the draw count with level.

Warning

When allow_duplicates: false and rolls exceeds the number of candidate lines, at most all candidate lines will be drawn.

Complete Configuration Example

yaml
# A drop configuration example
drops:
  diamond:
    source: "vanilla-diamond"
    count: "1~3"                    # Range shorthand

  gold:
    source: "vanilla-gold_ingot"
    count:                          # Gaussian distribution
      type: gaussian
      mean: 5
      stddev: 2
      min: 1
      max: 10

  rare_gem:
    source: "ia-emaki:rare_gem"
    count:                          # Triangular distribution
      type: triangle
      min: 0
      max: 5
      mode: 1

  bonus_exp:
    type: expression               # Expression
    expression: "floor(100 * pow(1.1, {player_level}))"

Random Text Configuration Example

Using random_text in skill parameters to generate random affixes:

yaml
skill_parameters:
  random_bonus_lines:
    type: "random_text"
    rolls: "{bonus_rolls}"
    allow_duplicates: false
    variables:
      bonus_rolls:
        type: "constant"
        value: 2
      phys_atk:
        type: "range"
        min: 2
        max: 6
      phys_def:
        type: "uniform"
        min: 1
        max: 4
    lines:
      - "Physical Attack: +{phys_atk}"
      - "Physical Defense: +{phys_def}"

Safety Limits

To prevent malicious or erroneous expressions from causing performance issues, the engine has the following safety limits:

LimitValueDescription
Max expression length256 charactersEvaluation is rejected if exceeded
Forbidden characters;, \, ", 'Prevents injection attacks
Max nesting depth10 levelsFunction nesting must not exceed 10 levels
Expression cacheL1: 256 entries / 30-min TTL, L2: global 1024 entriesDual-layer cache architecture, L1 is thread-local, L2 is cross-thread shared

Warning

Expression evaluation runs on the main thread — avoid using overly complex expressions in high-frequency call paths. For scenarios requiring frequent evaluation, the expression cache takes effect automatically, but the initial compilation still has overhead.

java
// Throws ExpressionException when limits are exceeded
try {
    double result = exprService.evaluate(veryLongExpression);
} catch (ExpressionException e) {
    logger.warning("表达式求值失败: " + e.getMessage());
}

Tip

The expression cache uses a dual-layer architecture: L1 is a thread-local cache (LRU, 256 entries, 30-minute TTL), and L2 is a globally shared cache (ConcurrentHashMap, 1024 entries). Identical expressions are compiled only once, and compiled results are shared across threads. For frequently evaluated formulas (like damage calculations), the cache effectively eliminates compilation overhead.

Released under the GPL-3.0 License