Skip to content

Commit

Permalink
Implement utility for more generalized formulas in modifier modules
Browse files Browse the repository at this point in the history
Right now just have the post fix loader implemented, want to eventually get it to load from infix notation in strings, but thats lower priority than just getting them less hardcoded.
See https://en.wikipedia.org/wiki/Reverse_Polish_notation for more info on using the formulas
  • Loading branch information
KnightMiner committed Dec 29, 2023
1 parent 0943849 commit 92c2f81
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package slimeknights.tconstruct.library.json.math;

import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import it.unimi.dsi.fastutil.floats.FloatStack;
import lombok.RequiredArgsConstructor;
import net.minecraft.network.FriendlyByteBuf;

/** Represents 2 argument stack operations */
@RequiredArgsConstructor
public enum BinaryOperator implements StackOperation {
ADD('+') {
@Override
public float apply(float left, float right) {
return left + right;
}
},
SUBTRACT('-') {
@Override
public float apply(float left, float right) {
return left - right;
}
},
MULTIPLY('*') {
@Override
public float apply(float left, float right) {
return left * right;
}
},
DIVIDE('/') {
@Override
public float apply(float left, float right) {
if (right == 0) {
return 0;
}
return left / right;
}
},
POWER('^') {
@Override
public float apply(float left, float right) {
return (float)Math.pow(left, right);
}
};

private final char ch;

/** Applies this operator to the given values */
public abstract float apply(float left, float right);

@Override
public void perform(FloatStack stack, float[] variables) {
// this may throw, but that is okay as we will run this formula during parsing to make sure its valid
// the way formulas are setup, if it does not throw during parsing, it cannot throw ever
float right = stack.popFloat();
float left = stack.popFloat();
stack.push(apply(left, right));
}


/* JSON and network */

/** Deserializes the operator from a character */
public static BinaryOperator deserialize(char ch) {
for (BinaryOperator operator : BinaryOperator.values()) {
if (operator.ch == ch) {
return operator;
}
}
throw new JsonSyntaxException("Unknown binary operator " + ch);
}

@Override
public JsonPrimitive serialize(String[] variableNames) {
return new JsonPrimitive(ch);
}

@Override
public void toNetwork(FriendlyByteBuf buffer) {
// comment on buffer internals: the indices of this enum and StackNetworkType match up until divide,
// so writing our ordinal allows us to read an ordinal for the other enum
buffer.writeEnum(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package slimeknights.tconstruct.library.json.math;

import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import net.minecraft.network.FriendlyByteBuf;
import slimeknights.tconstruct.library.json.LevelingValue;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.modules.ModifierModule;
import slimeknights.tconstruct.library.modifiers.modules.ModifierModuleCondition;
import slimeknights.tconstruct.library.tools.nbt.IToolContext;

/**
* Represents a modifier formula that may be either simple or complex.
*/
public sealed interface ModifierFormula permits PostFixFormula, SimpleLevelingFormula {
/** Variable index for the modifier level for the sake of the builder */
int LEVEL = 0;

/** Computes the level value for this formula, allows some optimizations to not compute level when not needed */
float computeLevel(IToolContext tool, ModifierEntry modifier);

/** Applies this formula to the given arguments */
float apply(float... arguments);

/** Serializes this object to JSON */
JsonObject serialize(JsonObject json);

/** Writes this object to the network */
void toNetwork(FriendlyByteBuf buffer);


/* Constructors */

/**
* Deserializes a formula from JSON
* @param json JSON object
* @param variableNames Variable names for when post fix is used
* @param fallback Fallback for when not using post fix
* @return Formula object
*/
static ModifierFormula deserialize(JsonObject json, String[] variableNames, FallbackFormula fallback) {
if (json.has("formula")) {
// TODO: string formulas using Shunting yard algorithm
return PostFixFormula.deserialize(json, variableNames);
}
LevelingValue leveling = LevelingValue.deserialize(json);
return new SimpleLevelingFormula(leveling, fallback);
}

/**
* Reads a formula from the network
* @param buffer Buffer instance
* @param variableNames Variable names for when post fix is used
* @param fallback Fallback for when not using post fix
* @return Formula object
*/
static ModifierFormula fromNetwork(FriendlyByteBuf buffer, String[] variableNames, FallbackFormula fallback) {
short size = buffer.readShort();
if (size == -1) {
LevelingValue leveling = LevelingValue.fromNetwork(buffer);
return new SimpleLevelingFormula(leveling, fallback);
}
return PostFixFormula.fromNetwork(buffer, size, variableNames);
}

/** Formula to use when not using the post fix formula */
@FunctionalInterface
interface FallbackFormula {
/** Formula that just returns the leveling value directly */
FallbackFormula IDENTITY = arguments -> arguments[LEVEL];
/** Formula adding the leveling value to the second argument, requires 1 additional argument */
FallbackFormula ADD = arguments -> arguments[LEVEL] + arguments[1];
/** Formula for standard percent boosts, requires 1 additional argument */
FallbackFormula PERCENT = arguments -> arguments[1] * (1 + arguments[LEVEL]);

/**
* Runs this formula
* @param arguments Additional arguments passed into the module, the result of {@link LevelingValue} is placed at index 0
* @return Value after applying the formula
*/
float apply(float[] arguments);
}


/** Builder for a module containing a modifier formula */
@RequiredArgsConstructor
abstract class Builder<T extends Builder<T>> extends ModifierModuleCondition.Builder<T> {
/** Variables to use for post fix formulas */
private final String[] variables;
/** Fallback formula for simple leveling */
private final FallbackFormula formula;

/** Builds the module given the formula */
protected abstract ModifierModule build(ModifierFormula formula);

/** Builds the module with the given amount */
public ModifierModule amount(float flat, float leveling) {
return build(new SimpleLevelingFormula(new LevelingValue(flat, leveling), formula));
}

/** Builds the module with a flat amount */
public ModifierModule flat(float flat) {
return amount(flat, 0);
}

/** Builds the module with an amount multiplied by the level */
public ModifierModule eachLevel(float eachLevel) {
return amount(0, eachLevel);
}

/** Switches this builder into formula building mode */
public FormulaBuilder formula() {
return new FormulaBuilder();
}

/** Builder for the formula segment of this module */
public class FormulaBuilder extends PostFixFormula.Builder<FormulaBuilder> {
protected FormulaBuilder() {
super(variables);
}

/** Builds the module given the formula */
public ModifierModule build() {
return Builder.this.build(buildFormula());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package slimeknights.tconstruct.library.json.math;

import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import it.unimi.dsi.fastutil.floats.AbstractFloatList;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import slimeknights.mantle.util.JsonHelper;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.tools.nbt.IToolContext;

import java.util.List;

/** Performs a math formula using a post fix calculator */
public record PostFixFormula(List<StackOperation> operations, String[] variableNames) implements ModifierFormula {
@Override
public float apply(float... values) {
// must have the right number of values to evaluate
if (values.length != variableNames.length) {
throw new IllegalArgumentException("Expected " + variableNames.length + " arguments, but received " + values.length);
}
AbstractFloatList stack = new FloatArrayList(5);
for (StackOperation operation : operations) {
operation.perform(stack, values);
}
if (stack.size() != 1) {
throw new IllegalStateException("Expected 1 value on the stack after evaluation, received " + stack.size());
}
return stack.popFloat();
}

@Override
public float computeLevel(IToolContext tool, ModifierEntry modifier) {
return modifier.getEffectiveLevel(tool);
}

/**
* Runs the formula with dummy arguments to ensure it is computationally valid
* @throws RuntimeException if something is invalid in the formula
*/
public void validateFormula() {
apply(new float[variableNames.length]);
}

/** Deserializes a formula from JSON */
public static PostFixFormula deserialize(JsonObject json, String[] variableNames) {
return new PostFixFormula(JsonHelper.parseList(json, "formula", (element, key) -> {
if (element.isJsonPrimitive()) {
return StackOperation.deserialize(element.getAsJsonPrimitive(), variableNames);
}
throw new JsonSyntaxException("Expected " + key + " to be a string or number, was " + GsonHelper.getType(element));
}), variableNames);
}

/** Serializes this object to JSON */
@Override
public JsonObject serialize(JsonObject json) {
JsonArray array = new JsonArray();
for (StackOperation operation : operations) {
array.add(operation.serialize(variableNames));
}
json.add("formula", array);
return json;
}

/** Reads a formula from the network */
public static PostFixFormula fromNetwork(FriendlyByteBuf buffer, String[] variableNames) {
return fromNetwork(buffer, buffer.readShort(), variableNames);
}

/** Common logic between {@link #fromNetwork(FriendlyByteBuf, String[])} and {@link ModifierFormula#fromNetwork(FriendlyByteBuf, String[], FallbackFormula)} */
static PostFixFormula fromNetwork(FriendlyByteBuf buffer, short size, String[] variableNames) {
ImmutableList.Builder<StackOperation> builder = ImmutableList.builder();
for (int i = 0; i < size; i++) {
builder.add(StackOperation.fromNetwork(buffer));
}
return new PostFixFormula(builder.build(), variableNames);
}

/** Writes this formula to the network */
@Override
public void toNetwork(FriendlyByteBuf buffer) {
buffer.writeShort(operations.size());
for (StackOperation operation : operations) {
operation.toNetwork(buffer);
}
}


/* Builder */

/** Creates a new builder instance */
public static Builder<?> builder(String[] variableNames) {
return new Builder<>(variableNames);
}

@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public static class Builder<T extends Builder<T>> {
private final String[] variableNames;
private final ImmutableList.Builder<StackOperation> operations = ImmutableList.builder();

/** Adds the given operation to the builder */
@SuppressWarnings("unchecked")
public T operation(StackOperation operation) {
this.operations.add(operation);
return (T) this;
}

/** Pushes a constant value into the formula */
public T constant(float value) {
return operation(new PushConstantOperation(value));
}

/** Pushes a variable value into the formula */
public T variable(int index) {
if (index < 0 || index >= variableNames.length) {
throw new IllegalArgumentException("Invalid variable index " + index);
}
return operation(new PushVariableOperation(index));
}

/** Pushes an add operation into the builder */
public T add() {
return operation(BinaryOperator.ADD);
}

/** Pushes a subtract operation into the builder */
public T subtract() {
return operation(BinaryOperator.SUBTRACT);
}

/** Pushes a multiply operation into the builder */
public T multiply() {
return operation(BinaryOperator.MULTIPLY);
}

/** Pushes a divide operation into the builder */
public T divide() {
return operation(BinaryOperator.DIVIDE);
}

/** Pushes a power operation into the builder */
public T power() {
return operation(BinaryOperator.POWER);
}

/** Validates and builds the formula */
public PostFixFormula buildFormula() {
PostFixFormula formula = new PostFixFormula(operations.build(), variableNames);
formula.validateFormula();
return formula;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package slimeknights.tconstruct.library.json.math;

import com.google.gson.JsonPrimitive;
import it.unimi.dsi.fastutil.floats.FloatStack;
import net.minecraft.network.FriendlyByteBuf;

/** Stack operation which pushes a constant float value */
record PushConstantOperation(float value) implements StackOperation {
@Override
public void perform(FloatStack stack, float[] variables) {
stack.push(value);
}

@Override
public JsonPrimitive serialize(String[] variableNames) {
return new JsonPrimitive(value);
}

@Override
public void toNetwork(FriendlyByteBuf buffer) {
buffer.writeEnum(StackNetworkType.VALUE);
buffer.writeFloat(value);
}
}

0 comments on commit 92c2f81

Please sign in to comment.