diff --git a/common/build.gradle b/common/build.gradle index 163f12a8..308484e2 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -10,9 +10,16 @@ dependencies { compileOnly 'org.apache.logging.log4j:log4j-api:2.0-beta9' compileOnly 'org.apache.logging.log4j:log4j-core:2.0-beta9' + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation 'org.junit.jupiter:junit-jupiter:5.14.2' + library "com.github.MPKMod.MPKNetworkAPI:common:${project.networkApiVersion}" } +test { + useJUnitPlatform() +} + compileJava.doLast { var classFile = new File("$temporaryDir${File.separator}classes.txt") classFile.createNewFile() diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Minecraft.java b/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Minecraft.java index 398b8c67..4d21f601 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Minecraft.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Minecraft.java @@ -4,7 +4,7 @@ import io.github.kurrycat.mpkmod.events.Event; import io.github.kurrycat.mpkmod.gui.MPKGuiScreen; import io.github.kurrycat.mpkmod.gui.infovars.InfoString; -import io.github.kurrycat.mpkmod.ticks.TickInput; +import io.github.kurrycat.mpkmod.util.input.TickInput; import io.github.kurrycat.mpknetapi.common.network.packet.MPKPacket; import java.text.SimpleDateFormat; diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Player.java b/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Player.java index 5b2bf68c..a39e6f9b 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Player.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/compatibility/MCClasses/Player.java @@ -32,7 +32,7 @@ public class Player { @InfoString.Field public Blip lastBlip = null; - public TimingInput timingInput = new TimingInput(""); + public TimingInput timingInput = TimingInput.stopTick(); public KeyInput keyInput = null; public ButtonMSList keyMSList = null; public Vector3D pos = null; diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/gui/components/Component.java b/common/src/main/java/io/github/kurrycat/mpkmod/gui/components/Component.java index cc3228f0..b6c8612e 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/gui/components/Component.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/gui/components/Component.java @@ -1,14 +1,15 @@ package io.github.kurrycat.mpkmod.gui.components; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import io.github.kurrycat.mpkmod.compatibility.MCClasses.Renderer2D; import io.github.kurrycat.mpkmod.util.JSONPos2D; import io.github.kurrycat.mpkmod.util.Mouse; import io.github.kurrycat.mpkmod.util.Vector2D; +@JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY +) public abstract class Component extends ComponentHolder { public boolean selected = false; public boolean highlighted = false; diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/save/Serializer.java b/common/src/main/java/io/github/kurrycat/mpkmod/save/Serializer.java index 9cb0d521..046c68fd 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/save/Serializer.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/save/Serializer.java @@ -1,7 +1,6 @@ package io.github.kurrycat.mpkmod.save; import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,10 +29,6 @@ public static void registerSerializer() { module.addDeserializer(Color.class, new ColorDeserializer()); mapper.registerModule(module); - mapper.enableDefaultTyping( - ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, - JsonTypeInfo.As.PROPERTY - ); mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/GroundState.java b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/GroundState.java new file mode 100644 index 00000000..5ccbf884 --- /dev/null +++ b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/GroundState.java @@ -0,0 +1,23 @@ +package io.github.kurrycat.mpkmod.ticks; + +public enum GroundState { + GROUNDED(false, true), + AIRBORNE(false, false), + JUMPING(true, false); + + public final boolean jump, ground; + + GroundState(boolean jump, boolean ground) { + this.jump = jump; + this.ground = ground; + } + + public static GroundState fromBooleans(boolean jump, boolean ground) { + if (jump && ground) + throw new IllegalArgumentException("Cannot be both jumping and on ground"); + + if (jump) return JUMPING; + if (ground) return GROUNDED; + return AIRBORNE; + } +} diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/Timing.java b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/Timing.java index 9faba166..892e6f01 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/Timing.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/Timing.java @@ -5,6 +5,7 @@ import io.github.kurrycat.mpkmod.compatibility.API; import io.github.kurrycat.mpkmod.util.MathUtil; import io.github.kurrycat.mpkmod.util.Tuple; +import io.github.kurrycat.mpkmod.util.input.InputPredicateReference; import java.util.ArrayList; import java.util.HashMap; @@ -13,14 +14,41 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Timing { +public final class Timing { private final LinkedHashMap format; private final TimingEntry[] timingEntries; + private final boolean symmetrical; + private final Timing mirrored; @JsonCreator - public Timing(@JsonProperty("format") LinkedHashMap format, @JsonProperty("timingEntries") TimingEntry[] timingEntries) { + public Timing( + @JsonProperty("format") + LinkedHashMap format, + @JsonProperty("timingEntries") + TimingEntry[] timingEntries, + @JsonProperty("symmetrical") + Boolean symmetrical + ) { + if (symmetrical == null) symmetrical = false; + this.format = format; this.timingEntries = timingEntries; + this.symmetrical = symmetrical; + + this.mirrored = this.symmetrical ? makeMirrored() : null; + + for (TimingEntry e : timingEntries) { + if (e.inputPredicate instanceof InputPredicateReference) + ((InputPredicateReference) e.inputPredicate).setParentTimingEntries(timingEntries); + } + } + + public boolean isSymmetrical() { + return symmetrical; + } + + public Timing getMirrored() { + return mirrored; } public Match match(List inputList) { @@ -53,6 +81,14 @@ private Match startsWithMatch(List inputList) { return new Match(getFormatString(vars), vars.size(), inputList.size() - startIndex); } + private Timing makeMirrored() { + TimingEntry[] mirroredTimingEntries = new TimingEntry[timingEntries.length]; + for (int i = 0; i < timingEntries.length; i++) { + mirroredTimingEntries[i] = timingEntries[i].mirrored(); + } + return new Timing(format, mirroredTimingEntries, false); + } + private String getFormatString(HashMap vars) { StringBuilder sb = new StringBuilder(); format.forEach((fc, fs) -> { diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingEntry.java b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingEntry.java index f92a9434..4f11ae93 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingEntry.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingEntry.java @@ -1,114 +1,97 @@ package io.github.kurrycat.mpkmod.ticks; import com.fasterxml.jackson.annotation.JsonCreator; -import io.github.kurrycat.mpkmod.util.MathUtil; +import com.fasterxml.jackson.annotation.JsonProperty; import io.github.kurrycat.mpkmod.util.Range; import io.github.kurrycat.mpkmod.util.Tuple; +import io.github.kurrycat.mpkmod.util.input.InputPredicate; +import io.github.kurrycat.mpkmod.util.input.InputPredicateBase; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -public class TimingEntry { - private static final String tickCountRegex = "^((((?\\d+)-)?((?[a-zA-Z]+)(\\{(?\\d+)?,(?\\d+)?})?))|(?\\d+))$"; - private static final Pattern tickCountPattern = Pattern.compile(tickCountRegex); - private static final String tickInputRegex = "^W?A?S?D?P?N?J?(!?G)?$"; - private static final Pattern tickInputPattern = Pattern.compile(tickInputRegex); +public final class TimingEntry { + public final String varName; + public final Range range; - public String timingEntry; - public TimingInput timingInput; - - private Integer number; - private String varName; - private Range range; + public final InputPredicateBase inputPredicate; + public final Boolean P, N; + public final GroundState G; @JsonCreator - public TimingEntry(String timingEntry) { - this.timingEntry = timingEntry; - String[] split = timingEntry.split(":", -1); - if (split.length == 1) { - number = 1; - varName = null; - range = null; - timingInput = new TimingInput(split[0]); - } else if (split.length == 2) { - Matcher matcher = tickCountPattern.matcher(split[0]); - if (!matcher.matches()) - throw new IllegalArgumentException(String.format("Invalid tick count: %s", split[0])); - - Integer diffNumber = MathUtil.parseInt(matcher.group("diffNumber"), null); - String varName = matcher.group("varName"); - Integer lower = MathUtil.parseInt(matcher.group("lowerRange"), null); - Integer upper = MathUtil.parseInt(matcher.group("upperRange"), null); - Integer rawCount = MathUtil.parseInt(matcher.group("rawCount"), null); - - number = diffNumber == null ? rawCount : diffNumber; - this.varName = varName; - range = new Range(lower, upper); - - timingInput = new TimingInput(split[1]); - } else { - throw new IllegalArgumentException(String.format("More than one : found in timingEntry '%s', expected one", timingEntry)); - } + public TimingEntry( + @JsonProperty("var") + String varName, + @JsonProperty("min") + Integer min, + @JsonProperty("max") + Integer max, + @JsonProperty("inputs") + InputPredicateBase inputPredicate, + @JsonProperty("sprint") + Boolean P, + @JsonProperty("sneak") + Boolean N, + @JsonProperty("groundState") + GroundState G + ) { + if (min == null && max == null) min = max = 1; + if (min == null || min <= 0) min = 0; + if (max != null && max < min) max = min; + + this.varName = varName; + this.range = new Range(min, max); + this.inputPredicate = inputPredicate; + this.P = P; + this.N = N; + this.G = G; } public boolean varNameMatches(TimingEntry other) { return varName != null && other.varName != null && varName.equals(other.varName); } + private boolean matchesInput(TimingInput timingInput) { + return ( + (inputPredicate == null || inputPredicate.matches(timingInput.inputVector)) && + (P == null || P == timingInput.P) && + (N == null || N == timingInput.N) && + (G == null || G == timingInput.G) + ); + } + /** * @param inputList List of {@link TimingInput} instances * @param startIndex the index it should start matching from - * @param vars HashMap containing all variables for that {@link Timing}. Vars of this TimingEntry are added - * @param repeatedVar if the current var already appeared before + * @param vars HashMap containing all variables for that {@link Timing}. Vars of this TimingEntry are added + * @param repeatedVar if the current var appeared in the previous {@link TimingEntry} * @return amount of matched inputs or null if no match was found (returns 0 for variable entries with 0 within range) */ public Integer matches(List inputList, int startIndex, HashMap vars, boolean repeatedVar) { - if (number == null && varName == null) return null; + int i = startIndex; - // is variable - if (number == null) { - int i = startIndex; - while (i < inputList.size() && inputList.get(i).equals(timingInput)) i++; + if (inputPredicate instanceof InputPredicate) ((InputPredicate) inputPredicate).resetLastMatch(); + while (i < inputList.size() && matchesInput(inputList.get(i))) i++; + int count = range.constrain(i - startIndex); - i -= startIndex; - - if (range.includes(i) || range.isAbove(i)) { - if (vars.containsKey(varName) && repeatedVar) - vars.get(varName).tickCount += range.constrain(i); - else vars.put(varName, new Timing.TickMS(range.constrain(i))); - - vars.get(varName).ms = getMS( - startIndex + i - vars.get(varName).tickCount, - vars.get(varName).tickCount, - inputList - ); - - return range.constrain(i); - } - return null; - } + if (range.isValueBelow(i - startIndex)) return null; - int countToMatch = number; if (varName != null) { - if (!vars.containsKey(varName)) { - throw new IllegalArgumentException(String.format("The variable %s has not been defined in a prior timing input (at %s)", varName, timingEntry)); + if (vars.containsKey(varName) && repeatedVar) { + vars.get(varName).tickCount += count; } else { - countToMatch -= vars.get(varName).tickCount; + vars.put(varName, new Timing.TickMS(count)); } - } - - if (inputList.size() == startIndex && number != 0) return null; - int i; - for (i = startIndex; countToMatch > 0; countToMatch--, i++) { - if (i >= inputList.size()) return null; - if (!inputList.get(i).equals(timingInput)) return null; + vars.get(varName).ms = getMS( + i - vars.get(varName).tickCount, + vars.get(varName).tickCount, + inputList + ); } - return i - startIndex; + + return count; } private Integer getMS(int startIndex, int matchCount, List inputList) { @@ -135,4 +118,12 @@ private Integer getMS(int startIndex, int matchCount, List inputLis if (endMS == null) return null; return endMS.msFrom(startMS); } + + public TimingEntry mirrored() { + InputPredicateBase mirroredInputPredicate = inputPredicate; + if (inputPredicate instanceof InputPredicate) + mirroredInputPredicate = ((InputPredicate) inputPredicate).mirrored(); + + return new TimingEntry(varName, range.getLower(), range.getUpper(), mirroredInputPredicate, P, N, G); + } } diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingInput.java b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingInput.java index 28998136..7a42705b 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingInput.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingInput.java @@ -2,36 +2,27 @@ import io.github.kurrycat.mpkmod.util.Copyable; import io.github.kurrycat.mpkmod.util.Tuple; +import io.github.kurrycat.mpkmod.util.input.InputVector; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; public class TimingInput implements Copyable { - public boolean W, A, S, D, P, N, J; - public Boolean G = null; - public ButtonMSList msList = new ButtonMSList(); - - public TimingInput(String inputString) { - W = inputString.contains("W"); - A = inputString.contains("A"); - S = inputString.contains("S"); - D = inputString.contains("D"); - P = inputString.contains("P"); - N = inputString.contains("N"); - J = inputString.contains("J"); - if (inputString.contains("G")) - G = !inputString.contains("!G"); - } - - public TimingInput(boolean W, boolean A, boolean S, boolean D, boolean P, boolean N, boolean J, Boolean G) { - this.W = W; - this.A = A; - this.S = S; - this.D = D; + public final InputVector inputVector; + public final boolean P, N; + public final GroundState G; + public final ButtonMSList msList = new ButtonMSList(); + + public TimingInput(boolean W, boolean A, boolean S, boolean D, boolean P, boolean N, boolean jump, boolean ground) { + this( + new InputVector(W, A, S, D), + P, N, GroundState.fromBooleans(jump, ground) + ); + } + public TimingInput(InputVector inputVector, boolean P, boolean N, GroundState G) { + this.inputVector = inputVector; this.P = P; this.N = N; - this.J = J; this.G = G; } @@ -46,10 +37,12 @@ public static Tuple findMSButtons(TimingInput ButtonMS.Button[] allButtons = ButtonMS.Button.values(); + // Succeeds if exactly one key was held for the whole match, but not immediately before or after it int onlyPressedCurr = findSingleOnlyPressedCurr(befInputs, aftInputs, curInputsList); if (onlyPressedCurr != -1) return new Tuple<>(allButtons[onlyPressedCurr], allButtons[onlyPressedCurr]); + // Fails if a non-jump key changes for (int i = 0; i < curr.size() - 1; i++) { if (!curr.get(i).equalsIgnoreJump(curr.get(i + 1))) return null; @@ -59,6 +52,7 @@ public static Tuple findMSButtons(TimingInput Tuple interruptedByMovMod = findInterruptedByMove(befInputs, curInputs, aftInputs); + // Succeeds if a key press is "interrupted" on the last tick (except for the jump key since holding it midair does nothing) if (interruptedByMovMod.getFirst() != -1 && interruptedByMovMod.getSecond() != -1) return new Tuple<>(allButtons[interruptedByMovMod.getFirst()], allButtons[interruptedByMovMod.getSecond()]); @@ -66,10 +60,23 @@ public static Tuple findMSButtons(TimingInput } public boolean[] inputBoolList() { - return new boolean[]{W, A, S, D, P, N, J}; - } - - //returns -1 on multiple matches + return new boolean[] { + inputVector.isW(), + inputVector.isA(), + inputVector.isS(), + inputVector.isD(), + P, N, G.jump + }; + } + + /** + * Finds the only key that was held for the whole match. + * + * @param befInputs inputs before the match + * @param aftInputs inputs after the match + * @param curInputs inputs throughout the match + * @return the detected key's index in an {@link TimingInput#inputBoolList} if exactly one was found; {@code -1} otherwise + */ private static int findSingleOnlyPressedCurr(boolean[] befInputs, boolean[] aftInputs, List curInputs) { int index = -1; for (int i = 0; i < befInputs.length - 1; i++) { @@ -86,10 +93,21 @@ private static int findSingleOnlyPressedCurr(boolean[] befInputs, boolean[] aftI } public boolean equalsIgnoreJump(TimingInput other) { - return W == other.W && A == other.A && S == other.S && D == other.D && P == other.P && N == other.N; - } - - //first and second can be -1 + return inputVector.equals(other.inputVector) && P == other.P && N == other.N; + } + + /** + * Finds the key change that occurs when entering the current state and the key change that occurs when exiting it, + * "interrupting" the previous one. + * + * @param befInputs inputs before the match + * @param curInputs inputs on the first match tick, assuming only the jump key state could change during the match + * @param aftInputs inputs after the match + * @return a {@link Tuple} containing the indices of the detected key changes: + * the first element is the key change entering the current state, + * and the second one is the key change exiting the current state. + * If a change is not detected, the corresponding value is {@code -1}. + */ private static Tuple findInterruptedByMove(boolean[] befInputs, boolean[] curInputs, boolean[] aftInputs) { int first = findMovButtonDiff(befInputs, curInputs); int second = findFirstButtonDiff(curInputs, aftInputs); @@ -119,12 +137,16 @@ private static int findFirstButtonDiff(boolean[] curInputs, boolean[] aftInputs) } public boolean isStopTick() { - return !W && !A && !S && !D && !P && !N && !J && G != null && G; + return inputVector.isStop() && !P && !N && G == GroundState.GROUNDED; } @Override public int hashCode() { - return Objects.hash(W, A, S, D, P, N, J, G); + int h = inputVector.hashCode(); + h = 31 * h + (P ? 1 : 0); + h = 31 * h + (N ? 1 : 0); + h = 31 * h + G.ordinal(); + return h; } @Override @@ -132,27 +154,24 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TimingInput that = (TimingInput) o; - return W == that.W && A == that.A && S == that.S && D == that.D && P == that.P && N == that.N && J == that.J && - (G == null || that.G == null || this.G == that.G); + + return inputVector.equals(that.inputVector) && P == that.P && N == that.N && G == that.G; } @Override public String toString() { - return toInputs() + (G == null ? "" : (G ? "G" : "!G")); + return toInputString() + (G.ground ? "G" : "!G"); } - public String toInputs() { - return (W ? "W" : "") + - (A ? "A" : "") + - (S ? "S" : "") + - (D ? "D" : "") + + public String toInputString() { + return inputVector.toString() + (P ? "P" : "") + (N ? "N" : "") + - (J ? "J" : ""); + (G.jump ? "J" : ""); } @Override public TimingInput copy() { - return new TimingInput(W, A, S, D, P, N, J, G); + return new TimingInput(inputVector, P, N, G); } } diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingStorage.java b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingStorage.java index ed1f2ee4..d613afb9 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingStorage.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TimingStorage.java @@ -7,8 +7,6 @@ import io.github.kurrycat.mpkmod.util.FileUtil; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -51,6 +49,10 @@ public static String match(List inputList) { List matches = new ArrayList<>(); for (Map.Entry entry : patterns.entrySet()) { Timing.Match match = entry.getValue().match(inputList); + + if (match == null && entry.getValue().isSymmetrical()) { + match = entry.getValue().getMirrored().match(inputList); + } if (match != null) { matches.add(match); } diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/Range.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/Range.java index a89d5a57..251a47c5 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/util/Range.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/Range.java @@ -1,12 +1,27 @@ package io.github.kurrycat.mpkmod.util; +import java.util.Objects; + public class Range { private final Integer lower; private final Integer upper; public Range(Integer lower, Integer upper) { - this.lower = lower; - this.upper = upper; + if (lower == null || upper == null) { + this.lower = lower; + this.upper = upper; + } else { + this.lower = Math.min(lower, upper); + this.upper = Math.max(lower, upper); + } + } + + public Integer getLower() { + return lower; + } + + public Integer getUpper() { + return upper; } public boolean includes(int v) { @@ -22,8 +37,22 @@ public int constrain(int v) { return v; } - public boolean isAbove(int v) { + public boolean isValueBelow(int v) { + if (lower == null) return false; + return v < lower; + } + + public boolean isValueAbove(int v) { if (upper == null) return false; return v > upper; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range that = (Range) o; + + return Objects.equals(lower, that.lower) && Objects.equals(upper, that.upper); + } } diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/Tuple.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/Tuple.java index c3c86983..547df6c8 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/util/Tuple.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/Tuple.java @@ -1,5 +1,7 @@ package io.github.kurrycat.mpkmod.util; +import java.util.Objects; + public class Tuple implements Copyable> { private A a; private B b; @@ -26,7 +28,7 @@ public void setSecond(B b) { } @SuppressWarnings("unchecked") - public Tuple copy() { + public Tuple copy() { return new Tuple<>( this.a instanceof Copyable ? ((Copyable) this.a).copy() : this.a, this.b instanceof Copyable ? ((Copyable) this.b).copy() : this.b @@ -37,4 +39,13 @@ public Tuple copy() { public String toString() { return "Tuple{" + a + ", " + b + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tuple that = (Tuple) o; + + return Objects.equals(a, that.a) && Objects.equals(b, that.b); + } } diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicate.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicate.java new file mode 100644 index 00000000..57d58623 --- /dev/null +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicate.java @@ -0,0 +1,111 @@ +package io.github.kurrycat.mpkmod.util.input; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public final class InputPredicate extends InputPredicateBase { + private final AxisConstraint wsConstraint; + private final AxisConstraint adConstraint; + private final boolean forbidStop; + + InputVector lastMatchedInput; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public InputPredicate(String inputs) { + // uppercase -> true (required); lowercase -> null (optional); absent -> false (forbidden) + this( + (inputs.indexOf('W') >= 0 ? Boolean.TRUE : (inputs.indexOf('w') >= 0 ? null : Boolean.FALSE)), + (inputs.indexOf('A') >= 0 ? Boolean.TRUE : (inputs.indexOf('a') >= 0 ? null : Boolean.FALSE)), + (inputs.indexOf('S') >= 0 ? Boolean.TRUE : (inputs.indexOf('s') >= 0 ? null : Boolean.FALSE)), + (inputs.indexOf('D') >= 0 ? Boolean.TRUE : (inputs.indexOf('d') >= 0 ? null : Boolean.FALSE)), + inputs.indexOf('?') >= 0 + ); + } + + public InputPredicate(Boolean W, Boolean A, Boolean S, Boolean D, boolean forbidStop) { + this( + AxisConstraint.fromAxisConstraints(W, S), + AxisConstraint.fromAxisConstraints(A, D), + forbidStop + ); + } + + private InputPredicate(AxisConstraint wsConstraint, AxisConstraint adConstraint, boolean forbidStop) { + this.wsConstraint = wsConstraint; + this.adConstraint = adConstraint; + this.forbidStop = forbidStop; + } + + public void resetLastMatch() { + lastMatchedInput = null; + } + + public boolean matches(InputVector input) { + if (lastMatchedInput != null) + return lastMatchedInput.equals(input); + + boolean isMatch = ( + (wsConstraint.allows(input.WS)) && + (adConstraint.allows(input.AD)) && + !(input.isStop() && forbidStop) + ); + if (isMatch) lastMatchedInput = input; + + return isMatch; + } + + public InputPredicate mirrored() { + return new InputPredicate(wsConstraint, adConstraint.opposite(), forbidStop); + } + + private enum AxisConstraint { + REQUIRED_POS(true, false, false), + OPTIONAL_POS(true, true, false), + FORBIDDEN(false, true, false), + ANY(true, true, true), + OPTIONAL_NEG(false, true, true), + REQUIRED_NEG(false, false, true); + + private final boolean allowPos; + private final boolean allowZero; + private final boolean allowNeg; + + AxisConstraint(boolean allowPos, boolean allowZero, boolean allowNeg) { + this.allowPos = allowPos; + this.allowZero = allowZero; + this.allowNeg = allowNeg; + } + + public boolean allows(int axisTernary) { + if (axisTernary > 0) return allowPos; + if (axisTernary < 0) return allowNeg; + return allowZero; + } + + public AxisConstraint opposite() { + switch (this) { + case ANY: case FORBIDDEN: return this; + + case REQUIRED_POS: return REQUIRED_NEG; + case REQUIRED_NEG: return REQUIRED_POS; + case OPTIONAL_POS: return OPTIONAL_NEG; + case OPTIONAL_NEG: return OPTIONAL_POS; + + default: return FORBIDDEN; + } + } + + public static AxisConstraint fromAxisConstraints(Boolean pos, Boolean neg) { + if (pos == neg) { + if (pos == null) return ANY; + return FORBIDDEN; + } + if (pos == null) return neg ? OPTIONAL_NEG : OPTIONAL_POS; + if (neg == null) return pos ? OPTIONAL_POS : OPTIONAL_NEG; + + if (pos) return REQUIRED_POS; + if (neg) return REQUIRED_NEG; + + return FORBIDDEN; + } + } +} diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateBase.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateBase.java new file mode 100644 index 00000000..0f1e2628 --- /dev/null +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateBase.java @@ -0,0 +1,18 @@ +package io.github.kurrycat.mpkmod.util.input; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.JsonNode; + +public abstract class InputPredicateBase { + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static InputPredicateBase create(JsonNode node) { + if (node.isTextual()) + return new InputPredicate(node.asText()); + if (node.isInt()) + return new InputPredicateReference(node.asInt()); + + return null; + } + + public abstract boolean matches(InputVector input); +} diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReference.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReference.java new file mode 100644 index 00000000..bdf8f6bf --- /dev/null +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReference.java @@ -0,0 +1,38 @@ +package io.github.kurrycat.mpkmod.util.input; + +import io.github.kurrycat.mpkmod.ticks.TimingEntry; + +public class InputPredicateReference extends InputPredicateBase { + private final int index; + private TimingEntry[] parentTimingEntries; + private InputPredicate referencedPredicate; + + public InputPredicateReference(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public void setParentTimingEntries(TimingEntry[] parentTimingEntries) { + if (parentTimingEntries == null) return; + + if (this.parentTimingEntries != null) + throw new IllegalStateException("Parent timing entry array is already set"); + + this.parentTimingEntries = parentTimingEntries; + + if (parentTimingEntries[index].inputPredicate.getClass().equals(InputPredicate.class)) { + this.referencedPredicate = (InputPredicate) parentTimingEntries[index].inputPredicate; + } else { + this.referencedPredicate = null; + } + } + + @Override + public boolean matches(InputVector input) { + if (referencedPredicate == null) return false; + return referencedPredicate.matches(input); + } +} diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputVector.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputVector.java new file mode 100644 index 00000000..bc0c6d7d --- /dev/null +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/InputVector.java @@ -0,0 +1,66 @@ +package io.github.kurrycat.mpkmod.util.input; + +public final class InputVector { + public static final InputVector ZERO = new InputVector(false, false, false, false); + public final int WS; + public final int AD; + + public InputVector(boolean W, boolean A, boolean S, boolean D) { + WS = (W ? 1 : 0) - (S ? 1 : 0); + AD = (A ? 1 : 0) - (D ? 1 : 0); + } + public InputVector(int WS, int AD) { + this.WS = Integer.compare(WS, 0); + this.AD = Integer.compare(AD, 0); + } + + public boolean isW() { + return WS > 0; + } + public boolean isA() { + return AD > 0; + } + public boolean isS() { + return WS < 0; + } + public boolean isD() { + return AD < 0; + } + + public boolean isStop() { + return WS == 0 && AD == 0; + } + + public static InputVector fromString(String inputString) { + if (inputString == null) return null; + + boolean W = inputString.indexOf('W') >= 0; + boolean A = inputString.indexOf('A') >= 0; + boolean S = inputString.indexOf('S') >= 0; + boolean D = inputString.indexOf('D') >= 0; + + return new InputVector(W, A, S, D); + } + + @Override + public String toString() { + return ( + (WS == 0 ? "" : (isW() ? "W" : "S")) + + (AD == 0 ? "" : (isA() ? "A" : "D")) + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InputVector that = (InputVector) o; + + return WS == that.WS && AD == that.AD; + } + + @Override + public int hashCode() { + return 3 * WS + AD; + } +} diff --git a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TickInput.java b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/TickInput.java similarity index 98% rename from common/src/main/java/io/github/kurrycat/mpkmod/ticks/TickInput.java rename to common/src/main/java/io/github/kurrycat/mpkmod/util/input/TickInput.java index b5decd38..cf2def80 100644 --- a/common/src/main/java/io/github/kurrycat/mpkmod/ticks/TickInput.java +++ b/common/src/main/java/io/github/kurrycat/mpkmod/util/input/TickInput.java @@ -1,4 +1,4 @@ -package io.github.kurrycat.mpkmod.ticks; +package io.github.kurrycat.mpkmod.util.input; import io.github.kurrycat.mpkmod.util.Copyable; diff --git a/common/src/main/resources/assets/mpkmod/strats/strats.json b/common/src/main/resources/assets/mpkmod/strats/strats.json index 155e7b22..88ff2946 100644 --- a/common/src/main/resources/assets/mpkmod/strats/strats.json +++ b/common/src/main/resources/assets/mpkmod/strats/strats.json @@ -6,11 +6,22 @@ "a!=1": " {a}t", "a_ms": " ({a_ms}ms)" }, - "timingEntries": [ - "a{1,1}:WJ", - "a:W!G", - "WP!G" - ] + "timingEntries": [{ + "var": "a", + "inputs": "Wad", + "sprint": false, + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": 0, + "sprint": false, + "groundState": "AIRBORNE" + }, { + "min": 1, + "inputs": 0, + "sprint": true, + "groundState": "AIRBORNE" + }] }, "c4.5": { "format": { @@ -20,50 +31,69 @@ "a_ms": " ({a_ms}ms)", "r!=1": " Run {r}t" }, - "timingEntries": [ - "a{1,1}:WJ", - "a:W!G", - "b:WP!G", - "WPG", - "r{1,}:WPG", - "WPJ" - ] + "timingEntries": [{ + "var": "a", + "inputs": "Wad", + "sprint": false, + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": 0, + "sprint": false, + "groundState": "AIRBORNE" + }, { + "min": 1, + "inputs": 0, + "sprint": true, + "groundState": "AIRBORNE" + }, { + "inputs": 0, + "groundState": "GROUNDED" + }, { + "var": "r", "min": 1, + "inputs": 0, + "groundState": "GROUNDED" + }, { + "inputs": 0, + "groundState": "JUMPING" + }] }, "pessi": { "format": { "a==1": "Max ", "default": "Pessi", "a!=1": " -{a}t", - "a_ms": " ({a_ms}ms}" - }, - "timingEntries": [ - "a{1,1}:J", - "a:!G", - "WP!G" - ] - }, - "bwpessi": { - "format": { - "a==1": "Max ", - "default": "BWPessi", - "a!=1": " -{a}t", - "a_ms": " ({a_ms}ms}" + "a_ms": " ({a_ms}ms)" }, - "timingEntries": [ - "a{1,1}:J", - "a:!G", - "S!G" - ] + "timingEntries": [{ + "var": "a", + "inputs": "", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "", + "groundState": "AIRBORNE" + }, { + "inputs": "wasd?", + "groundState": "AIRBORNE" + }] }, "a7hh": { "format": { "default": "A7HH" }, - "timingEntries": [ - "a{1,1}:J", - "a:!G", - "WPG" - ] + "timingEntries": [{ + "var": "a", + "inputs": "", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "", + "groundState": "AIRBORNE" + }, { + "inputs": "wasd?", + "groundState": "GROUNDED" + }] }, "panefmm": { "format": { @@ -72,65 +102,77 @@ "a!=1&&a!=4": " {a}t", "r!=1": " Run {r}t" }, - "timingEntries": [ - "a{1,1}:J", - "a:", - "b:WP!G", - "WPG", - "r{1,6}:WPG", - "WPJ" - ] + "timingEntries": [{ + "var": "a", + "inputs": "", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "", + "groundState": "AIRBORNE" + }, { + "min": 0, + "inputs": "wasd?", + "groundState": "AIRBORNE" + }, { + "inputs": 2, + "groundState": "GROUNDED" + }, { + "var": "r", "min": 1, + "inputs": 2, + "groundState": "GROUNDED" + }, { + "inputs": 2, + "groundState": "JUMPING" + }] }, "jam": { "format": { "default": "Jam" }, - "timingEntries": [ - "WPJ" - ] - }, - "bwjam": { - "format": { - "default": "BWJam" - }, - "timingEntries": [ - "SJ" - ] + "timingEntries": [{ + "inputs": "wasd?", + "groundState": "JUMPING" + }] }, "hh": { "format": { "default": "HH", "x!=1": " {x}t", - "x_ms": " ({x_ms}ms}" - }, - "timingEntries": [ - "x{1,}:WP", - "WPJ" - ] - }, - "bwhh": { - "format": { - "default": "BWHH", - "x!=1": " {x}t", - "x_ms": " ({x_ms}ms}" + "x_ms": " ({x_ms}ms)" }, - "timingEntries": [ - "x{1,}:S", - "SJ" - ] + "timingEntries": [{ + "var": "x", "min": 1, + "inputs": "wasd?", + "groundState": "GROUNDED" + }, { + "inputs": 0, + "groundState": "JUMPING" + }] }, "3bcbwmm": { "format": { "default": "3bc BWMM", "w!=1": " Walk {w}t" }, - "timingEntries": [ - "SJ", - "a:S!G", - "SG", - "w{1,}:SG", - "WPJ" - ] + "timingEntries": [{ + "inputs": "S", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "S", + "groundState": "AIRBORNE" + }, { + "inputs": "S", + "groundState": "GROUNDED" + }, { + "var": "w", "min": 1, + "inputs": "S", + "groundState": "GROUNDED" + }, { + "inputs": "W", + "groundState": "JUMPING" + }] }, "rexbwmm": { "format": { @@ -139,37 +181,97 @@ "s_ms": " ({s_ms}ms)", "w!=1": " Walk {w}t" }, - "timingEntries": [ - "SJ", - "a:S!G", - "SG", - "w{1,}:SG", - "s{1,1}:WAPJ", - "s:WAP!G", - "WP!G" - ] + "timingEntries": [{ + "inputs": "S", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "S", + "groundState": "AIRBORNE" + }, { + "inputs": "S", + "groundState": "GROUNDED" + }, { + "var": "w", "min": 1, + "inputs": "S", + "groundState": "GROUNDED" + }, { + "var": "s", + "inputs": "WA", + "groundState": "JUMPING" + }, { + "var": "s", "min": 0, + "inputs": "WA", + "groundState": "AIRBORNE" + }, { + "inputs": "W", + "groundState": "AIRBORNE" + }], + "symmetrical": true }, "mark": { + "format": { + "default": "{a}t Mark" + }, + "timingEntries": [{ + "var": "a", + "inputs": "D", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "D", + "groundState": "AIRBORNE" + }, { + "inputs": "WD", + "groundState": "AIRBORNE" + }], + "symmetrical": true + }, + "markrun": { "format": { "default": "{a}t Mark", "r!=1": " Run {r}t" }, - "timingEntries": [ - "DJ", - "a{1,}:D!G", - "b:WDP!G", - "r{1,}:WDPG" - ] + "timingEntries": [{ + "var": "a", + "inputs": "D", + "groundState": "JUMPING" + }, { + "var": "a", "min": 0, + "inputs": "D", + "groundState": "AIRBORNE" + }, { + "min": 0, + "inputs": "WD", + "groundState": "AIRBORNE" + }, { + "inputs": "WD", + "groundState": "GROUNDED" + }, { + "var": "r", "min": 1, + "inputs": "WD", + "groundState": "GROUNDED" + }, { + "inputs": "Wd", + "groundState": "JUMPING" + }], + "symmetrical": true }, "delayedhh": { "format": { "default": "Delayed HH", "x!=1": " {x}t", - "x_ms": " ({x_ms}ms}" + "x_ms": " ({x_ms}ms)" }, - "timingEntries": [ - "x{1,}:W", - "WPJ" - ] + "timingEntries": [{ + "var": "x", "min": 1, + "inputs": "Wad", + "sprint": false, + "groundState": "GROUNDED" + }, { + "inputs": 0, + "sprint": true, + "groundState": "JUMPING" + }] } } \ No newline at end of file diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingEntryTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingEntryTest.java new file mode 100644 index 00000000..86763c40 --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingEntryTest.java @@ -0,0 +1,127 @@ +package io.github.kurrycat.mpkmod.ticks; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.kurrycat.mpkmod.util.Range; +import io.github.kurrycat.mpkmod.util.input.InputPredicate; +import io.github.kurrycat.mpkmod.util.input.InputVector; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class TimingEntryTest { + private static final ObjectMapper mapper = new ObjectMapper(); + + private static TimingEntry createSprintStrafeTimingEntry() { + return new TimingEntry( + "x", 0, null, + new InputPredicate("WD"), + true, + null, + GroundState.GROUNDED + ); + } + + private static List createWadInputList() { + List wadInputs = new ArrayList<>(); + + wadInputs.add(TimingInput.stopTick()); + wadInputs.add(TimingInput.stopTick()); + wadInputs.add(new TimingInput(InputVector.fromString("WD"), true, true, GroundState.GROUNDED)); + wadInputs.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.GROUNDED)); + wadInputs.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.JUMPING)); + wadInputs.add(new TimingInput(InputVector.fromString("WA"), true, false, GroundState.AIRBORNE)); + + return wadInputs; + } + + @Test + void deserializesTimingEntryCorrectly() throws IOException { + String json = String.join("\n", + "{", + " \"var\": \"foo\",", + " \"min\": 1,", + " \"max\": 5,", + " \"inputs\": \"WASD\",", + " \"sprint\": false,", + " \"sneak\": true,", + " \"groundState\": \"JUMPING\"", + "}" + ); + + TimingEntry e = mapper.readValue(json, TimingEntry.class); + + assertEquals("foo", e.varName); + assertEquals(new Range(1, 5), e.range); + assertNotNull(e.inputPredicate); + assertFalse(e.P); + assertTrue(e.N); + assertEquals(GroundState.JUMPING, e.G); + } + + @Test + void deserializesTimingEntryDefaultsCorrectly() throws IOException { + String json = "{}"; + + TimingEntry e = mapper.readValue(json, TimingEntry.class); + + assertNull(e.varName); + assertEquals(new Range(1, 1), e.range); + assertNull(e.inputPredicate); + assertNull(e.P); + assertNull(e.N); + assertNull(e.G); + } + + @Test + void getMsMethodWorks() { + final int FIRST_MS = 3; + final int SECOND_MS = 69; + + TimingEntry timingEntry = new TimingEntry( + "x", 0, null, + new InputPredicate("W"), + null, + null, + GroundState.GROUNDED + ); + + List inputList = new ArrayList<>(); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.JUMPING)); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.AIRBORNE)); + + inputList.get(0).msList.add(ButtonMS.of(ButtonMS.Button.FORWARD, 1_000_000 * FIRST_MS, true)); + inputList.get(1).msList.add(ButtonMS.of(ButtonMS.Button.JUMP, 1_000_000 * SECOND_MS, true)); + + HashMap vars = new HashMap<>(); + + timingEntry.matches(inputList, 0, vars, false); + assertEquals(SECOND_MS - FIRST_MS, vars.get("x").ms); + } + + @Test + void matchesMethodCreatesVariablesProperly() { + HashMap vars = new HashMap<>(); + + Integer matchCount = createSprintStrafeTimingEntry().matches(createWadInputList(), 2, vars, false); + + assertEquals(2, matchCount); + assertEquals(2, vars.get("x").tickCount); + } + + @Test + void matchesMethodCumulatesVariablesProperly() { + HashMap vars = new HashMap<>(); + vars.put("x", new Timing.TickMS(2)); + + Integer matchCount = createSprintStrafeTimingEntry().matches(createWadInputList(), 2, vars, true); + + assertEquals(2, matchCount); + assertEquals(4, vars.get("x").tickCount); + } +} diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingInputTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingInputTest.java new file mode 100644 index 00000000..37c9df68 --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingInputTest.java @@ -0,0 +1,67 @@ +package io.github.kurrycat.mpkmod.ticks; + +import io.github.kurrycat.mpkmod.util.Tuple; +import io.github.kurrycat.mpkmod.util.input.InputVector; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TimingInputTest { + private void assertFindsCorrectMsButtonPair(ButtonMS.Button expected1, ButtonMS.Button expected2, TimingInput before, TimingInput after, List curr) { + assertFindsCorrectMsButtonReturn( + new Tuple<>(expected1, expected2), + before, after, curr + ); + } + private void assertFindsCorrectMsButtonReturn(Tuple expected, TimingInput before, TimingInput after, List curr) { + assertEquals( + expected, + TimingInput.findMSButtons(before, after, curr) + ); + } + + @Test + void findsCorrectSingleMsButton() { + TimingInput before = new TimingInput(InputVector.fromString("D"), true, false, GroundState.GROUNDED); + + List curr = new ArrayList<>(); + curr.add(new TimingInput(InputVector.fromString("WD"), false, false, GroundState.GROUNDED)); + curr.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.JUMPING)); + curr.add(new TimingInput(InputVector.fromString("WA"), true, false, GroundState.AIRBORNE)); + + TimingInput after = new TimingInput(InputVector.fromString("A"), true, false, GroundState.AIRBORNE); + + assertFindsCorrectMsButtonPair(ButtonMS.Button.FORWARD, ButtonMS.Button.FORWARD, before, after, curr); + } + + @Test + void findMsButtonsFailsIfANonJumpKeyChanges() { + TimingInput before = new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED); + + List curr = new ArrayList<>(); + for (int i = 0; i < 2; i++) curr.add(before.copy()); + curr.add(new TimingInput(before.inputVector, before.P, !before.N, before.G)); // changes non-jump key state + + TimingInput after = before.copy(); + + assertNull(TimingInput.findMSButtons(before, after, curr)); + } + + @Test + void findsCorrectInterruptionMsButtons() { + TimingInput before = new TimingInput(InputVector.fromString(""), false, false, GroundState.GROUNDED); + + List curr = new ArrayList<>(); + curr.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + curr.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + curr.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + + TimingInput after = new TimingInput(InputVector.fromString("W"), true, false, GroundState.JUMPING); + + assertFindsCorrectMsButtonPair(ButtonMS.Button.FORWARD, ButtonMS.Button.JUMP, before, after, curr); + } +} diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingTest.java new file mode 100644 index 00000000..acf007dd --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/ticks/TimingTest.java @@ -0,0 +1,107 @@ +package io.github.kurrycat.mpkmod.ticks; + +import io.github.kurrycat.mpkmod.util.input.InputPredicate; +import io.github.kurrycat.mpkmod.util.input.InputPredicateReference; +import io.github.kurrycat.mpkmod.util.input.InputVector; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TimingTest { + private static final InputPredicate stop = new InputPredicate(false, false, false, false, false); + private static final InputPredicate move = new InputPredicate(null, null, null, null, true); + private static final InputPredicate onlyW = new InputPredicate(true, false, false, false, true); + private static final InputPredicate onlyA = new InputPredicate(false, true, false, false, true); + private static final InputPredicate onlyS = new InputPredicate(false, false, true, false, true); + private static final InputPredicate onlyD = new InputPredicate(false, false, false, true, true); + + private static final Timing basicTiming; + private static final Timing paneFmmTiming; + + static { + LinkedHashMap paneFmmFormat = new LinkedHashMap<>(); + paneFmmFormat.put(new Timing.FormatCondition("default"), new Timing.FormatString("pane fmm -{a}t run {r}t")); + + TimingEntry[] paneFmmTimingEntries = new TimingEntry[] { + new TimingEntry("a", 1, 1, stop, null, null, GroundState.JUMPING), + new TimingEntry("a", 0, null, stop, null, null, GroundState.AIRBORNE), + new TimingEntry(null, 0, null, move, null, null, GroundState.AIRBORNE), + new TimingEntry(null, 1, 1, new InputPredicateReference(2), null, null, GroundState.GROUNDED), + new TimingEntry("r", 1, null, new InputPredicateReference(2), null, null, GroundState.GROUNDED), + new TimingEntry(null, 1, 1, new InputPredicateReference(2), null, null, GroundState.JUMPING) + }; + + paneFmmTiming = new Timing(paneFmmFormat, paneFmmTimingEntries, false); + + + LinkedHashMap basicFormat = new LinkedHashMap<>(); + basicFormat.put(new Timing.FormatCondition("default"), new Timing.FormatString("basic timing idk")); + + TimingEntry[] basicTimingEntries = new TimingEntry[] { + new TimingEntry(null, 1, 1, onlyW, null, null, GroundState.GROUNDED), + new TimingEntry(null, 1, 1, onlyD, null, null, GroundState.GROUNDED), + new TimingEntry(null, 1, 1, onlyA, null, null, GroundState.GROUNDED) + }; + + basicTiming = new Timing(basicFormat, basicTimingEntries, true); + } + + @Test + void basicTimingMatches() { + List inputList = new ArrayList<>(); + inputList.add(TimingInput.stopTick()); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("D"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("A"), true, false, GroundState.GROUNDED)); + + Timing.Match match = basicTiming.match(inputList); + assertNotNull(match); + } + + @Test + void createsMirroredTimingProperly() { + List inputList = new ArrayList<>(); + inputList.add(TimingInput.stopTick()); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("A"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("D"), true, false, GroundState.GROUNDED)); + + Timing.Match match = basicTiming.getMirrored().match(inputList); + assertNotNull(match); + } + + @Test + void transmitsEntryArrayToChildInputPredicateReferences() { + List inputList = new ArrayList<>(); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.JUMPING)); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.AIRBORNE)); + inputList.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.AIRBORNE)); + inputList.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.JUMPING)); + + Timing.Match match = paneFmmTiming.match(inputList); + assertNotNull(match); + } + + @Test + void failsIfChildInputPredicateReferenceConditionNotMet() { + List inputList = new ArrayList<>(); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.JUMPING)); + inputList.add(new TimingInput(InputVector.ZERO, false, false, GroundState.AIRBORNE)); + inputList.add(new TimingInput(InputVector.fromString("WD"), true, false, GroundState.AIRBORNE)); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.GROUNDED)); + inputList.add(new TimingInput(InputVector.fromString("W"), true, false, GroundState.JUMPING)); + + Timing.Match match = paneFmmTiming.match(inputList); + assertNull(match); + } +} diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReferenceTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReferenceTest.java new file mode 100644 index 00000000..023f23c9 --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateReferenceTest.java @@ -0,0 +1,26 @@ +package io.github.kurrycat.mpkmod.util.input; + +import io.github.kurrycat.mpkmod.ticks.TimingEntry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class InputPredicateReferenceTest { + @Test + void delegatesMatchProperly() { + InputPredicate predicate = new InputPredicate("wasd?"); + predicate.matches(new InputVector(true, true, false, false)); + + InputPredicateReference predicateReference = new InputPredicateReference(0); + + TimingEntry[] parentTimingEntries = new TimingEntry[] { + new TimingEntry(null, 1, 1, predicate, null, null, null), + new TimingEntry(null, 1, 1, predicateReference, null, null, null) + }; + predicateReference.setParentTimingEntries(parentTimingEntries); + + assertFalse(predicateReference.matches(new InputVector(true, false, false, false))); + assertTrue(predicateReference.matches(new InputVector(true, true, false, false))); + } +} diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateTest.java new file mode 100644 index 00000000..4143c1a1 --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputPredicateTest.java @@ -0,0 +1,72 @@ +package io.github.kurrycat.mpkmod.util.input; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InputPredicateTest { + static final InputVector VEC_ZERO = InputVector.fromString(""); + static final InputVector VEC_W = InputVector.fromString("W"); + static final InputVector VEC_A = InputVector.fromString("A"); + static final InputVector VEC_S = InputVector.fromString("S"); + static final InputVector VEC_D = InputVector.fromString("D"); + static final InputVector VEC_WA = InputVector.fromString("WA"); + static final InputVector VEC_WD = InputVector.fromString("WD"); + static final InputVector VEC_SA = InputVector.fromString("SA"); + static final InputVector VEC_SD = InputVector.fromString("SD"); + + @ParameterizedTest + @MethodSource("exactMatchCases") + @MethodSource("someMatchCases") + @MethodSource("anyMatchCases") + void inputPredicateMatchesVectorTest(boolean expected, String predicateString, InputVector v) { + InputPredicate p = new InputPredicate(predicateString); + assertEquals(expected, p.matches(v)); + } + + static Stream exactMatchCases() { + return Stream.of( + Arguments.of(false, "WD", VEC_W), + Arguments.of(false, "WD", VEC_WA), + Arguments.of(true, "WD", VEC_WD), + + Arguments.of(false, "A", VEC_D), + Arguments.of(false, "A", VEC_SA), + Arguments.of(true, "A", VEC_A) + ); + } + + static Stream someMatchCases() { + return Stream.of( + Arguments.of(false, "wad?", VEC_S), + Arguments.of(false, "wad?", VEC_SD), + Arguments.of(false, "wad?", VEC_ZERO), + Arguments.of(true, "wad?", VEC_WD), + Arguments.of(true, "wad?", VEC_A), + + Arguments.of(false, "w?", VEC_A), + Arguments.of(false, "w?", VEC_WA), + Arguments.of(true, "w?", VEC_W) + ); + } + + static Stream anyMatchCases() { + return Stream.of( + Arguments.of(true, "wasd", VEC_S), + Arguments.of(true, "wasd", VEC_SD), + Arguments.of(true, "wasd", VEC_WA), + Arguments.of(true, "wasd", VEC_ZERO), + + Arguments.of(false, "wad", VEC_SA), + Arguments.of(true, "wad", VEC_WA), + Arguments.of(true, "wad", VEC_ZERO), + + Arguments.of(false, "", VEC_W), + Arguments.of(true, "", VEC_ZERO) + ); + } +} \ No newline at end of file diff --git a/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputVectorTest.java b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputVectorTest.java new file mode 100644 index 00000000..0fb7fa4e --- /dev/null +++ b/common/src/test/java/io/github/kurrycat/mpkmod/util/input/InputVectorTest.java @@ -0,0 +1,25 @@ +package io.github.kurrycat.mpkmod.util.input; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class InputVectorTest { + @Test + void oppositeInputKeysCancelOut() { + InputVector inputVector = new InputVector(true, false, true, true); + + assertFalse(inputVector.isW()); + assertFalse(inputVector.isA()); + assertFalse(inputVector.isS()); + assertTrue(inputVector.isD()); + } + + @Test + void cancelledKeysProduceCorrectVector() { + InputVector inputVector = new InputVector(true, false, true, true); + + assertEquals(0, inputVector.WS); + assertEquals(-1, inputVector.AD); + } +}