-
Notifications
You must be signed in to change notification settings - Fork 756
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement utility for more generalized formulas in modifier modules
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
1 parent
0943849
commit 92c2f81
Showing
9 changed files
with
528 additions
and
0 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
src/main/java/slimeknights/tconstruct/library/json/math/BinaryOperator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
src/main/java/slimeknights/tconstruct/library/json/math/ModifierFormula.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} | ||
} |
159 changes: 159 additions & 0 deletions
159
src/main/java/slimeknights/tconstruct/library/json/math/PostFixFormula.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/main/java/slimeknights/tconstruct/library/json/math/PushConstantOperation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.