mirror of
https://github.com/AutismDevelopment/Autism-Client.git
synced 2026-10-28 16:19:47 +00:00
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.
This commit is contained in:
parent
026800738b
commit
b4417fd464
2 changed files with 420 additions and 402 deletions
408
src/main/java/autismclient/util/macro/MacroCaptureAutofill.java
Normal file
408
src/main/java/autismclient/util/macro/MacroCaptureAutofill.java
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
package autismclient.util.macro;
|
||||
|
||||
import autismclient.util.AutismCommandSuggestionIds;
|
||||
import autismclient.util.AutismNameHarvest;
|
||||
import autismclient.util.AutismNotifications;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class MacroCaptureAutofill {
|
||||
private MacroCaptureAutofill() {
|
||||
}
|
||||
private record CachedSuggestions(java.util.List<String> suggestions, Object connection) {}
|
||||
private record PendingFetch(CompletableFuture<java.util.List<String>> future, Object connection) {}
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, CachedSuggestions> SUGGESTION_CACHE =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, PendingFetch> PENDING_FETCHES =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final int SUGGESTION_CACHE_MAX = 256;
|
||||
private static final long SUGGESTION_CACHE_IDLE_RESET_MS = 3000L;
|
||||
|
||||
private static final int TRUNCATED_REPLY_SIZE = 1000;
|
||||
private static final int MIN_SWEEP_CAP = 8;
|
||||
private static final int MAX_SWEEP_NAMES = 60_000;
|
||||
private static final String DRILL_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789_";
|
||||
static volatile long allRunsStoppedAtMs;
|
||||
|
||||
private static final class SuggestionUnion {
|
||||
final Object connection;
|
||||
final java.util.LinkedHashSet<String> names = new java.util.LinkedHashSet<>();
|
||||
final java.util.ArrayDeque<String> sweepQueue = new java.util.ArrayDeque<>();
|
||||
int alphabetCursor;
|
||||
int cap;
|
||||
|
||||
SuggestionUnion(Object connection, String baseQuery) {
|
||||
this.connection = connection;
|
||||
sweepQueue.add(baseQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, SuggestionUnion> SUGGESTION_UNIONS =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
public static void expireSuggestionCacheAfterIdle() {
|
||||
long stoppedAt = allRunsStoppedAtMs;
|
||||
if (stoppedAt > 0 && System.currentTimeMillis() - stoppedAt > SUGGESTION_CACHE_IDLE_RESET_MS) {
|
||||
SUGGESTION_CACHE.clear();
|
||||
PENDING_FETCHES.clear();
|
||||
SUGGESTION_UNIONS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static String buildAutofillQuery(String resolvedCommand, CaptureListSelector.Filter filter, String filterText) {
|
||||
String query = resolvedCommand.startsWith("/") ? resolvedCommand : "/" + resolvedCommand;
|
||||
|
||||
if (!query.contains(" ")) query = query + " ";
|
||||
String prefix = filter == CaptureListSelector.Filter.PREFIX && filterText != null ? filterText.trim() : "";
|
||||
if (!prefix.isEmpty() && !prefix.contains(" ") && query.endsWith(" ")) {
|
||||
|
||||
int end = query.length();
|
||||
while (end > 0 && query.charAt(end - 1) == ' ') end--;
|
||||
query = query.substring(0, end) + " " + prefix;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private static java.util.List<String> capturePool(net.minecraft.client.multiplayer.ClientPacketListener connection,
|
||||
CaptureValueAction action, String query, java.util.List<String> base) {
|
||||
|
||||
if (!action.autofillCacheList && base.size() < TRUNCATED_REPLY_SIZE) return base;
|
||||
SuggestionUnion union = SUGGESTION_UNIONS.compute(query, (key, existing) ->
|
||||
existing != null && existing.connection == connection ? existing : new SuggestionUnion(connection, query));
|
||||
synchronized (union) {
|
||||
union.names.addAll(base);
|
||||
union.cap = Math.max(union.cap, base.size());
|
||||
advanceSweep(union, connection, action.autofillTimeoutMs);
|
||||
java.util.List<String> pool = new java.util.ArrayList<>(union.names);
|
||||
boolean ordered = action.listSelection == CaptureListSelector.Selection.SEQUENTIAL
|
||||
|| action.listSelection == CaptureListSelector.Selection.FIRST
|
||||
|| action.listSelection == CaptureListSelector.Selection.LAST
|
||||
|| action.listSelection == CaptureListSelector.Selection.POSITION;
|
||||
|
||||
if (ordered) pool.sort(String.CASE_INSENSITIVE_ORDER);
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
private static void advanceSweep(SuggestionUnion union, net.minecraft.client.multiplayer.ClientPacketListener connection, int timeoutMs) {
|
||||
if (union.names.size() >= MAX_SWEEP_NAMES) { union.sweepQueue.clear(); return; }
|
||||
while (!union.sweepQueue.isEmpty()) {
|
||||
if (union.alphabetCursor >= DRILL_ALPHABET.length()) {
|
||||
union.sweepQueue.poll();
|
||||
union.alphabetCursor = 0;
|
||||
continue;
|
||||
}
|
||||
String target = union.sweepQueue.peek() + DRILL_ALPHABET.charAt(union.alphabetCursor);
|
||||
CachedSuggestions cached = SUGGESTION_CACHE.get(target);
|
||||
if (cached == null || cached.connection() != connection) {
|
||||
fireSuggestionFetch(connection, target, timeoutMs);
|
||||
return;
|
||||
}
|
||||
union.names.addAll(cached.suggestions());
|
||||
int size = cached.suggestions().size();
|
||||
|
||||
boolean truncated = size >= union.cap && union.cap >= MIN_SWEEP_CAP;
|
||||
union.cap = Math.max(union.cap, size);
|
||||
if (truncated) union.sweepQueue.add(target);
|
||||
union.alphabetCursor++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyCaptureSuggestions(MacroExecutor.RunState run, CaptureValueAction action,
|
||||
java.util.List<String> suggestions, String query) {
|
||||
CaptureValueAction.Preview preview = action.previewSuggestions(suggestions, query);
|
||||
if (!preview.success()) return;
|
||||
if (run != null) run.variables.setAll(preview.values());
|
||||
else MacroVariables.setAll(preview.values());
|
||||
}
|
||||
|
||||
private static PendingFetch fireSuggestionFetch(net.minecraft.client.multiplayer.ClientPacketListener connection,
|
||||
String query, int timeoutMs) {
|
||||
PendingFetch existing = PENDING_FETCHES.get(query);
|
||||
if (existing != null && existing.connection() == connection && !existing.future().isDone()) return existing;
|
||||
if (PENDING_FETCHES.size() > SUGGESTION_CACHE_MAX) return existing;
|
||||
int requestId = autismclient.util.AutismCommandSuggestionIds.nextMacroId();
|
||||
CompletableFuture<java.util.List<String>> future = new CompletableFuture<>();
|
||||
MacroExecutor.OneShotPacketListener listener = MacroExecutor.awaitReceive(packet -> {
|
||||
if (!(packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket reply)
|
||||
|| reply.id() != requestId) {
|
||||
return false;
|
||||
}
|
||||
java.util.List<String> texts = new java.util.ArrayList<>();
|
||||
if (reply.suggestions() != null) {
|
||||
for (var entry : reply.suggestions()) {
|
||||
if (entry != null && entry.text() != null && !entry.text().isBlank()) texts.add(entry.text());
|
||||
}
|
||||
}
|
||||
future.complete(texts);
|
||||
return true;
|
||||
});
|
||||
PendingFetch pending = new PendingFetch(future, connection);
|
||||
PENDING_FETCHES.put(query, pending);
|
||||
future.completeOnTimeout(null, timeoutMs > 0 ? Math.max(1000, timeoutMs) : 30_000,
|
||||
java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
future.whenComplete((texts, error) -> {
|
||||
listener.cancel();
|
||||
PENDING_FETCHES.remove(query, pending);
|
||||
|
||||
if (texts != null) {
|
||||
if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear();
|
||||
SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(texts), connection));
|
||||
}
|
||||
});
|
||||
try {
|
||||
connection.send(new net.minecraft.network.protocol.game.ServerboundCommandSuggestionPacket(requestId, query));
|
||||
} catch (Throwable error) {
|
||||
future.complete(null);
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
private static void runCaptureCommandAutofill(Minecraft mc, CaptureValueAction action) {
|
||||
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
|
||||
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<String> all = sweepAutofillCommand(connection, query);
|
||||
if (all.isEmpty()) return;
|
||||
if (SUGGESTION_CACHE.size() >= SUGGESTION_CACHE_MAX) SUGGESTION_CACHE.clear();
|
||||
SUGGESTION_CACHE.put(query, new CachedSuggestions(java.util.List.copyOf(all), connection));
|
||||
applyCaptureSuggestions(run, action, all, query);
|
||||
return;
|
||||
}
|
||||
|
||||
CachedSuggestions cached = SUGGESTION_CACHE.get(query);
|
||||
|
||||
if (cached != null && (cached.connection() != connection || cached.suggestions().isEmpty())) cached = null;
|
||||
|
||||
if (cached != null) {
|
||||
|
||||
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<String> suggestions = pending.future().getNow(null);
|
||||
if (suggestions == null || suggestions.isEmpty()) return;
|
||||
applyCaptureSuggestions(run, action, capturePool(connection, action, query, suggestions), query);
|
||||
}
|
||||
|
||||
private static java.util.List<String> sweepAutofillCommand(net.minecraft.client.multiplayer.ClientPacketListener connection, String query) {
|
||||
autismclient.util.AutismNameHarvest.Control control = new autismclient.util.AutismNameHarvest.Control() {
|
||||
@Override public boolean cancelled() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
return !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<String> all = autismclient.util.AutismNameHarvest.sweepCommand(connection, query, control);
|
||||
all.sort(String.CASE_INSENSITIVE_ORDER);
|
||||
return all;
|
||||
}
|
||||
|
||||
private static void runCaptureNamescrape(Minecraft mc, CaptureValueAction action) {
|
||||
MacroExecutor.RunState run = MacroExecutor.CURRENT_RUN.get();
|
||||
MacroExecutor.clearCaptureOutputs(action);
|
||||
var connection = mc.getConnection();
|
||||
if (connection == null) return;
|
||||
String cacheKey = " | ||||