From a70b8d7baae58ba3fc1298b036e078c46cafebb1 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 9 Feb 2026 22:54:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=20EasyTier=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=8A=82=E7=82=B9=20(?= =?UTF-8?q?#5504)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/terracotta/TerracottaManager.java | 42 ++++--- .../hmcl/terracotta/TerracottaNodeList.java | 103 ++++++++++++++++++ .../src/main/resources/assets/terracotta.json | 42 +++---- .../jackhuang/hmcl/util/io/NetworkUtils.java | 21 ++++ gradle/libs.versions.toml | 2 +- 5 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaNodeList.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaManager.java index f93b11d7c..7a3238ace 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaManager.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.FXThread; import org.jackhuang.hmcl.util.InvocationDispatcher; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.HttpRequest; @@ -44,13 +45,15 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; +import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -310,11 +313,18 @@ public final class TerracottaManager { public static TerracottaState.HostScanning setScanning() { TerracottaState state = STATE_V.get(); if (state instanceof TerracottaState.PortSpecific portSpecific) { - String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of( - "player", getPlayerName() - )); + Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch) + .thenComposeAsync(nodes -> { + List> query = new ArrayList<>(nodes.size() + 1); + query.add(pair("player", getPlayerName())); + for (URI node : nodes) { + query.add(pair("public_nodes", node.toString())); + } + return new GetTask(NetworkUtils.withQuery( + "http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query + )).setSignificance(Task.TaskSignificance.MINOR); + }).start(); - new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start(); return new TerracottaState.HostScanning(-1, -1, null); } return null; @@ -323,15 +333,19 @@ public final class TerracottaManager { public static Task setGuesting(String room) { TerracottaState state = STATE_V.get(); if (state instanceof TerracottaState.PortSpecific portSpecific) { - String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of( - "room", room, - "player", getPlayerName() - )); - - return new GetTask(uri) - .setSignificance(Task.TaskSignificance.MINOR) - .thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null)) - .setSignificance(Task.TaskSignificance.MINOR); + return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch) + .thenComposeAsync(nodes -> { + ArrayList> query = new ArrayList<>(nodes.size() + 2); + query.add(pair("room", room)); + query.add(pair("player", getPlayerName())); + for (URI node : nodes) { + query.add(pair("public_nodes", node.toString())); + } + return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query)) + .setSignificance(Task.TaskSignificance.MINOR) + .thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null)) + .setSignificance(Task.TaskSignificance.MINOR); + }); } else { return null; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaNodeList.java b/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaNodeList.java new file mode 100644 index 000000000..bb5171ed1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaNodeList.java @@ -0,0 +1,103 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.terracotta; + +import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonSerializable; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.gson.TolerableValidationException; +import org.jackhuang.hmcl.util.gson.Validation; +import org.jackhuang.hmcl.util.i18n.LocaleUtils; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/// @author Glavo +public final class TerracottaNodeList { + private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes"; + + @JsonSerializable + private record TerracottaNode(String url, @Nullable String region) implements Validation { + @Override + public void validate() throws JsonParseException, TolerableValidationException { + Validation.requireNonNull(url, "TerracottaNode.url cannot be null"); + try { + new URI(url); + } catch (URISyntaxException e) { + throw new JsonParseException("Invalid URL: " + url, e); + } + } + } + + private static volatile List list; + + public static List fetch() { + List list = TerracottaNodeList.list; + if (list != null) + return list; + + synchronized (TerracottaNodeList.class) { + list = TerracottaNodeList.list; + if (list != null) + return list; + + try { + List nodes = HttpRequest.GET(NODE_LIST_URL) + .getJson(JsonUtils.listTypeOf(TerracottaNode.class)); + + if (nodes == null) { + list = List.of(); + LOG.info("No available Terracotta nodes found"); + } else { + list = nodes.stream() + .filter(node -> { + if (node == null) + return false; + + try { + node.validate(); + } catch (Exception e) { + LOG.warning("Invalid terracotta node: " + node, e); + return false; + } + + return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region); + }) + .map(it -> URI.create(it.url())) + .toList(); + LOG.info("Terracotta node list: " + list); + } + } catch (Exception e) { + LOG.warning("Failed to fetch terracotta node list", e); + list = List.of(); + } + + TerracottaNodeList.list = list; + return list; + } + } + + private TerracottaNodeList() { + } +} diff --git a/HMCL/src/main/resources/assets/terracotta.json b/HMCL/src/main/resources/assets/terracotta.json index 4074b3996..648a03e7e 100644 --- a/HMCL/src/main/resources/assets/terracotta.json +++ b/HMCL/src/main/resources/assets/terracotta.json @@ -1,63 +1,63 @@ { "__comment__": "THIS FILE IS MACHINE GENERATED! DO NOT EDIT!", - "version_latest": "0.4.1", + "version_latest": "0.4.2", "packages": { "windows-x86_64": { - "hash": "4693fec29bb54a0bb1a1a8a263a935f1673b8f76e84d125974cf259f1912a195beab4dfd04c23cae592cf71b116e82ecd48828b1445ab75c980c8cd79c777d21", + "hash": "6a98f524d4f00373696517306af8aa50d01d55ce4eadb27e9e4bc2f882707a0b5f20d5d4c33371d1459dcf5bf144ffed9beb414202d9ccf32b11dbbfcf19d650", "files": { "VCRUNTIME140.DLL": "3d4b24061f72c0e957c7b04a0c4098c94c8f1afb4a7e159850b9939c7210d73398be6f27b5ab85073b4e8c999816e7804fef0f6115c39cd061f4aaeb4dcda8cf", - "terracotta-0.4.1-windows-x86_64.exe": "3d29f7deb61b8b87e14032baecad71042340f6259d7ff2822336129714470e1a951cd96ee5d6a8c9312e7cf1bb9bb9d1cbf757cfa44d2b2f214362c32ea03b5b" + "terracotta-0.4.2-windows-x86_64.exe": "6e98d1f2380ed22fb5a2dd4aafce6c773e9cf69100c8bb8e49e7d6983756bdb9a31f80e06bcfbe5a2742144fe806d3d687dec54d8f09d87c659341f99dd9fd80" } }, "windows-arm64": { - "hash": "6c68da53142cc92598a9d3b803f110c77927ee7b481e2f623dfab17cd97fee4a58378e796030529713d3e394355135cc01d5f5d86cef7dbd31bbf8e913993d4c", + "hash": "fc1077247014ac0c712469498bde2ef7f6d881d5fcb7bdd5e11ebe20218fed365be19afdb8d453a79d77b729f866058522b910741767f4df947faa891434b463", "files": { "VCRUNTIME140.DLL": "5cb5ce114614101d260f4754c09e8a0dd57e4da885ebb96b91e274326f3e1dd95ed0ade9f542f1922fad0ed025e88a1f368e791e1d01fae69718f0ec3c7b98c8", - "terracotta-0.4.1-windows-arm64.exe": "d2cf0f29aac752d322e594da6144bbe2874aabde52b5890a6f04a33b77a377464cbf3367e40de6b046c7226517f48e23fd40e611198fcaa1f30503c36d99b20c" + "terracotta-0.4.2-windows-arm64.exe": "30a15c5c53e5817c5a3634532172559327474741d3b2c7ef4e8a30acc6f59cdcf3570bf5f583e3cbe9e2abc8253e977c1abda1e9f36c88c4e99240da257347d0" } }, "macos-x86_64": { - "hash": "c247ab9023cf47231f3a056ddf70fe823e504c19ce241bf264c0a3cf2981c044b236dc239f430afb2541445d1b61d18898f8af5e1c063f31fb952bdfbea4aff5", + "hash": "a762e4b2d6f84e899292b9e3856d009411a516d3c47f54575f843ce082f63dff2baa68ba0faa844b8b64fb12e91017386f15f5e7f975f8ee605bf8d4217cb091", "files": { - "terracotta-0.4.1-macos-x86_64": "dd2cde70a950849498425b8b00c17fb80edb8dd9bc0312da4885d97a28a4959d87782bd2202ef7f71bdbf2a92609089585c9e3faf24c10df6150e221e111a764", - "terracotta-0.4.1-macos-x86_64.pkg": "6057d5b4ea93da45796277a20ddaea7850e8c66326ded769f20fff74e132b453d6d615d937fc8f1d62b0b36b26961df5afb76535aecf85f25d2160889225ac6d" + "terracotta-0.4.2-macos-x86_64": "24efb85390eff88a538ed7e503fb1488e5e622730ca30c741a0e8b4c8f4e8d4868a2f9f38da8de540aeb535af2fd1e41c7081dae9c700e8a1a03b6c540218164", + "terracotta-0.4.2-macos-x86_64.pkg": "0f80437061231018ec0f860f875aba02cae9e0b36d21f2db8e99d25948806e1119f7844a4d4acca01d6124d7079718762cdebe3756b005a55632de7029fe0d24" } }, "macos-arm64": { - "hash": "0ddf48a44ea2563c37707caea8381ad3c69da73dd443d07dd98fe630f4e24ccda6f1fcdc9207a66bf61758b887a7b47186883ccd12bcb6b68387d8d756873f44", + "hash": "09e444fea2d9fd19f3e5cb62e29055228345be163924cbd408d947646fafed1012cf48508ee6a155ede3d571e2ffaa72d09ceeb1493c8a60feb05e0699f19ba3", "files": { - "terracotta-0.4.1-macos-arm64": "ddc335ee082b4c0323314d482933810fc2d377cfc131a9aa978b54f245f1bed8973139edf7cf94f7ae6c04660dfe18d275b1a81638781e482b4ff585f351eed9", - "terracotta-0.4.1-macos-arm64.pkg": "5cbb41a450f080234b66fa264351bd594e3f6ef1599c96f277baa308e34dd26caefa3a34b3d65e972bc20868f2d4a661e8b3910d4b0311a374c6ac680bdccf8f" + "terracotta-0.4.2-macos-arm64": "8e59a9d78acd57702dc044d6f2799c6af586b075a262f7d4dbbf0876e1af8d8271e04783c24ff820b801e3b14cd0190ab8403d097f3f2d98b6d911f95ed1e972", + "terracotta-0.4.2-macos-arm64.pkg": "b0b72f8883767c359f0700e958bd859dd5932c4674fbdf2a32f7de189fb9f54822d4825a754dc856416c8760338eb8ffca80b40c0a57c608fbe6ae9928888993" } }, "linux-x86_64": { - "hash": "c00b0622203c1610a8e72bde5230fca6fe56cf1f45dc9bc7e857350f3a050d442c814084758090799b9a5b875730fa666539ee75bec706c05d9338ea208301eb", + "hash": "d326ad95815d04568d485b5038e40ffc47ca54292fa0925eee6f5cea014024f901d661708aac2a743037b990882ad82b4d0b7bb03dc3b2fe720dbf0f3efe1c98", "files": { - "terracotta-0.4.1-linux-x86_64": "e53a9d8ec085ef7a7816b3da374e5c88fced2cf8319d447be3226422d01f9d7ee2019e403eafe21082135050652b2788b7d9520cc432c8d08931930b99595ed7" + "terracotta-0.4.2-linux-x86_64": "fac328ba8957a711b03557bb913940f22d61b76608cd203fdf51024b6f94b19f5bc91c9b8a9fa80baf6968e1e6873c1880fd4cf54a2f8e3c6cf1e6ac161f8d0c" } }, "linux-arm64": { - "hash": "4d661e1123ca050980960fe2d32b8d93a2c3ba807748569a45f16050fb1b52089bfc64c2dd64aba652dfed0e4ac3fba1ff9518cc3f95598c99fc71d93552b589", + "hash": "57c08f48d9535e93ad547d2dfc852d267992cc164a7208b42a2da0a6cbc2f21862f610e02a746b4b67150f4dec26b86a4f96eb9bd2f58d124d5b40ba50c6d55e", "files": { - "terracotta-0.4.1-linux-arm64": "d4ccf6ff8c2fac060fecaa3c8368677f9342e283f2435493b3e259e922ee0bb0b08f15a292bf91898800a462336c64d0dee7b73f03c1752e5b0988a05adb3b52" + "terracotta-0.4.2-linux-arm64": "d807744c2041c98686e4b505324713badea7a0f31e8810be49ae053a63fb6dfc474ac58d678fb93eea0dd5cccff7372d9ec6135a1046f4b306cad35cd90ecacd" } }, "linux-loongarch64": { - "hash": "8297bb37617e9e7ce282fc553c5b14c84a900bcff4d025be31fd4a4da8b3943d040afc6143aa17de9a88e5fa29af7254d38db8ae6418ee539c2301632448da09", + "hash": "a19f4ad4fa1fd2f3c1c867f8743178ab0524d1634d63e122ffc080623e4f14207ecab58830fa0d888696ef28ce4cfa189e6ffbe92c6e1aa1836652617a96a801", "files": { - "terracotta-0.4.1-linux-loongarch64": "ffdf7582d095307b91ddfc3e5d0daa78d434e4e34916d0cdf1520ae74b188fe5a48307047bf2da9a526eb725fe80cf230a93001bc8199d236b9cf28a1beaa6e9" + "terracotta-0.4.2-linux-loongarch64": "7d4896b275207def0861e544cf2afc9501c1946f1bc8bb2bad9ef4510dd915eabb63eeedff725e69565cdf5c27c146b52dd75d24115d5141909c0865f89cd7b9" } }, "linux-riscv64": { - "hash": "092f863885205107525e7ccb0e18977f6fd3018910ca5819772ec741dd8cffee52cc352a44b928bd2ba99ab881adaadff9e3bf4bf283f7384a35fea14becb0b4", + "hash": "ef3dfcf3c64dfcaf94adc518f0184d017a32a924f67ef33ffb8128f20669cf177cd345df3be8c00225a275bd44f70657b94fb43c60afeb21238000ac7a7f1721", "files": { - "terracotta-0.4.1-linux-riscv64": "a6e5d70ddc433bf804764b69e8c204a6a428ece22b9d8ab713ed339fb81bfa1d29daeb6bdfd62c85ff193396315f96172f4a28925e5a4efc45f7d6fa868782a9" + "terracotta-0.4.2-linux-riscv64": "afddc55f16f5cf3103548d13f7a3238964c8899dba8eb0e1fdc9e354da65e91fa4486de4b8680a7a3d9326eacf18c1ddb6b5b1f9459908f35f77b36f769172ea" } }, "freebsd-x86_64": { - "hash": "288bd7a97b2e2c5fb3c7d8107ed1311bc881fb74cf92da40b6c84156239d0832d2306b74b1e93a683188a5db896a6506326c6a7a4ac0ab2e798fa4f1f00787f0", + "hash": "a4d02ebbb1419f4e9265f5b24060432799151a27e0d1871690d2f3a51f40bcfd6d92c176001a15a62caa9bf31ea1b67a7b2da10e3351bfcdd36bfac1377d08d9", "files": { - "terracotta-0.4.1-freebsd-x86_64": "c90e7db3c5e5cc8d53d8d10a8bf88899e2c418758c14c3d14bc24efc32a711f76882bb5b5a49db5b0256ead4f22b666139bd2a212bf09036d7e7e64ddec4ec3c" + "terracotta-0.4.2-freebsd-x86_64": "a50da02752cef73d21804b26eb02261568ed3053c8fef1074014d50226843810f71e6eda39a53af3bad8e173cec40826631a4c8877af460d14a307f1b0a62519" } } }, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java index 830a852c3..689f67564 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java @@ -95,6 +95,27 @@ public final class NetworkUtils { return sb.toString(); } + public static String withQuery(String baseUrl, List> params) { + StringBuilder sb = new StringBuilder(baseUrl); + boolean first = true; + for (Pair param : params) { + if (param.getValue() == null) + continue; + if (first) { + if (!baseUrl.isEmpty()) { + sb.append('?'); + } + first = false; + } else { + sb.append(PARAMETER_SEPARATOR); + } + sb.append(encodeURL(param.getKey())); + sb.append(NAME_VALUE_SEPARATOR); + sb.append(encodeURL(param.getValue())); + } + return sb.toString(); + } + public static List withQuery(List list, Map params) { return list.stream().map(uri -> URI.create(withQuery(uri.toString(), params))).collect(Collectors.toList()); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2897211f6..22acb3170 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ pci-ids = "0.4.0" java-info = "1.0" authlib-injector = "1.2.7" monet-fx = "0.4.0" -terracotta = "0.4.1" +terracotta = "0.4.2" # testing junit = "6.0.1"