Refactor: extract MacroRace from MacroExecutor (upstream #4)

Move 28 group-execution methods (runPacketGateAction through
raceConditionFuture, plus waitForFutureDone) verbatim into MacroRace:
packet gates, finally/step waits, race/report/multi groups, timed and
anchor futures. Shared members referenced as MacroExecutor.xxx
(mechanical qualification only); waitForMacroCondition kept as public
delegate for the ReportAction caller. All bodies verified identical
against HEAD modulo that qualification.
This commit is contained in:
sectersion 2026-10-11 16:13:57 +01:00
parent f014d6e5c0
commit 57ca197042
5 changed files with 973 additions and 943 deletions

View file

@ -200,7 +200,7 @@ final class MacroCaptureAutofill {
MacroExecutor.setCurrentStatus("Requesting command suggestions"); MacroExecutor.setCurrentStatus("Requesting command suggestions");
PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs); PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs);
if (pending == null) return; if (pending == null) return;
if (!MacroExecutor.waitForFutureDone(pending.future(), 0)) return; if (!MacroRace.waitForFutureDone(pending.future(), 0)) return;
java.util.List<String> suggestions = pending.future().getNow(null); java.util.List<String> suggestions = pending.future().getNow(null);
if (suggestions == null || suggestions.isEmpty()) return; if (suggestions == null || suggestions.isEmpty()) return;
applyCaptureSuggestions(run, action, capturePool(connection, action, query, suggestions), query); 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; if (result.get().success() || System.nanoTime() >= deadline) break;
MacroExecutor.waitOneTick(); MacroExecutor.waitOneTick();
} }

File diff suppressed because it is too large Load diff

View file

@ -181,7 +181,7 @@ final class MacroPacketWait {
return true; return true;
}); });
try { try {
if (MacroExecutor.waitForFutureDone(listener.future, 0) && run != null) MacroPrearm.commitStagedCapture(action); if (MacroRace.waitForFutureDone(listener.future, 0) && run != null) MacroPrearm.commitStagedCapture(action);
} finally { } finally {
listener.cancel(); listener.cancel();
} }

View file

@ -102,7 +102,7 @@ final class MacroPrearm {
MacroExecutor.PrearmedWait wait = consumePrearmedWait(action); MacroExecutor.PrearmedWait wait = consumePrearmedWait(action);
if (wait == null) return false; if (wait == null) return false;
try { try {
MacroExecutor.waitForFutureDone(wait.future, timeoutMs); MacroRace.waitForFutureDone(wait.future, timeoutMs);
commitStagedCapture(action); commitStagedCapture(action);
return true; return true;
} finally { } finally {
@ -158,7 +158,7 @@ final class MacroPrearm {
return new MacroExecutor.PrearmedWait(action, future, listener::cancel, 0); return new MacroExecutor.PrearmedWait(action, future, listener::cancel, 0);
} }
if (action instanceof WaitForMacroStepAction macroStep) { 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) { if (action instanceof WaitForLanStepAction lanStep) {
CompletableFuture<Void> future = MacroConditionRegistry.waitForLanStep(lanStep); CompletableFuture<Void> future = MacroConditionRegistry.waitForLanStep(lanStep);
@ -166,7 +166,7 @@ final class MacroPrearm {
if (!future.isDone()) future.cancel(true); if (!future.isDone()) future.cancel(true);
}, 0); }, 0);
} }
CompletableFuture<?> future = MacroExecutor.raceConditionFuture(mc, action); CompletableFuture<?> future = MacroRace.raceConditionFuture(mc, action);
if (future == null) return null; if (future == null) return null;
int timeout = action instanceof WaitForChatAction chat ? chat.timeoutMs : 0; int timeout = action instanceof WaitForChatAction chat ? chat.timeoutMs : 0;
if (timeout > 0) future.completeOnTimeout(null, timeout, TimeUnit.MILLISECONDS); if (timeout > 0) future.completeOnTimeout(null, timeout, TimeUnit.MILLISECONDS);
@ -509,7 +509,7 @@ final class MacroPrearm {
else if (recv != null) future = recv.future; else if (recv != null) future = recv.future;
else return; else return;
if (action.timeoutMs > 0) future.completeOnTimeout(null, action.timeoutMs, TimeUnit.MILLISECONDS); if (action.timeoutMs > 0) future.completeOnTimeout(null, action.timeoutMs, TimeUnit.MILLISECONDS);
MacroExecutor.waitForFutureDone(future, action.timeoutMs); MacroRace.waitForFutureDone(future, action.timeoutMs);
} finally { } finally {
if (send != null) send.cancel(); if (send != null) send.cancel();
if (recv != null) recv.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 -> {
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<MacroAction> 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<Void> 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<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);
}
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<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));
MacroExecutor.setCurrentStatus("Race: timeout");
return bodyCount;
}
MacroExecutor.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()) {
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<MacroExecutor.MacroEvent> 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<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();
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<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()) {
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<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 (!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<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 (!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<String> 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<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 (MacroExecutor.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) {
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<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 = 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<MacroExecutor.MacroEvent> 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<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() && 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<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 = MacroExecutor.currentVariableContext();
java.util.concurrent.atomic.AtomicReference<java.util.Map<String, MacroValue>> 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<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<>();
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<MacroExecutor.MacroEvent> done = new CompletableFuture<>();
CompletableFuture<Void> 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<MacroExecutor.MacroEvent> 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<MacroExecutor.MacroEvent> 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<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(() -> 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<MacroExecutor.MacroEvent> 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<String, MacroValue> 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<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 = 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<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(() -> 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();
}
}