This commit is contained in:
Oliver Bigbee 2026-10-11 17:06:50 +01:00 • committed by GitHub
commit 5aa230438e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 8862 additions and 8476 deletions

View file

@ -0,0 +1,521 @@
package autismclient.util.macro;
import autismclient.modules.PackHideState;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismGuiActions;
import autismclient.util.AutismInventoryClickHelper;
import autismclient.util.AutismSharedState;
import net.minecraft.client.Minecraft;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.List;
import java.util.Locale;
final class MacroActionRunner {
private MacroActionRunner() {
}
public static void beginRaycastAim(Minecraft mc, MacroAction action) {
if (!(action instanceof RaycastAim aim) || !aim.isRaycast()) return;
autismclient.util.AutismRotationUtil.Rotation rotation;
try {
rotation = MacroClientThread.callOnClientThread(mc, () -> MacroRaycastAim.aimFor(mc, aim.raycastTarget(mc)), null);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
if (rotation == null) return;
MacroRaycastAim.hold(rotation);
MacroExecutor.waitOneTick();
}
static boolean dynamicTextInputsAvailable(MacroAction action, Minecraft mc) {
if (action instanceof WaitForChatAction chat && chat.effectiveMatchMode() == MacroCapturePattern.Mode.MATCH) {
return MacroVariables.resolve(chat.pattern, mc).success();
}
if (action instanceof WaitForGuiAction gui && gui.matchMode == MacroCapturePattern.Mode.MATCH) {
return MacroVariables.resolve(gui.guiTitle, mc).success();
}
if (action instanceof WaitGuiTypeAction gui && gui.matchMode == MacroCapturePattern.Mode.MATCH) {
return MacroVariables.resolve(gui.title, mc).success();
}
if (action instanceof WaitInventoryPredicateAction inventory) {
return MacroVariables.resolve(inventory.itemName, mc).success();
}
if (action instanceof WaitDurabilityAction durability && durability.targetMode == WaitDurabilityAction.TargetMode.ITEM) {
return MacroVariables.resolve(durability.itemName, mc).success();
}
if (action instanceof WaitEntityTargetAction entity) {
return MacroVariables.resolve(entity.entityId, mc).success();
}
return true;
}
public static java.util.List<net.minecraft.network.protocol.Packet<?>> preGeneratePacketsForRepeat(
int count, Minecraft mc) {
MacroExecutor.RepeatPacketContext context = MacroExecutor.REPEAT_PACKET_CONTEXT.get();
if (context == null || context.macro() == null) return new java.util.ArrayList<>();
return MacroRender.preGeneratePackets(context.macro(), context.startIdx(), count, context.endIdx());
}
public static boolean isTargetWaitInteractionAction(MacroAction action) {
return action instanceof OpenContainerAction || action instanceof InteractEntityAction;
}
public static CompletableFuture<Void> runTargetWaitInteractionAction(Minecraft mc, MacroAction action) throws InterruptedException {
boolean waitForTarget = switch (action) {
case OpenContainerAction open -> open.waitForTarget;
case InteractEntityAction interact -> interact.waitForTarget;
default -> false;
};
if (!waitForTarget) {
CompletableFuture<Void> postGuiFuture = createPostGuiFuture(mc, action);
runTargetInteractionAttempt(mc, action);
return postGuiFuture;
}
MacroLifecycle.setCurrentStatus("Waiting target: " + action.getDisplayName());
while (MacroLifecycle.isCurrentRunActive()) {
if (!canRunTargetInteractionNow(mc, action)) {
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) throw new InterruptedException();
continue;
}
CompletableFuture<Void> postGuiFuture = createPostGuiFuture(mc, action);
if (runTargetInteractionAttempt(mc, action)) {
return postGuiFuture;
}
if (postGuiFuture != null) postGuiFuture.cancel(true);
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) throw new InterruptedException();
}
return null;
}
private static boolean canRunTargetInteractionNow(Minecraft mc, MacroAction action) throws InterruptedException {
return MacroClientThread.callOnClientThread(mc, () -> {
if (action instanceof OpenContainerAction open) return open.canExecuteNow(mc);
if (action instanceof InteractEntityAction interact) return interact.canExecuteNow(mc);
return true;
}, Boolean.FALSE);
}
private static boolean runTargetInteractionAttempt(Minecraft mc, MacroAction action) throws InterruptedException {
return MacroClientThread.callOnClientThread(mc, () -> {
try {
if (action instanceof OpenContainerAction open) return open.tryExecute(mc);
if (action instanceof InteractEntityAction interact) return interact.tryExecute(mc);
action.execute(mc);
return true;
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cError in " + action.getDisplayName() + ": " + e.getMessage());
return true;
}
}, Boolean.FALSE, usesTickAlignedDispatch(action));
}
public static CompletableFuture<Void> createPostGuiFuture(Minecraft mc, MacroAction action) {
if (Boolean.TRUE.equals(MacroExecutor.SUPPRESS_POST_GUI_AFTER.get())) return null;
if (!(action instanceof WaitsForGui wfg) || !wfg.isWaitForGuiAfter()) return null;
return createWaitsForGuiFuture(mc, wfg);
}
private static CompletableFuture<Void> createWaitsForGuiFuture(Minecraft mc, WaitsForGui wfg) {
MacroTemplate.Resolution guiName = MacroVariables.resolve(wfg.getWaitGuiName(), mc);
if (!guiName.success()) return CompletableFuture.completedFuture(null);
return wfg.isWaitForGuiChange()
? MacroConditionRegistry.waitForGuiChange(mc.gui.screen())
: MacroConditionRegistry.waitForGui(guiName.value());
}
public static boolean waitForGuiBeforeAction(Minecraft mc, MacroAction action) throws InterruptedException {
if (!(action instanceof WaitsForGui wfg)) return true;
return waitForGuiBeforeAction(mc, wfg);
}
public static boolean waitForGuiBeforeAction(Minecraft mc, WaitsForGui action) throws InterruptedException {
if (mc == null || action == null || !action.isWaitForGuiBefore()) return true;
MacroTemplate.Resolution guiName = MacroVariables.resolve(action.getWaitGuiName(), mc);
if (!guiName.success()) return false;
if (isWaitGuiPresent(mc, guiName.value())) {
waitForCurrentGuiReady(mc);
return true;
}
CompletableFuture<Void> future = MacroConditionRegistry.waitForGui(guiName.value());
MacroRunLoop.waitForCondition(future);
waitForCurrentGuiReady(mc);
return true;
}
private static boolean isWaitGuiPresent(Minecraft mc, String guiName) {
if (mc == null || mc.gui.screen() == null) return false;
if (MacroGuiMatcher.isOwnScreen(mc.gui.screen())) return false;
String expected = guiName == null ? "" : guiName.trim();
if (expected.isEmpty()) return true;
String actual = mc.gui.screen().getTitle() == null ? "" : mc.gui.screen().getTitle().getString();
return matchesWaitGuiTitle(expected, actual);
}
private static boolean matchesWaitGuiTitle(String expected, String actual) {
if (expected == null || expected.isBlank() || actual == null || actual.isBlank()) return false;
String expectedLower = expected.toLowerCase(Locale.ROOT);
String actualLower = actual.toLowerCase(Locale.ROOT);
if (expectedLower.equals(actualLower) || actualLower.contains(expectedLower)) return true;
for (String word : expectedLower.split("\\s+")) {
if (!word.isBlank() && !actualLower.contains(word)) return false;
}
return true;
}
private static void waitForCurrentGuiReady(Minecraft mc) throws InterruptedException {
long deadline = System.nanoTime() + 500_000_000L;
while (MacroLifecycle.isCurrentRunActive()) {
Boolean ready = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null || mc.gui.screen() == null) return false;
if (mc.player.containerMenu != null && mc.player.containerMenu != mc.player.inventoryMenu) {
return mc.player.containerMenu.getStateId() > 0;
}
return true;
}, Boolean.FALSE);
if (Boolean.TRUE.equals(ready)) return;
if (System.nanoTime() >= deadline) return;
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) return;
}
}
public static void runMacroActionOnClientThread(Minecraft mc, MacroAction action, String label) throws InterruptedException {
Runnable body = () -> {
try {
action.execute(mc);
} catch (Throwable t) {
recordAddonActionError(action, "Action threw " + t.getClass().getSimpleName());
AutismClientMessaging.sendPrefixed("\u00a7cError in " + label + ": "
+ (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
}
};
if (usesTickAlignedDispatch(action)) MacroClientThread.runOnClientTick(mc, body);
else MacroClientThread.runOnClientThread(mc, body);
}
public static boolean usesTickAlignedDispatch(MacroAction action) {
return action instanceof PacketOrdered ordered && ordered.isTickAligned();
}
public static void runItemActionPaced(Minecraft mc, ItemAction ia) throws InterruptedException {
MacroLifecycle.setCurrentStatus(ia.getDisplayName());
java.util.List<Runnable> plan = MacroClientThread.callOnClientThread(mc, () -> ia.buildClickPlan(mc), java.util.Collections.<Runnable>emptyList());
if (plan == null || plan.isEmpty()) return;
long delayMs = Math.max(0, ia.clickDelayMs);
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
boolean first = true;
for (Runnable op : plan) {
if (!MacroLifecycle.isCurrentRunActive()) break;
if (!first && delayMs > 0) Thread.sleep(delayMs);
first = false;
mc.execute(() -> MacroLifecycle.withRunContext(scheduledRun, () -> {
if (PackHideState.isHardLocked()) return;
try {
op.run();
} catch (Throwable t) {
AutismClientMessaging.sendPrefixed("\u00a7cError in ItemAction: "
+ (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
}
}));
}
}
public static void runContextMacroAction(autismclient.api.macro.ContextMacroAction action) {
if (PackHideState.isHardLocked()) return;
if (action == null) return;
try {
action.run(new ApiExecutionContext(MacroExecutor.CURRENT_RUN.get()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable t) {
recordAddonActionError(action, "Context action threw " + t.getClass().getSimpleName());
AutismClientMessaging.sendPrefixed("\u00a7cAddon action error: "
+ (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
}
}
private static void recordAddonActionError(MacroAction action, String detail) {
if (action == null) return;
String owner = autismclient.api.macro.MacroActionRegistry.ownerOf(action.getTypeId());
if (owner != null && !owner.isBlank()) autismclient.addons.AddonManager.recordRuntimeError(owner, detail);
}
private static final class ApiExecutionContext implements autismclient.api.macro.MacroExecutionContext {
private final MacroExecutor.RunState run;
private ApiExecutionContext(MacroExecutor.RunState run) {
this.run = run;
}
@Override public Minecraft mc() { return Minecraft.getInstance(); }
@Override public void runOnClientThread(Runnable r) {
if (r == null) return;
Runnable safe = () -> MacroLifecycle.withRunContext(run, () -> {
try { r.run(); }
catch (Throwable t) { AutismClientMessaging.sendPrefixed("\u00a7cAddon action (client task) error: " + (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage())); }
});
try { MacroClientThread.runOnClientThread(Minecraft.getInstance(), safe); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
@Override public <T> T callOnClientThread(java.util.function.Supplier<T> s) {
if (s == null) return null;
java.util.function.Supplier<T> safe = () -> {
AtomicReference<T> value = new AtomicReference<>();
MacroLifecycle.withRunContext(run, () -> {
try { value.set(s.get()); }
catch (Throwable t) { AutismClientMessaging.sendPrefixed("\u00a7cAddon action (client task) error: " + (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage())); }
});
return value.get();
};
try { return MacroClientThread.callOnClientThread(Minecraft.getInstance(), safe, null); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; }
}
@Override public void waitTicks(int ticks) {
MacroLifecycle.withRunContext(run, () -> {
for (int i = 0; i < ticks && MacroLifecycle.isCurrentRunActive(); i++) {
MacroRunLoop.waitForCondition(MacroConditionRegistry.waitForNextTick());
}
});
}
@Override public void awaitCondition(CompletableFuture<Void> future) {
if (future != null) MacroLifecycle.withRunContext(run, () -> MacroRunLoop.waitForCondition(future));
}
@Override public void awaitCondition(autismclient.api.macro.AddonCondition condition) {
if (condition != null) MacroLifecycle.withRunContext(run, () -> MacroRunLoop.waitForCondition(MacroConditionRegistry.await(condition)));
}
@Override public boolean isActive() { return run != null && run.running.get() && !PackHideState.isHardLocked(); }
@Override public void setStatus(String status) { MacroLifecycle.withRunContext(run, () -> MacroLifecycle.setCurrentStatus(status)); }
@Override public void sendPacket(net.minecraft.network.protocol.Packet<?> packet) {
if (packet == null) return;
runOnClientThread(() -> {
Minecraft mc = Minecraft.getInstance();
if (mc.getConnection() != null) mc.getConnection().send(packet);
});
}
@Override public java.util.Optional<MacroValue> variable(String name) {
return run == null ? java.util.Optional.empty() : run.variables.get(name);
}
@Override public void setVariable(String name, MacroValue value) {
if (run != null) run.variables.set(name, value);
}
@Override public void removeVariable(String name) {
if (run != null) run.variables.remove(name);
}
@Override public java.util.Map<String, MacroValue> variables() {
return run == null ? java.util.Map.of() : run.variables.snapshot();
}
@Override public MacroTemplate.Resolution resolveTemplate(String template) {
return MacroTemplate.resolve(template, run == null ? null : run.variables, Minecraft.getInstance());
}
}
public static void runUseItemPhaseAction(Minecraft mc, UseItemPhaseAction action) throws InterruptedException {
int times = action.repeatTimes();
for (int i = 0; i < times && MacroLifecycle.isCurrentRunActive(); i++) {
MacroClientThread.runOnClientThread(mc, () -> {
try { action.sendUsePacket(mc); }
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in Use phase: " + e.getMessage()); }
});
final boolean[] gateInstalled = { false };
if (action.shouldGate()) {
MacroClientThread.runOnClientThread(mc, () -> {
try { gateInstalled[0] = action.installGate(mc); }
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in Use gate: " + e.getMessage()); }
});
}
try {
for (int t = 0; t < action.holdTicks && MacroLifecycle.isCurrentRunActive(); t++) {
CompletableFuture<Void> tf = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tf);
}
} finally {
if (gateInstalled[0]) {
final boolean wasOnThread = mc.isSameThread();
if (wasOnThread) {
action.removeGate();
} else {
MacroClientThread.runOnClientThread(mc, action::removeGate);
}
}
}
if (!MacroLifecycle.isCurrentRunActive()) break;
MacroClientThread.runOnClientThread(mc, () -> {
try { action.finishRelease(mc); }
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in Use release: " + e.getMessage()); }
});
}
}
public static void runClickActionBurst(Minecraft mc, ClickAction action) throws InterruptedException {
int times = Math.max(1, action.clickCount);
MacroClientThread.runOnClientThread(mc, () -> {
for (int c = 0; c < times && MacroLifecycle.isCurrentRunActive(); c++) {
try {
action.execute(mc);
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cError in Click: " + e.getMessage());
break;
}
}
}, action.isTickAligned());
}
public static boolean isInstantInventoryAction(MacroAction action) {
if (action instanceof ItemAction
|| action instanceof DropAction
|| action instanceof SwapSlotsAction
|| action instanceof ClickAction
|| action instanceof XCarryAction) {
return true;
}
return action instanceof StoreItemAction storeItemAction && !storeItemAction.persistent;
}
public static void runStoreItemAction(Minecraft mc, StoreItemAction action) throws InterruptedException {
if (action == null) return;
int delayTicks = StoreItemAction.clampDelayTicks(action.delayTicks);
if (delayTicks <= 0) {
MacroClientThread.runOnClientThread(mc, () -> action.doTransfer(mc));
return;
}
List<Integer> slots = MacroClientThread.callOnClientThread(mc, () -> action.collectTransferSlots(mc), List.of());
for (int i = 0; i < slots.size() && MacroLifecycle.isCurrentRunActive(); i++) {
AutismInventoryClickHelper.click(mc, slots.get(i), 0, net.minecraft.world.inventory.ContainerInput.QUICK_MOVE);
if (i + 1 < slots.size()) waitTicks(delayTicks);
}
if (action.closeAfter && !action.persistent) {
MacroClientThread.runOnClientThread(mc, () -> AutismGuiActions.closeCurrentScreen(mc, action.closeSendPkt, false));
}
}
private static void waitTicks(int ticks) {
for (int i = 0; i < ticks && MacroLifecycle.isCurrentRunActive(); i++) {
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
}
public static void runUseItemAction(Minecraft mc, UseItemAction action) throws InterruptedException {
if (action == null) return;
boolean sneaking = action.sneak;
try {
if (action.useMode == UseItemAction.UseMode.CUSTOM_HOLD) {
MacroClientThread.runOnClientThread(mc, () -> {
try {
if (sneaking) autismclient.util.AutismMacroSneak.forceSneak(mc, true);
action.execute(mc);
}
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in UseItem: " + e.getMessage()); }
}, action.isTickAligned());
if (action.holdTicks > 0) {
waitTicks(action.holdTicks);
if (!MacroLifecycle.isCurrentRunActive()) return;
MacroClientThread.runOnClientThread(mc, () -> {
try { action.sendRelease(mc); }
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in UseItem release: " + e.getMessage()); }
}, action.isTickAligned());
if (action.waitForFinish) waitForUseItemFinish(mc, action.holdTicks + 8);
}
return;
}
int times = Math.max(1, action.useCount);
for (int i = 0; i < times && MacroLifecycle.isCurrentRunActive(); i++) {
MacroClientThread.runOnClientThread(mc, () -> {
try {
if (sneaking) autismclient.util.AutismMacroSneak.forceSneak(mc, true);
action.execute(mc);
}
catch (Exception e) { AutismClientMessaging.sendPrefixed("\u00a7cError in UseItem: " + e.getMessage()); }
}, action.isTickAligned());
if (action.waitForFinish) waitForUseItemFinish(mc, 0);
if (i + 1 < times && MacroLifecycle.isCurrentRunActive()) MacroExecutor.waitOneTick();
}
} finally {
if (sneaking) mc.execute(() -> autismclient.util.AutismMacroSneak.forceSneak(mc, false));
}
}
public static void runContainerClickSequenceAction(Minecraft mc, ContainerClickSequenceAction action) throws InterruptedException {
if (action == null) return;
List<Integer> slots = action.resolvedSlots();
int repeats = Math.max(1, action.repeatCount);
int delay = Math.max(0, action.delayTicks);
int total = repeats * slots.size();
int sent = 0;
for (int repeat = 0; repeat < repeats && MacroLifecycle.isCurrentRunActive(); repeat++) {
for (int slot : slots) {
if (!MacroLifecycle.isCurrentRunActive()) return;
MacroClientThread.runOnClientThread(mc, () -> action.executeClick(mc, slot));
sent++;
if (delay > 0 && sent < total) waitTicks(delay);
}
}
}
public static void runPacketBurstAction(Minecraft mc, PacketBurstAction action) {
if (action == null || mc == null || mc.getConnection() == null) return;
if (action.flushBefore) AutismSharedState.get().flushDelayedPackets(mc.getConnection());
int total = action.effectiveCount();
int delay = Math.max(0, action.delayTicks);
for (int i = 0; i < total && MacroLifecycle.isCurrentRunActive(); i++) {
action.sendOne(mc);
if (delay > 0 && i + 1 < total) waitTicks(delay);
}
}
private static void waitForUseItemFinish(Minecraft mc, int fallbackTicks) throws InterruptedException {
if (mc == null || !MacroLifecycle.isCurrentRunActive()) return;
if (!Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc,
() -> mc.player != null && mc.player.isUsingItem(),
Boolean.FALSE))) {
return;
}
int remaining = MacroClientThread.callOnClientThread(mc,
() -> mc.player == null ? 0 : Math.max(0, mc.player.getUseItemRemainingTicks()),
0);
int maxTicks = Math.max(8, remaining > 0 ? remaining + 8 : fallbackTicks + 8);
maxTicks = Math.min(maxTicks, 72000);
for (int i = 0; i < maxTicks && MacroLifecycle.isCurrentRunActive(); i++) {
if (!Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc,
() -> mc.player != null && mc.player.isUsingItem(),
Boolean.FALSE))) {
return;
}
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
}
}

View file

@ -0,0 +1,548 @@
package autismclient.util.macro;
import autismclient.mixin.accessor.AutismMultiPlayerGameModeAccessor;
import autismclient.modules.AutismModule;
import autismclient.util.AutismAutoTool;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismInstaBreakRenderer;
import autismclient.util.AutismInventoryHelper;
import autismclient.util.AutismSharedState;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
final class MacroBreak {
private MacroBreak() {
}
public static void runInstaBreakAction(Minecraft mc, InstaBreakAction action) throws InterruptedException {
if (mc == null || mc.player == null || mc.level == null || mc.getConnection() == null || mc.gameMode == null) {
AutismClientMessaging.sendPrefixed("\u00a7cInstaBreak: missing world or connection");
return;
}
BlockPos target = action.blockPos == null ? BlockPos.ZERO : action.blockPos;
Direction direction = action.direction == null ? Direction.UP : action.direction;
int delayTicks = Math.max(0, action.delayTicks);
int targetTimes = Math.max(0, action.times);
int completed = 0;
boolean wasSolid = !isInstaBreakAir(mc, target);
if (!ensureInstaBreakPickaxe(mc, action)) return;
AutismInstaBreakRenderer.setTarget(target);
try {
long lastAttemptTick = AutismSharedState.get().getClientTickCounter() - (delayTicks + 1L);
long lastSneakTick = Long.MIN_VALUE;
while (MacroLifecycle.isCurrentRunActive() && isActionStillInCurrentMacro(action)) {
if (action.sneak) {
long sneakTick = AutismSharedState.get().getClientTickCounter();
if (sneakTick != lastSneakTick) {
lastSneakTick = sneakTick;
final String sneakMode = action.sneakMode;
MacroClientThread.runOnClientThread(mc, () -> holdMacroSneak(mc, sneakMode), action.isTickAligned());
}
}
boolean isAir = isInstaBreakAir(mc, target);
if (wasSolid && isAir) {
completed++;
MacroLifecycle.setCurrentStatus("InstaBreak " + completed + "/" + (targetTimes == 0 ? "\u221e" : targetTimes));
if (targetTimes > 0 && completed >= targetTimes) return;
wasSolid = false;
} else if (!isAir) {
wasSolid = true;
}
if (!isAir) {
long currentTick = AutismSharedState.get().getClientTickCounter();
if (currentTick != lastAttemptTick && (currentTick - lastAttemptTick) >= delayTicks) {
lastAttemptTick = currentTick;
if (!ensureInstaBreakPickaxe(mc, action)) return;
if (action.interact) {
InteractTiming t = action.interactTiming;
boolean interactFirst = t == InteractTiming.BEFORE || t == InteractTiming.WITH
|| (t == InteractTiming.CUSTOM && action.interactCustomMs < 0);
long ns = t == InteractTiming.CUSTOM
? Math.abs((long) action.interactCustomMs) * 1_000_000L : 0L;
if (interactFirst) {
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, target, direction), action.isTickAligned());
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
sendInstaBreakAttempt(mc, target, direction, action.isTickAligned());
} else if (t == InteractTiming.AFTER_PLUS) {
sendInstaBreakAttempt(mc, target, direction, action.isTickAligned());
MacroExecutor.waitOneTick();
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, target, direction), action.isTickAligned());
} else {
sendInstaBreakAttempt(mc, target, direction, action.isTickAligned());
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, target, direction), action.isTickAligned());
}
} else {
sendInstaBreakAttempt(mc, target, direction, action.isTickAligned());
}
}
}
MacroLifecycle.setCurrentStatus("InstaBreak " + target.getX() + "," + target.getY() + "," + target.getZ()
+ " " + completed + "/" + (targetTimes == 0 ? "\u221e" : targetTimes));
Thread.sleep(1);
}
} finally {
if (action.sneak) {
final String sneakMode = action.sneakMode;
try {
MacroClientThread.runOnClientThread(mc, () -> releaseMacroSneak(mc, sneakMode));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
AutismInstaBreakRenderer.clearTarget(target);
}
}
private static boolean isInstaBreakAir(Minecraft mc, BlockPos pos) {
return mc.level == null || mc.level.isOutsideBuildHeight(pos) || mc.level.getBlockState(pos).isAir();
}
private static boolean isActionStillInCurrentMacro(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || run.macro == null || run.macro.actions == null) return true;
return run.macro.actions.contains(action);
}
private static boolean ensureInstaBreakPickaxe(Minecraft mc, InstaBreakAction action) throws InterruptedException {
return MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null) return false;
if (mc.player.getMainHandItem().is(ItemTags.PICKAXES)) return true;
if (!action.autoPickaxe) return false;
int preferred = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
for (int slot = 0; slot < 9; slot++) {
if (mc.player.getInventory().getItem(slot).is(ItemTags.PICKAXES)) {
AutismInventoryHelper.selectHotbarSlot(mc, slot);
return true;
}
}
for (int slot = 9; slot < 36; slot++) {
if (mc.player.getInventory().getItem(slot).is(ItemTags.PICKAXES)) {
if (!AutismInventoryHelper.swapInventorySlots(mc, slot, preferred)) return false;
AutismInventoryHelper.selectHotbarSlot(mc, preferred);
return true;
}
}
return false;
}, false, action.isTickAligned());
}
private static void sendInstaBreakAttempt(Minecraft mc, BlockPos pos, Direction direction, boolean tickAligned) throws InterruptedException {
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null || mc.level == null || mc.gameMode == null || mc.getConnection() == null) return;
BlockPos blockPos = pos.immutable();
Direction dir = direction == null ? Direction.UP : direction;
if (mc.gameMode instanceof AutismMultiPlayerGameModeAccessor accessor) {
accessor.autism$startPrediction(mc.level, sequence -> new ServerboundPlayerActionPacket(
ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, blockPos, dir, sequence));
accessor.autism$startPrediction(mc.level, sequence -> new ServerboundPlayerActionPacket(
ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, blockPos, dir, sequence));
} else {
mc.getConnection().send(new ServerboundPlayerActionPacket(
ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, blockPos, dir));
mc.getConnection().send(new ServerboundPlayerActionPacket(
ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, blockPos, dir));
}
mc.player.swing(InteractionHand.MAIN_HAND);
}, tickAligned);
}
public static void runBreakAction(Minecraft mc, BreakAction action) throws InterruptedException {
if (mc == null || mc.player == null || mc.level == null || mc.getConnection() == null || mc.gameMode == null) {
AutismClientMessaging.sendPrefixed("\u00a7cBreak: missing world or connection");
return;
}
final BlockPos target = action.blockPos == null ? BlockPos.ZERO : action.blockPos;
final Direction direction = action.direction == null ? Direction.UP : action.direction;
int startDelay = Math.max(0, action.delayTicks);
int targetTimes = Math.max(0, action.times);
int completed = 0;
if (startDelay > 0) waitBreakClientTicks(startDelay);
final boolean[] started = { false };
final boolean useNextSteps = action.interact && action.runNextSteps;
final boolean[] nextStepsFired = { false };
final boolean[] missingNextStepsWarned = { false };
AutismInstaBreakRenderer.setTarget(target);
long lastTick = -1L;
try {
while (MacroLifecycle.isCurrentRunActive() && isActionStillInCurrentMacro(action)) {
long now = AutismSharedState.get().getClientTickCounter();
if (now == lastTick) {
Thread.sleep(1);
continue;
}
lastTick = now;
int result;
if (!started[0]) {
result = useNextSteps
? startBreakWithNextSteps(mc, action, target, direction, started, nextStepsFired, missingNextStepsWarned)
: MacroClientThread.callOnClientThread(mc, () -> startBreakOnClient(mc, action, target, direction, started), -1, action.isTickAligned());
} else if (action.interact && action.interactTiming == InteractTiming.CUSTOM) {
result = useNextSteps
? breakCustomTickWithNextSteps(mc, action, target, direction, nextStepsFired, missingNextStepsWarned)
: breakCustomTick(mc, action, target, direction);
} else {
result = useNextSteps
? breakContinueWithNextSteps(mc, action, target, direction, nextStepsFired, missingNextStepsWarned)
: MacroClientThread.callOnClientThread(mc, () -> breakContinueOnClient(mc, action, target, direction), -1, action.isTickAligned());
}
if (result < 0) {
return;
} else if (result == 1) {
boolean completedStartedBreak = started[0];
if (useNextSteps && completedStartedBreak) {
runBreakNextStepsAfterCompletion(mc, action, nextStepsFired, missingNextStepsWarned);
} else if (action.interact && action.interactTiming == InteractTiming.AFTER_PLUS) {
MacroExecutor.waitOneTick();
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, target, direction), action.isTickAligned());
}
completed++;
MacroLifecycle.setCurrentStatus("Break " + completed + "/" + (targetTimes == 0 ? "\u221e" : targetTimes));
if (targetTimes > 0 && completed >= targetTimes) return;
started[0] = false;
nextStepsFired[0] = false;
waitForBreakSolid(mc, target);
} else {
MacroLifecycle.setCurrentStatus("Break " + target.getX() + "," + target.getY() + "," + target.getZ()
+ " " + completed + "/" + (targetTimes == 0 ? "\u221e" : targetTimes));
}
}
} finally {
final boolean wasMining = started[0];
MacroClientThread.runOnClientThread(mc, () -> {
if (wasMining && mc.gameMode != null && mc.gameMode.isDestroying()) mc.gameMode.stopDestroyBlock();
if (action.sneak) releaseMacroSneak(mc, action.sneakMode);
}, action.isTickAligned());
AutismInstaBreakRenderer.clearTarget(target);
}
}
private static int startBreakWithNextSteps(Minecraft mc, BreakAction action, BlockPos pos, Direction dir,
boolean[] started, boolean[] nextStepsFired,
boolean[] missingNextStepsWarned) throws InterruptedException {
MacroExecutor.BreakStartPlan plan = MacroClientThread.callOnClientThread(mc, () -> prepareBreakStartOnClient(mc, action, pos), new MacroExecutor.BreakStartPlan(-1, false, false));
if (plan.result() != 0) return plan.result();
if (plan.instant() && plan.interactFirst()) {
runBreakNextStepsOnce(mc, action, nextStepsFired, missingNextStepsWarned);
if (!MacroLifecycle.isCurrentRunActive()) return -1;
}
int result = MacroClientThread.callOnClientThread(mc, () -> startPreparedBreakOnClient(mc, action, pos, dir, started), -1, action.isTickAligned());
return result;
}
private static MacroExecutor.BreakStartPlan prepareBreakStartOnClient(Minecraft mc, BreakAction action, BlockPos pos) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) {
return new MacroExecutor.BreakStartPlan(-1, false, false);
}
if (mc.level.isOutsideBuildHeight(pos)) return new MacroExecutor.BreakStartPlan(1, false, false);
BlockState state = mc.level.getBlockState(pos);
if (state.isAir()) return new MacroExecutor.BreakStartPlan(1, false, false);
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
if (action.autoTool) {
AutismAutoTool.equipBestTool(mc, state, action.considerInventory, true);
state = mc.level.getBlockState(pos);
if (state.isAir()) return new MacroExecutor.BreakStartPlan(1, false, false);
}
float startDelta = state.getDestroyProgress(mc.player, mc.player.level(), pos);
boolean instant = startDelta >= 1.0f;
boolean interactFirst = action.interact && (action.interactTiming == InteractTiming.BEFORE
|| action.interactTiming == InteractTiming.WITH
|| (action.interactTiming == InteractTiming.CUSTOM && action.interactCustomMs < 0));
return new MacroExecutor.BreakStartPlan(0, instant, interactFirst);
}
private static int startPreparedBreakOnClient(Minecraft mc, BreakAction action, BlockPos pos, Direction dir, boolean[] started) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) return -1;
if (mc.level.isOutsideBuildHeight(pos) || mc.level.getBlockState(pos).isAir()) return 1;
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
mc.gameMode.startDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
started[0] = true;
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}
private static int startBreakOnClient(Minecraft mc, BreakAction action, BlockPos pos, Direction dir, boolean[] started) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) return -1;
BlockState state = mc.level.getBlockState(pos);
if (state.isAir() || mc.level.isOutsideBuildHeight(pos)) return 1;
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
if (action.autoTool) {
AutismAutoTool.equipBestTool(mc, state, action.considerInventory, true);
state = mc.level.getBlockState(pos);
}
float startDelta = state.getDestroyProgress(mc.player, mc.player.level(), pos);
boolean instant = startDelta >= 1.0f;
boolean interactFirst = action.interact && (action.interactTiming == InteractTiming.BEFORE
|| action.interactTiming == InteractTiming.WITH
|| (action.interactTiming == InteractTiming.CUSTOM && action.interactCustomMs < 0));
if (instant && interactFirst) sendBlockInteract(mc, pos, dir);
mc.gameMode.startDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
started[0] = true;
if (instant && action.interact && !interactFirst && action.interactTiming != InteractTiming.AFTER_PLUS) sendBlockInteract(mc, pos, dir);
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}
private static int breakContinueWithNextSteps(Minecraft mc, BreakAction action, BlockPos pos, Direction dir,
boolean[] nextStepsFired,
boolean[] missingNextStepsWarned) throws InterruptedException {
MacroExecutor.BreakContinuePlan plan = MacroClientThread.callOnClientThread(mc, () -> inspectBreakContinueOnClient(mc, action, pos), new MacroExecutor.BreakContinuePlan(-1, false, false));
if (plan.result() != 0) return plan.result();
InteractTiming timing = action.interactTiming;
if (timing == InteractTiming.BEFORE && plan.breaksNextTick()) {
runBreakNextStepsOnce(mc, action, nextStepsFired, missingNextStepsWarned);
if (!MacroLifecycle.isCurrentRunActive()) return -1;
}
if (timing == InteractTiming.WITH && plan.breaksThisTick()) {
runBreakNextStepsOnce(mc, action, nextStepsFired, missingNextStepsWarned);
if (!MacroLifecycle.isCurrentRunActive()) return -1;
}
int result = MacroClientThread.callOnClientThread(mc, () -> continueBreakOnClient(mc, action, pos, dir), -1, action.isTickAligned());
return result;
}
private static MacroExecutor.BreakContinuePlan inspectBreakContinueOnClient(Minecraft mc, BreakAction action, BlockPos pos) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) {
return new MacroExecutor.BreakContinuePlan(-1, false, false);
}
BlockState state = mc.level.getBlockState(pos);
if (state.isAir()) return new MacroExecutor.BreakContinuePlan(1, false, false);
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
boolean breaksThisTick = willBreakThisTick(mc, pos, state);
boolean breaksNextTick = !breaksThisTick && willBreakNextTick(mc, pos, state);
return new MacroExecutor.BreakContinuePlan(0, breaksThisTick, breaksNextTick);
}
private static int continueBreakOnClient(Minecraft mc, BreakAction action, BlockPos pos, Direction dir) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) return -1;
if (mc.level.getBlockState(pos).isAir()) return 1;
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
mc.gameMode.continueDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}
private static int breakContinueOnClient(Minecraft mc, BreakAction action, BlockPos pos, Direction dir) {
if (mc.level == null || mc.gameMode == null || mc.player == null || mc.getConnection() == null) return -1;
BlockState state = mc.level.getBlockState(pos);
if (state.isAir()) return 1;
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
boolean breaksThisTick = willBreakThisTick(mc, pos, state);
boolean breaksNextTick = !breaksThisTick && willBreakNextTick(mc, pos, state);
InteractTiming t = action.interactTiming;
if (action.interact && t == InteractTiming.BEFORE && breaksNextTick) sendBlockInteract(mc, pos, dir);
if (action.interact && t == InteractTiming.WITH && breaksThisTick) sendBlockInteract(mc, pos, dir);
mc.gameMode.continueDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
if (action.interact && t == InteractTiming.AFTER && breaksThisTick) sendBlockInteract(mc, pos, dir);
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}
private static int breakCustomTickWithNextSteps(Minecraft mc, BreakAction action, BlockPos pos, Direction dir,
boolean[] nextStepsFired,
boolean[] missingNextStepsWarned) throws InterruptedException {
boolean willBreak = Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
if (mc.level == null || mc.player == null) return Boolean.FALSE;
BlockState state = mc.level.getBlockState(pos);
if (state.isAir()) return Boolean.FALSE;
return willBreakThisTick(mc, pos, state);
}, Boolean.FALSE));
if (!willBreak) {
return MacroClientThread.callOnClientThread(mc, () -> continueBreakOnClient(mc, action, pos, dir), -1, action.isTickAligned());
}
long ns = Math.abs((long) action.interactCustomMs) * 1_000_000L;
int broke;
if (action.interactCustomMs < 0) {
runBreakNextStepsOnce(mc, action, nextStepsFired, missingNextStepsWarned);
if (!MacroLifecycle.isCurrentRunActive()) return -1;
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
broke = MacroClientThread.callOnClientThread(mc, () -> breakAndDetectOnClient(mc, pos, dir), -1, action.isTickAligned());
} else {
broke = MacroClientThread.callOnClientThread(mc, () -> breakAndDetectOnClient(mc, pos, dir), -1, action.isTickAligned());
}
return broke;
}
private static int breakCustomTick(Minecraft mc, BreakAction action, BlockPos pos, Direction dir) throws InterruptedException {
boolean willBreak = Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
if (mc.level == null || mc.player == null) return Boolean.FALSE;
BlockState s = mc.level.getBlockState(pos);
if (s.isAir()) return Boolean.FALSE;
return willBreakThisTick(mc, pos, s);
}, Boolean.FALSE));
if (!willBreak) {
return MacroClientThread.callOnClientThread(mc, () -> {
if (mc.level == null || mc.gameMode == null || mc.player == null) return -1;
if (mc.level.getBlockState(pos).isAir()) return 1;
if (action.sneak) holdMacroSneak(mc, action.sneakMode);
mc.gameMode.continueDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}, -1, action.isTickAligned());
}
long ns = Math.abs((long) action.interactCustomMs) * 1_000_000L;
int broke;
if (action.interactCustomMs < 0) {
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, pos, dir), action.isTickAligned());
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
broke = MacroClientThread.callOnClientThread(mc, () -> breakAndDetectOnClient(mc, pos, dir), -1, action.isTickAligned());
} else {
broke = MacroClientThread.callOnClientThread(mc, () -> breakAndDetectOnClient(mc, pos, dir), -1, action.isTickAligned());
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
MacroClientThread.runOnClientThread(mc, () -> sendBlockInteract(mc, pos, dir), action.isTickAligned());
}
return broke;
}
private static int breakAndDetectOnClient(Minecraft mc, BlockPos pos, Direction dir) {
if (mc.level == null || mc.gameMode == null || mc.player == null) return -1;
if (mc.level.getBlockState(pos).isAir()) return 1;
mc.gameMode.continueDestroyBlock(pos, dir);
mc.player.swing(InteractionHand.MAIN_HAND);
return mc.level.getBlockState(pos).isAir() ? 1 : 0;
}
private static boolean willBreakThisTick(Minecraft mc, BlockPos pos, BlockState state) {
float progress = (mc.gameMode instanceof AutismMultiPlayerGameModeAccessor acc) ? acc.autism$getDestroyProgress() : 0f;
float delta = state.getDestroyProgress(mc.player, mc.player.level(), pos);
return progress > 0f && (progress + delta) >= 1.0f;
}
private static boolean willBreakNextTick(Minecraft mc, BlockPos pos, BlockState state) {
float progress = (mc.gameMode instanceof AutismMultiPlayerGameModeAccessor acc) ? acc.autism$getDestroyProgress() : 0f;
float delta = state.getDestroyProgress(mc.player, mc.player.level(), pos);
return progress > 0f && (progress + 2f * delta) >= 1.0f;
}
private static void runBreakNextStepsOnce(Minecraft mc, BreakAction action, boolean[] fired, boolean[] missingWarned) throws InterruptedException {
if (fired != null && fired[0]) return;
java.util.List<MacroAction> actions = action.getNextActions();
if (actions == null || actions.isEmpty()) {
if (missingWarned != null && !missingWarned[0]) {
missingWarned[0] = true;
AutismClientMessaging.sendPrefixed("\u00a7cBreak: no next steps found. Add action(s) after this Break step.");
}
return;
}
if (fired != null) fired[0] = true;
action.markNextActionsRan();
String names = actions.size() == 1 ? actions.get(0).getDisplayName() : actions.size() + " steps";
MacroLifecycle.setCurrentStatus("Break: next " + names);
AutismModule module = AutismModule.get();
boolean previousSuppressPostGui = Boolean.TRUE.equals(MacroExecutor.SUPPRESS_POST_GUI_AFTER.get());
try {
MacroExecutor.SUPPRESS_POST_GUI_AFTER.set(true);
for (MacroAction next : actions) {
if (next == null || !next.isEnabled() || !MacroLifecycle.isCurrentRunActive()) continue;
try {
MacroDispatcher.executeSingleActionWithWaits(mc, next, module);
} catch (InterruptedException e) {
throw e;
} catch (Throwable t) {
AutismClientMessaging.sendPrefixed("\u00a7cBreak next step failed (" + next.getDisplayName() + "): " + MacroRace.safeThrowableMessage(t));
}
}
} finally {
MacroExecutor.SUPPRESS_POST_GUI_AFTER.set(previousSuppressPostGui);
}
}
private static void runBreakNextStepsAfterCompletion(Minecraft mc, BreakAction action, boolean[] fired,
boolean[] missingWarned) throws InterruptedException {
if (!action.interact || !action.runNextSteps || action.interactTiming == null) return;
switch (action.interactTiming) {
case AFTER -> runBreakNextStepsOnce(mc, action, fired, missingWarned);
case AFTER_PLUS -> {
MacroExecutor.waitOneTick();
runBreakNextStepsOnce(mc, action, fired, missingWarned);
}
case CUSTOM -> {
if (action.interactCustomMs >= 0) {
long ns = Math.abs((long) action.interactCustomMs) * 1_000_000L;
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
runBreakNextStepsOnce(mc, action, fired, missingWarned);
}
}
default -> {
}
}
}
public static void sendBlockInteract(Minecraft mc, BlockPos pos, Direction dir) {
if (mc.gameMode == null || mc.player == null) return;
Vec3 hit = Vec3.atCenterOf(pos).add(dir.getStepX() * 0.5, dir.getStepY() * 0.5, dir.getStepZ() * 0.5);
BlockHitResult blockHit = new BlockHitResult(hit, dir, pos, false);
mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, blockHit);
}
private static void waitForBreakSolid(Minecraft mc, BlockPos pos) throws InterruptedException {
while (MacroLifecycle.isCurrentRunActive()) {
if (!isInstaBreakAir(mc, pos)) return;
Thread.sleep(1);
}
}
private static void waitBreakClientTicks(int ticks) throws InterruptedException {
long start = AutismSharedState.get().getClientTickCounter();
while (MacroLifecycle.isCurrentRunActive() && (AutismSharedState.get().getClientTickCounter() - start) < ticks) {
Thread.sleep(1);
}
}
private static void holdMacroSneak(Minecraft mc, String mode) {
autismclient.util.AutismMacroSneak.hold(mc, mode);
}
private static void releaseMacroSneak(Minecraft mc, String mode) {
autismclient.util.AutismMacroSneak.release(mc, mode);
if (!autismclient.util.AutismMacroSneak.isPacket(mode)) MacroLifecycle.applyAggregateSneakState(mc);
}
}

View file

@ -0,0 +1,408 @@
package autismclient.util.macro;
import autismclient.util.AutismCommandSuggestionIds;
import autismclient.util.AutismNameHarvest;
import autismclient.util.AutismNotifications;
import net.minecraft.client.Minecraft;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
final class MacroCaptureAutofill {
private MacroCaptureAutofill() {
}
private record CachedSuggestions(java.util.List<String> suggestions, Object connection) {}
private record PendingFetch(CompletableFuture<java.util.List<String>> future, Object connection) {}
private static final java.util.concurrent.ConcurrentHashMap<String, CachedSuggestions> SUGGESTION_CACHE =
new java.util.concurrent.ConcurrentHashMap<>();
private static final java.util.concurrent.ConcurrentHashMap<String, PendingFetch> PENDING_FETCHES =
new java.util.concurrent.ConcurrentHashMap<>();
private static final int SUGGESTION_CACHE_MAX = 256;
private static final long SUGGESTION_CACHE_IDLE_RESET_MS = 3000L;
private static final int TRUNCATED_REPLY_SIZE = 1000;
private static final int MIN_SWEEP_CAP = 8;
private static final int MAX_SWEEP_NAMES = 60_000;
private static final String DRILL_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789_";
static volatile long allRunsStoppedAtMs;
private static final class SuggestionUnion {
final Object connection;
final java.util.LinkedHashSet<String> names = new java.util.LinkedHashSet<>();
final java.util.ArrayDeque<String> sweepQueue = new java.util.ArrayDeque<>();
int alphabetCursor;
int cap;
SuggestionUnion(Object connection, String baseQuery) {
this.connection = connection;
sweepQueue.add(baseQuery);
}
}
private static final java.util.concurrent.ConcurrentHashMap<String, SuggestionUnion> SUGGESTION_UNIONS =
new java.util.concurrent.ConcurrentHashMap<>();
public static void expireSuggestionCacheAfterIdle() {
long stoppedAt = allRunsStoppedAtMs;
if (stoppedAt > 0 && System.currentTimeMillis() - stoppedAt > SUGGESTION_CACHE_IDLE_RESET_MS) {
SUGGESTION_CACHE.clear();
PENDING_FETCHES.clear();
SUGGESTION_UNIONS.clear();
}
}
static String buildAutofillQuery(String resolvedCommand, CaptureListSelector.Filter filter, String filterText) {
String query = resolvedCommand.startsWith("/") ? resolvedCommand : "/" + resolvedCommand;
if (!query.contains(" ")) query = query + " ";
String prefix = filter == CaptureListSelector.Filter.PREFIX && filterText != null ? filterText.trim() : "";
if (!prefix.isEmpty() && !prefix.contains(" ") && query.endsWith(" ")) {
int end = query.length();
while (end > 0 && query.charAt(end - 1) == ' ') end--;
query = query.substring(0, end) + " " + prefix;
}
return query;
}
private static java.util.List<String> capturePool(net.minecraft.client.multiplayer.ClientPacketListener connection,
CaptureValueAction action, String query, java.util.List<String> base) {
if (!action.autofillCacheList && base.size() < TRUNCATED_REPLY_SIZE) return base;
SuggestionUnion union = SUGGESTION_UNIONS.compute(query, (key, existing) ->
existing != null && existing.connection == connection ? existing : new SuggestionUnion(connection, query));
synchronized (union) {
union.names.addAll(base);
union.cap = Math.max(union.cap, base.size());
advanceSweep(union, connection, action.autofillTimeoutMs);
java.util.List<String> pool = new java.util.ArrayList<>(union.names);
boolean ordered = action.listSelection == CaptureListSelector.Selection.SEQUENTIAL
|| action.listSelection == CaptureListSelector.Selection.FIRST
|| action.listSelection == CaptureListSelector.Selection.LAST
|| action.listSelection == CaptureListSelector.Selection.POSITION;
if (ordered) pool.sort(String.CASE_INSENSITIVE_ORDER);
return pool;
}
}
private static void advanceSweep(SuggestionUnion union, net.minecraft.client.multiplayer.ClientPacketListener connection, int timeoutMs) {
if (union.names.size() >= MAX_SWEEP_NAMES) { union.sweepQueue.clear(); return; }
while (!union.sweepQueue.isEmpty()) {
if (union.alphabetCursor >= DRILL_ALPHABET.length()) {
union.sweepQueue.poll();
union.alphabetCursor = 0;
continue;
}
String target = union.sweepQueue.peek() + DRILL_ALPHABET.charAt(union.alphabetCursor);
CachedSuggestions cached = SUGGESTION_CACHE.get(target);
if (cached == null || cached.connection() != connection) {
fireSuggestionFetch(connection, target, timeoutMs);
return;
}
union.names.addAll(cached.suggestions());
int size = cached.suggestions().size();
boolean truncated = size >= union.cap && union.cap >= MIN_SWEEP_CAP;
union.cap = Math.max(union.cap, size);
if (truncated) union.sweepQueue.add(target);
union.alphabetCursor++;
}
}
private static void applyCaptureSuggestions(MacroExecutor.RunState run, CaptureValueAction action,
java.util.List<String> suggestions, String query) {
CaptureValueAction.Preview preview = action.previewSuggestions(suggestions, query);
if (!preview.success()) return;
if (run != null) run.variables.setAll(preview.values());
else MacroVariables.setAll(preview.values());
}
private static PendingFetch fireSuggestionFetch(net.minecraft.client.multiplayer.ClientPacketListener connection,
String query, int timeoutMs) {
PendingFetch existing = PENDING_FETCHES.get(query);
if (existing != null && existing.connection() == connection && !existing.future().isDone()) return existing;
if (PENDING_FETCHES.size() > SUGGESTION_CACHE_MAX) return existing;
int requestId = autismclient.util.AutismCommandSuggestionIds.nextMacroId();
CompletableFuture<java.util.List<String>> future = new CompletableFuture<>();
MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> {
if (!(packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket reply)
|| reply.id() != requestId) {
return false;
}
java.util.List<String> texts = new java.util.ArrayList<>();
if (reply.suggestions() != null) {
for (var entry : reply.suggestions()) {
if (entry != null && entry.text() != null && !entry.text().isBlank()) texts.add(entry.text());
}
}
future.complete(texts);
return true;
});
PendingFetch pending = new PendingFetch(future, connection);
PENDING_FETCHES.put(query, pending);
future.completeOnTimeout(null, timeoutMs > 0 ? Math.max(1000, timeoutMs) : 30_000,
java.util.concurrent.TimeUnit.MILLISECONDS);
future.whenComplete((texts, error) -> {
listener.cancel();
PENDING_FETCHES.remove(query, pending);
if (texts != null) {
if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear();
SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(texts), connection));
}
});
try {
connection.send(new net.minecraft.network.protocol.game.ServerboundCommandSuggestionPacket(requestId, query));
} catch (Throwable error) {
future.complete(null);
}
return pending;
}
private static void runCaptureCommandAutofill(Minecraft mc, CaptureValueAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroPrearm.clearCaptureOutputs(action);
MacroTemplate.Resolution resolved = MacroVariables.resolve(action.autofillCommand, mc);
if (!resolved.success() || resolved.value().isBlank()) return;
String query = buildAutofillQuery(resolved.value(), action.listFilter, action.listFilterText);
var connection = mc.getConnection();
if (connection == null) return;
if (action.autofillCacheList) {
CachedSuggestions listCache = SUGGESTION_CACHE.get(query);
if (listCache != null && listCache.connection() == connection && !listCache.suggestions().isEmpty()) {
MacroLifecycle.setCurrentStatus("Using cached suggestions");
applyCaptureSuggestions(run, action, listCache.suggestions(), query);
return;
}
MacroLifecycle.setCurrentStatus("Sweeping command suggestions");
java.util.List<String> all = sweepAutofillCommand(connection, query);
if (all.isEmpty()) return;
if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear();
SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(all), connection));
applyCaptureSuggestions(run, action, all, query);
return;
}
CachedSuggestions cached = SUGGESTION_CACHE.get(query);
if (cached != null && (cached.connection() != connection || cached.suggestions().isEmpty())) cached = null;
if (cached != null) {
MacroLifecycle.setCurrentStatus("Using cached suggestions");
applyCaptureSuggestions(run, action, capturePool(connection, action, query, cached.suggestions()), query);
fireSuggestionFetch(connection, query, action.autofillTimeoutMs);
return;
}
MacroLifecycle.setCurrentStatus("Requesting command suggestions");
PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs);
if (pending == null) return;
if (!MacroRace.waitForFutureDone(pending.future(), 0)) return;
java.util.List<String> suggestions = pending.future().getNow(null);
if (suggestions == null || suggestions.isEmpty()) return;
applyCaptureSuggestions(run, action, capturePool(connection, action, query, suggestions), query);
}
private static java.util.List<String> sweepAutofillCommand(net.minecraft.client.multiplayer.ClientPacketListener connection, String query) {
autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() {
@Override public boolean cancelled() {
Minecraft mc = Minecraft.getInstance();
return !MacroLifecycle.isCurrentRunActive() || mc == null || mc.getConnection() != connection;
}
@Override public boolean paused() { return false; }
@Override public int limit() { return MAX_SWEEP_NAMES; }
@Override public int maxQueries() { return 50_000; }
@Override public boolean usernamesOnly() { return false; }
};
java.util.List<String> all = autismclient.util.AutismNameHarvest.sweepCommand(connection, query, control);
all.sort(String.CASE_INSENSITIVE_ORDER);
return all;
}
private static void runCaptureNamescrape(Minecraft mc, CaptureValueAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroPrearm.clearCaptureOutputs(action);
var connection = mc.getConnection();
if (connection == null) return;
String cacheKey = "namescrape";
CachedSuggestions cached = SUGGESTION_CACHE.get(cacheKey);
if (cached != null && cached.connection() == connection && !cached.suggestions().isEmpty()) {
MacroLifecycle.setCurrentStatus("Using cached names");
applyCaptureSuggestions(run, action, cached.suggestions(), "");
return;
}
MacroLifecycle.setCurrentStatus("Scraping player names");
java.util.List<String> names = scrapeAllNames(mc, connection);
if (names.isEmpty()) return;
if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear();
SUGGESTION_CACHE.put(cacheKey, new CachedSuggestions(java.util.List.copyOf(names), connection));
applyCaptureSuggestions(run, action, names, "");
}
private static java.util.List<String> scrapeAllNames(Minecraft mc, net.minecraft.client.multiplayer.ClientPacketListener connection) {
String self = mc.player == null ? "" : mc.player.getName().getString();
java.util.LinkedHashMap<String, String> names = autismclient.util.AutismNameHarvest.instantNames(mc, self);
java.util.List<autismclient.util.AutismNameHarvest.Vector> vectors =
autismclient.util.AutismNameHarvest.discoverVectors(connection);
autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() {
@Override public boolean cancelled() {
Minecraft m = Minecraft.getInstance();
return !MacroLifecycle.isCurrentRunActive() || m == null || m.getConnection() != connection;
}
@Override public boolean paused() { return false; }
@Override public int limit() { return MAX_SWEEP_NAMES; }
@Override public int maxQueries() { return 100_000; }
};
autismclient.util.AutismNameHarvest.sweep(connection, names, self, vectors, control);
java.util.List<String> out = new java.util.ArrayList<>(names.values());
out.sort(String.CASE_INSENSITIVE_ORDER);
return out;
}
public static void runCaptureValueAction(Minecraft mc, CaptureValueAction action) {
if (action == null) return;
String modifierError = action.numberModifierError();
if (!modifierError.isBlank()) {
MacroLifecycle.setCurrentStatus("Capture failed: " + modifierError);
autismclient.util.AutismNotifications.error("Capture Value: " + modifierError);
return;
}
if (action.source == CaptureValueAction.Source.COMMAND_AUTOFILL) {
MacroLifecycle.setCurrentStatus("Requesting command suggestions");
runCaptureCommandAutofill(mc, action);
reportMissingCapture(action);
return;
}
if (action.source == CaptureValueAction.Source.NAMESCRAPE) {
MacroLifecycle.setCurrentStatus("Scraping player names");
runCaptureNamescrape(mc, action);
reportMissingCapture(action);
return;
}
if (action.waitForTrigger) {
CaptureValueAction.Source source = action.source == null ? CaptureValueAction.Source.GUI_TITLE : action.source;
MacroLifecycle.setCurrentStatus("Waiting for capture: " + source.name().toLowerCase(Locale.ROOT).replace('_', ' '));
if (source == CaptureValueAction.Source.RECENT_CHAT) MacroPacketWait.awaitCaptureValueChat(action);
else if (source == CaptureValueAction.Source.SCOREBOARD) awaitCaptureValueScoreboard(mc, action);
else awaitCaptureValueState(mc, action);
return;
}
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
java.util.concurrent.atomic.AtomicReference<CaptureValueAction.Preview> result =
new java.util.concurrent.atomic.AtomicReference<>(CaptureValueAction.Preview.unavailable("Not available"));
long deadline = System.nanoTime() + MacroExecutor.CAPTURE_SETTLE_MS * 1_000_000L;
while (MacroLifecycle.isCurrentRunActive()) {
CompletableFuture<Void> done = new CompletableFuture<>();
mc.execute(() -> MacroLifecycle.withRunContext(run, () -> {
try {
result.set(action.run(mc));
done.complete(null);
} catch (Throwable error) {
done.completeExceptionally(error);
}
}));
MacroRace.waitForFutureDone(done, 0);
if (result.get().success() || System.nanoTime() >= deadline) break;
MacroExecutor.waitOneTick();
}
CaptureValueAction.Preview preview = result.get();
if (!preview.success()) {
MacroLifecycle.setCurrentStatus("Capture failed: " + preview.message());
autismclient.util.AutismNotifications.error(
"Capture " + captureOutputLabel(action) + ": " + preview.message());
}
}
private static void reportMissingCapture(CaptureValueAction action) {
String name = MacroVariableContext.cleanRootName(action.saveAs);
if (name.isBlank() || MacroVariables.value(name).isPresent()) return;
MacroLifecycle.setCurrentStatus("Capture failed: nothing captured");
autismclient.util.AutismNotifications.error(
"Capture " + captureOutputLabel(action) + ": nothing captured");
}
private static String captureOutputLabel(CaptureValueAction action) {
String name = MacroVariableContext.cleanRootName(action.saveAs);
return name.isBlank() ? "value" : "{" + name + "}";
}
private static void awaitCaptureValueState(Minecraft mc, CaptureValueAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroPrearm.clearCaptureOutputs(action);
String lastMessage = "";
while (MacroLifecycle.isCurrentRunActive()) {
CaptureValueAction.Preview preview;
try {
preview = MacroClientThread.callOnClientThread(mc, () -> action.previewCurrent(mc),
CaptureValueAction.Preview.unavailable("Not available"));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
if (preview.success()) {
if (run != null) run.variables.setAll(preview.values());
else MacroVariables.setAll(preview.values());
return;
}
if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) {
lastMessage = preview.message();
MacroLifecycle.setCurrentStatus("Capture waiting: " + lastMessage);
}
MacroExecutor.waitOneTick();
}
}
private static void awaitCaptureValueScoreboard(Minecraft mc, CaptureValueAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroPrearm.clearCaptureOutputs(action);
String trackedKey = action.scoreboardRow;
int trackedRow = action.scoreboardRowIndex;
String trackedObjective = action.scoreboardObjective;
String previousText = null;
String lastMessage = "";
boolean baselineReady = false;
while (MacroLifecycle.isCurrentRunActive()) {
CaptureValueAction.ScoreboardLine line;
try {
String keySnapshot = trackedKey;
int rowSnapshot = trackedRow;
String objectiveSnapshot = trackedObjective;
line = MacroClientThread.callOnClientThread(mc,
() -> action.resolveScoreboardLine(mc, keySnapshot, rowSnapshot, objectiveSnapshot), null);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return;
}
if (line != null) {
String currentText = line.text() == null ? "" : line.text();
if (!baselineReady) {
trackedKey = line.key();
trackedRow = line.row();
trackedObjective = line.objective();
previousText = currentText;
baselineReady = true;
} else if (!currentText.equals(previousText)) {
CaptureValueAction.Preview preview = action.previewScoreboardLine(line);
if (preview.success()) {
if (run != null) run.variables.setAll(preview.values());
else MacroVariables.setAll(preview.values());
return;
}
if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) {
lastMessage = preview.message();
MacroLifecycle.setCurrentStatus("Capture waiting: " + lastMessage);
}
previousText = currentText;
}
}
MacroExecutor.waitOneTick();
}
}
}

View file

@ -0,0 +1,230 @@
package autismclient.util.macro;
import autismclient.modules.PackHideState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Locale;
final class MacroChatObserve {
private MacroChatObserve() {
}
public static void observeIncomingChat(Packet<?> packet) {
if (PackHideState.isHardLocked()) return;
MacroExecutor.ChatCapture capture = extractIncomingChat(packet);
if (capture == null || capture.message() == null || capture.message().isBlank()) return;
syncRecentChatServerContext();
recordRecentChat(capture);
}
private static void recordRecentChat(MacroExecutor.ChatCapture capture) {
MacroExecutor.RecentChatMessage message = new MacroExecutor.RecentChatMessage(
capture.sender() == null ? "" : capture.sender(),
capture.message(),
capture.displayText(),
capture.displayComponent(),
capture.source(),
System.currentTimeMillis()
);
synchronized (MacroExecutor.recentChatLock) {
MacroExecutor.recentChatMessages.removeIf(existing -> existing.displayText().equals(message.displayText()));
MacroExecutor.recentChatMessages.addFirst(message);
while (MacroExecutor.recentChatMessages.size() > MacroExecutor.MAX_RECENT_CHAT_MESSAGES) {
MacroExecutor.recentChatMessages.removeLast();
}
}
}
public static void syncRecentChatServerContext() {
String currentServerKey = getCurrentMultiplayerServerKey();
if (currentServerKey.isEmpty()) return;
if (currentServerKey.equals(MacroExecutor.recentChatServerKeyMirror)) return;
synchronized (MacroExecutor.recentChatLock) {
if (MacroExecutor.recentChatServerKey.isEmpty()) {
MacroExecutor.recentChatServerKey = currentServerKey;
MacroExecutor.recentChatServerKeyMirror = currentServerKey;
return;
}
if (!MacroExecutor.recentChatServerKey.equals(currentServerKey)) {
MacroExecutor.recentChatMessages.clear();
MacroExecutor.recentChatServerKey = currentServerKey;
MacroExecutor.recentChatServerKeyMirror = currentServerKey;
synchronized (MacroExecutor.recentGuiLock) {
MacroExecutor.recentGuiTitles.clear();
}
} else {
MacroExecutor.recentChatServerKeyMirror = currentServerKey;
}
}
}
private static String getCurrentMultiplayerServerKey() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.isLocalServer()) return "";
ServerData entry = mc.getCurrentServer();
if (entry != null && entry.ip != null && !entry.ip.isBlank()) {
return normalizeServerKey(entry.ip);
}
if (mc.getConnection() != null && mc.getConnection().getConnection() != null) {
SocketAddress address = mc.getConnection().getConnection().getRemoteAddress();
if (address instanceof InetSocketAddress inet) {
String host = inet.getHostString();
if ((host == null || host.isBlank()) && inet.getAddress() != null) {
host = inet.getAddress().getHostAddress();
}
if (host != null && !host.isBlank()) {
return normalizeServerKey(host + ":" + inet.getPort());
}
} else if (address != null) {
String raw = address.toString();
if (raw != null && !raw.isBlank()) {
return normalizeServerKey(raw);
}
}
}
return "";
}
private static String normalizeServerKey(String address) {
if (address == null) return "";
String stripped = address.startsWith("/") ? address.substring(1) : address;
return stripped.trim().toLowerCase(Locale.ROOT);
}
public static boolean matchesWaitingChat(MacroExecutor.ChatCapture capture) {
String patternJson = MacroExecutor.waitingChatPatternJson.get();
if (!MacroExecutor.waitingChatIsRegex.get() && patternJson != null && !patternJson.isBlank() && capture.displayComponent() != null) {
String captureJson = MacroChatText.serializeTextComponent(capture.displayComponent());
if (patternJson.equals(captureJson)) return true;
}
String pattern = MacroChatText.normalizeManualText(MacroExecutor.waitingChatPattern.get());
if (pattern == null || pattern.isBlank()) return true;
if (MacroExecutor.waitingChatIsRegex.get()) {
try {
java.util.regex.Pattern compiled = MacroCapturePattern.compiled(pattern);
return compiled.matcher(capture.message()).matches() || compiled.matcher(capture.displayText()).matches();
} catch (Exception ignored) {
return false;
}
}
int threshold = MacroExecutor.waitingChatFuzzyPercent.get();
return MacroChatText.fuzzyChatMatch(pattern, capture.message(), threshold)
|| MacroChatText.fuzzyChatMatch(pattern, capture.displayText(), threshold);
}
private static boolean matchesChatAction(WaitForChatAction action, MacroExecutor.ChatCapture capture) {
return captureChatAction(action, capture, MacroLifecycle.currentVariableContext()).isPresent();
}
private static java.util.Optional<java.util.Map<String, MacroValue>> captureChatAction(WaitForChatAction action, MacroExecutor.ChatCapture capture) {
return captureChatAction(action, capture, MacroLifecycle.currentVariableContext());
}
public static java.util.Optional<java.util.Map<String, MacroValue>> captureChatAction(WaitForChatAction action, MacroExecutor.ChatCapture capture,
MacroVariableContext context) {
if (action == null || capture == null) return java.util.Optional.empty();
MacroCapturePattern.Mode mode = action.effectiveMatchMode();
if (mode == MacroCapturePattern.Mode.MATCH && action.patternJson != null && !action.patternJson.isBlank() && capture.displayComponent() != null) {
String captureJson = MacroChatText.serializeTextComponent(capture.displayComponent());
if (action.patternJson.equals(captureJson)) return java.util.Optional.of(chatValues(action, capture, java.util.Map.of()));
}
String pattern = MacroChatText.normalizeManualText(action.pattern);
if (mode == MacroCapturePattern.Mode.MATCH) {
MacroTemplate.Resolution patternResolution = MacroTemplate.resolve(pattern, context, Minecraft.getInstance());
if (!patternResolution.success()) return java.util.Optional.empty();
pattern = patternResolution.value();
}
if (pattern == null || pattern.isBlank()) return java.util.Optional.of(chatValues(action, capture, java.util.Map.of()));
if (mode != MacroCapturePattern.Mode.MATCH) {
var messageResult = MacroCapturePattern.match(mode, pattern, capture.message());
var result = messageResult.isPresent() ? messageResult : MacroCapturePattern.match(mode, pattern, capture.displayText());
return result.map(value -> chatValues(action, capture, value.values()));
}
int threshold = WaitForChatAction.clampFuzzyPercent(action.fuzzyPercent);
boolean matched = MacroChatText.fuzzyChatMatch(pattern, capture.message(), threshold)
|| MacroChatText.fuzzyChatMatch(pattern, capture.displayText(), threshold);
return matched ? java.util.Optional.of(chatValues(action, capture, java.util.Map.of())) : java.util.Optional.empty();
}
private static java.util.Map<String, MacroValue> chatValues(WaitForChatAction action, MacroExecutor.ChatCapture capture,
java.util.Map<String, MacroValue> captures) {
java.util.Map<String, MacroValue> values = new java.util.LinkedHashMap<>(captures);
if (action.saveAs != null && !action.saveAs.isBlank()) {
java.util.Map<String, MacroValue> properties = new java.util.LinkedHashMap<>();
properties.put("sender", MacroValue.text(capture.sender()));
properties.put("message", MacroValue.text(capture.message()));
properties.put("display", MacroValue.text(capture.displayText()));
properties.put("source", MacroValue.text(capture.source().name()));
values.put(action.saveAs, MacroValue.structured(MacroValue.Kind.TEXT, capture.displayText(), properties));
}
return java.util.Map.copyOf(values);
}
public static MacroExecutor.ChatCapture extractIncomingChat(Packet<?> packet) {
if (packet instanceof net.minecraft.network.protocol.game.ClientboundSystemChatPacket systemChat) {
Component content = systemChat.content();
if (content == null) return null;
String raw = content.getString();
if (raw == null || raw.isBlank()) return null;
return buildChatCapture("", raw, raw, content, MacroExecutor.ChatSource.PLAYER);
}
if (packet instanceof net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket disguisedChat) {
Component content = disguisedChat.message();
String text = content == null ? null : content.getString();
if (text == null || text.isBlank()) return null;
Component decorated = disguisedChat.chatType().decorate(content);
String sender = disguisedChat.chatType().name().getString();
return buildChatCapture(sender, text, null, decorated, MacroExecutor.ChatSource.PLAYER);
}
if (packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket playerChat) {
Component content = playerChat.unsignedContent() != null
? playerChat.unsignedContent()
: Component.literal(playerChat.body().content());
String message = content.getString();
if (message == null || message.isBlank()) message = playerChat.body().content();
if (message == null || message.isBlank()) return null;
Component decorated = playerChat.chatType().decorate(content);
String sender = playerChat.chatType().name().getString();
if (sender == null || sender.isBlank()) {
String byUuid = MacroChatText.resolvePlayerName(playerChat.sender());
if (byUuid != null && !byUuid.isBlank()) sender = byUuid;
}
return buildChatCapture(sender, message, null, decorated, MacroExecutor.ChatSource.PLAYER);
}
return null;
}
private static MacroExecutor.ChatCapture buildChatCapture(String sender, String message, String displayText, Component displayComponent, MacroExecutor.ChatSource source) {
String cleanMessage = MacroChatText.sanitizeChatText(message);
if (cleanMessage.isBlank()) return null;
String cleanSender = MacroChatText.sanitizeChatText(sender);
String cleanDisplay = MacroChatText.sanitizeChatText(displayText);
Component rendered = MacroChatText.literalizeTextComponent(displayComponent);
String display = cleanDisplay;
if (rendered == null || rendered.getString().isBlank()) {
rendered = cleanSender.isBlank()
? Component.literal(cleanMessage)
: Component.literal("<" + cleanSender + "> ").append(Component.literal(cleanMessage));
}
if (display == null || display.isBlank()) {
display = MacroChatText.sanitizeChatText(rendered.getString());
}
MacroExecutor.ChatSource resolvedSource = source != null ? source : MacroExecutor.ChatSource.PLAYER;
return new MacroExecutor.ChatCapture(cleanSender, cleanMessage, MacroChatText.sanitizeChatText(display), rendered, resolvedSource);
}
}

View file

@ -0,0 +1,521 @@
package autismclient.util.macro;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.text.Normalizer;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
final class MacroChatText {
private MacroChatText() {
}
private static final Gson GSON = new Gson();
public static String sanitizeChatText(String text) {
if (text == null) return "";
String stripped = stripLegacyFormatting(text).replace('\n', ' ').replace('\r', ' ');
StringBuilder out = new StringBuilder(stripped.length());
for (int offset = 0; offset < stripped.length();) {
int cp = stripped.codePointAt(offset);
offset += Character.charCount(cp);
int type = Character.getType(cp);
if (type == Character.FORMAT
|| type == Character.CONTROL
|| cp == 0x200B
|| cp == 0x200C
|| cp == 0x200D
|| cp == 0x2060
|| cp == 0xFE0E
|| cp == 0xFE0F) {
continue;
}
out.appendCodePoint(cp);
}
return out.toString().trim();
}
public static String normalizeManualText(String text) {
if (text == null || text.isBlank()) return "";
String stripped = stripLegacyFormatting(text).trim();
String flattened = flattenManualTextComponentJson(stripped);
return sanitizeChatText(flattened != null ? flattened : stripped);
}
private static String flattenManualTextComponentJson(String text) {
if (text == null || text.isBlank()) return null;
String trimmed = text.trim();
if (!(trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\""))) return null;
try {
com.google.gson.JsonElement element = JsonParser.parseString(trimmed);
if (element == null || element.isJsonNull()) return "";
if (element.isJsonArray()) {
StringBuilder out = new StringBuilder();
for (com.google.gson.JsonElement child : element.getAsJsonArray()) {
Component component = parseTextComponentElement(child);
if (component != null) out.append(component.getString());
}
return out.toString();
}
Component component = parseTextComponentElement(element);
return component == null ? null : component.getString();
} catch (RuntimeException ignored) {
return null;
}
}
private static Component parseTextComponentElement(com.google.gson.JsonElement element) {
try {
Component component = ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow();
return literalizeTextComponent(component);
} catch (RuntimeException ignored) {
return null;
}
}
private static String stripLegacyFormatting(String text) {
if (text == null || text.isEmpty()) return "";
StringBuilder out = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if ((ch == '\u00A7' || ch == '&') && i + 1 < text.length()) {
char next = Character.toLowerCase(text.charAt(i + 1));
if ((next >= '0' && next <= '9')
|| (next >= 'a' && next <= 'f')
|| (next >= 'k' && next <= 'o')
|| next == 'r'
|| next == 'x') {
i++;
continue;
}
}
out.append(ch);
}
return out.toString();
}
private static boolean isLikelyPlayerSender(String sender) {
String cleanSender = sanitizeChatText(sender);
if (cleanSender.isBlank()) return false;
try {
Minecraft mc = Minecraft.getInstance();
if (mc.getConnection() != null) {
for (var entry : mc.getConnection().getListedOnlinePlayers()) {
String playerName = entry != null && entry.getProfile() != null ? extractTextValue(entry.getProfile()) : null;
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
return true;
}
}
}
} catch (Throwable ignored) { }
try {
Minecraft mc = Minecraft.getInstance();
if (mc.level != null) {
for (var player : mc.level.players()) {
String playerName = player != null && player.getGameProfile() != null ? extractTextValue(player.getGameProfile()) : null;
if (playerName != null && cleanSender.equalsIgnoreCase(playerName)) {
return true;
}
}
}
} catch (Throwable ignored) { }
return false;
}
public static boolean fuzzyChatMatch(String pattern, String candidate, int thresholdPercent) {
String normalizedPattern = normalizeChatText(pattern);
String normalizedCandidate = normalizeChatText(candidate);
if (normalizedPattern.isBlank()) return true;
if (normalizedCandidate.isBlank()) return false;
if (normalizedCandidate.contains(normalizedPattern)) return true;
List<String> patternTokens = tokenizeChat(normalizedPattern);
List<String> candidateTokens = tokenizeChat(normalizedCandidate);
if (patternTokens.isEmpty()) return true;
if (thresholdPercent >= 100) {
return allPatternTokensPresent(patternTokens, normalizedCandidate);
}
double tokenScore = computeTokenCoverage(patternTokens, candidateTokens);
double charScore = similarityRatio(normalizedPattern.replace(" ", ""), normalizedCandidate.replace(" ", ""));
double score = Math.max(tokenScore, tokenScore * 0.7 + charScore * 0.3);
return score + 1.0e-9 >= (Math.max(40, Math.min(100, thresholdPercent)) / 100.0);
}
private static String normalizeChatForCompare(String text) {
if (text == null) return "";
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKC).toLowerCase(Locale.ROOT);
StringBuilder out = new StringBuilder(normalized.length());
for (int i = 0; i < normalized.length(); i++) {
char ch = normalized.charAt(i);
if (Character.isLetterOrDigit(ch)) out.append(ch);
else if (Character.isWhitespace(ch)) out.append(' ');
else if (ch == '\'' || ch == '"' || ch == '`' || ch == '-' || ch == '_' || ch == '\u2019') {
} else {
out.append(' ');
}
}
return out.toString().trim().replaceAll("\\s+", " ");
}
public static String normalizeChatText(String text) {
if (text == null) return "";
String normalized = Normalizer.normalize(text, Normalizer.Form.NFKD).toLowerCase(Locale.ROOT);
StringBuilder out = new StringBuilder(normalized.length());
boolean lastWasSpace = true;
for (int offset = 0; offset < normalized.length();) {
int cp = normalized.codePointAt(offset);
offset += Character.charCount(cp);
cp = foldStyledLatinCodePoint(cp);
int type = Character.getType(cp);
if (type == Character.NON_SPACING_MARK
|| type == Character.COMBINING_SPACING_MARK
|| type == Character.ENCLOSING_MARK
|| type == Character.FORMAT
|| type == Character.CONTROL
|| type == Character.PRIVATE_USE
|| type == Character.SURROGATE) {
continue;
}
if (Character.isLetterOrDigit(cp)) {
out.appendCodePoint(cp);
lastWasSpace = false;
} else if (Character.isWhitespace(cp)) {
if (!lastWasSpace) out.append(' ');
lastWasSpace = true;
} else if (isJoinerPunctuation(cp)) {
} else {
if (!lastWasSpace) out.append(' ');
lastWasSpace = true;
}
}
int len = out.length();
if (len > 0 && out.charAt(len - 1) == ' ') out.setLength(len - 1);
return out.toString();
}
private static boolean isJoinerPunctuation(int cp) {
return switch (cp) {
case '\'', '"', '`', '-', '_', '\u2019', '\u2018', '\u201B', '\u2032', '\u02BC', '\uFF07',
'\u2010', '\u2011', '\u2012', '\u2013', '\u2014', '\u2015', '\u2212' -> true;
default -> false;
};
}
private static int foldStyledLatinCodePoint(int cp) {
return switch (cp) {
case '\u1D00' -> 'a';
case '\u0299' -> 'b';
case '\u1D04' -> 'c';
case '\u1D05' -> 'd';
case '\u1D07' -> 'e';
case '\uA730' -> 'f';
case '\u0262' -> 'g';
case '\u029C' -> 'h';
case '\u026A' -> 'i';
case '\u1D0A' -> 'j';
case '\u1D0B' -> 'k';
case '\u029F' -> 'l';
case '\u1D0D' -> 'm';
case '\u0274' -> 'n';
case '\u1D0F' -> 'o';
case '\u1D18' -> 'p';
case '\u01EB' -> 'q';
case '\u0280' -> 'r';
case '\uA731' -> 's';
case '\u1D1B' -> 't';
case '\u1D1C' -> 'u';
case '\u1D20' -> 'v';
case '\u1D21' -> 'w';
case '\u02E3' -> 'x';
case '\u028F' -> 'y';
case '\u1D22' -> 'z';
case '\u00DF' -> 's';
case '\u00E6' -> 'a';
case '\u0153' -> 'o';
default -> cp;
};
}
private static List<String> tokenizeChat(String normalized) {
if (normalized == null || normalized.isBlank()) return List.of();
return List.of(normalized.split(" "));
}
private static boolean allPatternTokensPresent(List<String> patternTokens, String normalizedCandidate) {
for (String token : patternTokens) {
if (!normalizedCandidate.contains(token)) return false;
}
return true;
}
private static double computeTokenCoverage(List<String> patternTokens, List<String> candidateTokens) {
if (patternTokens.isEmpty()) return 1.0;
if (candidateTokens.isEmpty()) return 0.0;
double total = 0.0;
for (String patternToken : patternTokens) {
double best = 0.0;
for (String candidateToken : candidateTokens) {
best = Math.max(best, tokenSimilarity(patternToken, candidateToken));
if (best >= 1.0) break;
}
total += best;
}
return total / patternTokens.size();
}
private static double tokenSimilarity(String left, String right) {
if (left.equals(right)) return 1.0;
if (left.isBlank() || right.isBlank()) return 0.0;
if (left.contains(right) || right.contains(left)) {
return (double) Math.min(left.length(), right.length()) / Math.max(left.length(), right.length());
}
return similarityRatio(left, right);
}
private static double similarityRatio(String left, String right) {
if (left.equals(right)) return 1.0;
int maxLen = Math.max(left.length(), right.length());
if (maxLen == 0) return 1.0;
int distance = levenshteinDistance(left, right);
return Math.max(0.0, 1.0 - (distance / (double) maxLen));
}
private static int levenshteinDistance(String left, String right) {
int[] previous = new int[right.length() + 1];
int[] current = new int[right.length() + 1];
for (int j = 0; j <= right.length(); j++) previous[j] = j;
for (int i = 1; i <= left.length(); i++) {
current[0] = i;
for (int j = 1; j <= right.length(); j++) {
int cost = left.charAt(i - 1) == right.charAt(j - 1) ? 0 : 1;
current[j] = Math.min(Math.min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost);
}
int[] temp = previous;
previous = current;
current = temp;
}
return previous[right.length()];
}
private static String extractChatName(Object value) {
Object direct = firstNonNull(
invokeFirstNoArg(value, "name", "getName", "targetName", "getTargetName"),
getRecordComponentValue(value, 0),
findRecordComponentByMethodNames(value, 2, "name", "targetName", "type")
);
return sanitizeChatText(extractTextValue(direct));
}
public static String resolvePlayerName(UUID uuid) {
if (uuid == null) return "";
try {
if (Minecraft.getInstance().getConnection() != null) {
var entry = Minecraft.getInstance().getConnection().getPlayerInfo(uuid);
if (entry != null && entry.getProfile() != null) {
String profileName = extractTextValue(entry.getProfile());
if (profileName != null && !profileName.isBlank()) return profileName;
}
}
} catch (Throwable ignored) { }
try {
if (Minecraft.getInstance().level != null) {
for (var player : Minecraft.getInstance().level.players()) {
if (uuid.equals(player.getUUID()) && player.getGameProfile() != null) {
String profileName = extractTextValue(player.getGameProfile());
if (profileName != null && !profileName.isBlank()) return profileName;
}
}
}
} catch (Throwable ignored) { }
return "";
}
private static Object firstNonNull(Object... values) {
if (values == null) return null;
for (Object value : values) {
if (value != null) return value;
}
return null;
}
private static Object invokeFirstNoArg(Object target, String... names) {
if (target == null || names == null) return null;
Class<?> type = target.getClass();
for (String name : names) {
try {
Method method = type.getMethod(name);
method.setAccessible(true);
return method.invoke(target);
} catch (Throwable ignored) { }
}
return null;
}
private static Object getRecordComponentValue(Object target, int index) {
if (target == null) return null;
try {
RecordComponent[] components = target.getClass().getRecordComponents();
if (components == null || index < 0 || index >= components.length) return null;
Method accessor = components[index].getAccessor();
accessor.setAccessible(true);
return accessor.invoke(target);
} catch (Throwable ignored) {
return null;
}
}
private static Object findRecordComponentByValueType(Object target, Class<?> type) {
if (target == null || type == null) return null;
try {
RecordComponent[] components = target.getClass().getRecordComponents();
if (components == null) return null;
for (RecordComponent component : components) {
if (type.isAssignableFrom(component.getType())) {
Method accessor = component.getAccessor();
accessor.setAccessible(true);
return accessor.invoke(target);
}
}
} catch (Throwable ignored) { }
return null;
}
private static Object findRecordComponentByMethodNames(Object target, int fallbackIndex, String... names) {
Object direct = invokeFirstNoArg(target, names);
return direct != null ? direct : getRecordComponentValue(target, fallbackIndex);
}
private static Object findRecordTextComponent(Object target, Object... excludedValues) {
if (target == null) return null;
try {
RecordComponent[] components = target.getClass().getRecordComponents();
if (components == null) return null;
outer:
for (RecordComponent component : components) {
Method accessor = component.getAccessor();
accessor.setAccessible(true);
Object value = accessor.invoke(target);
if (value == null) continue;
for (Object excluded : excludedValues) {
if (excluded == value) continue outer;
}
if (value instanceof Component || value instanceof CharSequence || value instanceof Optional<?>) {
return value;
}
}
} catch (Throwable ignored) { }
return null;
}
private static Component extractTextComponent(Object value) {
if (value == null) return null;
if (value instanceof Optional<?> optional) {
return optional.isPresent() ? extractTextComponent(optional.get()) : null;
}
if (value instanceof Component text) return literalizeTextComponent(text);
if (value instanceof CharSequence chars) return Component.literal(chars.toString());
if (value instanceof UUID uuid) return Component.literal(resolvePlayerName(uuid));
try {
Method getComponent = value.getClass().getMethod("getText");
getComponent.setAccessible(true);
Object result = getComponent.invoke(value);
Component text = extractTextComponent(result);
if (text != null) return text;
} catch (Throwable ignored) { }
try {
Method getString = value.getClass().getMethod("getString");
getString.setAccessible(true);
Object result = getString.invoke(value);
if (result != null) return Component.literal(String.valueOf(result));
} catch (Throwable ignored) { }
return null;
}
public static Component literalizeTextComponent(Component text) {
if (text == null) return null;
MutableComponent literalized = Component.empty();
try {
text.visit((style, string) -> {
if (string != null && !string.isEmpty()) {
literalized.append(Component.literal(string).setStyle(copyVisualStyle(style)));
}
return Optional.empty();
}, Style.EMPTY);
} catch (Throwable ignored) { }
if (!literalized.getString().isEmpty()) {
return literalized;
}
return Component.literal(text.getString());
}
private static Style copyVisualStyle(Style style) {
if (style == null || style.isEmpty()) return Style.EMPTY;
Style safe = Style.EMPTY;
if (style.getFont() != null) safe = safe.withFont(style.getFont());
if (style.getColor() != null) safe = safe.withColor(style.getColor());
if (style.isBold()) safe = safe.withBold(true);
if (style.isItalic()) safe = safe.withItalic(true);
if (style.isUnderlined()) safe = safe.withUnderlined(true);
if (style.isStrikethrough()) safe = safe.withStrikethrough(true);
if (style.isObfuscated()) safe = safe.withObfuscated(true);
return safe;
}
public static String serializeTextComponent(Component text) {
if (text == null) return "";
try {
Component safeComponent = literalizeTextComponent(text);
return safeComponent == null ? "" : GSON.toJson(ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, safeComponent).getOrThrow());
} catch (Throwable ignored) {
return "";
}
}
public static Component deserializeTextComponent(String json) {
if (json == null || json.isBlank()) return null;
try {
return literalizeTextComponent(ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json)).getOrThrow().copy());
} catch (Throwable ignored) {
return null;
}
}
private static String extractTextValue(Object value) {
if (value == null) return null;
if (value instanceof Optional<?> optional) {
return optional.isPresent() ? extractTextValue(optional.get()) : null;
}
if (value instanceof Component text) return text.getString();
if (value instanceof CharSequence chars) return chars.toString();
if (value instanceof UUID uuid) return resolvePlayerName(uuid);
try {
Method getString = value.getClass().getMethod("getString");
getString.setAccessible(true);
Object result = getString.invoke(value);
if (result != null) return String.valueOf(result);
} catch (Throwable ignored) { }
try {
Method getName = value.getClass().getMethod("getName");
getName.setAccessible(true);
Object result = getName.invoke(value);
if (result instanceof CharSequence chars) return chars.toString();
} catch (Throwable ignored) { }
return null;
}
}

View file

@ -0,0 +1,155 @@
package autismclient.util.macro;
import autismclient.AutismClientAddon;
import autismclient.modules.PackHideState;
import net.minecraft.client.Minecraft;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
final class MacroClientThread {
private MacroClientThread() {
}
static void runOnClientThread(Minecraft mc, Runnable runnable) throws InterruptedException {
if (PackHideState.isHardLocked()) return;
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
Runnable contextual = () -> MacroLifecycle.withRunContext(scheduledRun, runnable);
if (mc.isSameThread()) {
if (PackHideState.isHardLocked()) return;
contextual.run();
return;
}
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
AtomicBoolean pending = new AtomicBoolean(true);
mc.execute(() -> {
try {
if (!pending.compareAndSet(true, false)) return;
if (PackHideState.isHardLocked()) return;
contextual.run();
} finally {
latch.countDown();
}
});
if (!latch.await(2000, TimeUnit.MILLISECONDS)) pending.set(false);
}
static void runOnClientThread(Minecraft mc, Runnable runnable, boolean tickAligned) throws InterruptedException {
if (tickAligned) runOnClientTick(mc, runnable);
else runOnClientThread(mc, runnable);
}
public static void postOnClientThread(Minecraft mc, Runnable runnable, boolean tickAligned) throws InterruptedException {
if (tickAligned) {
runOnClientTick(mc, runnable);
return;
}
if (PackHideState.isHardLocked()) return;
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
mc.execute(() -> {
if (PackHideState.isHardLocked()) return;
MacroLifecycle.withRunContext(scheduledRun, runnable);
});
}
static void runOnClientTick(Minecraft mc, Runnable runnable) throws InterruptedException {
if (PackHideState.isHardLocked()) return;
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
Runnable contextual = () -> MacroLifecycle.withRunContext(scheduledRun, runnable);
if (mc.isSameThread()) {
if (PackHideState.isHardLocked()) return;
contextual.run();
return;
}
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
AtomicBoolean pending = new AtomicBoolean(true);
TICK_ALIGNED.add(() -> {
try {
if (!pending.compareAndSet(true, false)) return;
if (PackHideState.isHardLocked()) return;
contextual.run();
} finally {
latch.countDown();
}
});
if (!latch.await(2000, TimeUnit.MILLISECONDS)) pending.set(false);
}
static final java.util.concurrent.ConcurrentLinkedQueue<Runnable> TICK_ALIGNED =
new java.util.concurrent.ConcurrentLinkedQueue<>();
public static void drainTickAligned() {
if (autismclient.modules.AutoTotemModule.operationActive()) return;
Runnable task;
while ((task = TICK_ALIGNED.poll()) != null) {
try {
task.run();
} catch (Throwable t) {
AutismClientAddon.LOG.debug("[Autism] Tick-aligned macro task failed", t);
}
}
}
static <T> T callOnClientThread(Minecraft mc, java.util.function.Supplier<T> supplier, T fallback) throws InterruptedException {
if (PackHideState.isHardLocked()) return fallback;
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
java.util.function.Supplier<T> contextual = () -> {
AtomicReference<T> value = new AtomicReference<>(fallback);
MacroLifecycle.withRunContext(scheduledRun, () -> value.set(supplier.get()));
return value.get();
};
if (mc.isSameThread()) {
if (PackHideState.isHardLocked()) return fallback;
return contextual.get();
}
AtomicReference<T> result = new AtomicReference<>(fallback);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
AtomicBoolean pending = new AtomicBoolean(true);
mc.execute(() -> {
try {
if (!pending.compareAndSet(true, false)) return;
if (PackHideState.isHardLocked()) return;
result.set(contextual.get());
} finally {
latch.countDown();
}
});
if (!latch.await(2000, TimeUnit.MILLISECONDS)) pending.set(false);
return result.get();
}
static <T> T callOnClientThread(Minecraft mc, java.util.function.Supplier<T> supplier, T fallback,
boolean tickAligned) throws InterruptedException {
return tickAligned ? callOnClientTick(mc, supplier, fallback) : callOnClientThread(mc, supplier, fallback);
}
private static <T> T callOnClientTick(Minecraft mc, java.util.function.Supplier<T> supplier, T fallback) throws InterruptedException {
if (PackHideState.isHardLocked()) return fallback;
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
java.util.function.Supplier<T> contextual = () -> {
AtomicReference<T> value = new AtomicReference<>(fallback);
MacroLifecycle.withRunContext(scheduledRun, () -> value.set(supplier.get()));
return value.get();
};
if (mc.isSameThread()) {
if (PackHideState.isHardLocked()) return fallback;
return contextual.get();
}
AtomicReference<T> result = new AtomicReference<>(fallback);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
AtomicBoolean pending = new AtomicBoolean(true);
TICK_ALIGNED.add(() -> {
try {
if (!pending.compareAndSet(true, false)) return;
if (PackHideState.isHardLocked()) return;
result.set(contextual.get());
} finally {
latch.countDown();
}
});
if (!latch.await(2000, TimeUnit.MILLISECONDS)) pending.set(false);
return result.get();
}
}

View file

@ -0,0 +1,548 @@
package autismclient.util.macro;
import autismclient.modules.AutismModule;
import autismclient.util.AutismClientMessaging;
import net.minecraft.client.Minecraft;
import autismclient.util.macro.MacroActionResources.SharedClientResource;
import java.util.EnumSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
final class MacroDispatcher {
private MacroDispatcher() {
}
static void executeSingleActionWithWaits(Minecraft mc, MacroAction action, AutismModule module) throws InterruptedException {
if (!MacroDynamicBindings.apply(action, mc)) return;
if (!MacroActionRunner.dynamicTextInputsAvailable(action, mc)) return;
MacroPrearm.clearOwnCaptureOutput(action);
EnumSet<SharedClientResource> heldResources = MacroLifecycle.acquireClientResources(action);
try {
if (!MacroActionRunner.waitForGuiBeforeAction(mc, action)) return;
MacroActionRunner.beginRaycastAim(mc, action);
java.util.concurrent.CompletableFuture<Void> postGuiFuture =
(action instanceof ItemAction || action instanceof DropAction || MacroActionRunner.isTargetWaitInteractionAction(action)) ? null : MacroActionRunner.createPostGuiFuture(mc, action);
if (action instanceof DelayAction) {
DelayAction da = (DelayAction) action;
if (da.useTicks) {
for (int t = 0; t < da.delayTicks && MacroLifecycle.isCurrentRunActive(); t++) {
CompletableFuture<Void> tf = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tf);
}
} else {
Thread.sleep(da.delayMs);
}
} else if (action instanceof CustomMenuAction customMenu) {
MacroLifecycle.setCurrentStatus(customMenu.getDisplayName());
customMenu.execute(mc);
} else if (action instanceof WaitForPacketAction packetAction) {
packetAction.matchedPacket = null;
packetAction.matchedDirection = "";
MacroExecutor.RunState packetRun = MacroExecutor.CURRENT_RUN.get();
if (packetRun != null) {
packetRun.lastMatchedPacket = null;
packetRun.lastMatchedPacketDirection = "";
}
java.util.List<String> targets = packetAction.effectiveList();
if (targets.isEmpty()) {
MacroPacketWait.awaitPacket("");
} else {
for (String target : targets) {
if (!MacroLifecycle.isCurrentRunActive()) break;
MacroPacketWait.awaitPacket(target);
}
}
MacroPrearm.copyLastMatchedPacket(packetAction);
} else if (action instanceof WaitPacketMatchAction wpma) {
MacroLifecycle.setCurrentStatus(wpma.getDisplayName());
MacroPrearm.waitForPacketMatch(wpma);
} else if (action instanceof WaitInventoryPredicateAction wipa) {
MacroLifecycle.setCurrentStatus(wipa.getDisplayName());
MacroRunLoop.waitForPredicate(() -> wipa.matches(mc), wipa.timeoutMs);
} else if (action instanceof WaitDurabilityAction wda) {
MacroLifecycle.setCurrentStatus(wda.getDisplayName());
if (wda.useNext) MacroDurability.waitForDurabilityUseNext(mc, wda);
else MacroRunLoop.waitForPredicate(() -> wda.matches(mc), wda.timeoutMs);
} else if (action instanceof WaitFreeSlotsAction wfsa) {
MacroLifecycle.setCurrentStatus(wfsa.getDisplayName());
MacroRunLoop.waitForPredicate(() -> wfsa.matches(mc), wfsa.timeoutMs);
} else if (action instanceof WaitEntityTargetAction weta) {
MacroLifecycle.setCurrentStatus(weta.getDisplayName());
MacroRunLoop.waitForPredicate(() -> weta.matches(mc), weta.timeoutMs);
} else if (action instanceof WaitGuiTypeAction wgta) {
MacroLifecycle.setCurrentStatus(wgta.getDisplayName());
MacroPrearm.clearCaptureOutputs(wgta);
if (wgta.waitMode == WaitGuiTypeAction.WaitMode.CLOSE) MacroRunLoop.waitForPredicate(MacroRunLoop.guiClosePredicate(wgta, mc), wgta.timeoutMs);
else MacroRunLoop.waitForPredicate(() -> {
var captured = wgta.capture(mc);
captured.ifPresent(values -> MacroPrearm.stageCapture(wgta, values));
return captured.isPresent();
}, wgta.timeoutMs);
MacroPrearm.commitStagedCapture(wgta);
} else if (action instanceof PacketGateAction pga) {
MacroRace.runPacketGateAction(mc, pga);
} else if (MacroActionRunner.isTargetWaitInteractionAction(action)) {
postGuiFuture = MacroActionRunner.runTargetWaitInteractionAction(mc, action);
} else if (action instanceof WaitForHealthAction) {
WaitForHealthAction wh = (WaitForHealthAction) action;
MacroLifecycle.setCurrentStatus(wh.waitingStatusText());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForHealth(wh.healthThreshold, wh.below);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForBlockAction) {
WaitForBlockAction wba = (WaitForBlockAction) action;
MacroLifecycle.setCurrentStatus(wba.getDisplayName());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForBlock(wba);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForGuiAction) {
WaitForGuiAction wga = (WaitForGuiAction) action;
MacroPrearm.clearCaptureOutputs(wga);
if (wga.waitMode == WaitForGuiAction.WaitMode.CLOSE) {
MacroLifecycle.setCurrentStatus("Waiting for GUI close: " + wga.guiTitle);
MacroRunLoop.waitForPredicate(MacroRunLoop.guiClosePredicate(wga, mc), wga.timeoutMs);
} else {
MacroLifecycle.setCurrentStatus("Waiting for GUI: " + wga.guiTitle);
MacroRunLoop.waitForPredicate(() -> {
var captured = wga.captureGui(mc.gui.screen());
captured.ifPresent(values -> MacroPrearm.stageCapture(wga, values));
return captured.isPresent();
}, wga.timeoutMs);
}
MacroPrearm.commitStagedCapture(wga);
} else if (action instanceof WaitForCooldownAction) {
WaitForCooldownAction wca = (WaitForCooldownAction) action;
ItemTarget cooldownTarget = MacroWorldActions.resolveItemTarget(wca.itemTarget, wca.itemName).resolveTemplate(mc);
if (cooldownTarget == null) return;
String cooldownLabel = MacroWorldActions.describeItemTarget(cooldownTarget);
MacroLifecycle.setCurrentStatus("Waiting for cooldown: " +
(!cooldownLabel.isEmpty() ? cooldownLabel : (wca.checkMainInteractionHand ? "Main Hand" : "Off Hand")));
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForCooldown(cooldownTarget, wca.checkMainInteractionHand);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitPosAction) {
WaitPosAction wpa = (WaitPosAction) action;
MacroLifecycle.setCurrentStatus("Waiting for Pos: " + String.format("%.0f, %.0f, %.0f", wpa.x, wpa.y, wpa.z));
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForPos(
wpa.x, wpa.y, wpa.z, wpa.leeway, wpa.checkRotation, wpa.yaw, wpa.pitch, wpa.rotLeeway);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof CaptureValueAction captureValue) {
MacroCaptureAutofill.runCaptureValueAction(mc, captureValue);
} else if (action instanceof WaitForChatAction) {
WaitForChatAction wca = (WaitForChatAction) action;
MacroLifecycle.setCurrentStatus("Waiting for chat: " + wca.pattern);
MacroPacketWait.awaitChat(wca);
} else if (action instanceof WaitForEntityAction) {
WaitForEntityAction wea = (WaitForEntityAction) action;
MacroLifecycle.setCurrentStatus(wea.getDisplayName());
try {
MacroRunLoop.waitForPredicate(() -> wea.matchesEntityTarget(mc), wea.timeoutMs);
} catch (CancellationException e) { }
} else if (action instanceof WaitForSoundAction) {
WaitForSoundAction wsa = (WaitForSoundAction) action;
String sndDesc = wsa.soundIds.isEmpty() ? "any" : wsa.soundIds.get(0);
MacroLifecycle.setCurrentStatus("Waiting for sound: " + sndDesc);
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForSound(wsa);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForSlotChangeAction) {
WaitForSlotChangeAction wsca = (WaitForSlotChangeAction) action;
MacroLifecycle.setCurrentStatus(wsca.getDisplayName());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForSlotChange(wsca);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForWorldChangeAction world) {
MacroLifecycle.setCurrentStatus(world.getDisplayName());
try {
CompletableFuture<?> future = MacroRace.raceConditionFuture(mc, world);
MacroRace.waitForFutureDone(future, 0);
} catch (CancellationException e) { }
} else if (action instanceof WaitForPositionDeltaAction wpda) {
MacroLifecycle.setCurrentStatus(wpda.getDisplayName());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForPositionDelta(wpda.distance, wpda.horizontalOnly);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForTeleportAction teleport) {
MacroLifecycle.setCurrentStatus(teleport.getDisplayName());
try {
CompletableFuture<?> future = MacroRace.raceConditionFuture(mc, teleport);
MacroRace.waitForFutureDone(future, 0);
} catch (CancellationException e) { }
} else if (action instanceof WaitGamemodeChangeAction gm) {
MacroLifecycle.setCurrentStatus(gm.getDisplayName());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForGamemodeChange(gm);
MacroRace.waitForFutureDone(future, gm.timeoutMs);
} catch (CancellationException e) { }
} else if (action instanceof WaitMovementAction wm) {
MacroLifecycle.setCurrentStatus(wm.getDisplayName());
try {
CompletableFuture<?> future = MacroRace.raceConditionFuture(mc, wm.resolveSubAction());
MacroRace.waitForFutureDone(future, 0);
} catch (CancellationException e) { }
} else if (action instanceof WaitForLanStepAction) {
WaitForLanStepAction wla = (WaitForLanStepAction) action;
MacroLifecycle.setCurrentStatus(wla.getDisplayName());
try {
CompletableFuture<Void> future = MacroConditionRegistry.waitForLanStep(wla);
MacroRunLoop.waitForCondition(future);
} catch (CancellationException e) { }
} else if (action instanceof WaitForMacroStepAction) {
WaitForMacroStepAction wma = (WaitForMacroStepAction) action;
MacroLifecycle.setCurrentStatus(wma.getDisplayName());
MacroRace.waitForMacroStep(wma);
} else if (action instanceof GoToAction) {
GoToAction ga = (GoToAction) action;
MacroWorldActions.runGoToAction(mc, ga);
} else if (action instanceof MoveAction) {
MoveAction ma = (MoveAction) action;
if (ma.nonBlocking) {
mc.execute(() -> ma.execute(mc));
MacroLifecycle.currentBackgroundMoves().put(ma, new int[]{ma.durationTicks - 1});
} else {
for (int t = 0; t < ma.durationTicks && MacroLifecycle.isCurrentRunActive(); t++) {
mc.execute(() -> ma.execute(mc));
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
MacroExecutor.RunState movementRun = MacroExecutor.CURRENT_RUN.get();
mc.execute(() -> MacroLifecycle.releaseMoveIfUnused(movementRun, ma, mc));
}
} else if (action instanceof LookAtBlockAction) {
LookAtBlockAction la = (LookAtBlockAction) action;
LookAtBlockAction.RotationTarget rotationTarget = MacroRender.resolveLookAtTargetOnClient(mc, la);
if (rotationTarget == null) {
return;
}
if (la.smooth) {
MacroRender.startSmoothRotation(rotationTarget.yaw(), rotationTarget.pitch(), la.getRotationStep());
if (la.waitForCompletion) {
MacroRender.waitForSmoothRotationCompletion();
}
} else {
mc.execute(() -> {
if (mc.player == null) return;
mc.player.setYRot(rotationTarget.yaw());
mc.player.setXRot(rotationTarget.pitch());
});
}
} else if (action instanceof RotateAction) {
RotateAction ra = (RotateAction) action;
if (ra.smooth) {
MacroRender.startSmoothRotation(ra.yaw, ra.pitch, ra.getRotationStep());
if (ra.waitForCompletion) {
MacroRender.waitForSmoothRotationCompletion();
}
} else {
mc.execute(() -> action.execute(mc));
}
} else if (action instanceof MineAction) {
MineAction mna = (MineAction) action;
MacroWorldActions.runMineAction(mc, mna);
} else if (action instanceof InstaBreakAction) {
MacroBreak.runInstaBreakAction(mc, (InstaBreakAction) action);
} else if (action instanceof BreakAction) {
MacroBreak.runBreakAction(mc, (BreakAction) action);
} else if (action instanceof PlaceAction) {
MacroWorldActions.runPlaceAction(mc, (PlaceAction) action);
} else if (action instanceof UseItemAction) {
MacroActionRunner.runUseItemAction(mc, (UseItemAction) action);
} else if (action instanceof JumpAction) {
JumpAction ja = (JumpAction) action;
mc.execute(() -> ja.execute(mc));
if (ja.tap) {
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
} else {
for (int t = 0; t < ja.durationTicks && MacroLifecycle.isCurrentRunActive(); t++) {
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
}
mc.execute(() -> ja.release(mc));
} else if (action instanceof SneakAction) {
SneakAction sna = (SneakAction) action;
if (sna.persistent && sna.sneak) {
MacroLifecycle.currentPersistentActions().removeIf(p -> p instanceof SneakAction);
MacroLifecycle.currentPersistentActions().add(sna);
} else if (!sna.sneak) {
MacroLifecycle.currentPersistentActions().removeIf(p -> p instanceof SneakAction);
}
if (sna.persistent || !sna.sneak) MacroClientThread.postOnClientThread(mc, () -> MacroLifecycle.applyAggregateSneakState(mc), sna.isTickAligned());
else MacroClientThread.postOnClientThread(mc, () -> sna.execute(mc), sna.isTickAligned());
} else if (action instanceof SprintAction) {
SprintAction spa = (SprintAction) action;
if (spa.persistent && spa.sprint) {
MacroLifecycle.currentPersistentActions().removeIf(p -> p instanceof SprintAction);
MacroLifecycle.currentPersistentActions().add(spa);
} else if (!spa.sprint) {
MacroLifecycle.currentPersistentActions().removeIf(p -> p instanceof SprintAction);
}
if (spa.persistent || !spa.sprint) MacroClientThread.postOnClientThread(mc, () -> MacroLifecycle.applyAggregateSprintState(mc), spa.isTickAligned());
else MacroClientThread.postOnClientThread(mc, () -> spa.execute(mc), spa.isTickAligned());
} else if (action instanceof NbtBookAction nba) {
int totalBooks = Math.max(1, nba.bookCount);
long delayMs = Math.max(1, nba.delayTicks) * 50L;
boolean signedAny = false;
for (int b = 0; b < totalBooks; b++) {
if (!MacroLifecycle.isCurrentRunActive()) break;
final int bookIdx = b;
java.util.concurrent.CompletableFuture<Boolean> result = new java.util.concurrent.CompletableFuture<>();
mc.execute(() -> result.complete(nba.executeSingleBook(mc, bookIdx, totalBooks)));
try {
Boolean success = result.get();
if (!success) break;
signedAny = true;
} catch (java.util.concurrent.ExecutionException e) {
break;
}
if (b < totalBooks - 1) {
Thread.sleep(delayMs);
}
}
if (signedAny && nba.disconnectAfter) {
mc.execute(() -> nba.afterSigning(mc));
}
} else if (action instanceof ItemAction) {
ItemAction ia = (ItemAction) action;
if (ia.waitForItem) {
try {
MacroWorldActions.waitForItemActionTargets(mc, ia);
} catch (java.util.concurrent.CancellationException e) { }
}
postGuiFuture = MacroActionRunner.createPostGuiFuture(mc, ia);
MacroActionRunner.runMacroActionOnClientThread(mc, action, "ItemAction");
} else if (action instanceof DropAction || action instanceof SwapSlotsAction) {
if (action instanceof DropAction dropAction) {
postGuiFuture = MacroActionRunner.createPostGuiFuture(mc, dropAction);
}
MacroActionRunner.runMacroActionOnClientThread(mc, action, action.getDisplayName());
} else if (action instanceof SendPacketAction) {
if (module != null && module.useInstantExecutionMode()) {
try {
action.execute(mc);
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cError in SendPacket: " + e.getMessage());
}
} else {
mc.execute(() -> {
try {
action.execute(mc);
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cError in SendPacket: " + e.getMessage());
}
});
}
} else if (action instanceof ClickAction) {
ClickAction ca = (ClickAction) action;
MacroActionRunner.runClickActionBurst(mc, ca);
} else if (action instanceof PayAction) {
MacroWorldActions.runPayAction(mc, (PayAction) action);
} else if (action instanceof XCarryAction xsa) {
if (xsa.mode == XCarryAction.Mode.PUT_IN
&& (xsa.transferMode == XCarryAction.TransferMode.CLICK
|| xsa.transferMode == XCarryAction.TransferMode.SAFE_CLICK)) {
if (xsa.transferMode == XCarryAction.TransferMode.SAFE_CLICK) {
MacroLifecycle.setCurrentStatus("XCarry Safe");
MacroXCarry.runXCarrySafeClickPutIn(mc, xsa);
} else {
MacroLifecycle.setCurrentStatus("XCarry Click");
MacroXCarry.runXCarryClickPutIn(mc, xsa);
}
} else {
MacroLifecycle.setCurrentStatus(switch (xsa.mode) {
case PUT_IN -> "XCarry";
case TAKE_OUT -> "XCarry Out";
case DROP -> "XCarry Drop";
});
MacroClientThread.runOnClientThread(mc, () -> {
try {
xsa.execute(mc);
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cXCarry: " + e.getMessage());
}
});
}
} else if (action instanceof CraftAction) {
MacroWorldActions.runCraftAction(mc, (CraftAction) action);
} else if (action instanceof autismclient.util.macro.StoreItemAction) {
autismclient.util.macro.StoreItemAction sia = (autismclient.util.macro.StoreItemAction) action;
MacroLifecycle.setCurrentStatus((sia.mode == autismclient.util.macro.StoreItemAction.Mode.LOOT ? "Looting" : "Storing")
+ (sia.persistent ? " \u221e" : ""));
if (sia.persistent) {
while (MacroLifecycle.isCurrentRunActive()) {
try { MacroActionRunner.runStoreItemAction(mc, sia); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
catch (Exception e) { AutismClientMessaging.sendPrefixed("Store error: " + e.getMessage()); }
CompletableFuture<Void> siTick = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(siTick);
}
} else {
try { MacroActionRunner.runStoreItemAction(mc, sia); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
catch (Exception e) { AutismClientMessaging.sendPrefixed("Store error: " + e.getMessage()); }
}
} else if (action instanceof InventoryAuditAction auditAction
&& (auditAction.mode == InventoryAuditAction.Mode.DUPE
|| auditAction.mode == InventoryAuditAction.Mode.DUPE_SPAM)) {
MacroLifecycle.setCurrentStatus("Dupe: " + auditAction.dupeVector.name() + " " + auditAction.openMode.name());
try {
auditAction.executeDupe(mc);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Throwable t) {
AutismClientMessaging.sendPrefixed("\u00a7cDupe error: "
+ (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
}
} else if (action instanceof ContainerClickSequenceAction sequence) {
MacroActionRunner.runContainerClickSequenceAction(mc, sequence);
} else if (action instanceof PacketBurstAction burst) {
MacroActionRunner.runPacketBurstAction(mc, burst);
} else if (action instanceof TickSyncAction tsa) {
if (mc.level == null) return;
java.util.List<net.minecraft.network.protocol.Packet<?>> preGenerated =
MacroActionRunner.preGeneratePacketsForRepeat(tsa.preGenCount, mc);
long targetTick = mc.level.getGameTime() + tsa.tickOffset;
MacroLifecycle.setCurrentStatus("Tick Sync -> " + targetTick + " (" + preGenerated.size() + " pkts)");
while (MacroLifecycle.isCurrentRunActive() && mc.level.getGameTime() < targetTick) {
java.util.concurrent.locks.LockSupport.parkNanos(1_000_000L);
}
if (MacroLifecycle.isCurrentRunActive()) {
MacroRender.executePreGeneratedBurst(mc, preGenerated);
MacroExecutor.RepeatPacketContext context = MacroExecutor.REPEAT_PACKET_CONTEXT.get();
if (context != null) MacroExecutor.REPEAT_PACKET_SKIP.set(MacroRender.countPreGeneratedActions(context.macro(), context.startIdx(), preGenerated.size(), context.endIdx()));
AutismClientMessaging.sendPrefixed("Tick sync: " + preGenerated.size() + " pkts sent");
}
} else if (action instanceof RevisionSyncAction rsa) {
if (mc.player == null || mc.player.containerMenu == null) return;
java.util.List<net.minecraft.network.protocol.Packet<?>> preGenerated =
MacroActionRunner.preGeneratePacketsForRepeat(rsa.preGenCount, mc);
int baseRevision = mc.player.containerMenu.getStateId();
int targetRevision = baseRevision + rsa.revisionOffset;
MacroLifecycle.setCurrentStatus("Rev Sync -> " + targetRevision + " (" + preGenerated.size() + " pkts)");
while (MacroLifecycle.isCurrentRunActive()) {
if (mc.player == null || mc.player.containerMenu == null) break;
if (mc.player.containerMenu.getStateId() >= targetRevision) break;
java.util.concurrent.locks.LockSupport.parkNanos(500_000L);
}
if (MacroLifecycle.isCurrentRunActive() && mc.player != null && mc.player.containerMenu != null) {
MacroRender.executePreGeneratedBurst(mc, preGenerated);
MacroExecutor.RepeatPacketContext context = MacroExecutor.REPEAT_PACKET_CONTEXT.get();
if (context != null) MacroExecutor.REPEAT_PACKET_SKIP.set(MacroRender.countPreGeneratedActions(context.macro(), context.startIdx(), preGenerated.size(), context.endIdx()));
AutismClientMessaging.sendPrefixed("Rev sync: " + preGenerated.size() + " pkts sent");
}
} else if (action instanceof ServerTickSyncAction stsa) {
if (mc.getConnection() == null) return;
java.util.List<net.minecraft.network.protocol.Packet<?>> preGenerated =
MacroActionRunner.preGeneratePacketsForRepeat(stsa.preGenCount, mc);
int sampleWaitMs = 0;
while (MacroLifecycle.isCurrentRunActive() && !ServerTickTracker.isReady() && sampleWaitMs < stsa.maxWaitMs) {
float progress = ServerTickTracker.getWarmupProgress();
MacroLifecycle.setCurrentStatus(String.format("ServerSync warmup %.0f%% (%d samples, %dms)",
progress * 100, ServerTickTracker.getSampleCount(), ServerTickTracker.getTrackingTimeMs()));
Thread.sleep(50);
sampleWaitMs += 50;
}
if (!MacroLifecycle.isCurrentRunActive()) return;
long optimalTime = ServerTickTracker.getOptimalSendTime(stsa.bufferMs, stsa.ignorePing);
long msUntil = (optimalTime - System.nanoTime()) / 1_000_000L;
int ping = ServerTickTracker.getPingMs();
String pingStr = stsa.ignorePing ? " (no ping)" : " ping:" + ping + "ms";
MacroLifecycle.setCurrentStatus("ServerSync in " + Math.max(0, msUntil) + "ms" + pingStr + " (" + preGenerated.size() + " pkts)");
long startWait = System.nanoTime();
long maxWaitNanos = stsa.maxWaitMs * 1_000_000L;
while (MacroLifecycle.isCurrentRunActive()) {
long now = System.nanoTime();
if ((now - startWait) >= maxWaitNanos) break;
if (now >= optimalTime) break;
long remaining = optimalTime - now;
if (remaining > 2_000_000L) {
java.util.concurrent.locks.LockSupport.parkNanos(remaining - 2_000_000L);
} else {
Thread.onSpinWait();
}
}
if (MacroLifecycle.isCurrentRunActive()) {
MacroRender.executePreGeneratedBurst(mc, preGenerated);
MacroExecutor.RepeatPacketContext context = MacroExecutor.REPEAT_PACKET_CONTEXT.get();
if (context != null) MacroExecutor.REPEAT_PACKET_SKIP.set(MacroRender.countPreGeneratedActions(context.macro(), context.startIdx(), preGenerated.size(), context.endIdx()));
long actualDelay = (System.nanoTime() - optimalTime) / 1_000_000L;
AutismClientMessaging.sendPrefixed("ServerSync: " + preGenerated.size() + " pkts (+" + actualDelay + "ms)");
}
} else if (action instanceof BundleDupeV2Action bundleV2) {
MacroLifecycle.setCurrentStatus("Bundle Dupe V2");
bundleV2.execute(mc);
} else if (action instanceof RollbackAction rollback) {
MacroLifecycle.setCurrentStatus("Rollback");
try {
rollback.executeRollback(mc);
} catch (InterruptedException e) {
AutismClientMessaging.sendPrefixed("\u00a7eRollback canceled safely.");
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cRollback failed: " + MacroRace.safeThrowableMessage(e));
}
} else if (action instanceof StopMacroAction) {
StopMacroAction stopAction = (StopMacroAction) action;
if (stopAction.target == StopMacroAction.StopTarget.SELF) MacroLifecycle.stopCurrentRun();
else stopAction.execute(mc);
} else if (action instanceof UseItemPhaseAction phaseAction) {
MacroActionRunner.runUseItemPhaseAction(mc, phaseAction);
} else if (action instanceof autismclient.api.macro.ContextMacroAction contextAction) {
MacroActionRunner.runContextMacroAction(contextAction);
} else {
if (MacroActionRunner.usesTickAlignedDispatch(action)) {
MacroActionRunner.runMacroActionOnClientThread(mc, action, action.getDisplayName());
} else if (module != null && module.useInstantExecutionMode()) {
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
MacroExecutor.RunState scheduledRun = MacroExecutor.CURRENT_RUN.get();
mc.execute(() -> MacroLifecycle.withRunContext(scheduledRun, () -> {
try {
action.execute(mc);
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cError in action: " + e.getMessage());
} finally {
latch.countDown();
}
}));
try {
latch.await(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
MacroActionRunner.runMacroActionOnClientThread(mc, action, action.getDisplayName());
}
}
MacroPrearm.commitConditionOutput(action, mc);
if (postGuiFuture != null && MacroLifecycle.isCurrentRunActive()) {
try {
MacroRunLoop.waitForCondition(postGuiFuture);
} catch (java.util.concurrent.CancellationException e) { }
}
if (MacroLifecycle.isCurrentRunActive() && !MacroActionRunner.isInstantInventoryAction(action) && module != null && module.useInstantExecutionMode()) {
int delayUs = module.getActionDelayUs();
if (delayUs > 0) java.util.concurrent.locks.LockSupport.parkNanos(delayUs * 1000L);
else Thread.onSpinWait();
}
} finally {
MacroRaycastAim.release();
MacroLifecycle.releaseClientResources(heldResources);
}
}
}

View file

@ -0,0 +1,318 @@
package autismclient.util.macro;
import autismclient.util.AutismInventoryHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemStack;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
final class MacroDurability {
private MacroDurability() {
}
public static void waitForDurabilityUseNext(Minecraft mc, WaitDurabilityAction action) throws InterruptedException {
AtomicReference<DurabilityUseNextSession> sessionRef = new AtomicReference<>();
long start = System.currentTimeMillis();
while (MacroLifecycle.isCurrentRunActive()) {
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> tickDurabilityUseNext(mc, action, sessionRef), Boolean.FALSE))) {
return;
}
if (action.timeoutMs > 0 && System.currentTimeMillis() - start >= action.timeoutMs) return;
MacroExecutor.waitOneTick();
}
}
public static boolean durabilityObservationMatches(Minecraft mc, WaitDurabilityAction action) {
if (action == null || !action.useNext) return action != null && action.matches(mc);
DurabilityUseNextSession session = createDurabilityUseNextSession(mc, action);
if (session == null || session.fallbackOnly) return action.matches(mc);
if (session.firstSlotPending) {
ItemStack stack = stackForVisibleSlot(mc, session.activeSlot);
if (session.lockedRegistryId.isBlank() && canLockDurabilityInitialStack(action, session, stack)) {
session.lockedRegistryId = stackRegistryId(stack);
}
if (session.lockedRegistryId.isBlank()) return false;
return isSameDamageableItem(stack, session.lockedRegistryId)
&& action.matchesStack(stack)
&& findDurabilityReplacement(mc, action, session, session.activeSlot) == null;
}
if (session.lockedRegistryId.isBlank()) return action.matches(mc);
return findDurabilityReplacement(mc, action, session, -1) == null;
}
private static boolean tickDurabilityUseNext(Minecraft mc, WaitDurabilityAction action, AtomicReference<DurabilityUseNextSession> sessionRef) {
if (mc == null || mc.player == null || action == null) return false;
DurabilityUseNextSession session = sessionRef.get();
if (session == null) {
session = createDurabilityUseNextSession(mc, action);
sessionRef.set(session);
}
if (session == null || session.fallbackOnly) return action.matches(mc);
if (session.firstSlotPending) {
return tickDurabilityInitialSlot(mc, action, session);
}
adoptCurrentSameItem(mc, session);
ItemStack active = stackForVisibleSlot(mc, session.activeSlot);
if (isSameDamageableItem(active, session.lockedRegistryId) && !action.matchesStack(active)) {
return false;
}
return switchToNextDurabilityItemOrComplete(mc, action, session, session.activeSlot);
}
private static boolean tickDurabilityInitialSlot(Minecraft mc, WaitDurabilityAction action, DurabilityUseNextSession session) {
ItemStack stack = stackForVisibleSlot(mc, session.activeSlot);
if (session.lockedRegistryId.isBlank() && canLockDurabilityInitialStack(action, session, stack)) {
session.lockedRegistryId = stackRegistryId(stack);
}
if (!session.lockedRegistryId.isBlank() && isSameDamageableItem(stack, session.lockedRegistryId)) {
session.observedInitialSlot = true;
if (!action.matchesStack(stack)) return false;
session.firstSlotPending = false;
return switchToNextDurabilityItemOrComplete(mc, action, session, session.activeSlot);
}
if (session.observedInitialSlot) {
session.firstSlotPending = false;
return switchToNextDurabilityItemOrComplete(mc, action, session, session.activeSlot);
}
return false;
}
private static boolean switchToNextDurabilityItemOrComplete(Minecraft mc, WaitDurabilityAction action, DurabilityUseNextSession session, int excludeSlot) {
DurabilityCandidate replacement = findDurabilityReplacement(mc, action, session, excludeSlot);
if (replacement == null) return true;
if (!switchToDurabilityReplacement(mc, session, replacement)) return false;
return false;
}
private static DurabilityUseNextSession createDurabilityUseNextSession(Minecraft mc, WaitDurabilityAction action) {
if (mc == null || mc.player == null || action == null) return null;
DurabilityUseNextSession session = new DurabilityUseNextSession();
session.preferredHotbarSlot = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
ItemTarget target = action.targetMode == WaitDurabilityAction.TargetMode.ITEM
? ItemTarget.fromLegacyEntry(action.itemName)
: new ItemTarget();
session.initialTarget = target;
switch (action.targetMode) {
case SLOT -> {
session.activeSlot = Math.max(0, Math.min(40, action.slot));
session.firstSlotPending = true;
ItemStack stack = stackForVisibleSlot(mc, session.activeSlot);
if (canLockDurabilityInitialStack(action, session, stack)) {
session.lockedRegistryId = stackRegistryId(stack);
session.observedInitialSlot = true;
}
}
case HELD -> initializeHeldDurabilitySession(mc, action, session);
case ITEM -> initializeItemDurabilitySession(mc, action, session, target);
}
if (session.lockedRegistryId == null) session.lockedRegistryId = "";
if (session.lockedRegistryId.isBlank() && !session.firstSlotPending) {
session.fallbackOnly = true;
}
return session;
}
private static void initializeHeldDurabilitySession(Minecraft mc, WaitDurabilityAction action, DurabilityUseNextSession session) {
int selected = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
ItemStack main = mc.player.getInventory().getItem(selected);
if (main != null && !main.isEmpty() && main.isDamageableItem()) {
session.activeSlot = selected;
session.lockedRegistryId = stackRegistryId(main);
return;
}
ItemStack offhand = mc.player.getOffhandItem();
if (offhand != null && !offhand.isEmpty() && offhand.isDamageableItem()) {
session.activeSlot = 40;
session.lockedRegistryId = stackRegistryId(offhand);
return;
}
session.fallbackOnly = true;
}
private static void initializeItemDurabilitySession(Minecraft mc, WaitDurabilityAction action, DurabilityUseNextSession session, ItemTarget target) {
if (target != null && target.hasSlot()) {
session.activeSlot = Math.max(0, Math.min(40, target.slot));
session.firstSlotPending = true;
String explicitId = target.hasRegistryId() ? canonicalItemRegistryId(target.registryId) : "";
if (!explicitId.isBlank()) session.lockedRegistryId = explicitId;
ItemStack stack = stackForVisibleSlot(mc, session.activeSlot);
if (canLockDurabilityInitialStack(action, session, stack)) {
session.lockedRegistryId = stackRegistryId(stack);
session.observedInitialSlot = true;
}
return;
}
if (target != null && target.hasRegistryId()) {
session.lockedRegistryId = canonicalItemRegistryId(target.registryId);
session.activeSlot = findCurrentUsableDurabilitySlot(mc, action, session.lockedRegistryId, target);
return;
}
if (target == null || !target.hasIdentity()) {
initializeHeldDurabilitySession(mc, action, session);
return;
}
int slot = findInitialDurabilitySlot(mc, action, "", target);
if (slot >= 0) {
session.activeSlot = slot;
session.lockedRegistryId = stackRegistryId(stackForVisibleSlot(mc, slot));
} else {
session.fallbackOnly = true;
}
}
private static int findCurrentUsableDurabilitySlot(Minecraft mc, WaitDurabilityAction action, String lockedRegistryId, ItemTarget target) {
if (mc == null || mc.player == null) return -1;
int selected = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
if (initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, selected)) return selected;
return initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, 40) ? 40 : -1;
}
private static boolean canLockDurabilityInitialStack(WaitDurabilityAction action, DurabilityUseNextSession session, ItemStack stack) {
if (stack == null || stack.isEmpty() || !stack.isDamageableItem()) return false;
if (action != null && action.targetMode == WaitDurabilityAction.TargetMode.ITEM
&& session != null && session.initialTarget != null && session.initialTarget.hasIdentity()) {
return session.initialTarget.score(stack, session.activeSlot) >= 0;
}
return true;
}
private static int findInitialDurabilitySlot(Minecraft mc, WaitDurabilityAction action, String lockedRegistryId, ItemTarget target) {
if (mc == null || mc.player == null) return -1;
int selected = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
if (initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, selected)) return selected;
for (int slot = 0; slot < 9; slot++) {
if (slot != selected && initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, slot)) return slot;
}
for (int slot = 9; slot < Math.min(36, mc.player.getInventory().getContainerSize()); slot++) {
if (initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, slot)) return slot;
}
return initialDurabilitySlotMatches(mc, action, lockedRegistryId, target, 40) ? 40 : -1;
}
private static boolean initialDurabilitySlotMatches(Minecraft mc, WaitDurabilityAction action, String lockedRegistryId, ItemTarget target, int slot) {
ItemStack stack = stackForVisibleSlot(mc, slot);
if (stack == null || stack.isEmpty() || !stack.isDamageableItem()) return false;
if (lockedRegistryId != null && !lockedRegistryId.isBlank() && !isSameDamageableItem(stack, lockedRegistryId)) return false;
return target == null || !target.hasIdentity() || target.score(stack, slot) >= 0;
}
private static void adoptCurrentSameItem(Minecraft mc, DurabilityUseNextSession session) {
if (mc == null || mc.player == null || session == null || session.lockedRegistryId.isBlank()) return;
int selected = Math.max(0, Math.min(8, mc.player.getInventory().getSelectedSlot()));
if (isSameDamageableItem(mc.player.getInventory().getItem(selected), session.lockedRegistryId)) {
session.activeSlot = selected;
session.preferredHotbarSlot = selected;
return;
}
if (isSameDamageableItem(mc.player.getOffhandItem(), session.lockedRegistryId)) {
ItemStack active = stackForVisibleSlot(mc, session.activeSlot);
if (!isSameDamageableItem(active, session.lockedRegistryId)) session.activeSlot = 40;
}
}
private static DurabilityCandidate findDurabilityReplacement(Minecraft mc, WaitDurabilityAction action, DurabilityUseNextSession session, int excludeSlot) {
if (mc == null || mc.player == null || action == null || session == null || session.lockedRegistryId.isBlank()) return null;
DurabilityCandidate hotbar = null;
DurabilityCandidate inventory = null;
DurabilityCandidate offhand = null;
for (int slot = 0; slot < 9; slot++) {
DurabilityCandidate candidate = durabilityCandidate(mc, action, session.lockedRegistryId, slot, excludeSlot);
if (candidate != null && (hotbar == null || candidate.score() > hotbar.score())) hotbar = candidate;
}
int size = Math.min(36, mc.player.getInventory().getContainerSize());
for (int slot = 9; slot < size; slot++) {
DurabilityCandidate candidate = durabilityCandidate(mc, action, session.lockedRegistryId, slot, excludeSlot);
if (candidate != null && (inventory == null || candidate.score() > inventory.score())) inventory = candidate;
}
offhand = durabilityCandidate(mc, action, session.lockedRegistryId, 40, excludeSlot);
if (hotbar != null) return hotbar;
if (inventory != null) return inventory;
return offhand;
}
private static DurabilityCandidate durabilityCandidate(Minecraft mc, WaitDurabilityAction action, String registryId, int slot, int excludeSlot) {
if (slot == excludeSlot) return null;
ItemStack stack = stackForVisibleSlot(mc, slot);
if (!isSameDamageableItem(stack, registryId)) return null;
if (action.matchesStack(stack)) return null;
return new DurabilityCandidate(slot, remainingDurability(stack));
}
private static boolean switchToDurabilityReplacement(Minecraft mc, DurabilityUseNextSession session, DurabilityCandidate replacement) {
if (mc == null || mc.player == null || replacement == null) return false;
int slot = replacement.slot();
if (slot >= 0 && slot < 9) {
AutismInventoryHelper.selectHotbarSlot(mc, slot);
session.activeSlot = slot;
session.preferredHotbarSlot = slot;
return true;
}
if (slot >= 9 && slot < 36) {
int target = Math.max(0, Math.min(8, session.preferredHotbarSlot));
if (!AutismInventoryHelper.swapInventorySlots(mc, slot, target)) return false;
AutismInventoryHelper.selectHotbarSlot(mc, target);
session.activeSlot = target;
session.preferredHotbarSlot = target;
return true;
}
if (slot == 40) {
session.activeSlot = 40;
return true;
}
return false;
}
private static ItemStack stackForVisibleSlot(Minecraft mc, int visibleSlot) {
if (mc == null || mc.player == null) return ItemStack.EMPTY;
if (visibleSlot == 40) return mc.player.getOffhandItem();
if (visibleSlot < 0 || visibleSlot >= mc.player.getInventory().getContainerSize()) return ItemStack.EMPTY;
return mc.player.getInventory().getItem(visibleSlot);
}
private static boolean isSameDamageableItem(ItemStack stack, String registryId) {
return stack != null && !stack.isEmpty() && stack.isDamageableItem() && stackRegistryId(stack).equals(registryId);
}
private static int remainingDurability(ItemStack stack) {
return stack == null || !stack.isDamageableItem() ? 0 : Math.max(0, stack.getMaxDamage() - stack.getDamageValue());
}
private static String stackRegistryId(ItemStack stack) {
if (stack == null || stack.isEmpty()) return "";
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
return id == null ? "" : id.toString();
}
private static String canonicalItemRegistryId(String raw) {
if (raw == null || raw.isBlank()) return "";
String trimmed = raw.trim().toLowerCase(Locale.ROOT);
Identifier parsed = Identifier.tryParse(trimmed.contains(":") ? trimmed : "minecraft:" + trimmed);
if (parsed == null) return "";
return BuiltInRegistries.ITEM.getOptional(parsed)
.map(item -> BuiltInRegistries.ITEM.getKey(item).toString())
.orElse("");
}
private static final class DurabilityUseNextSession {
private String lockedRegistryId = "";
private int activeSlot = -1;
private int preferredHotbarSlot = 0;
private boolean firstSlotPending = false;
private boolean observedInitialSlot = false;
private boolean fallbackOnly = false;
private ItemTarget initialTarget = new ItemTarget();
}
private record DurabilityCandidate(int slot, int score) {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,623 @@
package autismclient.util.macro;
import autismclient.modules.PackHideState;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismInstaBreakRenderer;
import autismclient.util.AutismMacro;
import autismclient.util.macro.MacroActionResources.SharedClientResource;
import net.minecraft.client.Minecraft;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
final class MacroLifecycle {
private MacroLifecycle() {
}
private static java.util.Map<String, Integer> flowLabels(MacroExecutor.RunState run, AutismMacro macro) {
java.util.Map<String, Integer> cached = run.labelIndex;
if (cached != null) return cached;
java.util.LinkedHashMap<String, Integer> built = new java.util.LinkedHashMap<>();
for (int k = 0; k < macro.actions.size(); k++) {
if (macro.actions.get(k) instanceof LabelAction la) {
String key = la.normalizedName();
if (!key.isEmpty() && !built.containsKey(key)) built.put(key, k);
}
}
run.labelIndex = built;
return built;
}
private static Integer resolveFlowTarget(MacroExecutor.RunState run, AutismMacro macro, int i, FlowAction flow, Minecraft mc) {
return switch (flow.target) {
case STOP -> null;
case TOP -> 0;
case END -> macro.actions.size();
case FORWARD -> i + 1 + Math.max(0, flow.amount);
case BACK -> i - Math.max(0, flow.amount);
case STEP -> Math.max(1, flow.amount) - 1;
case LABEL -> {
String name = LabelAction.normalize(MacroVariables.expand(flow.labelName, mc));
Integer idx = flowLabels(run, macro).get(name);
if (idx == null) {
AutismClientMessaging.sendPrefixed("§eFlow: label '" + flow.labelName + "' not found.");
yield flow.onMissingLabel == FlowAction.MissingPolicy.STOP ? null : i + 1;
}
yield idx;
}
};
}
static int performFlowJump(MacroExecutor.RunState run, AutismMacro macro, int currentI, FlowAction flow, Minecraft mc) {
run.branchElseStart = -1;
run.branchElseCount = 0;
Integer dest = resolveFlowTarget(run, macro, currentI, flow, mc);
if (dest == null) return Integer.MIN_VALUE;
int size = macro.actions.size();
int target = Math.max(0, Math.min(size, dest));
if (target <= currentI) {
run.totalJumpsThisRun++;
run.jumpBudget++;
if (MacroExecutor.FLOW_JUMP_HARD_CAP >= 0 && run.totalJumpsThisRun > MacroExecutor.FLOW_JUMP_HARD_CAP) {
AutismClientMessaging.sendPrefixed("§cMacro aborted: jump limit exceeded (runaway loop).");
return Integer.MIN_VALUE;
}
if (run.jumpBudget >= MacroExecutor.FLOW_JUMP_BURST_CAP) {
run.jumpBudget = 0;
try { MacroRunLoop.waitForCondition(MacroConditionRegistry.waitForNextTick()); } catch (Exception ignored) { }
}
} else {
run.jumpBudget = 0;
}
return target - 1;
}
static boolean isCurrentRunActive() {
if (PackHideState.isHardLocked()) return false;
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) return run.running.get();
return MacroExecutor.isRunning.get();
}
public static boolean isCurrentActionRunActive() {
return isCurrentRunActive();
}
static CopyOnWriteArrayList<MacroAction> currentPersistentActions() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return run != null ? run.persistentActions : MacroExecutor.persistentActions;
}
static ConcurrentHashMap<MoveAction, int[]> currentBackgroundMoves() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return run != null ? run.backgroundMoves : MacroExecutor.backgroundMoves;
}
static void setCurrentStatus(String status) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) run.status = status == null ? "" : status;
MacroExecutor.currentStatus = status == null ? "" : status;
}
static void setCurrentStep(int step, int total) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) {
run.currentStepIndex = step;
run.totalSteps = total;
}
MacroExecutor.currentStepIndex = step;
MacroExecutor.totalSteps = total;
}
static void markStepCompleted(int stepOneBased) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) {
run.lastCompletedStep = Math.max(run.lastCompletedStep, stepOneBased);
}
}
static int stepOverrideEndIndex(AutismMacro macro, int headerIndex, int coveredRows) {
int last = macro == null ? headerIndex : macro.actions.size() - 1;
long end = (long) headerIndex + Math.max(0, coveredRows);
return (int) Math.max(headerIndex, Math.min(last, end));
}
static void armFpsStepOverride(MacroExecutor.RunState run, int headerIndex, int endIndexInclusive, int fpsLimit) {
if (run == null) return;
run.fpsStepHeaderIndex = headerIndex;
run.fpsStepEndIndex = endIndexInclusive;
FpsLimitController.applyUntilCleared(run.id, fpsLimit);
}
static void armPingStepOverride(MacroExecutor.RunState run, int headerIndex, int endIndexInclusive,
int delayMs, boolean realIncoming, boolean realOutgoing) {
if (run == null) return;
run.pingStepHeaderIndex = headerIndex;
run.pingStepEndIndex = endIndexInclusive;
PingSpoofController.applyUntilCleared(run.id, delayMs, realIncoming, realOutgoing);
}
static void clearFpsStepOverride(MacroExecutor.RunState run) {
if (run == null || run.fpsStepHeaderIndex < 0) return;
run.fpsStepHeaderIndex = -1;
run.fpsStepEndIndex = -1;
FpsLimitController.clear(run.id);
}
static void clearPingStepOverride(MacroExecutor.RunState run) {
if (run == null || run.pingStepHeaderIndex < 0) return;
run.pingStepHeaderIndex = -1;
run.pingStepEndIndex = -1;
PingSpoofController.clearMacro(run.id);
}
static void awaitBackgroundTimedOverrides(MacroExecutor.RunState run) throws InterruptedException {
if (run == null) return;
long deadline = Math.max(run.fpsBackgroundDeadlineNanos, run.pingBackgroundDeadlineNanos);
long remaining;
while (isCurrentRunActive() && (remaining = deadline - System.nanoTime()) > 0L) {
Thread.sleep(Math.min(25L, remaining / 1_000_000L + 1L));
}
}
static void updateStepBoundOverrides(MacroExecutor.RunState run, int index) {
if (run == null) return;
if (run.fpsStepHeaderIndex >= 0 && (index > run.fpsStepEndIndex || index <= run.fpsStepHeaderIndex)) {
clearFpsStepOverride(run);
}
if (run.pingStepHeaderIndex >= 0 && (index > run.pingStepEndIndex || index <= run.pingStepHeaderIndex)) {
clearPingStepOverride(run);
}
}
static void stopCurrentRun() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) {
run.running.set(false);
} else {
MacroExecutor.stop();
}
}
static void stopCurrentActionRun() {
stopCurrentRun();
}
private static EnumSet<SharedClientResource> resourcesForAction(MacroAction action) {
return MacroActionResources.resourcesForAction(action);
}
static EnumSet<SharedClientResource> acquireClientResources(MacroAction action) throws InterruptedException {
return acquireClientResources(resourcesForAction(action));
}
static EnumSet<SharedClientResource> acquireClientResources(EnumSet<SharedClientResource> resources) throws InterruptedException {
EnumSet<SharedClientResource> acquired = EnumSet.noneOf(SharedClientResource.class);
try {
for (SharedClientResource resource : SharedClientResource.values()) {
if (resources.contains(resource)) {
if (!isCurrentRunActive()) throw new InterruptedException("Macro stopped");
MacroExecutor.SHARED_RESOURCE_LOCKS.get(resource).lockInterruptibly();
acquired.add(resource);
if (!isCurrentRunActive()) throw new InterruptedException("Macro stopped");
}
}
} catch (InterruptedException e) {
releaseClientResources(acquired);
throw e;
}
return acquired;
}
static EnumSet<SharedClientResource> resourcesForActions(java.util.List<MacroAction> actions) {
return MacroActionResources.resourcesForActions(actions);
}
static void releaseClientResources(EnumSet<SharedClientResource> resources) {
if (resources == null || resources.isEmpty()) return;
SharedClientResource[] ordered = SharedClientResource.values();
for (int i = ordered.length - 1; i >= 0; i--) {
SharedClientResource resource = ordered[i];
if (resources.contains(resource)) {
ReentrantLock lock = MacroExecutor.SHARED_RESOURCE_LOCKS.get(resource);
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
}
static MacroVariableContext currentVariableContext() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return run == null ? null : run.variables;
}
static void withRunContext(MacroExecutor.RunState run, Runnable task) {
MacroExecutor.RunState previous = MacroExecutor.CURRENT_RUN.get();
if (run != null) MacroExecutor.CURRENT_RUN.set(run);
try {
task.run();
} finally {
if (previous != null) MacroExecutor.CURRENT_RUN.set(previous);
else MacroExecutor.CURRENT_RUN.remove();
}
}
static MacroExecutor.InlineNextActions collectInlineNextActions(java.util.List<MacroAction> actions, int currentIndex) {
if (actions == null || currentIndex < 0 || currentIndex >= actions.size() - 1) return MacroExecutor.InlineNextActions.empty();
java.util.ArrayList<MacroAction> collected = new java.util.ArrayList<>();
int lastIndex = -1;
for (int j = currentIndex + 1; j < actions.size(); j++) {
MacroAction candidate = actions.get(j);
if (candidate instanceof DisconnectAction
|| candidate instanceof RaceAction
|| candidate instanceof ReportAction
|| candidate instanceof RepeatAction
|| candidate instanceof BranchAction
|| candidate instanceof FinallyAction) {
break;
}
if (candidate == null || !candidate.isEnabled()) continue;
if (!RaceAction.isBodyAction(candidate)) continue;
collected.add(candidate);
lastIndex = j;
}
return collected.isEmpty() ? MacroExecutor.InlineNextActions.empty() : new MacroExecutor.InlineNextActions(java.util.List.copyOf(collected), lastIndex);
}
private static volatile java.util.List<MacroExecutor.RunState> ACTIVE_RUN_LIST = java.util.List.of();
public static void rebuildActiveRunListLocked() {
ACTIVE_RUN_LIST = java.util.List.copyOf(MacroExecutor.ACTIVE_RUNS.values());
}
static java.util.List<MacroExecutor.RunState> activeRunSnapshot() {
return ACTIVE_RUN_LIST;
}
static void finishCurrentRun() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null) return;
if (!run.finished.compareAndSet(false, true)) return;
requestStop(run, false);
clearFpsStepOverride(run);
clearPingStepOverride(run);
synchronized (MacroExecutor.RUN_LOCK) {
MacroExecutor.ACTIVE_RUNS.remove(run.id);
MacroExecutor.RUNS_BY_ID.remove(run.id);
rebuildActiveRunListLocked();
if (MacroExecutor.ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.allRunsStoppedAtMs = System.currentTimeMillis();
MacroExecutor.RUN_STATE_REVISION.incrementAndGet();
refreshPrimaryRunLocked();
}
if (!MacroExecutor.isRunning.get()) {
AutismInstaBreakRenderer.clear();
FpsLimitController.clearAll();
PingSpoofController.clearAllMacros();
}
MacroExecutor.CURRENT_RUN.remove();
}
private static boolean hasRunningRunsLocked() {
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
if (run.running.get()) return true;
}
return false;
}
private static MacroExecutor.RunState firstVisibleRunLocked() {
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
if (run.running.get() && !run.options.hiddenFromProgressHud()) return run;
}
return null;
}
public static int activeRunCountLocked() {
int count = 0;
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
if (run.running.get()) count++;
}
return count;
}
static boolean hasMacroCapacity(int activeCount) {
return Math.max(0, activeCount) < MacroExecutor.MAX_CONCURRENT_MACROS;
}
public static void refreshPrimaryRunLocked() {
MacroExecutor.primaryRun = firstVisibleRunLocked();
MacroExecutor.visibleRunActive = MacroExecutor.primaryRun != null;
MacroExecutor.currentMacro = MacroExecutor.primaryRun != null ? MacroExecutor.primaryRun.macro : null;
MacroExecutor.currentStatus = MacroExecutor.primaryRun != null ? MacroExecutor.primaryRun.status : "";
MacroExecutor.currentStepIndex = MacroExecutor.primaryRun != null ? MacroExecutor.primaryRun.currentStepIndex : -1;
MacroExecutor.totalSteps = MacroExecutor.primaryRun != null ? MacroExecutor.primaryRun.totalSteps : 0;
MacroExecutor.isRunning.set(hasRunningRunsLocked());
}
public static boolean hasRunningRuns() {
synchronized (MacroExecutor.RUN_LOCK) {
return hasRunningRunsLocked();
}
}
public static boolean isVisibleRunning() {
return MacroExecutor.visibleRunActive;
}
public static boolean isControllingInput() {
ReentrantLock inputLock = MacroExecutor.SHARED_RESOURCE_LOCKS.get(SharedClientResource.INPUT);
if (inputLock != null && inputLock.isLocked()) return true;
if (!MacroExecutor.backgroundMoves.isEmpty()) return true;
for (MacroAction action : MacroExecutor.persistentActions) {
if (action instanceof MoveAction || action instanceof JumpAction || action instanceof SneakAction || action instanceof SprintAction) return true;
}
synchronized (MacroExecutor.RUN_LOCK) {
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
if (run == null || !run.running.get()) continue;
if (!run.backgroundMoves.isEmpty()) return true;
for (MacroAction action : run.persistentActions) {
if (action instanceof MoveAction || action instanceof JumpAction || action instanceof SneakAction || action instanceof SprintAction) return true;
}
}
}
return false;
}
static void broadcastStepProgress(MacroExecutor.RunState run, int step, int total, String macroName) {
if (run != null && run.options.skipLanProgress()) return;
try {
autismclient.util.AutismLANSync sync = autismclient.util.AutismLANSync.getInstance();
if (sync.isInSession()) {
sync.broadcastStepProgress(step, total, macroName);
}
} catch (Exception ignored) { }
}
public static void requestStop(MacroExecutor.RunState run, boolean interruptThread) {
if (run == null) return;
if (run.running.getAndSet(false)) {
MacroExecutor.RUN_STATE_REVISION.incrementAndGet();
}
run.persistentActions.clear();
for (MacroExecutor.PrearmedWait wait : new ArrayList<>(run.prearmedWaits.values())) {
wait.cleanup();
}
run.prearmedWaits.clear();
releaseBackgroundMoves(run.backgroundMoves, Minecraft.getInstance(), run);
run.backgroundMoves.clear();
reconcilePersistentInput(Minecraft.getInstance());
synchronized (run.packetWaitLock) {
if (run.packetFuture != null) run.packetFuture.cancel(true);
run.packetFuture = null;
run.waitingForPacket.set(false);
run.waitingPacketName.set("");
}
CompletableFuture<Void> chatFuture = run.chatFuture;
if (chatFuture != null) chatFuture.cancel(true);
run.chatFuture = null;
run.waitingForChat.set(false);
run.waitingChatAction = null;
CompletableFuture<Void> baritoneFuture = run.baritoneGoalFuture;
if (baritoneFuture != null) baritoneFuture.cancel(true);
run.baritoneGoalFuture = null;
run.activeMineTracker = null;
Minecraft mc = Minecraft.getInstance();
PacketGateManager.clearOwnerAndFlushConfigured(run.id, mc == null ? null : mc.getConnection());
FpsLimitController.clear(run.id);
PingSpoofController.clearMacro(run.id);
MacroRender.cancelRotation(run);
MacroWorldActions.requestOwnedBaritoneStop(run, mc);
Thread thread = run.macroThread;
if (interruptThread && thread != null && thread != Thread.currentThread()) {
thread.interrupt();
}
}
static void releaseBackgroundMoves(ConcurrentHashMap<MoveAction, int[]> moves, Minecraft mc, MacroExecutor.RunState owner) {
if (moves == null || moves.isEmpty() || mc == null) return;
List<MoveAction> toRelease = new ArrayList<>(moves.keySet());
mc.execute(() -> {
for (MoveAction move : toRelease) {
try {
releaseMoveIfUnused(owner, move, mc);
} catch (Exception ignored) { }
}
});
}
static void releaseMoveIfUnused(MacroExecutor.RunState owner, MoveAction move, Minecraft mc) {
if (move == null || mc == null) return;
boolean stillOwned = false;
synchronized (MacroExecutor.RUN_LOCK) {
for (MacroExecutor.RunState active : MacroExecutor.ACTIVE_RUNS.values()) {
if (active == owner || !active.running.get()) continue;
for (MoveAction other : active.backgroundMoves.keySet()) {
if (other.direction == move.direction) {
stillOwned = true;
break;
}
}
if (stillOwned) break;
}
}
if (!stillOwned && owner != null) {
for (MoveAction other : MacroExecutor.backgroundMoves.keySet()) {
if (other.direction == move.direction) {
stillOwned = true;
break;
}
}
}
if (!stillOwned) move.release(mc);
}
public static void reconcilePersistentInput(Minecraft mc) {
if (mc == null) return;
mc.execute(() -> {
applyAggregateSneakState(mc);
applyAggregateSprintState(mc);
});
}
static void applyAggregateSneakState(Minecraft mc) {
if (mc == null || mc.options == null || mc.options.keyShift == null) return;
boolean held = hasPersistentInput(SneakAction.class);
mc.options.keyShift.setDown(held);
}
static void applyAggregateSprintState(Minecraft mc) {
if (mc == null) return;
boolean held = hasPersistentInput(SprintAction.class);
if (mc.options != null && mc.options.keySprint != null) mc.options.keySprint.setDown(held);
if (mc.player != null) mc.player.setSprinting(held);
}
private static boolean hasPersistentInput(Class<? extends MacroAction> type) {
for (MacroAction action : MacroExecutor.persistentActions) {
if (type.isInstance(action)) return true;
}
synchronized (MacroExecutor.RUN_LOCK) {
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
if (!run.running.get()) continue;
for (MacroAction action : run.persistentActions) {
if (type.isInstance(action)) return true;
}
}
}
return false;
}
public static String macroKey(String macroName) {
return macroName == null ? "" : macroName.trim().toLowerCase(Locale.ROOT);
}
public static String macroName(AutismMacro macro) {
return macro != null && macro.name != null ? macro.name : "";
}
public static boolean matchesMacro(MacroExecutor.RunState run, AutismMacro macro, String macroName) {
if (run == null || !run.running.get()) return false;
if (macro != null && (run.sourceMacro == macro || run.macro == macro)) return true;
String targetKey = macroKey(macroName);
if (targetKey.isEmpty() && macro != null) targetKey = macroKey(macro.name);
return !targetKey.isEmpty() && macroKey(macroName(run.macro)).equals(targetKey);
}
public static void execute(AutismMacro macro) {
executeTracked(macro);
}
public static long executeTracked(AutismMacro macro) {
return executeTracked(macro, MacroExecutor.RunOptions.NORMAL);
}
public static long executeTracked(AutismMacro macro, MacroExecutor.RunOptions options) {
if (PackHideState.isHardLocked()) return -1L;
MacroExecutor.RunOptions runOptions = options == null ? MacroExecutor.RunOptions.NORMAL : options;
if (macro == null || macro.actions == null || macro.actions.isEmpty()) {
if (runOptions.silentLifecycle()) return -1L;
AutismClientMessaging.sendPrefixed("\u00a7cMacro has no actions!");
return -1L;
}
AutismMacro executionMacro;
try {
executionMacro = macro.deepCopy();
} catch (Throwable copyError) {
if (!runOptions.silentLifecycle()) AutismClientMessaging.sendPrefixed("\u00a7cCould not prepare macro run.");
return -1L;
}
if (executionMacro == null || executionMacro.actions == null || executionMacro.actions.isEmpty()) return -1L;
MacroExecutor.RunState runState;
synchronized (MacroExecutor.RUN_LOCK) {
String name = macro.name == null ? "" : macro.name;
String nameKey = macroKey(name);
if (!runOptions.backgroundRun()) {
for (MacroExecutor.RunState active : MacroExecutor.ACTIVE_RUNS.values()) {
if (active.options.backgroundRun()) continue;
String activeName = active.macro != null && active.macro.name != null ? active.macro.name : "";
if (active.running.get() && macroKey(activeName).equals(nameKey)) {
if (!runOptions.silentLifecycle()) AutismClientMessaging.sendPrefixed("\u00a7eMacro already running: " + name);
return -1L;
}
}
if (!hasMacroCapacity(activeRunCountLocked())) {
if (!runOptions.silentLifecycle()) AutismClientMessaging.sendPrefixed("\u00a7cOnly " + MacroExecutor.MAX_CONCURRENT_MACROS + " macros can run at the same time.");
return -1L;
}
} else if (!hasMacroCapacity(activeRunCountLocked())) {
return -1L;
}
runState = new MacroExecutor.RunState(MacroExecutor.NEXT_RUN_ID.getAndIncrement(), macro, executionMacro, runOptions);
if (MacroExecutor.ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.expireSuggestionCacheAfterIdle();
MacroExecutor.ACTIVE_RUNS.put(runState.id, runState);
MacroExecutor.RUNS_BY_ID.put(runState.id, runState);
rebuildActiveRunListLocked();
MacroExecutor.RUN_STATE_REVISION.incrementAndGet();
refreshPrimaryRunLocked();
}
if (!runOptions.silentLifecycle()) AutismClientMessaging.sendPrefixed("\u00a7aStarted macro: " + macro.name);
broadcastStepProgress(runState, 0, executionMacro.actions.size(), executionMacro.name);
Thread thread = new Thread(() -> MacroRunLoop.run(runState), "Autism-Macro-" + runState.id);
thread.setDaemon(true);
MacroExecutor.macroThread = thread;
runState.macroThread = thread;
try {
thread.start();
} catch (Throwable startError) {
requestStop(runState, false);
synchronized (MacroExecutor.RUN_LOCK) {
MacroExecutor.ACTIVE_RUNS.remove(runState.id);
MacroExecutor.RUNS_BY_ID.remove(runState.id);
rebuildActiveRunListLocked();
if (MacroExecutor.ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.allRunsStoppedAtMs = System.currentTimeMillis();
MacroExecutor.RUN_STATE_REVISION.incrementAndGet();
refreshPrimaryRunLocked();
}
return -1L;
}
return runState.id;
}
public static boolean isRunActive(long id) {
if (id < 0L) return false;
MacroExecutor.RunState run = MacroExecutor.RUNS_BY_ID.get(id);
return run != null && run.running.get();
}
public static long runStateRevision() {
return MacroExecutor.RUN_STATE_REVISION.get();
}
public static long currentRunId() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return run == null ? -1L : run.id;
}
public static void stopRun(long id) {
if (id < 0L) return;
MacroExecutor.RunState run;
synchronized (MacroExecutor.RUN_LOCK) {
run = MacroExecutor.ACTIVE_RUNS.get(id);
}
if (run == null) return;
requestStop(run, true);
synchronized (MacroExecutor.RUN_LOCK) {
refreshPrimaryRunLocked();
}
}
}

View file

@ -0,0 +1,224 @@
package autismclient.util.macro;
import autismclient.modules.PackHideState;
import autismclient.util.AutismPacketNamer;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import java.util.concurrent.CompletableFuture;
final class MacroPacketObserve {
private MacroPacketObserve() {
}
static final class TimedOneShotPacketListener {
public final java.util.concurrent.CompletableFuture<MacroExecutor.MacroEvent> future = new java.util.concurrent.CompletableFuture<>();
private final java.util.List<TimedOneShotPacketListener> registry;
private final TimedPacketMatch match;
private final long expireAtNanos;
TimedOneShotPacketListener(java.util.List<TimedOneShotPacketListener> registry, TimedPacketMatch match) {
this.registry = registry;
this.match = match;
this.expireAtNanos = System.nanoTime() + MacroExecutor.ONE_SHOT_EXPIRE_NANOS;
registry.add(this);
}
boolean offer(MacroExecutor.MacroEvent event) {
if (future.isDone()) return true;
if (event.nanoTime() - expireAtNanos >= 0L) { future.cancel(true); return true; }
if (match.test(event.packet(), event.direction(), event.nanoTime())) {
future.complete(event);
return true;
}
return false;
}
public void cancel() {
registry.remove(this);
if (!future.isDone()) future.cancel(true);
}
}
interface TimedPacketMatch {
boolean test(Packet<?> packet, String direction, long nanoTime);
}
static final java.util.List<MacroExecutor.OneShotPacketListener> ONE_SHOT_RECV =
new java.util.concurrent.CopyOnWriteArrayList<>();
static final java.util.List<MacroExecutor.OneShotPacketListener> ONE_SHOT_SEND =
new java.util.concurrent.CopyOnWriteArrayList<>();
static final java.util.List<TimedOneShotPacketListener> TIMED_ONE_SHOT_RECV =
new java.util.concurrent.CopyOnWriteArrayList<>();
static final java.util.List<TimedOneShotPacketListener> TIMED_ONE_SHOT_SEND =
new java.util.concurrent.CopyOnWriteArrayList<>();
public static MacroExecutor.OneShotPacketListener awaitReceive(java.util.function.Predicate<Packet<?>> match) {
return new MacroExecutor.OneShotPacketListener(ONE_SHOT_RECV, match);
}
public static MacroExecutor.OneShotPacketListener awaitSend(java.util.function.Predicate<Packet<?>> match) {
return new MacroExecutor.OneShotPacketListener(ONE_SHOT_SEND, match);
}
static TimedOneShotPacketListener awaitTimedReceive(TimedPacketMatch match) {
return new TimedOneShotPacketListener(TIMED_ONE_SHOT_RECV, match);
}
static TimedOneShotPacketListener awaitTimedSend(java.util.function.Predicate<Packet<?>> match) {
return awaitTimedSend((packet, direction, nanoTime) -> match.test(packet));
}
static TimedOneShotPacketListener awaitTimedSend(TimedPacketMatch match) {
return new TimedOneShotPacketListener(TIMED_ONE_SHOT_SEND, match);
}
private static void fireOneShotObservers(java.util.List<MacroExecutor.OneShotPacketListener> list, Packet<?> packet) {
if (list.isEmpty()) return;
long now = System.nanoTime();
java.util.ArrayList<MacroExecutor.OneShotPacketListener> matched = null;
for (MacroExecutor.OneShotPacketListener listener : list) {
boolean drop;
try { drop = listener.offer(packet, now); }
catch (Throwable ignored) { drop = true; }
if (drop) {
if (matched == null) matched = new java.util.ArrayList<>(4);
matched.add(listener);
}
}
if (matched != null) list.removeAll(matched);
}
private static void fireTimedObservers(java.util.List<TimedOneShotPacketListener> list, MacroExecutor.MacroEvent event) {
if (list.isEmpty()) return;
java.util.ArrayList<TimedOneShotPacketListener> matched = null;
for (TimedOneShotPacketListener listener : list) {
boolean drop;
try { drop = listener.offer(event); }
catch (Throwable ignored) { drop = true; }
if (drop) {
if (matched == null) matched = new java.util.ArrayList<>(4);
matched.add(listener);
}
}
if (matched != null) list.removeAll(matched);
}
public static void drainOneShotListeners() {
for (MacroExecutor.OneShotPacketListener listener : ONE_SHOT_RECV) listener.cancel();
for (MacroExecutor.OneShotPacketListener listener : ONE_SHOT_SEND) listener.cancel();
for (TimedOneShotPacketListener listener : TIMED_ONE_SHOT_RECV) listener.cancel();
for (TimedOneShotPacketListener listener : TIMED_ONE_SHOT_SEND) listener.cancel();
}
public static String oneShotCensus() {
return "runs=" + MacroExecutor.getActiveRunCount()
+ " recv=" + ONE_SHOT_RECV.size()
+ " send=" + ONE_SHOT_SEND.size()
+ " timedRecv=" + TIMED_ONE_SHOT_RECV.size()
+ " timedSend=" + TIMED_ONE_SHOT_SEND.size();
}
public static void onPacketSent(Packet<?> packet) {
if (PackHideState.isHardLocked()) return;
if (!TIMED_ONE_SHOT_SEND.isEmpty()) {
fireTimedObservers(TIMED_ONE_SHOT_SEND, new MacroExecutor.MacroEvent(packet, "C2S", System.nanoTime(), null));
}
fireOneShotObservers(ONE_SHOT_SEND, packet);
onPacketObserved(packet, "C2S");
}
public static void onPacketReceived(Packet<?> packet) {
if (PackHideState.isHardLocked()) return;
if (!TIMED_ONE_SHOT_RECV.isEmpty()) {
fireTimedObservers(TIMED_ONE_SHOT_RECV, new MacroExecutor.MacroEvent(packet, "S2C", System.nanoTime(), null));
}
fireOneShotObservers(ONE_SHOT_RECV, packet);
if (packet instanceof net.minecraft.network.protocol.game.ClientboundRespawnPacket
|| packet instanceof net.minecraft.network.protocol.game.ClientboundLoginPacket) {
MacroConditionRegistry.onRespawnPacket();
}
if (packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket) {
MacroConditionRegistry.onTeleportPacket();
}
onPacketObserved(packet, "S2C");
}
public static void onPacketObserved(Packet<?> packet, String direction) {
if (PackHideState.isHardLocked()) return;
MacroChatObserve.syncRecentChatServerContext();
java.util.List<MacroExecutor.RunState> runs = MacroLifecycle.activeRunSnapshot();
boolean s2c = "S2C".equals(direction);
boolean chatWork = false;
boolean mineWork = false;
boolean packetWork = false;
for (int i = 0; i < runs.size(); i++) {
MacroExecutor.RunState run = runs.get(i);
if (!run.running.get()) continue;
if (run.waitingForChat.get()) chatWork = true;
if (run.activeMineTracker != null) mineWork = true;
if (run.waitingForPacket.get()) packetWork = true;
}
if (s2c && (chatWork || MacroExecutor.isWaitingForChat.get())) {
MacroExecutor.ChatCapture capture = MacroChatObserve.extractIncomingChat(packet);
if (capture != null && capture.message() != null && !capture.message().isBlank()) {
for (MacroExecutor.RunState run : runs) {
var matched = run.running.get() && run.waitingForChat.get()
? MacroChatObserve.captureChatAction(run.waitingChatAction, capture, run.variables)
: java.util.Optional.<java.util.Map<String, MacroValue>>empty();
if (matched.isPresent()) {
run.stagedCaptures.put(run.waitingChatAction, matched.get());
CompletableFuture<Void> f = run.chatFuture;
if (f != null) f.complete(null);
}
}
if (MacroLifecycle.isCurrentRunActive() && MacroExecutor.isWaitingForChat.get() && MacroChatObserve.matchesWaitingChat(capture)) {
MacroExecutor.lastMatchedChat.set(capture.displayText());
CompletableFuture<Void> f = MacroExecutor.activeChatFuture;
if (f != null) f.complete(null);
}
}
}
if (s2c && mineWork) {
Minecraft mc = Minecraft.getInstance();
for (MacroExecutor.RunState run : runs) {
MacroWorldActions.MineProgressTracker tracker = run.running.get() ? run.activeMineTracker : null;
if (tracker != null) tracker.observePacket(mc, packet);
}
}
if (!MacroLifecycle.isCurrentRunActive()) return;
if (packetWork) {
for (MacroExecutor.RunState run : runs) {
if (!run.running.get() || !run.waitingForPacket.get()) continue;
CompletableFuture<Void> f = null;
synchronized (run.packetWaitLock) {
if (run.waitingForPacket.get() && MacroPacketWait.matchesPacketTarget(run.waitingPacketName.get(), packet, direction)) {
run.lastMatchedPacket = packet;
run.lastMatchedPacketDirection = direction;
f = run.packetFuture;
}
}
if (f != null) f.complete(null);
}
}
if (MacroExecutor.isWaitingForPacket.get()) {
CompletableFuture<Void> f = null;
synchronized (MacroExecutor.packetWaitLock) {
if (!MacroExecutor.isWaitingForPacket.get()) return;
String target = MacroExecutor.waitingPacketName.get();
if (MacroPacketWait.matchesPacketTarget(target, packet, direction)) {
MacroExecutor.lastReceivedPacket.set(direction + " " + AutismPacketNamer.getFriendlyName(packet, direction));
f = MacroExecutor.activePacketFuture;
}
}
if (f != null) f.complete(null);
}
}
}

View file

@ -0,0 +1,197 @@
package autismclient.util.macro;
import autismclient.util.AutismPacketNamer;
import autismclient.util.AutismPacketRegistry;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import java.util.concurrent.CompletableFuture;
final class MacroPacketWait {
private MacroPacketWait() {
}
private static String normalizePacketKey(String value) {
return AutismPacketNamer.normalizePacketKey(value);
}
private static boolean packetKeyMatches(String normalizedExpected, String candidate) {
if (normalizedExpected.isEmpty() || candidate == null || candidate.isEmpty()) return false;
String normalizedCandidate = normalizePacketKey(candidate);
if (normalizedCandidate.isEmpty()) return false;
if (normalizedExpected.equals(normalizedCandidate)) return true;
if (normalizedCandidate.endsWith("packet")) {
String strippedCandidate = normalizedCandidate.substring(0, normalizedCandidate.length() - "packet".length());
if (normalizedExpected.equals(strippedCandidate)) return true;
}
return normalizedExpected.endsWith(normalizedCandidate) || normalizedCandidate.endsWith(normalizedExpected);
}
private static boolean packetNameMatches(String expected, String candidate) {
if (expected == null || expected.isEmpty()) return false;
return packetKeyMatches(normalizePacketKey(expected), candidate);
}
private record ParsedPacketTarget(String direction, String expectedName, String normalizedExpected) {}
private static final java.util.concurrent.ConcurrentHashMap<String, ParsedPacketTarget> PARSED_TARGET_CACHE =
new java.util.concurrent.ConcurrentHashMap<>();
private static final ParsedPacketTarget EMPTY_PACKET_TARGET = new ParsedPacketTarget("", "", "");
private static ParsedPacketTarget parsedPacketTarget(String target) {
if (target == null) return EMPTY_PACKET_TARGET;
if (PARSED_TARGET_CACHE.size() > 256) PARSED_TARGET_CACHE.clear();
return PARSED_TARGET_CACHE.computeIfAbsent(target, t -> {
String expectedName = WaitForPacketAction.getPacketName(t);
return new ParsedPacketTarget(WaitForPacketAction.getDirection(t), expectedName, normalizePacketKey(expectedName));
});
}
public static boolean matchesPacketTarget(String target, Packet<?> packet, String direction) {
if (packet == null) return false;
ParsedPacketTarget parsed = parsedPacketTarget(target);
if (!parsed.direction().isEmpty() && !parsed.direction().equalsIgnoreCase(direction)) return false;
if (parsed.expectedName().isEmpty()) return true;
String normalizedExpected = parsed.normalizedExpected();
if (packetKeyMatches(normalizedExpected, AutismPacketNamer.getFriendlyName(packet, direction))) return true;
if (packetKeyMatches(normalizedExpected, AutismPacketNamer.getFriendlyName(packet))) return true;
if (packetKeyMatches(normalizedExpected, packet.getClass().getSimpleName())) return true;
@SuppressWarnings("unchecked")
String fullName = AutismPacketRegistry.getName((Class<? extends Packet<?>>) packet.getClass());
return packetKeyMatches(normalizedExpected, fullName);
}
public static void awaitPacket(String target) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) {
CompletableFuture<Void> future = new CompletableFuture<>();
String normalizedTarget = WaitForPacketAction.normalizeTarget(target);
synchronized (run.packetWaitLock) {
run.waitingPacketName.set(normalizedTarget);
run.packetFuture = future;
run.waitingForPacket.set(true);
}
try {
MacroLifecycle.setCurrentStatus(normalizedTarget.isEmpty()
? "Waiting for packet: Any"
: "Waiting for packet: " + WaitForPacketAction.getDisplayLabel(normalizedTarget));
MacroRunLoop.waitForCondition(future);
} finally {
synchronized (run.packetWaitLock) {
if (run.packetFuture == future) {
run.waitingForPacket.set(false);
run.waitingPacketName.set("");
run.packetFuture = null;
}
}
}
return;
}
CompletableFuture<Void> future = new CompletableFuture<>();
String normalizedTarget = WaitForPacketAction.normalizeTarget(target);
synchronized (MacroExecutor.packetWaitLock) {
MacroExecutor.waitingPacketName.set(normalizedTarget);
MacroExecutor.lastReceivedPacket.set("");
MacroExecutor.activePacketFuture = future;
MacroExecutor.isWaitingForPacket.set(true);
}
try {
MacroLifecycle.setCurrentStatus(normalizedTarget.isEmpty()
? "Waiting for packet: Any"
: "Waiting for packet: " + WaitForPacketAction.getDisplayLabel(normalizedTarget));
MacroRunLoop.waitForCondition(future);
} finally {
synchronized (MacroExecutor.packetWaitLock) {
if (MacroExecutor.activePacketFuture == future) {
MacroExecutor.isWaitingForPacket.set(false);
MacroExecutor.waitingPacketName.set("");
MacroExecutor.activePacketFuture = null;
}
}
}
}
public static void awaitChat(WaitForChatAction wca) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) {
MacroPrearm.clearCaptureOutputs(wca);
CompletableFuture<Void> future = new CompletableFuture<>();
run.chatFuture = future;
run.waitingChatAction = wca;
run.waitingForChat.set(true);
try {
if (wca.timeoutMs > 0) future.completeOnTimeout(null, wca.timeoutMs, java.util.concurrent.TimeUnit.MILLISECONDS);
MacroRunLoop.waitForCondition(future);
MacroPrearm.commitStagedCapture(wca);
} finally {
if (run.chatFuture == future) {
run.waitingForChat.set(false);
run.waitingChatAction = null;
run.chatFuture = null;
}
}
return;
}
CompletableFuture<Void> future = new CompletableFuture<>();
MacroExecutor.activeChatFuture = future;
MacroExecutor.isWaitingForChat.set(true);
MacroExecutor.waitingChatPattern.set(MacroChatText.normalizeManualText(wca.pattern));
MacroExecutor.waitingChatPatternJson.set(wca.patternJson == null ? "" : wca.patternJson);
MacroExecutor.waitingChatIsRegex.set(wca.useRegex);
MacroExecutor.waitingChatFuzzyPercent.set(WaitForChatAction.clampFuzzyPercent(wca.fuzzyPercent));
MacroExecutor.lastMatchedChat.set("");
try {
if (wca.timeoutMs > 0) future.completeOnTimeout(null, wca.timeoutMs, java.util.concurrent.TimeUnit.MILLISECONDS);
MacroRunLoop.waitForCondition(future);
} finally {
MacroExecutor.isWaitingForChat.set(false);
MacroExecutor.waitingChatPattern.set("");
MacroExecutor.waitingChatPatternJson.set("");
MacroExecutor.waitingChatIsRegex.set(false);
MacroExecutor.waitingChatFuzzyPercent.set(100);
MacroExecutor.activeChatFuture = null;
}
}
public static void awaitCaptureValueChat(CaptureValueAction action) {
if (action == null) return;
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroPrearm.clearCaptureOutputs(action);
MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> {
MacroExecutor.ChatCapture capture = MacroChatObserve.extractIncomingChat(packet);
if (capture == null) return false;
MacroExecutor.RecentChatMessage message = new MacroExecutor.RecentChatMessage(
capture.sender() == null ? "" : capture.sender(),
capture.message() == null ? "" : capture.message(),
capture.displayText() == null ? "" : capture.displayText(),
capture.displayComponent(),
capture.source(),
System.currentTimeMillis()
);
CaptureValueAction.Preview preview = action.previewChat(message);
if (!preview.success()) return false;
if (run != null) run.stagedCaptures.put(action, preview.values());
else MacroVariables.setAll(preview.values());
return true;
});
try {
if (MacroRace.waitForFutureDone(listener.future, 0) && run != null) MacroPrearm.commitStagedCapture(action);
} finally {
listener.cancel();
}
}
static CaptureListSelector.State captureSelectionState(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null) return null;
synchronized (run.captureSelection) {
return run.captureSelection.computeIfAbsent(action, a -> new CaptureListSelector.State());
}
}
}

View file

@ -0,0 +1,518 @@
package autismclient.util.macro;
import autismclient.util.AutismInventoryHelper;
import autismclient.util.AutismPacketNamer;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.inventory.Slot;
import java.lang.reflect.RecordComponent;
import java.util.Locale;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import net.minecraft.world.item.ItemStack;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
final class MacroPrearm {
private MacroPrearm() {
}
public static CompletableFuture<Void> predicateFuture(java.util.function.BooleanSupplier predicate, int timeoutMs) {
CompletableFuture<Void> future = new CompletableFuture<>();
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
Thread worker = new Thread(() -> MacroLifecycle.withRunContext(run, () -> {
long start = System.currentTimeMillis();
while (!future.isDone() && MacroLifecycle.isCurrentRunActive()) {
try {
if (predicate.getAsBoolean()) {
future.complete(null);
return;
}
} catch (Throwable ignored) { }
if (timeoutMs > 0 && System.currentTimeMillis() - start >= timeoutMs) {
future.complete(null);
return;
}
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) return;
}
}), "MacroPredicateWait");
worker.setDaemon(true);
worker.start();
future.whenComplete((v, e) -> worker.interrupt());
return future;
}
public static void prearmNextActionWait(Minecraft mc, java.util.List<MacroAction> actions, int currentIndex) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || actions == null) return;
int nextIndex = currentIndex + 1;
if (nextIndex < 0 || nextIndex >= actions.size()) return;
MacroAction next = actions.get(nextIndex);
if (next == null || !next.isEnabled() || !next.listensDuringPreviousAction()) return;
if (!MacroDynamicBindings.apply(next, mc)) return;
if (!MacroActionRunner.dynamicTextInputsAvailable(next, mc)) return;
if (!isWaitConditionAction(next)) return;
synchronized (run.prearmedWaits) {
if (run.prearmedWaits.containsKey(next)) return;
clearCaptureOutputs(next);
clearOwnCaptureOutput(next);
MacroExecutor.PrearmedWait wait = createPrearmedWait(mc, next);
if (wait != null) run.prearmedWaits.put(next, wait);
}
}
private static boolean isWaitConditionAction(MacroAction action) {
return action instanceof WaitForPacketAction
|| action instanceof WaitPacketMatchAction
|| action instanceof WaitForChatAction
|| action instanceof WaitForGuiAction
|| action instanceof WaitGuiTypeAction
|| action instanceof WaitForSoundAction
|| action instanceof WaitForSlotChangeAction
|| action instanceof WaitInventoryPredicateAction
|| action instanceof WaitDurabilityAction
|| action instanceof WaitFreeSlotsAction
|| action instanceof WaitForHealthAction
|| action instanceof WaitForBlockAction
|| action instanceof WaitForEntityAction
|| action instanceof WaitEntityTargetAction
|| action instanceof WaitForCooldownAction
|| action instanceof WaitPosAction
|| action instanceof WaitForPositionDeltaAction
|| action instanceof WaitForTeleportAction
|| action instanceof WaitGamemodeChangeAction
|| action instanceof WaitForWorldChangeAction
|| action instanceof WaitMovementAction
|| action instanceof WaitForLanStepAction
|| action instanceof WaitForMacroStepAction;
}
public static MacroExecutor.PrearmedWait consumePrearmedWait(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null) return null;
synchronized (run.prearmedWaits) {
return run.prearmedWaits.remove(action);
}
}
public static boolean waitForPrearmed(MacroAction action, long timeoutMs) {
MacroExecutor.PrearmedWait wait = consumePrearmedWait(action);
if (wait == null) return false;
try {
MacroRace.waitForFutureDone(wait.future, timeoutMs);
commitStagedCapture(action);
return true;
} finally {
wait.cleanup();
}
}
private static MacroExecutor.PrearmedWait createPrearmedWait(Minecraft mc, MacroAction action) {
if (action instanceof WaitForPacketAction packetAction) {
java.util.List<String> targets = packetAction.effectiveList();
String firstTarget = targets.isEmpty() ? "" : targets.get(0);
return packetTargetFuture(packetAction, firstTarget, 1);
}
if (action instanceof WaitPacketMatchAction packetMatch) {
return packetMatchFuture(packetMatch);
}
if (action instanceof WaitInventoryPredicateAction inv) {
inv.resetBaseline();
return predicatePrearm(action, () -> inv.matches(mc), inv.timeoutMs);
}
if (action instanceof WaitDurabilityAction durability) {
if (durability.useNext) return null;
return predicatePrearm(action, () -> MacroDurability.durabilityObservationMatches(mc, durability), durability.timeoutMs);
}
if (action instanceof WaitFreeSlotsAction freeSlots) {
return predicatePrearm(action, () -> freeSlots.matches(mc), freeSlots.timeoutMs);
}
if (action instanceof WaitEntityTargetAction entityTarget) {
return predicatePrearm(action, () -> entityTarget.matches(mc), entityTarget.timeoutMs);
}
if (action instanceof WaitGuiTypeAction guiType) {
return predicatePrearm(action,
guiType.waitMode == WaitGuiTypeAction.WaitMode.CLOSE
? MacroRunLoop.guiClosePredicate(guiType, mc)
: () -> {
var capture = guiType.capture(mc);
capture.ifPresent(values -> stageCapture(guiType, values));
return capture.isPresent();
},
guiType.timeoutMs);
}
if (action instanceof WaitForChatAction chat) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> {
MacroExecutor.ChatCapture capture = MacroChatObserve.extractIncomingChat(packet);
var matched = MacroChatObserve.captureChatAction(chat, capture, run == null ? null : run.variables);
if (matched.isEmpty()) return false;
if (run != null) run.stagedCaptures.put(chat, matched.get());
return true;
});
CompletableFuture<?> future = listener.future;
if (chat.timeoutMs > 0) future.completeOnTimeout(null, chat.timeoutMs, TimeUnit.MILLISECONDS);
return new MacroExecutor.PrearmedWait(action, future, listener::cancel, 0);
}
if (action instanceof WaitForMacroStepAction macroStep) {
return predicatePrearm(action, () -> MacroRace.isMacroStepSatisfied(macroStep), macroStep.timeoutMs);
}
if (action instanceof WaitForLanStepAction lanStep) {
CompletableFuture<Void> future = MacroConditionRegistry.waitForLanStep(lanStep);
return new MacroExecutor.PrearmedWait(action, future, () -> {
if (!future.isDone()) future.cancel(true);
}, 0);
}
CompletableFuture<?> future = MacroRace.raceConditionFuture(mc, action);
if (future == null) return null;
int timeout = action instanceof WaitForChatAction chat ? chat.timeoutMs : 0;
if (timeout > 0) future.completeOnTimeout(null, timeout, TimeUnit.MILLISECONDS);
return new MacroExecutor.PrearmedWait(action, future, () -> {
if (!future.isDone()) future.cancel(true);
}, 0);
}
public static boolean hasPrearmedWait(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null) return false;
synchronized (run.prearmedWaits) {
return run.prearmedWaits.containsKey(action);
}
}
public static void recordMatchedPacket(MacroAction action, Packet<?> packet, String direction) {
if (action instanceof WaitForPacketAction wait) {
wait.matchedPacket = packet;
wait.matchedDirection = direction == null ? "" : direction;
} else if (action instanceof WaitPacketMatchAction wait) {
wait.matchedPacket = packet;
wait.matchedDirection = direction == null ? "" : direction;
}
}
public static void copyLastMatchedPacket(WaitForPacketAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (action == null || run == null || run.lastMatchedPacket == null) return;
action.matchedPacket = run.lastMatchedPacket;
action.matchedDirection = run.lastMatchedPacketDirection;
}
static void clearCaptureOutputs(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null) return;
run.stagedCaptures.remove(action);
if (action instanceof WaitForChatAction chat) {
for (String name : MacroCapturePattern.declaredNames(chat.effectiveMatchMode(), chat.pattern)) run.variables.remove(name);
if (chat.saveAs != null && !chat.saveAs.isBlank()) run.variables.remove(chat.saveAs);
} else if (action instanceof WaitForGuiAction gui) {
for (String name : MacroCapturePattern.declaredNames(gui.matchMode, gui.guiTitle)) run.variables.remove(name);
if (gui.saveAs != null && !gui.saveAs.isBlank()) run.variables.remove(gui.saveAs);
} else if (action instanceof WaitGuiTypeAction gui) {
for (String name : MacroCapturePattern.declaredNames(gui.matchMode, gui.title)) run.variables.remove(name);
if (gui.saveAs != null && !gui.saveAs.isBlank()) run.variables.remove(gui.saveAs);
} else if (action instanceof CaptureValueAction capture) {
for (String name : MacroCapturePattern.declaredNames(capture.matchMode, capture.pattern)) run.variables.remove(name);
if (capture.saveAs != null && !capture.saveAs.isBlank()) run.variables.remove(capture.saveAs);
}
}
public static void clearOwnCaptureOutput(MacroAction action) {
if (!(action instanceof MacroCaptureOutput output)) return;
String name = output.getSaveAs();
if (name != null && !name.isBlank()) MacroVariables.remove(name);
if (action instanceof WaitForSoundAction sound) sound.matchedSoundId = "";
if (action instanceof WaitForPacketAction packet) {
packet.matchedPacket = null;
packet.matchedDirection = "";
}
if (action instanceof WaitPacketMatchAction packet) {
packet.matchedPacket = null;
packet.matchedDirection = "";
}
}
public static void commitConditionOutput(MacroAction action, Minecraft mc) throws InterruptedException {
if (!(action instanceof MacroCaptureOutput output)) return;
String name = MacroVariableContext.cleanRootName(output.getSaveAs());
if (name.isEmpty()) return;
MacroValue value = MacroClientThread.callOnClientThread(mc, () -> conditionOutputValue(action, mc), null);
if (value != null) MacroVariables.setValue(name, value);
}
public static MacroValue conditionOutputValue(MacroAction action, Minecraft mc) {
if (action instanceof WaitForHealthAction healthAction && mc != null && mc.player != null) {
float health = mc.player.getHealth();
if (healthAction.below ? health >= healthAction.healthThreshold : health <= healthAction.healthThreshold) return null;
return MacroValue.structured(MacroValue.Kind.NUMBER, Float.toString(health), java.util.Map.of(
"health", MacroValue.number(health),
"max", MacroValue.number(mc.player.getMaxHealth()),
"absorption", MacroValue.number(mc.player.getAbsorptionAmount())
));
}
if (action instanceof WaitForEntityAction entityAction) {
net.minecraft.world.entity.Entity entity = entityAction.findMatchingEntity(mc);
if (entity == null) return null;
String id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString();
double distance = mc != null && mc.player != null ? mc.player.distanceTo(entity) : 0.0;
return MacroValue.structured(MacroValue.Kind.ENTITY, entity.getName().getString(), java.util.Map.of(
"name", MacroValue.text(entity.getName().getString()),
"id", MacroValue.identifier(id),
"uuid", MacroValue.text(entity.getStringUUID()),
"x", MacroValue.number(entity.getX()),
"y", MacroValue.number(entity.getY()),
"z", MacroValue.number(entity.getZ()),
"distance", MacroValue.number(distance)
));
}
if (action instanceof WaitForSoundAction sound) {
if (sound.matchedSoundId == null || sound.matchedSoundId.isBlank()) return null;
return MacroValue.structured(MacroValue.Kind.IDENTIFIER, sound.matchedSoundId, java.util.Map.of(
"id", MacroValue.identifier(sound.matchedSoundId),
"x", MacroValue.number(sound.matchedX),
"y", MacroValue.number(sound.matchedY),
"z", MacroValue.number(sound.matchedZ)
));
}
if (action instanceof WaitForPacketAction packet) {
return packetMacroValue(packet.matchedPacket, packet.matchedDirection);
}
if (action instanceof WaitPacketMatchAction packet) {
return packetMacroValue(packet.matchedPacket, packet.matchedDirection);
}
if (action instanceof WaitInventoryPredicateAction inventory) {
if (!inventory.matches(mc)) return null;
return inventoryMacroValue(mc, inventory.itemName, inventory.slot, inventory.condition.name());
}
if (action instanceof WaitForSlotChangeAction slotWait) {
if (slotWait.entries.isEmpty()) return inventoryMacroValue(mc, "", -1, "slot_change");
ItemTarget target = slotWait.entries.get(0).resolvedTarget().resolveTemplate(mc);
return target == null ? null : itemTargetMacroValue(mc, target);
}
if (action instanceof WaitPosAction || action instanceof WaitForTeleportAction
|| action instanceof WaitForPositionDeltaAction) {
return playerPositionMacroValue(mc);
}
if (action instanceof WaitForWorldChangeAction && mc != null && mc.level != null) {
String dimension = mc.level.dimension().identifier().toString();
return MacroValue.structured(MacroValue.Kind.IDENTIFIER, dimension, java.util.Map.of(
"id", MacroValue.identifier(dimension), "name", MacroValue.text(dimension)));
}
if (action instanceof WaitGamemodeChangeAction && mc != null && mc.gameMode != null) {
String mode = mc.gameMode.getPlayerMode().getName();
WaitGamemodeChangeAction wait = (WaitGamemodeChangeAction) action;
if (wait.match == WaitGamemodeChangeAction.Match.TO_MODE && !wait.accepts(mc.gameMode.getPlayerMode())) return null;
return MacroValue.structured(MacroValue.Kind.TEXT, mode, java.util.Map.of("name", MacroValue.text(mode)));
}
return null;
}
private static MacroValue playerPositionMacroValue(Minecraft mc) {
if (mc == null || mc.player == null) return null;
String display = String.format(Locale.ROOT, "%.3f, %.3f, %.3f", mc.player.getX(), mc.player.getY(), mc.player.getZ());
return MacroValue.structured(MacroValue.Kind.POSITION, display, java.util.Map.of(
"x", MacroValue.number(mc.player.getX()), "y", MacroValue.number(mc.player.getY()),
"z", MacroValue.number(mc.player.getZ()), "yaw", MacroValue.number(mc.player.getYRot()),
"pitch", MacroValue.number(mc.player.getXRot())
));
}
private static MacroValue inventoryMacroValue(Minecraft mc, String itemTemplate, int slot, String condition) {
if (mc == null || mc.player == null) return null;
MacroTemplate.Resolution item = MacroVariables.resolve(itemTemplate, mc);
if (!item.success()) return null;
ItemTarget target = ItemTarget.fromLegacyEntry(item.value());
if (slot >= 0) {
int safeSlot = Math.max(0, Math.min(mc.player.getInventory().getContainerSize() - 1, slot));
MacroValue stack = stackMacroValue(mc.player.getInventory().getItem(safeSlot), safeSlot);
return stack == null ? MacroValue.structured(MacroValue.Kind.SLOT, Integer.toString(slot), java.util.Map.of(
"slot", MacroValue.slot(slot), "empty", MacroValue.text("true"), "condition", MacroValue.text(condition))) : stack;
}
if (target.hasIdentity()) {
MacroValue matched = itemTargetMacroValue(mc, target);
if (matched != null) return matched.withProperty("condition", MacroValue.text(condition));
}
return MacroValue.structured(MacroValue.Kind.TEXT, condition, java.util.Map.of(
"condition", MacroValue.text(condition),
"selected_slot", MacroValue.slot(mc.player.getInventory().getSelectedSlot())
));
}
private static MacroValue itemTargetMacroValue(Minecraft mc, ItemTarget target) {
if (mc == null || mc.player == null || target == null) return null;
if (mc.player.containerMenu != null) {
for (Slot slot : mc.player.containerMenu.slots) {
if (slot == null || slot.getItem().isEmpty()) continue;
int visible = AutismInventoryHelper.toUserVisibleSlot(mc, slot.index);
if (target.score(slot.getItem(), visible) >= 0) return stackMacroValue(slot.getItem(), visible);
}
}
return null;
}
private static MacroValue stackMacroValue(ItemStack stack, int slot) {
if (stack == null || stack.isEmpty()) return null;
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
String itemId = id == null ? "" : id.toString();
String name = stack.getHoverName().getString();
return MacroValue.structured(MacroValue.Kind.ITEM, name, java.util.Map.of(
"name", MacroValue.text(name), "id", MacroValue.identifier(itemId),
"count", MacroValue.number(stack.getCount()), "slot", MacroValue.slot(slot)
));
}
private static MacroValue packetMacroValue(Packet<?> packet, String direction) {
if (packet == null) return null;
java.util.LinkedHashMap<String, MacroValue> properties = new java.util.LinkedHashMap<>();
String packetName = AutismPacketNamer.getFriendlyName(packet, direction);
properties.put("name", MacroValue.text(packetName));
properties.put("class", MacroValue.text(packet.getClass().getName()));
properties.put("direction", MacroValue.text(direction));
RecordComponent[] components = packet.getClass().getRecordComponents();
if (components != null) {
for (RecordComponent component : components) {
if (properties.size() >= 64) break;
try {
Object value = component.getAccessor().invoke(packet);
properties.put(component.getName().toLowerCase(Locale.ROOT), MacroValue.text(value));
} catch (ReflectiveOperationException ignored) { }
}
}
return MacroValue.structured(MacroValue.Kind.PACKET, packetName, properties);
}
public static void stageCapture(MacroAction action, java.util.Map<String, MacroValue> values) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null || values == null) return;
run.stagedCaptures.put(action, java.util.Map.copyOf(values));
}
static void commitStagedCapture(MacroAction action) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run == null || action == null) return;
java.util.Map<String, MacroValue> values = run.stagedCaptures.remove(action);
if (values != null) run.variables.setAll(values);
}
private static MacroExecutor.PrearmedWait predicatePrearm(MacroAction action, java.util.function.BooleanSupplier predicate, int timeoutMs) {
java.util.concurrent.atomic.AtomicBoolean active = new java.util.concurrent.atomic.AtomicBoolean(true);
CompletableFuture<Void> future = new CompletableFuture<>();
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
Thread worker = new Thread(() -> MacroLifecycle.withRunContext(run, () -> {
long start = System.currentTimeMillis();
while (active.get() && MacroLifecycle.isCurrentRunActive() && !future.isDone()) {
try {
if (predicate.getAsBoolean()) {
future.complete(null);
return;
}
} catch (Throwable ignored) { }
if (timeoutMs > 0 && System.currentTimeMillis() - start >= timeoutMs) {
future.complete(null);
return;
}
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) return;
}
}), "MacroPrearmedWait");
worker.setDaemon(true);
worker.start();
return new MacroExecutor.PrearmedWait(action, future, () -> {
active.set(false);
if (!future.isDone()) future.cancel(true);
worker.interrupt();
}, 0);
}
private static MacroExecutor.PrearmedWait packetTargetFuture(MacroAction action, String target, int completedPacketTargets) {
String normalizedTarget = WaitForPacketAction.normalizeTarget(target);
String direction = WaitForPacketAction.getDirection(normalizedTarget);
CompletableFuture<Packet<?>> matched = new CompletableFuture<>();
MacroExecutor.OneShotPacketListener send = null;
MacroExecutor.OneShotPacketListener recv = null;
if (!"S2C".equalsIgnoreCase(direction)) {
send = MacroExecutor.awaitSend(packet -> MacroPacketWait.matchesPacketTarget(normalizedTarget, packet, "C2S"));
send.future.thenAccept(packet -> {
recordMatchedPacket(action, packet, "C2S");
matched.complete(packet);
});
}
if (!"C2S".equalsIgnoreCase(direction)) {
recv = MacroExecutor.awaitReceive(packet -> MacroPacketWait.matchesPacketTarget(normalizedTarget, packet, "S2C"));
recv.future.thenAccept(packet -> {
recordMatchedPacket(action, packet, "S2C");
matched.complete(packet);
});
}
MacroExecutor.OneShotPacketListener finalSend = send;
MacroExecutor.OneShotPacketListener finalRecv = recv;
return new MacroExecutor.PrearmedWait(action, matched, () -> {
if (finalSend != null) finalSend.cancel();
if (finalRecv != null) finalRecv.cancel();
if (!matched.isDone()) matched.cancel(true);
}, completedPacketTargets);
}
private static MacroExecutor.PrearmedWait packetMatchFuture(WaitPacketMatchAction action) {
MacroExecutor.OneShotPacketListener send = null;
MacroExecutor.OneShotPacketListener recv = null;
CompletableFuture<Packet<?>> matched = new CompletableFuture<>();
if (action.direction != WaitPacketMatchAction.Direction.S2C) {
send = MacroExecutor.awaitSend(packet -> action.matches(packet, "C2S"));
send.future.thenAccept(packet -> {
recordMatchedPacket(action, packet, "C2S");
matched.complete(packet);
});
}
if (action.direction != WaitPacketMatchAction.Direction.C2S) {
recv = MacroExecutor.awaitReceive(packet -> action.matches(packet, "S2C"));
recv.future.thenAccept(packet -> {
recordMatchedPacket(action, packet, "S2C");
matched.complete(packet);
});
}
if (send == null && recv == null) return null;
if (action.timeoutMs > 0) matched.completeOnTimeout(null, action.timeoutMs, TimeUnit.MILLISECONDS);
MacroExecutor.OneShotPacketListener finalSend = send;
MacroExecutor.OneShotPacketListener finalRecv = recv;
return new MacroExecutor.PrearmedWait(action, matched, () -> {
if (finalSend != null) finalSend.cancel();
if (finalRecv != null) finalRecv.cancel();
if (!matched.isDone()) matched.cancel(true);
}, 0);
}
public static void waitForPacketMatch(WaitPacketMatchAction action) {
if (action == null) return;
MacroExecutor.OneShotPacketListener send = null;
MacroExecutor.OneShotPacketListener recv = null;
try {
if (action.direction != WaitPacketMatchAction.Direction.S2C) {
send = MacroExecutor.awaitSend(packet -> {
boolean matches = action.matches(packet, "C2S");
if (matches) recordMatchedPacket(action, packet, "C2S");
return matches;
});
}
if (action.direction != WaitPacketMatchAction.Direction.C2S) {
recv = MacroExecutor.awaitReceive(packet -> {
boolean matches = action.matches(packet, "S2C");
if (matches) recordMatchedPacket(action, packet, "S2C");
return matches;
});
}
CompletableFuture<?> future;
if (send != null && recv != null) future = CompletableFuture.anyOf(send.future, recv.future);
else if (send != null) future = send.future;
else if (recv != null) future = recv.future;
else return;
if (action.timeoutMs > 0) future.completeOnTimeout(null, action.timeoutMs, TimeUnit.MILLISECONDS);
MacroRace.waitForFutureDone(future, action.timeoutMs);
} finally {
if (send != null) send.cancel();
if (recv != null) recv.cancel();
}
}
}

View file

@ -0,0 +1,926 @@
package autismclient.util.macro;
import autismclient.modules.AutismModule;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismMacro;
import autismclient.util.AutismSharedState;
import autismclient.util.macro.MacroActionResources.SharedClientResource;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket;
import net.minecraft.network.protocol.game.ServerboundContainerClickPacket;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
final class MacroRace {
private MacroRace() {
}
public static void runPacketGateAction(Minecraft mc, PacketGateAction action) {
if (action == null) return;
action.execute(mc);
if (action.mode == PacketGateAction.GateMode.DISABLE_GATE) return;
switch (action.durationMode) {
case UNTIL_PACKET -> {
WaitPacketMatchAction wait = new WaitPacketMatchAction();
wait.direction = switch (action.direction) {
case C2S -> WaitPacketMatchAction.Direction.C2S;
case S2C -> WaitPacketMatchAction.Direction.S2C;
case ANY -> WaitPacketMatchAction.Direction.ANY;
};
wait.packetName = action.untilPacketName == null || action.untilPacketName.isBlank()
? (action.packetNames.isEmpty() ? "" : action.packetNames.get(0))
: action.untilPacketName;
wait.fieldName = action.untilPacketField;
wait.operator = MacroStringList.enumValue(WaitPacketMatchAction.Operator.class, action.untilPacketOperator, WaitPacketMatchAction.Operator.EXISTS);
wait.value = action.untilPacketValue;
wait.timeoutMs = Math.max(0, action.durationValue);
MacroPrearm.waitForPacketMatch(wait);
disablePacketGate(mc, action);
}
case UNTIL_GUI -> {
MacroRunLoop.waitForPredicate(() -> MacroGuiMatcher.matches(mc.gui.screen(), action.untilGuiType, action.untilGuiTitle), Math.max(0, action.durationValue));
disablePacketGate(mc, action);
}
case UNTIL_INVENTORY -> {
WaitInventoryPredicateAction wait = new WaitInventoryPredicateAction();
wait.condition = MacroStringList.enumValue(WaitInventoryPredicateAction.InventoryCondition.class, action.untilInventoryCondition, WaitInventoryPredicateAction.InventoryCondition.ITEM_EXISTS);
wait.itemName = action.untilInventoryItem;
wait.count = action.untilInventoryCount;
wait.slot = action.untilInventorySlot;
wait.timeoutMs = Math.max(0, action.durationValue);
wait.resetBaseline();
MacroRunLoop.waitForPredicate(() -> wait.matches(mc), wait.timeoutMs);
disablePacketGate(mc, action);
}
default -> {}
}
}
private static void disablePacketGate(Minecraft mc, PacketGateAction action) {
PacketGateManager.disableAndFlushConfigured(
action.gateId == null || action.gateId.isBlank() ? "auto" : action.gateId,
MacroExecutor.currentRunId(),
mc == null ? null : mc.getConnection(),
action.flushOnDisable);
}
public static void runFinallyActions(MacroExecutor.RunState run, Minecraft mc) {
if (run == null || run.finallyActions.isEmpty()) return;
ArrayList<MacroAction> actions = new ArrayList<>(run.finallyActions);
run.finallyActions.clear();
for (MacroAction action : actions) {
if (action == null || !action.isEnabled()) continue;
try {
MacroDispatcher.executeSingleActionWithWaits(mc, action, AutismModule.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Throwable t) {
AutismClientMessaging.sendPrefixed("\u00a7cCleanup failed: " + safeThrowableMessage(t));
}
}
}
public static void waitForMacroStep(WaitForMacroStepAction action) {
long startMs = System.currentTimeMillis();
while (MacroLifecycle.isCurrentRunActive()) {
if (isMacroStepSatisfied(action)) return;
if (action.timeoutMs > 0 && System.currentTimeMillis() - startMs >= action.timeoutMs) return;
CompletableFuture<Void> tick = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tick);
}
}
static boolean isMacroStepSatisfied(WaitForMacroStepAction action) {
String target = action.macroName == null ? "" : action.macroName.trim();
if (target.isEmpty()) return true;
synchronized (MacroExecutor.RUN_LOCK) {
for (MacroExecutor.RunState run : MacroExecutor.ACTIVE_RUNS.values()) {
String name = run.macro != null && run.macro.name != null ? run.macro.name : "";
if (!name.equalsIgnoreCase(target)) continue;
return switch (action.mode) {
case STARTED_STEP -> run.currentStepIndex + 1 >= Math.max(1, action.step);
case COMPLETED_STEP -> run.lastCompletedStep >= Math.max(1, action.step);
case FINISHED -> !run.running.get();
};
}
}
return action.mode == WaitForMacroStepAction.WaitMode.FINISHED;
}
public static int runRaceGroup(Minecraft mc, AutismModule module, java.util.List<MacroAction> actions, int headerIndex, RaceAction race) throws InterruptedException {
int bodyCount = race.normalizedBodyCount(actions, headerIndex);
java.util.List<MacroAction> conditions = new ArrayList<>();
java.util.List<MacroAction> bodyActions = new ArrayList<>();
for (int offset = 1; offset <= bodyCount && headerIndex + offset < actions.size(); offset++) {
MacroAction child = actions.get(headerIndex + offset);
if (child == null || !child.isEnabled()) continue;
if (RaceAction.isConditionAction(child)) conditions.add(child);
else if (RaceAction.isBodyAction(child)) bodyActions.add(child);
}
MacroLifecycle.setCurrentStatus(conditions.isEmpty()
? "Race: dispatching"
: "Race: waiting for " + (conditions.size() == 1 ? conditions.get(0).getDisplayName() : conditions.size() + " conditions"));
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
CompletableFuture<Void> bodyFuture = new CompletableFuture<>();
AtomicBoolean dispatched = new AtomicBoolean(false);
CompletableFuture<?> triggerFuture = raceTriggerBarrier(mc, conditions);
triggerFuture.thenRun(() -> {
if (!dispatched.compareAndSet(false, true)) return;
if (run != null && !run.running.get()) {
bodyFuture.cancel(true);
return;
}
dispatchRaceBody(mc, module, bodyActions, run, bodyFuture);
}).exceptionally(error -> {
if (!bodyFuture.isDone()) bodyFuture.completeExceptionally(error);
return null;
});
boolean fired = waitForTimedFuture(triggerFuture, Math.max(0, race.timeoutMs));
if (!fired) {
triggerFuture.cancel(true);
bodyFuture.cancel(true);
AutismClientMessaging.sendPrefixed("\u00A7eRace timed out: " + (race.label == null || race.label.isBlank() ? "Race" : race.label));
MacroLifecycle.setCurrentStatus("Race: timeout");
return bodyCount;
}
MacroLifecycle.setCurrentStatus("Race: fired");
waitForTimedFuture(bodyFuture, 250);
return bodyCount;
}
private static CompletableFuture<?> raceTriggerBarrier(Minecraft mc, java.util.List<MacroAction> conditions) {
if (conditions == null || conditions.isEmpty()) return CompletableFuture.completedFuture(null);
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
java.util.List<CompletableFuture<MacroExecutor.MacroEvent>> baseFutures = new ArrayList<>();
java.util.List<CompletableFuture<MacroExecutor.MacroEvent>> futures = new ArrayList<>();
for (MacroAction condition : conditions) {
MacroPrearm.clearOwnCaptureOutput(condition);
CompletableFuture<MacroExecutor.MacroEvent> base = timedConditionFuture(mc, condition);
baseFutures.add(base);
futures.add(base.thenApply(event -> {
if (event.packet() != null) MacroPrearm.recordMatchedPacket(condition, event.packet(), event.direction());
if (!(condition instanceof MacroCaptureOutput output)) return event;
String name = MacroVariableContext.cleanRootName(output.getSaveAs());
if (name.isEmpty()) return event;
MacroValue value = MacroPrearm.conditionOutputValue(condition, mc);
if (value == null) return event;
java.util.LinkedHashMap<String, MacroValue> captures = new java.util.LinkedHashMap<>(event.captures());
captures.put(name, value);
return new MacroExecutor.MacroEvent(event.packet(), event.direction(), event.nanoTime(), event.chatCapture(), captures);
}));
}
CompletableFuture<Object> first = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]));
CompletableFuture<Object> out = new CompletableFuture<>();
first.whenComplete((value, e) -> {
if (e != null) {
out.completeExceptionally(e);
return;
}
if (value instanceof MacroExecutor.MacroEvent event && run != null && !event.captures().isEmpty()) {
run.variables.setAll(event.captures());
}
out.complete(value);
});
out.whenComplete((v, e) -> {
for (CompletableFuture<MacroExecutor.MacroEvent> future : baseFutures) {
if (!future.isDone()) future.cancel(true);
}
});
return out;
}
public static int runReportGroup(Minecraft mc, java.util.List<MacroAction> actions, int headerIndex, ReportAction report) throws InterruptedException {
int conditionCount = report.normalizedConditionCount(actions, headerIndex);
if (conditionCount < 2 || headerIndex + 2 >= actions.size()) {
MacroLifecycle.setCurrentStatus(report.getDisplayName());
report.execute(mc);
return conditionCount;
}
MacroAction startCondition = actions.get(headerIndex + 1);
MacroAction endCondition = actions.get(headerIndex + 2);
if (RaceAction.isBodyAction(startCondition) && RaceAction.isConditionAction(endCondition)) {
int timeoutMs = Math.max(100, report.timeoutMs);
String label = report.reportLabel == null || report.reportLabel.isBlank() ? "Report" : report.reportLabel.trim();
MacroLifecycle.setCurrentStatus("Report: start " + startCondition.getDisplayName());
CompletableFuture<MacroExecutor.MacroEvent> startAnchor = startActionAnchorFuture(startCondition);
MacroDispatcher.executeSingleActionWithWaits(mc, startCondition, null);
if (!MacroLifecycle.isCurrentRunActive()) return conditionCount;
MacroExecutor.MacroEvent startEvent = waitForAnchorOrNow(startAnchor, 250, startCondition.getDisplayName(), "client action");
MacroLifecycle.setCurrentStatus("Report: waiting end " + endCondition.getDisplayName());
CompletableFuture<MacroExecutor.MacroEvent> endFuture = timedConditionFuture(mc, endCondition);
MacroExecutor.MacroEvent endEvent = waitForTimedEvent(endFuture, timeoutMs);
if (endEvent == null) {
endFuture.cancel(true);
AutismClientMessaging.sendPrefixed("\u00A7eReport \"" + label + "\": timeout waiting for end condition");
return conditionCount;
}
long deltaNanos = Math.max(0L, endEvent.nanoTime() - startEvent.nanoTime());
long deltaMs = deltaNanos / 1_000_000L;
String start = startEvent.label();
String end = endEvent.label();
AutismClientMessaging.sendPrefixed("\u00A7b" + label + ": " + formatNanos(deltaNanos) + " (" + start + " -> " + end + ")");
if (report.stashToSharedState) {
AutismSharedState.get().pushReport(new AutismSharedState.ReportSample(label, deltaMs, start, end, System.currentTimeMillis()));
}
return conditionCount;
}
if (!RaceAction.isConditionAction(startCondition) || !RaceAction.isConditionAction(endCondition)) {
report.execute(mc);
return conditionCount;
}
int timeoutMs = Math.max(100, report.timeoutMs);
long startWaitNanos = System.nanoTime();
MacroLifecycle.setCurrentStatus("Report: waiting start " + startCondition.getDisplayName());
boolean startFired = waitForMacroCondition(mc, startCondition, timeoutMs);
if (!startFired) {
AutismClientMessaging.sendPrefixed("\u00A7eReport \"" + report.reportLabel + "\": timeout waiting for start condition");
return conditionCount;
}
MacroExecutor.MacroEvent startEvent = new MacroExecutor.MacroEvent(null, "condition " + startCondition.getDisplayName(), System.nanoTime(), null);
long spentMs = (startEvent.nanoTime() - startWaitNanos) / 1_000_000L;
int remainingMs = (int) Math.max(1, timeoutMs - spentMs);
MacroLifecycle.setCurrentStatus("Report: waiting end " + endCondition.getDisplayName());
CompletableFuture<MacroExecutor.MacroEvent> endFuture = timedConditionFuture(mc, endCondition);
MacroExecutor.MacroEvent endEvent = waitForTimedEvent(endFuture, remainingMs);
if (endEvent == null) {
endFuture.cancel(true);
AutismClientMessaging.sendPrefixed("\u00A7eReport \"" + report.reportLabel + "\": timeout waiting for end condition");
return conditionCount;
}
long deltaNanos = Math.max(0L, endEvent.nanoTime() - startEvent.nanoTime());
long deltaMs = deltaNanos / 1_000_000L;
String label = report.reportLabel == null || report.reportLabel.isBlank() ? "Report" : report.reportLabel.trim();
String start = startEvent.label();
String end = endEvent.label();
AutismClientMessaging.sendPrefixed("\u00A7b" + label + ": " + formatNanos(deltaNanos) + " (" + start + " -> " + end + ")");
if (report.stashToSharedState) {
AutismSharedState.get().pushReport(new AutismSharedState.ReportSample(label, deltaMs, start, end, System.currentTimeMillis()));
}
return conditionCount;
}
private static boolean waitForRaceTrigger(Minecraft mc, RaceAction race) throws InterruptedException {
MacroAction trigger = race.triggerAction == null ? new DelayAction(0) : race.triggerAction;
int timeoutMs = Math.max(0, race.timeoutMs);
return waitForMacroCondition(mc, trigger, timeoutMs);
}
public static int runMultiGroup(Minecraft mc, AutismModule module, String macroName, java.util.List<MacroAction> actions, int headerIndex, MultiAction multi) throws InterruptedException {
if (autismclient.util.AutismLiteVariant.enabled()) return 0;
int stepCount = multi.normalizedStepCount(actions, headerIndex);
if (stepCount <= 0) return 0;
autismclient.util.multi.MultiManager manager = autismclient.util.multi.MultiManager.get();
autismclient.util.multi.MultiProfile profile = manager.isActive()
? manager.activeProfile()
: autismclient.util.multi.MultiProfileManager.get().find(autismclient.util.multi.MultiProfileManager.get().selectedId());
if (profile == null) {
AutismClientMessaging.sendPrefixed("\u00A7eMulti: no Multi profile to start");
return stepCount;
}
java.util.LinkedHashSet<String> scope = new java.util.LinkedHashSet<>();
java.util.Set<String> profileIds = new java.util.HashSet<>();
for (autismclient.util.multi.MultiProfile.SessionSpec spec : profile.sessions) {
if (spec != null && spec.accountId() != null && !spec.accountId().isBlank()) profileIds.add(spec.accountId());
}
for (String accountId : multi.effectiveAccountIds()) {
if (profileIds.contains(accountId)) scope.add(accountId);
}
if (scope.isEmpty()) {
AutismClientMessaging.sendPrefixed("\u00A7eMulti: no selected accounts in the active Multi profile");
return stepCount;
}
if (multi.connectIfDown) {
if (!manager.isActive()) {
MacroLifecycle.setCurrentStatus("Multi: starting batch");
autismclient.util.multi.MultiManager.StartResult started = manager.start(profile);
if (!started.ok()) {
AutismClientMessaging.sendPrefixed("\u00A7eMulti: could not start the Multi batch");
return stepCount;
}
} else {
java.util.Map<String, autismclient.util.multi.MultiSession.Snapshot> byId = multiSnapshotsById(manager);
for (String accountId : scope) {
autismclient.util.multi.MultiSession.Snapshot snapshot = byId.get(accountId);
if (snapshot == null || autismclient.util.multi.MultiManager.isRetryable(snapshot.status())) {
manager.retry(accountId);
}
}
}
}
if (!MacroLifecycle.isCurrentRunActive()) return stepCount;
int waitMs = Math.max(0, multi.waitMs);
if (MultiAction.WAIT_UNTIL_READY.equals(multi.waitMode)) {
MacroLifecycle.setCurrentStatus("Multi: waiting for " + scope.size() + (scope.size() == 1 ? " account" : " accounts"));
multiWaitUntilReady(manager, scope, waitMs);
} else if (MultiAction.WAIT_FIXED_DELAY.equals(multi.waitMode) && waitMs > 0) {
MacroLifecycle.setCurrentStatus("Multi: delay " + waitMs + " ms");
multiSleepCancellable(waitMs);
}
if (!MacroLifecycle.isCurrentRunActive()) return stepCount;
AutismMacro subMacro = new AutismMacro();
subMacro.name = (macroName == null || macroName.isBlank() ? "Macro" : macroName) + " (Multi)";
for (int offset = 1; offset <= stepCount && headerIndex + offset < actions.size(); offset++) {
MacroAction child = actions.get(headerIndex + offset);
if (child == null) continue;
MacroAction copy = AutismMacro.createActionFromTag(AutismMacro.serializeAction(child));
if (copy != null) subMacro.actions.add(copy);
}
MacroLifecycle.setCurrentStatus("Multi: running on " + scope.size() + (scope.size() == 1 ? " account" : " accounts"));
autismclient.util.multi.MultiManager.BroadcastResult result = manager.runMacroDirect(subMacro, scope);
AutismClientMessaging.sendPrefixed("\u00A7bMulti: " + result.summary());
MacroLifecycle.setCurrentStatus("Multi: waiting for completion");
multiWaitForCompletion(manager, scope, subMacro.name);
if (!MacroLifecycle.isCurrentRunActive()) return stepCount;
if (multi.disconnectAfter) {
for (String accountId : scope) {
if (manager.hasQueuedMacroIntent(accountId, subMacro.name)) continue;
manager.disconnectSession(accountId);
}
}
return stepCount;
}
private static java.util.Map<String, autismclient.util.multi.MultiSession.Snapshot> multiSnapshotsById(autismclient.util.multi.MultiManager manager) {
java.util.Map<String, autismclient.util.multi.MultiSession.Snapshot> byId = new java.util.HashMap<>();
for (autismclient.util.multi.MultiSession.Snapshot snapshot : manager.snapshots()) {
if (snapshot != null) byId.put(snapshot.accountId(), snapshot);
}
return byId;
}
private static void multiSleepCancellable(long totalMs) throws InterruptedException {
long deadline = System.nanoTime() + Math.max(0L, totalMs) * 1_000_000L;
long remaining;
while ((remaining = deadline - System.nanoTime()) > 0L) {
if (!MacroLifecycle.isCurrentRunActive()) return;
Thread.sleep(Math.min(100L, Math.max(1L, remaining / 1_000_000L)));
}
}
private static void multiWaitUntilReady(autismclient.util.multi.MultiManager manager, java.util.Set<String> scope, long timeoutMs) throws InterruptedException {
long deadline = System.nanoTime() + Math.max(0L, timeoutMs) * 1_000_000L;
while (MacroLifecycle.isCurrentRunActive()) {
boolean allReady = true;
java.util.Map<String, autismclient.util.multi.MultiSession.Snapshot> byId = multiSnapshotsById(manager);
for (String accountId : scope) {
autismclient.util.multi.MultiSession.Snapshot snapshot = byId.get(accountId);
if (snapshot == null || !snapshot.ready()) {
allReady = false;
break;
}
}
if (allReady) return;
if (System.nanoTime() >= deadline) {
AutismClientMessaging.sendPrefixed("\u00A7eMulti: timed out waiting for accounts to be ready");
return;
}
Thread.sleep(150L);
}
}
private static void multiWaitForCompletion(autismclient.util.multi.MultiManager manager, java.util.Set<String> scope, String subMacroName) throws InterruptedException {
long deadline = System.nanoTime() + 10L * 60L * 1_000_000_000L;
Thread.sleep(200L);
while (MacroLifecycle.isCurrentRunActive()) {
boolean anyPending = false;
java.util.Map<String, autismclient.util.multi.MultiSession.Snapshot> byId = multiSnapshotsById(manager);
for (String accountId : scope) {
if (manager.hasQueuedMacroIntent(accountId, subMacroName)) {
anyPending = true;
break;
}
autismclient.util.multi.MultiSession.Snapshot snapshot = byId.get(accountId);
autismclient.util.multi.MultiSession.MacroProgress progress = snapshot == null ? null : snapshot.macroProgress();
if (progress != null && progress.running()) {
anyPending = true;
break;
}
}
if (!anyPending) return;
if (System.nanoTime() >= deadline) {
AutismClientMessaging.sendPrefixed("\u00A7eMulti: timed out waiting for " + scope.size()
+ (scope.size() == 1 ? " account" : " accounts"));
return;
}
Thread.sleep(200L);
}
}
public static boolean waitForMacroCondition(Minecraft mc, MacroAction trigger, int timeoutMs) throws InterruptedException {
if (trigger == null) return true;
CompletableFuture<?> future = raceConditionFuture(mc, trigger);
if (future == null) {
MacroDispatcher.executeSingleActionWithWaits(mc, trigger, null);
return true;
}
boolean fired = waitForFutureDone(future, timeoutMs);
if (!fired) future.cancel(true);
return fired;
}
private static void dispatchRaceBody(Minecraft mc, AutismModule module, java.util.List<MacroAction> bodyActions,
MacroExecutor.RunState run, CompletableFuture<Void> bodyFuture) {
if (bodyActions == null || bodyActions.isEmpty()) {
bodyFuture.complete(null);
return;
}
Runnable task = () -> {
MacroExecutor.RunState previous = MacroExecutor.CURRENT_RUN.get();
if (run != null) MacroExecutor.CURRENT_RUN.set(run);
EnumSet<SharedClientResource> heldResources = EnumSet.noneOf(SharedClientResource.class);
try {
heldResources = MacroLifecycle.acquireClientResources(MacroLifecycle.resourcesForActions(bodyActions));
Throwable firstFailure = null;
for (MacroAction body : bodyActions) {
if (body == null || !body.isEnabled() || !RaceAction.isBodyAction(body)) continue;
if (run != null && !run.running.get()) {
bodyFuture.cancel(true);
return;
}
try {
executeRaceBodyActionFast(mc, body);
} catch (InterruptedException e) {
throw e;
} catch (Throwable t) {
if (firstFailure == null) firstFailure = t;
AutismClientMessaging.sendPrefixed("\u00A7cRace action failed (" + body.getDisplayName() + "): " + safeThrowableMessage(t));
}
}
if (firstFailure == null) bodyFuture.complete(null);
else bodyFuture.completeExceptionally(firstFailure);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
bodyFuture.cancel(true);
} catch (Throwable t) {
AutismClientMessaging.sendPrefixed("\u00A7cRace action failed: " + safeThrowableMessage(t));
bodyFuture.completeExceptionally(t);
} finally {
MacroLifecycle.releaseClientResources(heldResources);
if (previous != null) MacroExecutor.CURRENT_RUN.set(previous);
else MacroExecutor.CURRENT_RUN.remove();
}
};
if (bodyActions.stream().allMatch(MacroRace::isRaceDirectPacketAction)) {
task.run();
} else {
mc.execute(task);
}
}
private static void executeRaceBodyActionFast(Minecraft mc, MacroAction body) throws InterruptedException {
if (body == null || !body.isEnabled() || !RaceAction.isBodyAction(body)) return;
if (!MacroDynamicBindings.apply(body, mc)) return;
body.execute(mc);
}
private static boolean isRaceDirectPacketAction(MacroAction action) {
if (action instanceof SendChatAction chat) return !chat.isWaitForGuiBefore() && !chat.isWaitForGuiAfter();
if (action instanceof SendPacketAction sendPacket) return !sendPacket.isWaitForGuiBefore() && !sendPacket.isWaitForGuiAfter();
return action instanceof PacketClickAction;
}
private static CompletableFuture<MacroExecutor.MacroEvent> startActionAnchorFuture(MacroAction action) {
if (action instanceof PacketClickAction) {
return MacroPacketObserve.awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future;
}
if (action instanceof ItemAction) {
return MacroPacketObserve.awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future;
}
if (action instanceof CloseGuiAction) {
return MacroPacketObserve.awaitTimedSend(packet -> packet instanceof net.minecraft.network.protocol.game.ServerboundContainerClosePacket).future;
}
if (action instanceof SendChatAction) {
return MacroPacketObserve.awaitTimedSend(packet -> packet instanceof net.minecraft.network.protocol.game.ServerboundChatPacket
|| packet instanceof net.minecraft.network.protocol.game.ServerboundChatCommandPacket
|| packet instanceof net.minecraft.network.protocol.game.ServerboundChatCommandSignedPacket).future;
}
if (action instanceof SendPacketAction || action instanceof PayloadAction) {
return MacroPacketObserve.awaitTimedSend(packet -> true).future;
}
return null;
}
private static MacroExecutor.MacroEvent waitForAnchorOrNow(CompletableFuture<MacroExecutor.MacroEvent> anchor, int timeoutMs, String label, String fallbackDirection) {
if (anchor != null) {
MacroExecutor.MacroEvent event = waitForTimedEvent(anchor, timeoutMs);
if (event != null) return event;
anchor.cancel(true);
}
return new MacroExecutor.MacroEvent(null, fallbackDirection + " " + (label == null ? "" : label), System.nanoTime(), null);
}
private static MacroExecutor.MacroEvent waitForTimedEvent(CompletableFuture<MacroExecutor.MacroEvent> future, long timeoutMs) {
if (future == null) return null;
try {
if (timeoutMs <= 0) return future.get();
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.cancel(true);
return null;
} catch (ExecutionException | TimeoutException | CancellationException e) {
return null;
}
}
private static boolean waitForTimedFuture(CompletableFuture<?> future, long timeoutMs) {
if (future == null) return true;
try {
if (timeoutMs <= 0) future.get();
else future.get(timeoutMs, TimeUnit.MILLISECONDS);
return !future.isCancelled() && MacroLifecycle.isCurrentRunActive();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.cancel(true);
return false;
} catch (ExecutionException | TimeoutException | CancellationException e) {
return false;
}
}
private static String formatNanos(long nanos) {
return String.format(Locale.ROOT, "%.3f ms", nanos / 1_000_000.0);
}
public static String safeThrowableMessage(Throwable throwable) {
if (throwable == null) return "unknown error";
String message = throwable.getMessage();
return message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message;
}
private static boolean isWorldTransitionPacket(Packet<?> packet) {
return packet instanceof net.minecraft.network.protocol.game.ClientboundRespawnPacket
|| packet instanceof net.minecraft.network.protocol.game.ClientboundLoginPacket;
}
private static CompletableFuture<MacroExecutor.MacroEvent> timedConditionFuture(Minecraft mc, MacroAction trigger) {
if (trigger == null) {
return CompletableFuture.completedFuture(new MacroExecutor.MacroEvent(null, "condition", System.nanoTime(), null));
}
if (trigger instanceof WaitForChatAction chat) {
MacroVariableContext context = MacroLifecycle.currentVariableContext();
java.util.concurrent.atomic.AtomicReference<java.util.Map<String, MacroValue>> captures =
new java.util.concurrent.atomic.AtomicReference<>(java.util.Map.of());
MacroPacketObserve.TimedOneShotPacketListener listener = MacroPacketObserve.awaitTimedReceive((packet, direction, now) -> {
MacroExecutor.ChatCapture capture = MacroChatObserve.extractIncomingChat(packet);
var matched = MacroChatObserve.captureChatAction(chat, capture, context);
matched.ifPresent(captures::set);
return matched.isPresent();
});
CompletableFuture<MacroExecutor.MacroEvent> out = new CompletableFuture<>();
listener.future.whenComplete((event, e) -> {
if (e != null) out.completeExceptionally(e);
else out.complete(new MacroExecutor.MacroEvent(event.packet(), event.direction(), event.nanoTime(),
MacroChatObserve.extractIncomingChat(event.packet()), captures.get()));
});
out.whenComplete((v, e) -> listener.cancel());
return out;
}
if (trigger instanceof WaitForPacketAction packetAction) {
java.util.List<String> targets = packetAction.effectiveList();
CompletableFuture<MacroExecutor.MacroEvent> matched = new CompletableFuture<>();
MacroPacketObserve.TimedOneShotPacketListener recv = MacroPacketObserve.awaitTimedReceive((packet, direction, now) -> {
if (targets.isEmpty()) return true;
for (String target : targets) {
if (MacroPacketWait.matchesPacketTarget(target, packet, "S2C")) return true;
}
return false;
});
MacroPacketObserve.TimedOneShotPacketListener send = MacroPacketObserve.awaitTimedSend((packet, direction, now) -> {
if (targets.isEmpty()) return true;
for (String target : targets) {
if (MacroPacketWait.matchesPacketTarget(target, packet, "C2S")) return true;
}
return false;
});
recv.future.thenAccept(matched::complete);
send.future.thenAccept(matched::complete);
matched.whenComplete((v, e) -> {
recv.cancel();
send.cancel();
});
return matched;
}
if (trigger instanceof WaitForWorldChangeAction world) {
CompletableFuture<MacroExecutor.MacroEvent> done = new CompletableFuture<>();
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForWorldChange(world);
MacroPacketObserve.TimedOneShotPacketListener worldListener = MacroPacketObserve.awaitTimedReceive((packet, direction, now) -> isWorldTransitionPacket(packet));
tickFuture.whenComplete((v, e) -> {
if (e != null) {
if (!done.isDone()) done.cancel(true);
} else {
done.complete(new MacroExecutor.MacroEvent(null, "world change", System.nanoTime(), null));
}
});
worldListener.future.whenComplete((event, error) -> {
if (error != null) {
if (!done.isDone()) done.cancel(true);
return;
}
if (world.targetDimension == null || world.targetDimension.isBlank()) {
done.complete(event);
}
});
done.whenComplete((v, e) -> {
worldListener.cancel();
if (!tickFuture.isDone()) tickFuture.cancel(true);
});
return done;
}
if (trigger instanceof WaitForTeleportAction teleport) {
net.minecraft.world.phys.Vec3 origin = mc.player == null ? net.minecraft.world.phys.Vec3.ZERO : mc.player.position();
CompletableFuture<MacroExecutor.MacroEvent> done = new CompletableFuture<>();
MacroPacketObserve.TimedOneShotPacketListener teleListener = MacroPacketObserve.awaitTimedReceive((packet, direction, now) ->
packet instanceof ClientboundPlayerPositionPacket || isWorldTransitionPacket(packet));
teleListener.future.whenComplete((event, error) -> {
teleListener.cancel();
if (error != null) {
done.cancel(true);
return;
}
if (teleport.minDistance <= 0.0) {
done.complete(event);
return;
}
MacroConditionRegistry.waitForPositionDeltaFrom(origin, teleport.minDistance, teleport.horizontalOnly)
.whenComplete((v, e) -> {
if (e != null) done.cancel(true);
else done.complete(new MacroExecutor.MacroEvent(event.packet(), event.direction(), System.nanoTime(), event.chatCapture()));
});
});
done.whenComplete((v, e) -> teleListener.cancel());
return done;
}
CompletableFuture<?> base = raceConditionFuture(mc, trigger);
if (base == null) {
CompletableFuture<MacroExecutor.MacroEvent> done = new CompletableFuture<>();
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
mc.execute(() -> MacroLifecycle.withRunContext(run, () -> {
try {
MacroDispatcher.executeSingleActionWithWaits(mc, trigger, null);
done.complete(new MacroExecutor.MacroEvent(null, trigger.getDisplayName(), System.nanoTime(), null));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
done.cancel(true);
} catch (Throwable t) {
done.completeExceptionally(t);
}
}));
return done;
}
CompletableFuture<MacroExecutor.MacroEvent> timed = new CompletableFuture<>();
base.whenComplete((v, e) -> {
if (e != null) timed.cancel(true);
else timed.complete(new MacroExecutor.MacroEvent(null, trigger.getDisplayName(), System.nanoTime(), null));
});
timed.whenComplete((v, e) -> {
if (!base.isDone()) base.cancel(true);
});
return timed;
}
static CompletableFuture<?> raceConditionFuture(Minecraft mc, MacroAction trigger) {
if (trigger instanceof WaitMovementAction wm) {
return raceConditionFuture(mc, wm.resolveSubAction());
}
if (trigger instanceof WaitGamemodeChangeAction gm) {
return MacroConditionRegistry.waitForGamemodeChange(gm);
}
if (trigger instanceof DelayAction delay) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return CompletableFuture.runAsync(() -> MacroLifecycle.withRunContext(run, () -> {
try {
if (delay.useTicks) {
for (int i = 0; i < delay.delayTicks && MacroLifecycle.isCurrentRunActive(); i++) {
MacroRunLoop.waitForCondition(MacroConditionRegistry.waitForNextTick());
}
} else {
Thread.sleep(Math.max(0, delay.delayMs));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
}
if (trigger instanceof WaitForGuiAction gui) {
CompletableFuture<MacroExecutor.MacroEvent> done = new CompletableFuture<>();
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
Thread worker = new Thread(() -> MacroLifecycle.withRunContext(run, () -> {
long start = System.currentTimeMillis();
boolean saw = false;
java.util.Map<String, MacroValue> observed = java.util.Map.of();
while (!done.isDone() && MacroLifecycle.isCurrentRunActive()) {
var captured = gui.captureGui(Minecraft.getInstance().gui.screen());
if (captured.isPresent()) {
saw = true;
observed = captured.get();
if (gui.waitMode == WaitForGuiAction.WaitMode.OPEN) {
done.complete(new MacroExecutor.MacroEvent(null, "GUI", System.nanoTime(), null, observed));
return;
}
} else if (saw && gui.waitMode == WaitForGuiAction.WaitMode.CLOSE) {
done.complete(new MacroExecutor.MacroEvent(null, "GUI close", System.nanoTime(), null, observed));
return;
}
if (gui.timeoutMs > 0 && System.currentTimeMillis() - start >= gui.timeoutMs) {
done.cancel(false);
return;
}
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
}
}), "MacroGuiCaptureWait");
worker.setDaemon(true);
worker.start();
done.whenComplete((v, e) -> worker.interrupt());
return done;
}
if (trigger instanceof WaitForChatAction chat) {
MacroVariableContext context = MacroLifecycle.currentVariableContext();
MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> {
MacroExecutor.ChatCapture capture = MacroChatObserve.extractIncomingChat(packet);
return capture != null && MacroChatObserve.captureChatAction(chat, capture, context).isPresent();
});
listener.future.whenComplete((v, e) -> listener.cancel());
return listener.future;
}
if (trigger instanceof WaitForPacketAction packetAction) {
java.util.List<String> targets = packetAction.effectiveList();
CompletableFuture<Packet<?>> matched = new CompletableFuture<>();
MacroExecutor.OneShotPacketListener recv = MacroExecutor.awaitReceive(packet -> {
if (targets.isEmpty()) return true;
for (String target : targets) {
if (MacroPacketWait.matchesPacketTarget(target, packet, "S2C")) return true;
}
return false;
});
MacroExecutor.OneShotPacketListener send = MacroExecutor.awaitSend(packet -> {
if (targets.isEmpty()) return true;
for (String target : targets) {
if (MacroPacketWait.matchesPacketTarget(target, packet, "C2S")) return true;
}
return false;
});
recv.future.thenAccept(matched::complete);
send.future.thenAccept(matched::complete);
matched.whenComplete((v, e) -> {
recv.cancel();
send.cancel();
});
return matched;
}
if (trigger instanceof WaitForHealthAction health) {
return MacroConditionRegistry.waitForHealth(health.healthThreshold, health.below);
}
if (trigger instanceof WaitForBlockAction block) {
return MacroConditionRegistry.waitForBlock(block);
}
if (trigger instanceof WaitForCooldownAction cooldown) {
ItemTarget target = MacroWorldActions.resolveItemTarget(cooldown.itemTarget, cooldown.itemName);
return MacroConditionRegistry.waitForCooldown(target, cooldown.checkMainInteractionHand);
}
if (trigger instanceof WaitPosAction pos) {
return MacroConditionRegistry.waitForPos(pos.x, pos.y, pos.z, pos.leeway, pos.checkRotation, pos.yaw, pos.pitch, pos.rotLeeway);
}
if (trigger instanceof WaitForEntityAction entity) {
return MacroPrearm.predicateFuture(() -> entity.matchesEntityTarget(Minecraft.getInstance()), entity.timeoutMs);
}
if (trigger instanceof WaitForSoundAction sound) {
return MacroConditionRegistry.waitForSound(sound);
}
if (trigger instanceof WaitForSlotChangeAction slot) {
return MacroConditionRegistry.waitForSlotChange(slot);
}
if (trigger instanceof WaitDurabilityAction durability) {
return MacroPrearm.predicateFuture(() -> MacroDurability.durabilityObservationMatches(Minecraft.getInstance(), durability), durability.timeoutMs);
}
if (trigger instanceof WaitFreeSlotsAction freeSlots) {
return MacroPrearm.predicateFuture(() -> freeSlots.matches(Minecraft.getInstance()), freeSlots.timeoutMs);
}
if (trigger instanceof TickSyncAction) {
return MacroConditionRegistry.waitForNextTick();
}
if (trigger instanceof ServerTickSyncAction serverTick) {
return MacroConditionRegistry.waitForServerTick(serverTick.bufferMs, serverTick.maxWaitMs, serverTick.ignorePing);
}
if (trigger instanceof WaitForWorldChangeAction world) {
CompletableFuture<Void> done = new CompletableFuture<>();
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForWorldChange(world);
MacroExecutor.OneShotPacketListener worldListener = MacroExecutor.awaitReceive(MacroRace::isWorldTransitionPacket);
tickFuture.whenComplete((v, e) -> {
if (e != null) {
if (!done.isDone()) done.cancel(true);
} else {
done.complete(null);
}
});
worldListener.future.whenComplete((packet, error) -> {
if (error != null) {
if (!done.isDone()) done.cancel(true);
return;
}
if (world.targetDimension == null || world.targetDimension.isBlank()) {
done.complete(null);
}
});
done.whenComplete((v, e) -> {
worldListener.cancel();
if (!tickFuture.isDone()) tickFuture.cancel(true);
});
return done;
}
if (trigger instanceof WaitForPositionDeltaAction delta) {
return MacroConditionRegistry.waitForPositionDelta(delta.distance, delta.horizontalOnly);
}
if (trigger instanceof WaitForTeleportAction teleport) {
net.minecraft.world.phys.Vec3 origin = mc.player == null ? net.minecraft.world.phys.Vec3.ZERO : mc.player.position();
CompletableFuture<Void> done = new CompletableFuture<>();
MacroExecutor.OneShotPacketListener teleListener = MacroExecutor.awaitReceive(packet ->
packet instanceof ClientboundPlayerPositionPacket
|| isWorldTransitionPacket(packet));
teleListener.future.whenComplete((packet, error) -> {
teleListener.cancel();
if (error != null) {
done.cancel(true);
return;
}
if (teleport.minDistance <= 0.0) {
done.complete(null);
return;
}
MacroConditionRegistry.waitForPositionDeltaFrom(origin, teleport.minDistance, teleport.horizontalOnly)
.whenComplete((v, e) -> {
if (e != null) done.cancel(true);
else done.complete(null);
});
});
return done;
}
if (trigger instanceof RevisionSyncAction) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
return CompletableFuture.runAsync(() -> MacroLifecycle.withRunContext(run, () -> {
try {
MacroDispatcher.executeSingleActionWithWaits(mc, trigger, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
}
return null;
}
static boolean waitForFutureDone(CompletableFuture<?> future, long timeoutMs) {
Thread t = Thread.currentThread();
long start = System.nanoTime();
long timeoutNanos = timeoutMs <= 0 ? Long.MAX_VALUE : timeoutMs * 1_000_000L;
future.whenComplete((v, e) -> java.util.concurrent.locks.LockSupport.unpark(t));
while (!future.isDone() && MacroLifecycle.isCurrentRunActive()) {
if (System.nanoTime() - start >= timeoutNanos) break;
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
if (Thread.interrupted()) break;
}
return future.isDone() && !future.isCancelled() && MacroLifecycle.isCurrentRunActive();
}
}

View file

@ -0,0 +1,262 @@
package autismclient.util.macro;
import autismclient.modules.AutismModule;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismMacro;
import autismclient.util.PacketRegenerator;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerboundContainerClickPacket;
final class MacroRender {
private MacroRender() {
}
public static void onRender(float tickDelta) {
if (!MacroExecutor.isRotating.get()) return;
MacroExecutor.RunState owner = MacroExecutor.ROTATION_OWNER.get();
if (owner != null && !owner.running.get()) {
cancelRotation(owner);
return;
}
if (owner == null && !MacroExecutor.isRunning.get()) {
synchronized (MacroExecutor.ROTATION_STATE_LOCK) {
if (MacroExecutor.ROTATION_OWNER.get() == null) MacroExecutor.isRotating.set(false);
}
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.player == null) return;
float currentYaw = mc.player.getYRot();
float currentPitch = mc.player.getXRot();
double deltaYaw = net.minecraft.util.Mth.wrapDegrees(MacroExecutor.targetYaw - currentYaw);
double deltaPitch = MacroExecutor.targetPitch - currentPitch;
double diagonalDistance = Math.hypot(deltaYaw, deltaPitch);
if (diagonalDistance <= 0.35) {
mc.player.setYRot(MacroExecutor.targetYaw);
mc.player.setXRot(MacroExecutor.targetPitch);
MacroExecutor.rotationAlignedFrames++;
if (MacroExecutor.rotationAlignedFrames >= 2) {
synchronized (MacroExecutor.ROTATION_STATE_LOCK) {
if (MacroExecutor.ROTATION_OWNER.get() == owner) {
MacroExecutor.isRotating.set(false);
MacroExecutor.ROTATION_OWNER.set(null);
}
}
}
return;
}
MacroExecutor.rotationAlignedFrames = 0;
double frameStep = Math.min(diagonalDistance, MacroExecutor.rotationSpeed * Math.max(tickDelta, 1.0f));
double stepYaw = (deltaYaw / diagonalDistance) * frameStep;
double stepPitch = (deltaPitch / diagonalDistance) * frameStep;
mc.player.setYRot(currentYaw + (float) stepYaw);
mc.player.setXRot(currentPitch + (float) stepPitch);
}
public static boolean hasRenderWork() {
return MacroExecutor.isRotating.get();
}
static void startSmoothRotation(float yaw, float pitch, double speed) {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
while (run != null && run.running.get()) {
synchronized (MacroExecutor.ROTATION_STATE_LOCK) {
MacroExecutor.RunState owner = MacroExecutor.ROTATION_OWNER.get();
if (owner == null || owner == run || !owner.running.get()) {
MacroExecutor.ROTATION_OWNER.set(run);
MacroExecutor.targetYaw = yaw;
MacroExecutor.targetPitch = pitch;
MacroExecutor.rotationSpeed = speed;
MacroExecutor.rotationAlignedFrames = 0;
MacroExecutor.isRotating.set(true);
return;
}
}
java.util.concurrent.locks.LockSupport.parkNanos(MacroExecutor.MACRO_WAIT_POLL_NANOS);
}
if (run == null) {
synchronized (MacroExecutor.ROTATION_STATE_LOCK) {
if (MacroExecutor.ROTATION_OWNER.get() != null) return;
MacroExecutor.targetYaw = yaw;
MacroExecutor.targetPitch = pitch;
MacroExecutor.rotationSpeed = speed;
MacroExecutor.rotationAlignedFrames = 0;
MacroExecutor.isRotating.set(true);
}
}
}
static void waitForSmoothRotationCompletion() {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
while (MacroLifecycle.isCurrentRunActive() && MacroExecutor.isRotating.get()
&& (run == null || MacroExecutor.ROTATION_OWNER.get() == run)) {
java.util.concurrent.locks.LockSupport.parkNanos(1_000_000L);
}
}
static void cancelRotation(MacroExecutor.RunState run) {
if (run == null) return;
synchronized (MacroExecutor.ROTATION_STATE_LOCK) {
if (MacroExecutor.ROTATION_OWNER.get() != run) return;
MacroExecutor.isRotating.set(false);
MacroExecutor.rotationAlignedFrames = 0;
MacroExecutor.ROTATION_OWNER.set(null);
}
}
static LookAtBlockAction.RotationTarget resolveLookAtTargetOnClient(Minecraft mc, LookAtBlockAction action) {
if (mc == null || action == null) return null;
java.util.concurrent.atomic.AtomicReference<LookAtBlockAction.RotationTarget> targetRef = new java.util.concurrent.atomic.AtomicReference<>();
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
mc.execute(() -> {
try {
targetRef.set(action.resolveRotationTarget(mc));
} finally {
latch.countDown();
}
});
while (MacroLifecycle.isCurrentRunActive()) {
try {
if (latch.await(10, java.util.concurrent.TimeUnit.MILLISECONDS)) {
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return targetRef.get();
}
public static void stopAll() {
MacroExecutor.stop();
}
static void executeBurstPackets(Minecraft mc, java.util.List<SendPacketAction> batch) {
if (mc.getConnection() == null) {
AutismClientMessaging.sendPrefixed("\u00a7cNo network connection for burst!");
return;
}
java.util.List<autismclient.util.AutismSharedState.QueuedPacket> allQueuedPackets = new java.util.ArrayList<>();
for (SendPacketAction action : batch) {
allQueuedPackets.addAll(action.getPackets());
}
if (allQueuedPackets.isEmpty()) {
return;
}
java.util.List<Packet<?>> regeneratedPackets = new java.util.ArrayList<>(allQueuedPackets.size());
for (autismclient.util.AutismSharedState.QueuedPacket qp : allQueuedPackets) {
if (qp.packet == null) continue;
regeneratedPackets.add(qp.packet);
}
if (regeneratedPackets.isEmpty()) {
AutismClientMessaging.sendPrefixed("\u00a7cBurst: All packets failed to regenerate!");
return;
}
net.minecraft.client.multiplayer.ClientPacketListener handler = mc.getConnection();
for (Packet<?> packet : regeneratedPackets) {
Packet<?> toSend = packet;
if (packet instanceof ServerboundContainerClickPacket) {
toSend = PacketRegenerator.regenerate(packet);
if (toSend == null) {
continue;
}
}
handler.send(toSend);
}
if (regeneratedPackets.size() > 1) {
AutismClientMessaging.sendPrefixed("\u00a7a Burst sent " + regeneratedPackets.size() + " packets");
}
}
static java.util.List<Packet<?>> preGeneratePackets(AutismMacro macro, int startIdx, int count) {
java.util.List<Packet<?>> result = new java.util.ArrayList<>();
int maxIdx = count < 0 ? macro.actions.size() : Math.min(startIdx + count, macro.actions.size());
return preGeneratePackets(macro, startIdx, count, maxIdx);
}
static java.util.List<Packet<?>> preGeneratePackets(AutismMacro macro, int startIdx, int count, int endIdx) {
java.util.List<Packet<?>> result = new java.util.ArrayList<>();
int maxIdx = Math.min(endIdx, count < 0 ? macro.actions.size() : Math.min(startIdx + count, macro.actions.size()));
Minecraft mc = Minecraft.getInstance();
for (int j = startIdx; j < maxIdx; j++) {
MacroAction act = macro.actions.get(j);
if (!(act instanceof SendPacketAction)) break;
SendPacketAction spa = (SendPacketAction) act;
for (autismclient.util.AutismSharedState.QueuedPacket qp : spa.getPackets()) {
if (qp.packet == null) continue;
result.add(qp.packet);
}
}
return result;
}
static void executePreGeneratedBurst(Minecraft mc, java.util.List<Packet<?>> packets) {
if (packets.isEmpty() || mc.getConnection() == null) return;
net.minecraft.client.multiplayer.ClientPacketListener handler = mc.getConnection();
for (Packet<?> p : packets) {
Packet<?> toSend = p;
if (p instanceof ServerboundContainerClickPacket) {
toSend = PacketRegenerator.regenerate(p);
if (toSend == null) {
continue;
}
}
handler.send(toSend);
}
AutismModule module = AutismModule.get();
if (module.shouldForceChannelFlush()) {
try {
net.minecraft.network.Connection connection = handler.getConnection();
if (connection != null) {
connection.flushChannel();
}
} catch (Exception e) { }
}
}
static int countPreGeneratedActions(AutismMacro macro, int startIdx, int packetCount) {
return countPreGeneratedActions(macro, startIdx, packetCount, macro.actions.size());
}
static int countPreGeneratedActions(AutismMacro macro, int startIdx, int packetCount, int endIdx) {
int actionsToSkip = 0;
int packetsRemaining = packetCount;
for (int j = startIdx; j < endIdx && packetsRemaining > 0; j++) {
MacroAction act = macro.actions.get(j);
if (!(act instanceof SendPacketAction)) break;
SendPacketAction spa = (SendPacketAction) act;
int actionPackets = spa.getPackets().size();
packetsRemaining -= actionPackets;
actionsToSkip++;
}
return actionsToSkip;
}
}

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,7 @@ public final class MacroVariables {
}
private static MacroVariableContext context() {
MacroVariableContext active = MacroExecutor.currentVariableContext();
MacroVariableContext active = MacroLifecycle.currentVariableContext();
return active == null ? FALLBACK.get() : active;
}
}

View file

@ -0,0 +1,617 @@
package autismclient.util.macro;
import autismclient.AutismClientAddon;
import autismclient.modules.PackHideState;
import autismclient.util.AutismClientMessaging;
import autismclient.util.AutismCompatManager;
import autismclient.util.AutismCraftingHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.protocol.Packet;
import net.minecraft.world.level.block.state.BlockState;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
final class MacroWorldActions {
private MacroWorldActions() {
}
public static void runPlaceAction(Minecraft mc, PlaceAction action) throws InterruptedException {
if (mc == null || mc.player == null || mc.level == null) return;
ItemTarget target = action.resolvedItemTarget().resolveTemplate(mc);
if (target == null) return;
if (action.waitForItem && target != null && (target.hasIdentity() || target.hasSlot())) {
MacroLifecycle.setCurrentStatus("Waiting for " + describeItemTarget(target));
waitForItemActionEntry(mc, target);
}
if (!action.interact) {
MacroActionRunner.runMacroActionOnClientThread(mc, action, "Place");
return;
}
final BlockPos placePos = action.blockPos == null ? BlockPos.ZERO : action.blockPos;
final Direction face = action.manualDirection && action.direction != null ? action.direction : Direction.UP;
InteractTiming t = action.interactTiming;
boolean interactFirst = t == InteractTiming.BEFORE || (t == InteractTiming.CUSTOM && action.interactCustomMs < 0);
long ns = t == InteractTiming.CUSTOM ? Math.abs((long) action.interactCustomMs) * 1_000_000L : 0L;
if (interactFirst) {
MacroClientThread.runOnClientThread(mc, () -> MacroBreak.sendBlockInteract(mc, placePos, face), action.isTickAligned());
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
MacroActionRunner.runMacroActionOnClientThread(mc, action, "Place");
} else if (t == InteractTiming.WITH) {
MacroClientThread.runOnClientThread(mc, () -> {
try { action.execute(mc); } catch (Exception e) { AutismClientAddon.LOG.debug("[Autism] Macro WITH-interact action failed", e); }
MacroBreak.sendBlockInteract(mc, placePos, face);
}, action.isTickAligned());
} else if (t == InteractTiming.AFTER_PLUS) {
MacroActionRunner.runMacroActionOnClientThread(mc, action, "Place");
MacroExecutor.waitOneTick();
MacroClientThread.runOnClientThread(mc, () -> MacroBreak.sendBlockInteract(mc, placePos, face), action.isTickAligned());
} else {
MacroActionRunner.runMacroActionOnClientThread(mc, action, "Place");
if (ns > 0) java.util.concurrent.locks.LockSupport.parkNanos(ns);
MacroClientThread.runOnClientThread(mc, () -> MacroBreak.sendBlockInteract(mc, placePos, face), action.isTickAligned());
}
}
private static void waitForItemInSpecificHandlerSlot(Minecraft mc, int slotId, ItemTarget target) throws InterruptedException {
while (MacroLifecycle.isCurrentRunActive()) {
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> handlerSlotMatchesItem(mc, slotId, target), Boolean.FALSE))) return;
MacroExecutor.waitOneTick();
}
}
private static void waitForAnyItemInSpecificHandlerSlot(Minecraft mc, int slotId) throws InterruptedException {
while (MacroLifecycle.isCurrentRunActive()) {
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> handlerSlotHasAnyItem(mc, slotId), Boolean.FALSE))) return;
MacroExecutor.waitOneTick();
}
}
private static boolean handlerSlotMatchesItem(Minecraft mc, int slotId, ItemTarget target) {
if (mc.player == null || mc.player.containerMenu == null) return false;
int handlerSlot = autismclient.util.AutismInventoryHelper.resolveConfiguredHandlerSlot(mc, slotId);
if (handlerSlot < 0 || handlerSlot >= mc.player.containerMenu.slots.size()) return false;
var slot = mc.player.containerMenu.slots.get(handlerSlot);
if (slot == null || slot.getItem().isEmpty()) return false;
ItemTarget resolvedTarget = target == null ? ItemTarget.slotOnly(slotId) : target.copy();
if (!resolvedTarget.hasSlot()) resolvedTarget.slot = slotId;
return resolvedTarget.matches(slot.getItem(), slotId);
}
private static boolean handlerSlotHasAnyItem(Minecraft mc, int slotId) {
if (mc.player == null || mc.player.containerMenu == null) return false;
int handlerSlot = autismclient.util.AutismInventoryHelper.resolveConfiguredHandlerSlot(mc, slotId);
if (handlerSlot < 0 || handlerSlot >= mc.player.containerMenu.slots.size()) return false;
var slot = mc.player.containerMenu.slots.get(handlerSlot);
return slot != null && !slot.getItem().isEmpty();
}
public static void waitForItemActionTargets(Minecraft mc, ItemAction ia) throws InterruptedException {
if (ia == null) return;
List<ItemTarget> targets = resolveItemTargets(mc, ia.itemTargets, ia.itemNames);
if (!targets.isEmpty()) {
for (ItemTarget target : targets) {
waitForItemActionEntry(mc, target);
}
return;
}
if (ia.useSlot && ia.targetSlot >= 0) {
MacroLifecycle.setCurrentStatus("Waiting for slot " + ia.targetSlot);
waitForAnyItemInSpecificHandlerSlot(mc, ia.targetSlot);
}
}
public static void waitForItemActionEntry(Minecraft mc, ItemTarget target) throws InterruptedException {
ItemTarget resolvedTarget = target == null ? null : target.resolveTemplate(mc);
if (resolvedTarget == null) return;
String itemLabel = describeItemTarget(resolvedTarget);
int slot = resolvedTarget.hasSlot() ? resolvedTarget.slot : -1;
if (slot >= 0 && resolvedTarget.hasIdentity()) {
MacroLifecycle.setCurrentStatus("Waiting for " + itemLabel);
waitForItemInSpecificHandlerSlot(mc, slot, resolvedTarget);
return;
}
if (slot >= 0) {
MacroLifecycle.setCurrentStatus("Waiting for slot " + slot);
waitForAnyItemInSpecificHandlerSlot(mc, slot);
return;
}
if (resolvedTarget.hasIdentity()) {
MacroLifecycle.setCurrentStatus("Waiting for item in GUI: " + itemLabel);
CompletableFuture<Void> waitFuture = MacroConditionRegistry.waitForItemInHandler(resolvedTarget);
MacroRunLoop.waitForCondition(waitFuture);
}
}
static ItemTarget resolveItemTarget(ItemTarget target, String legacyEntry) {
if (target != null && (target.hasSlot() || target.hasIdentity())) return target.copy();
return ItemTarget.fromLegacyEntry(legacyEntry);
}
private static List<ItemTarget> resolveItemTargets(Minecraft mc, List<ItemTarget> targets, List<String> legacyEntries) {
List<ItemTarget> resolved = new ArrayList<>();
if (targets != null) {
for (ItemTarget target : targets) {
if (target == null || (!target.hasSlot() && !target.hasIdentity())) continue;
ItemTarget runtimeTarget = target.resolveTemplate(mc);
if (runtimeTarget != null) resolved.add(runtimeTarget);
}
}
if (!resolved.isEmpty()) return resolved;
if (legacyEntries == null) return resolved;
for (String entry : legacyEntries) {
ItemTarget parsed = ItemTarget.fromLegacyEntry(entry);
ItemTarget runtimeTarget = parsed.resolveTemplate(mc);
if (runtimeTarget != null && (runtimeTarget.hasSlot() || runtimeTarget.hasIdentity())) resolved.add(runtimeTarget);
}
return resolved;
}
public static String describeItemTarget(ItemTarget target) {
if (target == null) return "";
String summary = target.summaryText();
if (summary != null && !summary.isBlank()) return summary;
String legacy = target.toLegacyEntry();
return legacy == null ? "" : legacy;
}
public static void runCraftAction(Minecraft mc, CraftAction craftAction) throws InterruptedException {
if (craftAction == null || !craftAction.hasEntries()) {
MacroLifecycle.setCurrentStatus("Crafting");
AutismClientMessaging.sendPrefixed("\u00a7cNo craft recipes selected.");
return;
}
for (int index = 0; index < craftAction.entries.size(); index++) {
CraftAction.CraftEntry entry = craftAction.entries.get(index);
if (entry == null || !entry.hasRecipe()) continue;
MacroLifecycle.setCurrentStatus(entry.useMaxAmount
? "Crafting " + entry.resultName + " [Max] (" + (index + 1) + "/" + craftAction.entries.size() + ")"
: "Crafting " + entry.resultName + " x" + Math.max(1, entry.amount) + " (" + (index + 1) + "/" + craftAction.entries.size() + ")");
AutismCraftingHelper.CraftableRecipeOption option = AutismCraftingHelper.findCraftableRecipe(mc, entry.recipeKey);
if (option == null) option = AutismCraftingHelper.findCraftableRecipe(mc, entry.recipeId);
if (option == null) {
MacroLifecycle.setCurrentStatus("Recipe not found");
AutismClientMessaging.sendPrefixed("\u00a7cRecipe not found: " + (entry.resultName == null || entry.resultName.isBlank() ? "unknown" : entry.resultName));
return;
}
int desiredAmount = AutismCraftingHelper.getEffectiveRequestedOutput(option, entry.amount, entry.useMaxAmount);
if (desiredAmount <= 0) {
MacroLifecycle.setCurrentStatus("No space or materials");
AutismClientMessaging.sendPrefixed("\u00a7cNo space or materials for " + entry.resultName + ".");
return;
}
AutismCraftingHelper.CraftExecutionResult result = AutismCraftingHelper.executeCraftImmediately(
mc,
entry.recipeKey,
entry.recipeId,
desiredAmount
);
MacroLifecycle.setCurrentStatus(result.message);
if (!result.success) {
AutismClientMessaging.sendPrefixed("\u00a7c" + result.message);
return;
}
if (MacroLifecycle.isCurrentRunActive()) {
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
}
}
public static void runGoToAction(Minecraft mc, GoToAction goToAction) throws InterruptedException {
MacroLifecycle.setCurrentStatus(String.format("Going to %.0f,%.0f,%.0f", goToAction.x, goToAction.y, goToAction.z));
if (!AutismCompatManager.isBaritoneAvailable()) {
AutismClientMessaging.sendPrefixed("Baritone not installed; skipping goto action.");
return;
}
int goalX = (int) Math.floor(goToAction.x);
int goalY = (int) Math.floor(goToAction.y);
int goalZ = (int) Math.floor(goToAction.z);
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (!runBaritoneStart(mc, () -> AutismCompatManager.startBaritoneGoTo(mc, goalX, goalY, goalZ))) {
AutismClientMessaging.sendPrefixed("Failed to start Baritone goto.");
return;
}
try {
if (!goToAction.waitForArrival) {
MacroLifecycle.setCurrentStatus("Goto started");
return;
}
long startMs = System.currentTimeMillis();
boolean sawBaritoneActivity = false;
while (MacroLifecycle.isCurrentRunActive()) {
boolean goalActive = AutismCompatManager.isBaritoneGoalActive();
boolean baritoneBusy = AutismCompatManager.isBaritoneBusy();
if (goalActive || baritoneBusy) sawBaritoneActivity = true;
if (sawBaritoneActivity && !goalActive && !baritoneBusy) {
MacroLifecycle.setCurrentStatus("Goto finished");
return;
}
if (!sawBaritoneActivity && System.currentTimeMillis() - startMs >= 1500 && !goalActive && !baritoneBusy) {
MacroLifecycle.setCurrentStatus("Goto finished");
return;
}
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
} finally {
if (!MacroLifecycle.isCurrentRunActive()) requestOwnedBaritoneStop(run, mc);
else releaseBaritoneOwner(run);
}
}
public static void runMineAction(Minecraft mc, MineAction mineAction) throws InterruptedException {
String mineDesc = mineAction.targetBlocks.isEmpty() ? "none" : mineAction.targetBlocks.get(0);
MacroLifecycle.setCurrentStatus("Mining: " + mineDesc);
if (mineAction.targetBlocks.isEmpty()) return;
if (!AutismCompatManager.isBaritoneAvailable()) {
AutismClientMessaging.sendPrefixed("Baritone not installed; skipping mine action.");
return;
}
java.util.List<String> blockNames = new java.util.ArrayList<>();
for (String id : mineAction.targetBlocks) {
if (id == null || id.isBlank()) continue;
blockNames.add(id.startsWith("minecraft:") ? id.substring(10) : id);
}
if (blockNames.isEmpty()) return;
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
MineProgressTracker mineTracker = mineAction.stopMinedCount ? MineProgressTracker.create(mc, mineAction.targetBlocks) : null;
if (mineTracker != null) {
mineTracker.poll(mc);
if (run != null) run.activeMineTracker = mineTracker;
}
try {
if (!runBaritoneStart(mc, () -> AutismCompatManager.startBaritoneMine(mc, blockNames), mineAction.isTickAligned())) {
AutismClientMessaging.sendPrefixed("Failed to start Baritone mine.");
return;
}
long mineStartMs = System.currentTimeMillis();
boolean sawBaritoneActivity = false;
while (MacroLifecycle.isCurrentRunActive()) {
boolean mineActive = AutismCompatManager.isBaritoneMineActive();
boolean baritoneBusy = AutismCompatManager.isBaritoneBusy();
if (mineActive || baritoneBusy) sawBaritoneActivity = true;
String stopReason = null;
int minedDelta = 0;
if (mineTracker != null) {
mineTracker.poll(mc);
minedDelta = mineTracker.destroyedCount();
MacroLifecycle.setCurrentStatus("Mining: " + mineDesc + " (" + minedDelta + "/" + mineAction.minedCountTarget + ")");
if (minedDelta >= mineAction.minedCountTarget) {
stopReason = "Mine target reached";
}
}
if (stopReason == null && mineAction.stopInventoryFull && isInventoryFull(mc)) {
stopReason = "Mine stop: inventory full";
}
if (stopReason == null && mineAction.stopSlotsUsed) {
int used = countUsedMainInventorySlots(mc);
if (used >= mineAction.slotsUsedThreshold) {
stopReason = "Mine stop: slots used";
}
}
if (stopReason == null && mineAction.stopAfterTime
&& System.currentTimeMillis() - mineStartMs >= mineAction.timeoutSeconds * 1000L) {
stopReason = "Mine timeout";
}
if (stopReason != null) {
MacroLifecycle.setCurrentStatus(stopReason);
requestOwnedBaritoneStop(run, mc);
waitForBaritoneIdle();
return;
}
if (sawBaritoneActivity && !mineActive && !baritoneBusy) {
MacroLifecycle.setCurrentStatus("Mine finished");
return;
}
if (!sawBaritoneActivity && System.currentTimeMillis() - mineStartMs >= 1500 && !mineActive && !baritoneBusy) {
MacroLifecycle.setCurrentStatus("Mine finished");
return;
}
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
} finally {
if (run != null && run.activeMineTracker == mineTracker) run.activeMineTracker = null;
if (!MacroLifecycle.isCurrentRunActive()) requestOwnedBaritoneStop(run, mc);
else releaseBaritoneOwner(run);
}
}
private static boolean runBaritoneStart(Minecraft mc, java.util.function.BooleanSupplier starter) throws InterruptedException {
return runBaritoneStart(mc, starter, false);
}
private static boolean runBaritoneStart(Minecraft mc, java.util.function.BooleanSupplier starter,
boolean tickAligned) throws InterruptedException {
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
if (run != null) MacroExecutor.BARITONE_OWNER.set(run);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
AtomicBoolean started = new AtomicBoolean(false);
Runnable task = () -> {
try {
started.set(starter.getAsBoolean());
} finally {
latch.countDown();
}
};
if (tickAligned && !mc.isSameThread()) MacroClientThread.TICK_ALIGNED.add(task);
else mc.execute(task);
latch.await(2, TimeUnit.SECONDS);
if (!started.get()) releaseBaritoneOwner(run);
return started.get();
}
private static void requestBaritoneStop(Minecraft mc) {
if (mc == null || !AutismCompatManager.isBaritoneAvailable()) return;
mc.execute(() -> AutismCompatManager.stopBaritone(mc));
}
public static void requestOwnedBaritoneStop(MacroExecutor.RunState run, Minecraft mc) {
if (run == null || !MacroExecutor.BARITONE_OWNER.compareAndSet(run, null)) return;
requestBaritoneStop(mc);
}
private static void releaseBaritoneOwner(MacroExecutor.RunState run) {
if (run != null) MacroExecutor.BARITONE_OWNER.compareAndSet(run, null);
}
private static void waitForBaritoneIdle() {
long waitStartMs = System.currentTimeMillis();
while (MacroLifecycle.isCurrentRunActive() && AutismCompatManager.isBaritoneBusy() && System.currentTimeMillis() - waitStartMs < 3000L) {
CompletableFuture<Void> tickFuture = MacroConditionRegistry.waitForNextTick();
MacroRunLoop.waitForCondition(tickFuture);
}
}
private static boolean isInventoryFull(Minecraft mc) {
if (mc.player == null) return false;
for (int slot = 9; slot < 36; slot++) {
if (mc.player.getInventory().getItem(slot).isEmpty()) return false;
}
return true;
}
private static int countUsedMainInventorySlots(Minecraft mc) {
if (mc.player == null) return 0;
int used = 0;
for (int slot = 9; slot < 36; slot++) {
if (!mc.player.getInventory().getItem(slot).isEmpty()) used++;
}
return used;
}
public static final class MineProgressTracker {
private static final int POLL_RADIUS = 8;
private final java.util.Set<net.minecraft.world.level.block.Block> targetBlocks;
private final java.util.HashMap<Long, Boolean> observedTargetPositions = new java.util.HashMap<>();
private final java.util.HashSet<Long> countedPositions = new java.util.HashSet<>();
private int destroyed;
private MineProgressTracker(java.util.Set<net.minecraft.world.level.block.Block> targetBlocks) {
this.targetBlocks = targetBlocks;
}
static MineProgressTracker create(Minecraft mc, java.util.List<String> rawTargets) {
java.util.Set<net.minecraft.world.level.block.Block> blocks = new java.util.LinkedHashSet<>();
if (rawTargets != null) {
for (String raw : rawTargets) {
net.minecraft.world.level.block.Block block = resolveBlock(raw);
if (block != null && block != net.minecraft.world.level.block.Blocks.AIR) blocks.add(block);
}
}
if (blocks.isEmpty()) return null;
return new MineProgressTracker(blocks);
}
private static net.minecraft.world.level.block.Block resolveBlock(String raw) {
if (raw == null || raw.isBlank()) return null;
String value = raw.trim();
try {
net.minecraft.resources.Identifier id = value.contains(":")
? net.minecraft.resources.Identifier.parse(value)
: net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", value);
if (!net.minecraft.core.registries.BuiltInRegistries.BLOCK.containsKey(id)) return null;
return net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(id);
} catch (Throwable ignored) {
return null;
}
}
synchronized int destroyedCount() {
return destroyed;
}
synchronized void poll(Minecraft mc) {
if (mc == null || mc.player == null || mc.level == null) return;
BlockPos center = mc.player.blockPosition();
int minY = Math.max(mc.level.getMinY(), center.getY() - POLL_RADIUS);
int maxY = Math.min(mc.level.getMaxY() - 1, center.getY() + POLL_RADIUS);
for (int x = center.getX() - POLL_RADIUS; x <= center.getX() + POLL_RADIUS; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = center.getZ() - POLL_RADIUS; z <= center.getZ() + POLL_RADIUS; z++) {
BlockPos pos = new BlockPos(x, y, z);
updateObservedPosition(pos, mc.level.getBlockState(pos));
}
}
}
}
synchronized void observePacket(Minecraft mc, Packet<?> packet) {
if (mc == null || mc.level == null || packet == null) return;
if (packet instanceof net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket blockUpdate) {
observeBlockChange(mc, blockUpdate.getPos(), blockUpdate.getBlockState());
} else if (packet instanceof net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket sectionUpdate) {
try {
sectionUpdate.runUpdates((pos, state) -> {
if (pos != null && state != null) observeBlockChange(mc, pos, state);
});
} catch (Throwable ignored) { }
}
}
private void observeBlockChange(Minecraft mc, BlockPos pos, BlockState newState) {
if (mc == null || mc.level == null || pos == null || newState == null) return;
if (!isNearPlayer(mc, pos)) return;
BlockPos stablePos = new BlockPos(pos.getX(), pos.getY(), pos.getZ());
BlockState oldState = mc.level.getBlockState(stablePos);
if (isTarget(oldState) && !isTarget(newState)) {
countDestroyed(stablePos.asLong());
}
updateObservedPosition(stablePos, newState);
}
private boolean isNearPlayer(Minecraft mc, BlockPos pos) {
if (mc == null || mc.player == null || pos == null) return false;
BlockPos playerPos = mc.player.blockPosition();
int radius = POLL_RADIUS + 4;
return Math.abs(pos.getX() - playerPos.getX()) <= radius
&& Math.abs(pos.getY() - playerPos.getY()) <= radius
&& Math.abs(pos.getZ() - playerPos.getZ()) <= radius;
}
private void updateObservedPosition(BlockPos pos, BlockState state) {
if (pos == null || state == null) return;
long key = pos.asLong();
boolean nowTarget = isTarget(state);
boolean wasTarget = Boolean.TRUE.equals(observedTargetPositions.get(key));
if (nowTarget) {
observedTargetPositions.put(key, Boolean.TRUE);
} else {
if (wasTarget) countDestroyed(key);
observedTargetPositions.remove(key);
}
}
private boolean isTarget(BlockState state) {
return state != null && targetBlocks.contains(state.getBlock());
}
private void countDestroyed(long posKey) {
if (countedPositions.add(posKey)) destroyed++;
}
}
public static void runPayAction(Minecraft mc, PayAction payAction) throws InterruptedException {
payAction.delayMs = PayAction.normalizeDelay(payAction.delayMs);
java.util.List<String> targets = new java.util.ArrayList<>();
for (String player : payAction.players) {
if (player == null) continue;
MacroTemplate.Resolution playerResolution = MacroVariables.resolve(player, mc);
if (!playerResolution.success()) continue;
String trimmed = playerResolution.value().trim();
if (!trimmed.isEmpty() && !targets.contains(trimmed)) targets.add(trimmed);
}
if (targets.isEmpty()) targets.addAll(autismclient.util.AutismPlayerProbe.everyone(mc, payAction.probeHidden));
if (targets.isEmpty() || mc.getConnection() == null) return;
MacroTemplate.Resolution amountResolution = MacroVariables.resolve(payAction.amountInput, mc);
if (!amountResolution.success()) return;
long resolvedAmount = Math.max(0L, PayAction.parseAmount(amountResolution.value()));
long[] dividedAmounts = payAction.divideEnabled ? PayAction.distribute(resolvedAmount, targets.size()) : null;
int sent = 0;
for (int i = 0; i < targets.size(); i++) {
if (!MacroLifecycle.isCurrentRunActive()) break;
String player = targets.get(i);
long perAmount = dividedAmounts != null ? dividedAmounts[i] : resolvedAmount;
if (perAmount <= 0L) continue;
String amountValue = String.valueOf(perAmount);
String amountLabel = PayAction.formatAmount(perAmount);
String template = (payAction.commandTemplate == null || payAction.commandTemplate.isBlank())
? "/pay <player> <amount>"
: payAction.commandTemplate;
String command = template
.replace("<player>", player)
.replace("{player}", player)
.replace("<amount>", amountValue)
.replace("{amount}", amountValue)
.trim();
MacroTemplate.Resolution commandResolution = MacroVariables.resolve(command, mc);
if (!commandResolution.success()) continue;
command = commandResolution.value().trim();
if (command.isEmpty()) continue;
final String commandToSend = command;
if (sent > 0 && payAction.delayEnabled && payAction.delayMs > 0) Thread.sleep(payAction.delayMs);
MacroLifecycle.setCurrentStatus("Paying " + player + " " + amountLabel + " (" + (sent + 1) + "/" + targets.size() + ")");
MacroClientThread.runOnClientThread(mc, () -> {
try {
if (PackHideState.isHardLocked() || mc.getConnection() == null) return;
if (commandToSend.startsWith("/") && commandToSend.length() > 1) {
mc.getConnection().sendCommand(commandToSend.substring(1));
} else {
mc.getConnection().sendChat(commandToSend);
}
} catch (Exception e) {
AutismClientMessaging.sendPrefixed("\u00a7cPay failed: " + e.getMessage());
}
});
sent++;
if (payAction.confirmMacro != null && !payAction.confirmMacro.isBlank()) {
MacroLifecycle.setCurrentStatus("Confirming " + player);
PayAction.runConfirmMacro(payAction.confirmMacro, () -> !MacroLifecycle.isCurrentRunActive());
}
}
}
}

View file

@ -0,0 +1,860 @@
package autismclient.util.macro;
import autismclient.util.AutismClientMessaging;
import net.minecraft.client.Minecraft;
import net.minecraft.world.inventory.ContainerInput;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
final class MacroXCarry {
private MacroXCarry() {
}
public static void runXCarryClickPutIn(Minecraft mc, XCarryAction xsa) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null) return;
if (xsa.requiresCursorStorage()) {
MacroLifecycle.setCurrentStatus("XCarry Cursor");
MacroClientThread.runOnClientThread(mc, () -> xsa.execute(mc));
return;
}
autismclient.util.AutismSharedState sharedState = autismclient.util.AutismSharedState.get();
boolean prevBypass = sharedState.isXCarryArmorBypass();
sharedState.setXCarryArmorBypass(true);
try {
AtomicReference<net.minecraft.world.inventory.AbstractContainerMenu> containerHandlerRef = new AtomicReference<>();
AtomicReference<autismclient.util.AutismContainerTarget> containerTargetRef = new AtomicReference<>();
AtomicReference<List<Integer>> containerSlotIdsRef = new AtomicReference<>(List.of());
AtomicBoolean usingContainer = new AtomicBoolean(false);
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null) return;
net.minecraft.world.inventory.AbstractContainerMenu current = mc.player.containerMenu;
if (current != null && current != mc.player.inventoryMenu) {
usingContainer.set(true);
containerHandlerRef.set(current);
containerTargetRef.set(autismclient.util.AutismSharedState.get().getLastContainerTarget());
containerSlotIdsRef.set(new ArrayList<>(xsa.collectContainerTransferSlots(current, mc.player.inventoryMenu)));
}
});
if (usingContainer.get() && containerTargetRef.get() == null) {
AutismClientMessaging.sendPrefixed("XCarry click mode: missing container target, using fast mode.");
MacroClientThread.runOnClientThread(mc, () -> xsa.execute(mc));
return;
}
if (usingContainer.get()) {
for (int slotId : containerSlotIdsRef.get()) {
if (!MacroLifecycle.isCurrentRunActive()) return;
MacroLifecycle.setCurrentStatus("XCarry Click: collect");
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null) return;
net.minecraft.world.inventory.AbstractContainerMenu handler = containerHandlerRef.get();
if (handler == null || mc.player.containerMenu != handler) return;
if (slotId < 0 || slotId >= handler.slots.size()) return;
if (handler.slots.get(slotId).getItem().isEmpty()) return;
mc.gameMode.handleContainerInput(handler.containerId, slotId, 0, net.minecraft.world.inventory.ContainerInput.QUICK_MOVE, mc.player);
});
MacroExecutor.waitOneTick();
}
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null || mc.getConnection() == null) return;
net.minecraft.world.inventory.AbstractContainerMenu handler = containerHandlerRef.get();
if (handler == null) return;
mc.getConnection().send(new net.minecraft.network.protocol.game.ServerboundContainerClosePacket(handler.containerId));
});
MacroExecutor.waitOneTick();
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player != null) {
mc.player.containerMenu = mc.player.inventoryMenu;
}
});
MacroExecutor.waitOneTick();
}
MacroClientThread.runOnClientThread(mc, xsa::preparePutInStorageGuard);
int xCarryAttempts = 0;
while (MacroLifecycle.isCurrentRunActive() && xCarryAttempts++ < 512) {
XCarryAction.PutInMove move = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null) return null;
return xsa.findNextPutInMove(mc.player.inventoryMenu);
}, null);
if (move == null) break;
MacroLifecycle.setCurrentStatus("XCarry Click -> " + move.targetSlotId + " x" + move.count);
int targetBefore = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null) return 0;
net.minecraft.world.inventory.InventoryMenu handler = mc.player.inventoryMenu;
if (move.targetSlotId < 0 || move.targetSlotId >= handler.slots.size()) return 0;
return handler.slots.get(move.targetSlotId).getItem().getCount();
}, 0);
boolean moved = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null) return false;
net.minecraft.world.inventory.InventoryMenu handler = mc.player.inventoryMenu;
mc.player.containerMenu = handler;
return XCarryAction.movePlannedCount(handler, mc, move.sourceSlotId, move.targetSlotId, move.count);
}, Boolean.FALSE);
if (!moved) break;
boolean placed = waitForXCarryCondition(mc, 500L, () -> {
if (mc.player == null) return false;
net.minecraft.world.inventory.InventoryMenu handler = mc.player.inventoryMenu;
if (move.targetSlotId < 0 || move.targetSlotId >= handler.slots.size()) return false;
return handler.getCarried().isEmpty()
&& handler.slots.get(move.targetSlotId).getItem().getCount() >= targetBefore + move.count;
});
if (!placed) {
rollbackXCarryCursor(mc, move.sourceSlotId);
break;
}
MacroExecutor.waitOneTick();
}
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player != null) {
boolean active = XCarryAction.hasStoredItems(mc.player.inventoryMenu, xsa.carryCursor, xsa.activeStorageSlotIds());
autismclient.util.AutismSharedState shared = autismclient.util.AutismSharedState.get();
if (active) {
shared.mergeXCarryForcedTargets(xsa.activeStorageSlotIds(), xsa.carryCursor);
}
shared.setXCarryForced(active);
shared.setXCarryActive(active);
}
});
if (usingContainer.get()) {
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null) return;
net.minecraft.world.inventory.AbstractContainerMenu containerHandler = containerHandlerRef.get();
if (containerHandler != null) {
mc.player.containerMenu = containerHandler;
}
autismclient.util.AutismContainerTarget containerTarget = containerTargetRef.get();
if (containerTarget != null) {
XCarryAction.sendOpenTarget(mc, containerTarget);
}
});
MacroExecutor.waitOneTick();
}
} finally {
sharedState.setXCarryArmorBypass(prevBypass);
}
}
public static void runXCarrySafeClickPutIn(Minecraft mc, XCarryAction xsa) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null) return;
if (xsa.requiresCursorStorage()) {
MacroLifecycle.setCurrentStatus("XCarry Safe: Cursor");
MacroClientThread.runOnClientThread(mc, () -> xsa.execute(mc));
return;
}
autismclient.util.AutismSharedState sharedState = autismclient.util.AutismSharedState.get();
boolean prevBypass = sharedState.isXCarryArmorBypass();
sharedState.setXCarryArmorBypass(true);
try {
AtomicReference<net.minecraft.world.inventory.AbstractContainerMenu> containerHandlerRef = new AtomicReference<>();
AtomicReference<autismclient.util.AutismContainerTarget> containerTargetRef = new AtomicReference<>();
AtomicReference<List<Integer>> containerSlotIdsRef = new AtomicReference<>(List.of());
AtomicBoolean usingContainer = new AtomicBoolean(false);
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null) return;
net.minecraft.world.inventory.AbstractContainerMenu current = mc.player.containerMenu;
if (current != null && current != mc.player.inventoryMenu) {
usingContainer.set(true);
containerHandlerRef.set(current);
containerTargetRef.set(autismclient.util.AutismSharedState.get().getLastContainerTarget());
containerSlotIdsRef.set(new ArrayList<>(xsa.collectContainerTransferSlots(current, mc.player.inventoryMenu)));
}
});
if (usingContainer.get() && containerTargetRef.get() == null) {
AutismClientMessaging.sendPrefixed("XCarry safe mode: missing container target, using fast mode.");
MacroClientThread.runOnClientThread(mc, () -> xsa.execute(mc));
return;
}
if (usingContainer.get()) {
for (int slotId : containerSlotIdsRef.get()) {
if (!MacroLifecycle.isCurrentRunActive()) return;
MacroLifecycle.setCurrentStatus("XCarry Safe: collect");
sendContainerClickThenTick(mc, containerHandlerRef.get(), slotId, 0, ContainerInput.QUICK_MOVE);
}
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null || mc.getConnection() == null) return;
net.minecraft.world.inventory.AbstractContainerMenu handler = containerHandlerRef.get();
if (handler == null) return;
mc.getConnection().send(new net.minecraft.network.protocol.game.ServerboundContainerClosePacket(handler.containerId));
});
MacroExecutor.waitOneTick();
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player != null) {
mc.player.containerMenu = mc.player.inventoryMenu;
}
});
MacroExecutor.waitOneTick();
}
MacroClientThread.runOnClientThread(mc, xsa::preparePutInStorageGuard);
int safeDelayTicks = XCarryAction.clampSafeClickDelayTicks(xsa.safeClickDelayTicks);
boolean delayAfterPickup = xsa.safeClickDelayAfterPickup;
boolean delayBeforeReturn = xsa.safeClickDelayBeforeReturn;
int attempts = 0;
while (MacroLifecycle.isCurrentRunActive() && attempts++ < 512) {
List<XCarryAction.PutInMove> group = MacroClientThread.callOnClientThread(mc, () -> nextXCarrySafeGroup(mc, xsa), List.of());
if (group.isEmpty()) break;
XCarryAction.PutInMove first = group.get(0);
int total = 0;
for (XCarryAction.PutInMove move : group) total += Math.max(0, move.count());
MacroLifecycle.setCurrentStatus("XCarry Safe -> " + group.size() + " slots x" + total);
if (!executeXCarrySafeGroup(mc, group, safeDelayTicks, delayAfterPickup, delayBeforeReturn)) {
rollbackXCarryCursorSafe(mc, first.sourceSlotId(), safeDelayTicks, delayBeforeReturn);
break;
}
}
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player != null) {
boolean active = XCarryAction.hasStoredItems(mc.player.inventoryMenu, xsa.carryCursor, xsa.activeStorageSlotIds());
autismclient.util.AutismSharedState shared = autismclient.util.AutismSharedState.get();
if (active) {
shared.mergeXCarryForcedTargets(xsa.activeStorageSlotIds(), xsa.carryCursor);
}
shared.setXCarryForced(active);
shared.setXCarryActive(active);
}
});
if (usingContainer.get()) {
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null) return;
net.minecraft.world.inventory.AbstractContainerMenu containerHandler = containerHandlerRef.get();
if (containerHandler != null) {
mc.player.containerMenu = containerHandler;
}
autismclient.util.AutismContainerTarget containerTarget = containerTargetRef.get();
if (containerTarget != null) {
XCarryAction.sendOpenTarget(mc, containerTarget);
}
});
MacroExecutor.waitOneTick();
}
} finally {
sharedState.setXCarryArmorBypass(prevBypass);
}
}
private static List<XCarryAction.PutInMove> nextXCarrySafeGroup(Minecraft mc, XCarryAction xsa) {
if (mc == null || mc.player == null || xsa == null) return List.of();
InventoryMenu handler = mc.player.inventoryMenu;
List<XCarryAction.PutInMove> planned = xsa.planPutInMoves(handler);
if (planned.isEmpty()) return List.of();
XCarryAction.PutInMove first = planned.get(0);
if (first == null) return List.of();
if (first.sourceSlotId() < 0 || first.sourceSlotId() >= handler.slots.size()) return List.of();
ItemStack source = handler.slots.get(first.sourceSlotId()).getItem();
if (source.isEmpty()) return List.of();
ArrayList<XCarryAction.PutInMove> group = new ArrayList<>();
int remainingSource = source.getCount();
for (XCarryAction.PutInMove move : planned) {
if (move == null || move.sourceSlotId() != first.sourceSlotId()) break;
if (move.count() <= 0 || move.count() > remainingSource) break;
if (!canSafelyPlaceXCarryMove(handler, source, move)) break;
group.add(move);
remainingSource -= move.count();
}
return group;
}
private static boolean executeXCarrySafeGroup(Minecraft mc, List<XCarryAction.PutInMove> group, int delayTicks,
boolean delayAfterPickup, boolean delayBeforeReturn) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null || group == null || group.isEmpty()) return false;
SafeGroupSnapshot snapshot = MacroClientThread.callOnClientThread(mc, () -> snapshotXCarrySafeGroup(mc, group), SafeGroupSnapshot.invalid());
if (!snapshot.valid) return false;
SafeGroupSnapshot expected = executeXCarrySafeScript(mc, group, snapshot, delayTicks, delayAfterPickup, delayBeforeReturn);
return expected.valid;
}
private static SafeGroupSnapshot executeXCarrySafeScript(Minecraft mc, List<XCarryAction.PutInMove> group,
SafeGroupSnapshot snapshot,
int delayTicks,
boolean delayAfterPickup,
boolean delayBeforeReturn) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null || group == null || group.isEmpty() || snapshot == null || !snapshot.valid) {
return SafeGroupSnapshot.invalid();
}
XCarryAction.PutInMove first = group.get(0);
if (!Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return handler != null
&& handler.getCarried().isEmpty()
&& xcarrySafeSnapshotStillMatches(handler, group, snapshot);
}, Boolean.FALSE))) {
return SafeGroupSnapshot.invalid();
}
SafeQuickCraftPlan quickCraftPlan = MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return createXCarryQuickCraftPlan(handler, group, snapshot);
}, SafeQuickCraftPlan.invalid());
SafeGroupSnapshot expected = snapshot;
if (quickCraftPlan.valid) {
MacroLifecycle.setCurrentStatus("XCarry Safe drag -> " + group.size() + " slots x" + quickCraftPlan.placeCount);
int start = net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(0, quickCraftPlan.quickCraftType);
int add = net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(1, quickCraftPlan.quickCraftType);
int end = net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(2, quickCraftPlan.quickCraftType);
int nextDragIndex = 0;
if (delayAfterPickup) {
if (!sendXCarrySafeInventoryClick(mc, first.sourceSlotId(), 0, ContainerInput.PICKUP)) {
return SafeGroupSnapshot.invalid();
}
waitXCarrySafeAfterPickupDelay(delayTicks, true);
if (!Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return xcarrySafeCarryingNow(handler, snapshot.sourceStack, snapshot.sourceCount);
}, Boolean.FALSE))) {
return SafeGroupSnapshot.invalid();
}
if (!sendXCarrySafeInventoryClick(mc, -999, start, ContainerInput.QUICK_CRAFT)) return SafeGroupSnapshot.invalid();
} else {
ArrayList<SafeInventoryClick> dragClicks = new ArrayList<>(group.size() + 3);
dragClicks.add(new SafeInventoryClick(first.sourceSlotId(), 0, ContainerInput.PICKUP));
dragClicks.add(new SafeInventoryClick(-999, start, ContainerInput.QUICK_CRAFT));
for (XCarryAction.PutInMove move : group) {
if (move != null) {
dragClicks.add(new SafeInventoryClick(move.targetSlotId(), add, ContainerInput.QUICK_CRAFT));
}
}
dragClicks.add(new SafeInventoryClick(-999, end, ContainerInput.QUICK_CRAFT));
if (!sendXCarrySafeInventoryClickBatch(mc, dragClicks.toArray(SafeInventoryClick[]::new))) {
return SafeGroupSnapshot.invalid();
}
nextDragIndex = group.size();
}
for (int i = nextDragIndex; i < group.size(); i++) {
XCarryAction.PutInMove move = group.get(i);
if (i > nextDragIndex || nextDragIndex > 0) waitXCarrySafeDelayTicks(delayTicks);
if (!sendXCarrySafeInventoryClick(mc, move.targetSlotId(), add, ContainerInput.QUICK_CRAFT)) {
return SafeGroupSnapshot.invalid();
}
}
if (nextDragIndex < group.size()
&& !sendXCarrySafeInventoryClick(mc, -999, end, ContainerInput.QUICK_CRAFT)) {
return SafeGroupSnapshot.invalid();
}
boolean firstResidual = true;
for (java.util.Map.Entry<Integer, Integer> residual : quickCraftPlan.residualCounts.entrySet()) {
int targetSlotId = residual.getKey();
int count = residual.getValue() == null ? 0 : residual.getValue();
if (count <= 0) continue;
if (!firstResidual) waitXCarrySafeDelayTicks(delayTicks);
firstResidual = false;
if (!placeXCarrySafeDirectCount(mc, targetSlotId, count, delayTicks)) {
return SafeGroupSnapshot.invalid();
}
}
expected = snapshot.withExpectedTargetCounts(quickCraftPlan.expectedTargetCounts);
} else {
int nextMoveIndex = 0;
if (delayAfterPickup) {
if (!sendXCarrySafeInventoryClick(mc, first.sourceSlotId(), 0, ContainerInput.PICKUP)) {
return SafeGroupSnapshot.invalid();
}
waitXCarrySafeAfterPickupDelay(delayTicks, true);
} else {
XCarryAction.PutInMove firstMove = group.get(0);
int firstButton = firstMove.count() >= snapshot.sourceCount ? 0 : 1;
if (!sendXCarrySafeInventoryClickBatch(mc,
new SafeInventoryClick(first.sourceSlotId(), 0, ContainerInput.PICKUP),
new SafeInventoryClick(firstMove.targetSlotId(), firstButton, ContainerInput.PICKUP))) {
return SafeGroupSnapshot.invalid();
}
int remainingFirst = firstButton == 0 ? 0 : Math.max(0, firstMove.count() - 1);
if (remainingFirst > 0) {
waitXCarrySafeDelayTicks(delayTicks);
if (!placeXCarrySafeDirectCount(mc, firstMove.targetSlotId(), remainingFirst, delayTicks)) {
return SafeGroupSnapshot.invalid();
}
}
nextMoveIndex = 1;
}
for (int i = nextMoveIndex; i < group.size(); i++) {
XCarryAction.PutInMove move = group.get(i);
if (move == null || move.count() <= 0) continue;
if (!placeXCarrySafeDirectCount(mc, move.targetSlotId(), move.count(), delayTicks)) {
return SafeGroupSnapshot.invalid();
}
}
}
boolean hasLeftover = Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return handler != null && !handler.getCarried().isEmpty();
}, Boolean.FALSE));
if (hasLeftover) {
if (delayBeforeReturn) {
waitXCarrySafeDelayTicks(delayTicks);
}
int returnSlot = MacroClientThread.callOnClientThread(mc, () -> findXCarrySafeReturnSlot(mc, first.sourceSlotId()), -1);
if (returnSlot < 0) return SafeGroupSnapshot.invalid();
if (!sendXCarrySafeInventoryClick(mc, returnSlot, 0, ContainerInput.PICKUP)) return SafeGroupSnapshot.invalid();
}
boolean cursorEmpty = Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return handler != null && handler.getCarried().isEmpty();
}, Boolean.FALSE));
return cursorEmpty ? expected : SafeGroupSnapshot.invalid();
}
private static boolean placeXCarrySafeDirectCount(Minecraft mc, int targetSlotId, int count, int delayTicks) throws InterruptedException {
if (mc == null || count <= 0) return false;
int remaining = count;
while (remaining > 0 && MacroLifecycle.isCurrentRunActive()) {
int carriedCount = MacroClientThread.callOnClientThread(mc, () -> {
InventoryMenu handler = mc.player == null ? null : mc.player.inventoryMenu;
return handler == null ? 0 : handler.getCarried().getCount();
}, 0);
if (carriedCount <= 0) return false;
int button = remaining >= carriedCount ? 0 : 1;
if (!sendXCarrySafeInventoryClick(mc, targetSlotId, button, ContainerInput.PICKUP)) {
return false;
}
if (button == 0) {
remaining = 0;
} else {
remaining--;
}
if (remaining > 0) {
waitXCarrySafeDelayTicks(delayTicks);
}
}
return remaining <= 0;
}
private static SafeGroupSnapshot snapshotXCarrySafeGroup(Minecraft mc, List<XCarryAction.PutInMove> group) {
if (mc == null || mc.player == null || group == null || group.isEmpty()) return SafeGroupSnapshot.invalid();
InventoryMenu handler = mc.player.inventoryMenu;
if (handler == null || !handler.getCarried().isEmpty()) return SafeGroupSnapshot.invalid();
XCarryAction.PutInMove first = group.get(0);
if (first.sourceSlotId() < 0 || first.sourceSlotId() >= handler.slots.size()) return SafeGroupSnapshot.invalid();
Slot sourceSlot = handler.slots.get(first.sourceSlotId());
ItemStack source = sourceSlot.getItem();
if (source.isEmpty()) return SafeGroupSnapshot.invalid();
int total = 0;
java.util.LinkedHashMap<Integer, Integer> startCounts = new java.util.LinkedHashMap<>();
java.util.LinkedHashMap<Integer, Integer> expectedCounts = new java.util.LinkedHashMap<>();
for (XCarryAction.PutInMove move : group) {
if (move == null || move.sourceSlotId() != first.sourceSlotId()) return SafeGroupSnapshot.invalid();
if (!canSafelyPlaceXCarryMove(handler, source, move)) return SafeGroupSnapshot.invalid();
int start = startCounts.computeIfAbsent(move.targetSlotId(), slotId -> safeSlotCount(handler, slotId));
if (start < 0) return SafeGroupSnapshot.invalid();
expectedCounts.put(move.targetSlotId(), expectedCounts.getOrDefault(move.targetSlotId(), start) + move.count());
total += move.count();
}
if (total <= 0 || total > source.getCount()) return SafeGroupSnapshot.invalid();
return new SafeGroupSnapshot(true, source.copy(), source.getCount(), startCounts, expectedCounts);
}
private static boolean canSafelyPlaceXCarryMove(InventoryMenu handler, ItemStack source, XCarryAction.PutInMove move) {
if (handler == null || source == null || source.isEmpty() || move == null || move.count() <= 0) return false;
if (move.targetSlotId() < 0 || move.targetSlotId() >= handler.slots.size()) return false;
if (move.sourceSlotId() == move.targetSlotId()) return false;
Slot targetSlot = handler.slots.get(move.targetSlotId());
if (targetSlot == null || !targetSlot.mayPlace(source)) return false;
ItemStack target = targetSlot.getItem();
if (!target.isEmpty() && !ItemStack.isSameItemSameComponents(target, source)) return false;
int current = target.isEmpty() ? 0 : target.getCount();
int max = Math.min(source.getMaxStackSize(), targetSlot.getMaxStackSize(source));
return current + move.count() <= max;
}
private static boolean xcarrySafeCarryingNow(InventoryMenu handler, ItemStack expected, int count) {
if (handler == null || expected == null || expected.isEmpty()) return false;
ItemStack carried = handler.getCarried();
return !carried.isEmpty()
&& carried.getCount() == count
&& ItemStack.isSameItemSameComponents(carried, expected);
}
private static boolean xcarrySafeSnapshotStillMatches(InventoryMenu handler, List<XCarryAction.PutInMove> group, SafeGroupSnapshot snapshot) {
if (handler == null || group == null || snapshot == null || !snapshot.valid) return false;
XCarryAction.PutInMove first = group.isEmpty() ? null : group.get(0);
if (first == null || first.sourceSlotId() < 0 || first.sourceSlotId() >= handler.slots.size()) return false;
ItemStack source = handler.slots.get(first.sourceSlotId()).getItem();
if (source.isEmpty()
|| source.getCount() != snapshot.sourceCount
|| !ItemStack.isSameItemSameComponents(source, snapshot.sourceStack)) {
return false;
}
for (XCarryAction.PutInMove move : group) {
if (move == null || move.sourceSlotId() != first.sourceSlotId()) return false;
int current = safeSlotCount(handler, move.targetSlotId());
Integer expectedStart = snapshot.startTargetCounts.get(move.targetSlotId());
if (expectedStart == null || current != expectedStart) return false;
if (!canSafelyPlaceXCarryMove(handler, source, move)) return false;
}
return true;
}
private static SafeQuickCraftPlan createXCarryQuickCraftPlan(InventoryMenu handler, List<XCarryAction.PutInMove> group, SafeGroupSnapshot snapshot) {
if (handler == null || group == null || group.size() < 2 || snapshot == null || !snapshot.valid) {
return SafeQuickCraftPlan.invalid();
}
java.util.LinkedHashMap<Integer, Integer> desiredAdds = new java.util.LinkedHashMap<>();
for (XCarryAction.PutInMove move : group) {
if (move == null || move.count() <= 0) return SafeQuickCraftPlan.invalid();
if (move.targetSlotId() < 0 || move.targetSlotId() >= handler.slots.size()) return SafeQuickCraftPlan.invalid();
Slot targetSlot = handler.slots.get(move.targetSlotId());
if (!isXCarryQuickCraftTarget(handler, targetSlot, snapshot.sourceStack)) return SafeQuickCraftPlan.invalid();
int start = snapshot.startTargetCounts.getOrDefault(move.targetSlotId(), safeSlotCount(handler, move.targetSlotId()));
int finalForMove = snapshot.expectedTargetCounts.getOrDefault(move.targetSlotId(), start);
int desiredAdd = finalForMove - start;
if (desiredAdd <= 0) return SafeQuickCraftPlan.invalid();
int max = Math.min(snapshot.sourceStack.getMaxStackSize(), targetSlot.getMaxStackSize(snapshot.sourceStack));
if (start + desiredAdd > max) return SafeQuickCraftPlan.invalid();
desiredAdds.put(move.targetSlotId(), desiredAdd);
}
SafeQuickCraftPlan best = SafeQuickCraftPlan.invalid();
SafeQuickCraftPlan even = createXCarryQuickCraftCandidate(handler, snapshot, desiredAdds, 0);
SafeQuickCraftPlan rightDrag = createXCarryQuickCraftCandidate(handler, snapshot, desiredAdds, 1);
if (even.valid) best = even;
if (rightDrag.valid && (!best.valid || rightDrag.cost < best.cost)) best = rightDrag;
return best;
}
private static SafeQuickCraftPlan createXCarryQuickCraftCandidate(InventoryMenu handler, SafeGroupSnapshot snapshot,
java.util.LinkedHashMap<Integer, Integer> desiredAdds,
int quickCraftType) {
if (handler == null || snapshot == null || !snapshot.valid || desiredAdds == null || desiredAdds.size() < 2) {
return SafeQuickCraftPlan.invalid();
}
int slotCount = desiredAdds.size();
int placeCount = switch (quickCraftType) {
case 0 -> snapshot.sourceCount / slotCount;
case 1 -> 1;
default -> 0;
};
if (placeCount <= 0 || snapshot.sourceCount < placeCount * slotCount) {
return SafeQuickCraftPlan.invalid();
}
java.util.LinkedHashMap<Integer, Integer> residual = new java.util.LinkedHashMap<>();
java.util.LinkedHashMap<Integer, Integer> expected = new java.util.LinkedHashMap<>();
int residualTotal = 0;
for (java.util.Map.Entry<Integer, Integer> entry : desiredAdds.entrySet()) {
int targetSlotId = entry.getKey();
int desiredAdd = entry.getValue();
if (desiredAdd < placeCount) return SafeQuickCraftPlan.invalid();
Slot targetSlot = targetSlotId >= 0 && targetSlotId < handler.slots.size() ? handler.slots.get(targetSlotId) : null;
if (!isXCarryQuickCraftTarget(handler, targetSlot, snapshot.sourceStack)) return SafeQuickCraftPlan.invalid();
int start = snapshot.startTargetCounts.getOrDefault(targetSlotId, safeSlotCount(handler, targetSlotId));
int max = Math.min(snapshot.sourceStack.getMaxStackSize(), targetSlot.getMaxStackSize(snapshot.sourceStack));
if (start + placeCount > max) return SafeQuickCraftPlan.invalid();
int leftover = desiredAdd - placeCount;
if (leftover > 0) {
residual.put(targetSlotId, leftover);
residualTotal += leftover;
}
expected.put(targetSlotId, start + desiredAdd);
}
int carriedAfterQuickCraft = snapshot.sourceCount - placeCount * slotCount;
if (residualTotal > carriedAfterQuickCraft) return SafeQuickCraftPlan.invalid();
int cost = slotCount + 2 + residualTotal;
return new SafeQuickCraftPlan(true, quickCraftType, placeCount, cost, residual, expected);
}
private static boolean isXCarryQuickCraftTarget(InventoryMenu handler, Slot targetSlot, ItemStack sourceStack) {
if (handler == null || targetSlot == null || sourceStack == null || sourceStack.isEmpty()) return false;
if (targetSlot.index == XCarryAction.CRAFT_RESULT_SLOT) return false;
if (!targetSlot.mayPlace(sourceStack) || !handler.canDragTo(targetSlot)) return false;
return net.minecraft.world.inventory.AbstractContainerMenu.canItemQuickReplace(targetSlot, sourceStack, true);
}
private record SafeInventoryClick(int slotId, int button, ContainerInput input) {}
private static boolean sendXCarrySafeInventoryClick(Minecraft mc, int slotId, int button, ContainerInput input) throws InterruptedException {
return sendXCarrySafeInventoryClickBatch(mc, new SafeInventoryClick(slotId, button, input));
}
private static boolean sendXCarrySafeInventoryClickBatch(Minecraft mc, SafeInventoryClick... clicks) throws InterruptedException {
if (mc == null || clicks == null || clicks.length == 0) return false;
return Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null) return false;
InventoryMenu handler = mc.player.inventoryMenu;
if (handler == null) return false;
mc.player.containerMenu = handler;
for (SafeInventoryClick click : clicks) {
if (click == null || click.input == null) return false;
if (click.slotId != -999 && (click.slotId < 0 || click.slotId >= handler.slots.size())) return false;
mc.gameMode.handleContainerInput(handler.containerId, click.slotId, click.button, click.input, mc.player);
}
return true;
}, Boolean.FALSE));
}
private static void waitXCarrySafeDelayTicks(int delayTicks) {
int ticks = XCarryAction.clampSafeClickDelayTicks(delayTicks);
for (int i = 0; i < ticks && MacroLifecycle.isCurrentRunActive(); i++) {
MacroExecutor.waitOneTick();
}
}
private static void waitXCarrySafeAfterPickupDelay(int delayTicks, boolean enabled) {
if (enabled) {
waitXCarrySafeDelayTicks(delayTicks);
}
}
private static boolean sendInventoryClickThenTick(Minecraft mc, int slotId, int button, ContainerInput input) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null) return false;
boolean clicked = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null) return false;
InventoryMenu handler = mc.player.inventoryMenu;
mc.player.containerMenu = handler;
if (slotId < 0 || slotId >= handler.slots.size()) return false;
mc.gameMode.handleContainerInput(handler.containerId, slotId, button, input, mc.player);
return true;
}, Boolean.FALSE);
if (!clicked) return false;
MacroExecutor.waitOneTick();
return true;
}
private static boolean sendContainerClickThenTick(Minecraft mc, net.minecraft.world.inventory.AbstractContainerMenu handler,
int slotId, int button, ContainerInput input) throws InterruptedException {
if (mc == null || handler == null || input == null) return false;
boolean clicked = MacroClientThread.callOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null || mc.player.containerMenu != handler) return false;
if (slotId < 0 || slotId >= handler.slots.size()) return false;
if (handler.slots.get(slotId).getItem().isEmpty()) return false;
mc.gameMode.handleContainerInput(handler.containerId, slotId, button, input, mc.player);
return true;
}, Boolean.FALSE);
if (!clicked) return false;
MacroExecutor.waitOneTick();
return true;
}
private static boolean waitForXCarrySafeLocal(Minecraft mc, int ticks, java.util.function.BooleanSupplier accepted) throws InterruptedException {
int maxTicks = Math.max(1, ticks);
for (int i = 0; i < maxTicks && MacroLifecycle.isCurrentRunActive(); i++) {
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> accepted != null && accepted.getAsBoolean(), Boolean.FALSE))) return true;
MacroExecutor.waitOneTick();
}
return Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> accepted != null && accepted.getAsBoolean(), Boolean.FALSE));
}
private static void waitForXCarrySafeSettle(Minecraft mc, long startRevision, int minTicks, long maxMs) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(Math.max(50L, maxMs));
int ticks = Math.max(1, minTicks);
for (int i = 0; i < ticks && MacroLifecycle.isCurrentRunActive(); i++) MacroExecutor.waitOneTick();
while (MacroLifecycle.isCurrentRunActive() && System.nanoTime() < deadline) {
long revision = MacroConditionRegistry.inventorySyncRevision();
if (revision != startRevision) {
MacroExecutor.waitOneTick();
return;
}
MacroExecutor.waitOneTick();
}
}
private static boolean waitForXCarrySafeFinalState(Minecraft mc, SafeGroupSnapshot snapshot, long startRevision) throws InterruptedException {
waitForXCarrySafeSettle(mc, startRevision, 2, 250L);
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> xcarrySafeFinalMatches(mc, snapshot), Boolean.FALSE))) return true;
waitForXCarrySafeSettle(mc, MacroConditionRegistry.inventorySyncRevision(), 3, 350L);
return Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, () -> xcarrySafeFinalMatches(mc, snapshot), Boolean.FALSE));
}
private static boolean xcarrySafeFinalMatches(Minecraft mc, SafeGroupSnapshot snapshot) {
if (mc == null || mc.player == null || snapshot == null || !snapshot.valid) return false;
InventoryMenu handler = mc.player.inventoryMenu;
if (handler == null || !handler.getCarried().isEmpty()) return false;
for (java.util.Map.Entry<Integer, Integer> entry : snapshot.expectedTargetCounts.entrySet()) {
int slotId = entry.getKey();
if (slotId < 0 || slotId >= handler.slots.size()) return false;
ItemStack stack = handler.slots.get(slotId).getItem();
if (entry.getValue() <= 0) {
if (!stack.isEmpty()) return false;
continue;
}
if (stack.isEmpty() || !ItemStack.isSameItemSameComponents(stack, snapshot.sourceStack)) return false;
if (stack.getCount() != entry.getValue()) return false;
}
return true;
}
private static boolean returnXCarrySafeLeftovers(Minecraft mc, int preferredSlotId, ItemStack expected,
int delayTicks, boolean delayBeforeReturn) throws InterruptedException {
if (mc == null || mc.player == null || expected == null || expected.isEmpty()) return false;
if (MacroClientThread.callOnClientThread(mc, () -> mc.player != null && mc.player.inventoryMenu.getCarried().isEmpty(), Boolean.FALSE)) {
return true;
}
int returnSlot = MacroClientThread.callOnClientThread(mc, () -> findXCarrySafeReturnSlot(mc, preferredSlotId), -1);
if (returnSlot < 0) return false;
if (delayBeforeReturn) waitXCarrySafeDelayTicks(delayTicks);
return sendInventoryClickThenTick(mc, returnSlot, 0, ContainerInput.PICKUP)
&& waitForXCarrySafeLocal(mc, 3, () -> mc.player != null && mc.player.inventoryMenu.getCarried().isEmpty());
}
private static int findXCarrySafeReturnSlot(Minecraft mc, int preferredSlotId) {
if (mc == null || mc.player == null) return -1;
InventoryMenu handler = mc.player.inventoryMenu;
ItemStack carried = handler.getCarried();
if (carried.isEmpty()) return -1;
if (canAcceptXCarryLeftover(handler, preferredSlotId, carried)) return preferredSlotId;
for (Slot slot : handler.slots) {
if (slot == null) continue;
if (slot.index < 9 || slot.index == 45) continue;
if (canAcceptXCarryLeftover(handler, slot.index, carried)) return slot.index;
}
return -1;
}
private static boolean canAcceptXCarryLeftover(InventoryMenu handler, int slotId, ItemStack carried) {
if (handler == null || carried == null || carried.isEmpty()) return false;
if (slotId < 0 || slotId >= handler.slots.size()) return false;
Slot slot = handler.slots.get(slotId);
if (slot == null || !slot.mayPlace(carried)) return false;
ItemStack stack = slot.getItem();
if (stack.isEmpty()) return carried.getCount() <= slot.getMaxStackSize(carried);
return ItemStack.isSameItemSameComponents(stack, carried)
&& stack.getCount() + carried.getCount() <= Math.min(carried.getMaxStackSize(), slot.getMaxStackSize(carried));
}
private static int safeSlotCount(Minecraft mc, int slotId) {
if (mc == null || mc.player == null) return -1;
return safeSlotCount(mc.player.inventoryMenu, slotId);
}
private static int safeSlotCount(InventoryMenu handler, int slotId) {
if (handler == null) return -1;
if (slotId < 0 || slotId >= handler.slots.size()) return -1;
ItemStack stack = handler.slots.get(slotId).getItem();
return stack.isEmpty() ? 0 : stack.getCount();
}
private static void rollbackXCarryCursorSafe(Minecraft mc, int sourceSlotId, int delayTicks, boolean delayBeforeReturn) throws InterruptedException {
if (mc == null || mc.player == null || mc.gameMode == null) return;
if (MacroClientThread.callOnClientThread(mc, () -> mc.player != null && mc.player.inventoryMenu.getCarried().isEmpty(), Boolean.FALSE)) return;
int returnSlot = MacroClientThread.callOnClientThread(mc, () -> findXCarrySafeReturnSlot(mc, sourceSlotId), -1);
if (returnSlot >= 0) {
if (delayBeforeReturn) waitXCarrySafeDelayTicks(delayTicks);
sendInventoryClickThenTick(mc, returnSlot, 0, ContainerInput.PICKUP);
waitForXCarrySafeLocal(mc, 3, () -> mc.player != null && mc.player.inventoryMenu.getCarried().isEmpty());
} else {
rollbackXCarryCursor(mc, sourceSlotId);
}
}
private static final class SafeGroupSnapshot {
final boolean valid;
final ItemStack sourceStack;
final int sourceCount;
final java.util.Map<Integer, Integer> startTargetCounts;
final java.util.Map<Integer, Integer> expectedTargetCounts;
SafeGroupSnapshot(boolean valid, ItemStack sourceStack, int sourceCount,
java.util.Map<Integer, Integer> startTargetCounts,
java.util.Map<Integer, Integer> expectedTargetCounts) {
this.valid = valid;
this.sourceStack = sourceStack == null ? ItemStack.EMPTY : sourceStack;
this.sourceCount = sourceCount;
this.startTargetCounts = startTargetCounts == null ? java.util.Map.of() : java.util.Map.copyOf(startTargetCounts);
this.expectedTargetCounts = expectedTargetCounts == null ? java.util.Map.of() : java.util.Map.copyOf(expectedTargetCounts);
}
SafeGroupSnapshot withExpectedTargetCounts(java.util.Map<Integer, Integer> expectedTargetCounts) {
return new SafeGroupSnapshot(valid, sourceStack, sourceCount, startTargetCounts, expectedTargetCounts);
}
static SafeGroupSnapshot invalid() {
return new SafeGroupSnapshot(false, ItemStack.EMPTY, 0, java.util.Map.of(), java.util.Map.of());
}
}
private static final class SafeQuickCraftPlan {
final boolean valid;
final int quickCraftType;
final int placeCount;
final int cost;
final java.util.Map<Integer, Integer> residualCounts;
final java.util.Map<Integer, Integer> expectedTargetCounts;
SafeQuickCraftPlan(boolean valid, int quickCraftType, int placeCount, int cost,
java.util.Map<Integer, Integer> residualCounts,
java.util.Map<Integer, Integer> expectedTargetCounts) {
this.valid = valid;
this.quickCraftType = quickCraftType;
this.placeCount = placeCount;
this.cost = cost;
this.residualCounts = residualCounts == null
? java.util.Map.of()
: java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(residualCounts));
this.expectedTargetCounts = expectedTargetCounts == null
? java.util.Map.of()
: java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(expectedTargetCounts));
}
static SafeQuickCraftPlan invalid() {
return new SafeQuickCraftPlan(false, 0, 0, Integer.MAX_VALUE, java.util.Map.of(), java.util.Map.of());
}
}
private static void rollbackXCarryCursor(Minecraft mc, int sourceSlotId) throws InterruptedException {
MacroClientThread.runOnClientThread(mc, () -> {
if (mc.player == null || mc.gameMode == null) return;
net.minecraft.world.inventory.InventoryMenu handler = mc.player.inventoryMenu;
mc.player.containerMenu = handler;
if (handler.getCarried().isEmpty()) return;
if (sourceSlotId < 0 || sourceSlotId >= handler.slots.size()) return;
mc.gameMode.handleContainerInput(handler.containerId, sourceSlotId, 0, net.minecraft.world.inventory.ContainerInput.PICKUP, mc.player);
});
waitForXCarryCondition(mc, 300L, () -> mc.player != null && mc.player.inventoryMenu.getCarried().isEmpty());
}
private static boolean waitForXCarryCondition(Minecraft mc, long timeoutMs, java.util.function.Supplier<Boolean> condition) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
while (MacroLifecycle.isCurrentRunActive()) {
if (Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, condition, Boolean.FALSE))) {
return true;
}
if (System.nanoTime() >= deadline) break;
MacroExecutor.waitOneTick();
}
return Boolean.TRUE.equals(MacroClientThread.callOnClientThread(mc, condition, Boolean.FALSE));
}
}