18 Commits

Author SHA1 Message Date
方块君
1439139404 Update README_en-US.md 2022-07-26 20:49:52 +08:00
方块君
2799a06312 Add multi server deployment tutorial 2022-07-26 20:47:25 +08:00
9fe473d6cc Update version to v1.3.0 2022-07-26 20:19:03 +08:00
d2499bad5f Merge pull request #12 from 577fkj/master
Add multi server support
2022-07-26 20:08:28 +08:00
方块君
6db7c89196 Update build.yml 2022-07-26 19:52:13 +08:00
方块君
dc5cbcc981 Update build.yml 2022-07-26 19:45:07 +08:00
方块君
3ed8e233bc Update README.md 2022-07-26 19:30:31 +08:00
方块君
4712e2cd73 Fix run permission 2022-07-26 19:23:10 +08:00
方块君
6aa19ce860 Add github actions 2022-07-26 19:20:42 +08:00
方块君
4d08acd084 Add multi server run console command 2022-07-26 19:14:49 +08:00
方块君
37dcd0f1a7 Update README 2022-07-22 23:03:25 +08:00
方块君
a4048d4fd1 Add auto sending player list 2022-07-22 22:08:02 +08:00
方块君
e62b1d4967 Change the server authentication method 2022-07-22 21:21:12 +08:00
方块君
8030ef8034 Prevent some exceptions 2022-07-22 14:44:22 +08:00
方块君
67f3eb180d Change return type 2022-07-21 21:17:24 +08:00
方块君
3bc9b0ab14 Fix client disconnect not clean player list 2022-07-21 20:34:56 +08:00
方块君
8b91e50d02 Update README 2022-07-21 20:30:26 +08:00
方块君
684ab421bd Add multi server support 2022-07-21 00:50:42 +08:00
29 changed files with 1770 additions and 57 deletions

50
.github/workflows/build.yml vendored Normal file
View File

@@ -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

3
.gitignore vendored
View File

@@ -23,3 +23,6 @@
hs_err_pid* hs_err_pid*
/lib/ /lib/
.gradle
.idea
/build

224
README.md
View File

@@ -5,8 +5,10 @@
一个为第三方客户端开放GC命令执行接口的插件 一个为第三方客户端开放GC命令执行接口的插件
## 服务端安装 ## 服务端安装
1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar` 1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar`
2. 放入 `plugins` 文件夹即可 2. 放入 `plugins` 文件夹即可
> 注意,如果出现以下错误: > 注意,如果出现以下错误:
> ```log > ```log
> INFO:PluginManager Enabling plugin: opencommand-plugin > INFO:PluginManager Enabling plugin: opencommand-plugin
@@ -17,24 +19,42 @@
> 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版! > 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版!
## 控制台连接 ## 控制台连接
1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json` 1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json`
2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。 2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。
3. 重新启动服务端即可生效配置 3. 重新启动服务端即可生效配置
4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令 4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令
## 多服务器
### 主服务器 (Dispatch)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketPort` 值为一个未被使用的端口
3. 设置 `socketToken` 多服务器通信密钥建议使用至少32字符的长随机字符串。
4. 重新启动服务端即可生效配置
### 子服务器 (Game)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketHost``socketPort` 值为主服务器的地址和端口
3. 设置 `socketToken` 和主服务器相同的值
4. 设置 `socketDisplayName` 值为你的服务器名称 (用途请见[下方](https://github.com/jie65535/gc-opencommand-plugin#%E8%8E%B7%E5%8F%96%E5%A4%9A%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%97%E8%A1%A8))
5. 重新启动服务端即可生效配置
## 构建说明 ## 构建说明
1. 克隆仓库 1. 克隆仓库
2. 在目录下新建 `lib` 目录 2. 在目录下新建 `lib` 目录
3.`grasscutter-1.1.x-dev.jar` 放入 `lib` 目录 3.`grasscutter-1.1.x-dev.jar` 放入 `lib` 目录
4. `gradle build` 4. `gradle build`
## 玩家使用流程 ## 玩家使用流程
1. 在客户端中填写服务地址,确认是否支持 1. 在客户端中填写服务地址,确认是否支持
2. 填写UID发送验证码 2. 填写UID发送验证码
3. 将游戏内收到的**4位整数验证码**填入客户端校验 3. 将游戏内收到的**4位整数验证码**填入客户端校验
4. 享受便利! 4. 享受便利!
## 客户端请求流程 ## 客户端请求流程
1. `ping` 确认是否支持 `opencommand` 插件 1. `ping` 确认是否支持 `opencommand` 插件
2. `sendCode` 向指定玩家发送验证码1分钟内不允许重发保存返回的 `token` 2. `sendCode` 向指定玩家发送验证码1分钟内不允许重发保存返回的 `token`
3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验 3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验
@@ -43,6 +63,7 @@
--- ---
## `config.json` ## `config.json`
```json ```json
{ {
// 控制台连接令牌 // 控制台连接令牌
@@ -52,56 +73,217 @@
// 临时令牌过期时间(秒) // 临时令牌过期时间(秒)
"tempTokenExpirationTime_S": 300, "tempTokenExpirationTime_S": 300,
// 授权令牌最后使用过期时间(小时) // 授权令牌最后使用过期时间(小时)
"tokenLastUseExpirationTime_H": 48 "tokenLastUseExpirationTime_H": 48,
// 多服务器通信端口
"socketPort": 5746,
// 多服务器通信密钥
"socketToken": "",
// 多服务器Dispatch服务器地址
"socketHost": "127.0.0.1",
// 多服务器显示名称
"socketDisplayName": ""
} }
``` ```
## API `/opencommand/api` ## API `/opencommand/api`
示例 示例
``` ```
https://127.0.0.1/opencommand/api https://127.0.0.1/opencommand/api
``` ```
### Request 请求 ### Request 请求
```java ```java
public final class JsonRequest { public final class JsonRequest {
public String token = ""; public String token = "";
public String action = ""; public String action = "";
public String server = "";
public Object data = null; public Object data = null;
} }
``` ```
### Response 响应 ### Response 响应
```java ```java
public final class JsonResponse { public final class JsonResponse {
public int retcode = 200; public int retcode = 200;
public String message = "success"; public String message = "Success";
public Object data; public Object data;
} }
``` ```
### Actions 动作 ### Actions 动作
#### `ping`
data = null
#### `sendCode` #### `测试连接`
##### Request
data = uid (int)
##### Response
data = token (string)
#### `verify` 要求 `token`
##### Request ##### Request
data = code (int)
##### Response
###### Success:
code = 200
###### Verification failed:
code = 400
#### `command` 要求 `token` | 请求参数 | 请求数据 | 类型 |
##### Request |--------|--------|----------|
data = command (string) | action | `ping` | `String` |
##### Response ##### 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
> 如果为单服务器则无需填写服务器 UUID
| 请求参数 | 请求数据 | 类型 |
|--------|-----------|----------|
| action | `command` | `String` |
| token | `token` | `String` |
| server | `UUID` | `String` |
| data | `command` | `String` |
##### Response
成功
| 返回参数 | 返回数据 | 类型 |
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |

View File

@@ -5,16 +5,35 @@
A plugin that opens the GC command execution interface for third-party clients A plugin that opens the GC command execution interface for third-party clients
## Server installation ## Server installation
1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases)
2. Put it in the `plugins` folder 2. Put it in the `plugins` folder
## Console connection ## Console connection
1. When starting for the first time, a `opencommand-plugin` directory will be generated under the `plugins` directory, open and edit `config.json`
2. Set the value of `consoleToken` to your connection key. It is recommended to use a long random string of at least 32 characters. 1. When starting for the first time, a `opencommand-plugin` directory will be generated under the `plugins` directory,
open and edit `config.json`
2. Set the value of `consoleToken` to your connection key. It is recommended to use a long random string of at least 32
characters.
3. Restart the server to take effect 3. Restart the server to take effect
4. Select the console identity in the client, and fill in your `consoleToken` to run the command as the console identity 4. Select the console identity in the client, and fill in your `consoleToken` to run the command as the console identity
## Multi server
### Master server (Dispatch)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `socketPort` value to an unused port
3. Set `sockettoken` multi server communication key. It is recommended to use a long random string of at least 32 characters.
4. Restart the server to make the configuration effective
### Sub server (Game)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `sockethost` and `socketport` values to the address and port of the primary server
3. Set the same value of `sockettoken` and the primary server
4. Set the `socketDisplayName` value to your server name (See below for usage [Jump](https://github.com/jie65535/gc-opencommand-plugin/blob/master/README_en-US.md#get-mulit-server-list))
5. Restart the server to make the configuration effective
## Build ## Build
1. `git clone https://github.com/jie65535/gc-opencommand-plugin` 1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin` 2. `cd gc-opencommand-plugin`
3. `mkdir lib` 3. `mkdir lib`
@@ -22,75 +41,235 @@ A plugin that opens the GC command execution interface for third-party clients
5. `gradle build` 5. `gradle build`
## Player ## Player
1. Fill in the service address in the client to confirm whether it supports 1. Fill in the service address in the client to confirm whether it supports
2. Fill in the UID and send the verification code 2. Fill in the UID and send the verification code
3. Fill in the **4-digit integer verification code** received in the game into the client verification 3. Fill in the **4-digit integer verification code** received in the game into the client verification
4. Enjoy the convenience! 4. Enjoy the convenience!
## Client request ## Client request
1. `ping` to confirm whether the `opencommand` plugin is supported 1. `ping` to confirm whether the `opencommand` plugin is supported
2. `sendCode` sends a verification code to the specified player (re-send is not allowed within 1 minute), and save the returned `token` 2. `sendCode` sends a verification code to the specified player (re-send is not allowed within 1 minute), and save the
returned `token`
3. Send `verify` check using `token` and **4-digit integer verification code** 3. Send `verify` check using `token` and **4-digit integer verification code**
4. If the verification is passed, you can use the `token` to execute the `command` action 4. If the verification is passed, you can use the `token` to execute the `command` action
--- ---
## `config.json` ## `config.json`
```json ```json
{ {
"consoleToken": "", "consoleToken": "",
"codeExpirationTime_S": 60, "codeExpirationTime_S": 60,
"tempTokenExpirationTime_S": 300, "tempTokenExpirationTime_S": 300,
"tokenLastUseExpirationTime_H": 48 "tokenLastUseExpirationTime_H": 48,
"socketPort": 5746,
"socketToken": "",
"socketHost": "127.0.0.1"
} }
``` ```
## API `/opencommand/api` ## API `/opencommand/api`
Example Example
``` ```
https://127.0.0.1/opencommand/api https://127.0.0.1/opencommand/api
``` ```
## Request ## Request
```java ```java
public final class JsonRequest { public final class JsonRequest {
public String token = ""; public String token = "";
public String action = ""; public String action = "";
public Seting server = "";
public Object data = null; public Object data = null;
} }
``` ```
## Response ## Response
```java ```java
public final class JsonResponse { public final class JsonResponse {
public int retcode = 200; public int retcode = 200;
public String message = "success"; public String message = "Success";
public Object data; public Object data;
} }
``` ```
## Actions ### Actions
### `ping`
data = null
### `sendCode` #### `Test connect`
#### Request
data = uid (int)
#### Response
data = token (string)
### `verify`: Requires `token` ##### Request
#### Request
data = code (int)
#### Response
##### Success:
code = 200
##### Verification failed:
code = 400
### `command`: Requires `token` | Request | Request data | type |
#### Request |---------|--------------|----------|
data = command (string) | action | `ping` | `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
> If it is a single server, there is no need to fill in server UUID.
| 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` |

View File

@@ -4,7 +4,7 @@ plugins {
} }
group 'com.github.jie65535.opencommand' group 'com.github.jie65535.opencommand'
version 'dev-1.2.4' version 'dev-1.3.0'
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17

View File

@@ -1,17 +1,57 @@
package com.github.jie65535.opencommand; 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.game.ReceiveCommandFeedbackEvent;
import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.utils.MessageHandler; import emu.grasscutter.utils.MessageHandler;
import java.util.ArrayList;
public final class EventListeners { public final class EventListeners {
private static MessageHandler consoleMessageHandler; private static MessageHandler consoleMessageHandler;
public static void setConsoleMessageHandler(MessageHandler handler) { public static void setConsoleMessageHandler(MessageHandler handler) {
consoleMessageHandler = handler; consoleMessageHandler = handler;
} }
public static void onCommandResponse(ReceiveCommandFeedbackEvent event) { public static void onCommandResponse(ReceiveCommandFeedbackEvent event) {
if (consoleMessageHandler != null && event.getPlayer() == null) { if (consoleMessageHandler != null && event.getPlayer() == null) {
consoleMessageHandler.setMessage(event.getMessage()); consoleMessageHandler.setMessage(event.getMessage());
} }
} }
public static void onPlayerJoin(PlayerJoinEvent playerJoinEvent) {
PlayerList playerList = new PlayerList();
playerList.player = Grasscutter.getGameServer().getPlayers().size();
ArrayList<String> 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<String> 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);
}
} }

View File

@@ -22,4 +22,8 @@ public class OpenCommandConfig {
public int codeExpirationTime_S = 60; public int codeExpirationTime_S = 60;
public int tempTokenExpirationTime_S = 300; public int tempTokenExpirationTime_S = 300;
public int tokenLastUseExpirationTime_H = 48; public int tokenLastUseExpirationTime_H = 48;
public int socketPort = 5746;
public String socketToken = "";
public String socketHost = "127.0.0.1";
public String socketDisplayName = "";
} }

View File

@@ -19,6 +19,7 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest; import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse; import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.socket.SocketData;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto; 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.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -85,6 +87,11 @@ public final class OpenCommandHandler implements Router {
} else if (req.action.equals("ping")) { } else if (req.action.equals("ping")) {
response.json(new JsonResponse()); response.json(new JsonResponse());
return; return;
} else if (req.action.equals("online")) {
var p = new ArrayList<String>();
plugin.getServer().getPlayers().forEach((uid, player) -> p.add(player.getNickname()));
response.json(new JsonResponse(200, "Success", new SocketData.OnlinePlayer(p)));
return;
} }
// token is required // token is required
@@ -118,6 +125,9 @@ public final class OpenCommandHandler implements Router {
} }
} }
return; return;
} else if (req.action.equals("runmode")) {
response.json(new JsonResponse(200, "Success", 0));
return;
} }
} else if (codes.containsKey(req.token)) { } else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) { if (req.action.equals("verify")) {

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Integer> clients = new HashMap<>();
private static final Map<String, Date> tokenExpireTime = new HashMap<>();
private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> 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<HttpPacket>(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<HttpPacket> 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());
}
}
}
}

View File

@@ -17,11 +17,15 @@
*/ */
package com.github.jie65535.opencommand; 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.Grasscutter;
import emu.grasscutter.plugin.Plugin; import emu.grasscutter.plugin.Plugin;
import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; 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.File;
import java.io.FileReader; import java.io.FileReader;
@@ -31,7 +35,10 @@ import java.io.IOException;
public final class OpenCommandPlugin extends Plugin { public final class OpenCommandPlugin extends Plugin {
private static OpenCommandPlugin instance; private static OpenCommandPlugin instance;
public static OpenCommandPlugin getInstance() { return instance; }
public static OpenCommandPlugin getInstance() {
return instance;
}
private OpenCommandConfig config; private OpenCommandConfig config;
@@ -47,7 +54,21 @@ public final class OpenCommandPlugin extends Plugin {
.priority(HandlerPriority.HIGH) .priority(HandlerPriority.HIGH)
.listener(EventListeners::onCommandResponse) .listener(EventListeners::onCommandResponse)
.register(this); .register(this);
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); getHandle().addRouter(OpenCommandHandler.class);
}
getLogger().info("[OpenCommand] Enabled"); getLogger().info("[OpenCommand] Enabled");
} }
@@ -67,16 +88,32 @@ public final class OpenCommandPlugin extends Plugin {
try (var file = new FileWriter(configFile)) { try (var file = new FileWriter(configFile)) {
file.write(Grasscutter.getGsonFactory().toJson(config)); file.write(Grasscutter.getGsonFactory().toJson(config));
} catch (IOException e) { } catch (IOException e) {
getLogger().error("Unable to write to config file."); getLogger().error("[OpenCommand] Unable to write to config file.");
} catch (Exception e) { } catch (Exception e) {
getLogger().error("Unable to save config file."); getLogger().error("[OpenCommand] Unable to save config file.");
} }
} else { } else {
try (var file = new FileReader(configFile)) { try (var file = new FileReader(configFile)) {
config = Grasscutter.getGsonFactory().fromJson(file, OpenCommandConfig.class); config = Grasscutter.getGsonFactory().fromJson(file, OpenCommandConfig.class);
} catch (Exception exception) { } catch (Exception exception) {
config = new OpenCommandConfig(); 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);
} }
} }
} }

View File

@@ -20,5 +20,6 @@ package com.github.jie65535.opencommand.json;
public final class JsonRequest { public final class JsonRequest {
public String token = ""; public String token = "";
public String action = ""; public String action = "";
public String server = "";
public Object data = null; public Object data = null;
} }

View File

@@ -19,7 +19,7 @@ package com.github.jie65535.opencommand.json;
public final class JsonResponse { public final class JsonResponse {
public int retcode = 200; public int retcode = 200;
public String message = "success"; public String message = "Success";
public Object data; public Object data;
public JsonResponse() { public JsonResponse() {

View File

@@ -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;
}
}

View File

@@ -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<String> 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();
}
}
}
}

View File

@@ -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<String, PlayerList> 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<String> ret = new AtomicReference<>();
playerList.forEach((key, value) -> {
if (value.playerMap.get(uid) != null) {
ret.set(key);
}
});
return ret.get();
}
public static OnlinePlayer getOnlinePlayer() {
ArrayList<String> 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<String> playerList;
public OnlinePlayer(ArrayList<String> playerList) {
this.playerList = playerList;
this.count = playerList.size();
}
}
}

View File

@@ -0,0 +1,60 @@
package com.github.jie65535.opencommand.socket;
// 异步等待数据返回
public abstract class SocketDataWait<T> 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;
}
}

View File

@@ -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<String, ClientInfo> clientList = new HashMap<>();
private static final HashMap<String, Integer> clientTimeout = new HashMap<>();
private static Logger mLogger;
public static HashMap<String, String> getOnlineClient() {
HashMap<String, String> 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<String, ClientThread> old = (HashMap<String, ClientThread>) 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<String, Integer> old = (HashMap<String, Integer>) 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<String, SocketDataWait<?>> 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();
}
}
}
}
}

View File

@@ -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<String> getPacketAndPackID(BasePacket bPacket) {
Packet packet = new Packet();
packet.type = bPacket.getType();
packet.data = bPacket.getPacket();
packet.packetID = UUID.randomUUID().toString();
List<String> 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;
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -0,0 +1,8 @@
package com.github.jie65535.opencommand.socket.packet;
// 基本数据包
public abstract class BasePacket {
public abstract String getPacket();
public abstract PacketEnum getType();
}

View File

@@ -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;
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -0,0 +1,11 @@
package com.github.jie65535.opencommand.socket.packet;
// 数据包类型列表
public enum PacketEnum {
PlayerList,
Player,
HttpPacket,
AuthPacket,
RunConsoleCommand,
HeartBeat
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package com.github.jie65535.opencommand.socket.packet.player;
// 玩家操作列表
public enum PlayerEnum {
DropMessage,
RunCommand
}

View File

@@ -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<String> playerList = new ArrayList<>();
public Map<Integer, String> playerMap = new HashMap<>();
@Override
public String getPacket() {
return Grasscutter.getGsonFactory().toJson(this);
}
@Override
public PacketEnum getType() {
return PacketEnum.PlayerList;
}
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "opencommand-plugin", "name": "opencommand-plugin",
"description": "Open command interface for third-party clients", "description": "Open command interface for third-party clients",
"version": "dev-1.2.4", "version": "dev-1.3.0",
"mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin", "mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin",
"authors": ["jie65535"] "authors": ["jie65535"]
} }