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 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 diff --git a/src/client/java/com/lnlfly/IPickClient.java b/src/client/java/com/lnlfly/IPickClient.java new file mode 100644 index 0000000..8afd866 --- /dev/null +++ b/src/client/java/com/lnlfly/IPickClient.java @@ -0,0 +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/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/ipickClient.java b/src/client/java/com/lnlfly/ipickClient.java deleted file mode 100644 index d1db4f3..0000000 --- a/src/client/java/com/lnlfly/ipickClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.lnlfly; - -import net.fabricmc.api.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/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 7c206a4..30b0859 100644 --- a/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ConnectScreenMixin.java @@ -1,8 +1,10 @@ package com.lnlfly.mixin.client; +import com.lnlfly.IPick; 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; @@ -21,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 测速完成")); } } @@ -37,36 +36,17 @@ 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) { - 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); } } } 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 aa463a2..fa74370 100644 --- a/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java +++ b/src/client/java/com/lnlfly/mixin/client/ServerEntryMixin.java @@ -1,7 +1,9 @@ package com.lnlfly.mixin.client; 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; @@ -16,6 +18,10 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + @Mixin(MultiplayerServerListWidget.ServerEntry.class) public abstract class ServerEntryMixin { @Shadow @Final private ServerInfo server; @@ -24,61 +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 (!ServerUtil.isTargetServer(this.server.address)) return; - if (isTargetServerEntry()) { - IpManager ipManager = IpManager.get(); - - Text statusText; - if (ipManager.isReloading()) { - 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); + 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); } - - 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(); + } else if (ipManager.isTimeout()) { + statusText = Text.literal("获取 IP 超时").formatted(Formatting.RED); + } else { + statusText = Text.literal("获取 IP 失败").formatted(Formatting.RED); } - } - private boolean isTargetServerEntry() { - String target = ModConfig.get().targetServerAddress; - if (target == null) return false; + TextRenderer textRenderer = this.client.textRenderer; - String targetHost = normalizeHost(target); - String entryHost = normalizeHost(this.server.address); - return !targetHost.isEmpty() && targetHost.equalsIgnoreCase(entryHost); - } + // 显示在服务器名称右侧,字体缩小 + 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); - 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.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; + } + + Text line = Text.literal(ip.hostname + ": ").formatted(Formatting.GRAY) + .append(Text.literal(latencyStr).formatted(color)); + tooltip.add(line); + } + + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } } - return value; } } 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 28be857..50f1fb6 100644 --- a/src/client/java/com/lnlfly/util/IpManager.java +++ b/src/client/java/com/lnlfly/util/IpManager.java @@ -1,28 +1,44 @@ 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; import java.io.*; 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.*; -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; +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 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; @@ -31,17 +47,37 @@ 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; - + // 延迟测试相关 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..."); + 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."); + } + }); + } + + /** + * 进入多人游戏界面时调用:从本地缓存加载 IP 并测速 + */ public void load() { if (!ModConfig.get().selectBestIpOnEnter) { this.activeRunId = runSequence.incrementAndGet(); @@ -55,33 +91,41 @@ public void load() { this.activeRunId = runId; this.testingLatency = true; resetState(); - + CompletableFuture.runAsync(() -> { - System.out.println("IPick: Loading IPs..."); + IPick.LOGGER.info("Loading IPs from cache..."); try { - URL url = new URL(ModConfig.get().ipListUrl); - 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(); + 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 { + // 本地缓存为空,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; - System.out.println("IPick: Successfully loaded " + this.ipCount + " IPs."); - testLatencies(runId, loadedIps); + IPick.LOGGER.info("Fetched and cached {} IPs from remote.", this.ipCount); + testLatencies(runId, fetchedIps); } else { - System.err.println("IPick: Loaded IPs is null."); + IPick.LOGGER.warn("Failed to fetch IPs from remote."); } } } catch (Exception e) { + this.fetching = false; 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; @@ -89,6 +133,10 @@ public void load() { this.reloading = false; if (!this.success) this.testingLatency = false; } + }).exceptionally(e -> { + IPick.LOGGER.error("Unexpected error during IP loading: {}", e.getMessage()); + this.fetching = false; + return null; }); } @@ -100,55 +148,114 @@ 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 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 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); + } 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 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) { if (runIps == null || runIps.isEmpty()) return; - + 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)); - } - + 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()); - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - } finally { - executor.shutdown(); - } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenRun(() -> { + if (!isRunActive(runId)) return; - if (!isRunActive(runId)) return; - - this.bestIp = runIps.stream() - .filter(ip -> ip.latency >= 0) - .min(Comparator.comparingLong(ip -> ip.latency)) - .orElse(null); - - if (this.bestIp != null) { - System.out.println("IPick: Best IP is " + this.bestIp.hostname + " with " + this.bestIp.latency + "ms"); - } else { - System.out.println("IPick: 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) { @@ -160,7 +267,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()); @@ -184,7 +291,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; @@ -230,10 +337,12 @@ 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; } 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; } 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); + } +} diff --git a/src/main/java/com/lnlfly/IPick.java b/src/main/java/com/lnlfly/IPick.java new file mode 100644 index 0000000..a1ea710 --- /dev/null +++ b/src/main/java/com/lnlfly/IPick.java @@ -0,0 +1,15 @@ +package com.lnlfly; + +import net.fabricmc.api.ModInitializer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IPick implements ModInitializer { + public static final Logger LOGGER = LoggerFactory.getLogger("ipick"); + + @Override + public void onInitialize() { + LOGGER.info("IPick initialized."); + } +} diff --git a/src/main/java/com/lnlfly/ipick.java b/src/main/java/com/lnlfly/ipick.java deleted file mode 100644 index 882ca58..0000000 --- a/src/main/java/com/lnlfly/ipick.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.lnlfly; - -import net.fabricmc.api.ModInitializer; - -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 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!"); - } -} \ 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": [