diff --git a/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java b/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java index 75b8f07..d3edde7 100644 --- a/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java +++ b/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java @@ -200,7 +200,7 @@ final class MacroCaptureAutofill { MacroExecutor.setCurrentStatus("Requesting command suggestions"); PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs); if (pending == null) return; - if (!MacroExecutor.waitForFutureDone(pending.future(), 0)) return; + if (!MacroRace.waitForFutureDone(pending.future(), 0)) return; java.util.List suggestions = pending.future().getNow(null); if (suggestions == null || suggestions.isEmpty()) return; applyCaptureSuggestions(run, action, capturePool(connection, action, query, suggestions), query); @@ -307,7 +307,7 @@ final class MacroCaptureAutofill { } })); - MacroExecutor.waitForFutureDone(done, 0); + MacroRace.waitForFutureDone(done, 0); if (result.get().success() || System.nanoTime() >= deadline) break; MacroExecutor.waitOneTick(); } diff --git a/src/main/java/autismclient/util/macro/MacroExecutor.java b/src/main/java/autismclient/util/macro/MacroExecutor.java index a29b8bf..4bcc7d2 100644 --- a/src/main/java/autismclient/util/macro/MacroExecutor.java +++ b/src/main/java/autismclient/util/macro/MacroExecutor.java @@ -85,7 +85,7 @@ public class MacroExecutor { static final long CAPTURE_SETTLE_MS = 300L; private static final AtomicBoolean isRunning = new AtomicBoolean(false); public static final int MAX_CONCURRENT_MACROS = 4; - private static final Object RUN_LOCK = new Object(); + static final Object RUN_LOCK = new Object(); private static final EnumMap SHARED_RESOURCE_LOCKS = new EnumMap<>(SharedClientResource.class); static { for (SharedClientResource resource : SharedClientResource.values()) { @@ -93,7 +93,7 @@ public class MacroExecutor { } } private static final AtomicLong NEXT_RUN_ID = new AtomicLong(1L); - private static final java.util.LinkedHashMap ACTIVE_RUNS = new java.util.LinkedHashMap<>(); + static final java.util.LinkedHashMap ACTIVE_RUNS = new java.util.LinkedHashMap<>(); private static final java.util.concurrent.ConcurrentHashMap RUNS_BY_ID = new java.util.concurrent.ConcurrentHashMap<>(); private static final AtomicLong RUN_STATE_REVISION = new AtomicLong(); @@ -245,7 +245,7 @@ public class MacroExecutor { } private record BreakStartPlan(int result, boolean instant, boolean interactFirst) {} private record BreakContinuePlan(int result, boolean breaksThisTick, boolean breaksNextTick) {} - private record MacroEvent(Packet packet, String direction, long nanoTime, ChatCapture chatCapture, + record MacroEvent(Packet packet, String direction, long nanoTime, ChatCapture chatCapture, java.util.Map captures) { MacroEvent(Packet packet, String direction, long nanoTime, ChatCapture chatCapture) { this(packet, direction, nanoTime, chatCapture, java.util.Map.of()); @@ -440,11 +440,11 @@ public class MacroExecutor { return MacroActionResources.resourcesForAction(action); } - private static EnumSet acquireClientResources(MacroAction action) throws InterruptedException { + static EnumSet acquireClientResources(MacroAction action) throws InterruptedException { return acquireClientResources(resourcesForAction(action)); } - private static EnumSet acquireClientResources(EnumSet resources) throws InterruptedException { + static EnumSet acquireClientResources(EnumSet resources) throws InterruptedException { EnumSet acquired = EnumSet.noneOf(SharedClientResource.class); try { for (SharedClientResource resource : SharedClientResource.values()) { @@ -462,11 +462,11 @@ public class MacroExecutor { return acquired; } - private static EnumSet resourcesForActions(java.util.List actions) { + static EnumSet resourcesForActions(java.util.List actions) { return MacroActionResources.resourcesForActions(actions); } - private static void releaseClientResources(EnumSet resources) { + static void releaseClientResources(EnumSet resources) { if (resources == null || resources.isEmpty()) return; SharedClientResource[] ordered = SharedClientResource.values(); for (int i = ordered.length - 1; i >= 0; i--) { @@ -1161,7 +1161,7 @@ public class MacroExecutor { int startTarget = 0; if (prearmed != null) { try { - waitForFutureDone(prearmed.future, 0); + MacroRace.waitForFutureDone(prearmed.future, 0); startTarget = prearmed.completedPacketTargets; } finally { prearmed.cleanup(); @@ -1366,15 +1366,15 @@ public class MacroExecutor { break; } else if (action instanceof RaceAction race) { - int bodyCount = runRaceGroup(mc, module, macro.actions, i, race); + int bodyCount = MacroRace.runRaceGroup(mc, module, macro.actions, i, race); if (bodyCount > 0) i += bodyCount; } else if (action instanceof MultiAction multi) { - int stepCount = runMultiGroup(mc, module, macro.name, macro.actions, i, multi); + int stepCount = MacroRace.runMultiGroup(mc, module, macro.name, macro.actions, i, multi); if (stepCount > 0) i += stepCount; } else if (action instanceof ReportAction reportAction) { - int conditionCount = runReportGroup(mc, macro.actions, i, reportAction); + int conditionCount = MacroRace.runReportGroup(mc, macro.actions, i, reportAction); if (conditionCount > 0) i += conditionCount; } else if (action instanceof NbtBookAction nba) { @@ -1541,8 +1541,8 @@ public class MacroExecutor { setCurrentStatus(teleport.getDisplayName()); try { if (!MacroPrearm.waitForPrearmed(teleport, 0)) { - CompletableFuture future = raceConditionFuture(mc, teleport); - waitForFutureDone(future, 0); + CompletableFuture future = MacroRace.raceConditionFuture(mc, teleport); + MacroRace.waitForFutureDone(future, 0); } } catch (CancellationException e) { } } @@ -1551,7 +1551,7 @@ public class MacroExecutor { try { if (!MacroPrearm.waitForPrearmed(gm, gm.timeoutMs)) { CompletableFuture future = MacroConditionRegistry.waitForGamemodeChange(gm); - waitForFutureDone(future, gm.timeoutMs); + MacroRace.waitForFutureDone(future, gm.timeoutMs); } } catch (CancellationException e) { } } @@ -1559,8 +1559,8 @@ public class MacroExecutor { setCurrentStatus(wm.getDisplayName()); try { if (!MacroPrearm.waitForPrearmed(wm, 0)) { - CompletableFuture future = raceConditionFuture(mc, wm.resolveSubAction()); - waitForFutureDone(future, 0); + CompletableFuture future = MacroRace.raceConditionFuture(mc, wm.resolveSubAction()); + MacroRace.waitForFutureDone(future, 0); } } catch (CancellationException e) { } } @@ -1577,7 +1577,7 @@ public class MacroExecutor { else if (action instanceof WaitForMacroStepAction) { WaitForMacroStepAction wma = (WaitForMacroStepAction) action; setCurrentStatus(wma.getDisplayName()); - if (!MacroPrearm.waitForPrearmed(wma, wma.timeoutMs)) waitForMacroStep(wma); + if (!MacroPrearm.waitForPrearmed(wma, wma.timeoutMs)) MacroRace.waitForMacroStep(wma); } else if (action instanceof StopMacroAction) { setCurrentStatus("Stopping Macro"); @@ -1845,11 +1845,11 @@ public class MacroExecutor { } catch (InterruptedException e) { AutismClientMessaging.sendPrefixed("\u00a7eRollback canceled safely."); } catch (Exception e) { - AutismClientMessaging.sendPrefixed("\u00a7cRollback failed: " + safeThrowableMessage(e)); + AutismClientMessaging.sendPrefixed("\u00a7cRollback failed: " + MacroRace.safeThrowableMessage(e)); } } else if (action instanceof PacketGateAction pga) { - runPacketGateAction(mc, pga); + MacroRace.runPacketGateAction(mc, pga); } else if (isTargetWaitInteractionAction(action)) { postGuiFuture = runTargetWaitInteractionAction(mc, action); @@ -2001,7 +2001,7 @@ public class MacroExecutor { AutismClientAddon.LOG.warn("[Autism] Macro crashed: {}", macro.name, e); } finally { if (!PackHideState.isHardLocked()) { - runFinallyActions(runState, mc); + MacroRace.runFinallyActions(runState, mc); } PacketGateManager.clearOwnerAndFlushConfigured(runState.id, mc == null ? null : mc.getConnection()); finishCurrentRun(); @@ -2073,7 +2073,7 @@ public class MacroExecutor { } } - private static void waitForPredicate(java.util.function.BooleanSupplier predicate, int timeoutMs) { + static void waitForPredicate(java.util.function.BooleanSupplier predicate, int timeoutMs) { long start = System.currentTimeMillis(); while (isCurrentRunActive()) { try { @@ -2115,908 +2115,12 @@ public class MacroExecutor { - private 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 -> { - 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(); - 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, - currentRunId(), - mc == null ? null : mc.getConnection(), - action.flushOnDisable); - } - - private static void runFinallyActions(RunState run, Minecraft mc) { - if (run == null || run.finallyActions.isEmpty()) return; - ArrayList actions = new ArrayList<>(run.finallyActions); - run.finallyActions.clear(); - for (MacroAction action : actions) { - if (action == null || !action.isEnabled()) continue; - try { - executeSingleActionWithWaits(mc, action, AutismModule.get()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } catch (Throwable t) { - AutismClientMessaging.sendPrefixed("\u00a7cCleanup failed: " + safeThrowableMessage(t)); - } - } - } - - private static void waitForMacroStep(WaitForMacroStepAction action) { - long startMs = System.currentTimeMillis(); - while (isCurrentRunActive()) { - if (isMacroStepSatisfied(action)) return; - if (action.timeoutMs > 0 && System.currentTimeMillis() - startMs >= action.timeoutMs) return; - CompletableFuture tick = MacroConditionRegistry.waitForNextTick(); - waitForCondition(tick); - } - } - - static boolean isMacroStepSatisfied(WaitForMacroStepAction action) { - String target = action.macroName == null ? "" : action.macroName.trim(); - if (target.isEmpty()) return true; - synchronized (RUN_LOCK) { - for (RunState run : 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; - } - - private static int runRaceGroup(Minecraft mc, AutismModule module, java.util.List actions, int headerIndex, RaceAction race) throws InterruptedException { - int bodyCount = race.normalizedBodyCount(actions, headerIndex); - - java.util.List conditions = new ArrayList<>(); - java.util.List 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); - } - - setCurrentStatus(conditions.isEmpty() - ? "Race: dispatching" - : "Race: waiting for " + (conditions.size() == 1 ? conditions.get(0).getDisplayName() : conditions.size() + " conditions")); - - RunState run = CURRENT_RUN.get(); - CompletableFuture 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)); - setCurrentStatus("Race: timeout"); - return bodyCount; - } - - setCurrentStatus("Race: fired"); - waitForTimedFuture(bodyFuture, 250); - return bodyCount; - } - - private static CompletableFuture raceTriggerBarrier(Minecraft mc, java.util.List conditions) { - if (conditions == null || conditions.isEmpty()) return CompletableFuture.completedFuture(null); - RunState run = CURRENT_RUN.get(); - - java.util.List> baseFutures = new ArrayList<>(); - java.util.List> futures = new ArrayList<>(); - for (MacroAction condition : conditions) { - MacroPrearm.clearOwnCaptureOutput(condition); - CompletableFuture 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 captures = new java.util.LinkedHashMap<>(event.captures()); - captures.put(name, value); - return new MacroEvent(event.packet(), event.direction(), event.nanoTime(), event.chatCapture(), captures); - })); - } - CompletableFuture first = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0])); - CompletableFuture out = new CompletableFuture<>(); - first.whenComplete((value, e) -> { - if (e != null) { - out.completeExceptionally(e); - return; - } - if (value instanceof MacroEvent event && run != null && !event.captures().isEmpty()) { - run.variables.setAll(event.captures()); - } - out.complete(value); - }); - out.whenComplete((v, e) -> { - for (CompletableFuture future : baseFutures) { - if (!future.isDone()) future.cancel(true); - } - }); - return out; - } - - private static int runReportGroup(Minecraft mc, java.util.List actions, int headerIndex, ReportAction report) throws InterruptedException { - int conditionCount = report.normalizedConditionCount(actions, headerIndex); - if (conditionCount < 2 || headerIndex + 2 >= actions.size()) { - 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(); - setCurrentStatus("Report: start " + startCondition.getDisplayName()); - CompletableFuture startAnchor = startActionAnchorFuture(startCondition); - executeSingleActionWithWaits(mc, startCondition, null); - if (!isCurrentRunActive()) return conditionCount; - MacroEvent startEvent = waitForAnchorOrNow(startAnchor, 250, startCondition.getDisplayName(), "client action"); - - setCurrentStatus("Report: waiting end " + endCondition.getDisplayName()); - CompletableFuture endFuture = timedConditionFuture(mc, endCondition); - 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(); - 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; - } - - MacroEvent startEvent = new MacroEvent(null, "condition " + startCondition.getDisplayName(), System.nanoTime(), null); - long spentMs = (startEvent.nanoTime() - startWaitNanos) / 1_000_000L; - int remainingMs = (int) Math.max(1, timeoutMs - spentMs); - setCurrentStatus("Report: waiting end " + endCondition.getDisplayName()); - CompletableFuture endFuture = timedConditionFuture(mc, endCondition); - 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); - } - - private static int runMultiGroup(Minecraft mc, AutismModule module, String macroName, java.util.List 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 scope = new java.util.LinkedHashSet<>(); - java.util.Set 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()) { - 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 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 (!isCurrentRunActive()) return stepCount; - - int waitMs = Math.max(0, multi.waitMs); - if (MultiAction.WAIT_UNTIL_READY.equals(multi.waitMode)) { - 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) { - setCurrentStatus("Multi: delay " + waitMs + " ms"); - multiSleepCancellable(waitMs); - } - if (!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); - } - - 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()); - - setCurrentStatus("Multi: waiting for completion"); - multiWaitForCompletion(manager, scope, subMacro.name); - if (!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 multiSnapshotsById(autismclient.util.multi.MultiManager manager) { - java.util.Map 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 (!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 scope, long timeoutMs) throws InterruptedException { - long deadline = System.nanoTime() + Math.max(0L, timeoutMs) * 1_000_000L; - while (isCurrentRunActive()) { - boolean allReady = true; - java.util.Map 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 scope, String subMacroName) throws InterruptedException { - long deadline = System.nanoTime() + 10L * 60L * 1_000_000_000L; - Thread.sleep(200L); - while (isCurrentRunActive()) { - boolean anyPending = false; - java.util.Map 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) { - executeSingleActionWithWaits(mc, trigger, null); - return true; - } - boolean fired = waitForFutureDone(future, timeoutMs); - if (!fired) future.cancel(true); - return fired; + return MacroRace.waitForMacroCondition(mc, trigger, timeoutMs); } - private static void dispatchRaceBody(Minecraft mc, AutismModule module, java.util.List bodyActions, - RunState run, CompletableFuture bodyFuture) { - if (bodyActions == null || bodyActions.isEmpty()) { - bodyFuture.complete(null); - return; - } - Runnable task = () -> { - RunState previous = CURRENT_RUN.get(); - if (run != null) CURRENT_RUN.set(run); - EnumSet heldResources = EnumSet.noneOf(SharedClientResource.class); - try { - heldResources = acquireClientResources(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 { - releaseClientResources(heldResources); - if (previous != null) CURRENT_RUN.set(previous); - else CURRENT_RUN.remove(); - } - }; - if (bodyActions.stream().allMatch(MacroExecutor::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 startActionAnchorFuture(MacroAction action) { - if (action instanceof PacketClickAction) { - return awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future; - } - if (action instanceof ItemAction) { - return awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future; - } - if (action instanceof CloseGuiAction) { - return awaitTimedSend(packet -> packet instanceof net.minecraft.network.protocol.game.ServerboundContainerClosePacket).future; - } - if (action instanceof SendChatAction) { - return 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 awaitTimedSend(packet -> true).future; - } - return null; - } - - private static MacroEvent waitForAnchorOrNow(CompletableFuture anchor, int timeoutMs, String label, String fallbackDirection) { - if (anchor != null) { - MacroEvent event = waitForTimedEvent(anchor, timeoutMs); - if (event != null) return event; - anchor.cancel(true); - } - return new MacroEvent(null, fallbackDirection + " " + (label == null ? "" : label), System.nanoTime(), null); - } - - private static MacroEvent waitForTimedEvent(CompletableFuture 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() && 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); - } - - private 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 timedConditionFuture(Minecraft mc, MacroAction trigger) { - if (trigger == null) { - return CompletableFuture.completedFuture(new MacroEvent(null, "condition", System.nanoTime(), null)); - } - if (trigger instanceof WaitForChatAction chat) { - MacroVariableContext context = currentVariableContext(); - java.util.concurrent.atomic.AtomicReference> captures = - new java.util.concurrent.atomic.AtomicReference<>(java.util.Map.of()); - TimedOneShotPacketListener listener = awaitTimedReceive((packet, direction, now) -> { - ChatCapture capture = MacroChatObserve.extractIncomingChat(packet); - var matched = MacroChatObserve.captureChatAction(chat, capture, context); - matched.ifPresent(captures::set); - return matched.isPresent(); - }); - - CompletableFuture out = new CompletableFuture<>(); - listener.future.whenComplete((event, e) -> { - if (e != null) out.completeExceptionally(e); - else out.complete(new 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 targets = packetAction.effectiveList(); - CompletableFuture matched = new CompletableFuture<>(); - TimedOneShotPacketListener recv = awaitTimedReceive((packet, direction, now) -> { - if (targets.isEmpty()) return true; - for (String target : targets) { - if (MacroPacketWait.matchesPacketTarget(target, packet, "S2C")) return true; - } - return false; - }); - TimedOneShotPacketListener send = 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 done = new CompletableFuture<>(); - CompletableFuture tickFuture = MacroConditionRegistry.waitForWorldChange(world); - TimedOneShotPacketListener worldListener = awaitTimedReceive((packet, direction, now) -> isWorldTransitionPacket(packet)); - tickFuture.whenComplete((v, e) -> { - if (e != null) { - if (!done.isDone()) done.cancel(true); - } else { - done.complete(new 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 done = new CompletableFuture<>(); - TimedOneShotPacketListener teleListener = 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 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 done = new CompletableFuture<>(); - RunState run = CURRENT_RUN.get(); - mc.execute(() -> withRunContext(run, () -> { - try { - executeSingleActionWithWaits(mc, trigger, null); - done.complete(new 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 timed = new CompletableFuture<>(); - base.whenComplete((v, e) -> { - if (e != null) timed.cancel(true); - else timed.complete(new 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) { - RunState run = CURRENT_RUN.get(); - return CompletableFuture.runAsync(() -> withRunContext(run, () -> { - try { - if (delay.useTicks) { - for (int i = 0; i < delay.delayTicks && isCurrentRunActive(); i++) { - waitForCondition(MacroConditionRegistry.waitForNextTick()); - } - } else { - Thread.sleep(Math.max(0, delay.delayMs)); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - })); - } - if (trigger instanceof WaitForGuiAction gui) { - CompletableFuture done = new CompletableFuture<>(); - RunState run = CURRENT_RUN.get(); - Thread worker = new Thread(() -> withRunContext(run, () -> { - long start = System.currentTimeMillis(); - boolean saw = false; - java.util.Map observed = java.util.Map.of(); - while (!done.isDone() && 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 MacroEvent(null, "GUI", System.nanoTime(), null, observed)); - return; - } - } else if (saw && gui.waitMode == WaitForGuiAction.WaitMode.CLOSE) { - done.complete(new 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(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 = currentVariableContext(); - OneShotPacketListener listener = awaitReceive(packet -> { - 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 targets = packetAction.effectiveList(); - CompletableFuture> matched = new CompletableFuture<>(); - OneShotPacketListener recv = awaitReceive(packet -> { - if (targets.isEmpty()) return true; - for (String target : targets) { - if (MacroPacketWait.matchesPacketTarget(target, packet, "S2C")) return true; - } - return false; - }); - OneShotPacketListener send = 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 = 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 done = new CompletableFuture<>(); - CompletableFuture tickFuture = MacroConditionRegistry.waitForWorldChange(world); - OneShotPacketListener worldListener = awaitReceive(MacroExecutor::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 done = new CompletableFuture<>(); - - OneShotPacketListener teleListener = 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) { - RunState run = CURRENT_RUN.get(); - return CompletableFuture.runAsync(() -> withRunContext(run, () -> { - try { - 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() && isCurrentRunActive()) { - if (System.nanoTime() - start >= timeoutNanos) break; - java.util.concurrent.locks.LockSupport.parkNanos(MACRO_WAIT_POLL_NANOS); - if (Thread.interrupted()) break; - } - return future.isDone() && !future.isCancelled() && isCurrentRunActive(); - } - - private static void executeSingleActionWithWaits(Minecraft mc, MacroAction action, AutismModule module) throws InterruptedException { + static void executeSingleActionWithWaits(Minecraft mc, MacroAction action, AutismModule module) throws InterruptedException { if (!MacroDynamicBindings.apply(action, mc)) return; if (!dynamicTextInputsAvailable(action, mc)) return; MacroPrearm.clearOwnCaptureOutput(action); @@ -3086,7 +2190,7 @@ public class MacroExecutor { }, wgta.timeoutMs); MacroPrearm.commitStagedCapture(wgta); } else if (action instanceof PacketGateAction pga) { - runPacketGateAction(mc, pga); + MacroRace.runPacketGateAction(mc, pga); } else if (isTargetWaitInteractionAction(action)) { postGuiFuture = runTargetWaitInteractionAction(mc, action); } else if (action instanceof WaitForHealthAction) { @@ -3167,8 +2271,8 @@ public class MacroExecutor { } else if (action instanceof WaitForWorldChangeAction world) { setCurrentStatus(world.getDisplayName()); try { - CompletableFuture future = raceConditionFuture(mc, world); - waitForFutureDone(future, 0); + CompletableFuture future = MacroRace.raceConditionFuture(mc, world); + MacroRace.waitForFutureDone(future, 0); } catch (CancellationException e) { } } else if (action instanceof WaitForPositionDeltaAction wpda) { setCurrentStatus(wpda.getDisplayName()); @@ -3179,20 +2283,20 @@ public class MacroExecutor { } else if (action instanceof WaitForTeleportAction teleport) { setCurrentStatus(teleport.getDisplayName()); try { - CompletableFuture future = raceConditionFuture(mc, teleport); - waitForFutureDone(future, 0); + CompletableFuture future = MacroRace.raceConditionFuture(mc, teleport); + MacroRace.waitForFutureDone(future, 0); } catch (CancellationException e) { } } else if (action instanceof WaitGamemodeChangeAction gm) { setCurrentStatus(gm.getDisplayName()); try { CompletableFuture future = MacroConditionRegistry.waitForGamemodeChange(gm); - waitForFutureDone(future, gm.timeoutMs); + MacroRace.waitForFutureDone(future, gm.timeoutMs); } catch (CancellationException e) { } } else if (action instanceof WaitMovementAction wm) { setCurrentStatus(wm.getDisplayName()); try { - CompletableFuture future = raceConditionFuture(mc, wm.resolveSubAction()); - waitForFutureDone(future, 0); + CompletableFuture future = MacroRace.raceConditionFuture(mc, wm.resolveSubAction()); + MacroRace.waitForFutureDone(future, 0); } catch (CancellationException e) { } } else if (action instanceof WaitForLanStepAction) { WaitForLanStepAction wla = (WaitForLanStepAction) action; @@ -3204,7 +2308,7 @@ public class MacroExecutor { } else if (action instanceof WaitForMacroStepAction) { WaitForMacroStepAction wma = (WaitForMacroStepAction) action; setCurrentStatus(wma.getDisplayName()); - waitForMacroStep(wma); + MacroRace.waitForMacroStep(wma); } else if (action instanceof GoToAction) { GoToAction ga = (GoToAction) action; runGoToAction(mc, ga); @@ -3493,7 +2597,7 @@ public class MacroExecutor { } catch (InterruptedException e) { AutismClientMessaging.sendPrefixed("\u00a7eRollback canceled safely."); } catch (Exception e) { - AutismClientMessaging.sendPrefixed("\u00a7cRollback failed: " + safeThrowableMessage(e)); + AutismClientMessaging.sendPrefixed("\u00a7cRollback failed: " + MacroRace.safeThrowableMessage(e)); } } else if (action instanceof StopMacroAction) { StopMacroAction stopAction = (StopMacroAction) action; @@ -5367,7 +4471,7 @@ public class MacroExecutor { } catch (InterruptedException e) { throw e; } catch (Throwable t) { - AutismClientMessaging.sendPrefixed("\u00a7cBreak next step failed (" + next.getDisplayName() + "): " + safeThrowableMessage(t)); + AutismClientMessaging.sendPrefixed("\u00a7cBreak next step failed (" + next.getDisplayName() + "): " + MacroRace.safeThrowableMessage(t)); } } } finally { @@ -5688,7 +4792,7 @@ public class MacroExecutor { } } - private static ItemTarget resolveItemTarget(ItemTarget target, String legacyEntry) { + static ItemTarget resolveItemTarget(ItemTarget target, String legacyEntry) { if (target != null && (target.hasSlot() || target.hasIdentity())) return target.copy(); return ItemTarget.fromLegacyEntry(legacyEntry); } @@ -6420,7 +5524,7 @@ public class MacroExecutor { } } - private static final class TimedOneShotPacketListener { + static final class TimedOneShotPacketListener { public final java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); private final java.util.List registry; private final TimedPacketMatch match; @@ -6447,7 +5551,7 @@ public class MacroExecutor { } } - private interface TimedPacketMatch { + interface TimedPacketMatch { boolean test(Packet packet, String direction, long nanoTime); } @@ -6468,15 +5572,15 @@ public class MacroExecutor { return new OneShotPacketListener(ONE_SHOT_SEND, match); } - private static TimedOneShotPacketListener awaitTimedReceive(TimedPacketMatch match) { + static TimedOneShotPacketListener awaitTimedReceive(TimedPacketMatch match) { return new TimedOneShotPacketListener(TIMED_ONE_SHOT_RECV, match); } - private static TimedOneShotPacketListener awaitTimedSend(java.util.function.Predicate> match) { + static TimedOneShotPacketListener awaitTimedSend(java.util.function.Predicate> match) { return awaitTimedSend((packet, direction, nanoTime) -> match.test(packet)); } - private static TimedOneShotPacketListener awaitTimedSend(TimedPacketMatch match) { + static TimedOneShotPacketListener awaitTimedSend(TimedPacketMatch match) { return new TimedOneShotPacketListener(TIMED_ONE_SHOT_SEND, match); } diff --git a/src/main/java/autismclient/util/macro/MacroPacketWait.java b/src/main/java/autismclient/util/macro/MacroPacketWait.java index 0c71544..14371b3 100644 --- a/src/main/java/autismclient/util/macro/MacroPacketWait.java +++ b/src/main/java/autismclient/util/macro/MacroPacketWait.java @@ -181,7 +181,7 @@ final class MacroPacketWait { return true; }); try { - if (MacroExecutor.waitForFutureDone(listener.future, 0) && run != null) MacroPrearm.commitStagedCapture(action); + if (MacroRace.waitForFutureDone(listener.future, 0) && run != null) MacroPrearm.commitStagedCapture(action); } finally { listener.cancel(); } diff --git a/src/main/java/autismclient/util/macro/MacroPrearm.java b/src/main/java/autismclient/util/macro/MacroPrearm.java index bd40b77..cc64d71 100644 --- a/src/main/java/autismclient/util/macro/MacroPrearm.java +++ b/src/main/java/autismclient/util/macro/MacroPrearm.java @@ -102,7 +102,7 @@ final class MacroPrearm { MacroExecutor.PrearmedWait wait = consumePrearmedWait(action); if (wait == null) return false; try { - MacroExecutor.waitForFutureDone(wait.future, timeoutMs); + MacroRace.waitForFutureDone(wait.future, timeoutMs); commitStagedCapture(action); return true; } finally { @@ -158,7 +158,7 @@ final class MacroPrearm { return new MacroExecutor.PrearmedWait(action, future, listener::cancel, 0); } if (action instanceof WaitForMacroStepAction macroStep) { - return predicatePrearm(action, () -> MacroExecutor.isMacroStepSatisfied(macroStep), macroStep.timeoutMs); + return predicatePrearm(action, () -> MacroRace.isMacroStepSatisfied(macroStep), macroStep.timeoutMs); } if (action instanceof WaitForLanStepAction lanStep) { CompletableFuture future = MacroConditionRegistry.waitForLanStep(lanStep); @@ -166,7 +166,7 @@ final class MacroPrearm { if (!future.isDone()) future.cancel(true); }, 0); } - CompletableFuture future = MacroExecutor.raceConditionFuture(mc, action); + 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); @@ -509,7 +509,7 @@ final class MacroPrearm { else if (recv != null) future = recv.future; else return; if (action.timeoutMs > 0) future.completeOnTimeout(null, action.timeoutMs, TimeUnit.MILLISECONDS); - MacroExecutor.waitForFutureDone(future, action.timeoutMs); + MacroRace.waitForFutureDone(future, action.timeoutMs); } finally { if (send != null) send.cancel(); if (recv != null) recv.cancel(); diff --git a/src/main/java/autismclient/util/macro/MacroRace.java b/src/main/java/autismclient/util/macro/MacroRace.java new file mode 100644 index 0000000..770337d --- /dev/null +++ b/src/main/java/autismclient/util/macro/MacroRace.java @@ -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 -> { + MacroExecutor.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(); + MacroExecutor.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 actions = new ArrayList<>(run.finallyActions); + run.finallyActions.clear(); + for (MacroAction action : actions) { + if (action == null || !action.isEnabled()) continue; + try { + MacroExecutor.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 (MacroExecutor.isCurrentRunActive()) { + if (isMacroStepSatisfied(action)) return; + if (action.timeoutMs > 0 && System.currentTimeMillis() - startMs >= action.timeoutMs) return; + CompletableFuture tick = MacroConditionRegistry.waitForNextTick(); + MacroExecutor.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 actions, int headerIndex, RaceAction race) throws InterruptedException { + int bodyCount = race.normalizedBodyCount(actions, headerIndex); + + java.util.List conditions = new ArrayList<>(); + java.util.List 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); + } + + MacroExecutor.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 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)); + MacroExecutor.setCurrentStatus("Race: timeout"); + return bodyCount; + } + + MacroExecutor.setCurrentStatus("Race: fired"); + waitForTimedFuture(bodyFuture, 250); + return bodyCount; + } + + private static CompletableFuture raceTriggerBarrier(Minecraft mc, java.util.List conditions) { + if (conditions == null || conditions.isEmpty()) return CompletableFuture.completedFuture(null); + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + + java.util.List> baseFutures = new ArrayList<>(); + java.util.List> futures = new ArrayList<>(); + for (MacroAction condition : conditions) { + MacroPrearm.clearOwnCaptureOutput(condition); + CompletableFuture 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 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 first = CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture 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 future : baseFutures) { + if (!future.isDone()) future.cancel(true); + } + }); + return out; + } + + public static int runReportGroup(Minecraft mc, java.util.List actions, int headerIndex, ReportAction report) throws InterruptedException { + int conditionCount = report.normalizedConditionCount(actions, headerIndex); + if (conditionCount < 2 || headerIndex + 2 >= actions.size()) { + MacroExecutor.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(); + MacroExecutor.setCurrentStatus("Report: start " + startCondition.getDisplayName()); + CompletableFuture startAnchor = startActionAnchorFuture(startCondition); + MacroExecutor.executeSingleActionWithWaits(mc, startCondition, null); + if (!MacroExecutor.isCurrentRunActive()) return conditionCount; + MacroExecutor.MacroEvent startEvent = waitForAnchorOrNow(startAnchor, 250, startCondition.getDisplayName(), "client action"); + + MacroExecutor.setCurrentStatus("Report: waiting end " + endCondition.getDisplayName()); + CompletableFuture 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(); + MacroExecutor.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); + MacroExecutor.setCurrentStatus("Report: waiting end " + endCondition.getDisplayName()); + CompletableFuture 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 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 scope = new java.util.LinkedHashSet<>(); + java.util.Set 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()) { + MacroExecutor.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 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 (!MacroExecutor.isCurrentRunActive()) return stepCount; + + int waitMs = Math.max(0, multi.waitMs); + if (MultiAction.WAIT_UNTIL_READY.equals(multi.waitMode)) { + MacroExecutor.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) { + MacroExecutor.setCurrentStatus("Multi: delay " + waitMs + " ms"); + multiSleepCancellable(waitMs); + } + if (!MacroExecutor.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); + } + + MacroExecutor.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()); + + MacroExecutor.setCurrentStatus("Multi: waiting for completion"); + multiWaitForCompletion(manager, scope, subMacro.name); + if (!MacroExecutor.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 multiSnapshotsById(autismclient.util.multi.MultiManager manager) { + java.util.Map 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 (!MacroExecutor.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 scope, long timeoutMs) throws InterruptedException { + long deadline = System.nanoTime() + Math.max(0L, timeoutMs) * 1_000_000L; + while (MacroExecutor.isCurrentRunActive()) { + boolean allReady = true; + java.util.Map 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 scope, String subMacroName) throws InterruptedException { + long deadline = System.nanoTime() + 10L * 60L * 1_000_000_000L; + Thread.sleep(200L); + while (MacroExecutor.isCurrentRunActive()) { + boolean anyPending = false; + java.util.Map 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) { + MacroExecutor.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 bodyActions, + MacroExecutor.RunState run, CompletableFuture 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 heldResources = EnumSet.noneOf(SharedClientResource.class); + try { + heldResources = MacroExecutor.acquireClientResources(MacroExecutor.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 { + MacroExecutor.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 startActionAnchorFuture(MacroAction action) { + if (action instanceof PacketClickAction) { + return MacroExecutor.awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future; + } + if (action instanceof ItemAction) { + return MacroExecutor.awaitTimedSend(packet -> packet instanceof ServerboundContainerClickPacket).future; + } + if (action instanceof CloseGuiAction) { + return MacroExecutor.awaitTimedSend(packet -> packet instanceof net.minecraft.network.protocol.game.ServerboundContainerClosePacket).future; + } + if (action instanceof SendChatAction) { + return MacroExecutor.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 MacroExecutor.awaitTimedSend(packet -> true).future; + } + return null; + } + + private static MacroExecutor.MacroEvent waitForAnchorOrNow(CompletableFuture 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 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() && MacroExecutor.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 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 = MacroExecutor.currentVariableContext(); + java.util.concurrent.atomic.AtomicReference> captures = + new java.util.concurrent.atomic.AtomicReference<>(java.util.Map.of()); + MacroExecutor.TimedOneShotPacketListener listener = MacroExecutor.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 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 targets = packetAction.effectiveList(); + CompletableFuture matched = new CompletableFuture<>(); + MacroExecutor.TimedOneShotPacketListener recv = MacroExecutor.awaitTimedReceive((packet, direction, now) -> { + if (targets.isEmpty()) return true; + for (String target : targets) { + if (MacroPacketWait.matchesPacketTarget(target, packet, "S2C")) return true; + } + return false; + }); + MacroExecutor.TimedOneShotPacketListener send = MacroExecutor.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 done = new CompletableFuture<>(); + CompletableFuture tickFuture = MacroConditionRegistry.waitForWorldChange(world); + MacroExecutor.TimedOneShotPacketListener worldListener = MacroExecutor.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 done = new CompletableFuture<>(); + MacroExecutor.TimedOneShotPacketListener teleListener = MacroExecutor.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 done = new CompletableFuture<>(); + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + mc.execute(() -> MacroExecutor.withRunContext(run, () -> { + try { + MacroExecutor.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 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(() -> MacroExecutor.withRunContext(run, () -> { + try { + if (delay.useTicks) { + for (int i = 0; i < delay.delayTicks && MacroExecutor.isCurrentRunActive(); i++) { + MacroExecutor.waitForCondition(MacroConditionRegistry.waitForNextTick()); + } + } else { + Thread.sleep(Math.max(0, delay.delayMs)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + })); + } + if (trigger instanceof WaitForGuiAction gui) { + CompletableFuture done = new CompletableFuture<>(); + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + Thread worker = new Thread(() -> MacroExecutor.withRunContext(run, () -> { + long start = System.currentTimeMillis(); + boolean saw = false; + java.util.Map observed = java.util.Map.of(); + while (!done.isDone() && MacroExecutor.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 = MacroExecutor.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 targets = packetAction.effectiveList(); + CompletableFuture> 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 = MacroExecutor.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 done = new CompletableFuture<>(); + CompletableFuture 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 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(() -> MacroExecutor.withRunContext(run, () -> { + try { + MacroExecutor.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() && MacroExecutor.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() && MacroExecutor.isCurrentRunActive(); + } +}