diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b16fd78 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: "Build" +on: + workflow_dispatch: ~ + push: + paths: + - "**.java" + branches: + - "main" + pull_request: + paths: + - "**.java" + types: + - opened + - synchronize + - reopened +jobs: + Build-Jar: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '17' + - name: Cache gradle files + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ./.gradle/loom-cache + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Download latest grasscutter jar + run: wget https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip && mkdir lib && unzip Grasscutter.zip -d lib + + - name: Change permission + run: chmod +x gradlew + + - name: Run Gradle + run: ./gradlew jar + + - name: Upload build + uses: actions/upload-artifact@v3 + with: + name: opencommand + path: opencommand*.jar \ No newline at end of file diff --git a/.gitignore b/.gitignore index aecefae..e2cd7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ hs_err_pid* /lib/ +.gradle +.idea +/build \ No newline at end of file diff --git a/README.md b/README.md index 6337adf..aa6cbac 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ 一个为第三方客户端开放GC命令执行接口的插件 ## 服务端安装 + 1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar` 2. 放入 `plugins` 文件夹即可 + > 注意,如果出现以下错误: > ```log > INFO:PluginManager Enabling plugin: opencommand-plugin @@ -17,24 +19,28 @@ > 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版! ## 控制台连接 + 1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json` 2. 设置 `consoleToken` 的值为你的连接秘钥,建议使用至少32字符的长随机字符串。 3. 重新启动服务端即可生效配置 4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令 ## 构建说明 + 1. 克隆仓库 2. 在目录下新建 `lib` 目录 3. 将 `grasscutter-1.1.x-dev.jar` 放入 `lib` 目录 4. `gradle build` ## 玩家使用流程 + 1. 在客户端中填写服务地址,确认是否支持 2. 填写UID,发送验证码 3. 将游戏内收到的**4位整数验证码**填入客户端校验 4. 享受便利! ## 客户端请求流程 + 1. `ping` 确认是否支持 `opencommand` 插件 2. `sendCode` 向指定玩家发送验证码(1分钟内不允许重发),保存返回的 `token` 3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验 @@ -43,6 +49,7 @@ --- ## `config.json` + ```json { // 控制台连接令牌 @@ -52,56 +59,216 @@ // 临时令牌过期时间(秒) "tempTokenExpirationTime_S": 300, // 授权令牌最后使用过期时间(小时) - "tokenLastUseExpirationTime_H": 48 + "tokenLastUseExpirationTime_H": 48, + // 多服务器通信端口 + "socketPort": 5746, + // 多服务器通信密钥 + "socketToken": "", + // 多服务器Dispatch服务器地址 + "socketHost": "127.0.0.1", + // 多服务器显示名称 + "socketDisplayName": "" } ``` - ## API `/opencommand/api` + 示例 + ``` https://127.0.0.1/opencommand/api ``` ### Request 请求 + ```java public final class JsonRequest { public String token = ""; public String action = ""; + public String server = ""; public Object data = null; } ``` ### Response 响应 + ```java public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; } ``` ### Actions 动作 -#### `ping` -data = null -#### `sendCode` -##### Request -data = uid (int) -##### Response -data = token (string) +#### `测试连接` -#### `verify` 要求 `token` ##### Request -data = code (int) -##### Response -###### Success: -code = 200 -###### Verification failed: -code = 400 -#### `command` 要求 `token` -##### Request -data = command (string) +| 请求参数 | 请求数据 | 类型 | +|--------|--------|----------| +| action | `ping` | `String` | + ##### Response -data = message (string) + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `null` | `null` | + + +#### `获取在线玩家` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `online` | `String` | + +##### Response + +| 返回参数 | 返回数据 | 类型 | +|---------|---------------------------------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{"count": 0, playerList": []}` | `JsonObject` | + +#### `发送验证码` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|------------|----------| +| action | `sendCode` | `String` | +| data | `uid` | `Int` | + +##### Response + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `token` | `String` | + +#### `验证验证码` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `verify` | `String` | +| token | `token` | `String` | +| data | `code` | `Int` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `null` | `null` | + +失败 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------------------|----------| +| retcode | `400` | `Int` | +| message | `Verification failed` | `String` | +| data | `null` | `null` | + +#### `执行命令` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| data | `command` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | + +### 执行控制台命令 + +#### `获取运行模式` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `runmode` | `String` | +| token | `token` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `1 (多服务器) / 0 (单服务器)` | `Int` | + +#### `获取多服务器列表` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `server` | `String` | +| token | `token` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{}` | `JsonObject` | + +```json +{ + "retcode": 200, + "message": "success", + "data": { + // 服务器 UUID + "13d82d0d-c7d9-47dd-830c-76588006ef6e": "2.8.0 服务器", + "e6b83224-a761-4023-be57-e054c5bb823a": "2.8.0 开发服务器" + } +} +``` + +#### `执行命令` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| server | `UUID` | `String` | +| data | `command` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | \ No newline at end of file diff --git a/README_en-US.md b/README_en-US.md index 6997b53..afda1ab 100644 --- a/README_en-US.md +++ b/README_en-US.md @@ -41,7 +41,10 @@ A plugin that opens the GC command execution interface for third-party clients "consoleToken": "", "codeExpirationTime_S": 60, "tempTokenExpirationTime_S": 300, - "tokenLastUseExpirationTime_H": 48 + "tokenLastUseExpirationTime_H": 48, + "socketPort": 5746, + "socketToken": "", + "socketHost": "127.0.0.1" } ``` @@ -57,6 +60,7 @@ https://127.0.0.1/opencommand/api public final class JsonRequest { public String token = ""; public String action = ""; + public Seting server = ""; public Object data = null; } ``` @@ -65,32 +69,178 @@ public final class JsonRequest { ```java public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; } ``` -## Actions -### `ping` -data = null +### Actions +#### `Test connect` -### `sendCode` -#### Request -data = uid (int) -#### Response -data = token (string) +##### Request -### `verify`: Requires `token` -#### Request -data = code (int) -#### Response -##### Success: -code = 200 -##### Verification failed: -code = 400 +| Request | Request data | type | +|---------|--------------|----------| +| action | `ping` | `String` | -### `command`: Requires `token` -#### Request -data = command (string) -#### Response -data = message (string) +##### Response + +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `null` | `null` | + +#### `Get online players` + +##### Request + +| Request | Request data | type | +|---------|--------------|----------| +| action | `online` | `String` | + +##### Response + +| Response | Response data | type | +|----------|---------------------------------|--------------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `{"count": 0, playerList": []}` | `JsonObject` | + +#### `Send code` + +##### Request + +| Request | Request data | type | +|---------|--------------|----------| +| action | `sendCode` | `String` | +| data | `uid` | `Int` | + +##### Response + +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `token` | `String` | + +#### `Verify code` + +##### Request + +| Request | Request data | type | +|---------|--------------|----------| +| action | `verify` | `String` | +| token | `token` | `String` | +| data | `code` | `Int` | + +##### Response + +Success + +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `null` | `null` | + +Failed + +| Response | Response data | type | +|----------|-----------------------|----------| +| retcode | `400` | `String` | +| message | `Verification failed` | `String` | +| data | `null` | `null` | + +#### `Run command` + +##### Request + +| Request | Request data | type | +|---------|--------------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| data | `command` | `String` | + +##### Response + +Success + +| Response | Response data | type | +|----------|------------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `Command return` | `String` | + +### Run console command + +#### `Get run mode` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `runmode` | `String` | +| token | `token` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|----------------------------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `1 (Multi server) / 0 (Single server)` | `Int` | + +#### `Get mulit server list` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `server` | `String` | +| token | `token` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|---------------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{}` | `JsonObject` | + +```json +{ + "retcode": 200, + "message": "success", + "data": { + // Server UUID + "13d82d0d-c7d9-47dd-830c-76588006ef6e": "2.8.0 Server", + "e6b83224-a761-4023-be57-e054c5bb823a": "2.8.0 Dev server" + } +} +``` + +#### `Run command` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| server | `UUID` | `String` | +| data | `command` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/EventListeners.java b/src/main/java/com/github/jie65535/opencommand/EventListeners.java index 7c0bc6e..df27331 100644 --- a/src/main/java/com/github/jie65535/opencommand/EventListeners.java +++ b/src/main/java/com/github/jie65535/opencommand/EventListeners.java @@ -1,17 +1,57 @@ package com.github.jie65535.opencommand; +import com.github.jie65535.opencommand.socket.SocketClient; +import com.github.jie65535.opencommand.socket.SocketUtils; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.utils.MessageHandler; +import java.util.ArrayList; + public final class EventListeners { private static MessageHandler consoleMessageHandler; + public static void setConsoleMessageHandler(MessageHandler handler) { consoleMessageHandler = handler; } + public static void onCommandResponse(ReceiveCommandFeedbackEvent event) { if (consoleMessageHandler != null && event.getPlayer() == null) { consoleMessageHandler.setMessage(event.getMessage()); } } + + public static void onPlayerJoin(PlayerJoinEvent playerJoinEvent) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + playerNames.add(playerJoinEvent.getPlayer().getNickname()); + playerList.playerMap.put(playerJoinEvent.getPlayer().getUid(), playerJoinEvent.getPlayer().getNickname()); + for (Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerList = playerNames; + + SocketClient.sendPacket(playerList); + } + + public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + for (Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerMap.remove(playerQuitEvent.getPlayer().getUid()); + playerNames.remove(playerQuitEvent.getPlayer().getNickname()); + playerList.playerList = playerNames; + SocketClient.sendPacket(playerList); + } } diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java index 5be40f4..766bb5d 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java @@ -22,4 +22,8 @@ public class OpenCommandConfig { public int codeExpirationTime_S = 60; public int tempTokenExpirationTime_S = 300; public int tokenLastUseExpirationTime_H = 48; + public int socketPort = 5746; + public String socketToken = ""; + public String socketHost = "127.0.0.1"; + public String socketDisplayName = ""; } diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java index 13677b3..5b2a92a 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java @@ -19,6 +19,7 @@ package com.github.jie65535.opencommand; import com.github.jie65535.opencommand.json.JsonRequest; import com.github.jie65535.opencommand.json.JsonResponse; +import com.github.jie65535.opencommand.socket.SocketData; import emu.grasscutter.command.CommandMap; import emu.grasscutter.server.http.Router; import emu.grasscutter.utils.Crypto; @@ -31,6 +32,7 @@ import io.javalin.Javalin; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -85,6 +87,11 @@ public final class OpenCommandHandler implements Router { } else if (req.action.equals("ping")) { response.json(new JsonResponse()); return; + } else if (req.action.equals("online")) { + var p = new ArrayList(); + plugin.getServer().getPlayers().forEach((uid, player) -> p.add(player.getNickname())); + response.json(new JsonResponse(200, "Success", new SocketData.OnlinePlayer(p))); + return; } // token is required @@ -118,6 +125,9 @@ public final class OpenCommandHandler implements Router { } } return; + } else if (req.action.equals("runmode")) { + response.json(new JsonResponse(200, "Success", 0)); + return; } } else if (codes.containsKey(req.token)) { if (req.action.equals("verify")) { diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java new file mode 100644 index 0000000..db21c84 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java @@ -0,0 +1,224 @@ +/* + * gc-opencommand + * Copyright (C) 2022 jie65535 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.github.jie65535.opencommand; + +import com.github.jie65535.opencommand.json.JsonRequest; +import com.github.jie65535.opencommand.json.JsonResponse; +import com.github.jie65535.opencommand.socket.SocketData; +import com.github.jie65535.opencommand.socket.SocketDataWait; +import com.github.jie65535.opencommand.socket.SocketServer; +import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.RunConsoleCommand; +import com.github.jie65535.opencommand.socket.packet.player.Player; +import com.github.jie65535.opencommand.socket.packet.player.PlayerEnum; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.MessageHandler; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.FutureTask; + +public final class OpenCommandOnlyHttpHandler implements Router { + + @Override + public void applyRoutes(Express express, Javalin javalin) { + express.post("/opencommand/api", OpenCommandOnlyHttpHandler::handle); + } + + private static final Map clients = new HashMap<>(); + private static final Map tokenExpireTime = new HashMap<>(); + private static final Map codes = new HashMap<>(); + private static final Int2ObjectMap codeExpireTime = new Int2ObjectOpenHashMap<>(); + + public static void handle(Request request, Response response) { + // Trigger cleanup action + cleanupExpiredData(); + var plugin = OpenCommandPlugin.getInstance(); + var config = plugin.getConfig(); + var now = new Date(); + + var req = request.body(JsonRequest.class); + response.type("application/json"); + if (req.action.equals("sendCode")) { + int playerId = (int) req.data; + var player = SocketData.getPlayer(playerId); + if (player == null) { + response.json(new JsonResponse(404, "Player Not Found.")); + } else { + if (codeExpireTime.containsKey(playerId)) { + var expireTime = codeExpireTime.get(playerId); + if (now.before(expireTime)) { + response.json(new JsonResponse(403, "Requests are too frequent")); + return; + } + } + + String token = req.token; + if (token == null || token.isEmpty()) + token = Utils.bytesToHex(Crypto.createSessionKey(32)); + int code = Utils.randomRange(1000, 9999); + codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L)); + tokenExpireTime.put(token, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)); + codes.put(token, code); + clients.put(token, playerId); + Player.dropMessage(playerId, "[Open Command] Verification code: " + code); + response.json(new JsonResponse(token)); + } + return; + } else if (req.action.equals("ping")) { + response.json(new JsonResponse()); + return; + } else if (req.action.equals("online")) { + response.json(new JsonResponse(200, "Success", SocketData.getOnlinePlayer())); + return; + } + + // token is required + if (req.token == null || req.token.isEmpty()) { + response.json(new JsonResponse(401, "Unauthorized")); + return; + } + var isConsole = req.token.equals(config.consoleToken); + if (!isConsole && !clients.containsKey(req.token)) { + response.json(new JsonResponse(401, "Unauthorized")); + return; + } + + if (isConsole) { + if (req.action.equals("verify")) { + response.json(new JsonResponse()); + return; + } else if (req.action.equals("command")) { + var server = SocketServer.getClientInfoByUuid(req.server); + if (server == null) { + response.json(new JsonResponse(404, "Server Not Found.")); + return; + } + plugin.getLogger().info(String.format("IP: %s run command in console > %s", request.ip(), req.data)); + var wait = new SocketDataWait(2000L) { + @Override + public void run() { + } + + @Override + public HttpPacket initData(HttpPacket data) { + return data; + } + + @Override + public void timeout() { + } + }; + + SocketServer.sendPacketAndWait(server.ip, new RunConsoleCommand(req.data.toString()), wait); + var data = wait.getData(); + if (data == null) { + response.json(new JsonResponse(408, "Timeout")); + return; + } + response.json(new JsonResponse(data.code, data.message, data.data)); + return; + } else if (req.action.equals("server")) { + response.json(new JsonResponse(200, "Success", SocketServer.getOnlineClient())); + return; + } else if (req.action.equals("runmode")) { + response.json(new JsonResponse(200, "Success", 1)); + return; + } + } else if (codes.containsKey(req.token)) { + if (req.action.equals("verify")) { + if (codes.get(req.token).equals(req.data)) { + codes.remove(req.token); + // update token expire time + tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L)); + response.json(new JsonResponse()); + plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), request.ip())); + } else { + response.json(new JsonResponse(400, "Verification failed")); + } + return; + } + } else { + if (req.action.equals("command")) { + SocketDataWait socketDataWait = new SocketDataWait<>(1000L * 10L) { + @Override + public void run() { + } + + @Override + public HttpPacket initData(HttpPacket data) { + return data; + } + + @Override + public void timeout() { + } + }; + + // update token expire time + tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L)); + var playerId = clients.get(req.token); + var command = req.data.toString(); + var player = new Player(); + player.uid = playerId; + player.type = PlayerEnum.RunCommand; + player.data = command; + + if (!SocketServer.sendUidPacket(playerId, player, socketDataWait)) { + response.json(new JsonResponse(404, "Player Not Found.")); + return; + } + + + HttpPacket httpPacket = socketDataWait.getData(); + if (httpPacket == null) { + response.json(new JsonResponse(500, "error", "Wait timeout")); + return; + } + response.json(new JsonResponse(httpPacket.code, httpPacket.message)); + return; + } + } + response.json(new JsonResponse(403, "forbidden")); + } + + private static void cleanupExpiredData() { + var now = new Date(); + codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now)); + + var it = tokenExpireTime.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.getValue().before(now)) { + it.remove(); + // remove expired token + clients.remove(entry.getKey()); + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java index 39af95e..6337e37 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java @@ -17,11 +17,15 @@ */ package com.github.jie65535.opencommand; +import com.github.jie65535.opencommand.socket.SocketClient; +import com.github.jie65535.opencommand.socket.SocketServer; import emu.grasscutter.Grasscutter; import emu.grasscutter.plugin.Plugin; import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; import java.io.File; import java.io.FileReader; @@ -31,7 +35,10 @@ import java.io.IOException; public final class OpenCommandPlugin extends Plugin { private static OpenCommandPlugin instance; - public static OpenCommandPlugin getInstance() { return instance; } + + public static OpenCommandPlugin getInstance() { + return instance; + } private OpenCommandConfig config; @@ -47,7 +54,21 @@ public final class OpenCommandPlugin extends Plugin { .priority(HandlerPriority.HIGH) .listener(EventListeners::onCommandResponse) .register(this); - getHandle().addRouter(OpenCommandHandler.class); + if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { + // 仅运行游戏服务器时注册玩家加入和离开事件 + new EventHandler<>(PlayerJoinEvent.class) + .priority(HandlerPriority.HIGH) + .listener(EventListeners::onPlayerJoin) + .register(this); + new EventHandler<>(PlayerQuitEvent.class) + .priority(HandlerPriority.HIGH) + .listener(EventListeners::onPlayerQuit) + .register(this); + } else if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { + getHandle().addRouter(OpenCommandOnlyHttpHandler.class); + } else { + getHandle().addRouter(OpenCommandHandler.class); + } getLogger().info("[OpenCommand] Enabled"); } @@ -64,19 +85,35 @@ public final class OpenCommandPlugin extends Plugin { var configFile = new File(getDataFolder(), "config.json"); if (!configFile.exists()) { config = new OpenCommandConfig(); - try (var file = new FileWriter(configFile)){ + try (var file = new FileWriter(configFile)) { file.write(Grasscutter.getGsonFactory().toJson(config)); } catch (IOException e) { - getLogger().error("Unable to write to config file."); + getLogger().error("[OpenCommand] Unable to write to config file."); } catch (Exception e) { - getLogger().error("Unable to save config file."); + getLogger().error("[OpenCommand] Unable to save config file."); } } else { try (var file = new FileReader(configFile)) { config = Grasscutter.getGsonFactory().fromJson(file, OpenCommandConfig.class); } catch (Exception exception) { config = new OpenCommandConfig(); - getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); + getLogger().error("[OpenCommand] There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); + } + } + // 启动Socket + startSocket(); + } + + private void startSocket() { + if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { + getLogger().info("[OpenCommand] Starting socket client..."); + SocketClient.connectServer(); + } else if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { + getLogger().info("[OpenCommand] Starting socket server..."); + try { + SocketServer.startServer(); + } catch (IOException e) { + getLogger().error("[OpenCommand] Unable to start socket server.", e); } } } diff --git a/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java b/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java index e88e426..ef8dd9f 100644 --- a/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java +++ b/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java @@ -20,5 +20,6 @@ package com.github.jie65535.opencommand.json; public final class JsonRequest { public String token = ""; public String action = ""; + public String server = ""; public Object data = null; } \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java b/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java index 5a37837..1163142 100644 --- a/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java +++ b/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java @@ -19,7 +19,7 @@ package com.github.jie65535.opencommand.json; public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; public JsonResponse() { diff --git a/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java b/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java new file mode 100644 index 0000000..df004f7 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java @@ -0,0 +1,14 @@ +package com.github.jie65535.opencommand.socket; + +public class ClientInfo { + + public final String uuid; + public final SocketServer.ClientThread clientThread; + public final String ip; + + public ClientInfo(String uuid, String ip, SocketServer.ClientThread clientThread) { + this.uuid = uuid; + this.clientThread = clientThread; + this.ip = ip; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java new file mode 100644 index 0000000..92245e5 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -0,0 +1,246 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.EventListeners; +import com.github.jie65535.opencommand.OpenCommandConfig; +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.*; +import com.github.jie65535.opencommand.socket.packet.player.Player; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.utils.MessageHandler; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; + +// Socket 客户端 +public class SocketClient { + public static ClientThread clientThread; + + public static Logger mLogger; + + public static Timer timer; + + public static boolean connect = false; + + public static ReceiveThread receiveThread; + + // 连接服务器 + public static void connectServer() { + if (connect) return; + if (clientThread != null) { + mLogger.warn("[OpenCommand] Retry connecting to the server after 15 seconds"); + try { + Thread.sleep(15000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + OpenCommandConfig config = OpenCommandPlugin.getInstance().getConfig(); + mLogger = OpenCommandPlugin.getInstance().getLogger(); + clientThread = new ClientThread(config.socketHost, config.socketPort); + + if (timer != null) { + timer.cancel(); + } + timer = new Timer(); + timer.schedule(new SendHeartBeatPacket(), 500); + timer.schedule(new SendPlayerListPacket(), 1000); + } + + // 发送数据包 + public static boolean sendPacket(BasePacket packet) { + var p = SocketUtils.getPacket(packet); + if (!clientThread.sendPacket(p)) { + mLogger.warn("[OpenCommand] Send packet to server failed"); + mLogger.info("[OpenCommand] Reconnect to server"); + connect = false; + connectServer(); + return false; + } + return true; + } + + // 发送数据包带数据包ID + public static boolean sendPacket(BasePacket packet, String packetID) { + if (!clientThread.sendPacket(SocketUtils.getPacketAndPackID(packet, packetID))) { + mLogger.warn("[OpenCommand] Send packet to server failed"); + mLogger.info("[OpenCommand] Reconnect to server"); + connect = false; + connectServer(); + return false; + } + return true; + } + + // 心跳包发送 + private static class SendHeartBeatPacket extends TimerTask { + @Override + public void run() { + if (connect) { + sendPacket(new HeartBeat("Pong")); + } + } + } + + private static class SendPlayerListPacket extends TimerTask { + @Override + public void run() { + if (connect) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + for (emu.grasscutter.game.player.Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerList = playerNames; + sendPacket(playerList); + } + } + } + + // 数据包接收 + private static class ReceiveThread extends Thread { + private InputStream is; + private boolean exit = false; + + public ReceiveThread(Socket socket) { + try { + is = socket.getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + @Override + public void run() { + //noinspection InfiniteLoopStatement + while (true) { + try { + if (exit) { + return; + } + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + switch (packet.type) { + // 玩家类 + case Player: + var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); + switch (player.type) { + // 运行命令 + case RunCommand -> { + var command = player.data; + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); + return; + } + // Player MessageHandler do not support concurrency + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (playerData) { + try { + var resultCollector = new MessageHandler(); + playerData.setMessageHandler(resultCollector); + CommandMap.getInstance().invoke(playerData, playerData, command); + sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + OpenCommandPlugin.getInstance().getLogger().warn("[OpenCommand] Run command failed.", e); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + playerData.setMessageHandler(null); + } + } + } + // 发送信息 + case DropMessage -> { + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + return; + } + playerData.dropMessage(player.data); + } + } + break; + case RunConsoleCommand: + var consoleCommand = Grasscutter.getGsonFactory().fromJson(packet.data, RunConsoleCommand.class); + var plugin = OpenCommandPlugin.getInstance(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (plugin) { + try { + var resultCollector = new MessageHandler(); + EventListeners.setConsoleMessageHandler(resultCollector); + CommandMap.getInstance().invoke(null, null, consoleCommand.command); + sendPacket(new HttpPacket(resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + mLogger.warn("[OpenCommand] Run command failed.", e); + EventListeners.setConsoleMessageHandler(null); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + EventListeners.setConsoleMessageHandler(null); + } + } + } + } catch (Throwable e) { + e.printStackTrace(); + if (!sendPacket(new HeartBeat("Pong"))) { + return; + } + } + } + } + + public void exit() { + exit = true; + } + } + + // 客户端连接线程 + private static class ClientThread extends Thread { + private final String ip; + private final int port; + private Socket socket; + private OutputStream os; + + public ClientThread(String ip, int port) { + this.ip = ip; + this.port = port; + start(); + } + + public Socket getSocket() { + return socket; + } + + public boolean sendPacket(String string) { + return SocketUtils.writeString(os, string); + } + + @Override + public void run() { + try { + connect = true; + if (receiveThread != null) { + receiveThread.exit(); + } + + socket = new Socket(ip, port); + os = socket.getOutputStream(); + mLogger.info("[OpenCommand] Connect to server: " + ip + ":" + port); + SocketClient.sendPacket(new AuthPacket(OpenCommandPlugin.getInstance().getConfig().socketToken, OpenCommandPlugin.getInstance().getConfig().socketDisplayName)); + receiveThread = new ReceiveThread(socket); + } catch (IOException e) { + connect = false; + mLogger.warn("[OpenCommand] Connect to server failed: " + ip + ":" + port); + connectServer(); + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java new file mode 100644 index 0000000..b05ba6b --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java @@ -0,0 +1,48 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +// Socket 数据保存 +public class SocketData { + public static HashMap playerList = new HashMap<>(); + + public static String getPlayer(int uid) { + for (PlayerList player : playerList.values()) { + if (player.playerMap.get(uid) != null) { + return player.playerMap.get(uid); + } + } + return null; + } + + public static String getPlayerInServer(int uid) { + AtomicReference ret = new AtomicReference<>(); + playerList.forEach((key, value) -> { + if (value.playerMap.get(uid) != null) { + ret.set(key); + } + }); + return ret.get(); + } + + public static OnlinePlayer getOnlinePlayer() { + ArrayList player = new ArrayList<>(); + playerList.forEach((address, playerMap) -> playerMap.playerMap.forEach((uid, name) -> player.add(name))); + return new OnlinePlayer(player); + } + + public static class OnlinePlayer { + public int count; + public ArrayList playerList; + + public OnlinePlayer(ArrayList playerList) { + this.playerList = playerList; + this.count = playerList.size(); + } + + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java new file mode 100644 index 0000000..e1f8e0e --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java @@ -0,0 +1,60 @@ +package com.github.jie65535.opencommand.socket; + +// 异步等待数据返回 +public abstract class SocketDataWait extends Thread { + public T data; + public long timeout; + public long time; + public String uid; + + /** + * 异步等待数据返回 + * @param timeout 超时时间 + */ + public SocketDataWait(long timeout) { + this.timeout = timeout; + start(); + } + + public abstract void run(); + + /** + * 数据处理 + * @param data 数据 + * @return 处理后的数据 + */ + public abstract T initData(T data); + + /** + * 超时回调 + */ + public abstract void timeout(); + + /** + * 异步设置数据 + * @param data 数据 + */ + public void setData(Object data) { + this.data = initData((T) data); + } + + /** + * 获取异步数据(此操作会一直堵塞直到获取到数据) + * @return 数据 + */ + public T getData() { + while (data == null) { + try { + time += 100; + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (time > timeout) { + timeout(); + return null; + } + } + return data; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java new file mode 100644 index 0000000..4c8d192 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -0,0 +1,264 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.*; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +// Socket 服务器 +public class SocketServer { + // 客户端超时时间 + private static final int TIMEOUT = 5000; + private static final HashMap clientList = new HashMap<>(); + + private static final HashMap clientTimeout = new HashMap<>(); + private static Logger mLogger; + + public static HashMap getOnlineClient() { + HashMap onlineClient = new HashMap<>(); + for (var key : clientList.entrySet()) { + onlineClient.put(key.getValue().uuid, key.getValue().clientThread.getDisplayName()); + } + return onlineClient; + } + + public static ClientInfo getClientInfoByUuid(String uuid) { + for (var key : clientList.entrySet()) { + if (key.getValue().uuid.equals(uuid)) { + return key.getValue(); + } + } + return null; + } + + public static void startServer() throws IOException { + int port = OpenCommandPlugin.getInstance().getConfig().socketPort; + mLogger = OpenCommandPlugin.getInstance().getLogger(); + new Timer().schedule(new SocketClientCheck(), 500); + new WaitClientConnect(port); + } + + // 向全部客户端发送数据 + public static boolean sendAllPacket(BasePacket packet) { + var p = SocketUtils.getPacket(packet); + HashMap old = (HashMap) clientList.clone(); + for (var client : old.entrySet()) { + if (!client.getValue().sendPacket(p)) { + mLogger.warn("[OpenCommand] Send packet to client {} failed", client.getKey()); + clientList.remove(client.getKey()); + } + } + return false; + } + + // 根据地址发送到相应的客户端 + public static boolean sendPacket(String address, BasePacket packet) { + var p = SocketUtils.getPacket(packet); + var client = clientList.get(address); + if (client != null) { + if (client.clientThread.sendPacket(p)) { + return true; + } + mLogger.warn("[OpenCommand] Send packet to client {} failed", address); + clientList.remove(address); + } + return false; + } + + public static boolean sendPacketAndWait(String address, BasePacket packet, SocketDataWait wait) { + var p = SocketUtils.getPacketAndPackID(packet); + var client = clientList.get(address); + if (client != null) { + wait.uid = p.get(0); + if (client.clientThread.sendPacket(p.get(1), wait)) { + return true; + } + mLogger.warn("[OpenCommand] Send packet to client {} failed", address); + clientList.remove(address); + } + return false; + } + + // 根据Uid发送到相应的客户端异步返回数据 + public static boolean sendUidPacket(Integer playerId, BasePacket player, SocketDataWait socketDataWait) { + var p = SocketUtils.getPacketAndPackID(player); + var clientID = SocketData.getPlayerInServer(playerId); + if (clientID == null) return false; + var client = clientList.get(clientID); + if (client != null) { + socketDataWait.uid = p.get(0); + if (!client.clientThread.sendPacket(p.get(1), socketDataWait)) { + mLogger.warn("[OpenCommand] Send packet to client {} failed", clientID); + clientList.remove(clientID); + return false; + } + return true; + } + return false; + } + + // 客户端超时检测 + private static class SocketClientCheck extends TimerTask { + @Override + public void run() { + HashMap old = (HashMap) clientTimeout.clone(); + for (var client : old.entrySet()) { + var clientID = client.getKey(); + var clientTime = client.getValue(); + if (clientTime > TIMEOUT) { + mLogger.info("[OpenCommand] Client {} timeout, disconnect.", clientID); + clientList.remove(clientID); + clientTimeout.remove(clientID); + SocketData.playerList.remove(clientID); + } else { + clientTimeout.put(clientID, clientTime + 500); + } + } + } + } + + // 客户端数据包处理 + static class ClientThread extends Thread { + private final Socket socket; + private InputStream is; + private OutputStream os; + private final String address; + private final String token; + private boolean auth = false; + private String displayName = ""; + + private final HashMap> socketDataWaitList = new HashMap<>(); + + public ClientThread(Socket accept) { + socket = accept; + address = socket.getInetAddress() + ":" + socket.getPort(); + token = OpenCommandPlugin.getInstance().getConfig().socketToken; + try { + is = accept.getInputStream(); + os = accept.getOutputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + public Socket getSocket() { + return socket; + } + + // 发送数据包 + public boolean sendPacket(String packet) { + return SocketUtils.writeString(os, packet); + } + + // 发送异步数据包 + public boolean sendPacket(String packet, SocketDataWait socketDataWait) { + if (SocketUtils.writeString(os, packet)) { + socketDataWaitList.put(socketDataWait.uid, socketDataWait); + return true; + } else { + return false; + } + } + + @Override + public void run() { + // noinspection InfiniteLoopStatement + while (true) { + try { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.type == PacketEnum.AuthPacket) { + AuthPacket authPacket = Grasscutter.getGsonFactory().fromJson(packet.data, AuthPacket.class); + if (authPacket.token.equals(token)) { + auth = true; + displayName = authPacket.displayName; + mLogger.info("[OpenCommand] Client {} auth success, name: {}", address, displayName); + clientList.put(address, new ClientInfo(UUID.randomUUID().toString(), address, this)); + clientTimeout.put(address, 0); + } else { + mLogger.warn("[OpenCommand] Client {} auth failed", address); + socket.close(); + return; + } + } + if (!auth) { + mLogger.warn("[OpenCommand] Client {} auth failed", address); + socket.close(); + return; + } + switch (packet.type) { + // 缓存玩家列表 + case PlayerList -> { + PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); + SocketData.playerList.put(address, playerList); + } + // Http信息返回 + case HttpPacket -> { + HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); + var socketWait = socketDataWaitList.get(packet.packetID); + if (socketWait == null) { + mLogger.error("[OpenCommand] HttpPacket: " + packet.packetID + " not found"); + return; + } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); + } + } + } catch (Throwable e) { + e.printStackTrace(); + mLogger.error("[OpenCommand] Client {} disconnect.", address); + clientList.remove(address); + clientTimeout.remove(address); + SocketData.playerList.remove(address); + break; + } + } + } + + public String getDisplayName() { + return displayName; + } + } + + // 等待客户端连接 + private static class WaitClientConnect extends Thread { + ServerSocket socketServer; + + public WaitClientConnect(int port) throws IOException { + socketServer = new ServerSocket(port); + start(); + } + + @Override + public void run() { + mLogger.info("[OpenCommand] Start socket server on port " + socketServer.getLocalPort()); + // noinspection InfiniteLoopStatement + while (true) { + try { + Socket accept = socketServer.accept(); + String address = accept.getInetAddress() + ":" + accept.getPort(); + mLogger.info("[OpenCommand] Client connect: " + address); + new ClientThread(accept); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java new file mode 100644 index 0000000..79ad9e9 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java @@ -0,0 +1,143 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.Packet; +import emu.grasscutter.Grasscutter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +// Socket 工具类 +public class SocketUtils { + + /** + * 获取打包后的数据包 + * + * @param bPacket 数据包 + * @return 打包后的数据包 + */ + public static String getPacket(BasePacket bPacket) { + Packet packet = new Packet(); + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = UUID.randomUUID().toString(); + return Grasscutter.getGsonFactory().toJson(packet); + } + + /** + * 获取打包后的数据包 + * + * @param bPacket BasePacket + * @return list[0] 是包ID, list[1] 是数据包 + */ + public static List getPacketAndPackID(BasePacket bPacket) { + Packet packet = new Packet(); + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = UUID.randomUUID().toString(); + + List list = new ArrayList<>(); + list.add(packet.packetID); + list.add(Grasscutter.getGsonFactory().toJson(packet)); + return list; + } + + /** + * 获取打包后的数据包 + * + * @param bPacket 数据包 + * @param packetID 数据包ID + * @return 打包后的数据包 + */ + public static String getPacketAndPackID(BasePacket bPacket, String packetID) { + Packet packet = new Packet(); + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = packetID; + return Grasscutter.getGsonFactory().toJson(packet); + } + + /** + * 读整数 + * + * @param is 输入流 + * @return 整数 + */ + public static int readInt(InputStream is) { + int[] values = new int[4]; + try { + for (int i = 0; i < 4; i++) { + values[i] = is.read(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return values[0] << 24 | values[1] << 16 | values[2] << 8 | values[3]; + } + + /** + * 写整数 + * + * @param os 输出流 + * @param value 整数 + */ + public static void writeInt(OutputStream os, int value) { + int[] values = new int[4]; + values[0] = (value >> 24) & 0xFF; + values[1] = (value >> 16) & 0xFF; + values[2] = (value >> 8) & 0xFF; + values[3] = (value) & 0xFF; + + try { + for (int i = 0; i < 4; i++) { + os.write(values[i]); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 读字符串 + * + * @param is 输入流 + * @return 字符串 + */ + public static String readString(InputStream is) { + int len = readInt(is); + byte[] sByte = new byte[len]; + try { + is.read(sByte); + } catch (IOException e) { + e.printStackTrace(); + } + String s = new String(sByte); + return s; + } + + /** + * 写字符串 + * + * @param os 输出流 + * @param s 字符串 + * @return 是否成功 + */ + public static boolean writeString(OutputStream os, String s) { + try { + byte[] bytes = s.getBytes(); + int len = bytes.length; + writeInt(os, len); + os.write(bytes); + return true; + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java new file mode 100644 index 0000000..768beea --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java @@ -0,0 +1,28 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class AuthPacket extends BasePacket { + public String token; + public String displayName; + + public AuthPacket(String token, String displayName) { + this.displayName = displayName; + this.token = token; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.AuthPacket; + } + + @Override + public String toString() { + return "AuthPacket [token=" + token + ", displayName=" + displayName + "]"; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java new file mode 100644 index 0000000..a04d977 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java @@ -0,0 +1,8 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 基本数据包 +public abstract class BasePacket { + public abstract String getPacket(); + + public abstract PacketEnum getType(); +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java new file mode 100644 index 0000000..d109e40 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java @@ -0,0 +1,22 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +// 心跳包 +public class HeartBeat extends BasePacket { + public String ping; + + public HeartBeat(String ping) { + this.ping = ping; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.HeartBeat; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java new file mode 100644 index 0000000..4e81e7d --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java @@ -0,0 +1,39 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +// http返回数据 +public class HttpPacket extends BasePacket { + public int code = 200; + public String message = "Success"; + public String data; + + public HttpPacket(int code, String message, String data) { + this.code = code; + this.message = message; + this.data = data; + } + + public HttpPacket(int code, String message) { + this.code = code; + this.message = message; + } + public HttpPacket(String data) { + this.data = data; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.HttpPacket; + } + + @Override + public String toString() { + return "HttpPacket [code=" + code + ", message=" + message + ", data=" + data + "]"; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java new file mode 100644 index 0000000..6fe303a --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java @@ -0,0 +1,13 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包结构 +public class Packet { + public PacketEnum type; + public String data; + public String packetID; + + @Override + public String toString() { + return "Packet [type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java new file mode 100644 index 0000000..54a8fdf --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java @@ -0,0 +1,11 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包类型列表 +public enum PacketEnum { + PlayerList, + Player, + HttpPacket, + AuthPacket, + RunConsoleCommand, + HeartBeat +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java new file mode 100644 index 0000000..48d8d5a --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java @@ -0,0 +1,21 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class RunConsoleCommand extends BasePacket { + public String command; + + public RunConsoleCommand(String command) { + this.command = command; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.RunConsoleCommand; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java new file mode 100644 index 0000000..ee07fd8 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java @@ -0,0 +1,31 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +import com.github.jie65535.opencommand.socket.SocketServer; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.PacketEnum; +import emu.grasscutter.Grasscutter; + +// 玩家操作类 +public class Player extends BasePacket { + public PlayerEnum type; + public int uid; + public String data; + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.Player; + } + + public static void dropMessage(int uid, String str) { + Player p = new Player(); + p.type = PlayerEnum.DropMessage; + p.uid = uid; + p.data = str; + SocketServer.sendAllPacket(p); + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java new file mode 100644 index 0000000..155d45e --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java @@ -0,0 +1,7 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +// 玩家操作列表 +public enum PlayerEnum { + DropMessage, + RunCommand +} \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java new file mode 100644 index 0000000..a2782dc --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java @@ -0,0 +1,28 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.PacketEnum; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// 玩家列表信息 +public class PlayerList extends BasePacket { + public int player = -1; + public List playerList = new ArrayList<>(); + public Map playerMap = new HashMap<>(); + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.PlayerList; + } +}