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/src/main/java/com/github/jie65535/opencommand/EventListeners.java b/src/main/java/com/github/jie65535/opencommand/EventListeners.java index 7c0bc6e..0201fe9 100644 --- a/src/main/java/com/github/jie65535/opencommand/EventListeners.java +++ b/src/main/java/com/github/jie65535/opencommand/EventListeners.java @@ -1,8 +1,17 @@ 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; @@ -14,4 +23,33 @@ public final class EventListeners { 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..fad781a 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java @@ -22,4 +22,7 @@ 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"; } 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..8530217 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java @@ -0,0 +1,200 @@ +/* + * 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.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; + } + + // 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")) { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (plugin) { + try { + plugin.getLogger().info(String.format("IP: %s run command in console > %s", request.ip(), req.data)); + var resultCollector = new MessageHandler(); + EventListeners.setConsoleMessageHandler(resultCollector); + CommandMap.getInstance().invoke(null, null, req.data.toString()); + response.json(new JsonResponse(resultCollector.getMessage())); + } catch (Exception e) { + plugin.getLogger().warn("Run command failed.", e); + EventListeners.setConsoleMessageHandler(null); + response.json(new JsonResponse(500, "error", e.getLocalizedMessage())); + } + } + 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() { + response.json(new JsonResponse(408, "Wait server 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", "Server connect failed.")); + 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..4529878 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; @@ -47,7 +51,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"); } @@ -79,5 +97,21 @@ public final class OpenCommandPlugin extends Plugin { 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."); } } + // 启动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("Unable to start socket server.", e); + } + } } } 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..cf0fc14 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -0,0 +1,185 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandConfig; +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.HeartBeat; +import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.Packet; +import com.github.jie65535.opencommand.socket.packet.player.Player; +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.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 void connectServer() { + 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); + } + + // 发送数据包 + 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 ReceiveThread extends Thread { + private InputStream is; + private String token; + + public ReceiveThread(Socket socket) { + token = OpenCommandPlugin.getInstance().getConfig().socketToken; + try { + is = socket.getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + @Override + public void run() { + //noinspection InfiniteLoopStatement + while (true) { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + 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("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; + } + } + } + } + } + + // 客户端连接线程 + 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 { + socket = new Socket(ip, port); + connect = true; + os = socket.getOutputStream(); + mLogger.info("Connect to server: " + ip + ":" + port); + new ReceiveThread(socket); + } catch (IOException e) { + connect = false; + mLogger.warn("Connect to server failed: " + ip + ":" + port); + mLogger.warn("[OpenCommand] Reconnect to server"); + connectServer(); + throw new RuntimeException(e); + } + } + } +} 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..74affcd --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java @@ -0,0 +1,30 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +// Socket 数据保存 +public class SocketData { + public static HashMap playerList = new HashMap<>(); + + public static PlayerList getPlayer(int uid) { + for (PlayerList player : playerList.values()) { + if (player.playerMap.get(uid) != null) { + return player; + } + } + 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(); + } +} 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..3612cc5 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -0,0 +1,204 @@ +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.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.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; + +// 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 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.sendPacket(p)) { + 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.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("Client {} timeout, disconnect.", clientID); + clientList.remove(clientID); + clientTimeout.remove(clientID); + } else { + clientTimeout.put(clientID, clientTime + 500); + } + } + } + } + + // 客户端数据包处理 + private static class ClientThread extends Thread { + private final Socket socket; + private InputStream is; + private OutputStream os; + private final String address; + private final String token; + + 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) { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + 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("HttpPacket: " + packet.packetID + " not found"); + return; + } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); + } + } + } + } + } + } + + // 等待客户端连接 + 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("Start socket server on port " + socketServer.getLocalPort()); + // noinspection InfiniteLoopStatement + while (true) { + try { + Socket accept = socketServer.accept(); + String address = accept.getInetAddress() + ":" + accept.getPort(); + mLogger.info("Client connect: " + address); + ClientThread clientThread = new ClientThread(accept); + clientList.put(address, clientThread); + clientTimeout.put(address, 0); + } 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..74a5faa --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java @@ -0,0 +1,139 @@ +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.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + 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.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + 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.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + 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/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..5cc5a46 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java @@ -0,0 +1,36 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +// http返回数据 +public class HttpPacket extends BasePacket { + public int code; + public String message; + 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; + } + + @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..bc45524 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java @@ -0,0 +1,14 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包结构 +public class Packet { + public String token; + public PacketEnum type; + public String data; + public String packetID; + + @Override + public String toString() { + return "Packet [token=" + token + ", 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..128be71 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java @@ -0,0 +1,9 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包类型列表 +public enum PacketEnum { + PlayerList, + Player, + HttpPacket, + HeartBeat +} 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; + } +}