├── gc-plugin ├── .gitignore ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── mojo │ │ │ └── consoleplus │ │ │ ├── forms │ │ │ ├── RequestAuth.java │ │ │ ├── RequestJson.java │ │ │ ├── ResponseAuth.java │ │ │ └── ResponseJson.java │ │ │ ├── socket │ │ │ ├── packet │ │ │ │ ├── player │ │ │ │ │ ├── PlayerEnum.java │ │ │ │ │ ├── Player.java │ │ │ │ │ └── PlayerList.java │ │ │ │ ├── BasePacket.java │ │ │ │ ├── PacketEnum.java │ │ │ │ ├── Packet.java │ │ │ │ ├── HeartBeat.java │ │ │ │ ├── SignaturePacket.java │ │ │ │ ├── AuthPacket.java │ │ │ │ ├── OtpPacket.java │ │ │ │ └── HttpPacket.java │ │ │ ├── SocketData.java │ │ │ ├── SocketDataWait.java │ │ │ ├── SocketUtils.java │ │ │ ├── SocketClient.java │ │ │ └── SocketServer.java │ │ │ ├── AuthHandler.java │ │ │ ├── EventListeners.java │ │ │ ├── config │ │ │ └── MojoConfig.java │ │ │ ├── ConsolePlus.java │ │ │ ├── RequestHandler.java │ │ │ ├── RequestOnlyHttpHandler.java │ │ │ └── command │ │ │ └── PluginCommand.java │ │ └── resources │ │ └── plugin.json.tmpl └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── .gitignore ├── .github └── workflows │ └── build.yml ├── gradlew.bat ├── README-zh.md ├── README.md └── gradlew /gc-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | lib/grasscutter-*.jar 2 | mojoconsole.jar 3 | bin -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mojoconsole-plus' 2 | 3 | include 'gc-plugin' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gc-mojoconsole/gc-mojoconsole-backend/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/forms/RequestAuth.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.forms; 2 | 3 | public class RequestAuth { 4 | public int uid; 5 | public String otp; 6 | } 7 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerEnum.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet.player; 2 | 3 | // 玩家操作列表 4 | public enum PlayerEnum { 5 | DropMessage, 6 | RunCommand 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/forms/RequestJson.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.forms; 2 | 3 | public final class RequestJson { 4 | public String k; 5 | public String k2; 6 | public String request = ""; 7 | public String payload = ""; 8 | } 9 | -------------------------------------------------------------------------------- /gc-plugin/src/main/resources/plugin.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mojoconsole-plus", 3 | "description": "Grasscutter In Game Web Based Console", 4 | "version": "{{VERSION}}", 5 | 6 | "mainClass": "com.mojo.consoleplus.ConsolePlus", 7 | "authors": ["mingjun97"] 8 | } -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/BasePacket.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | // 基本数据包 4 | public abstract class BasePacket { 5 | public abstract String getPacket(); 6 | 7 | public abstract PacketEnum getType(); 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | #idea 5 | *.idea 6 | 7 | # Ignore Gradle build output directory 8 | build 9 | .vscode 10 | .DS_Store 11 | 12 | # Ignore lib 13 | gc-plugin/lib/ 14 | 15 | gc-plugin/src/main/resources/plugin.json -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/PacketEnum.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | // 数据包类型列表 4 | public enum PacketEnum { 5 | PlayerList, 6 | Player, 7 | HttpPacket, 8 | HeartBeat, 9 | Signature, 10 | OtpPacket, 11 | AuthPacket 12 | } 13 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/Packet.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | // 数据包结构 4 | public class Packet { 5 | public PacketEnum type; 6 | public String data; 7 | public String packetID; 8 | 9 | @Override 10 | public String toString() { 11 | return "Packet [type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HeartBeat.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | import emu.grasscutter.Grasscutter; 4 | 5 | // 心跳包 6 | public class HeartBeat extends BasePacket { 7 | public String ping; 8 | 9 | public HeartBeat(String ping) { 10 | this.ping = ping; 11 | } 12 | 13 | @Override 14 | public String getPacket() { 15 | return Grasscutter.getGsonFactory().toJson(this); 16 | } 17 | 18 | @Override 19 | public PacketEnum getType() { 20 | return PacketEnum.HeartBeat; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/SignaturePacket.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | import emu.grasscutter.Grasscutter; 4 | 5 | public class SignaturePacket extends BasePacket { 6 | public String signature; 7 | 8 | public SignaturePacket(String signature) { 9 | this.signature = signature; 10 | } 11 | 12 | @Override 13 | public String getPacket() { 14 | return Grasscutter.getGsonFactory().toJson(this); 15 | } 16 | 17 | @Override 18 | public PacketEnum getType() { 19 | return PacketEnum.Signature; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/forms/ResponseAuth.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.forms; 2 | 3 | public class ResponseAuth { 4 | int code = 0; 5 | String message = ""; 6 | String key = ""; 7 | 8 | public ResponseAuth(int code, String message, String key) { 9 | this.code = code; 10 | this.message = message; 11 | this.key = key; 12 | } 13 | 14 | public int getCode() { 15 | return code; 16 | } 17 | 18 | public String getMessage() { 19 | return message; 20 | } 21 | 22 | public String getKey() { 23 | return key; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/AuthPacket.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | import com.google.gson.GsonBuilder; 4 | 5 | import static com.mojo.consoleplus.ConsolePlus.gson; 6 | 7 | public class AuthPacket extends BasePacket { 8 | public String token; 9 | 10 | public AuthPacket(String token) { 11 | this.token = token; 12 | } 13 | 14 | @Override 15 | public String getPacket() { 16 | return gson.toJson(this); 17 | } 18 | 19 | @Override 20 | public PacketEnum getType() { 21 | return PacketEnum.AuthPacket; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/OtpPacket.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | import emu.grasscutter.Grasscutter; 4 | 5 | public class OtpPacket extends BasePacket { 6 | public int uid; 7 | public String otp; 8 | public long expire; 9 | public Boolean api; 10 | public String key; 11 | 12 | public boolean remove = false; 13 | 14 | public OtpPacket(int uid, String opt, long expire, Boolean api) { 15 | this.uid = uid; 16 | this.expire = expire; 17 | this.api = api; 18 | this.otp = opt; 19 | } 20 | 21 | public OtpPacket(String opt) { 22 | this.otp = opt; 23 | remove = true; 24 | } 25 | 26 | @Override 27 | public String getPacket() { 28 | return Grasscutter.getGsonFactory().toJson(this); 29 | } 30 | 31 | @Override 32 | public PacketEnum getType() { 33 | return PacketEnum.OtpPacket; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/Player.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet.player; 2 | 3 | import com.mojo.consoleplus.socket.SocketServer; 4 | import com.mojo.consoleplus.socket.packet.BasePacket; 5 | import com.mojo.consoleplus.socket.packet.PacketEnum; 6 | import emu.grasscutter.Grasscutter; 7 | 8 | import static com.mojo.consoleplus.ConsolePlus.gson; 9 | 10 | // 玩家操作类 11 | public class Player extends BasePacket { 12 | public PlayerEnum type; 13 | public int uid; 14 | public String data; 15 | 16 | @Override 17 | public String getPacket() { 18 | return gson.toJson(this); 19 | } 20 | 21 | @Override 22 | public PacketEnum getType() { 23 | return PacketEnum.Player; 24 | } 25 | 26 | public static void dropMessage(int uid, String str) { 27 | Player p = new Player(); 28 | p.type = PlayerEnum.DropMessage; 29 | p.uid = uid; 30 | p.data = str; 31 | SocketServer.sendAllPacket(p); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HttpPacket.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet; 2 | 3 | import emu.grasscutter.Grasscutter; 4 | 5 | // http返回数据 6 | public class HttpPacket extends BasePacket { 7 | public int code; 8 | public String message; 9 | public String data; 10 | 11 | public HttpPacket(int code, String message, String data) { 12 | this.code = code; 13 | this.message = message; 14 | this.data = data; 15 | } 16 | 17 | public HttpPacket(int code, String message) { 18 | this.code = code; 19 | this.message = message; 20 | } 21 | 22 | @Override 23 | public String getPacket() { 24 | return Grasscutter.getGsonFactory().toJson(this); 25 | } 26 | 27 | @Override 28 | public PacketEnum getType() { 29 | return PacketEnum.HttpPacket; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "HttpPacket [code=" + code + ", message=" + message + ", data=" + data + "]"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerList.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket.packet.player; 2 | 3 | import com.mojo.consoleplus.socket.packet.BasePacket; 4 | import com.mojo.consoleplus.socket.packet.PacketEnum; 5 | import emu.grasscutter.Grasscutter; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | // 玩家列表信息 13 | public class PlayerList extends BasePacket { 14 | public int player = -1; 15 | public List playerList = new ArrayList<>(); 16 | public Map playerMap = new HashMap<>(); 17 | 18 | @Override 19 | public String getPacket() { 20 | return Grasscutter.getGsonFactory().toJson(this); 21 | } 22 | 23 | @Override 24 | public PacketEnum getType() { 25 | return PacketEnum.PlayerList; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "PlayerList [player=" + player + ", playerList=" + playerList + ", playerMap=" + playerMap + "]"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/forms/ResponseJson.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.forms; 2 | 3 | public final class ResponseJson { 4 | public String message = "success"; 5 | public int code = 200; 6 | public String payload = ""; 7 | 8 | public ResponseJson(String message, int code) { 9 | this.message = message; 10 | this.code = code; 11 | } 12 | 13 | public ResponseJson(String message, int code, String payload) { 14 | this.message = message; 15 | this.code = code; 16 | this.payload = payload; 17 | } 18 | 19 | public String getMessage() { 20 | return message; 21 | } 22 | 23 | public void setMessage(String message) { 24 | this.message = message; 25 | } 26 | 27 | public int getCode() { 28 | return code; 29 | } 30 | 31 | public void setCode(int code) { 32 | this.code = code; 33 | } 34 | 35 | public String getPayload() { 36 | return payload; 37 | } 38 | 39 | public void setPayload(String payload) { 40 | this.payload = payload; 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketData.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket; 2 | 3 | import com.mojo.consoleplus.socket.packet.OtpPacket; 4 | import com.mojo.consoleplus.socket.packet.player.PlayerList; 5 | import org.luaj.vm2.ast.Str; 6 | 7 | import java.util.HashMap; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | // Socket 数据保存 11 | public class SocketData { 12 | public static HashMap playerList = new HashMap<>(); 13 | 14 | public static HashMap tickets = new HashMap<>(); 15 | 16 | public static String getPlayer(int uid) { 17 | for (PlayerList player : playerList.values()) { 18 | if (player.playerMap.get(uid) != null) { 19 | return player.playerMap.get(uid); 20 | } 21 | } 22 | return null; 23 | } 24 | 25 | public static String getPlayerInServer(int uid) { 26 | AtomicReference ret = new AtomicReference<>(); 27 | playerList.forEach((key, value) -> { 28 | if (value.playerMap.get(uid) != null) { 29 | ret.set(key); 30 | } 31 | }); 32 | return ret.get(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketDataWait.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket; 2 | 3 | // 异步等待数据返回 4 | public abstract class SocketDataWait extends Thread { 5 | public T data; 6 | public long timeout; 7 | public long time; 8 | public String uid; 9 | 10 | /** 11 | * 异步等待数据返回 12 | * @param timeout 超时时间 13 | */ 14 | public SocketDataWait(long timeout) { 15 | this.timeout = timeout; 16 | start(); 17 | } 18 | 19 | public abstract void run(); 20 | 21 | /** 22 | * 数据处理 23 | * @param data 数据 24 | * @return 处理后的数据 25 | */ 26 | public abstract T initData(T data); 27 | 28 | /** 29 | * 超时回调 30 | */ 31 | public abstract void timeout(); 32 | 33 | /** 34 | * 异步设置数据 35 | * @param data 数据 36 | */ 37 | public void setData(Object data) { 38 | this.data = initData((T) data); 39 | } 40 | 41 | /** 42 | * 获取异步数据(此操作会一直堵塞直到获取到数据) 43 | * @return 数据 44 | */ 45 | public T getData() { 46 | while (data == null) { 47 | try { 48 | time += 100; 49 | Thread.sleep(100); 50 | } catch (InterruptedException e) { 51 | e.printStackTrace(); 52 | } 53 | if (time > timeout) { 54 | timeout(); 55 | return null; 56 | } 57 | } 58 | return data; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/AuthHandler.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus; 2 | import java.security.MessageDigest; 3 | import java.util.UUID; 4 | 5 | public class AuthHandler { 6 | public static String signatureStub; 7 | 8 | public AuthHandler(){ 9 | try { 10 | signatureStub = UUID.randomUUID().toString(); 11 | } catch (Exception e) { 12 | e.printStackTrace(); 13 | } 14 | } 15 | 16 | public AuthHandler(String stub) { 17 | signatureStub = stub; 18 | } 19 | 20 | public String getSignature() { 21 | return signatureStub; 22 | } 23 | 24 | public void setSignature(String signature) { 25 | signatureStub = signature; 26 | } 27 | 28 | public Boolean auth(int uid, long expire, String dg) { 29 | return digestUid(uid+":"+expire).equals(dg); 30 | } 31 | 32 | public String genKey(int uid, long expire){ 33 | String part1 = uid +":"+expire; 34 | 35 | return part1 + ":" + digestUid(part1); 36 | } 37 | 38 | private String digestUid(String payload) { 39 | MessageDigest digest; 40 | try { 41 | digest = MessageDigest.getInstance("SHA-256"); 42 | return bytesToHex(digest.digest((payload + ":" + signatureStub).getBytes("UTF-8"))); 43 | } catch (Exception e) { 44 | e.printStackTrace(); 45 | return ""; 46 | } 47 | } 48 | 49 | private static String bytesToHex(byte[] hash) { 50 | StringBuilder hexString = new StringBuilder(2 * hash.length); 51 | for (int i = 0; i < hash.length; i++) { 52 | String hex = Integer.toHexString(0xff & hash[i]); 53 | if(hex.length() == 1) { 54 | hexString.append('0'); 55 | } 56 | hexString.append(hex); 57 | } 58 | return hexString.toString(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | workflow_dispatch: ~ 4 | push: 5 | paths: 6 | - "**.java" 7 | branches: 8 | - "main" 9 | pull_request: 10 | paths: 11 | - "**.java" 12 | types: 13 | - opened 14 | - synchronize 15 | - reopened 16 | jobs: 17 | Build-Server-Jar: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | - name: Setup Java 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: temurin 26 | java-version: '17' 27 | - name: Cache gradle files 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.gradle/caches 32 | ~/.gradle/wrapper 33 | ./.gradle/loom-cache 34 | key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }} 35 | restore-keys: | 36 | ${{ runner.os }}-gradle- 37 | - name: Download latest grasscutter jar 38 | run: wget https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip && mkdir gc-plugin/lib && unzip Grasscutter.zip -d gc-plugin/lib 39 | - name: Run Gradle 40 | run: ./gradlew build 41 | - name: Upload build 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: mojoconsole 45 | path: gc-plugin/mojoconsole.jar 46 | 47 | - name: Automatic create a pre-relase 48 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 49 | uses: "marvinpinto/action-automatic-releases@latest" 50 | with: 51 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 52 | automatic_release_tag: "latest" 53 | prerelease: true 54 | title: "Development Build" 55 | files: | 56 | gc-plugin/mojoconsole.jar -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/EventListeners.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus; 2 | 3 | import com.mojo.consoleplus.socket.SocketClient; 4 | import com.mojo.consoleplus.socket.packet.player.PlayerList; 5 | import emu.grasscutter.Grasscutter; 6 | import emu.grasscutter.game.player.Player; 7 | import emu.grasscutter.server.event.player.PlayerJoinEvent; 8 | import emu.grasscutter.server.event.player.PlayerQuitEvent; 9 | 10 | import java.util.ArrayList; 11 | 12 | public class EventListeners { 13 | public static void onPlayerJoin(PlayerJoinEvent playerJoinEvent) { 14 | PlayerList playerList = new PlayerList(); 15 | playerList.player = Grasscutter.getGameServer().getPlayers().size(); 16 | ArrayList playerNames = new ArrayList<>(); 17 | playerNames.add(playerJoinEvent.getPlayer().getNickname()); 18 | playerList.playerMap.put(playerJoinEvent.getPlayer().getUid(), playerJoinEvent.getPlayer().getNickname()); 19 | for (Player player : Grasscutter.getGameServer().getPlayers().values()) { 20 | playerNames.add(player.getNickname()); 21 | playerList.playerMap.put(player.getUid(), player.getNickname()); 22 | } 23 | playerList.playerList = playerNames; 24 | SocketClient.sendPacket(playerList); 25 | } 26 | 27 | public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) { 28 | PlayerList playerList = new PlayerList(); 29 | playerList.player = Grasscutter.getGameServer().getPlayers().size(); 30 | ArrayList playerNames = new ArrayList<>(); 31 | for (Player player : Grasscutter.getGameServer().getPlayers().values()) { 32 | playerNames.add(player.getNickname()); 33 | playerList.playerMap.put(player.getUid(), player.getNickname()); 34 | } 35 | playerList.playerMap.remove(playerQuitEvent.getPlayer().getUid()); 36 | playerNames.remove(playerQuitEvent.getPlayer().getNickname()); 37 | playerList.playerList = playerNames; 38 | SocketClient.sendPacket(playerList); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /gc-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java project to get you started. 5 | * For more details take a look at the Java Quickstart chapter in the Gradle 6 | * User Manual available at https://docs.gradle.org/5.6.3/userguide/tutorial_java_projects.html 7 | */ 8 | buildscript { 9 | 10 | } 11 | 12 | plugins { 13 | // Apply the java plugin to add support for Java 14 | id 'java' 15 | 16 | id 'idea' 17 | } 18 | 19 | sourceCompatibility = 17 20 | targetCompatibility = 17 21 | 22 | def version_tag = "dev-1.5.0" 23 | 24 | repositories { 25 | mavenCentral() 26 | } 27 | 28 | repositories { 29 | // Use Maven Central for resolving dependencies. 30 | mavenCentral() 31 | 32 | maven { 33 | url "https://repo.spring.io/release" 34 | } 35 | 36 | maven { 37 | url "https://s01.oss.sonatype.org/content/groups/public/" 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'lib', include: ['*.jar']) 43 | 44 | implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' 45 | 46 | implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.6' 47 | 48 | //implementation group: 'tech.xigam', name: 'grasscutter', version: '1.0.2-dev' 49 | 50 | implementation 'io.jsonwebtoken:jjwt-api:0.11.3' 51 | runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.3', 'io.jsonwebtoken:jjwt-gson:0.11.3' 52 | 53 | } 54 | 55 | 56 | task injectGitHash { 57 | def gitCommitHash = { 58 | try { 59 | return 'git rev-parse --verify --short HEAD'.execute().text.trim() 60 | } catch (e) { 61 | return "GIT_NOT_FOUND" 62 | } 63 | } 64 | def pluginJson = { 65 | return new File(projectDir, "src/main/resources/plugin.json.tmpl").text.replace("{{VERSION}}", "${version_tag}-${gitCommitHash()}") 66 | } 67 | new File(projectDir, "src/main/resources/plugin.json").text = pluginJson() 68 | } 69 | 70 | jar { 71 | jar.baseName = 'mojoconsole' 72 | 73 | destinationDir = file(".") 74 | } 75 | 76 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/config/MojoConfig.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.config; 2 | 3 | import java.io.File; 4 | import java.nio.charset.StandardCharsets; 5 | import java.nio.file.Files; 6 | 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | 10 | import emu.grasscutter.Grasscutter; 11 | 12 | public class MojoConfig { 13 | 14 | public boolean UseCDN = false; 15 | public String CDNLink = "https://gc-mojoconsole.github.io/"; 16 | public String interfacePath = "/mojoplus/console.html"; 17 | public String responseMessage = "[MojoConsole] Link sent to your mailbox, check it!"; 18 | public String responseMessageThird = "[MojoConsole] You are trying to obtain link for third-party user, please ask him/her send \"/mojo {{OTP}}\" to server in-game"; 19 | public String responseMessageError = "[MojoConsole] Invalid argument."; 20 | public String responseMessageSuccess = "[MojoConsole] Success!"; 21 | public String socketToken = ""; 22 | public int socketPort = 7812; 23 | public String socketHost = "127.0.0.1"; 24 | 25 | static public class MailTemplate { 26 | public String title = "Mojo Console Link"; 27 | public String author = "Mojo Console"; 28 | public String content = "Here is your mojo console link: {{ LINK }}\n" + 29 | "Note that the link will expire in some time, you may retrieve a new one after that."; 30 | public int expireHour = 3; 31 | }; 32 | 33 | public MailTemplate mail = new MailTemplate(); 34 | 35 | static String getConfigPath(){ 36 | try{ 37 | String result = new File(MojoConfig.class.getProtectionDomain().getCodeSource().getLocation() 38 | .toURI()).getParent() + "/mojoconfig.json"; 39 | return result; 40 | } catch (Exception e){ 41 | e.printStackTrace(); 42 | return ""; 43 | } 44 | } 45 | 46 | public static MojoConfig loadConfig(){ 47 | String configPath = getConfigPath(); 48 | File configFile = new File(configPath); 49 | Gson gson = new Gson(); 50 | 51 | try{ 52 | String s = Files.readString(configFile.toPath(), StandardCharsets.UTF_8); 53 | MojoConfig config = gson.fromJson(s, MojoConfig.class); 54 | config.saveConfig(); 55 | return config; 56 | } catch (Exception e) { 57 | MojoConfig config = new MojoConfig(); 58 | config.saveConfig(); 59 | return config; 60 | } 61 | } 62 | 63 | public void saveConfig(){ 64 | String configPath = getConfigPath(); 65 | File configFile = new File(configPath); 66 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 67 | try{ 68 | Files.writeString(configFile.toPath(), gson.toJson(this, MojoConfig.class)); 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | Grasscutter.getLogger().error("[Mojoconsole] Config save failed!"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketUtils.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket; 2 | 3 | import com.mojo.consoleplus.ConsolePlus; 4 | import com.mojo.consoleplus.socket.packet.BasePacket; 5 | import com.mojo.consoleplus.socket.packet.Packet; 6 | import emu.grasscutter.Grasscutter; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | // Socket 工具类 16 | public class SocketUtils { 17 | 18 | /** 19 | * 获取打包后的数据包 20 | * @param bPacket 数据包 21 | * @return 打包后的数据包 22 | */ 23 | public static String getPacket(BasePacket bPacket) { 24 | Packet packet = new Packet(); 25 | packet.type = bPacket.getType(); 26 | packet.data = bPacket.getPacket(); 27 | packet.packetID = UUID.randomUUID().toString(); 28 | return Grasscutter.getGsonFactory().toJson(packet); 29 | } 30 | 31 | /** 32 | * 获取打包后的数据包 33 | * @param bPacket BasePacket 34 | * @return list[0] 是包ID, list[1] 是数据包 35 | */ 36 | public static List getPacketAndPackID(BasePacket bPacket) { 37 | Packet packet = new Packet(); 38 | packet.type = bPacket.getType(); 39 | packet.data = bPacket.getPacket(); 40 | packet.packetID = UUID.randomUUID().toString(); 41 | 42 | List list = new ArrayList<>(); 43 | list.add(packet.packetID); 44 | list.add(Grasscutter.getGsonFactory().toJson(packet)); 45 | return list; 46 | } 47 | 48 | /** 49 | * 获取打包后的数据包 50 | * @param bPacket 数据包 51 | * @param packetID 数据包ID 52 | * @return 打包后的数据包 53 | */ 54 | public static String getPacketAndPackID(BasePacket bPacket, String packetID) { 55 | Packet packet = new Packet(); 56 | packet.type = bPacket.getType(); 57 | packet.data = bPacket.getPacket(); 58 | packet.packetID = packetID; 59 | return Grasscutter.getGsonFactory().toJson(packet); 60 | } 61 | 62 | /** 63 | * 读整数 64 | * @param is 输入流 65 | * @return 整数 66 | */ 67 | public static int readInt(InputStream is) { 68 | int[] values = new int[4]; 69 | try { 70 | for (int i = 0; i < 4; i++) { 71 | values[i] = is.read(); 72 | } 73 | } catch (IOException e) { 74 | e.printStackTrace(); 75 | } 76 | 77 | return values[0]<<24 | values[1]<<16 | values[2]<<8 | values[3]; 78 | } 79 | 80 | /** 81 | * 写整数 82 | * @param os 输出流 83 | * @param value 整数 84 | */ 85 | public static void writeInt(OutputStream os, int value) { 86 | int[] values = new int[4]; 87 | values[0] = (value>>24)&0xFF; 88 | values[1] = (value>>16)&0xFF; 89 | values[2] = (value>>8)&0xFF; 90 | values[3] = (value)&0xFF; 91 | 92 | try{ 93 | for (int i = 0; i < 4; i++) { 94 | os.write(values[i]); 95 | } 96 | }catch (IOException e){ 97 | e.printStackTrace(); 98 | } 99 | } 100 | 101 | /** 102 | * 读字符串 103 | * @param is 输入流 104 | * @return 字符串 105 | */ 106 | public static String readString(InputStream is) { 107 | int len = readInt(is); 108 | byte[] sByte = new byte[len]; 109 | try { 110 | is.read(sByte); 111 | } catch (IOException e) { 112 | e.printStackTrace(); 113 | } 114 | String s = new String(sByte); 115 | return s; 116 | } 117 | 118 | /** 119 | * 写字符串 120 | * @param os 输出流 121 | * @param s 字符串 122 | * @return 是否成功 123 | */ 124 | public static boolean writeString(OutputStream os,String s) { 125 | try { 126 | byte[] bytes = s.getBytes(); 127 | int len = bytes.length; 128 | writeInt(os,len); 129 | os.write(bytes); 130 | return true; 131 | } catch (IOException e) { 132 | e.printStackTrace(); 133 | } 134 | return false; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/ConsolePlus.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.mojo.consoleplus.command.PluginCommand; 6 | import com.mojo.consoleplus.config.MojoConfig; 7 | import com.mojo.consoleplus.socket.SocketClient; 8 | import com.mojo.consoleplus.socket.SocketServer; 9 | import emu.grasscutter.Grasscutter; 10 | import emu.grasscutter.command.CommandMap; 11 | import emu.grasscutter.plugin.Plugin; 12 | import emu.grasscutter.plugin.PluginConfig; 13 | import emu.grasscutter.server.event.EventHandler; 14 | import emu.grasscutter.server.event.HandlerPriority; 15 | import emu.grasscutter.server.event.player.PlayerJoinEvent; 16 | import emu.grasscutter.server.event.player.PlayerQuitEvent; 17 | import io.javalin.http.staticfiles.Location; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.File; 21 | import java.io.InputStream; 22 | import java.io.InputStreamReader; 23 | 24 | import static emu.grasscutter.config.Configuration.HTTP_POLICIES; 25 | import static emu.grasscutter.config.Configuration.PLUGIN; 26 | 27 | public class ConsolePlus extends Plugin { 28 | public static MojoConfig config = MojoConfig.loadConfig(); 29 | public static String versionTag; 30 | public static AuthHandler authHandler; 31 | public static Gson gson = new GsonBuilder().setPrettyPrinting().create(); 32 | 33 | @Override 34 | public void onLoad() { 35 | try (InputStream in = getClass().getResourceAsStream("/plugin.json"); 36 | BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { 37 | Gson gson = new Gson(); 38 | PluginConfig pluginConfig = gson.fromJson(reader, PluginConfig.class); 39 | this.getLogger().info("[MojoConsole] loaded!"); 40 | versionTag = pluginConfig.version; 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | // Use resource 45 | } 46 | 47 | 48 | @Override 49 | public void onEnable() { 50 | String folder_name = PLUGIN("mojoconsole/"); 51 | File folder = new File(folder_name); 52 | if (!folder.exists()) { 53 | Grasscutter.getLogger().warn("To make mojo console works, you have to put your frontend file(console.html) inside" + folder.getAbsolutePath()); 54 | folder.mkdirs(); 55 | } 56 | if (!config.UseCDN) { 57 | Grasscutter.getHttpServer().getHandle()._conf.addStaticFiles(staticFileConfig -> { 58 | staticFileConfig.hostedPath = "/mojoplus"; 59 | staticFileConfig.directory = folder_name; 60 | staticFileConfig.location = Location.EXTERNAL; 61 | }); 62 | } else { 63 | if (!HTTP_POLICIES.cors.enabled) { 64 | Grasscutter.getLogger().error("[MojoConsole] You enabled the useCDN option, in this option, you have to configure Grasscutter accept CORS request. See `config.json`->`server`->`policies`->`cors`."); 65 | return; 66 | } 67 | } 68 | 69 | authHandler = new AuthHandler(); 70 | 71 | if (Grasscutter.config.server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { 72 | SocketServer.startServer(getLogger()); 73 | Grasscutter.getHttpServer().addRouter(RequestOnlyHttpHandler.class); 74 | } else if (Grasscutter.config.server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { 75 | SocketClient.connectServer(getLogger()); 76 | new EventHandler<>(PlayerJoinEvent.class) 77 | .priority(HandlerPriority.HIGH) 78 | .listener(EventListeners::onPlayerJoin) 79 | .register(this); 80 | new EventHandler<>(PlayerQuitEvent.class) 81 | .priority(HandlerPriority.HIGH) 82 | .listener(EventListeners::onPlayerQuit) 83 | .register(this); 84 | } else { 85 | Grasscutter.getHttpServer().addRouter(RequestHandler.class); 86 | } 87 | CommandMap.getInstance().registerCommand("mojoconsole", new PluginCommand()); 88 | this.getLogger().info("[MojoConsole] enabled. Version: " + versionTag); 89 | } 90 | 91 | @Override 92 | public void onDisable() { 93 | CommandMap.getInstance().unregisterCommand("mojoconsole"); 94 | this.getLogger().info("[MojoConsole] Mojoconsole Disabled"); 95 | } 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus; 2 | 3 | import com.mojo.consoleplus.command.PluginCommand; 4 | import com.mojo.consoleplus.forms.RequestAuth; 5 | import com.mojo.consoleplus.forms.RequestJson; 6 | import com.mojo.consoleplus.forms.ResponseAuth; 7 | import com.mojo.consoleplus.forms.ResponseJson; 8 | import emu.grasscutter.Grasscutter; 9 | import emu.grasscutter.command.CommandMap; 10 | import emu.grasscutter.game.player.Player; 11 | import emu.grasscutter.server.http.Router; 12 | import emu.grasscutter.utils.MessageHandler; 13 | import io.javalin.Javalin; 14 | import io.javalin.http.Context; 15 | 16 | import java.io.IOException; 17 | import java.text.DecimalFormat; 18 | import java.util.Map; 19 | import java.util.Random; 20 | 21 | import static java.lang.Integer.parseInt; 22 | import static java.lang.Long.parseLong; 23 | 24 | 25 | public final class RequestHandler implements Router { 26 | // private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); 27 | 28 | @Override public void applyRoutes(Javalin javalin) { 29 | javalin.post("/mojoplus/api", RequestHandler::processRequest); 30 | javalin.post("/mojoplus/auth", RequestHandler::requestKey); 31 | } 32 | 33 | 34 | public static void processRequest(Context context) { 35 | RequestJson request = context.bodyAsClass(RequestJson.class); 36 | Player player = null; 37 | 38 | if (request.k2 != null) { // version 2 token 39 | int uid; 40 | long expire; 41 | String hashDigest; 42 | uid = parseInt(request.k2.split(":")[0]); 43 | expire = parseLong(request.k2.split(":")[1]); 44 | hashDigest = request.k2.split(":")[2]; 45 | if (ConsolePlus.authHandler.auth(uid, expire, hashDigest)){ 46 | Map playersMap = Grasscutter.getGameServer().getPlayers(); 47 | for (int playerid: playersMap.keySet()) { 48 | if (playersMap.get(playerid).getUid() == uid) { 49 | player = playersMap.get(playerid); 50 | } 51 | } 52 | } 53 | } 54 | 55 | if (player != null) { 56 | MessageHandler resultCollector = new MessageHandler(); 57 | player.setMessageHandler(resultCollector); // hook the message 58 | switch (request.request){ 59 | case "invoke": 60 | try{ 61 | // TODO: Enable execut commands to third party 62 | CommandMap.getInstance().invoke(player, player, request.payload); 63 | } catch (Exception e) { 64 | context.json(new ResponseJson("error", 500, e.getStackTrace().toString())); 65 | break; 66 | } 67 | case "ping": 68 | // res.json(new ResponseJson("success", 200)); 69 | context.json(new ResponseJson("success", 200, resultCollector.getMessage())); 70 | break; 71 | default: 72 | context.json(new ResponseJson("400 Bad Request", 400)); 73 | break; 74 | } 75 | player.setMessageHandler(null); 76 | return; 77 | } 78 | 79 | context.json(new ResponseJson("403 Forbidden", 403)); 80 | } 81 | 82 | public static void requestKey(Context context) throws IOException { 83 | RequestAuth request = context.bodyAsClass(RequestAuth.class); 84 | if (request.otp != null && !request.otp.equals("")) { 85 | if (PluginCommand.getInstance().tickets.get(request.otp) == null) { 86 | context.json(new ResponseAuth(404, "Not found", null)); 87 | return; 88 | } 89 | String key = PluginCommand.getInstance().tickets.get(request.otp).key; 90 | if (key == null){ 91 | context.json(new ResponseAuth(403, "Not ready yet", null)); 92 | } else { 93 | PluginCommand.getInstance().tickets.remove(request.otp); 94 | context.json(new ResponseAuth(200, "", key)); 95 | } 96 | } else if (request.uid != 0) { 97 | String otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 98 | while (PluginCommand.getInstance().tickets.containsKey(otp)){ 99 | otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 100 | } 101 | Map playersMap = Grasscutter.getGameServer().getPlayers(); 102 | Player targetPlayer = null; 103 | for (int playerid: playersMap.keySet()) { 104 | if (playersMap.get(playerid).getUid() == request.uid) { 105 | targetPlayer = playersMap.get(playerid); 106 | } 107 | } 108 | if (targetPlayer == null){ 109 | context.json(new ResponseAuth(404, "Not found", null)); 110 | return; 111 | } 112 | PluginCommand.getInstance().tickets.put(otp, new PluginCommand.Ticket(targetPlayer, System.currentTimeMillis()/ 1000 + 300, true)); 113 | context.json(new ResponseAuth(201, "Code generated", otp)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/RequestOnlyHttpHandler.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus; 2 | 3 | import com.mojo.consoleplus.command.PluginCommand; 4 | import com.mojo.consoleplus.forms.RequestAuth; 5 | import com.mojo.consoleplus.forms.RequestJson; 6 | import com.mojo.consoleplus.forms.ResponseAuth; 7 | import com.mojo.consoleplus.forms.ResponseJson; 8 | import com.mojo.consoleplus.socket.SocketData; 9 | import com.mojo.consoleplus.socket.SocketDataWait; 10 | import com.mojo.consoleplus.socket.SocketServer; 11 | import com.mojo.consoleplus.socket.packet.HttpPacket; 12 | import com.mojo.consoleplus.socket.packet.OtpPacket; 13 | import com.mojo.consoleplus.socket.packet.player.Player; 14 | import com.mojo.consoleplus.socket.packet.player.PlayerEnum; 15 | import emu.grasscutter.server.http.Router; 16 | import io.javalin.Javalin; 17 | import io.javalin.http.Context; 18 | 19 | import java.text.DecimalFormat; 20 | import java.util.Random; 21 | 22 | import static java.lang.Integer.parseInt; 23 | import static java.lang.Long.parseLong; 24 | 25 | 26 | public final class RequestOnlyHttpHandler implements Router { 27 | // private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); 28 | 29 | @Override public void applyRoutes(Javalin javalin) { 30 | javalin.post("/mojoplus/api", RequestOnlyHttpHandler::processRequest); 31 | javalin.post("/mojoplus/auth", RequestOnlyHttpHandler::requestKey); 32 | } 33 | 34 | 35 | public static void processRequest(Context context) { 36 | RequestJson request = context.bodyAsClass(RequestJson.class); 37 | String player = null; 38 | int uid = -1; 39 | 40 | if (request.k2 != null) { // version 2 token 41 | long expire; 42 | String hashDigest; 43 | uid = parseInt(request.k2.split(":")[0]); 44 | expire = parseLong(request.k2.split(":")[1]); 45 | hashDigest = request.k2.split(":")[2]; 46 | if (ConsolePlus.authHandler.auth(uid, expire, hashDigest)){ 47 | player = SocketData.getPlayer(uid); 48 | } 49 | } 50 | 51 | if (player != null) { 52 | SocketDataWait wait = null; 53 | switch (request.request){ 54 | case "invoke": 55 | wait = new SocketDataWait<>(2000) { 56 | @Override 57 | public void run() {} 58 | @Override 59 | public HttpPacket initData(HttpPacket data) { 60 | return data; 61 | } 62 | 63 | @Override 64 | public void timeout() { 65 | context.json(new ResponseJson("timeout", 500)); 66 | } 67 | }; 68 | try{ 69 | // TODO: Enable execut commands to third party 70 | Player p = new Player(); 71 | p.type = PlayerEnum.RunCommand; 72 | p.uid = uid; 73 | p.data = request.payload; 74 | SocketServer.sendUidPacket(uid, p, wait); 75 | } catch (Exception e) { 76 | context.json(new ResponseJson("error", 500, e.getStackTrace().toString())); 77 | break; 78 | } 79 | case "ping": 80 | // res.json(new ResponseJson("success", 200)); 81 | if (wait == null) { 82 | context.json(new ResponseJson("success", 200, null)); 83 | } else { 84 | var data = wait.getData(); 85 | if (data == null) { 86 | context.json(new ResponseJson("timeout", 500)); 87 | } else { 88 | context.json(new ResponseJson(data.message, data.code, data.data)); 89 | } 90 | } 91 | break; 92 | default: 93 | context.json(new ResponseJson("400 Bad Request", 400)); 94 | break; 95 | } 96 | return; 97 | } 98 | 99 | context.json(new ResponseJson("403 Forbidden", 403)); 100 | } 101 | 102 | public static void requestKey(Context context) { 103 | RequestAuth request = context.bodyAsClass(RequestAuth.class); 104 | if (request.otp != null && !request.otp.equals("")) { 105 | if (PluginCommand.getInstance().tickets.get(request.otp) == null) { 106 | context.json(new ResponseAuth(404, "Not found", null)); 107 | return; 108 | } 109 | String key = SocketData.tickets.get(request.otp).key; 110 | if (key == null){ 111 | context.json(new ResponseAuth(403, "Not ready yet", null)); 112 | } else { 113 | SocketData.tickets.remove(request.otp); 114 | context.json(new ResponseAuth(200, "", key)); 115 | } 116 | } else if (request.uid != 0) { 117 | String otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 118 | while (PluginCommand.getInstance().tickets.containsKey(otp)){ 119 | otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 120 | } 121 | String targetPlayer = SocketData.getPlayer(request.uid); 122 | if (targetPlayer == null){ 123 | context.json(new ResponseAuth(404, "Not found", null)); 124 | return; 125 | } 126 | var otpPacket = new OtpPacket(request.uid, otp, System.currentTimeMillis() / 1000 + 300, true); 127 | if (!SocketServer.sendPacket(SocketData.getPlayerInServer(request.uid), otpPacket)) { 128 | context.json(new ResponseAuth(500, "Send otp to server failed.", null)); 129 | return; 130 | } 131 | SocketData.tickets.put(otp, otpPacket); 132 | context.json(new ResponseAuth(201, "Code generated", otp)); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # MojoConsolePlus 2 | 3 | [EN](./README.md)|中文 4 | 5 | MojoConsolePlus(MCP)是一个[Grasscutter](https://github.com/Grasscutters/Grasscutter)插件,旨在提供一个游戏内可用的带用户界面的控制台后端。 6 | 7 | ## 当前支持功能: 8 | - [x] 在游戏内发送控制台链接 9 | - [x] 支持游戏内控制台的全部功能 10 | - [x] 继承了Grasscutter的权限系统,用户本来没有权限的指令,在MojoConsole中也不能使用 11 | - [x] 将指令执行结果通过MojoConsolePlus返回(而不是由server角色在游戏内发送给用户) 12 | - [x] 自定义配置 13 | - [x] 使用外部CDN 14 | 15 | ## 重要提醒: 16 | 17 | 该插件基于Grasscutter的[Development](https://github.com/Grasscutters/Grasscutter/tree/development)分支进行开发。 \ 18 | 19 | **如果你遇到了问题,请到[Discord](https://discord.gg/T5vZU6UyeG)寻求支持。但是我们不保证我们能提供相应的支持。** 20 | 21 | **MojoConsolePlus设计时与前端解藕,你可以发起PR来介绍你实现的前端。** 22 | 23 | 自行为MojoConsolePlus开发前端时,不需要重启grasscutter服务器,刷新页面即可。使用/mojo o指令可以在外部打开浏览器来帮助您开发。 24 | 25 | ## 配置步骤 26 | ### 下载Jar文件 27 | 28 | 在Github中找到Releases并进行下载。 29 | 30 | ### 自行编译(可选) 31 | 1. 使用`git`下载代码 ``git clone https://github.com/gc-mojoconsole/gc-mojoconsole-backend``。 32 | 2. 找到grasscutter的安装位置,复制 ``grasscutter`` 的jar文件到刚刚新创建的 ``gc-mojoconsole-backend/gc-plugin/lib`` 文件夹。 33 | 3. 进入 ``gc-mojoconsole-plus`` 文件夹,执行``gradlew build`` (cmd) **或者** ``./gradlew build`` (Powershell, Linux & Mac)来进行编译。 34 | 4. 如果编译成功, 你可以找到``gc-plugin`` 文件夹下的 ``mojoconsole.jar``文件. 35 | 5. 将编译好的 ``mojoconsole.jar`` 文件复制到``Grasscutter`` 安装位置下的 ``plugins`` 文件夹。 36 | 6. 启动grasscutter。 37 | 38 | ...之后请看使用说明... 39 | 40 | ### 使用说明 41 | 42 | 你有两种选择来获得前端界面 43 | 44 | #### 使用CDN服务的前端界面(推荐使用) 45 | 46 | 7. 从仓库中下载`mojoconfig.json`,并将其放置在 `plugins` 文件夹中, 将 `useCDN` 设置为 `true`。或者,您可以先运行grasscutter,mojoconsoleplus将会自动为您生成`mojoconfig.json`。 47 | 8. 修改Grasscutter的配置文件 `config.json`,确保您激活了 `cors`选项。在CDN模式下需要CORS设置为True。具体路径为 `config.json`->`server`->`policies`->`cors`. 48 | 49 | CDN前端界面由我们在Github Pages上提供。沟通Grasscutter的相关密钥信息是以Hash锚点的形式提供的,所以您的密钥信息在任何情况下都不会通过网络传输,请您放心。我们会经常更新CDN提供的前端,所以您可以在不需要自行管理前端的情况下永远获得最新的功能!激活 `CORS` 不会给您的Grasscutter带来任何形式上的危险,请您放心。 50 | 51 | #### 自行提供前端界面 52 | 7. 将所有前端文件放入 `GRASSCUTTER_ROOT/plugins/mojoconsole/` 文件夹中。注意你必须要有一个名为 `console.html` 的入口,你也可以放入其他js,css等辅助文件至相同目录下。 53 | 54 | #### MojoConsole的游戏指令 55 | 56 | 8. 游戏内发送 `/mojoconsole` or `/mojo` 给server虚拟角色后,你会在邮箱中收到链接。此外,你可以使用 `o` 参数来调用外部浏览器打开链接,例如发送`/mojo o`给server虚拟角色。默认情况下,mojoconsole是使用游戏内的浏览器进行打开的。 57 | 58 | 你的目录结构看起来会是这样: 59 | ``` 60 | GRASSCUTTER根目录 61 | | grasscutter.jar 62 | | resources 63 | | data 64 | | ... 65 | └───plugins 66 | │ mojoconsole.jar 67 | │ mojoconfig.json 68 | │ ... 69 | └───mojoconsole 70 | │ console.html 71 | | ... 72 | └───any other file that you want to include in your frontend 73 | │ ... 74 | ``` 75 | 76 | 77 | ## 自行开发API的使用说明 78 | 79 | URL: `/mojoplus/api` 80 | 81 | Request: `Content-Type: application/json` 82 | ```json 83 | { 84 | "k": "SESSION_KEY", // **DEPRECATED** sesssion key is embedded in the mail, can be retreved via the GET params. 85 | "k2": "AUTH_KEY", // auth key, this is the second version auth key, choose either `k` or `k2` 86 | "request": "invoke", // set request to ping will ignore the payload, which just check the aliveness of current sessionKey 87 | "payload": "command just like what you do in your in game chat console" // example: "heal" for heal all avatars 88 | } 89 | ``` 90 | 91 | Response: `Content-Type: application/json` 92 | ```json 93 | { 94 | "message": "success", // message saying the execution status, 95 | "code": 200, // could be 200 - success, 403 - SessionKey invalid, 500 - Command execution error (should from command), 400 - request not supported 96 | "payload": "response for the command", // example: got "All characters have been healed." when invoking with "heal" 97 | } 98 | ``` 99 | 100 | 101 | URL: `/mojoplus/auth` Request a auth key for player 102 | 103 | Request: `Content-Type: application/json` 104 | ```json 105 | { 106 | "uid": "UID", // player uid to be requested 107 | "otp": "OTP", // **OPTIONAL**, use the OTP returned from previous `auth` request to check the status of the ticket. 108 | } 109 | ``` 110 | 111 | Response: `Content-Type: application/json` 112 | ```json 113 | { 114 | "message": "success", // message saying the execution status, 115 | "code": 200, // could be 200 - success, check content in `key` field, 116 | // 404 - Player not found or offline 117 | // 201 - Not ready yet, player has not confirmed yet 118 | // 400 - request not supported 119 | "key": "OTP or AUTH_KEY", // with `otp` field: AUTH_KEY for that player 120 | // without `otp` field: `OTP` for further request 121 | } 122 | ``` 123 | 124 | ## 其他资源 125 | 126 | You can use the following function to send the request, just plug it after you finished the command generation job. `payload` is the command you wish to send. 127 | 128 | ```javascript 129 | function sendCommand(payload){ 130 | var client = new XMLHttpRequest(); 131 | var key = new window.URLSearchParams(window.location.search).get("k"); 132 | var url = '/mojoplus/api'; 133 | client.open("POST", url, true); 134 | client.setRequestHeader("Content-Type", "application/json"); 135 | client.onreadystatechange = function () { 136 | if (client.readyState === 4 && client.status === 200) { 137 | var result = document.getElementById("c2"); 138 | // Print received data from server 139 | result.innerHTML = JSON.parse(this.responseText).payload.replace(/\n/g, "

"); 140 | } 141 | }; 142 | 143 | // Converting JSON data to string 144 | var data = JSON.stringify({ "k": key, "request": "invoke", "payload": payload }); 145 | // Sending data with the request 146 | client.send(data); 147 | } 148 | ``` 149 | 150 | ### 前端 151 | 152 | By SpikeHD: https://github.com/SpikeHD/MojoFrontend (under development) 153 | CDN前端:https://github.com/gc-mojoconsole/gc-mojoconsole.github.io 154 | 155 | Win 桌面版本 https://github.com/SwetyCore/MojoDesktop 156 | 157 | ...你可以自行开发前端,然后发起PR来让你的前端显示在这里... 158 | 159 | 160 | ## 贡献者 161 | 162 | 感谢以下开发者对本项目的贡献! 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/command/PluginCommand.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.command; 2 | 3 | import java.net.URLEncoder; 4 | import java.text.DecimalFormat; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Random; 8 | 9 | import com.mojo.consoleplus.socket.SocketClient; 10 | import com.mojo.consoleplus.socket.packet.OtpPacket; 11 | import emu.grasscutter.command.Command; 12 | import emu.grasscutter.command.CommandHandler; 13 | import emu.grasscutter.game.mail.Mail; 14 | import emu.grasscutter.game.player.Player; 15 | import static emu.grasscutter.config.Configuration.*; 16 | 17 | import com.mojo.consoleplus.ConsolePlus; 18 | import com.google.gson.Gson; 19 | import emu.grasscutter.BuildConfig; 20 | 21 | @Command(label = "mojoconsole", 22 | usage = { 23 | "", 24 | "o" 25 | }, 26 | aliases = {"mojo" }, 27 | permission = "mojo.console" 28 | ) 29 | public class PluginCommand implements CommandHandler { 30 | static class HashParams{ 31 | public String k2; // session key 32 | public String d; // mojo backend url 33 | public String gcv; 34 | } 35 | public static class Ticket { 36 | public Player sender; 37 | public Player targetPlayer; 38 | public long expire; 39 | public Boolean api = false; // from api 40 | public String key; 41 | 42 | public Ticket(Player sender2, Player targetPlayer2, long l) { 43 | sender = sender2; 44 | targetPlayer = targetPlayer2; 45 | expire = l; 46 | } 47 | public Ticket(Player targetPlayer, long expire, Boolean api) { 48 | this.sender = null; 49 | this.targetPlayer = targetPlayer; 50 | this.expire = expire; 51 | this.api = api; 52 | } 53 | } 54 | public HashMap tickets = new HashMap(); 55 | public static PluginCommand instance; 56 | 57 | public PluginCommand(){ 58 | instance = this; 59 | } 60 | 61 | @Override 62 | public void execute(Player sender, Player targetPlayer, List args) { 63 | if (sender != targetPlayer){ 64 | String otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 65 | while (tickets.containsKey(otp)){ 66 | otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); 67 | } 68 | CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessageThird.replace("{{OTP}}", otp)); 69 | flushTicket(); 70 | var time = System.currentTimeMillis()/ 1000 + 300; 71 | SocketClient.sendPacket(new OtpPacket(targetPlayer.getUid(), otp, time, false)); 72 | tickets.put(otp, new Ticket(sender, targetPlayer, time)); 73 | return; 74 | } 75 | String link_type = "webview"; 76 | if (args.size() > 0) { 77 | if (args.get(0).equals("o")){ 78 | link_type = "browser"; 79 | } else { 80 | String otp = args.get(0); 81 | Ticket resolved; 82 | Boolean valid = false; 83 | if (tickets.containsKey(otp)) { 84 | resolved = tickets.get(otp); 85 | if (sender == resolved.targetPlayer && resolved.expire > System.currentTimeMillis() / 1000){ 86 | sender = resolved.sender; 87 | targetPlayer = resolved.targetPlayer; 88 | valid = true; 89 | CommandHandler.sendMessage(targetPlayer, ConsolePlus.config.responseMessageSuccess); 90 | if (resolved.api == false) { 91 | tickets.remove(otp); 92 | } 93 | } 94 | } 95 | if (!valid){ 96 | CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessageError); 97 | return; 98 | } 99 | } 100 | } 101 | String authKey = ConsolePlus.authHandler.genKey(targetPlayer.getUid(), System.currentTimeMillis() / 1000 + ConsolePlus.config.mail.expireHour * 3600); 102 | String link = getServerURL(authKey); 103 | // Grasscutter.getLogger().info(link); 104 | 105 | if (sender != null) { 106 | Mail mail = new Mail(); 107 | mail.mailContent.title = ConsolePlus.config.mail.title; 108 | mail.mailContent.sender = ConsolePlus.config.mail.author; 109 | mail.mailContent.content = ConsolePlus.config.mail.content.replace("{{ LINK }}", ""); 110 | mail.expireTime = System.currentTimeMillis() / 1000 + 3600 * ConsolePlus.config.mail.expireHour; 111 | sender.sendMail(mail); 112 | CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessage); 113 | } else { 114 | tickets.get(args.get(0)).key = authKey; 115 | } 116 | 117 | } 118 | 119 | private static String getServerURL(String sessionKey) { 120 | if (ConsolePlus.config.UseCDN){ 121 | Gson gson = new Gson(); 122 | HashParams hp = new HashParams(); 123 | hp.k2 = sessionKey; 124 | hp.d = getMojoBackendURL(); 125 | hp.gcv = BuildConfig.VERSION; 126 | try { 127 | sessionKey = URLEncoder.encode(sessionKey, "utf-8"); 128 | } catch (Exception e) { 129 | e.printStackTrace(); 130 | } 131 | try{ 132 | return ConsolePlus.config.CDNLink + "#" + URLEncoder.encode(gson.toJson(hp), "utf-8"); 133 | } catch (Exception e){ 134 | e.printStackTrace(); 135 | return ConsolePlus.config.CDNLink + "?k2=" + sessionKey + "&gcv=" + BuildConfig.VERSION; 136 | } 137 | } else { 138 | return getMojoBackendURL() + ConsolePlus.config.interfacePath + "?k2=" + sessionKey + "&gcv=" + BuildConfig.VERSION; 139 | } 140 | } 141 | 142 | private static String getMojoBackendURL() { 143 | return "http" + (HTTP_ENCRYPTION.useEncryption ? "s" : "") + "://" 144 | + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" 145 | + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort); 146 | } 147 | 148 | private void flushTicket() { 149 | Long curtime = System.currentTimeMillis() / 1000; 150 | for (String otp : tickets.keySet()) { 151 | if (curtime > tickets.get(otp).expire) { 152 | tickets.remove(otp); 153 | SocketClient.sendPacket(new OtpPacket(otp)); 154 | } 155 | } 156 | } 157 | 158 | public static PluginCommand getInstance() { 159 | return instance; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MojoConsolePlus 2 | 3 | EN|[中文](./README-zh.md) 4 | 5 | MojoConsolePlus(MCP) is a [Grasscutter](https://github.com/Grasscutters/Grasscutter) plugin (Apart from 4Benj's GCGM plugin) and it's goal is to implement fully in-game webwiew based . 6 | 7 | | Grasscutter version | MojoConsole version | 8 | | ------------------- | ------------------- | 9 | | 1.2.1-dev | [1.2.0-dev](https://github.com/gc-mojoconsole/gc-mojoconsole-backend/releases/tag/dev-1.2.0) | 10 | | 1.2.2-dev | [1.3.0-dev](https://github.com/gc-mojoconsole/gc-mojoconsole-backend/releases/tag/dev-1.3.0) | 11 | | 1.2.3-dev | [1.4.0-dev](https://github.com/gc-mojoconsole/gc-mojoconsole-backend/releases/tag/dev-1.4.0) | 12 | 13 | ## Currently Features: 14 | - [x] Send console link in game 15 | - [x] Do what players can do in the in-game chat based console 16 | - [x] Inherit the original permission system 17 | - [x] Capture command response to plugin instead of send chat to player 18 | - [x] More configurable 19 | - [x] Use external CDN 20 | 21 | ## Important Notes: 22 | This plugin is made to run on the current [Development](https://github.com/Grasscutters/Grasscutter/tree/development) branch of Grasscutter. \ 23 | This plugin is in very early development and only have the backend now. Frontend is extermely hardcore. 24 | **If you require support please ask on the [Grasscutter Discord](https://discord.gg/T5vZU6UyeG). However, support is not guarenteed.** 25 | 26 | **MojoConsolePlus is decoupled with any frontend yet, feel free to open a PR to introduce your implented frontend.** 27 | 28 | Development frontend for MojoConsolePlus don't have to restart the Grasscutter server. Just refresh page! There will be a URL printed to the console as log to help you access the console in your browser for development. 29 | 30 | ## Setup 31 | ### Download Plugin Jar 32 | 33 | See realeases. 34 | 35 | ### Compile yourself 36 | 1. Pull the latest code from github using ``git clone https://github.com/mingjun97/gc-mojoconsole-plus`` in your terminal of choice. 37 | 2. Locate your grasscutter server and copy the ``grasscutter`` server jar into the newly created ``gc-mojoconsole-backend/gc-plugin/lib`` folder 38 | 3. Navigate back into the project root folder called ``gc-mojoconsole-plus`` folder and run ``gradlew build`` (cmd) **or** ``./gradlew build`` (Powershell, Linux & Mac). 39 | 4. Assuming the build succeeded, in your file explorer navigate to the ``gc-plugin`` folder, you should have a ``mojoconsole.jar`` file, copy it. 40 | 5. Navigate to your ``Grasscutter`` server, find the ``plugins`` folder and paste the ``mojoconsole.jar`` into it. 41 | 6. Start your server. 42 | 43 | ...Jump to next section... 44 | 45 | ### Usage 46 | 47 | You have two options for hosting frontend. 48 | 49 | #### Use CDN hosted frontend(Recommended) 50 | 51 | 7. Pull the file `mojoconfig.json` into your `plugins` folder, set the `useCDN` to `true`. Alternatively, you can launch the grasscutter first, then the `mojoconfig.json` will be auto generated. 52 | 8. Modify the `config.json` of Grasscutter, make sure the `cors` is enabled. It's required in CDN mode. See `config.json`->`server`->`policies`->`cors`. 53 | 54 | CDN serving frontend is hosted by us via the github pages. The credentials will be passed via the hash paramets, which means the credentials(i.e. User session key, access address for your grasscutter server) will not be passed through the network. We will update our CDN frontend regularly, so that you don't have to handle the frontend update by yourself and always enjoy the latest version. Enabling `CORS` will not put your grasscutter in dangerous. It's just for protecting users accessed on browser from potential information leakage in some rare circumstances, and grasscutter actually don't have this concern. 55 | 56 | #### Self hosted frontend 57 | 7. Put the all the frontend files into the folder `GRASSCUTTER_ROOT/plugins/mojoconsole/`. Note that your must have `console.html` for now. You are free to put any other dynamiclly loaded file(e.g. `.js`, `.css`) in that folder. Check the last section for current avialable frontend. 58 | 59 | #### Commands for mojo 60 | 61 | 8. Send command `/mojoconsole` or `/mojo` to server in game, and you will receive mail in your mailbox. Then follow the instructions there. Note that you may use `o` option to use the pop out browser instead of in-game webwiew, e.g. `/mojo o`. By default, the console is in-game webview. 62 | 63 | Your final plugins folder's directory structure should look similar to this 64 | ``` 65 | GRASSCUTTER_ROOT 66 | | grasscutter.jar 67 | | resources 68 | | data 69 | | ... 70 | └───plugins 71 | │ mojoconsole.jar 72 | │ ... 73 | └───mojoconsole 74 | │ console.html 75 | | ... 76 | └───any other file that you want to include in your frontend 77 | │ ... 78 | ``` 79 | 80 | 81 | ## API 82 | 83 | URL: `/mojoplus/api` 84 | 85 | Request: `Content-Type: application/json` 86 | ```json 87 | { 88 | "k": "SESSION_KEY", // **DEPRECATED** sesssion key is embedded in the mail, can be retreved via the GET params. 89 | "k2": "AUTH_KEY", // auth key, this is the second version auth key, choose either `k` or `k2` 90 | "request": "invoke", // set request to ping will ignore the payload, which just check the aliveness of current sessionKey 91 | "payload": "command just like what you do in your in game chat console" // example: "heal" for heal all avatars 92 | } 93 | ``` 94 | 95 | Response: `Content-Type: application/json` 96 | ```json 97 | { 98 | "message": "success", // message saying the execution status, 99 | "code": 200, // could be 200 - success, 403 - SessionKey invalid, 500 - Command execution error (should from command), 400 - request not supported 100 | "payload": "response for the command", // example: got "All characters have been healed." when invoking with "heal" 101 | } 102 | ``` 103 | 104 | URL: `/mojoplus/auth` Request a auth key for player 105 | 106 | Request: `Content-Type: application/json` 107 | ```json 108 | { 109 | "uid": "UID", // player uid to be requested 110 | "otp": "OTP", // **OPTIONAL**, use the OTP returned from previous `auth` request to check the status of the ticket. 111 | } 112 | ``` 113 | 114 | Response: `Content-Type: application/json` 115 | ```json 116 | { 117 | "message": "success", // message saying the execution status, 118 | "code": 200, // could be 200 - success, check content in `key` field, 119 | // 404 - Player not found or offline 120 | // 201 - Not ready yet, player has not confirmed yet 121 | // 400 - request not supported 122 | "key": "OTP or AUTH_KEY", // with `otp` field: AUTH_KEY for that player 123 | // without `otp` field: `OTP` for further request 124 | } 125 | ``` 126 | 127 | ## Resources 128 | 129 | You can use the following function to send the request, just plug it after you finished the command generation job. `payload` is the command you wish to send. 130 | 131 | ```javascript 132 | function sendCommand(payload){ 133 | var client = new XMLHttpRequest(); 134 | var key = new window.URLSearchParams(window.location.search).get("k"); 135 | var url = '/mojoplus/api'; 136 | client.open("POST", url, true); 137 | client.setRequestHeader("Content-Type", "application/json"); 138 | client.onreadystatechange = function () { 139 | if (client.readyState === 4 && client.status === 200) { 140 | var result = document.getElementById("c2"); 141 | // Print received data from server 142 | result.innerHTML = JSON.parse(this.responseText).payload.replace(/\n/g, "

"); 143 | } 144 | }; 145 | 146 | // Converting JSON data to string 147 | var data = JSON.stringify({ "k": key, "request": "invoke", "payload": payload }); 148 | // Sending data with the request 149 | client.send(data); 150 | } 151 | ``` 152 | 153 | ### Frontend 154 | 155 | By SpikeHD: https://github.com/SpikeHD/MojoFrontend (under development) 156 | CDN hosted frontend:https://github.com/gc-mojoconsole/gc-mojoconsole.github.io 157 | 158 | Desktop version on Windows https://github.com/SwetyCore/MojoDesktop 159 | ...You can develop your own frontend and make PR to put yours here... 160 | 161 | 162 | ## Contributors 163 | 164 | Special thanks to the following users contributed to this project. 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketClient.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket; 2 | 3 | import com.mojo.consoleplus.ConsolePlus; 4 | import com.mojo.consoleplus.command.PluginCommand; 5 | import com.mojo.consoleplus.config.MojoConfig; 6 | import com.mojo.consoleplus.socket.packet.*; 7 | import com.mojo.consoleplus.socket.packet.player.Player; 8 | import com.mojo.consoleplus.socket.packet.player.PlayerList; 9 | import emu.grasscutter.Grasscutter; 10 | import emu.grasscutter.command.CommandMap; 11 | import emu.grasscutter.utils.MessageHandler; 12 | import org.slf4j.Logger; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.OutputStream; 17 | import java.net.Socket; 18 | import java.util.ArrayList; 19 | import java.util.Timer; 20 | import java.util.TimerTask; 21 | 22 | import static com.mojo.consoleplus.ConsolePlus.gson; 23 | 24 | // Socket 客户端 25 | public class SocketClient { 26 | public static ClientThread clientThread; 27 | 28 | public static Logger mLogger; 29 | 30 | public static Timer timer; 31 | 32 | public static boolean connect = false; 33 | 34 | public static ReceiveThread receiveThread; 35 | 36 | public static void connectServer(Logger logger) { 37 | mLogger = logger; 38 | connectServer(); 39 | } 40 | 41 | // 连接服务器 42 | public static void connectServer() { 43 | if (connect) return; 44 | if (clientThread != null) { 45 | mLogger.warn("[Mojo Console] Retry connecting to the server after 15 seconds"); 46 | try { 47 | Thread.sleep(15000); 48 | } catch (InterruptedException ex) { 49 | throw new RuntimeException(ex); 50 | } 51 | } 52 | MojoConfig config = ConsolePlus.config; 53 | clientThread = new ClientThread(config.socketHost, config.socketPort); 54 | 55 | if (timer != null) { 56 | timer.cancel(); 57 | } 58 | timer = new Timer(); 59 | timer.schedule(new SendHeartBeatPacket(), 500); 60 | timer.schedule(new SendPlayerListPacket(), 1000); 61 | } 62 | 63 | // 发送数据包 64 | public static boolean sendPacket(BasePacket packet) { 65 | var p = SocketUtils.getPacket(packet); 66 | if (!clientThread.sendPacket(p)) { 67 | mLogger.warn("[Mojo Console] Send packet to server failed"); 68 | connect = false; 69 | connectServer(); 70 | return false; 71 | } 72 | return true; 73 | } 74 | 75 | // 发送数据包带数据包ID 76 | public static boolean sendPacket(BasePacket packet, String packetID) { 77 | if (!clientThread.sendPacket(SocketUtils.getPacketAndPackID(packet, packetID))) { 78 | mLogger.warn("[Mojo Console] Send packet to server failed"); 79 | connect = false; 80 | connectServer(); 81 | return false; 82 | } 83 | return true; 84 | } 85 | 86 | // 心跳包发送 87 | private static class SendHeartBeatPacket extends TimerTask { 88 | @Override 89 | public void run() { 90 | if (connect) { 91 | sendPacket(new HeartBeat("Pong")); 92 | } 93 | } 94 | } 95 | 96 | private static class SendPlayerListPacket extends TimerTask { 97 | @Override 98 | public void run() { 99 | if (connect) { 100 | PlayerList playerList = new PlayerList(); 101 | playerList.player = Grasscutter.getGameServer().getPlayers().size(); 102 | ArrayList playerNames = new ArrayList<>(); 103 | for (emu.grasscutter.game.player.Player player : Grasscutter.getGameServer().getPlayers().values()) { 104 | playerNames.add(player.getNickname()); 105 | playerList.playerMap.put(player.getUid(), player.getNickname()); 106 | } 107 | playerList.playerList = playerNames; 108 | sendPacket(playerList); 109 | } 110 | } 111 | } 112 | 113 | // 数据包接收 114 | private static class ReceiveThread extends Thread { 115 | private InputStream is; 116 | private boolean exit; 117 | 118 | public ReceiveThread(Socket socket) { 119 | try { 120 | is = socket.getInputStream(); 121 | } catch (IOException e) { 122 | e.printStackTrace(); 123 | } 124 | start(); 125 | } 126 | 127 | @Override 128 | public void run() { 129 | //noinspection InfiniteLoopStatement 130 | while (true) { 131 | try { 132 | if (exit) return; 133 | String data = SocketUtils.readString(is); 134 | Packet packet = gson.fromJson(data, Packet.class); 135 | switch (packet.type) { 136 | // 玩家类 137 | case Player: 138 | var player = gson.fromJson(packet.data, Player.class); 139 | switch (player.type) { 140 | // 运行命令 141 | case RunCommand -> { 142 | var command = player.data; 143 | var playerData = Grasscutter.getGameServer().getPlayerByUid(player.uid); 144 | if (playerData == null) { 145 | sendPacket(new HttpPacket(404, "[Mojo Console] Player not found."), packet.packetID); 146 | return; 147 | } 148 | // Player MessageHandler do not support concurrency 149 | //noinspection SynchronizationOnLocalVariableOrMethodParameter 150 | synchronized (playerData) { 151 | try { 152 | var resultCollector = new MessageHandler(); 153 | playerData.setMessageHandler(resultCollector); 154 | CommandMap.getInstance().invoke(playerData, playerData, command); 155 | sendPacket(new HttpPacket(200, "success", resultCollector.getMessage()), packet.packetID); 156 | } catch (Exception e) { 157 | mLogger.warn("[Mojo Console] Run command failed.", e); 158 | sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); 159 | } finally { 160 | playerData.setMessageHandler(null); 161 | } 162 | } 163 | } 164 | // 发送信息 165 | case DropMessage -> { 166 | var playerData = Grasscutter.getGameServer().getPlayerByUid(player.uid); 167 | if (playerData == null) { 168 | return; 169 | } 170 | playerData.dropMessage(player.data); 171 | } 172 | } 173 | break; 174 | case OtpPacket: 175 | var otpPacket = gson.fromJson(packet.data, OtpPacket.class); 176 | PluginCommand.getInstance().tickets.put(otpPacket.otp, new PluginCommand.Ticket(Grasscutter.getGameServer().getPlayerByUid(otpPacket.uid), otpPacket.expire, otpPacket.api)); 177 | case Signature: 178 | var signaturePacket = gson.fromJson(packet.data, SignaturePacket.class); 179 | ConsolePlus.authHandler.setSignature(signaturePacket.signature); 180 | break; 181 | } 182 | } catch (Throwable e) { 183 | e.printStackTrace(); 184 | if (!sendPacket(new HeartBeat("Pong"))) { 185 | return; 186 | } 187 | } 188 | } 189 | } 190 | 191 | public void exit() { 192 | exit = true; 193 | } 194 | } 195 | 196 | // 客户端连接线程 197 | private static class ClientThread extends Thread { 198 | private final String ip; 199 | private final int port; 200 | private Socket socket; 201 | private OutputStream os; 202 | 203 | public ClientThread(String ip, int port) { 204 | this.ip = ip; 205 | this.port = port; 206 | start(); 207 | } 208 | 209 | public Socket getSocket() { 210 | return socket; 211 | } 212 | 213 | public boolean sendPacket(String string) { 214 | return SocketUtils.writeString(os, string); 215 | } 216 | 217 | @Override 218 | public void run() { 219 | try { 220 | if (receiveThread != null) { 221 | receiveThread.exit(); 222 | } 223 | 224 | socket = new Socket(ip, port); 225 | connect = true; 226 | os = socket.getOutputStream(); 227 | mLogger.info("[Mojo Console] Connect to server: " + ip + ":" + port); 228 | SocketClient.sendPacket(new AuthPacket(ConsolePlus.config.socketToken)); 229 | receiveThread = new ReceiveThread(socket); 230 | } catch (IOException e) { 231 | connect = false; 232 | mLogger.warn("[Mojo Console] Connect to server failed: " + ip + ":" + port); 233 | connectServer(); 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketServer.java: -------------------------------------------------------------------------------- 1 | package com.mojo.consoleplus.socket; 2 | 3 | import com.mojo.consoleplus.ConsolePlus; 4 | import com.mojo.consoleplus.socket.packet.*; 5 | import com.mojo.consoleplus.socket.packet.player.PlayerList; 6 | import emu.grasscutter.Grasscutter; 7 | import org.slf4j.Logger; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.net.ServerSocket; 13 | import java.net.Socket; 14 | import java.util.HashMap; 15 | import java.util.Timer; 16 | import java.util.TimerTask; 17 | 18 | import static com.mojo.consoleplus.ConsolePlus.gson; 19 | 20 | // Socket 服务器 21 | public class SocketServer { 22 | // 客户端超时时间 23 | private static final int TIMEOUT = 5000; 24 | private static final HashMap clientList = new HashMap<>(); 25 | 26 | private static final HashMap clientTimeout = new HashMap<>(); 27 | private static Logger mLogger; 28 | 29 | public static void startServer(Logger logger) { 30 | mLogger = logger; 31 | try { 32 | int port = ConsolePlus.config.socketPort; 33 | new Timer().schedule(new SocketClientCheck(), 500); 34 | new WaitClientConnect(port); 35 | } catch (Throwable e) { 36 | mLogger.error("[Mojo Console] Socket server start failed", e); 37 | } 38 | } 39 | 40 | // 向全部客户端发送数据 41 | public static boolean sendAllPacket(BasePacket packet) { 42 | var p = SocketUtils.getPacket(packet); 43 | HashMap old = (HashMap) clientList.clone(); 44 | for (var client : old.entrySet()) { 45 | if (!client.getValue().sendPacket(p)) { 46 | mLogger.warn("[Mojo Console] Send packet to client {} failed", client.getKey()); 47 | clientList.remove(client.getKey()); 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | // 根据地址发送到相应的客户端 54 | public static boolean sendPacket(String address, BasePacket packet) { 55 | var p = SocketUtils.getPacket(packet); 56 | var client = clientList.get(address); 57 | if (client != null) { 58 | if (client.sendPacket(p)) { 59 | return true; 60 | } 61 | mLogger.warn("[Mojo Console] Send packet to client {} failed", address); 62 | clientList.remove(address); 63 | } 64 | return false; 65 | } 66 | 67 | // 根据Uid发送到相应的客户端异步返回数据 68 | public static boolean sendUidPacket(Integer playerId, BasePacket player, SocketDataWait socketDataWait) { 69 | var p = SocketUtils.getPacketAndPackID(player); 70 | var clientID = SocketData.getPlayerInServer(playerId); 71 | if (clientID == null) return false; 72 | var client = clientList.get(clientID); 73 | if (client != null) { 74 | socketDataWait.uid = p.get(0); 75 | if (!client.sendPacket(p.get(1), socketDataWait)) { 76 | mLogger.warn("[Mojo Console] Send packet to client {} failed", clientID); 77 | clientList.remove(clientID); 78 | return false; 79 | } 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | // 客户端超时检测 86 | private static class SocketClientCheck extends TimerTask { 87 | @Override 88 | public void run() { 89 | HashMap old = (HashMap) clientTimeout.clone(); 90 | for (var client : old.entrySet()) { 91 | var clientID = client.getKey(); 92 | var clientTime = client.getValue(); 93 | if (clientTime > TIMEOUT) { 94 | mLogger.info("[Mojo Console] Client {} timeout, disconnect.", clientID); 95 | clientList.remove(clientID); 96 | clientTimeout.remove(clientID); 97 | SocketData.playerList.remove(clientID); 98 | } else { 99 | clientTimeout.put(clientID, clientTime + 500); 100 | } 101 | } 102 | } 103 | } 104 | 105 | // 客户端数据包处理 106 | private static class ClientThread extends Thread { 107 | private final Socket socket; 108 | private InputStream is; 109 | private OutputStream os; 110 | private final String address; 111 | private final String token; 112 | private boolean auth = false; 113 | 114 | private final HashMap> socketDataWaitList = new HashMap<>(); 115 | 116 | public ClientThread(Socket accept) { 117 | socket = accept; 118 | address = socket.getInetAddress() + ":" + socket.getPort(); 119 | token = ConsolePlus.config.socketToken; 120 | try { 121 | is = accept.getInputStream(); 122 | os = accept.getOutputStream(); 123 | } catch (IOException e) { 124 | e.printStackTrace(); 125 | } 126 | start(); 127 | } 128 | 129 | public Socket getSocket() { 130 | return socket; 131 | } 132 | 133 | // 发送数据包 134 | public boolean sendPacket(String packet) { 135 | return SocketUtils.writeString(os, packet); 136 | } 137 | 138 | // 发送异步数据包 139 | public boolean sendPacket(String packet, SocketDataWait socketDataWait) { 140 | if (SocketUtils.writeString(os, packet)) { 141 | socketDataWaitList.put(socketDataWait.uid, socketDataWait); 142 | return true; 143 | } else { 144 | return false; 145 | } 146 | } 147 | 148 | @Override 149 | public void run() { 150 | // noinspection InfiniteLoopStatement 151 | while (true) { 152 | try { 153 | String data = SocketUtils.readString(is); 154 | Packet packet = gson.fromJson(data, Packet.class); 155 | if (packet.type == PacketEnum.AuthPacket) { 156 | AuthPacket authPacket = gson.fromJson(packet.data, AuthPacket.class); 157 | if (authPacket.token.equals(token)) { 158 | mLogger.info("[Mojo Console] Client {} auth success.", address); 159 | auth = true; 160 | clientList.put(address, this); 161 | clientTimeout.put(address, 0); 162 | sendPacket(SocketUtils.getPacket(new SignaturePacket(ConsolePlus.authHandler.getSignature()))); 163 | } else { 164 | mLogger.error("[Mojo Console] AuthPacket: {} auth filed.", address); 165 | socket.close(); 166 | return; 167 | } 168 | } 169 | if (!auth) { 170 | mLogger.error("[Mojo Console] AuthPacket: {} not auth", address); 171 | socket.close(); 172 | return; 173 | } 174 | switch (packet.type) { 175 | // 缓存玩家列表 176 | case PlayerList -> { 177 | PlayerList playerList = gson.fromJson(packet.data, PlayerList.class); 178 | SocketData.playerList.put(address, playerList); 179 | } 180 | // Http信息返回 181 | case HttpPacket -> { 182 | HttpPacket httpPacket = gson.fromJson(packet.data, HttpPacket.class); 183 | var socketWait = socketDataWaitList.get(packet.packetID); 184 | if (socketWait == null) { 185 | mLogger.error("[Mojo Console] HttpPacket: {} not found", packet.packetID); 186 | return; 187 | } 188 | socketWait.setData(httpPacket); 189 | socketDataWaitList.remove(packet.packetID); 190 | } 191 | case OtpPacket -> { 192 | OtpPacket otpPacket = gson.fromJson(packet.data, OtpPacket.class); 193 | if (otpPacket.remove) { 194 | SocketData.tickets.remove(otpPacket.otp); 195 | } else { 196 | SocketData.tickets.put(otpPacket.otp, otpPacket); 197 | } 198 | } 199 | // 心跳包 200 | case HeartBeat -> { 201 | clientTimeout.put(address, 0); 202 | } 203 | } 204 | } catch (Throwable e) { 205 | e.printStackTrace(); 206 | mLogger.error("[Mojo Console] Client {} disconnect.", address); 207 | clientList.remove(address); 208 | clientTimeout.remove(address); 209 | SocketData.playerList.remove(address); 210 | return; 211 | } 212 | } 213 | } 214 | } 215 | 216 | // 等待客户端连接 217 | private static class WaitClientConnect extends Thread { 218 | ServerSocket socketServer; 219 | 220 | public WaitClientConnect(int port) throws IOException { 221 | socketServer = new ServerSocket(port); 222 | start(); 223 | } 224 | 225 | @Override 226 | public void run() { 227 | mLogger.info("[Mojo Console] Start socket server on port " + socketServer.getLocalPort()); 228 | // noinspection InfiniteLoopStatement 229 | while (true) { 230 | try { 231 | Socket accept = socketServer.accept(); 232 | String address = accept.getInetAddress() + ":" + accept.getPort(); 233 | mLogger.info("[Mojo Console] Client connect: " + address); 234 | new ClientThread(accept); 235 | } catch (IOException e) { 236 | e.printStackTrace(); 237 | } 238 | } 239 | } 240 | } 241 | } 242 | --------------------------------------------------------------------------------