Expressions
As explained in 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.
Using expressions directly
The 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, Instantand Duration.
To apply any of the expression functions you must have an instance of LootContext.
public 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();
public String worldTimeInHumanTime(LootContext context) {
return EXP.apply(context).getAsString();
}Special functions
Commander 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.
2.2, "random(0, 3)" //Arithmetica
true, "level.isDay" //BooleanExpressionExpressions as optional integration.
When 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.
Let'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.
public interface BooleanIntermediary {
boolean asBoolean(Supplier<LootContext> supplier);
}Now let's implement the constant delegate.
public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary {
@Override
public boolean asBoolean(Supplier<LootContext> supplier) {
return this.value;
}
}And our commander delegate.
public final class CommanderBooleanIntermediary implements BooleanIntermediary {
private final BooleanExpression expression;
private final boolean constant;
public CommanderBooleanIntermediary(BooleanExpression expression) {
this.expression = expression;
this.constant = expression.toSource().left().isPresent(); //micro optimization
}
@Override
public boolean asBoolean(Supplier<LootContext> supplier) {
return this.expression.applyAsBoolean(constant ? null : supplier.get());
}
public BooleanExpression getExpression() {
return this.expression;
}
}With 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:
public static <T, F extends T, S extends T> T support(String mod, Supplier<F> expected, Supplier<S> fallback) {
return FabricLoader.getInstance().isModLoaded(mod) ? expected.get() : fallback.get();
}The method will check if Commander is installed and will pick the correct factory.
public interface BooleanIntermediary {
Function<Boolean, BooleanIntermediary> FACTORY = Support.support("commander",
() -> b -> new CommanderBooleanIntermediary(BooleanExpression.constant(b)),
() -> ConstantBooleanIntermediary::new);
static BooleanIntermediary of(boolean value) {
return FACTORY.apply(value);
}
//...
}Now we can define expressions in our config!
public class Config {
public BooleanIntermediary value = BooleanIntermediary.of(true);
}But 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.
public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary {
public static final Codec<ConstantBooleanIntermediary> CODEC = Codec.BOOL.xmap(ConstantBooleanIntermediary::new, ConstantBooleanIntermediary::value);
//...
}
public final class CommanderBooleanIntermediary implements BooleanIntermediary {
public static final Codec<CommanderBooleanIntermediary> CODEC = BooleanExpression.CODEC.xmap(CommanderBooleanIntermediary::new, CommanderBooleanIntermediary::getExpression);
//...
}Now we can use our codecs.
Codec<BooleanIntermediary> codec = (Codec<BooleanIntermediary>) Support.fallback("commander", () -> CommanderBooleanIntermediary.CODEC, () -> ConstantBooleanIntermediary.CODEC);Using intermediaries in configs.
Note
This section only applies if you use Gson to read/write your configs.
If 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.
In Gson you can provide custom JsonSerializers/JsonDeserializers, which allows us to encode the intermediary using Codecs.
Let's create our CodecSerializer class.
Code
public record CodecSerializer<C>(Codec<C> codec) implements JsonSerializer<C>, JsonDeserializer<C> {
public static <C> CodecSerializer<C> of(Codec<C> codec) {
return new CodecSerializer<>(codec);
}
@Override
public C deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
var r = this.codec.parse(JsonOps.INSTANCE, json);
if (r.error().isPresent()) throw new JsonParseException(r.error().orElseThrow().message());
return r.result().orElseThrow();
}
@Override
public JsonElement serialize(C src, Type typeOfSrc, JsonSerializationContext context) {
var r = codec.encodeStart(JsonOps.INSTANCE, src);
if (r.error().isPresent()) throw new IllegalStateException(r.error().orElseThrow().message());
return r.result().orElseThrow();
}
}And now we can register a type hierarchy adapter with our GsonBuilder.
builder.registerTypeHierarchyAdapter(BooleanIntermediary.class, CodecSerializer.of(codec));And we're done!