Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ loader_version=0.19.3
loom_version=1.17-SNAPSHOT

# Mod Properties
mod_version=2.1.1-beta.1
mod_version=2.1.2-beta.0
maven_group=com.kingjayan

# Dependencies
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/com/kingjayan/keybinds/core/action/ActionTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,31 @@ public final class ActionTypes {
GsonHelper.getAsInt(json, "useDelay", 3),
GsonHelper.getAsInt(json, "swapBackDelay", 2))));

public static final ActionType EQUIP_ARMOR = ActionRegistry.register(new ActionType("equip_armor",
json -> new EquipArmorAction(
GsonHelper.getAsString(json, "item", ""),
GsonHelper.getAsString(json, "hotbarGate", UseItemAction.GATE_ANY),
GsonHelper.getAsBoolean(json, "vanillaSwap", false),
GsonHelper.getAsInt(json, "swapToDelay", 2),
GsonHelper.getAsInt(json, "useDelay", 3),
GsonHelper.getAsInt(json, "swapBackDelay", 2))));

public static final ActionType SHIELD_TOTEM_SWAP = ActionRegistry.register(new ActionType("shield_totem_swap",
json -> new ShieldTotemSwapAction()));

public static final ActionType LOOK = ActionRegistry.register(new ActionType("look",
json -> new LookAction(GsonHelper.getAsString(json, "direction", "north"))));
json -> new LookAction(
GsonHelper.getAsString(json, "direction", "north"),
GsonHelper.getAsString(json, "motion", CameraMotion.MOTION_INSTANT),
GsonHelper.getAsString(json, "easing", "linear"),
GsonHelper.getAsInt(json, "ticks", 10))));

public static final ActionType TURN = ActionRegistry.register(new ActionType("turn",
json -> new TurnAction(GsonHelper.getAsFloat(json, "degrees", 0.0F))));
json -> new TurnAction(
GsonHelper.getAsFloat(json, "degrees", 0.0F),
GsonHelper.getAsString(json, "motion", CameraMotion.MOTION_INSTANT),
GsonHelper.getAsString(json, "easing", "linear"),
GsonHelper.getAsInt(json, "ticks", 10))));

public static final ActionType SPRINT = ActionRegistry.register(new ActionType("sprint",
json -> new SprintAction(GsonHelper.getAsString(json, "mode", "on"))));
Expand Down
81 changes: 81 additions & 0 deletions src/main/java/com/kingjayan/keybinds/core/action/CameraMotion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.kingjayan.keybinds.core.action;

import com.kingjayan.keybinds.core.schedule.TickScheduler;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;

import java.util.concurrent.ThreadLocalRandom;

final class CameraMotion {

static final String MOTION_INSTANT = "instant";
static final String MOTION_LERP = "lerp";
static final String MOTION_JITTER = "jitter";
static final String MOTION_RANDOM = "random";

private static final float JITTER_DEGREES = 2.0F;

private CameraMotion() {
}

static int animate(LocalPlayer player, TickScheduler scheduler, float yawDelta, float pitchDelta,
String motion, String easing, int ticks) {
if (motion == null || motion.equals(MOTION_INSTANT) || ticks <= 0) {
apply(player, player.getYRot() + yawDelta, player.getXRot() + pitchDelta);
return 0;
}
float startYaw = player.getYRot();
float startPitch = player.getXRot();
float[] progress = progressCurve(motion, easing, ticks);
boolean jitter = motion.equals(MOTION_JITTER);
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int step = 1; step <= ticks; step++) {
float p = progress[step - 1];
float noise = jitter && step < ticks
? (random.nextFloat() * 2.0F - 1.0F) * JITTER_DEGREES * (1.0F - p) : 0.0F;
float yaw = startYaw + yawDelta * p + noise;
float pitch = startPitch + pitchDelta * p + (pitchDelta != 0.0F ? noise : 0.0F);
scheduler.schedule(step, () -> apply(player, yaw, pitch));
}
return ticks;
}

private static void apply(LocalPlayer player, float yaw, float pitch) {
player.setYRot(Mth.wrapDegrees(yaw));
player.setXRot(Mth.clamp(pitch, -90.0F, 90.0F));
}

private static float[] progressCurve(String motion, String easing, int ticks) {
float[] progress = new float[ticks];
if (motion.equals(MOTION_RANDOM)) {
ThreadLocalRandom random = ThreadLocalRandom.current();
float total = 0.0F;
for (int i = 0; i < ticks; i++) {
total += random.nextFloat() + 0.1F;
progress[i] = total;
}
for (int i = 0; i < ticks; i++) {
progress[i] = ease(easing, progress[i] / total);
}
} else {
for (int i = 0; i < ticks; i++) {
progress[i] = ease(easing, (i + 1.0F) / ticks);
}
}
progress[ticks - 1] = 1.0F;
return progress;
}

private static float ease(String easing, float p) {
return switch (easing == null ? "linear" : easing) {
case "cubic" -> p < 0.5F
? 4.0F * p * p * p
: 1.0F - (float) Math.pow(-2.0F * p + 2.0F, 3) / 2.0F;
case "quartic" -> p < 0.5F
? 8.0F * p * p * p * p
: 1.0F - (float) Math.pow(-2.0F * p + 2.0F, 4) / 2.0F;
case "bezier" -> p * p * (3.0F - 2.0F * p);
default -> p;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.kingjayan.keybinds.core.action;

import com.google.gson.JsonObject;

public record EquipArmorAction(String item, String hotbarGate, boolean vanillaSwap,
int swapToDelay, int useDelay, int swapBackDelay) implements Action {

@Override
public ActionType type() {
return ActionTypes.EQUIP_ARMOR;
}

@Override
public void write(JsonObject out) {
out.addProperty("item", item);
out.addProperty("hotbarGate", hotbarGate);
out.addProperty("vanillaSwap", vanillaSwap);
out.addProperty("swapToDelay", swapToDelay);
out.addProperty("useDelay", useDelay);
out.addProperty("swapBackDelay", swapBackDelay);
}

@Override
public ActionResult execute(ActionContext ctx) {
return new UseItemAction(item, hotbarGate, vanillaSwap,
swapToDelay, useDelay, swapBackDelay).execute(ctx);
}
}
27 changes: 17 additions & 10 deletions src/main/java/com/kingjayan/keybinds/core/action/LookAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;

public record LookAction(String direction) implements Action {
public record LookAction(String direction, String motion, String easing, int ticks) implements Action {

@Override
public ActionType type() {
Expand All @@ -14,6 +14,9 @@ public ActionType type() {
@Override
public void write(JsonObject out) {
out.addProperty("direction", direction);
out.addProperty("motion", motion);
out.addProperty("easing", easing);
out.addProperty("ticks", ticks);
}

@Override
Expand All @@ -22,18 +25,22 @@ public ActionResult execute(ActionContext ctx) {
if (player == null) {
return ActionResult.proceed();
}
float yawDelta = 0.0F;
float pitchDelta = 0.0F;
switch (direction) {
case "north" -> player.setYRot(180.0F);
case "south" -> player.setYRot(0.0F);
case "east" -> player.setYRot(-90.0F);
case "west" -> player.setYRot(90.0F);
case "up" -> player.setXRot(-90.0F);
case "down" -> player.setXRot(90.0F);
case "left" -> player.setYRot(Mth.wrapDegrees(player.getYRot() - 90.0F));
case "right" -> player.setYRot(Mth.wrapDegrees(player.getYRot() + 90.0F));
case "north" -> yawDelta = Mth.wrapDegrees(180.0F - player.getYRot());
case "south" -> yawDelta = Mth.wrapDegrees(0.0F - player.getYRot());
case "east" -> yawDelta = Mth.wrapDegrees(-90.0F - player.getYRot());
case "west" -> yawDelta = Mth.wrapDegrees(90.0F - player.getYRot());
case "up" -> pitchDelta = -90.0F - player.getXRot();
case "down" -> pitchDelta = 90.0F - player.getXRot();
case "left" -> yawDelta = -90.0F;
case "right" -> yawDelta = 90.0F;
default -> {
return ActionResult.proceed();
}
}
return ActionResult.proceed();
int delay = CameraMotion.animate(player, ctx.scheduler(), yawDelta, pitchDelta, motion, easing, ticks);
return ActionResult.delay(delay);
}
}
13 changes: 9 additions & 4 deletions src/main/java/com/kingjayan/keybinds/core/action/TurnAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;

public record TurnAction(float degrees) implements Action {
public record TurnAction(float degrees, String motion, String easing, int ticks) implements Action {

@Override
public ActionType type() {
Expand All @@ -14,14 +14,19 @@ public ActionType type() {
@Override
public void write(JsonObject out) {
out.addProperty("degrees", degrees);
out.addProperty("motion", motion);
out.addProperty("easing", easing);
out.addProperty("ticks", ticks);
}

@Override
public ActionResult execute(ActionContext ctx) {
LocalPlayer player = ctx.client() != null ? ctx.client().player : null;
if (player != null) {
player.setYRot(Mth.wrapDegrees(degrees));
if (player == null) {
return ActionResult.proceed();
}
return ActionResult.proceed();
float yawDelta = Mth.wrapDegrees(degrees - player.getYRot());
int delay = CameraMotion.animate(player, ctx.scheduler(), yawDelta, 0.0F, motion, easing, ticks);
return ActionResult.delay(delay);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.kingjayan.keybinds.core.trigger;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import java.util.List;

public record EquippedTrigger(List<String> items, String enchant) implements Trigger {

@Override
public TriggerType type() {
return TriggerTypes.EQUIPPED;
}

@Override
public void write(JsonObject out) {
JsonArray array = new JsonArray();
for (String item : items) {
array.add(item);
}
out.add("items", array);
out.addProperty("enchant", enchant);
}

@Override
public TriggerResult evaluate(TriggerContext ctx) {
return new TriggerResult(ctx.equippedAny(items, enchant), false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public boolean holdingAny(List<String> items) {
return ItemLookup.matchesAny(client.player.getMainHandItem(), items);
}

public boolean equippedAny(List<String> items, String enchant) {
return client != null && ItemLookup.equippedAny(client.player, items, enchant);
}

public boolean edge(Trigger node, boolean active) {
current.put(node, active);
boolean was = previous.getOrDefault(node, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public final class TriggerTypes {
public static final TriggerType HOLDING = TriggerRegistry.register(new TriggerType("holding",
json -> new HoldingTrigger(readStrings(json, "items"))));

public static final TriggerType EQUIPPED = TriggerRegistry.register(new TriggerType("equipped",
json -> new EquippedTrigger(readStrings(json, "items"), GsonHelper.getAsString(json, "enchant", ""))));

public static final TriggerType KEY_COMBO = TriggerRegistry.register(new TriggerType("key_combo",
json -> new KeyComboTrigger(readStrings(json, "keys"))));

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/kingjayan/keybinds/dsl/ScriptCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ private static Condition leafCondition(CallExpr call) {
case "condition" -> new ScriptProgram.PlayerCondition(json.get("condition").getAsString());
case "holding" -> new ScriptProgram.HoldingCondition(
EditorSpecs.readStringList(json, "items"));
case "equipped" -> new ScriptProgram.EquippedCondition(
EditorSpecs.readStringList(json, "items"), json.get("enchant").getAsString());
case "key_combo" -> new ScriptProgram.ComboCondition(
EditorSpecs.readStringList(json, "keys"));
case "riding" -> new ScriptProgram.RidingCondition();
Expand Down
8 changes: 5 additions & 3 deletions src/main/java/com/kingjayan/keybinds/dsl/ScriptDocs.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ static Entry text(String description) {
ENTRIES.add(Entry.text("A script starts with one 'when <trigger>' line, followed by actions, one per line. '#' starts a comment."));
ENTRIES.add(Entry.text("Triggers combine with and / or / not and parentheses: when key(r) and not riding"));
ENTRIES.add(Entry.text("Arguments are positional or named: use_item(item=\"minecraft:ender_pearl\", useDelay=3). Lists use [\"a\", \"b\"]."));
ENTRIES.add(Entry.text("if <check> { ... } else { ... } branches while the macro runs. Checks are live state: holding, riding, key (held down), mouse, slot_selected, held_durability, and any player condition."));
ENTRIES.add(Entry.text("if <check> { ... } else if <check> { ... } else { ... } branches while the macro runs. Checks combine with and / or / not and parentheses. Checks are live state: holding, equipped, riding, key (held down), mouse, slot_selected, held_durability, and any player condition."));
ENTRIES.add(Entry.text("Key names: a-z, 0-9, f1-f25, space, left_shift, ... or a raw GLFW code. Mouse: left, right, middle, button4+."));
ENTRIES.add(Entry.text("random(min, max) can replace any number argument and rolls a new value on every run: use_item(useDelay=random(0, 2))."));

Expand All @@ -41,6 +41,7 @@ static Entry text(String description) {
trigger("mouse_release", "mouse_release(left)", "Fires when the mouse button is released.");
trigger("combo", "combo(ctrl, c)", "Fires when the modifiers (ctrl, shift, alt) plus at most one normal key are held together; modifiers match either side. Inside if: true while all held.");
trigger("holding", "holding(\"minecraft:mace\")", "Fires when you start holding one of the items; empty matches anything.");
trigger("equipped", "equipped(\"minecraft:elytra\", enchant=\"minecraft:unbreaking\")", "Fires when an armor piece or elytra is equipped; items and enchant are optional filters, multiple items use [\"a\", \"b\"].");
trigger("slot_selected", "slot_selected(0)", "Fires when the hotbar slot (0-8) is selected.");
trigger("riding", "riding", "Fires when you mount a vehicle or mob.");
trigger("held_durability", "held_durability(10)", "Fires when the held item's remaining durability drops to the value or less.");
Expand All @@ -58,8 +59,9 @@ static Entry text(String description) {
action("use", "use", "Uses the held item once.");
action("use_item", "use_item(item=\"minecraft:ender_pearl\", hotbarGate=any, vanillaSwap=false, swapToDelay=2, useDelay=3, swapBackDelay=2)", "Swaps to an item, uses it, and swaps back with configurable tick delays.");
action("shield_totem_swap", "shield_totem_swap", "Swaps shield and totem in the offhand.");
action("look", "look(north)", "Snaps the camera toward north, south, east, west, up, down, or 90° left/right.");
action("turn", "turn(90)", "Sets the camera yaw to the given degrees (0 south, 90 west, 180 north, -90 east).");
action("equip_armor", "equip_armor(item=\"minecraft:elytra\", useDelay=3)", "Equips an armor piece or elytra by using it; same arguments and swap behavior as use_item.");
action("look", "look(north, motion=lerp, easing=cubic, ticks=10)", "Moves the camera toward north, south, east, west, up, down, or 90° left/right; motion is instant, lerp, jitter, or random with linear, cubic, quartic, or bezier easing over ticks.");
action("turn", "turn(90, motion=jitter, ticks=8)", "Turns the camera to the given yaw (0 south, 90 west, 180 north, -90 east); motion is instant, lerp, jitter, or random with linear, cubic, quartic, or bezier easing over ticks.");
action("sprint", "sprint(on)", "Turns sprinting on, off, or toggle.");
action("sneak", "sneak(toggle)", "Turns sneaking on, off, or toggle.");
action("jump", "jump", "Taps the jump key once.");
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/com/kingjayan/keybinds/dsl/ScriptParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ private IfStmt parseIf() {
List<Stmt> elseBody = List.of();
if (peek().type() == TokenType.IDENT && peek().text().equals("else")) {
advance();
expect(TokenType.LBRACE, "'{' after else");
elseBody = parseStatements(TokenType.RBRACE);
expect(TokenType.RBRACE, "'}' to close else block");
if (peek().type() == TokenType.IDENT && peek().text().equals("if")) {
elseBody = List.of(parseIf());
} else {
expect(TokenType.LBRACE, "'{' after else");
elseBody = parseStatements(TokenType.RBRACE);
expect(TokenType.RBRACE, "'}' to close else block");
}
}
return new IfStmt(condition, thenBody, elseBody, line);
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/kingjayan/keybinds/dsl/ScriptPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ private static String expr(JsonObject json, int depth) {
case "mouse_release" -> "mouse_release(" + keyArg(json, "button", false) + ")";
case "condition" -> GsonHelper.getAsString(json, "condition", "low_health");
case "holding" -> "holding(" + quotedList(json, "items") + ")";
case "equipped" -> {
String enchant = GsonHelper.getAsString(json, "enchant", "");
String items = quotedList(json, "items");
yield "equipped(" + items
+ (enchant.isEmpty() ? "" : (items.isEmpty() ? "" : ", ") + "enchant=\"" + enchant + "\"")
+ ")";
}
case "key_combo" -> "combo(" + String.join(", ", EditorSpecs.readStringList(json, "keys")) + ")";
case "slot_selected" -> "slot_selected(" + GsonHelper.getAsInt(json, "slot", 0) + ")";
case "riding" -> "riding";
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/kingjayan/keybinds/dsl/ScriptProgram.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static Instruction jumpIfFalse(Condition condition) {

public sealed interface Condition permits AllCondition, AnyCondition, NotCondition, KeyDownCondition,
MouseDownCondition, PlayerCondition, HoldingCondition, RidingCondition, SlotCondition,
DurabilityCondition, ComboCondition {
DurabilityCondition, ComboCondition, EquippedCondition {

boolean test(Minecraft client);
}
Expand Down Expand Up @@ -130,6 +130,14 @@ public boolean test(Minecraft client) {
}
}

public record EquippedCondition(List<String> items, String enchant) implements Condition {

@Override
public boolean test(Minecraft client) {
return client != null && ItemLookup.equippedAny(client.player, items, enchant);
}
}

public record RidingCondition() implements Condition {

@Override
Expand Down
Loading