Compare commits

..

No commits in common. "master" and "v1.4.0" have entirely different histories.

12 changed files with 361 additions and 561 deletions

View File

@ -5,7 +5,7 @@ on:
paths:
- "**.java"
branches:
- "master"
- "main"
pull_request:
paths:
- "**.java"

View File

@ -4,55 +4,26 @@
一个为第三方客户端开放GC命令执行接口的插件
`1.7.0` 起可以通过 `|` 或者换行来分隔多条命令,例如:
```shell
/a 1 | /a 2
/a 3
```
调用 `ping` 响应数据将包含插件版本号。
## 使用本插件的应用
- [GrasscutterTools](https://github.com/jie65535/GrasscutterCommandGenerator) —— Windows 客户端工具
- [JGrasscutterCommand](https://github.com/jie65535/JGrasscutterCommand) —— [Mirai](https://github.com/mamoe/mirai) 插件在QQ里执行命令
- [Yunzai-GrasscutterCommand](https://github.com/Zyy-boop/Yunzai-GrasscutterCommand) —— Yunzai-bot插件在QQ里执行命令
- 待补充
## 服务端安装
1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar`
2. 放入 `grasscutter/plugins` 文件夹
3. 重启 `grasscutter` 即可生效
2. 放入 `plugins` 文件夹即可
## 玩家使用流程
1. 在远程工具中填写服务地址,查询插件状态
2. 填写UID发送验证码需要在线
3. 将游戏内收到的**4位整数验证码**填入工具校验
4. 享受便利!
> 注意,如果出现以下错误:
> ```log
> INFO:PluginManager Enabling plugin: opencommand-plugin
> Exception in thread "main" java.lang.NoSuchMethodError: 'void emu.grasscutter.server.event.EventHandler.register(emu.grasscutter.plugin.Plugin)'
> at com.github.jie65535.opencommand.OpenCommandPlugin.onEnable(OpenCommandPlugin.java:49)
> at emu.grasscutter.plugin.PluginManager.lambda$enablePlugins$3(PluginManager.java:131)
> ```
> 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版!
## 控制台连接
1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json`
2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。(检测到为空时会自动生成,生成时会在控制台中输出)
2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。
3. 重新启动服务端即可生效配置
4. 在工具中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令
---
## 客户端请求流程
1. `ping` 确认是否支持 `opencommand` 插件
2. `sendCode` 向指定玩家发送验证码1分钟内不允许重发保存返回的 `token`
3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验
4. 如果验证通过,可以使用该 `token` 执行 `command` 动作
## 插件构建说明
1. 克隆仓库
2. 在目录下新建 `lib` 目录
3. 将 `grasscutter.jar` 放入 `lib` 目录
4. 执行 `gradle build`
4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令
## 多服务器
### 主服务器 (Dispatch)
@ -68,13 +39,34 @@
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. 克隆仓库
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` 校验
4. 如果验证通过,可以使用该 `token` 执行 `command` 动作
---
## `config.json`
```json5
```json
{
// 控制台连接令牌(检测到空时会自动生成)
// 控制台连接令牌
"consoleToken": "",
// 验证码过期时间(秒)
"codeExpirationTime_S": 60,
@ -261,7 +253,7 @@ public final class JsonResponse {
| message | `Success` | `String` |
| data | `{}` | `JsonObject` |
```json5
```json
{
"retcode": 200,
"message": "success",

View File

@ -4,60 +4,20 @@
A plugin that opens the GC command execution interface for third-party clients
Since `1.7.0`, multiple commands can be separated by `|` or newline, for example:
```shell
/a 1 | /a 2
/a 3
```
Invoking `ping` the response data will contain the plugin version.
## Applications using this plug-in
- [GrasscutterTools](https://github.com/jie65535/GrasscutterCommandGenerator) —— Windows Client Tools
- [JGrasscutterCommand](https://github.com/jie65535/JGrasscutterCommand) —— [Mirai](https://github.com/mamoe/mirai) Plugin, run commands in QQ
- [Yunzai-GrasscutterCommand](https://github.com/Zyy-boop/Yunzai-GrasscutterCommand) —— Yunzai-bot plugin, execute commands in QQ
- More...
## Server installation
1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases)
2. Put it in the `grasscutter/plugins` folder
3. Restart `grasscutter` server
## Player
1. Fill in the service address in the Tool to check plugin status
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 Tool verification
4. Enjoy the convenience!
2. Put it in the `plugins` folder
## 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. (automatically generated when empty is detected)
characters.
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
---
## Client request
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`
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
## Build
1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin`
3. `mkdir lib`
4. `mv path/to/grasscutter-1.x.x-dev.jar ./lib`
5. `gradle build`
## Multi server
### Master server (Dispatch)
1. Open `config.json` in the `opencommand-plugin` directory
@ -72,28 +32,42 @@ Invoking `ping` the response data will contain the plugin version.
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
1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin`
3. `mkdir lib`
4. `mv path/to/grasscutter-1.x.x-dev.jar ./lib`
5. `gradle build`
## Player
1. Fill in the service address in the client to confirm whether it supports
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
4. Enjoy the convenience!
## Client request
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`
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
---
## `config.json`
```json5
```json
{
// console connection token (automatically generated when empty is detected)
"consoleToken": "",
// Verification code expiration time (seconds)
"codeExpirationTime_S": 60,
// Temporary token expiration time (seconds)
"tempTokenExpirationTime_S": 300,
// Authorization token last used expiration time (hours)
"tokenLastUseExpirationTime_H": 48,
// Multi-server communication port
"socketPort": 5746,
// Multi-server communication key
"socketToken": "",
// Multi-server Dispatch server address
"socketHost": "127.0.0.1",
// multi-server display name
"socketDisplayName": ""
"socketHost": "127.0.0.1"
}
```
@ -265,7 +239,7 @@ Success
| message | `Success` | `String` |
| data | `{}` | `JsonObject` |
```json5
```json
{
"retcode": 200,
"message": "success",

View File

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

View File

@ -24,52 +24,21 @@ 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 it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import emu.grasscutter.utils.MessageHandler;
import java.util.ArrayList;
public final class EventListeners {
private static StringBuilder consoleMessageHandler;
private static final Int2ObjectMap<StringBuilder> playerMessageHandlers = new Int2ObjectOpenHashMap<>();
private static MessageHandler consoleMessageHandler;
public static void setConsoleMessageHandler(StringBuilder handler) {
public static void setConsoleMessageHandler(MessageHandler handler) {
consoleMessageHandler = handler;
}
/**
* 获取新的玩家消息处理类
* 获取时将创建或清空消息处理器并返回实例**在执行命令前获取**
* @param uid 玩家uid
* @return 新的玩家消息处理类
*/
public static StringBuilder getPlayerMessageHandler(int uid) {
var handler = playerMessageHandlers.get(uid);
if (handler == null) {
handler = new StringBuilder();
playerMessageHandlers.put(uid, handler);
}
return handler;
}
/**
* 命令执行反馈事件处理
*/
public static void onCommandResponse(ReceiveCommandFeedbackEvent event) {
StringBuilder handler;
if (event.getPlayer() == null) {
handler = consoleMessageHandler;
} else {
handler = playerMessageHandlers.get(event.getPlayer().getUid());
}
if (handler != null) {
if (!handler.isEmpty()) {
// New line
handler.append(System.lineSeparator());
}
handler.append(event.getMessage());
if (consoleMessageHandler != null && event.getPlayer() == null) {
consoleMessageHandler.setMessage(event.getMessage());
}
}
@ -88,10 +57,6 @@ public final class EventListeners {
SocketClient.sendPacket(playerList);
}
/**
* 仅游戏模式下玩家离开事件处理方法
* 用于更新玩家列表
*/
public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) {
PlayerList playerList = new PlayerList();
playerList.player = Grasscutter.getGameServer().getPlayers().size();
@ -105,15 +70,4 @@ public final class EventListeners {
playerList.playerList = playerNames;
SocketClient.sendPacket(playerList);
}
/**
* 玩家离开事件处理 2
* 用于清理内存
*/
public static void onPlayerQuit2(PlayerQuitEvent playerQuitEvent) {
var uid = playerQuitEvent.getPlayer().getUid();
if (playerMessageHandlers.containsKey(uid)) {
playerMessageHandlers.remove(uid);
}
}
}

View File

@ -1,42 +0,0 @@
package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.model.Client;
import java.util.Date;
import java.util.Vector;
/**
* 插件持久化数据
*/
public class OpenCommandData {
/**
* 连接的客户端列表
*/
public Vector<Client> clients = new Vector<>();
/**
* 通过令牌获取客户端
* @param token 令牌
* @return 客户端对象若未找到返回null
*/
public Client getClientByToken(String token) {
for (var c : clients) {
if (c.token.equals(token))
return c;
}
return null;
}
/**
* 移除所有过期的客户端
*/
public void removeExpiredClients() {
var now = new Date();
clients.removeIf(client -> client.tokenExpireTime.before(now));
}
public void addClient(Client client) {
clients.add(client);
}
}

View File

@ -19,12 +19,11 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.MessageHandler;
import emu.grasscutter.utils.Utils;
import io.javalin.Javalin;
import io.javalin.http.Context;
@ -43,23 +42,21 @@ public final class OpenCommandHandler implements Router {
javalin.post("/opencommand/api", OpenCommandHandler::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(Context context) {
var plugin = OpenCommandPlugin.getInstance();
try {
var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
cleanupExpiredData();
var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig();
var now = new Date();
var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) {
int playerId = (int)Double.parseDouble(req.data.toString());
int playerId = (int) req.data;
var player = plugin.getServer().getPlayerByUid(playerId);
if (player == null) {
context.json(new JsonResponse(404, "Player Not Found."));
@ -77,14 +74,15 @@ public final class OpenCommandHandler implements Router {
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);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
clients.put(token, playerId);
player.dropMessage("[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
return;
} else if (req.action.equals("ping")) {
context.json(new JsonResponse(plugin.getVersion()));
context.json(new JsonResponse());
return;
} else if (req.action.equals("online")) {
var p = new ArrayList<String>();
@ -99,8 +97,7 @@ public final class OpenCommandHandler implements Router {
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
if (!isConsole && !clients.containsKey(req.token)) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
@ -114,10 +111,10 @@ public final class OpenCommandHandler implements Router {
synchronized (plugin) {
try {
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
var resultCollector = new StringBuilder();
var resultCollector = new MessageHandler();
EventListeners.setConsoleMessageHandler(resultCollector);
tryInvokeCommand(null, null, req.data.toString());
context.json(new JsonResponse(resultCollector.toString()));
CommandMap.getInstance().invoke(null, null, req.data.toString());
context.json(new JsonResponse(resultCollector.getMessage()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
EventListeners.setConsoleMessageHandler(null);
@ -131,13 +128,12 @@ public final class OpenCommandHandler implements Router {
}
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token) == (int)Double.parseDouble(req.data.toString())) {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L));
context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), context.ip()));
} else {
context.json(new JsonResponse(400, "Verification failed"));
}
@ -146,49 +142,47 @@ public final class OpenCommandHandler implements Router {
} else {
if (req.action.equals("command")) {
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var player = plugin.getServer().getPlayerByUid(client.playerId);
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L));
var playerId = clients.get(req.token);
var player = plugin.getServer().getPlayerByUid(playerId);
var command = req.data.toString();
if (player == null) {
context.json(new JsonResponse(404, "Player not found"));
return;
}
// Player MessageHandler do not support concurrency
var handler = EventListeners.getPlayerMessageHandler(player.getUid());
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (handler) {
synchronized (player) {
try {
handler.setLength(0);
tryInvokeCommand(player, player, req.data.toString());
context.json(new JsonResponse(handler.toString()));
var resultCollector = new MessageHandler();
player.setMessageHandler(resultCollector);
CommandMap.getInstance().invoke(player, player, command);
context.json(new JsonResponse(resultCollector.getMessage()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
} finally {
player.setMessageHandler(null);
}
}
return;
}
}
context.json(new JsonResponse(403, "forbidden"));
} catch (Throwable ex) {
plugin.getLogger().error("[OpenCommand] handler error.", ex);
}
}
private static void tryInvokeCommand(Player sender, Player target, String rawMessage) {
for (var command : rawMessage.split("\n[/!]|\\|")) {
command = command.trim();
if (command.isEmpty()) continue;
if (command.charAt(0) == '/' || command.charAt(0) == '!') {
command = command.substring(1);
}
CommandMap.getInstance().invoke(sender, target, command);
}
}
private static void cleanupExpiredCodes() {
private static void cleanupExpiredData() {
var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
if (codeExpireTime.isEmpty())
codes.clear();
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

@ -19,7 +19,6 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData;
import com.github.jie65535.opencommand.socket.SocketDataWait;
import com.github.jie65535.opencommand.socket.SocketServer;
@ -46,22 +45,21 @@ public final class OpenCommandOnlyHttpHandler implements Router {
javalin.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(Context context) {
var plugin = OpenCommandPlugin.getInstance();
try {
var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
cleanupExpiredData();
var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig();
var now = new Date();
var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) {
int playerId = (int)Double.parseDouble(req.data.toString());
int playerId = (int) req.data;
var player = SocketData.getPlayer(playerId);
if (player == null) {
context.json(new JsonResponse(404, "Player Not Found."));
@ -79,8 +77,9 @@ public final class OpenCommandOnlyHttpHandler implements Router {
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);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
clients.put(token, playerId);
Player.dropMessage(playerId, "[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
@ -99,8 +98,7 @@ public final class OpenCommandOnlyHttpHandler implements Router {
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
if (!isConsole && !clients.containsKey(req.token)) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
@ -132,12 +130,12 @@ public final class OpenCommandOnlyHttpHandler implements Router {
};
SocketServer.sendPacketAndWait(server.ip, new RunConsoleCommand(req.data.toString()), wait);
var packet = wait.getData();
if (packet == null) {
var data = wait.getData();
if (data == null) {
context.json(new JsonResponse(408, "Timeout"));
return;
}
context.json(new JsonResponse(packet.code, packet.message, packet.data));
context.json(new JsonResponse(data.code, data.message, data.data));
return;
} else if (req.action.equals("server")) {
context.json(new JsonResponse(200, "Success", SocketServer.getOnlineClient()));
@ -148,13 +146,12 @@ public final class OpenCommandOnlyHttpHandler implements Router {
}
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token) == (int)Double.parseDouble(req.data.toString())) {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L));
context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), context.ip()));
} else {
context.json(new JsonResponse(400, "Verification failed"));
}
@ -178,14 +175,15 @@ public final class OpenCommandOnlyHttpHandler implements Router {
};
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
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 = client.playerId;
player.uid = playerId;
player.type = PlayerEnum.RunCommand;
player.data = command;
if (!SocketServer.sendUidPacketAndWait(client.playerId, player, socketDataWait)) {
if (!SocketServer.sendUidPacketAndWait(playerId, player, socketDataWait)) {
context.json(new JsonResponse(404, "Player Not Found."));
return;
}
@ -201,15 +199,20 @@ public final class OpenCommandOnlyHttpHandler implements Router {
}
}
context.json(new JsonResponse(403, "forbidden"));
} catch (Exception ex) {
plugin.getLogger().error("[OpenCommand] handler error.", ex);
}
}
private static void cleanupExpiredCodes() {
private static void cleanupExpiredData() {
var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
if (codeExpireTime.isEmpty())
codes.clear();
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

@ -26,13 +26,9 @@ 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 emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class OpenCommandPlugin extends Plugin {
@ -44,8 +40,6 @@ public final class OpenCommandPlugin extends Plugin {
private OpenCommandConfig config;
private OpenCommandData data;
private Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID;
@Override
@ -53,24 +47,16 @@ public final class OpenCommandPlugin extends Plugin {
instance = this;
// 加载配置
loadConfig();
// 加载数据
loadData();
// 启动Socket
startSocket();
}
@Override
public void onEnable() {
// 监听命令执行反馈
new EventHandler<>(ReceiveCommandFeedbackEvent.class)
.priority(HandlerPriority.HIGH)
.listener(EventListeners::onCommandResponse)
.register(this);
// 监听玩家离开事件
new EventHandler<>(PlayerQuitEvent.class)
.priority(HandlerPriority.NORMAL)
.listener(EventListeners::onPlayerQuit2)
.register(this);
if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) {
// 仅运行游戏服务器时注册玩家加入和离开事件
new EventHandler<>(PlayerJoinEvent.class)
@ -86,12 +72,11 @@ public final class OpenCommandPlugin extends Plugin {
} else {
getHandle().addRouter(OpenCommandHandler.class);
}
getLogger().info("[OpenCommand] Enabled. https://github.com/jie65535/gc-opencommand-plugin");
getLogger().info("[OpenCommand] Enabled");
}
@Override
public void onDisable() {
saveData();
getLogger().info("[OpenCommand] Disabled");
}
@ -99,41 +84,10 @@ public final class OpenCommandPlugin extends Plugin {
return config;
}
public OpenCommandData getData() {
return data;
}
private void loadConfig() {
var configFile = new File(getDataFolder(), "config.json");
if (!configFile.exists()) {
config = new OpenCommandConfig();
saveConfig();
} else {
try {
config = JsonUtils.decode(Files.readString(configFile.toPath(), StandardCharsets.UTF_8),
OpenCommandConfig.class);
} catch (Exception exception) {
config = new OpenCommandConfig();
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.");
}
}
// 检查控制台Token
if (config.consoleToken == null || config.consoleToken.isEmpty()) {
config.consoleToken = Utils.base64Encode(Crypto.createSessionKey(24));
saveConfig();
getLogger().warn("Detected that consoleToken is empty, automatically generated Token for you as follows: {}", config.consoleToken);
}
try {
runMode = Grasscutter.getConfig().server.runMode;
} catch (Exception ex) {
getLogger().warn("[OpenCommand] Failed to load server configuration, default HYBRID mode is being used.");
}
}
private void saveConfig() {
var configFile = new File(getDataFolder(), "config.json");
try (var file = new FileWriter(configFile)) {
file.write(JsonUtils.encode(config));
} catch (IOException e) {
@ -141,33 +95,18 @@ public final class OpenCommandPlugin extends Plugin {
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save config file.");
}
}
private void loadData() {
var dataFile = new File(getDataFolder(), "data.json");
if (!dataFile.exists()) {
data = new OpenCommandData();
saveData();
} else {
try {
data = JsonUtils.decode(Files.readString(dataFile.toPath(), StandardCharsets.UTF_8),
OpenCommandData.class);
config = JsonUtils.loadToClass(configFile.getAbsolutePath(), OpenCommandConfig.class);
} catch (Exception exception) {
getLogger().error("[OpenCommand] There was an error while trying to load the data from data.json. Please make sure that there are no syntax errors. If you want to start with a default data, delete your existing data.json.");
}
if (data == null) {
data = new OpenCommandData();
config = new OpenCommandConfig();
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.");
}
}
}
public void saveData() {
try (var file = new FileWriter(new File(getDataFolder(), "data.json"))) {
file.write(JsonUtils.encode(data));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to data file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save data file.");
try {
runMode = Grasscutter.getConfig().server.runMode;
} catch (Exception ex) {
getLogger().warn("[OpenCommand] Failed to load server configuration, default HYBRID mode is being used.");
}
}

View File

@ -1,16 +0,0 @@
package com.github.jie65535.opencommand.model;
import java.util.Date;
public final class Client {
public String token;
public Integer playerId;
public Date tokenExpireTime;
public Client(String token, Integer playerId, Date tokenExpireTime) {
this.token = token;
this.playerId = playerId;
this.tokenExpireTime = tokenExpireTime;
}
}

View File

@ -26,6 +26,7 @@ import com.github.jie65535.opencommand.socket.packet.player.PlayerList;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.MessageHandler;
import org.slf4j.Logger;
import java.io.IOException;
@ -160,16 +161,18 @@ public class SocketClient {
return;
}
// Player MessageHandler do not support concurrency
var handler = EventListeners.getPlayerMessageHandler(playerData.getUid());
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (handler) {
synchronized (playerData) {
try {
handler.setLength(0);
var resultCollector = new MessageHandler();
playerData.setMessageHandler(resultCollector);
CommandMap.getInstance().invoke(playerData, playerData, command);
sendPacket(new HttpPacket(200, handler.toString()), packet.packetID);
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);
}
}
}
@ -189,10 +192,10 @@ public class SocketClient {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (plugin) {
try {
var resultCollector = new StringBuilder();
var resultCollector = new MessageHandler();
EventListeners.setConsoleMessageHandler(resultCollector);
CommandMap.getInstance().invoke(null, null, consoleCommand.command);
sendPacket(new HttpPacket(resultCollector.toString()), packet.packetID);
sendPacket(new HttpPacket(resultCollector.getMessage()), packet.packetID);
} catch (Exception e) {
mLogger.warn("[OpenCommand] Run command failed.", e);
EventListeners.setConsoleMessageHandler(null);

View File

@ -1,8 +1,7 @@
{
"name": "opencommand-plugin",
"description": "Open command interface for third-party clients",
"version": "1.7.0",
"version": "dev-1.4.0",
"mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin",
"authors": ["jie65535", "方块君"],
"api": 2
"authors": ["jie65535", "方块君"]
}