data-driven event system
1# Expressions
2
3As explained in [Expressions](/Expressions), Commander introduces an extensive expression system. This system is exposed as part of the API. On this page we will go over usage examples and a possible way to make expressions an optional integration.
4
5## Using expressions directly
6
7The built-in `Expression` class provides a string -> expression codec and a `parse` method. An expression is a `LootContext -> Expression.Result` function. `Expression.Result` represents a result value that can be converted to `BigDecimal`, `boolean`, `String`, `Instant`and `Duration`.
8
9To apply any of the expression functions you must have an instance of `LootContext`.
10
11```java
12public static final Expression EXP = Expression.parse("strFormat('%02.0f:%02.0f', floor((level.getDayTime / 1000 + 8) % 24), floor(60 * (level.getDayTime % 1000) / 1000))").result().orElseThrow();
13
14public String worldTimeInHumanTime(LootContext context) {
15 return EXP.apply(context).getAsString();
16}
17```
18
19## Special functions
20
21Commander comes with `Arithmetica` and `BooleanExpression` which are `double` and `boolean` functions that can be encoded either as a constant or as an expression. It's worth noting that `"true"` will be decoded as an expression, but `true` will not.
22
23```json
242.2, "random(0, 3)" //Arithmetica
25
26true, "level.isDay" //BooleanExpression
27```
28
29## Expressions as optional integration.
30
31When implementing support for Commander, you may want to make this integration optional. The easiest way to do this is to create a common interface that is then delegated to one of the implementations. This is how expressions are set-up in Andromeda's new config system.
32
33Let's start by defining a simple boolean intermediary interface. I recommend using a supplier as the parameter, so you don't construct a `LootContext` for constant values.
34
35```java
36public interface BooleanIntermediary {
37 boolean asBoolean(Supplier<LootContext> supplier);
38}
39```
40
41Now let's implement the constant delegate.
42
43```java
44public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary {
45
46 @Override
47 public boolean asBoolean(Supplier<LootContext> supplier) {
48 return this.value;
49 }
50}
51```
52
53And our commander delegate.
54
55```java
56public final class CommanderBooleanIntermediary implements BooleanIntermediary {
57
58 private final BooleanExpression expression;
59 private final boolean constant;
60
61 public CommanderBooleanIntermediary(BooleanExpression expression) {
62 this.expression = expression;
63 this.constant = expression.toSource().left().isPresent(); //micro optimization
64 }
65
66 @Override
67 public boolean asBoolean(Supplier<LootContext> supplier) {
68 return this.expression.applyAsBoolean(constant ? null : supplier.get());
69 }
70
71 public BooleanExpression getExpression() {
72 return this.expression;
73 }
74}
75```
76
77With our delegates set-up we have to dynamically pick one or the other. Let's return to our intermediary interface and create a factory for constant values. Here I'm using `Support` from Dark Matter, but you can just copy this method:
78
79```java
80public static <T, F extends T, S extends T> T support(String mod, Supplier<F> expected, Supplier<S> fallback) {
81 return FabricLoader.getInstance().isModLoaded(mod) ? expected.get() : fallback.get();
82}
83```
84
85The method will check if Commander is installed and will pick the correct factory.
86
87```java
88public interface BooleanIntermediary {
89 Function<Boolean, BooleanIntermediary> FACTORY = Support.support("commander",
90 () -> b -> new CommanderBooleanIntermediary(BooleanExpression.constant(b)),
91 () -> ConstantBooleanIntermediary::new);
92
93 static BooleanIntermediary of(boolean value) {
94 return FACTORY.apply(value);
95 }
96 //...
97}
98```
99
100Now we can define expressions in our config!
101
102```java
103public class Config {
104 public BooleanIntermediary value = BooleanIntermediary.of(true);
105}
106```
107
108But wait, what about encoding/decoding? Let's actually get to that. Here we can create a codec for our delegates and use `support` to pick the correct one.
109
110```java
111public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary {
112 public static final Codec<ConstantBooleanIntermediary> CODEC = Codec.BOOL.xmap(ConstantBooleanIntermediary::new, ConstantBooleanIntermediary::value);
113 //...
114}
115
116public final class CommanderBooleanIntermediary implements BooleanIntermediary {
117 public static final Codec<CommanderBooleanIntermediary> CODEC = BooleanExpression.CODEC.xmap(CommanderBooleanIntermediary::new, CommanderBooleanIntermediary::getExpression);
118 //...
119}
120```
121
122Now we can use our codecs.
123
124```java
125Codec<BooleanIntermediary> codec = (Codec<BooleanIntermediary>) Support.fallback("commander", () -> CommanderBooleanIntermediary.CODEC, () -> ConstantBooleanIntermediary.CODEC);
126```
127
128### Using intermediaries in configs.
129
130::: info Note
131
132This section only applies if you use Gson to read/write your configs.
133
134If you use a third-party library and the library doesn't allow you to pass a custom Gson instance, you could modify it using mixins.
135
136:::
137
138In Gson you can provide custom JsonSerializers/JsonDeserializers, which allows us to encode the intermediary using Codecs.
139
140Let's create our `CodecSerializer` class.
141
142::: details Code
143
144```java
145public record CodecSerializer<C>(Codec<C> codec) implements JsonSerializer<C>, JsonDeserializer<C> {
146
147 public static <C> CodecSerializer<C> of(Codec<C> codec) {
148 return new CodecSerializer<>(codec);
149 }
150
151 @Override
152 public C deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
153 var r = this.codec.parse(JsonOps.INSTANCE, json);
154 if (r.error().isPresent()) throw new JsonParseException(r.error().orElseThrow().message());
155 return r.result().orElseThrow();
156 }
157
158 @Override
159 public JsonElement serialize(C src, Type typeOfSrc, JsonSerializationContext context) {
160 var r = codec.encodeStart(JsonOps.INSTANCE, src);
161 if (r.error().isPresent()) throw new IllegalStateException(r.error().orElseThrow().message());
162 return r.result().orElseThrow();
163 }
164}
165```
166
167:::
168
169And now we can register a type hierarchy adapter with our GsonBuilder.
170
171```java
172builder.registerTypeHierarchyAdapter(BooleanIntermediary.class, CodecSerializer.of(codec));
173```
174
175And we're done!