From b4417fd46475c4a2b75af40ceb44015eda0b4afa Mon Sep 17 00:00:00 2001 From: sectersion Date: Sun, 11 Oct 2026 16:04:56 +0100 Subject: [PATCH] Refactor: extract MacroCaptureAutofill from MacroExecutor (upstream #4) Move 15 capture autofill/namescrape/scoreboard methods plus the suggestion-cache records and fields verbatim into MacroCaptureAutofill. Shared MacroExecutor members referenced as MacroExecutor.xxx (mechanical qualification only); same-package buildAutofillQuery kept as delegate so the existing unit test is unaffected. All bodies verified identical against HEAD modulo that qualification. --- .../util/macro/MacroCaptureAutofill.java | 408 +++++++++++++++++ .../util/macro/MacroExecutor.java | 414 +----------------- 2 files changed, 420 insertions(+), 402 deletions(-) create mode 100644 src/main/java/autismclient/util/macro/MacroCaptureAutofill.java diff --git a/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java b/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java new file mode 100644 index 0000000..0c96a89 --- /dev/null +++ b/src/main/java/autismclient/util/macro/MacroCaptureAutofill.java @@ -0,0 +1,408 @@ +package autismclient.util.macro; + +import autismclient.util.AutismCommandSuggestionIds; +import autismclient.util.AutismNameHarvest; +import autismclient.util.AutismNotifications; +import net.minecraft.client.Minecraft; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +final class MacroCaptureAutofill { + private MacroCaptureAutofill() { + } + private record CachedSuggestions(java.util.List suggestions, Object connection) {} + private record PendingFetch(CompletableFuture> future, Object connection) {} + private static final java.util.concurrent.ConcurrentHashMap SUGGESTION_CACHE = + new java.util.concurrent.ConcurrentHashMap<>(); + private static final java.util.concurrent.ConcurrentHashMap PENDING_FETCHES = + new java.util.concurrent.ConcurrentHashMap<>(); + private static final int SUGGESTION_CACHE_MAX = 256; + private static final long SUGGESTION_CACHE_IDLE_RESET_MS = 3000L; + + private static final int TRUNCATED_REPLY_SIZE = 1000; + private static final int MIN_SWEEP_CAP = 8; + private static final int MAX_SWEEP_NAMES = 60_000; + private static final String DRILL_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789_"; + static volatile long allRunsStoppedAtMs; + + private static final class SuggestionUnion { + final Object connection; + final java.util.LinkedHashSet names = new java.util.LinkedHashSet<>(); + final java.util.ArrayDeque sweepQueue = new java.util.ArrayDeque<>(); + int alphabetCursor; + int cap; + + SuggestionUnion(Object connection, String baseQuery) { + this.connection = connection; + sweepQueue.add(baseQuery); + } + } + + private static final java.util.concurrent.ConcurrentHashMap SUGGESTION_UNIONS = + new java.util.concurrent.ConcurrentHashMap<>(); + + public static void expireSuggestionCacheAfterIdle() { + long stoppedAt = allRunsStoppedAtMs; + if (stoppedAt > 0 && System.currentTimeMillis() - stoppedAt > SUGGESTION_CACHE_IDLE_RESET_MS) { + SUGGESTION_CACHE.clear(); + PENDING_FETCHES.clear(); + SUGGESTION_UNIONS.clear(); + } + } + + static String buildAutofillQuery(String resolvedCommand, CaptureListSelector.Filter filter, String filterText) { + String query = resolvedCommand.startsWith("/") ? resolvedCommand : "/" + resolvedCommand; + + if (!query.contains(" ")) query = query + " "; + String prefix = filter == CaptureListSelector.Filter.PREFIX && filterText != null ? filterText.trim() : ""; + if (!prefix.isEmpty() && !prefix.contains(" ") && query.endsWith(" ")) { + + int end = query.length(); + while (end > 0 && query.charAt(end - 1) == ' ') end--; + query = query.substring(0, end) + " " + prefix; + } + return query; + } + + private static java.util.List capturePool(net.minecraft.client.multiplayer.ClientPacketListener connection, + CaptureValueAction action, String query, java.util.List base) { + + if (!action.autofillCacheList && base.size() < TRUNCATED_REPLY_SIZE) return base; + SuggestionUnion union = SUGGESTION_UNIONS.compute(query, (key, existing) -> + existing != null && existing.connection == connection ? existing : new SuggestionUnion(connection, query)); + synchronized (union) { + union.names.addAll(base); + union.cap = Math.max(union.cap, base.size()); + advanceSweep(union, connection, action.autofillTimeoutMs); + java.util.List pool = new java.util.ArrayList<>(union.names); + boolean ordered = action.listSelection == CaptureListSelector.Selection.SEQUENTIAL + || action.listSelection == CaptureListSelector.Selection.FIRST + || action.listSelection == CaptureListSelector.Selection.LAST + || action.listSelection == CaptureListSelector.Selection.POSITION; + + if (ordered) pool.sort(String.CASE_INSENSITIVE_ORDER); + return pool; + } + } + + private static void advanceSweep(SuggestionUnion union, net.minecraft.client.multiplayer.ClientPacketListener connection, int timeoutMs) { + if (union.names.size() >= MAX_SWEEP_NAMES) { union.sweepQueue.clear(); return; } + while (!union.sweepQueue.isEmpty()) { + if (union.alphabetCursor >= DRILL_ALPHABET.length()) { + union.sweepQueue.poll(); + union.alphabetCursor = 0; + continue; + } + String target = union.sweepQueue.peek() + DRILL_ALPHABET.charAt(union.alphabetCursor); + CachedSuggestions cached = SUGGESTION_CACHE.get(target); + if (cached == null || cached.connection() != connection) { + fireSuggestionFetch(connection, target, timeoutMs); + return; + } + union.names.addAll(cached.suggestions()); + int size = cached.suggestions().size(); + + boolean truncated = size >= union.cap && union.cap >= MIN_SWEEP_CAP; + union.cap = Math.max(union.cap, size); + if (truncated) union.sweepQueue.add(target); + union.alphabetCursor++; + } + } + + private static void applyCaptureSuggestions(MacroExecutor.RunState run, CaptureValueAction action, + java.util.List suggestions, String query) { + CaptureValueAction.Preview preview = action.previewSuggestions(suggestions, query); + if (!preview.success()) return; + if (run != null) run.variables.setAll(preview.values()); + else MacroVariables.setAll(preview.values()); + } + + private static PendingFetch fireSuggestionFetch(net.minecraft.client.multiplayer.ClientPacketListener connection, + String query, int timeoutMs) { + PendingFetch existing = PENDING_FETCHES.get(query); + if (existing != null && existing.connection() == connection && !existing.future().isDone()) return existing; + if (PENDING_FETCHES.size() > SUGGESTION_CACHE_MAX) return existing; + int requestId = autismclient.util.AutismCommandSuggestionIds.nextMacroId(); + CompletableFuture> future = new CompletableFuture<>(); + MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> { + if (!(packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket reply) + || reply.id() != requestId) { + return false; + } + java.util.List texts = new java.util.ArrayList<>(); + if (reply.suggestions() != null) { + for (var entry : reply.suggestions()) { + if (entry != null && entry.text() != null && !entry.text().isBlank()) texts.add(entry.text()); + } + } + future.complete(texts); + return true; + }); + PendingFetch pending = new PendingFetch(future, connection); + PENDING_FETCHES.put(query, pending); + future.completeOnTimeout(null, timeoutMs > 0 ? Math.max(1000, timeoutMs) : 30_000, + java.util.concurrent.TimeUnit.MILLISECONDS); + future.whenComplete((texts, error) -> { + listener.cancel(); + PENDING_FETCHES.remove(query, pending); + + if (texts != null) { + if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); + SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(texts), connection)); + } + }); + try { + connection.send(new net.minecraft.network.protocol.game.ServerboundCommandSuggestionPacket(requestId, query)); + } catch (Throwable error) { + future.complete(null); + } + return pending; + } + + private static void runCaptureCommandAutofill(Minecraft mc, CaptureValueAction action) { + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + MacroExecutor.clearCaptureOutputs(action); + MacroTemplate.Resolution resolved = MacroVariables.resolve(action.autofillCommand, mc); + if (!resolved.success() || resolved.value().isBlank()) return; + String query = buildAutofillQuery(resolved.value(), action.listFilter, action.listFilterText); + var connection = mc.getConnection(); + if (connection == null) return; + + if (action.autofillCacheList) { + CachedSuggestions listCache = SUGGESTION_CACHE.get(query); + if (listCache != null && listCache.connection() == connection && !listCache.suggestions().isEmpty()) { + MacroExecutor.setCurrentStatus("Using cached suggestions"); + applyCaptureSuggestions(run, action, listCache.suggestions(), query); + return; + } + MacroExecutor.setCurrentStatus("Sweeping command suggestions"); + java.util.List all = sweepAutofillCommand(connection, query); + if (all.isEmpty()) return; + if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); + SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(all), connection)); + applyCaptureSuggestions(run, action, all, query); + return; + } + + CachedSuggestions cached = SUGGESTION_CACHE.get(query); + + if (cached != null && (cached.connection() != connection || cached.suggestions().isEmpty())) cached = null; + + if (cached != null) { + + MacroExecutor.setCurrentStatus("Using cached suggestions"); + applyCaptureSuggestions(run, action, capturePool(connection, action, query, cached.suggestions()), query); + fireSuggestionFetch(connection, query, action.autofillTimeoutMs); + return; + } + + MacroExecutor.setCurrentStatus("Requesting command suggestions"); + PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs); + if (pending == null) return; + if (!MacroExecutor.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); + } + + private static java.util.List sweepAutofillCommand(net.minecraft.client.multiplayer.ClientPacketListener connection, String query) { + autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() { + @Override public boolean cancelled() { + Minecraft mc = Minecraft.getInstance(); + return !MacroExecutor.isCurrentRunActive() || mc == null || mc.getConnection() != connection; + } + @Override public boolean paused() { return false; } + @Override public int limit() { return MAX_SWEEP_NAMES; } + @Override public int maxQueries() { return 50_000; } + @Override public boolean usernamesOnly() { return false; } + }; + java.util.List all = autismclient.util.AutismNameHarvest.sweepCommand(connection, query, control); + all.sort(String.CASE_INSENSITIVE_ORDER); + return all; + } + + private static void runCaptureNamescrape(Minecraft mc, CaptureValueAction action) { + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + MacroExecutor.clearCaptureOutputs(action); + var connection = mc.getConnection(); + if (connection == null) return; + String cacheKey = "namescrape"; + CachedSuggestions cached = SUGGESTION_CACHE.get(cacheKey); + if (cached != null && cached.connection() == connection && !cached.suggestions().isEmpty()) { + MacroExecutor.setCurrentStatus("Using cached names"); + applyCaptureSuggestions(run, action, cached.suggestions(), ""); + return; + } + MacroExecutor.setCurrentStatus("Scraping player names"); + java.util.List names = scrapeAllNames(mc, connection); + if (names.isEmpty()) return; + if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); + SUGGESTION_CACHE.put(cacheKey, new CachedSuggestions(java.util.List.copyOf(names), connection)); + applyCaptureSuggestions(run, action, names, ""); + } + + private static java.util.List scrapeAllNames(Minecraft mc, net.minecraft.client.multiplayer.ClientPacketListener connection) { + String self = mc.player == null ? "" : mc.player.getName().getString(); + java.util.LinkedHashMap names = autismclient.util.AutismNameHarvest.instantNames(mc, self); + java.util.List vectors = + autismclient.util.AutismNameHarvest.discoverVectors(connection); + autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() { + @Override public boolean cancelled() { + Minecraft m = Minecraft.getInstance(); + return !MacroExecutor.isCurrentRunActive() || m == null || m.getConnection() != connection; + } + @Override public boolean paused() { return false; } + @Override public int limit() { return MAX_SWEEP_NAMES; } + @Override public int maxQueries() { return 100_000; } + }; + autismclient.util.AutismNameHarvest.sweep(connection, names, self, vectors, control); + java.util.List out = new java.util.ArrayList<>(names.values()); + out.sort(String.CASE_INSENSITIVE_ORDER); + return out; + } + + public static void runCaptureValueAction(Minecraft mc, CaptureValueAction action) { + if (action == null) return; + String modifierError = action.numberModifierError(); + if (!modifierError.isBlank()) { + + MacroExecutor.setCurrentStatus("Capture failed: " + modifierError); + autismclient.util.AutismNotifications.error("Capture Value: " + modifierError); + return; + } + if (action.source == CaptureValueAction.Source.COMMAND_AUTOFILL) { + MacroExecutor.setCurrentStatus("Requesting command suggestions"); + runCaptureCommandAutofill(mc, action); + reportMissingCapture(action); + return; + } + if (action.source == CaptureValueAction.Source.NAMESCRAPE) { + MacroExecutor.setCurrentStatus("Scraping player names"); + runCaptureNamescrape(mc, action); + reportMissingCapture(action); + return; + } + if (action.waitForTrigger) { + CaptureValueAction.Source source = action.source == null ? CaptureValueAction.Source.GUI_TITLE : action.source; + MacroExecutor.setCurrentStatus("Waiting for capture: " + source.name().toLowerCase(Locale.ROOT).replace('_', ' ')); + if (source == CaptureValueAction.Source.RECENT_CHAT) MacroPacketWait.awaitCaptureValueChat(action); + else if (source == CaptureValueAction.Source.SCOREBOARD) awaitCaptureValueScoreboard(mc, action); + else awaitCaptureValueState(mc, action); + return; + } + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + java.util.concurrent.atomic.AtomicReference result = + new java.util.concurrent.atomic.AtomicReference<>(CaptureValueAction.Preview.unavailable("Not available")); + + long deadline = System.nanoTime() + MacroExecutor.CAPTURE_SETTLE_MS * 1_000_000L; + while (MacroExecutor.isCurrentRunActive()) { + CompletableFuture done = new CompletableFuture<>(); + mc.execute(() -> MacroExecutor.withRunContext(run, () -> { + try { + result.set(action.run(mc)); + done.complete(null); + } catch (Throwable error) { + done.completeExceptionally(error); + } + })); + + MacroExecutor.waitForFutureDone(done, 0); + if (result.get().success() || System.nanoTime() >= deadline) break; + MacroExecutor.waitOneTick(); + } + CaptureValueAction.Preview preview = result.get(); + if (!preview.success()) { + + MacroExecutor.setCurrentStatus("Capture failed: " + preview.message()); + autismclient.util.AutismNotifications.error( + "Capture " + captureOutputLabel(action) + ": " + preview.message()); + } + } + + private static void reportMissingCapture(CaptureValueAction action) { + String name = MacroVariableContext.cleanRootName(action.saveAs); + if (name.isBlank() || MacroVariables.value(name).isPresent()) return; + MacroExecutor.setCurrentStatus("Capture failed: nothing captured"); + autismclient.util.AutismNotifications.error( + "Capture " + captureOutputLabel(action) + ": nothing captured"); + } + + private static String captureOutputLabel(CaptureValueAction action) { + String name = MacroVariableContext.cleanRootName(action.saveAs); + return name.isBlank() ? "value" : "{" + name + "}"; + } + + private static void awaitCaptureValueState(Minecraft mc, CaptureValueAction action) { + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + MacroExecutor.clearCaptureOutputs(action); + String lastMessage = ""; + while (MacroExecutor.isCurrentRunActive()) { + CaptureValueAction.Preview preview; + try { + preview = MacroExecutor.callOnClientThread(mc, () -> action.previewCurrent(mc), + CaptureValueAction.Preview.unavailable("Not available")); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return; + } + if (preview.success()) { + if (run != null) run.variables.setAll(preview.values()); + else MacroVariables.setAll(preview.values()); + return; + } + if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) { + lastMessage = preview.message(); + MacroExecutor.setCurrentStatus("Capture waiting: " + lastMessage); + } + MacroExecutor.waitOneTick(); + } + } + + private static void awaitCaptureValueScoreboard(Minecraft mc, CaptureValueAction action) { + MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get(); + MacroExecutor.clearCaptureOutputs(action); + String trackedKey = action.scoreboardRow; + int trackedRow = action.scoreboardRowIndex; + String trackedObjective = action.scoreboardObjective; + String previousText = null; + String lastMessage = ""; + boolean baselineReady = false; + while (MacroExecutor.isCurrentRunActive()) { + CaptureValueAction.ScoreboardLine line; + try { + String keySnapshot = trackedKey; + int rowSnapshot = trackedRow; + String objectiveSnapshot = trackedObjective; + line = MacroExecutor.callOnClientThread(mc, + () -> action.resolveScoreboardLine(mc, keySnapshot, rowSnapshot, objectiveSnapshot), null); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return; + } + if (line != null) { + String currentText = line.text() == null ? "" : line.text(); + if (!baselineReady) { + trackedKey = line.key(); + trackedRow = line.row(); + trackedObjective = line.objective(); + previousText = currentText; + baselineReady = true; + } else if (!currentText.equals(previousText)) { + CaptureValueAction.Preview preview = action.previewScoreboardLine(line); + if (preview.success()) { + if (run != null) run.variables.setAll(preview.values()); + else MacroVariables.setAll(preview.values()); + return; + } + if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) { + lastMessage = preview.message(); + MacroExecutor.setCurrentStatus("Capture waiting: " + lastMessage); + } + previousText = currentText; + } + } + MacroExecutor.waitOneTick(); + } + } +} diff --git a/src/main/java/autismclient/util/macro/MacroExecutor.java b/src/main/java/autismclient/util/macro/MacroExecutor.java index 102b0f3..63da1f4 100644 --- a/src/main/java/autismclient/util/macro/MacroExecutor.java +++ b/src/main/java/autismclient/util/macro/MacroExecutor.java @@ -82,7 +82,7 @@ public class MacroExecutor { private static final long MACRO_WAIT_POLL_NANOS = 500_000L; - private static final long CAPTURE_SETTLE_MS = 300L; + 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(); @@ -324,7 +324,7 @@ public class MacroExecutor { return target - 1; } - private static boolean isCurrentRunActive() { + static boolean isCurrentRunActive() { if (PackHideState.isHardLocked()) return false; RunState run = CURRENT_RUN.get(); if (run != null) return run.running.get(); @@ -483,7 +483,7 @@ public class MacroExecutor { return run == null ? null : run.variables; } - private static void withRunContext(RunState run, Runnable task) { + static void withRunContext(RunState run, Runnable task) { RunState previous = CURRENT_RUN.get(); if (run != null) CURRENT_RUN.set(run); try { @@ -539,7 +539,7 @@ public class MacroExecutor { ACTIVE_RUNS.remove(run.id); RUNS_BY_ID.remove(run.id); rebuildActiveRunListLocked(); - if (ACTIVE_RUNS.isEmpty()) allRunsStoppedAtMs = System.currentTimeMillis(); + if (ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.allRunsStoppedAtMs = System.currentTimeMillis(); RUN_STATE_REVISION.incrementAndGet(); refreshPrimaryRunLocked(); } @@ -804,7 +804,7 @@ public class MacroExecutor { return -1L; } runState = new RunState(NEXT_RUN_ID.getAndIncrement(), macro, executionMacro, runOptions); - if (ACTIVE_RUNS.isEmpty()) expireSuggestionCacheAfterIdle(); + if (ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.expireSuggestionCacheAfterIdle(); ACTIVE_RUNS.put(runState.id, runState); RUNS_BY_ID.put(runState.id, runState); rebuildActiveRunListLocked(); @@ -828,7 +828,7 @@ public class MacroExecutor { ACTIVE_RUNS.remove(runState.id); RUNS_BY_ID.remove(runState.id); rebuildActiveRunListLocked(); - if (ACTIVE_RUNS.isEmpty()) allRunsStoppedAtMs = System.currentTimeMillis(); + if (ACTIVE_RUNS.isEmpty()) MacroCaptureAutofill.allRunsStoppedAtMs = System.currentTimeMillis(); RUN_STATE_REVISION.incrementAndGet(); refreshPrimaryRunLocked(); } @@ -1465,7 +1465,7 @@ public class MacroExecutor { i = endIdx - 1; } else if (action instanceof CaptureValueAction captureValue) { - runCaptureValueAction(mc, captureValue); + MacroCaptureAutofill.runCaptureValueAction(mc, captureValue); } else if (action instanceof WaitForChatAction) { WaitForChatAction wca = (WaitForChatAction) action; @@ -2017,400 +2017,10 @@ public class MacroExecutor { } - private record CachedSuggestions(java.util.List suggestions, Object connection) {} - private record PendingFetch(CompletableFuture> future, Object connection) {} - private static final java.util.concurrent.ConcurrentHashMap SUGGESTION_CACHE = - new java.util.concurrent.ConcurrentHashMap<>(); - private static final java.util.concurrent.ConcurrentHashMap PENDING_FETCHES = - new java.util.concurrent.ConcurrentHashMap<>(); - private static final int SUGGESTION_CACHE_MAX = 256; - private static final long SUGGESTION_CACHE_IDLE_RESET_MS = 3000L; - - private static final int TRUNCATED_REPLY_SIZE = 1000; - private static final int MIN_SWEEP_CAP = 8; - private static final int MAX_SWEEP_NAMES = 60_000; - private static final String DRILL_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789_"; - private static volatile long allRunsStoppedAtMs; - - private static final class SuggestionUnion { - final Object connection; - final java.util.LinkedHashSet names = new java.util.LinkedHashSet<>(); - final java.util.ArrayDeque sweepQueue = new java.util.ArrayDeque<>(); - int alphabetCursor; - int cap; - - SuggestionUnion(Object connection, String baseQuery) { - this.connection = connection; - sweepQueue.add(baseQuery); - } - } - - private static final java.util.concurrent.ConcurrentHashMap SUGGESTION_UNIONS = - new java.util.concurrent.ConcurrentHashMap<>(); - - private static void expireSuggestionCacheAfterIdle() { - long stoppedAt = allRunsStoppedAtMs; - if (stoppedAt > 0 && System.currentTimeMillis() - stoppedAt > SUGGESTION_CACHE_IDLE_RESET_MS) { - SUGGESTION_CACHE.clear(); - PENDING_FETCHES.clear(); - SUGGESTION_UNIONS.clear(); - } - } - static String buildAutofillQuery(String resolvedCommand, CaptureListSelector.Filter filter, String filterText) { - String query = resolvedCommand.startsWith("/") ? resolvedCommand : "/" + resolvedCommand; - - if (!query.contains(" ")) query = query + " "; - String prefix = filter == CaptureListSelector.Filter.PREFIX && filterText != null ? filterText.trim() : ""; - if (!prefix.isEmpty() && !prefix.contains(" ") && query.endsWith(" ")) { - - int end = query.length(); - while (end > 0 && query.charAt(end - 1) == ' ') end--; - query = query.substring(0, end) + " " + prefix; - } - return query; + return MacroCaptureAutofill.buildAutofillQuery(resolvedCommand, filter, filterText); } - private static java.util.List capturePool(net.minecraft.client.multiplayer.ClientPacketListener connection, - CaptureValueAction action, String query, java.util.List base) { - - if (!action.autofillCacheList && base.size() < TRUNCATED_REPLY_SIZE) return base; - SuggestionUnion union = SUGGESTION_UNIONS.compute(query, (key, existing) -> - existing != null && existing.connection == connection ? existing : new SuggestionUnion(connection, query)); - synchronized (union) { - union.names.addAll(base); - union.cap = Math.max(union.cap, base.size()); - advanceSweep(union, connection, action.autofillTimeoutMs); - java.util.List pool = new java.util.ArrayList<>(union.names); - boolean ordered = action.listSelection == CaptureListSelector.Selection.SEQUENTIAL - || action.listSelection == CaptureListSelector.Selection.FIRST - || action.listSelection == CaptureListSelector.Selection.LAST - || action.listSelection == CaptureListSelector.Selection.POSITION; - - if (ordered) pool.sort(String.CASE_INSENSITIVE_ORDER); - return pool; - } - } - - private static void advanceSweep(SuggestionUnion union, net.minecraft.client.multiplayer.ClientPacketListener connection, int timeoutMs) { - if (union.names.size() >= MAX_SWEEP_NAMES) { union.sweepQueue.clear(); return; } - while (!union.sweepQueue.isEmpty()) { - if (union.alphabetCursor >= DRILL_ALPHABET.length()) { - union.sweepQueue.poll(); - union.alphabetCursor = 0; - continue; - } - String target = union.sweepQueue.peek() + DRILL_ALPHABET.charAt(union.alphabetCursor); - CachedSuggestions cached = SUGGESTION_CACHE.get(target); - if (cached == null || cached.connection() != connection) { - fireSuggestionFetch(connection, target, timeoutMs); - return; - } - union.names.addAll(cached.suggestions()); - int size = cached.suggestions().size(); - - boolean truncated = size >= union.cap && union.cap >= MIN_SWEEP_CAP; - union.cap = Math.max(union.cap, size); - if (truncated) union.sweepQueue.add(target); - union.alphabetCursor++; - } - } - - private static void applyCaptureSuggestions(RunState run, CaptureValueAction action, - java.util.List suggestions, String query) { - CaptureValueAction.Preview preview = action.previewSuggestions(suggestions, query); - if (!preview.success()) return; - if (run != null) run.variables.setAll(preview.values()); - else MacroVariables.setAll(preview.values()); - } - - private static PendingFetch fireSuggestionFetch(net.minecraft.client.multiplayer.ClientPacketListener connection, - String query, int timeoutMs) { - PendingFetch existing = PENDING_FETCHES.get(query); - if (existing != null && existing.connection() == connection && !existing.future().isDone()) return existing; - if (PENDING_FETCHES.size() > SUGGESTION_CACHE_MAX) return existing; - int requestId = autismclient.util.AutismCommandSuggestionIds.nextMacroId(); - CompletableFuture> future = new CompletableFuture<>(); - OneShotPacketListener listener = awaitReceive(packet -> { - if (!(packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket reply) - || reply.id() != requestId) { - return false; - } - java.util.List texts = new java.util.ArrayList<>(); - if (reply.suggestions() != null) { - for (var entry : reply.suggestions()) { - if (entry != null && entry.text() != null && !entry.text().isBlank()) texts.add(entry.text()); - } - } - future.complete(texts); - return true; - }); - PendingFetch pending = new PendingFetch(future, connection); - PENDING_FETCHES.put(query, pending); - future.completeOnTimeout(null, timeoutMs > 0 ? Math.max(1000, timeoutMs) : 30_000, - java.util.concurrent.TimeUnit.MILLISECONDS); - future.whenComplete((texts, error) -> { - listener.cancel(); - PENDING_FETCHES.remove(query, pending); - - if (texts != null) { - if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); - SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(texts), connection)); - } - }); - try { - connection.send(new net.minecraft.network.protocol.game.ServerboundCommandSuggestionPacket(requestId, query)); - } catch (Throwable error) { - future.complete(null); - } - return pending; - } - - private static void runCaptureCommandAutofill(Minecraft mc, CaptureValueAction action) { - RunState run = CURRENT_RUN.get(); - clearCaptureOutputs(action); - MacroTemplate.Resolution resolved = MacroVariables.resolve(action.autofillCommand, mc); - if (!resolved.success() || resolved.value().isBlank()) return; - String query = buildAutofillQuery(resolved.value(), action.listFilter, action.listFilterText); - var connection = mc.getConnection(); - if (connection == null) return; - - if (action.autofillCacheList) { - CachedSuggestions listCache = SUGGESTION_CACHE.get(query); - if (listCache != null && listCache.connection() == connection && !listCache.suggestions().isEmpty()) { - setCurrentStatus("Using cached suggestions"); - applyCaptureSuggestions(run, action, listCache.suggestions(), query); - return; - } - setCurrentStatus("Sweeping command suggestions"); - java.util.List all = sweepAutofillCommand(connection, query); - if (all.isEmpty()) return; - if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); - SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(all), connection)); - applyCaptureSuggestions(run, action, all, query); - return; - } - - CachedSuggestions cached = SUGGESTION_CACHE.get(query); - - if (cached != null && (cached.connection() != connection || cached.suggestions().isEmpty())) cached = null; - - if (cached != null) { - - setCurrentStatus("Using cached suggestions"); - applyCaptureSuggestions(run, action, capturePool(connection, action, query, cached.suggestions()), query); - fireSuggestionFetch(connection, query, action.autofillTimeoutMs); - return; - } - - setCurrentStatus("Requesting command suggestions"); - PendingFetch pending = fireSuggestionFetch(connection, query, action.autofillTimeoutMs); - if (pending == null) return; - if (!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); - } - - private static java.util.List sweepAutofillCommand(net.minecraft.client.multiplayer.ClientPacketListener connection, String query) { - autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() { - @Override public boolean cancelled() { - Minecraft mc = Minecraft.getInstance(); - return !isCurrentRunActive() || mc == null || mc.getConnection() != connection; - } - @Override public boolean paused() { return false; } - @Override public int limit() { return MAX_SWEEP_NAMES; } - @Override public int maxQueries() { return 50_000; } - @Override public boolean usernamesOnly() { return false; } - }; - java.util.List all = autismclient.util.AutismNameHarvest.sweepCommand(connection, query, control); - all.sort(String.CASE_INSENSITIVE_ORDER); - return all; - } - - private static void runCaptureNamescrape(Minecraft mc, CaptureValueAction action) { - RunState run = CURRENT_RUN.get(); - clearCaptureOutputs(action); - var connection = mc.getConnection(); - if (connection == null) return; - String cacheKey = "namescrape"; - CachedSuggestions cached = SUGGESTION_CACHE.get(cacheKey); - if (cached != null && cached.connection() == connection && !cached.suggestions().isEmpty()) { - setCurrentStatus("Using cached names"); - applyCaptureSuggestions(run, action, cached.suggestions(), ""); - return; - } - setCurrentStatus("Scraping player names"); - java.util.List names = scrapeAllNames(mc, connection); - if (names.isEmpty()) return; - if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear(); - SUGGESTION_CACHE.put(cacheKey, new CachedSuggestions(java.util.List.copyOf(names), connection)); - applyCaptureSuggestions(run, action, names, ""); - } - - private static java.util.List scrapeAllNames(Minecraft mc, net.minecraft.client.multiplayer.ClientPacketListener connection) { - String self = mc.player == null ? "" : mc.player.getName().getString(); - java.util.LinkedHashMap names = autismclient.util.AutismNameHarvest.instantNames(mc, self); - java.util.List vectors = - autismclient.util.AutismNameHarvest.discoverVectors(connection); - autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() { - @Override public boolean cancelled() { - Minecraft m = Minecraft.getInstance(); - return !isCurrentRunActive() || m == null || m.getConnection() != connection; - } - @Override public boolean paused() { return false; } - @Override public int limit() { return MAX_SWEEP_NAMES; } - @Override public int maxQueries() { return 100_000; } - }; - autismclient.util.AutismNameHarvest.sweep(connection, names, self, vectors, control); - java.util.List out = new java.util.ArrayList<>(names.values()); - out.sort(String.CASE_INSENSITIVE_ORDER); - return out; - } - - private static void runCaptureValueAction(Minecraft mc, CaptureValueAction action) { - if (action == null) return; - String modifierError = action.numberModifierError(); - if (!modifierError.isBlank()) { - - setCurrentStatus("Capture failed: " + modifierError); - autismclient.util.AutismNotifications.error("Capture Value: " + modifierError); - return; - } - if (action.source == CaptureValueAction.Source.COMMAND_AUTOFILL) { - setCurrentStatus("Requesting command suggestions"); - runCaptureCommandAutofill(mc, action); - reportMissingCapture(action); - return; - } - if (action.source == CaptureValueAction.Source.NAMESCRAPE) { - setCurrentStatus("Scraping player names"); - runCaptureNamescrape(mc, action); - reportMissingCapture(action); - return; - } - if (action.waitForTrigger) { - CaptureValueAction.Source source = action.source == null ? CaptureValueAction.Source.GUI_TITLE : action.source; - setCurrentStatus("Waiting for capture: " + source.name().toLowerCase(Locale.ROOT).replace('_', ' ')); - if (source == CaptureValueAction.Source.RECENT_CHAT) MacroPacketWait.awaitCaptureValueChat(action); - else if (source == CaptureValueAction.Source.SCOREBOARD) awaitCaptureValueScoreboard(mc, action); - else awaitCaptureValueState(mc, action); - return; - } - RunState run = CURRENT_RUN.get(); - java.util.concurrent.atomic.AtomicReference result = - new java.util.concurrent.atomic.AtomicReference<>(CaptureValueAction.Preview.unavailable("Not available")); - - long deadline = System.nanoTime() + CAPTURE_SETTLE_MS * 1_000_000L; - while (isCurrentRunActive()) { - CompletableFuture done = new CompletableFuture<>(); - mc.execute(() -> withRunContext(run, () -> { - try { - result.set(action.run(mc)); - done.complete(null); - } catch (Throwable error) { - done.completeExceptionally(error); - } - })); - - waitForFutureDone(done, 0); - if (result.get().success() || System.nanoTime() >= deadline) break; - waitOneTick(); - } - CaptureValueAction.Preview preview = result.get(); - if (!preview.success()) { - - setCurrentStatus("Capture failed: " + preview.message()); - autismclient.util.AutismNotifications.error( - "Capture " + captureOutputLabel(action) + ": " + preview.message()); - } - } - - private static void reportMissingCapture(CaptureValueAction action) { - String name = MacroVariableContext.cleanRootName(action.saveAs); - if (name.isBlank() || MacroVariables.value(name).isPresent()) return; - setCurrentStatus("Capture failed: nothing captured"); - autismclient.util.AutismNotifications.error( - "Capture " + captureOutputLabel(action) + ": nothing captured"); - } - - private static String captureOutputLabel(CaptureValueAction action) { - String name = MacroVariableContext.cleanRootName(action.saveAs); - return name.isBlank() ? "value" : "{" + name + "}"; - } - - private static void awaitCaptureValueState(Minecraft mc, CaptureValueAction action) { - RunState run = CURRENT_RUN.get(); - clearCaptureOutputs(action); - String lastMessage = ""; - while (isCurrentRunActive()) { - CaptureValueAction.Preview preview; - try { - preview = callOnClientThread(mc, () -> action.previewCurrent(mc), - CaptureValueAction.Preview.unavailable("Not available")); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - return; - } - if (preview.success()) { - if (run != null) run.variables.setAll(preview.values()); - else MacroVariables.setAll(preview.values()); - return; - } - if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) { - lastMessage = preview.message(); - setCurrentStatus("Capture waiting: " + lastMessage); - } - waitOneTick(); - } - } - - private static void awaitCaptureValueScoreboard(Minecraft mc, CaptureValueAction action) { - RunState run = CURRENT_RUN.get(); - clearCaptureOutputs(action); - String trackedKey = action.scoreboardRow; - int trackedRow = action.scoreboardRowIndex; - String trackedObjective = action.scoreboardObjective; - String previousText = null; - String lastMessage = ""; - boolean baselineReady = false; - while (isCurrentRunActive()) { - CaptureValueAction.ScoreboardLine line; - try { - String keySnapshot = trackedKey; - int rowSnapshot = trackedRow; - String objectiveSnapshot = trackedObjective; - line = callOnClientThread(mc, - () -> action.resolveScoreboardLine(mc, keySnapshot, rowSnapshot, objectiveSnapshot), null); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - return; - } - if (line != null) { - String currentText = line.text() == null ? "" : line.text(); - if (!baselineReady) { - trackedKey = line.key(); - trackedRow = line.row(); - trackedObjective = line.objective(); - previousText = currentText; - baselineReady = true; - } else if (!currentText.equals(previousText)) { - CaptureValueAction.Preview preview = action.previewScoreboardLine(line); - if (preview.success()) { - if (run != null) run.variables.setAll(preview.values()); - else MacroVariables.setAll(preview.values()); - return; - } - if (!preview.message().isBlank() && !preview.message().equals(lastMessage)) { - lastMessage = preview.message(); - setCurrentStatus("Capture waiting: " + lastMessage); - } - previousText = currentText; - } - } - waitOneTick(); - } - } static void waitForCondition(CompletableFuture future) { @@ -4327,7 +3937,7 @@ public class MacroExecutor { waitForCondition(future); } catch (CancellationException e) { } } else if (action instanceof CaptureValueAction captureValue) { - runCaptureValueAction(mc, captureValue); + MacroCaptureAutofill.runCaptureValueAction(mc, captureValue); } else if (action instanceof WaitForChatAction) { WaitForChatAction wca = (WaitForChatAction) action; setCurrentStatus("Waiting for chat: " + wca.pattern); @@ -6084,7 +5694,7 @@ public class MacroExecutor { return Boolean.TRUE.equals(callOnClientThread(mc, condition, Boolean.FALSE)); } - private static void waitOneTick() { + static void waitOneTick() { CompletableFuture tickFuture = MacroConditionRegistry.waitForNextTick(); waitForCondition(tickFuture); } @@ -6696,7 +6306,7 @@ public class MacroExecutor { } } - private static T callOnClientThread(Minecraft mc, java.util.function.Supplier supplier, T fallback) throws InterruptedException { + static T callOnClientThread(Minecraft mc, java.util.function.Supplier supplier, T fallback) throws InterruptedException { if (PackHideState.isHardLocked()) return fallback; RunState scheduledRun = CURRENT_RUN.get(); java.util.function.Supplier contextual = () -> { @@ -6724,7 +6334,7 @@ public class MacroExecutor { return result.get(); } - private static T callOnClientThread(Minecraft mc, java.util.function.Supplier supplier, T fallback, + static T callOnClientThread(Minecraft mc, java.util.function.Supplier supplier, T fallback, boolean tickAligned) throws InterruptedException { return tickAligned ? callOnClientTick(mc, supplier, fallback) : callOnClientThread(mc, supplier, fallback); }