diff --git a/.gitignore b/.gitignore
index c6d98d1..5ff6309 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,98 +1,38 @@
-# ---> JetBrains
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-# ---> Maven
target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-# https://github.com/takari/maven-wrapper#usage-without-binary-jar
-.mvn/wrapper/maven-wrapper.jar
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
-# Eclipse m2e generated files
-# Eclipse Core
-.project
-# JDT-specific (Eclipse Java Development Tools)
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..8029398
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 9954939..ce7aa6b 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
-# FMCServer
+# FMCServer - ForeverMC 服务器插件
-ForeverMC 服务器状态报告插件
\ No newline at end of file
+适用版本:1.5.2
+
+详细说明请看主分支 README.md
diff --git a/lib/install.sh b/lib/install.sh
new file mode 100644
index 0000000..b36b37a
--- /dev/null
+++ b/lib/install.sh
@@ -0,0 +1,7 @@
+mvn install:install-file \
+ -Dfile=./mcpc.jar \
+ -DgroupId=local \
+ -DartifactId=fmc-mcpc \
+ -Dversion=1.5.2 \
+ -Dpackaging=jar \
+ -DgeneratePom=true
\ No newline at end of file
diff --git a/lib/mcpc.jar b/lib/mcpc.jar
new file mode 100644
index 0000000..cbf2f3b
Binary files /dev/null and b/lib/mcpc.jar differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..8f81333
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ cn.forevermc.server.bukkit
+ FMCServer
+ 0.0.1+1.5.2
+
+
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.7.1
+
+
+
+ cn.forevermc.server.mcpc.FMCServer
+
+
+
+ jar-with-dependencies
+
+
+
+
+ package
+
+ single
+
+
+
+
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+ local
+ fmc-mcpc
+ 1.5.2
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+
+
+
diff --git a/src/main/java/cn/forevermc/server/mcpc/FMCServer.java b/src/main/java/cn/forevermc/server/mcpc/FMCServer.java
new file mode 100644
index 0000000..60ccee1
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/FMCServer.java
@@ -0,0 +1,90 @@
+package cn.forevermc.server.mcpc;
+
+import cn.forevermc.server.mcpc.command.DebugCommand;
+import cn.forevermc.server.mcpc.command.StaffCommand;
+import cn.forevermc.server.mcpc.event.DebugEvent;
+import cn.forevermc.server.mcpc.service.ReportService;
+import cn.forevermc.server.mcpc.util.Util;
+import com.google.gson.Gson;
+import lombok.Getter;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * ForeverMC 服务器状态报告插件
+ *
+ * @author 夜雨
+ * @since 2022-11-14 11:05
+ */
+public class FMCServer extends JavaPlugin {
+
+ public static final Gson GSON = new Gson();
+
+ @Getter
+ private static FileConfiguration configuration;
+
+ private static Logger log;
+ private BukkitTask statusServiceTask;
+
+ @Override
+ public void onEnable() {
+ log = getLogger();
+ configuration = getConfig();
+ defConfig();
+
+ {
+ // config
+ log.info("debug: " + configuration.getBoolean("debug.enable"));
+ log.info("debug.staff: " + configuration.getStringList("debug.staff"));
+ log.info("report: " + configuration.getBoolean("report.enable"));
+ log.info("report.id: " + configuration.getString("report.id"));
+ }
+
+ getCommand("debug").setExecutor(new DebugCommand(this));
+ getCommand("staff").setExecutor(new StaffCommand(this));
+
+ getServer().getPluginManager().registerEvents(new DebugEvent(), this);
+
+ if (configuration.getBoolean("report.enable")) {
+ statusServiceTask = new ReportService(FMCServer.this).runTaskTimerAsynchronously(FMCServer.this, 0, 60);
+ }
+ }
+
+ @Override
+ public void onDisable() {
+ if (statusServiceTask != null) {
+ statusServiceTask.cancel();
+ }
+ }
+
+ @Override
+ public void reloadConfig() {
+ super.reloadConfig();
+ configuration = getConfig();
+ }
+
+ public static void log(Level level, String msg) {
+ log.log(level, msg);
+ }
+
+ /** 默认配置 */
+ private void defConfig() {
+ configuration.addDefault("debug.enable", false);
+ configuration.addDefault("debug.staff", new String[0]);
+ configuration.addDefault("report.enable", false);
+ configuration.addDefault("report.id", UUID.randomUUID().toString());
+ configuration.addDefault("report.api.info", "http://localhost:8091/fmc/server/info");
+ configuration.addDefault("report.api.status", "http://localhost:8091/fmc/server/status");
+ configuration.addDefault("report.token", "");
+ configuration.addDefault("report.name", "FMCServer#" + Util.randomString(8));
+ configuration.addDefault("report.clientURL", "");
+ configuration.addDefault("report.ip", "");
+ configuration.options().copyDefaults(true);
+ saveConfig();
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java b/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java
new file mode 100644
index 0000000..a4c90d5
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java
@@ -0,0 +1,94 @@
+package cn.forevermc.server.mcpc.bean;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 服务器状态
+ *
+ * @author 夜雨
+ * @since 2024-08-06 20:38
+ */
+@Data
+public class ServerStatusRequest {
+
+ private String id;
+
+ private ServerStatus status;
+
+ /**
+ * @author 夜雨
+ * @since 2021-12-02 19:56
+ */
+ @Data
+ public static class ServerStatus {
+
+ /** true 为维护中 */
+ private boolean isDebugging;
+
+ /** TPS */
+ private double tps;
+
+ /** 报告时间 */
+ private long reportAt;
+
+ /** JVM 状态 */
+ private JVM jvm = new JVM();
+
+ /** 在线列表 */
+ private List onlineList = new ArrayList<>();
+
+ /**
+ * 设置 TPS 取平均
+ *
+ * @param tpsList TPS 列表
+ */
+ public void setTps(double[] tpsList) {
+ double sum = 0;
+ for (int i = 0; i < tpsList.length; i++) {
+ sum += tpsList[i];
+ }
+ tps = sum / tpsList.length;
+ }
+
+ /**
+ * JVM 状态
+ *
+ * @author 夜雨
+ * @since 2022-11-11 14:52
+ */
+ @Data
+ public static class JVM {
+
+ /** CPU 已使用 */
+ private double cpuUsed;
+
+ /** 内存状态 */
+ private Memory memory = new Memory();
+
+ /**
+ * 内存状态
+ *
+ * @author 夜雨
+ * @since 2022-11-15 09:54
+ */
+ @Data
+ public static class Memory {
+
+ /** 已使用 */
+ private long used;
+
+ /** 已申请 */
+ private long committed;
+
+ /** 最大内存 */
+ private long max;
+
+ /** JVM 名称 */
+ private String name;
+ }
+ }
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java b/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java
new file mode 100644
index 0000000..265dc21
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java
@@ -0,0 +1,111 @@
+package cn.forevermc.server.mcpc.command;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import cn.forevermc.server.mcpc.util.Util;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * 维护模式
+ *
+ * @author 夜雨
+ * @since 2024-08-07 12:44
+ */
+public class DebugCommand implements CommandExecutor {
+
+ /**
+ * 指令动作
+ *
+ * @author 夜雨
+ * @since 2024-08-07 20:15
+ */
+ private enum Action {
+
+ /** 启用 */
+ TRUE,
+
+ /** 禁用 */
+ FALSE,
+
+ /** 当前状态 */
+ STATUS
+ }
+
+ private final JavaPlugin plugin;
+ private final List actionList;
+
+ public DebugCommand(JavaPlugin plugin) {
+ this.plugin = plugin;
+
+ // 提示
+ actionList = new ArrayList<>();
+ for (Action action : Action.values()) {
+ actionList.add(action.toString().toLowerCase());
+ }
+ plugin.getCommand("debug").setTabCompleter(new TabCompleter() {
+
+ @Override
+ public List onTabComplete(CommandSender commandSender, Command command, String s, String[] args) {
+ if (args.length == 1) {
+ return actionList;
+ }
+ return new ArrayList<>();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ FileConfiguration config = FMCServer.getConfiguration();
+
+ boolean isEnable = config.getBoolean("debug.enable");
+ Action action;
+ if (args.length == 1) {
+ // 含动作参数
+ action = Util.enumValueOf(Action.class, args[0]);
+ if (action == Action.TRUE) {
+ isEnable = true;
+ }
+ if (action == Action.FALSE) {
+ isEnable = false;
+ }
+ if (action == Action.STATUS) {
+ if (isEnable) {
+ Util.returnMsg(sender, Level.INFO, "当前维护模式:启用");
+ } else {
+ Util.returnMsg(sender, Level.INFO, "当前维护模式:禁用");
+ }
+ return true;
+ }
+ } else {
+ // 直接切换
+ isEnable = !isEnable;
+ }
+ config.set("debug.enable", isEnable);
+ if (isEnable) {
+ Player[] players = plugin.getServer().getOnlinePlayers();
+ List staffList = config.getStringList("debug.staff");
+ // 踢出非维护人员
+ for (Player player : players) {
+ if (!staffList.contains(player.getName())) {
+ player.kickPlayer("服务器正在维护,请稍后重试");
+ }
+ }
+ Util.returnMsg(sender, Level.INFO, "已启用维护模式");
+ } else {
+ Util.returnMsg(sender, Level.INFO, "已禁用维护模式");
+ }
+ plugin.saveConfig();
+ plugin.reloadConfig();
+ return true;
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java b/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java
new file mode 100644
index 0000000..64e27bd
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java
@@ -0,0 +1,104 @@
+package cn.forevermc.server.mcpc.command;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import cn.forevermc.server.mcpc.util.Util;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+
+/**
+ * 维护人员操作
+ *
+ * @author 夜雨
+ * @since 2024-08-07 12:44
+ */
+public class StaffCommand implements CommandExecutor {
+
+ /**
+ * 指令动作
+ *
+ * @author 夜雨
+ * @since 2024-08-07 14:29
+ */
+ private enum Action {
+
+ /** 添加 */
+ ADD,
+
+ /** 移除 */
+ REMOVE,
+
+ /** 列表 */
+ LIST
+ }
+
+ private final JavaPlugin plugin;
+ private final List actionList;
+
+ public StaffCommand(JavaPlugin plugin) {
+ this.plugin = plugin;
+
+ // 提示
+ actionList = new ArrayList<>();
+ for (Action action : Action.values()) {
+ actionList.add(action.toString().toLowerCase());
+ }
+ plugin.getCommand("staff").setTabCompleter(new TabCompleter() {
+
+ @Override
+ public List onTabComplete(CommandSender commandSender, Command command, String s, String[] args) {
+ if (args.length == 1) {
+ return actionList;
+ }
+ return new ArrayList<>();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ FileConfiguration config = FMCServer.getConfiguration();
+ Action action = Util.enumValueOf(Action.class, args[0]);
+ if (action == null) {
+ Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] ");
+ return true;
+ }
+ Set staffSet = new HashSet<>(config.getStringList("debug.staff"));
+ if (action == Action.LIST) {
+ Util.returnMsg(sender, Level.INFO, "维护人员列表: " + staffSet);
+ } else {
+ if (args.length < 2) {
+ Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] ");
+ return true;
+ }
+ String value = args[1];
+ if (value.isEmpty()) {
+ Util.returnMsg(sender, Level.WARNING, "请输入维护人员 ID");
+ return true;
+ }
+ switch (action) {
+ case ADD:
+ staffSet.add(value);
+ Util.returnMsg(sender, Level.INFO, String.format("已添加 %s 维护人员", value));
+ break;
+ case REMOVE:
+ staffSet.remove(value);
+ Util.returnMsg(sender, Level.INFO, String.format("已移除 %s 维护人员", value));
+ break;
+ }
+ config.set("debug.staff", staffSet.toArray(new String[0]));
+ plugin.saveConfig();
+ plugin.reloadConfig();
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java b/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java
new file mode 100644
index 0000000..fcf5005
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java
@@ -0,0 +1,30 @@
+package cn.forevermc.server.mcpc.event;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+
+import java.util.List;
+
+/**
+ * 调试事件
+ *
+ * @author 夜雨
+ * @since 2024-08-07 12:35
+ */
+public class DebugEvent implements Listener {
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ FileConfiguration config = FMCServer.getConfiguration();
+ if (!config.getBoolean("debug.enable")) {
+ return;
+ }
+ List staff = config.getStringList("debug.staff");
+ if (!staff.contains(event.getPlayer().getName())) {
+ event.getPlayer().kickPlayer("服务器正在维护,请稍后重试");
+ }
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java b/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java
new file mode 100644
index 0000000..d2d0281
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java
@@ -0,0 +1,159 @@
+package cn.forevermc.server.mcpc.service;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import cn.forevermc.server.mcpc.bean.ServerStatusRequest;
+import cn.forevermc.server.mcpc.util.HTTP;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.sun.management.OperatingSystemMXBean;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+/**
+ * 状态报告服务
+ *
+ * @author 夜雨
+ * @since 2022-11-11 14:38
+ */
+public class ReportService extends BukkitRunnable {
+
+ private final Server server = Bukkit.getServer();
+ private final ServerStatusRequest request = new ServerStatusRequest();
+ private final MemoryMXBean jvmMemory = ManagementFactory.getMemoryMXBean();
+ private final OperatingSystemMXBean operatingSystemMXBean;
+
+ private final String api;
+ private final JavaPlugin plugin;
+ private final double[] tpsList = new double[9];
+
+ /** 失败时间,用于失败时任务执行周期将会更长 */
+ private long failAt = -1;
+
+ public ReportService(JavaPlugin plugin) {
+ this.plugin = plugin;
+
+ api = FMCServer.getConfiguration().getString("report.api.status");
+ operatingSystemMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
+
+ if (api == null || api.isEmpty()) {
+ FMCServer.log(Level.WARNING, "start report task fail because of not found report.api.status in configuration");
+ return;
+ }
+ request.setId(plugin.getConfig().getString("report.id"));
+ request.setStatus(new ServerStatusRequest.ServerStatus());
+
+ // TPS 计算
+ server.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() {
+
+ private int ticks;
+ private int tpsI;
+ private long cs;
+ private double tps;
+
+ @Override
+ public void run() {
+ long s = System.currentTimeMillis() / 1000;
+ if (cs == s) {
+ ticks++;
+ } else {
+ cs = s;
+ tpsI++;
+ tpsList[tpsI % tpsList.length] = tps = tps == 0 ? ticks : ((tps + ticks) / 2);
+ ticks = 0;
+ }
+ }
+ }, 0, 1);
+ }
+
+ /** 报告基本信息 */
+ private void sendInfo() throws Exception {
+ FileConfiguration config = FMCServer.getConfiguration();
+ String api = config.getString("report.api.info");
+ if (api == null || api.isEmpty()) {
+ FMCServer.log(Level.WARNING, "start report task fail because of not found report.api.info in configuration");
+ return;
+ }
+ File iconFile = new File(new File(plugin.getDataFolder().getParent()).getParent(), "server-icon.png");
+ Server server = plugin.getServer();
+
+ Map params = new HashMap<>();
+ List postFileList = new ArrayList<>();
+
+ params.put("id", config.getString("report.id").trim());
+ params.put("name", config.getString("report.name").trim());
+ params.put("core", server.getVersion().trim());
+ params.put("bootAt", System.currentTimeMillis());
+ params.put("clientURL", config.getString("report.clientURL").trim());
+ params.put("maxOnline", server.getMaxPlayers());
+ params.put("ip", config.getString("report.ip").trim());
+ if (iconFile.exists()) {
+ HTTP.PostFile file = new HTTP.PostFile();
+ file.setField("icon");
+ file.setFileName("server-icon.png");
+ file.setBytes(Files.readAllBytes(iconFile.toPath()));
+ postFileList.add(file);
+ }
+ String resp = HTTP.post(api, params, postFileList);
+ JsonObject respObj = JsonParser.parseString(resp).getAsJsonObject();
+ Integer code = respObj.get("code").getAsInt();
+ if (!code.equals(20000)) {
+ FMCServer.log(Level.WARNING, "report server info error: " + respObj.get("msg").getAsString());
+ }
+ }
+
+ /** 报告状态 */
+ @Override
+ public void run() {
+ // 失败时延长周期,(15+-3) 秒
+ if (System.currentTimeMillis() - failAt < 1E3 * 15) {
+ return;
+ }
+ ServerStatusRequest.ServerStatus status = request.getStatus();
+ status.getOnlineList().clear();
+ Player[] players = server.getOnlinePlayers();
+ for (Player player : players) {
+ status.getOnlineList().add(player.getName());
+ }
+ status.setTps(tpsList);
+ status.setDebugging(FMCServer.getConfiguration().getBoolean("debug.enable"));
+ status.setReportAt(System.currentTimeMillis());
+ status.getJvm().setCpuUsed(operatingSystemMXBean.getProcessCpuLoad());
+ status.getJvm().getMemory().setUsed(jvmMemory.getHeapMemoryUsage().getUsed());
+ status.getJvm().getMemory().setCommitted(jvmMemory.getHeapMemoryUsage().getCommitted());
+
+ try {
+ String resp = HTTP.post(api, HTTP.ContentType.JSON, FMCServer.GSON.toJson(request));
+ if (resp == null || resp.isEmpty()) {
+ FMCServer.log(Level.WARNING, "report fail for empty result");
+ return;
+ }
+ JsonObject respObj = JsonParser.parseString(resp).getAsJsonObject();
+ Integer code = respObj.get("code").getAsInt();
+ // 数据中心通知需要发送基本信息,与接口约定返回代码为 20001,消息为 REQUIRED_BASE_INFO
+ if (code.equals(20001) && respObj.get("msg").getAsString().equals("REQUIRED_BASE_INFO")) {
+ FMCServer.log(Level.WARNING, "server required send base info");
+ sendInfo();
+ } else if (!code.equals(20000)) {
+ failAt = System.currentTimeMillis();
+ FMCServer.log(Level.SEVERE, "report fail: " + respObj.get("msg").getAsString());
+ }
+ } catch (Exception ignored) {
+ failAt = System.currentTimeMillis();
+ // 超时,连接失败的视为终端离线
+ }
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java b/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java
new file mode 100644
index 0000000..c27742f
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java
@@ -0,0 +1,252 @@
+package cn.forevermc.server.mcpc.util;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import lombok.Data;
+import lombok.Getter;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HTTP 操作
+ *
+ * @author 夜雨
+ * @since 2024-08-06 14:35
+ */
+public class HTTP {
+
+ /**
+ * 请求类型
+ *
+ * @author 夜雨
+ * @version 2022-01-02 12:16
+ */
+ @Getter
+ public enum ContentType {
+
+ /** 默认 URL 请求 */
+ DEFAULT(null),
+
+ /** FORM 表单模拟 */
+ FORM("application/x-www-form-urlencoded"),
+
+ /** JSON */
+ JSON("application/json; charset=utf-8"),
+
+ /** 数据流 */
+ STREAM("multipart/form-data; charset=utf-8; boundary=B7018kpqFPpgrWAKIR0lHNAanJEqJEyz");
+
+ final String value;
+
+ ContentType(String value) {
+ this.value = value;
+ }
+
+ }
+
+ /**
+ * 发送 POST 请求
+ *
+ * @param url 请求地址
+ * @param type 数据体类型
+ * @param data 数据
+ * @return 返回结果
+ * @throws Exception 请求异常
+ */
+ public static String post(String url, ContentType type, String data) throws Exception {
+ HttpURLConnection connection = getConnection(url, type);
+ connection.setRequestMethod("POST");
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+ connection.setUseCaches(false);
+ connection.setInstanceFollowRedirects(true);
+ if (data != null && !data.isEmpty()) {
+ BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream());
+ bos.write(data.getBytes(StandardCharsets.UTF_8));
+ bos.flush();
+ bos.close();
+ }
+
+ String result = "";
+ if (HttpURLConnection.HTTP_OK == connection.getResponseCode()) {
+ result = toString(connection.getInputStream());
+ }
+ connection.disconnect();
+ return result;
+ }
+
+ public static String post(String url, Map params, List postFileList) throws Exception {
+ final String newLine = "\r\n";
+ final String paramHeader = "Content-Disposition: form-data; name=\"%s\"";
+ final String fileHeader = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"";
+ final String split = ContentType.STREAM.getValue().split("boundary=")[1];
+
+ HttpURLConnection connection = getConnection(url, ContentType.STREAM);
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+ connection.setUseCaches(false);
+ connection.setRequestMethod("POST");
+ connection.setChunkedStreamingMode(4096);
+
+ DataOutputStream dos = new DataOutputStream(connection.getOutputStream());
+ {
+ // 参数
+ for (Map.Entry param : params.entrySet()) {
+ dos.writeBytes("--" + split);
+ dos.writeBytes(newLine);
+ // 参数协议
+ dos.writeBytes(String.format(paramHeader, param.getKey()));
+ dos.writeBytes(newLine);
+ dos.writeBytes("Content-Type: text/plain; charset=UTF-8");
+ dos.writeBytes(newLine);
+ dos.writeBytes("Content-Transfer-Encoding: 8bit");
+ dos.writeBytes(newLine);
+ dos.writeBytes(newLine);
+ Object value = param.getValue();
+ if (value != null) {
+ dos.write(value.toString().getBytes(StandardCharsets.UTF_8));
+ }
+ dos.writeBytes(newLine);
+ }
+ // 文件
+ for (PostFile postFile : postFileList) {
+ ByteArrayInputStream bais = new ByteArrayInputStream(postFile.getBytes());
+ dos.writeBytes("--" + split);
+ dos.writeBytes(newLine);
+ dos.writeBytes(String.format(fileHeader, postFile.getField(), URLEncoder.encode(postFile.getFileName(), "UTF-8")));
+ dos.writeBytes(newLine);
+ dos.writeBytes("Content-Type: application/octet-stream");
+ dos.writeBytes(newLine);
+ dos.writeBytes(newLine);
+
+ byte[] buffer = new byte[4096];
+ int l;
+ while ((l = bais.read(buffer)) != -1) {
+ dos.write(buffer, 0, l);
+ }
+ // 协议结尾
+ dos.writeBytes(newLine);
+ dos.writeBytes("--" + split + "--");
+ dos.writeBytes(newLine);
+ }
+ // 协议结尾
+ dos.writeBytes(newLine);
+ dos.writeBytes("--" + split + "--");
+ dos.writeBytes(newLine);
+ dos.flush();
+ }
+ // 返回
+ if (connection.getResponseCode() >= 300) {
+ throw new Exception("HTTP Request error, Response code: " + connection.getResponseCode());
+ }
+ String result = toString(connection.getInputStream());
+ connection.disconnect();
+ return result;
+ }
+
+ /**
+ * 获取连接
+ *
+ * @param url URL 地址
+ * @param contentType Content-ContentType 数据类型
+ * @return HTTP 连接
+ */
+ private static HttpURLConnection getConnection(String url, ContentType contentType) throws IOException, NoSuchAlgorithmException, KeyManagementException, URISyntaxException {
+ // 转 URL 对象
+ url = url.startsWith("http") ? url : "http://" + url;
+ URL connectURL = new URI(url).toURL();
+
+ HttpURLConnection connection = (HttpURLConnection) connectURL.openConnection();
+ // SSL 连接
+ if (url.trim().startsWith("https")) {
+ SSLContext sslcontext = SSLContext.getInstance("TLS");
+ sslcontext.init(null, new TrustManager[] {new X509TrustManager() {
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+ }}, null);
+ if (connection instanceof HttpsURLConnection) {
+ ((HttpsURLConnection) connection).setSSLSocketFactory(sslcontext.getSocketFactory());
+ }
+ }
+ // 数据类型
+ if (contentType != null && contentType.getValue() != null) {
+ connection.setRequestProperty("Content-Type", contentType.getValue());
+ }
+ connection.setRequestProperty("Token", FMCServer.getConfiguration().getString("report.token"));
+ // 超时
+ connection.setReadTimeout(3000);
+ connection.setConnectTimeout(3000);
+ return connection;
+ }
+
+ /**
+ * 读取数据流为字符串
+ *
+ * @param is 输入流
+ * @return 字符串
+ * @throws IOException IO 异常
+ */
+ private static String toString(InputStream is) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ BufferedInputStream bis = new BufferedInputStream(is);
+
+ InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8);
+
+ char[] buffer = new char[4096];
+ int l;
+ while ((l = isr.read(buffer)) != -1) {
+ sb.append(buffer, 0, l);
+ }
+ isr.close();
+ bis.close();
+ is.close();
+ return sb.toString();
+ }
+
+ /**
+ * 发送文件
+ *
+ * @author 夜雨
+ * @since 2024-08-07 23:50
+ */
+ @Data
+ public static class PostFile {
+
+ /** 字段名 */
+ private String field;
+
+ /** 文件名 */
+ private String fileName;
+
+ /** 字节数据 */
+ private byte[] bytes;
+ }
+}
diff --git a/src/main/java/cn/forevermc/server/mcpc/util/Util.java b/src/main/java/cn/forevermc/server/mcpc/util/Util.java
new file mode 100644
index 0000000..dba5824
--- /dev/null
+++ b/src/main/java/cn/forevermc/server/mcpc/util/Util.java
@@ -0,0 +1,75 @@
+package cn.forevermc.server.mcpc.util;
+
+import cn.forevermc.server.mcpc.FMCServer;
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.security.SecureRandom;
+import java.util.logging.Level;
+
+/**
+ * 工具
+ *
+ * @author 夜雨
+ * @since 2024-08-06 20:40
+ */
+public class Util {
+
+ /**
+ * 空安全的字符串转枚举
+ *
+ * @param enumType 枚举类
+ * @param name 枚举字段字符串
+ * @return 枚举
+ * @param 枚举类型
+ */
+ public static > T enumValueOf(Class enumType, String name) {
+ if (name == null) {
+ return null;
+ }
+ T[] ts = enumType.getEnumConstants();
+ if (ts == null) {
+ throw new IllegalArgumentException(enumType.getName() + " is not an enum type");
+ }
+ for (int i = 0; i < ts.length; i++) {
+ if (ts[i].name().equalsIgnoreCase(name)) {
+ return ts[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 输出指令结果,如果是控制台则输出控制台,如果是玩家则输出到玩家
+ *
+ * @param sender 发送者
+ * @param level 等级
+ * @param msg 消息
+ */
+ public static void returnMsg(CommandSender sender, Level level, String msg) {
+ if (sender instanceof Player) {
+ Player player = (Player) sender;
+ ChatColor color = level == Level.WARNING ? ChatColor.GREEN : ChatColor.RED;
+ player.sendMessage(color + msg);
+ } else {
+ FMCServer.log(level, msg);
+ }
+ }
+
+ /**
+ * 随机字符串
+ *
+ * @param length 长度
+ * @return 随机字符串
+ */
+ public static String randomString(int length) {
+ String pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ SecureRandom r = new SecureRandom();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ sb.append(pool.charAt(r.nextInt(pool.length() - 1)));
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..7897d1a
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,17 @@
+name: FMCServer
+main: cn.forevermc.server.mcpc.FMCServer
+version: '0.0.1'
+
+author: Timi
+website: imyeyu.net
+description: This plugin is ForeverMC Server status reporter
+
+commands:
+ debug:
+ description: 启用或禁用维护模式
+ usage: "/ [true|false|status]"
+ permission: op
+ staff:
+ description: 添加或移除维护人员
+ usage: "/ [add|remove|list] "
+ permission: op