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:
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.0Built-in Functions
| Function | Description | Example | Result |
|---|---|---|---|
ceil(x) | Ceiling (round up) | ceil(3.2) | 4.0 |
floor(x) | Floor (round down) | floor(3.8) | 3.0 |
round(x) | Round to nearest | round(3.5) | 4.0 |
log10(x) | Base-10 logarithm | log10(100) | 2.0 |
min(a, b) | Minimum value | min(3, 7) | 3.0 |
max(a, b) | Maximum value | max(3, 7) | 7.0 |
pow(a, b) | Exponentiation | pow(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:
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.5Using variables in YAML configuration:
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.
# Writing a number directly is a constant
drop_count: 5
# Or explicitly declared
drop_count:
type: constant
value: 5range — Range Shorthand
A shorthand using the min~max format, equivalent to a uniform distribution.
# 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.
drop_count:
type: uniform
min: 5
max: 15gaussian — Gaussian (Normal) Distribution
A normal distribution centered on mean with standard deviation stddev. Optional min / max truncation.
damage:
type: gaussian
mean: 100
stddev: 15
min: 50 # Optional, minimum truncation value
max: 150 # Optional, maximum truncation valueskew_normal — Skew Normal Distribution
Adds a skewness parameter skew on top of the Gaussian distribution. Positive values skew right, negative values skew left.
rare_drop_quality:
type: skew_normal
mean: 50
stddev: 20
skew: 3.0 # Positive: right-skewed (higher values are rarer)
min: 0
max: 100triangle — Triangular Distribution
A triangular distribution defined by min, max, and mode (the most likely value).
crafting_quality:
type: triangle
min: 0
max: 100
mode: 70 # Most likely valueexpression — 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.
custom_damage:
type: expression
expression: "floor({base_damage} * (1 + {random} * 0.5))"
variables:
base_damage: 100String 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.
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| Field | Required | Type | Default | Description |
|---|---|---|---|---|
type | Yes | string | — | Must be random_text (aliases: random_text_lines, random_lines, random_line, text_lines) |
rolls | No | numeric config | 1 | Number of draws, supports constant, range, expression, etc. |
allow_duplicates | No | boolean | false | Whether the same line can be drawn more than once |
lines | Yes | list | — | Candidate text line list (aliases: values, options, texts) |
variables | No | map | — | Local variable definitions, each supporting numeric config types |
separator | No | string | \n | Separator 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
# 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:
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:
| Limit | Value | Description |
|---|---|---|
| Max expression length | 256 characters | Evaluation is rejected if exceeded |
| Forbidden characters | ;, \, ", ' | Prevents injection attacks |
| Max nesting depth | 10 levels | Function nesting must not exceed 10 levels |
| Expression cache | L1: 256 entries / 30-min TTL, L2: global 1024 entries | Dual-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.
// 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.