From 6caa6b261f02d3742446b5e6ac3665f77b1efb69 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 8 Dec 2025 23:09:57 -0800 Subject: [PATCH] feat: submit --- lib/build.gradle.kts | 4 + .../java/online/mineroo/velocity/Config.java | 18 ++- .../mineroo/velocity/MinerooPlugin.java | 59 +++++--- .../velocity/commands/MainCommand.java | 135 +++++++++++++++++- .../velocity/listeners/BindListener.java | 36 +++++ .../mineroo/velocity/utils/BindRequest.java | 73 +++++++++- .../velocity/utils/MessageManager.java | 67 +++++++++ .../resources/mineroo/messages.properties | 11 ++ 8 files changed, 372 insertions(+), 31 deletions(-) create mode 100644 lib/src/main/java/online/mineroo/velocity/listeners/BindListener.java create mode 100644 lib/src/main/java/online/mineroo/velocity/utils/MessageManager.java create mode 100644 lib/src/main/resources/mineroo/messages.properties diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index f843b3e..6ac479a 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -8,6 +8,7 @@ plugins { `java-library` + id("com.gradleup.shadow") version "9.3.0" id("xyz.jpenilla.run-velocity") version "3.0.2" } @@ -38,6 +39,9 @@ java { } tasks { + shadowJar { + archiveFileName.set("mineroo-velocity.jar") + } runVelocity { // Configure the Velocity version for our task. // This is the only required configuration besides applying the plugin. diff --git a/lib/src/main/java/online/mineroo/velocity/Config.java b/lib/src/main/java/online/mineroo/velocity/Config.java index 3e20d6b..3a068d6 100644 --- a/lib/src/main/java/online/mineroo/velocity/Config.java +++ b/lib/src/main/java/online/mineroo/velocity/Config.java @@ -19,16 +19,24 @@ public class Config { @ConfigSerializable public static class ServerSection { - @Setting("hostname") - @Comment("Server Hostname") - private String hostname = ""; + @Setting("address") + @Comment("Server Address") + private String address = ""; + + @Setting("port") + @Comment("Server Port") + private Integer port = null; @Setting("token") @Comment("Server Bind Token") private String bindToken = "get token from `mineroo.online/resources/servers` page!"; - public String getHostname() { - return hostname; + public String getAddress() { + return address; + } + + public Integer getPort() { + return port; } public String getBindToken() { diff --git a/lib/src/main/java/online/mineroo/velocity/MinerooPlugin.java b/lib/src/main/java/online/mineroo/velocity/MinerooPlugin.java index 9e78119..8e06d5a 100644 --- a/lib/src/main/java/online/mineroo/velocity/MinerooPlugin.java +++ b/lib/src/main/java/online/mineroo/velocity/MinerooPlugin.java @@ -4,12 +4,13 @@ import com.google.inject.Inject; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; -import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import online.mineroo.velocity.commands.MainCommand; +import online.mineroo.velocity.listeners.BindListener; +import online.mineroo.velocity.utils.MessageManager; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +32,10 @@ public class MinerooPlugin { private final Path dataDirectory; private Config config; + private BindListener bindListener; + + private MessageManager messageManager; + @Inject public MinerooPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { this.server = server; @@ -40,50 +45,68 @@ public class MinerooPlugin { @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) { - loadConfig(); + + this.bindListener = new BindListener(); + server.getEventManager().register(this, bindListener); + + reloadConfig(); + + this.messageManager = new MessageManager(config); CommandManager commandManager = server.getCommandManager(); - commandManager.register(commandManager.metaBuilder("mineroo").build(), new MainCommand()); - } - - @Subscribe - public void onProxyPing(ProxyPingEvent event) { - String token = "1"; + commandManager.register(commandManager.metaBuilder("mineroo").build(), + new MainCommand(this)); } // load & create config - private void loadConfig() { + public boolean reloadConfig() { try { - // create dir if not exists if (Files.notExists(dataDirectory)) { Files.createDirectories(dataDirectory); } Path configPath = dataDirectory.resolve("config.yaml"); - // Inital Loader ConfigurationLoader loader = YamlConfigurationLoader.builder() .path(configPath) .nodeStyle(NodeStyle.BLOCK) .build(); - // Create & load config file if (Files.notExists(configPath)) { - logger.warn("Configuration file not found, creating default config.toml..."); - logger.warn("Please change `config.toml` before bind server."); - + logger.warn("Configuration file not found, creating default config.yaml..."); this.config = new Config(); CommentedConfigurationNode node = loader.createNode(); node.set(Config.class, this.config); loader.save(node); } else { - logger.info("Loading configuration..."); + logger.info("Reloading configuration..."); CommentedConfigurationNode node = loader.load(); this.config = node.get(Config.class); } - + return true; } catch (IOException e) { - logger.error("Failed to load or create config.yaml!", e); + logger.error("Failed to reload config!", e); + return false; } } + + public ProxyServer getServer() { + return this.server; + } + + public Logger getLogger() { + return logger; + } + + public Config getConfig() { + return config; + } + + public BindListener getBindListener() { + return bindListener; + } + + public MessageManager getMessageManager() { + return messageManager; + } } diff --git a/lib/src/main/java/online/mineroo/velocity/commands/MainCommand.java b/lib/src/main/java/online/mineroo/velocity/commands/MainCommand.java index d061361..44702a1 100644 --- a/lib/src/main/java/online/mineroo/velocity/commands/MainCommand.java +++ b/lib/src/main/java/online/mineroo/velocity/commands/MainCommand.java @@ -1,18 +1,151 @@ package online.mineroo.velocity.commands; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; + import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.SimpleCommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import online.mineroo.velocity.Config; +import online.mineroo.velocity.MinerooPlugin; +import online.mineroo.velocity.utils.BindRequest; +import online.mineroo.velocity.utils.MessageManager; public class MainCommand implements SimpleCommand { + + private final MinerooPlugin plugin; + + public MainCommand(MinerooPlugin plugin) { + this.plugin = plugin; + } + @Override public void execute(Invocation invocation) { String[] args = invocation.arguments(); - if (args.length == 0) { + if (args.length < 1) { sendHelp(invocation.source()); + return; } + + String subCommand = args[0]; + + if (subCommand.equals("bind")) { + bindServer(invocation.source()); + } + + if (subCommand.equalsIgnoreCase("reload")) { + reload(invocation.source()); + } + + } + + @Override + public List suggest(Invocation invocation) { + String[] args = invocation.arguments(); + + if (args.length == 0) { + return List.of("bind", "reload"); + } + + if (args.length == 1) { + String input = args[0].toLowerCase(); + return Stream.of("bind", "reload") + .filter(cmd -> cmd.startsWith(input)) + .collect(Collectors.toList()); + } + + return List.of(); + } + + public void reload(CommandSource source) { + MessageManager msg = plugin.getMessageManager(); + + long start = System.currentTimeMillis(); + boolean success = plugin.reloadConfig(); + + if (success) { + long time = System.currentTimeMillis() - start; + source.sendMessage(msg.get("command.reload.success", "time", String.valueOf(time))); + } else { + + source.sendMessage(msg.get("command.reload.failed")); + } + return; + } + + public void bindServer(CommandSource source) { + + Config config = plugin.getConfig(); + MessageManager msg = plugin.getMessageManager(); + + String bind_name = config.getServerName(); + String bind_description = config.getDescription(); + + String bind_token = config.getServer().getBindToken(); + String bind_address = config.getServer().getAddress(); + Integer bind_port = config.getServer().getPort(); + + source.sendMessage(msg.get("command.bind.server.start")); + + Logger logger = plugin.getLogger(); + + plugin.getServer().getScheduler().buildTask(plugin, () -> { + + BindRequest request = new BindRequest(logger, bind_token); + + try { + boolean inital_bind = request.initalMotdVerifyRequest(bind_address, bind_port); + if (inital_bind) { + String motdToken = request.getMotdToken(); + plugin.getBindListener().setVerificationToken(motdToken); + + source.sendMessage(msg.get("command.bind.server.wait")); + + Thread.sleep(2 * 60 * 1000 + 5000); + + // Final Bind + boolean success = false; + int maxRetries = 3; + + for (int i = 1; i <= maxRetries; i++) { + if (request.finalizeServerBinding(bind_name, bind_description)) { + success = true; + break; + } + + if (i < maxRetries) { + + source.sendMessage(msg.get("command.bind.server.retry", + "current", String.valueOf(i), + "max", String.valueOf(maxRetries))); + Thread.sleep(10000); + } + } + + if (success) { + source.sendMessage(msg.get("command.bind.server.success")); + } else { + source.sendMessage(msg.get("command.bind.server.failed")); + } + + } else { + source.sendMessage( + Component.text("Initial handshake failed. Check your token or network.", NamedTextColor.RED)); + } + + } catch (Exception e) { + logger.error("Mineroo Bind: " + e.toString()); + } finally { + plugin.getBindListener().clearVerificationToken(); + } + + }).schedule(); + } public void sendHelp(CommandSource source) { diff --git a/lib/src/main/java/online/mineroo/velocity/listeners/BindListener.java b/lib/src/main/java/online/mineroo/velocity/listeners/BindListener.java new file mode 100644 index 0000000..31b03eb --- /dev/null +++ b/lib/src/main/java/online/mineroo/velocity/listeners/BindListener.java @@ -0,0 +1,36 @@ +package online.mineroo.velocity.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import com.velocitypowered.api.proxy.server.ServerPing; +import net.kyori.adventure.text.Component; + +import java.util.concurrent.atomic.AtomicReference; + +public class BindListener { + + private final AtomicReference verificationToken = new AtomicReference<>(null); + + public void setVerificationToken(String token) { + this.verificationToken.set(token); + } + + public void clearVerificationToken() { + this.verificationToken.set(null); + } + + @Subscribe + public void onPing(ProxyPingEvent event) { + String token = verificationToken.get(); + + if (token == null) { + return; + } + + ServerPing.Builder builder = event.getPing().asBuilder(); + + builder.description(Component.text(token)); + + event.setPing(builder.build()); + } +} diff --git a/lib/src/main/java/online/mineroo/velocity/utils/BindRequest.java b/lib/src/main/java/online/mineroo/velocity/utils/BindRequest.java index d7aaba8..117d73e 100644 --- a/lib/src/main/java/online/mineroo/velocity/utils/BindRequest.java +++ b/lib/src/main/java/online/mineroo/velocity/utils/BindRequest.java @@ -8,9 +8,12 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Base64; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; public class BindRequest { @@ -19,23 +22,31 @@ public class BindRequest { private final Logger logger; private final HttpClient httpClient; - public BindRequest(Logger logger, HttpClient httpClient) { + private final String bindToken; + + private String motdToken = ""; + private String sessionToken = ""; + + public BindRequest(Logger logger, String bindToken) { + this.logger = logger; - this.httpClient = httpClient; + this.bindToken = bindToken; + + this.httpClient = HttpClient.newHttpClient(); } - public void InitalMotdVerifyRequest(String token, String hostname, int port) - throws IOException, InterruptedException { + public boolean initalMotdVerifyRequest(String hostname, Integer port) + throws IOException, InterruptedException, JsonParseException { // generate uri String uri = endpoint + "/server/motd-verify"; // build jsonBody JsonObject json = new JsonObject(); - json.addProperty("server_adress", hostname); + json.addProperty("server_address", hostname); json.addProperty("server_port", port); - String basicAuth = "mbind:" + token; + String basicAuth = "mbind:" + bindToken; String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8)); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) @@ -46,8 +57,56 @@ public class BindRequest { HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { - logger.debug("test"); + String body = response.body(); + JsonObject responseData = JsonParser.parseString(body).getAsJsonObject(); + + if (responseData.has("motd_token")) { + motdToken = responseData.get("motd_token").getAsString(); + } else { + logger.error("Mineroo Bind: MOTD Token not received."); + return false; + } + + if (responseData.has("session_token")) { + sessionToken = responseData.get("session_token").getAsString(); + } else { + logger.error("Mineroo Bind: Session Token not received."); + return false; + } + + return true; + + } else { + logger.error("Mineroo Bind: " + response.body().toString()); } + return false; + } + + public boolean finalizeServerBinding(@Nullable String name, @Nullable String description) + throws IOException, InterruptedException, JsonParseException { + String uri = endpoint + "/server/server-bind"; + + JsonObject json = new JsonObject(); + json.addProperty("session_token", sessionToken); + json.addProperty("name", name); + json.addProperty("description", description); + + String basicAuth = "mbind:" + bindToken; + String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8)); + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri)) + .header("Content-Type", "application/json") + .header("Authorization", "Basic " + encodedAuth) + .POST(HttpRequest.BodyPublishers.ofString(json.toString())).build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return response.statusCode() == 200; + + } + + public String getMotdToken() { + return motdToken; } } diff --git a/lib/src/main/java/online/mineroo/velocity/utils/MessageManager.java b/lib/src/main/java/online/mineroo/velocity/utils/MessageManager.java new file mode 100644 index 0000000..5556421 --- /dev/null +++ b/lib/src/main/java/online/mineroo/velocity/utils/MessageManager.java @@ -0,0 +1,67 @@ +package online.mineroo.velocity.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import online.mineroo.velocity.Config; + +import java.util.Locale; +import java.util.ResourceBundle; + +public class MessageManager { + + private final Config config; + private ResourceBundle bundle; + private final MiniMessage miniMessage; + + public MessageManager(Config config) { + this.config = config; + this.miniMessage = MiniMessage.miniMessage(); + reload(); + } + + public void reload() { + String lang = "zh-CN"; + + Locale locale; + if (lang.contains("-")) { + String[] parts = lang.split("-"); + locale = Locale.of(parts[0], parts[1]); + } else { + locale = Locale.of(lang); + } + + try { + this.bundle = ResourceBundle.getBundle("mineroo.messages", locale); + } catch (Exception e) { + this.bundle = ResourceBundle.getBundle("mineroo.messages", Locale.ROOT); + } + } + + public Component get(String key) { + String raw = getString(key); + return miniMessage.deserialize(raw); + } + + public Component get(String key, String... placeholders) { + String raw = getString(key); + + TagResolver.Builder builder = TagResolver.builder(); + for (int i = 0; i < placeholders.length; i += 2) { + if (i + 1 < placeholders.length) { + builder.resolver(Placeholder.parsed(placeholders[i], placeholders[i + 1])); + } + } + + return miniMessage.deserialize(raw, builder.build()); + } + + private String getString(String key) { + try { + return bundle.getString(key); + } catch (Exception e) { + return "Missing key: " + key; + } + } +} diff --git a/lib/src/main/resources/mineroo/messages.properties b/lib/src/main/resources/mineroo/messages.properties new file mode 100644 index 0000000..9324560 --- /dev/null +++ b/lib/src/main/resources/mineroo/messages.properties @@ -0,0 +1,11 @@ +# Command Bind +command.bind.server.start=开始服务器绑定流程,请稍等... +command.bind.server.inital.failed=初次握手失败。检查您的令牌或网络。 +command.bind.server.wait=正在验证服务器所有权... 数据同步可能需要 2 分钟,请稍候。 +command.bind.server.success=绑定成功! +command.bind.server.failed=绑定失败。 +command.bind.server.retry=验证未通过,10 秒后重试 (/) ... + +# Command Reload +command.reload.success=配置文件重载成功!耗时 +command.reload.failed=配置文件重载失败,请查看控制台日志。