From 2792dd66916699baed45e794c0addb89b4732255 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Wed, 18 Feb 2026 16:18:08 +0800 Subject: [PATCH 01/14] fix: ExecutorService leakage and thread safety issues --- .../java/com/lnlfly/util/IpManager.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 28be857..5b77ce6 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -11,9 +11,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -21,8 +19,20 @@ public class IpManager { private static final IpManager INSTANCE = new IpManager(); private static final int LATENCY_TIMEOUT = 3000; + private static final int MAX_CONCURRENT_TESTS = 10; + private List ips = Collections.emptyList(); - private ExecutorService executor; + private final ExecutorService executor = new ThreadPoolExecutor( + 0, MAX_CONCURRENT_TESTS, + 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), + r -> { + Thread t = new Thread(r, "IPick-LatencyTest"); + t.setDaemon(true); + return t; + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); private final AtomicInteger runSequence = new AtomicInteger(0); private volatile int activeRunId = 0; @@ -111,11 +121,7 @@ private void testLatencies(int runId, List runIps) { this.testingLatency = true; this.latencyTestCompleted = false; System.out.println("IPick: Starting latency tests (Handshake Mode)..."); - - if (executor == null || executor.isShutdown()) { - executor = Executors.newFixedThreadPool(Math.min(runIps.size(), 10)); - } - + List> futures = runIps.stream() .filter(ip -> ip.enabled) .map(ip -> CompletableFuture.runAsync(() -> { @@ -128,11 +134,7 @@ private void testLatencies(int runId, List runIps) { }, executor)) .collect(Collectors.toList()); - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - } finally { - executor.shutdown(); - } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); if (!isRunActive(runId)) return; From 6656515839b3b030da8162dedeb554257991c4c2 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Wed, 18 Feb 2026 16:23:30 +0800 Subject: [PATCH 02/14] fix: replace new URL(String) with URI --- src/client/java/com/lnlfly/util/IpManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 5b77ce6..1108588 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -8,6 +8,7 @@ import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.*; @@ -70,7 +71,7 @@ public void load() { System.out.println("IPick: Loading IPs..."); try { - URL url = new URL(ModConfig.get().ipListUrl); + URL url = URI.create(ModConfig.get().ipListUrl).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(2000); connection.setReadTimeout(2000); From 915cbc5a4d14ca059d6f44af5ce092c8630e1fca Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Wed, 18 Feb 2026 16:29:11 +0800 Subject: [PATCH 03/14] ci: change java version to 17 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b01da52..5e238e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: matrix: # Use these Java versions java: [ - 21, # Current Java LTS + 17, # Minimum supported Java version ] runs-on: ubuntu-22.04 steps: @@ -30,7 +30,7 @@ jobs: - name: build run: ./gradlew build - name: capture build artifacts - if: ${{ matrix.java == '21' }} # Only upload artifacts built from latest java + if: ${{ matrix.java == '17' }} uses: actions/upload-artifact@v4 with: name: Artifacts From af6919e5c073567e06c7f2fc5ec0bf546d9ecb2c Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Wed, 18 Feb 2026 20:11:38 +0800 Subject: [PATCH 04/14] fix: handle unexpected exceptions during ip loading --- src/client/java/com/lnlfly/util/IpManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 1108588..6f3fa0b 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -100,6 +100,9 @@ public void load() { this.reloading = false; if (!this.success) this.testingLatency = false; } + }).exceptionally(e -> { + System.err.println("IPick: Unexpected error during IP loading: " + e.getMessage()); + return null; }); } From a74f01c7ef005cc41c379bd1bc96e8542a3bc22b Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Wed, 18 Feb 2026 20:24:49 +0800 Subject: [PATCH 05/14] ref: rename classes, encapsulate latency and improve logging --- src/client/java/com/lnlfly/ipickClient.java | 5 ++- .../mixin/client/ConnectScreenMixin.java | 3 +- src/client/java/com/lnlfly/util/IpInfo.java | 10 +++++- .../java/com/lnlfly/util/IpManager.java | 31 ++++++++++--------- src/main/java/com/lnlfly/ipick.java | 13 ++------ src/main/resources/fabric.mod.json | 4 +-- 6 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/client/java/com/lnlfly/ipickClient.java b/src/client/java/com/lnlfly/ipickClient.java index d1db4f3..d119ce4 100644 --- a/src/client/java/com/lnlfly/ipickClient.java +++ b/src/client/java/com/lnlfly/ipickClient.java @@ -2,9 +2,8 @@ import net.fabricmc.api.ClientModInitializer; -public class ipickClient implements ClientModInitializer { +public class IPickClient implements ClientModInitializer { @Override public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. } -} \ No newline at end of file +} diff --git a/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java b/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java index 7c206a4..0a2f1c8 100644 --- a/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java @@ -1,5 +1,6 @@ package com.lnlfly.mixin.client; +import com.lnlfly.IPick; import com.lnlfly.config.ModConfig; import com.lnlfly.util.IpInfo; import com.lnlfly.util.IpManager; @@ -45,7 +46,7 @@ private static ServerAddress modifyAddress(ServerAddress address) { // 如果找到了最优 IP,则替换 if (bestIp != null) { - System.out.println("IPick: Redirecting connection from " + address.getAddress() + " to best IP: " + bestIp.ip + ":" + bestIp.port); + IPick.LOGGER.info("Redirecting connection from {} to best IP: {}:{}", address.getAddress(), bestIp.ip, bestIp.port); return new ServerAddress(bestIp.ip, bestIp.port); } } diff --git a/src/client/java/com/lnlfly/util/IpInfo.java b/src/client/java/com/lnlfly/util/IpInfo.java index 740c8d6..e475b95 100644 --- a/src/client/java/com/lnlfly/util/IpInfo.java +++ b/src/client/java/com/lnlfly/util/IpInfo.java @@ -6,5 +6,13 @@ public class IpInfo { public String ip; public int port; public boolean enabled; - public long latency = -1; // -1 means not tested or failed + private volatile long latency = -1; // -1 means not tested or failed + + public long getLatency() { + return latency; + } + + public void setLatency(long latency) { + this.latency = latency; + } } diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 6f3fa0b..fc09503 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.lnlfly.IPick; import com.lnlfly.config.ModConfig; import java.io.*; @@ -68,7 +69,7 @@ public void load() { resetState(); CompletableFuture.runAsync(() -> { - System.out.println("IPick: Loading IPs..."); + IPick.LOGGER.info("Loading IPs..."); try { URL url = URI.create(ModConfig.get().ipListUrl).toURL(); @@ -84,15 +85,15 @@ public void load() { this.ips = loadedIps; this.ipCount = loadedIps.size(); this.success = true; - System.out.println("IPick: Successfully loaded " + this.ipCount + " IPs."); + IPick.LOGGER.info("Successfully loaded {} IPs.", this.ipCount); testLatencies(runId, loadedIps); } else { - System.err.println("IPick: Loaded IPs is null."); + IPick.LOGGER.warn("Loaded IPs is null."); } } } catch (Exception e) { if (!isRunActive(runId)) return; - System.err.println("IPick: Failed to load IPs: " + e.getMessage()); + IPick.LOGGER.error("Failed to load IPs: {}", e.getMessage()); this.timeout = (e instanceof java.net.SocketTimeoutException); } finally { if (!isRunActive(runId)) return; @@ -101,7 +102,7 @@ public void load() { if (!this.success) this.testingLatency = false; } }).exceptionally(e -> { - System.err.println("IPick: Unexpected error during IP loading: " + e.getMessage()); + IPick.LOGGER.error("Unexpected error during IP loading: {}", e.getMessage()); return null; }); } @@ -124,16 +125,16 @@ private void testLatencies(int runId, List runIps) { this.testingLatency = true; this.latencyTestCompleted = false; - System.out.println("IPick: Starting latency tests (Handshake Mode)..."); + IPick.LOGGER.info("Starting latency tests (Handshake Mode)..."); List> futures = runIps.stream() .filter(ip -> ip.enabled) .map(ip -> CompletableFuture.runAsync(() -> { - ip.latency = measureLatency(ip.ip, ip.port); - if (ip.latency >= 0) { - System.out.println("IPick: IP " + ip.hostname + " latency: " + ip.latency + "ms"); + ip.setLatency(measureLatency(ip.ip, ip.port)); + if (ip.getLatency() >= 0) { + IPick.LOGGER.info("IP {} latency: {}ms", ip.hostname, ip.getLatency()); } else { - System.out.println("IPick: IP " + ip.hostname + " unreachable"); + IPick.LOGGER.info("IP {} unreachable", ip.hostname); } }, executor)) .collect(Collectors.toList()); @@ -143,14 +144,14 @@ private void testLatencies(int runId, List runIps) { if (!isRunActive(runId)) return; this.bestIp = runIps.stream() - .filter(ip -> ip.latency >= 0) - .min(Comparator.comparingLong(ip -> ip.latency)) + .filter(ip -> ip.getLatency() >= 0) + .min(Comparator.comparingLong(IpInfo::getLatency)) .orElse(null); - + if (this.bestIp != null) { - System.out.println("IPick: Best IP is " + this.bestIp.hostname + " with " + this.bestIp.latency + "ms"); + IPick.LOGGER.info("Best IP is {} with {}ms", this.bestIp.hostname, this.bestIp.getLatency()); } else { - System.out.println("IPick: No reachable IP found."); + IPick.LOGGER.info("No reachable IP found."); } this.testingLatency = false; diff --git a/src/main/java/com/lnlfly/ipick.java b/src/main/java/com/lnlfly/ipick.java index 882ca58..a1ea710 100644 --- a/src/main/java/com/lnlfly/ipick.java +++ b/src/main/java/com/lnlfly/ipick.java @@ -5,18 +5,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ipick implements ModInitializer { - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. +public class IPick implements ModInitializer { public static final Logger LOGGER = LoggerFactory.getLogger("ipick"); @Override public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); + LOGGER.info("IPick initialized."); } -} \ No newline at end of file +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index bce5d1f..c86a6be 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -16,10 +16,10 @@ "environment": "*", "entrypoints": { "main": [ - "com.lnlfly.ipick" + "com.lnlfly.IPick" ], "client": [ - "com.lnlfly.ipickClient" + "com.lnlfly.IPickClient" ] }, "mixins": [ From cfd057aa9e3d3295842cc07a5ae068747ebda4e2 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 09:58:27 +0800 Subject: [PATCH 06/14] fix: fix thread safety issues and make latency tests asynchronous --- .../java/com/lnlfly/config/ModConfig.java | 6 ++-- .../java/com/lnlfly/util/IpManager.java | 34 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/client/java/com/lnlfly/config/ModConfig.java b/src/client/java/com/lnlfly/config/ModConfig.java index fb27582..82e158c 100644 --- a/src/client/java/com/lnlfly/config/ModConfig.java +++ b/src/client/java/com/lnlfly/config/ModConfig.java @@ -18,14 +18,14 @@ public class ModConfig { public String ipListUrl = "https://gitea.dusays.com/fenychn0206/static/raw/branch/main/IPs.json"; // 默认 API 地址 public String targetServerAddress = "mc.lnlfly.com"; // 默认目标服务器地址 - public static ModConfig get() { + public static synchronized ModConfig get() { if (INSTANCE == null) { load(); } return INSTANCE; } - public static void load() { + public static synchronized void load() { if (CONFIG_FILE.exists()) { try (FileReader reader = new FileReader(CONFIG_FILE)) { INSTANCE = GSON.fromJson(reader, ModConfig.class); @@ -39,7 +39,7 @@ public static void load() { } } - public static void save() { + public static synchronized void save() { if (INSTANCE == null) return; try (FileWriter writer = new FileWriter(CONFIG_FILE)) { GSON.toJson(INSTANCE, writer); diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index fc09503..4a14465 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -122,7 +122,7 @@ public void reload() { private void testLatencies(int runId, List runIps) { if (runIps == null || runIps.isEmpty()) return; - + this.testingLatency = true; this.latencyTestCompleted = false; IPick.LOGGER.info("Starting latency tests (Handshake Mode)..."); @@ -139,23 +139,23 @@ private void testLatencies(int runId, List runIps) { }, executor)) .collect(Collectors.toList()); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenRun(() -> { + if (!isRunActive(runId)) return; - if (!isRunActive(runId)) return; - - this.bestIp = runIps.stream() - .filter(ip -> ip.getLatency() >= 0) - .min(Comparator.comparingLong(IpInfo::getLatency)) - .orElse(null); - - if (this.bestIp != null) { - IPick.LOGGER.info("Best IP is {} with {}ms", this.bestIp.hostname, this.bestIp.getLatency()); - } else { - IPick.LOGGER.info("No reachable IP found."); - } - - this.testingLatency = false; - this.latencyTestCompleted = true; + this.bestIp = runIps.stream() + .filter(ip -> ip.getLatency() >= 0) + .min(Comparator.comparingLong(IpInfo::getLatency)) + .orElse(null); + + if (this.bestIp != null) { + IPick.LOGGER.info("Best IP is {} with {}ms", this.bestIp.hostname, this.bestIp.getLatency()); + } else { + IPick.LOGGER.info("No reachable IP found."); + } + + this.testingLatency = false; + this.latencyTestCompleted = true; + }); } private boolean isRunActive(int runId) { From 81f6b5365f5ea4308866e9e576acf4caa7f8f4e0 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 10:06:57 +0800 Subject: [PATCH 07/14] fix: rename IPick.java to fix workflow error --- src/main/java/com/lnlfly/{ipick.java => IPick.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/com/lnlfly/{ipick.java => IPick.java} (100%) diff --git a/src/main/java/com/lnlfly/ipick.java b/src/main/java/com/lnlfly/IPick.java similarity index 100% rename from src/main/java/com/lnlfly/ipick.java rename to src/main/java/com/lnlfly/IPick.java From 10f29fc4356ca66fe4c51ef938485864326df3ae Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 10:14:35 +0800 Subject: [PATCH 08/14] fix: rename IPickClient.java to fix workflow error --- src/client/java/com/lnlfly/{ipickClient.java => IPickClient.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/client/java/com/lnlfly/{ipickClient.java => IPickClient.java} (100%) diff --git a/src/client/java/com/lnlfly/ipickClient.java b/src/client/java/com/lnlfly/IPickClient.java similarity index 100% rename from src/client/java/com/lnlfly/ipickClient.java rename to src/client/java/com/lnlfly/IPickClient.java From c6d31828aa2ddd246a43f74fbd22f809336de3e3 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 11:47:09 +0800 Subject: [PATCH 09/14] feat: implement ip caching system and optimize latency testing flow --- src/client/java/com/lnlfly/IPickClient.java | 2 + .../lnlfly/mixin/client/ServerEntryMixin.java | 4 +- .../java/com/lnlfly/util/IpManager.java | 132 ++++++++++++++---- 3 files changed, 107 insertions(+), 31 deletions(-) diff --git a/src/client/java/com/lnlfly/IPickClient.java b/src/client/java/com/lnlfly/IPickClient.java index d119ce4..8afd866 100644 --- a/src/client/java/com/lnlfly/IPickClient.java +++ b/src/client/java/com/lnlfly/IPickClient.java @@ -1,9 +1,11 @@ package com.lnlfly; +import com.lnlfly.util.IpManager; import net.fabricmc.api.ClientModInitializer; public class IPickClient implements ClientModInitializer { @Override public void onInitializeClient() { + IpManager.get().fetchAndCacheIps(); } } diff --git a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java index aa463a2..f2128ac 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java @@ -29,9 +29,7 @@ public void render(DrawContext context, int index, int y, int x, int entryWidth, IpManager ipManager = IpManager.get(); Text statusText; - if (ipManager.isReloading()) { - statusText = Text.literal("正在重新获取 IP...").formatted(Formatting.YELLOW); - } else if (ipManager.isTestingLatency()) { + if (ipManager.isTestingLatency()) { statusText = Text.literal("正在测试延迟...").formatted(Formatting.YELLOW); } else if (ipManager.isSuccess()) { if (!ipManager.isLatencyTestCompleted()) { diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 4a14465..bef4cd4 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -1,6 +1,7 @@ package com.lnlfly.util; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.lnlfly.IPick; import com.lnlfly.config.ModConfig; @@ -17,11 +18,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import net.fabricmc.loader.api.FabricLoader; + public class IpManager { private static final IpManager INSTANCE = new IpManager(); private static final int LATENCY_TIMEOUT = 3000; - private static final int MAX_CONCURRENT_TESTS = 10; + private static final File CACHE_FILE = FabricLoader.getInstance().getConfigDir().resolve("IPs.json").toFile(); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private List ips = Collections.emptyList(); private final ExecutorService executor = new ThreadPoolExecutor( @@ -44,16 +48,47 @@ public class IpManager { private volatile boolean timeout = false; private volatile boolean reloading = false; private volatile int ipCount = 0; - + // 延迟测试相关 private volatile boolean testingLatency = false; private volatile boolean latencyTestCompleted = false; private volatile IpInfo bestIp = null; - + public static IpManager get() { return INSTANCE; } + /** + * MC 启动时调用:从远程获取 IP 列表并缓存到本地文件 + */ + public void fetchAndCacheIps() { + CompletableFuture.runAsync(() -> { + IPick.LOGGER.info("Fetching IP list from remote..."); + try { + URL url = URI.create(ModConfig.get().ipListUrl).toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(2000); + connection.setReadTimeout(2000); + connection.setRequestMethod("GET"); + + try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + List fetchedIps = new Gson().fromJson(reader, new TypeToken>(){}.getType()); + if (fetchedIps != null) { + saveCacheFile(fetchedIps); + IPick.LOGGER.info("Fetched and cached {} IPs to {}.", fetchedIps.size(), CACHE_FILE.getName()); + } else { + IPick.LOGGER.warn("Fetched IP list is null."); + } + } + } catch (Exception e) { + IPick.LOGGER.error("Failed to fetch IPs from remote: {}", e.getMessage()); + } + }); + } + + /** + * 进入多人游戏界面时调用:从本地缓存加载 IP 并测速 + */ public void load() { if (!ModConfig.get().selectBestIpOnEnter) { this.activeRunId = runSequence.incrementAndGet(); @@ -67,34 +102,25 @@ public void load() { this.activeRunId = runId; this.testingLatency = true; resetState(); - + CompletableFuture.runAsync(() -> { - IPick.LOGGER.info("Loading IPs..."); + IPick.LOGGER.info("Loading IPs from cache..."); try { - URL url = URI.create(ModConfig.get().ipListUrl).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(2000); - connection.setReadTimeout(2000); - connection.setRequestMethod("GET"); - - try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { - List loadedIps = new Gson().fromJson(reader, new TypeToken>(){}.getType()); - if (loadedIps != null) { - if (!isRunActive(runId)) return; - this.ips = loadedIps; - this.ipCount = loadedIps.size(); - this.success = true; - IPick.LOGGER.info("Successfully loaded {} IPs.", this.ipCount); - testLatencies(runId, loadedIps); - } else { - IPick.LOGGER.warn("Loaded IPs is null."); - } + List cachedIps = loadCacheFile(); + if (cachedIps != null && !cachedIps.isEmpty()) { + if (!isRunActive(runId)) return; + this.ips = cachedIps; + this.ipCount = cachedIps.size(); + this.success = true; + IPick.LOGGER.info("Loaded {} IPs from cache.", this.ipCount); + testLatencies(runId, cachedIps); + } else { + IPick.LOGGER.warn("No cached IPs found. Please restart MC to fetch from remote."); } } catch (Exception e) { if (!isRunActive(runId)) return; - IPick.LOGGER.error("Failed to load IPs: {}", e.getMessage()); - this.timeout = (e instanceof java.net.SocketTimeoutException); + IPick.LOGGER.error("Failed to load cached IPs: {}", e.getMessage()); } finally { if (!isRunActive(runId)) return; this.loaded = true; @@ -115,9 +141,59 @@ private void resetState() { this.latencyTestCompleted = false; } + /** + * 刷新时调用:只用已加载的 IP 列表重新测速 + */ public void reload() { + if (!ModConfig.get().selectBestIpOnEnter) return; + if (this.ips.isEmpty()) { + // 没有已加载的 IP,回退到从缓存加载 + this.reloading = true; + this.load(); + return; + } + + int runId = runSequence.incrementAndGet(); + this.activeRunId = runId; this.reloading = true; - this.load(); + this.testingLatency = true; + this.bestIp = null; + this.latencyTestCompleted = false; + + // 重置每个 IP 的延迟 + for (IpInfo ip : this.ips) { + ip.setLatency(-1); + } + + CompletableFuture.runAsync(() -> { + IPick.LOGGER.info("Re-testing latencies for {} IPs...", this.ipCount); + testLatencies(runId, this.ips); + }).thenRun(() -> { + if (!isRunActive(runId)) return; + this.reloading = false; + }).exceptionally(e -> { + IPick.LOGGER.error("Unexpected error during latency re-test: {}", e.getMessage()); + this.reloading = false; + return null; + }); + } + + private void saveCacheFile(List ipList) { + try (FileWriter writer = new FileWriter(CACHE_FILE)) { + GSON.toJson(ipList, writer); + } catch (IOException e) { + IPick.LOGGER.error("Failed to save IP cache: {}", e.getMessage()); + } + } + + private List loadCacheFile() { + if (!CACHE_FILE.exists()) return null; + try (FileReader reader = new FileReader(CACHE_FILE)) { + return new Gson().fromJson(reader, new TypeToken>(){}.getType()); + } catch (Exception e) { + IPick.LOGGER.error("Failed to read IP cache: {}", e.getMessage()); + return null; + } } private void testLatencies(int runId, List runIps) { @@ -167,7 +243,7 @@ private long measureLatency(String host, int port) { try (Socket socket = new Socket()) { socket.setSoTimeout(LATENCY_TIMEOUT); socket.connect(new InetSocketAddress(host, port), 2000); - + DataOutputStream out = new DataOutputStream(socket.getOutputStream()); DataInputStream in = new DataInputStream(socket.getInputStream()); @@ -191,7 +267,7 @@ private long measureLatency(String host, int port) { // 3. 等待响应 readVarInt(in); // Length readVarInt(in); // Packet ID - + return System.currentTimeMillis() - start; } catch (Exception e) { return -1; From c578a641a3056570cb960bdebf593faaee26f0fc Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 12:03:57 +0800 Subject: [PATCH 10/14] feat: add fetching status and fallback to remote ip fetching when cache is empty --- .../lnlfly/mixin/client/ServerEntryMixin.java | 4 +- .../java/com/lnlfly/util/IpManager.java | 65 +++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java index f2128ac..a35021e 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java @@ -29,7 +29,9 @@ public void render(DrawContext context, int index, int y, int x, int entryWidth, IpManager ipManager = IpManager.get(); Text statusText; - if (ipManager.isTestingLatency()) { + if (ipManager.isFetching()) { + statusText = Text.literal("正在获取 IP...").formatted(Formatting.YELLOW); + } else if (ipManager.isTestingLatency()) { statusText = Text.literal("正在测试延迟...").formatted(Formatting.YELLOW); } else if (ipManager.isSuccess()) { if (!ipManager.isLatencyTestCompleted()) { diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index bef4cd4..e62977a 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -47,6 +47,7 @@ public class IpManager { private volatile boolean success = false; private volatile boolean timeout = false; private volatile boolean reloading = false; + private volatile boolean fetching = false; private volatile int ipCount = 0; // 延迟测试相关 @@ -64,24 +65,12 @@ public static IpManager get() { public void fetchAndCacheIps() { CompletableFuture.runAsync(() -> { IPick.LOGGER.info("Fetching IP list from remote..."); - try { - URL url = URI.create(ModConfig.get().ipListUrl).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(2000); - connection.setReadTimeout(2000); - connection.setRequestMethod("GET"); - - try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { - List fetchedIps = new Gson().fromJson(reader, new TypeToken>(){}.getType()); - if (fetchedIps != null) { - saveCacheFile(fetchedIps); - IPick.LOGGER.info("Fetched and cached {} IPs to {}.", fetchedIps.size(), CACHE_FILE.getName()); - } else { - IPick.LOGGER.warn("Fetched IP list is null."); - } - } - } catch (Exception e) { - IPick.LOGGER.error("Failed to fetch IPs from remote: {}", e.getMessage()); + List fetchedIps = fetchIpsFromRemote(); + if (fetchedIps != null) { + saveCacheFile(fetchedIps); + IPick.LOGGER.info("Fetched and cached {} IPs to {}.", fetchedIps.size(), CACHE_FILE.getName()); + } else { + IPick.LOGGER.warn("Failed to fetch IP list from remote."); } }); } @@ -116,11 +105,28 @@ public void load() { IPick.LOGGER.info("Loaded {} IPs from cache.", this.ipCount); testLatencies(runId, cachedIps); } else { - IPick.LOGGER.warn("No cached IPs found. Please restart MC to fetch from remote."); + // 本地缓存为空,fallback 到远程获取 + IPick.LOGGER.warn("No cached IPs found, fetching from remote..."); + this.fetching = true; + List fetchedIps = fetchIpsFromRemote(); + this.fetching = false; + if (!isRunActive(runId)) return; + if (fetchedIps != null && !fetchedIps.isEmpty()) { + saveCacheFile(fetchedIps); + this.ips = fetchedIps; + this.ipCount = fetchedIps.size(); + this.success = true; + IPick.LOGGER.info("Fetched and cached {} IPs from remote.", this.ipCount); + testLatencies(runId, fetchedIps); + } else { + IPick.LOGGER.warn("Failed to fetch IPs from remote."); + } } } catch (Exception e) { + this.fetching = false; if (!isRunActive(runId)) return; - IPick.LOGGER.error("Failed to load cached IPs: {}", e.getMessage()); + IPick.LOGGER.error("Failed to load IPs: {}", e.getMessage()); + this.timeout = (e instanceof java.net.SocketTimeoutException); } finally { if (!isRunActive(runId)) return; this.loaded = true; @@ -129,6 +135,7 @@ public void load() { } }).exceptionally(e -> { IPick.LOGGER.error("Unexpected error during IP loading: {}", e.getMessage()); + this.fetching = false; return null; }); } @@ -178,6 +185,23 @@ public void reload() { }); } + private List fetchIpsFromRemote() { + try { + URL url = URI.create(ModConfig.get().ipListUrl).toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(2000); + connection.setReadTimeout(2000); + connection.setRequestMethod("GET"); + + try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + return new Gson().fromJson(reader, new TypeToken>(){}.getType()); + } + } catch (Exception e) { + IPick.LOGGER.error("Failed to fetch IPs from remote: {}", e.getMessage()); + return null; + } + } + private void saveCacheFile(List ipList) { try (FileWriter writer = new FileWriter(CACHE_FILE)) { GSON.toJson(ipList, writer); @@ -317,6 +341,7 @@ private int readVarInt(DataInputStream in) throws IOException { public boolean isSuccess() { return success; } public boolean isTimeout() { return timeout; } public boolean isReloading() { return reloading; } + public boolean isFetching() { return fetching; } public int getIpCount() { return ipCount; } public boolean isTestingLatency() { return testingLatency; } public boolean isLatencyTestCompleted() { return latencyTestCompleted; } From c8bc30cfc24936fd86ad66d501217a94d268685d Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 14:28:24 +0800 Subject: [PATCH 11/14] feat: add latency tooltip display and ip list getter --- .../lnlfly/mixin/client/ServerEntryMixin.java | 55 +++++++++++++++++-- .../java/com/lnlfly/util/IpManager.java | 1 + 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java index a35021e..08a257a 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java @@ -1,6 +1,7 @@ package com.lnlfly.mixin.client; import com.lnlfly.config.ModConfig; +import com.lnlfly.util.IpInfo; import com.lnlfly.util.IpManager; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; @@ -16,6 +17,9 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.List; + @Mixin(MultiplayerServerListWidget.ServerEntry.class) public abstract class ServerEntryMixin { @Shadow @Final private ServerInfo server; @@ -48,19 +52,62 @@ public void render(DrawContext context, int index, int y, int x, int entryWidth, } TextRenderer textRenderer = this.client.textRenderer; - + // 显示在服务器名称右侧,字体缩小 int nameWidth = textRenderer.getWidth(this.server.name); float scale = 0.75f; - + context.getMatrices().push(); // 平移到名称右侧,垂直居中对齐 context.getMatrices().translate(x + 32 + 3 + nameWidth + 5, y + 1 + (textRenderer.fontHeight * (1 - scale)) / 2, 0); context.getMatrices().scale(scale, scale, 1.0f); - + context.drawText(textRenderer, statusText, 0, 0, 0xFFFFFF, true); - + context.getMatrices().pop(); + + // 信号图标区域悬停 tooltip:显示各节点延迟 + if (ipManager.isLatencyTestCompleted()) { + int iconX = x + entryWidth - 15; + int iconY = y; + int iconW = 10; + int iconH = entryHeight; + + if (mouseX >= iconX && mouseX <= iconX + iconW && mouseY >= iconY && mouseY <= iconY + iconH) { + List tooltip = new ArrayList<>(); + tooltip.add(Text.literal("节点延迟:").formatted(Formatting.GRAY)); + + IpInfo bestIp = ipManager.getBestIp(); + List sortedIps = ipManager.getIps().stream() + .filter(ip -> ip.enabled) + .sorted((a, b) -> { + // 延迟 >= 0 的排前面,按延迟升序;-1(超时)排后面 + if (a.getLatency() < 0 && b.getLatency() < 0) return 0; + if (a.getLatency() < 0) return 1; + if (b.getLatency() < 0) return -1; + return Long.compare(a.getLatency(), b.getLatency()); + }) + .collect(java.util.stream.Collectors.toList()); + for (IpInfo ip : sortedIps) { + + String latencyStr; + Formatting color; + if (ip.getLatency() < 0) { + latencyStr = "超时"; + color = Formatting.RED; + } else { + latencyStr = ip.getLatency() + "ms"; + color = (bestIp != null && ip == bestIp) ? Formatting.GREEN : Formatting.WHITE; + } + + Text line = Text.literal(ip.hostname + ": ").formatted(Formatting.GRAY) + .append(Text.literal(latencyStr).formatted(color)); + tooltip.add(line); + } + + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } } } diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index e62977a..6a5e567 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -337,6 +337,7 @@ private int readVarInt(DataInputStream in) throws IOException { } // Getters + public List getIps() { return ips; } public boolean isLoaded() { return loaded; } public boolean isSuccess() { return success; } public boolean isTimeout() { return timeout; } From 05be7d7a955314a27f4e1032c1bb435b7a7db8db Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 20:21:24 +0800 Subject: [PATCH 12/14] ref: refactor server detection logic into dedicated utility class --- .../mixin/client/AddServerScreenMixin.java | 37 ++-- .../mixin/client/ConnectScreenMixin.java | 31 +--- .../mixin/client/ServerAddressMixin.java | 15 +- .../lnlfly/mixin/client/ServerEntryMixin.java | 160 ++++++++---------- .../java/com/lnlfly/util/IpManager.java | 4 +- 5 files changed, 88 insertions(+), 159 deletions(-) diff --git a/src/client/java/com/lnlfly/mixin/client/AddServerScreenMixin.java b/src/client/java/com/lnlfly/mixin/client/AddServerScreenMixin.java index 958fe16..a78e0af 100644 --- a/src/client/java/com/lnlfly/mixin/client/AddServerScreenMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/AddServerScreenMixin.java @@ -1,6 +1,7 @@ package com.lnlfly.mixin.client; import com.lnlfly.config.ModConfig; +import com.lnlfly.util.ServerUtil; import net.minecraft.client.gui.screen.multiplayer.AddServerScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.CheckboxWidget; @@ -29,33 +30,27 @@ protected AddServerScreenMixin(Text title) { @Inject(method = "init", at = @At("TAIL")) private void onInit(CallbackInfo ci) { int centerX = this.width / 2; - int yStart = this.height / 4 + 96; - + int yStart = this.height / 4 + 96; + Text label = Text.literal("进入时选择最优IP"); - - // 计算宽度以居中 - // CheckboxWidget 的宽度通常是 20 (方框) + 4 (间距) + 文本宽度 + int width = this.textRenderer.getWidth(label) + 24; - // 加载配置 ModConfig config = ModConfig.get(); - // 居中显示“进入时选择最优IP” this.autoSelectBestIpCheckbox = CheckboxWidget.builder(label, this.textRenderer) .pos(centerX - width / 2, yStart) - .checked(config.selectBestIpOnEnter) // 设置初始状态 + .checked(config.selectBestIpOnEnter) .callback((checkbox, checked) -> { config.selectBestIpOnEnter = checked; - ModConfig.save(); // 保存配置 + ModConfig.save(); }) .build(); this.addDrawableChild(this.autoSelectBestIpCheckbox); - // 初始可见性检查 updateCheckboxVisibility(); - // 为地址栏添加监听器以更新可见性 if (this.addressField != null) { this.addressField.setChangedListener(text -> { updateCheckboxVisibility(); @@ -66,23 +61,11 @@ private void onInit(CallbackInfo ci) { private void updateCheckboxVisibility() { if (this.addressField == null) return; - - String enteredHost = normalizeHost(this.addressField.getText()); - String target = ModConfig.get().targetServerAddress; - String targetHost = target == null ? "" : normalizeHost(target); - boolean isSpecificDomain = !targetHost.isEmpty() && targetHost.equalsIgnoreCase(enteredHost); - - if (this.autoSelectBestIpCheckbox != null) { - this.autoSelectBestIpCheckbox.visible = isSpecificDomain; - } - } - private String normalizeHost(String address) { - String value = address.trim(); - int colon = value.indexOf(':'); - if (colon > 0 && value.indexOf(']') < 0) { - return value.substring(0, colon); + boolean isTarget = ServerUtil.isTargetServer(this.addressField.getText()); + + if (this.autoSelectBestIpCheckbox != null) { + this.autoSelectBestIpCheckbox.visible = isTarget; } - return value; } } diff --git a/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java b/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java index 0a2f1c8..30b0859 100644 --- a/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java @@ -4,6 +4,7 @@ import com.lnlfly.config.ModConfig; import com.lnlfly.util.IpInfo; import com.lnlfly.util.IpManager; +import com.lnlfly.util.ServerUtil; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.multiplayer.ConnectScreen; @@ -22,14 +23,11 @@ public class ConnectScreenMixin { @Inject(method = "connect(Lnet/minecraft/client/gui/screen/Screen;Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/client/network/ServerAddress;Lnet/minecraft/client/network/ServerInfo;Z)V", at = @At("HEAD"), cancellable = true) private static void onConnect(Screen parent, MinecraftClient client, ServerAddress address, ServerInfo info, boolean quickPlay, CallbackInfo ci) { - if (isTargetServer(address)) { - // 检查配置是否开启 + if (ServerUtil.isTargetServer(address.getAddress())) { if (ModConfig.get().selectBestIpOnEnter) { IpManager ipManager = IpManager.get(); - // 如果 IP 还没获取到或正在测速 if (!ipManager.isLoaded() || ipManager.isTestingLatency()) { - ci.cancel(); // 取消连接 - // 弹出提示 + ci.cancel(); SystemToast.add(client.getToastManager(), SystemToast.Type.PERIODIC_NOTIFICATION, Text.literal("IPick"), Text.literal("请等待服务器 IP 测速完成")); } } @@ -38,13 +36,11 @@ private static void onConnect(Screen parent, MinecraftClient client, ServerAddre @ModifyVariable(method = "connect(Lnet/minecraft/client/gui/screen/Screen;Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/client/network/ServerAddress;Lnet/minecraft/client/network/ServerInfo;Z)V", at = @At("HEAD"), argsOnly = true) private static ServerAddress modifyAddress(ServerAddress address) { - if (isTargetServer(address)) { - // 检查配置是否开启 + if (ServerUtil.isTargetServer(address.getAddress())) { if (ModConfig.get().selectBestIpOnEnter) { IpManager ipManager = IpManager.get(); IpInfo bestIp = ipManager.getBestIp(); - - // 如果找到了最优 IP,则替换 + if (bestIp != null) { IPick.LOGGER.info("Redirecting connection from {} to best IP: {}:{}", address.getAddress(), bestIp.ip, bestIp.port); return new ServerAddress(bestIp.ip, bestIp.port); @@ -53,21 +49,4 @@ private static ServerAddress modifyAddress(ServerAddress address) { } return address; } - - private static boolean isTargetServer(ServerAddress address) { - String target = ModConfig.get().targetServerAddress; - if (target == null) return false; - - String targetHost = normalizeHost(target); - return !targetHost.isEmpty() && address.getAddress().equalsIgnoreCase(targetHost); - } - - private static String normalizeHost(String address) { - String value = address.trim(); - int colon = value.indexOf(':'); - if (colon > 0 && value.indexOf(']') < 0) { - return value.substring(0, colon); - } - return value; - } } diff --git a/src/client/java/com/lnlfly/mixin/client/ServerAddressMixin.java b/src/client/java/com/lnlfly/mixin/client/ServerAddressMixin.java index 2a139c5..b2e481f 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerAddressMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerAddressMixin.java @@ -3,6 +3,7 @@ import com.lnlfly.config.ModConfig; import com.lnlfly.util.IpInfo; import com.lnlfly.util.IpManager; +import com.lnlfly.util.ServerUtil; import net.minecraft.client.network.ServerAddress; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -16,9 +17,7 @@ public class ServerAddressMixin { ModConfig config = ModConfig.get(); if (!config.selectBestIpOnEnter) return; - String targetHost = normalizeHost(config.targetServerAddress); - String requestHost = normalizeHost(address); - if (targetHost.isEmpty() || !targetHost.equalsIgnoreCase(requestHost)) return; + if (!ServerUtil.isTargetServer(address)) return; IpManager ipManager = IpManager.get(); IpInfo bestIp = ipManager.getBestIp(); @@ -26,14 +25,4 @@ public class ServerAddressMixin { cir.setReturnValue(new ServerAddress(bestIp.ip, bestIp.port)); } - - private static String normalizeHost(String value) { - if (value == null) return ""; - String host = value.trim(); - int colon = host.indexOf(':'); - if (colon > 0 && host.indexOf(']') < 0) { - return host.substring(0, colon); - } - return host; - } } diff --git a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java index 08a257a..fa74370 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java @@ -3,6 +3,7 @@ import com.lnlfly.config.ModConfig; import com.lnlfly.util.IpInfo; import com.lnlfly.util.IpManager; +import com.lnlfly.util.ServerUtil; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; @@ -19,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Mixin(MultiplayerServerListWidget.ServerEntry.class) public abstract class ServerEntryMixin { @@ -28,104 +30,80 @@ public abstract class ServerEntryMixin { @Inject(method = "render", at = @At("TAIL")) public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta, CallbackInfo ci) { if (!ModConfig.get().selectBestIpOnEnter) return; - - if (isTargetServerEntry()) { - IpManager ipManager = IpManager.get(); - - Text statusText; - if (ipManager.isFetching()) { - statusText = Text.literal("正在获取 IP...").formatted(Formatting.YELLOW); - } else if (ipManager.isTestingLatency()) { - statusText = Text.literal("正在测试延迟...").formatted(Formatting.YELLOW); - } else if (ipManager.isSuccess()) { - if (!ipManager.isLatencyTestCompleted()) { - statusText = Text.literal("正在测试延迟...").formatted(Formatting.YELLOW); - } else if (ipManager.getBestIp() != null) { - statusText = Text.literal("最快 IP 为 " + ipManager.getBestIp().hostname).formatted(Formatting.GREEN); - } else { - statusText = Text.literal("所有 IP 均超时").formatted(Formatting.RED); - } - } else if (ipManager.isTimeout()) { - statusText = Text.literal("获取 IP 超时").formatted(Formatting.RED); + if (!ServerUtil.isTargetServer(this.server.address)) return; + + IpManager ipManager = IpManager.get(); + + Text statusText; + if (ipManager.isFetching()) { + statusText = Text.literal("正在获取 IP...").formatted(Formatting.YELLOW); + } else if (ipManager.isTestingLatency() || (ipManager.isSuccess() && !ipManager.isLatencyTestCompleted())) { + statusText = Text.literal("正在测试延迟...").formatted(Formatting.YELLOW); + } else if (ipManager.isLatencyTestCompleted()) { + if (ipManager.getBestIp() != null) { + statusText = Text.literal("最快 IP 为 " + ipManager.getBestIp().hostname).formatted(Formatting.GREEN); } else { - statusText = Text.literal("获取 IP 失败").formatted(Formatting.RED); + statusText = Text.literal("所有 IP 均超时").formatted(Formatting.RED); } + } else if (ipManager.isTimeout()) { + statusText = Text.literal("获取 IP 超时").formatted(Formatting.RED); + } else { + statusText = Text.literal("获取 IP 失败").formatted(Formatting.RED); + } - TextRenderer textRenderer = this.client.textRenderer; - - // 显示在服务器名称右侧,字体缩小 - int nameWidth = textRenderer.getWidth(this.server.name); - float scale = 0.75f; - - context.getMatrices().push(); - // 平移到名称右侧,垂直居中对齐 - context.getMatrices().translate(x + 32 + 3 + nameWidth + 5, y + 1 + (textRenderer.fontHeight * (1 - scale)) / 2, 0); - context.getMatrices().scale(scale, scale, 1.0f); - - context.drawText(textRenderer, statusText, 0, 0, 0xFFFFFF, true); - - context.getMatrices().pop(); - - // 信号图标区域悬停 tooltip:显示各节点延迟 - if (ipManager.isLatencyTestCompleted()) { - int iconX = x + entryWidth - 15; - int iconY = y; - int iconW = 10; - int iconH = entryHeight; - - if (mouseX >= iconX && mouseX <= iconX + iconW && mouseY >= iconY && mouseY <= iconY + iconH) { - List tooltip = new ArrayList<>(); - tooltip.add(Text.literal("节点延迟:").formatted(Formatting.GRAY)); - - IpInfo bestIp = ipManager.getBestIp(); - List sortedIps = ipManager.getIps().stream() - .filter(ip -> ip.enabled) - .sorted((a, b) -> { - // 延迟 >= 0 的排前面,按延迟升序;-1(超时)排后面 - if (a.getLatency() < 0 && b.getLatency() < 0) return 0; - if (a.getLatency() < 0) return 1; - if (b.getLatency() < 0) return -1; - return Long.compare(a.getLatency(), b.getLatency()); - }) - .collect(java.util.stream.Collectors.toList()); - for (IpInfo ip : sortedIps) { - - String latencyStr; - Formatting color; - if (ip.getLatency() < 0) { - latencyStr = "超时"; - color = Formatting.RED; - } else { - latencyStr = ip.getLatency() + "ms"; - color = (bestIp != null && ip == bestIp) ? Formatting.GREEN : Formatting.WHITE; - } - - Text line = Text.literal(ip.hostname + ": ").formatted(Formatting.GRAY) - .append(Text.literal(latencyStr).formatted(color)); - tooltip.add(line); + TextRenderer textRenderer = this.client.textRenderer; + + // 显示在服务器名称右侧,字体缩小 + int nameWidth = textRenderer.getWidth(this.server.name); + float scale = 0.75f; + + context.getMatrices().push(); + context.getMatrices().translate(x + 32 + 3 + nameWidth + 5, y + 1 + (textRenderer.fontHeight * (1 - scale)) / 2, 0); + context.getMatrices().scale(scale, scale, 1.0f); + + context.drawText(textRenderer, statusText, 0, 0, 0xFFFFFF, true); + + context.getMatrices().pop(); + + // 信号图标区域悬停 tooltip:显示各节点延迟 + if (ipManager.isLatencyTestCompleted()) { + int iconX = x + entryWidth - 15; + int iconY = y; + int iconW = 10; + int iconH = entryHeight; + + if (mouseX >= iconX && mouseX <= iconX + iconW && mouseY >= iconY && mouseY <= iconY + iconH) { + List tooltip = new ArrayList<>(); + tooltip.add(Text.literal("节点延迟:").formatted(Formatting.GRAY)); + + IpInfo bestIp = ipManager.getBestIp(); + List sortedIps = ipManager.getIps().stream() + .filter(ip -> ip.enabled) + .sorted((a, b) -> { + if (a.getLatency() < 0 && b.getLatency() < 0) return 0; + if (a.getLatency() < 0) return 1; + if (b.getLatency() < 0) return -1; + return Long.compare(a.getLatency(), b.getLatency()); + }) + .collect(Collectors.toList()); + for (IpInfo ip : sortedIps) { + String latencyStr; + Formatting color; + if (ip.getLatency() < 0) { + latencyStr = "超时"; + color = Formatting.RED; + } else { + latencyStr = ip.getLatency() + "ms"; + color = (bestIp != null && ip == bestIp) ? Formatting.GREEN : Formatting.WHITE; } - context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + Text line = Text.literal(ip.hostname + ": ").formatted(Formatting.GRAY) + .append(Text.literal(latencyStr).formatted(color)); + tooltip.add(line); } - } - } - } - - private boolean isTargetServerEntry() { - String target = ModConfig.get().targetServerAddress; - if (target == null) return false; - String targetHost = normalizeHost(target); - String entryHost = normalizeHost(this.server.address); - return !targetHost.isEmpty() && targetHost.equalsIgnoreCase(entryHost); - } - - private String normalizeHost(String address) { - String value = address == null ? "" : address.trim(); - int colon = value.indexOf(':'); - if (colon > 0 && value.indexOf(']') < 0) { - return value.substring(0, colon); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } } - return value; } } diff --git a/src/client/java/com/lnlfly/util/IpManager.java b/src/client/java/com/lnlfly/util/IpManager.java index 6a5e567..50f1fb6 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -194,7 +194,7 @@ private List fetchIpsFromRemote() { connection.setRequestMethod("GET"); try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { - return new Gson().fromJson(reader, new TypeToken>(){}.getType()); + return GSON.fromJson(reader, new TypeToken>(){}.getType()); } } catch (Exception e) { IPick.LOGGER.error("Failed to fetch IPs from remote: {}", e.getMessage()); @@ -213,7 +213,7 @@ private void saveCacheFile(List ipList) { private List loadCacheFile() { if (!CACHE_FILE.exists()) return null; try (FileReader reader = new FileReader(CACHE_FILE)) { - return new Gson().fromJson(reader, new TypeToken>(){}.getType()); + return GSON.fromJson(reader, new TypeToken>(){}.getType()); } catch (Exception e) { IPick.LOGGER.error("Failed to read IP cache: {}", e.getMessage()); return null; From 89e1e192dc467d56a5d6199bd3d465a50e3e8700 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 19 Feb 2026 20:31:43 +0800 Subject: [PATCH 13/14] fix: add ServerUtil.java --- .../java/com/lnlfly/util/ServerUtil.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/client/java/com/lnlfly/util/ServerUtil.java diff --git a/src/client/java/com/lnlfly/util/ServerUtil.java b/src/client/java/com/lnlfly/util/ServerUtil.java new file mode 100644 index 0000000..e3e2167 --- /dev/null +++ b/src/client/java/com/lnlfly/util/ServerUtil.java @@ -0,0 +1,24 @@ +package com.lnlfly.util; + +import com.lnlfly.config.ModConfig; + +public class ServerUtil { + public static String normalizeHost(String address) { + if (address == null) return ""; + String value = address.trim(); + int colon = value.indexOf(':'); + if (colon > 0 && value.indexOf(']') < 0) { + return value.substring(0, colon); + } + return value; + } + + public static boolean isTargetServer(String address) { + String target = ModConfig.get().targetServerAddress; + if (target == null) return false; + + String targetHost = normalizeHost(target); + String host = normalizeHost(address); + return !targetHost.isEmpty() && targetHost.equalsIgnoreCase(host); + } +} From 2b0c19b7bf9950a2edd21724df4160feff21e887 Mon Sep 17 00:00:00 2001 From: AirTouch666 Date: Thu, 26 Feb 2026 11:08:16 +0800 Subject: [PATCH 14/14] chore: update mod version from 1.0.0 to 1.1.0 --- CHANGELOG.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++ gradle.properties | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4a8386d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,64 @@ +# Changelog + +## v1.1.0 Release + +### Features + +- **IP 缓存系统** - 实现 IP 缓存机制,优化延迟测试流程 (`c6d3182`) +- **获取状态与远程回退** - 添加获取状态显示,缓存为空时回退到远程 IP 获取 (`c578a64`) +- **延迟 Tooltip 显示** - 添加延迟 Tooltip 展示和 IP 列表获取接口 (`c8bc30c`) + +### Bug Fixes + +- **线程安全与异步测速** - 修复线程安全问题,将延迟测试改为异步执行 (`cfd057a`) +- **异常处理** - 处理 IP 加载过程中的意外异常 (`af6919e`) +- **URL 弃用 API 替换** - 使用 URI 替代已弃用的 `new URL(String)` (`6656515`) +- **线程泄漏** - 修复 ExecutorService 泄漏和线程安全问题 (`2792dd6`) +- **补充 Mixin** - 添加缺失的 ServerAddressMixin (`c4fc3bf`)、ServerUtil (`89e1e19`) +- **工作流修复** - 重命名类文件以修复 CI 构建错误 (`10f29fc`, `81f6b53`) + +### Refactor + +- **服务器检测逻辑抽取** - 将服务器检测逻辑重构为独立的工具类 (`05be7d7`) +- **类重命名与封装优化** - 重命名类,封装延迟逻辑并改进日志 (`a74f01c`) + +### CI + +- **Java 版本变更** - 将 CI 的 Java 版本更改为 17 (`915cbc5`) + +--- + +## v1.0.0 Release + +### Features + +- **统一配置并优化底层连接** - 统一配置管理,所有底层连接切换为优选 IP (`83d5985`) +- **IP 管理刷新功能** - 添加 IP 管理刷新功能并优化状态显示 (`cdfe06c`) +- **IP 延迟测试** - 添加 IP 延迟测试并显示最快 IP (`82ca4e3`) +- **IP 获取超时提示** - 添加 IP 获取超时状态显示 (`b7f5b61`) +- **服务器列表状态展示** - 在服务器列表中显示自定义 IP 状态 (`b3daff1`) +- **服务器 IP 优选配置** - 添加服务器 IP 优选配置选项 (`d048dd6`) +- **简化 UI** - 移除自动 IP 切换,简化用户界面 (`4163285`) + +### Bug Fixes + +- **消息显示错误** - 修复消息显示异常 (`e81a100`) +- **测速等待问题** - 修复未等待测速完成就访问的问题 (`19ce16f`) +- **自动优选连接** - 修复连接时自动选择最优 IP 的逻辑 (`cd38b79`) +- **状态文本位置** - 优化状态文本显示位置 (`aee0a8a`) + +### Refactor + +- **更新默认目标服务器地址** (`d052c52`) +- **重构 IP 管理器** (`b173d5d`) + +### Performance + +- **优化延迟测试逻辑** (`74d2060`) + +### Chores + +- **优化代码结构** (`3771a4e`) +- **移除示例 mixin 文件** (`801f50c`) +- **更新模组基础信息** (`7b2fd8a`, `fa340f5`) +- **初始化 README** (`791c6f8`) diff --git a/gradle.properties b/gradle.properties index f3577c7..e9f33fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.4+build.3 loader_version=0.15.7 # Mod Properties -mod_version=1.0.0 +mod_version=1.1.0 maven_group=com.lnlfly archives_base_name=ipick