diff --git a/README.md b/README.md
index 725c7dc..17dd70c 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,9 @@ GCGM is the first [Grasscutter](https://github.com/Grasscutters/Grasscutter) plu
## Currently Planned Features:
- [x] Loading basic web page
- [x] Nice looking CSS
-- [ ] Websockets
+- [x] Websockets (Although it is very basic)
- [ ] Widgets
-- [ ] Server performance stats
+- [x] Server performance stats
- [ ] See players registered to dispatch server
- [ ] See players currently online
- [ ] Send mail to all players
@@ -17,8 +17,10 @@ The features listed are to achieve an MVP for the first release.
## Important Notes:
This plugin is made to run on the current [Development](https://github.com/Grasscutters/Grasscutter/tree/development) branch of Grasscutter. \
-This plugin is in very early development and the web dashboard only displays a side panel, so it is not really in a usable state. \
-**If you require support please ask on the [Grasscutter Discord](https://discord.gg/T5vZU6UyeG). However, support is not guarenteed.**
+This plugin is in very early development and the web dashboard only displays a side panel and a performance graph. \
+**If you require support please ask on the [Grasscutter Discord](https://discord.gg/T5vZU6UyeG). However, support is not guaranteed.** \
+**If you encounter any issues, please report them on the [issue tracker](https://github.com/Grasscutters/gcgm-plugin/issues). However, please search to see if anyone else has encountered your issue before. Any duplicate issues will be closed.**
+
THE ISSUE TRACKER IS NOT A SUPPORT FORM.
## Setup
### Download Plugin Jar
@@ -31,13 +33,21 @@ Coming soon!
4. Assuming the build succeeded, in your file explorer navigate to the ``gc-plugin`` folder, you should have a ``gcgm-plugin.jar`` file, copy it.
5. Navigate to your ``Grasscutter`` server, find the ``plugins`` folder and paste the ``gcgm-plugin.jar`` into it.
6. Start your server.
-7. Your server should then start and after a few seconds, you should be greated with these messages and the server will quit.
+7. Your server should then start and after a few seconds, you should be greated with these messages.
```
-[WARN] The './plugins/gcgm/www' folder does not exist.
-[WARN] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/gcgm' to './plugins/gcgm/www
-[WARN] Your server will now exit to allow this process to be completed
+[WARN] [GCGM] The './plugins/gcgm/www' folder does not exist.
+[INFO] [GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'
+[WARN] [GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www
+[WARN] [GCGM] GCGM has now loaded...
+...
+[ERROR] [GCGM] GCGM could not find the 'www' folder inside its plugin directory
+[ERROR] [GCGM] Please make sure a 'www' folder exists by extracting 'DefaultWebApp.zip' into a new 'www' or download a third-party dashboard
+[ERROR] [GCGM] GCGM could not be enabled
+
```
-7. Inside the ``plugins`` folder there now should be a new folder with the name of ``gcgm``, when you open the folder, there should be a ``.zip`` file called ``DefaultWebApp.zip``. Extract the contents of this zip into a folder called ``www``.
+8. Type ``stop`` to stop the server
+9. Inside the ``plugins`` folder there now should be a new folder with the name of ``gcgm``, when you open the folder, there should be file called ``DefaultWebApp.zip``. Extract the contents of this zip into a folder called ``www``.
+10. Start your server
Your final plugins folder's directory structure should look similar to this
```
diff --git a/gc-plugin/build.gradle b/gc-plugin/build.gradle
index 6d50449..a62cfc0 100644
--- a/gc-plugin/build.gradle
+++ b/gc-plugin/build.gradle
@@ -19,10 +19,6 @@ plugins {
sourceCompatibility = 17
targetCompatibility = 17
-repositories {
- mavenCentral()
-}
-
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
@@ -39,16 +35,6 @@ repositories {
dependencies {
implementation fileTree(dir: 'lib', include: ['*.jar'])
- implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8'
-
- implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.6'
-
- //implementation group: 'tech.xigam', name: 'grasscutter', version: '1.0.2-dev'
-
- implementation 'io.jsonwebtoken:jjwt-api:0.11.3'
- runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.3', 'io.jsonwebtoken:jjwt-gson:0.11.3'
-
- implementation group: 'net.lingala.zip4j', name: 'zip4j', version: '2.10.0'
runtimeOnly project(':web-interface')
}
@@ -83,14 +69,19 @@ jar {
from {
configurations.runtimeClasspath.collect {
- if(it.name.equalsIgnoreCase("zip4j-2.10.0.jar")) {
+ if(!it.name.contains("grasscutter")) {
println ('Packaging ' + it.name)
- zipTree(it);
+ it.isDirectory() ? it : zipTree(it)
}
}
+ } {
+ exclude('META-INF/LICENSE*')
+ exclude("META-INF/versions/9/module-info.class")
+ exclude("META-INF/NOTICE")
}
- //duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
destinationDir = file(".")
}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java
index f5c8efa..d98b28b 100644
--- a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java
@@ -1,26 +1,36 @@
package com.benj4.gcgm;
+import com.benj4.gcgm.handlers.ServerTickHandler;
+import com.benj4.gcgm.server.websocket.WebSocketServer;
+import com.benj4.gcgm.utils.*;
+import com.benj4.gcgm.utils.web.WebUtils;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.Plugin;
+import emu.grasscutter.server.event.EventHandler;
+import emu.grasscutter.server.event.HandlerPriority;
+import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.utils.Utils;
import express.Express;
-import io.javalin.http.staticfiles.Location;
-import java.io.*;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
+import java.io.File;
public class GCGMPlugin extends Plugin {
- File webData;
+ private static GCGMPlugin INSTANCE;
+ EventHandler serverTickEventHandler;
+ private static WebSocketServer webSocketServer;
+ private File webData;
@Override
public void onLoad() {
- File pluginDataDir = getDataFolder();
+ INSTANCE = this;
webData = new File(Utils.toFilePath(getDataFolder().getPath() + "/www"));
+
+ serverTickEventHandler = new EventHandler(ServerTickEvent.class);
+ serverTickEventHandler.listener(new ServerTickHandler());
+ serverTickEventHandler.priority(HandlerPriority.HIGH);
+
+ File pluginDataDir = getDataFolder();
String zipFileLoc = Utils.toFilePath(getDataFolder().getPath() + "/DefaultWebApp.zip");
if(!pluginDataDir.exists() && !pluginDataDir.mkdirs()) {
@@ -30,35 +40,14 @@ public class GCGMPlugin extends Plugin {
if(!webData.exists()) {
Grasscutter.getLogger().warn("[GCGM] The './plugins/GCGM/www' folder does not exist.");
-
- // Get the ZIP
- URL url = null;
- try {
- url = new File(Utils.toFilePath(Grasscutter.getConfig().PLUGINS_FOLDER + "/gcgm-plugin.jar")).toURI().toURL();
- } catch (MalformedURLException e) {
- e.printStackTrace();
- }
-
- //URLClassLoader loader = new URLClassLoader(new URL[]{url});
-
- InputStream defaultWebAppZip = getResource("DefaultWebApp.zip");
- try {
- // Copy the the zip from resources to the plugin's data directory
- if(!new File(zipFileLoc).exists()) {
- Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'");
- Files.copy(defaultWebAppZip, Paths.get(new File(zipFileLoc).toURI()), StandardCopyOption.REPLACE_EXISTING);
+ // Copy the the zip from resources to the plugin's data directory
+ if(!new File(zipFileLoc).exists()) {
+ if(GCGMUtils.CopyFile("DefaultWebApp.zip", zipFileLoc)) {
+ Grasscutter.getLogger().warn("[GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www");
+ } else {
+ Grasscutter.getLogger().error("[GCGM] GCGM cannot start due to setup errors.");
+ return;
}
-
- Grasscutter.getLogger().warn("[GCGM] Please extract the contents of 'DefaultWebApp.zip' from within './plugins/GCGM' to './plugins/GCGM/www");
- Grasscutter.getLogger().warn("[GCGM] Your server will now exit to allow this process to be completed");
-
- System.exit(0);
- } catch (Exception e) {
- e.printStackTrace();
- }
- } else {
- if(new File(zipFileLoc).exists()) {
- Grasscutter.getLogger().info("[GCGM] Note: You can now safely delete 'DefaultWebApp.zip' from within './plugins/GCGM'");
}
}
@@ -67,22 +56,33 @@ public class GCGMPlugin extends Plugin {
@Override
public void onEnable() {
- Express app = Grasscutter.getDispatchServer().getServer();
+ if(webData.exists()) {
+ Express app = Grasscutter.getDispatchServer().getServer();
+ WebUtils.addStaticFiles(app, webData);
+ webSocketServer = new WebSocketServer();
+ webSocketServer.start(app);
- app.raw().config.precompressStaticFiles = false;
- app.raw().config.addStaticFiles("/gm", webData.getAbsolutePath(), Location.EXTERNAL);
- app.raw().config.addSinglePageRoot("/gm", Utils.toFilePath(getDataFolder().getPath() + "/www/index.html"), Location.EXTERNAL);
+ Grasscutter.getPluginManager().registerListener(serverTickEventHandler);
- Grasscutter.getLogger().info("[GCGM] GCGM Enabled");
- Grasscutter.getLogger().info("[GCGM] You can access your GM panel by navigating to http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" +
- (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) +
- ":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 ? Grasscutter.getConfig().getDispatchOptions().PublicPort : Grasscutter.getConfig().getDispatchOptions().Port) +
- "/gm"
- );
+ Grasscutter.getLogger().info("[GCGM] GCGM Enabled");
+ Grasscutter.getLogger().info("[GCGM] You can access your GM panel by navigating to " + GCGMUtils.GetDispatchAddress() + WebUtils.PAGE_ROOT);
+ } else {
+ Grasscutter.getLogger().error("[GCGM] GCGM could not find the 'www' folder inside its plugin directory");
+ Grasscutter.getLogger().error("[GCGM] Please make sure a 'www' folder exists by extracting 'DefaultWebApp.zip' into a new 'www' or download a third-party dashboard");
+ Grasscutter.getLogger().error("[GCGM] GCGM could not be enabled");
+ }
}
@Override
public void onDisable() {
Grasscutter.getLogger().info("[GCGM] GCGM Disabled");
}
+
+ public static GCGMPlugin GetInstance() {
+ return INSTANCE;
+ }
+
+ public static WebSocketServer getWebSocketServer() {
+ return webSocketServer;
+ }
}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java
new file mode 100644
index 0000000..bc04937
--- /dev/null
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java
@@ -0,0 +1,24 @@
+package com.benj4.gcgm.handlers;
+
+import com.benj4.gcgm.GCGMPlugin;
+import com.benj4.gcgm.server.websocket.json.WSData;
+import emu.grasscutter.server.event.game.ServerTickEvent;
+import emu.grasscutter.utils.EventConsumer;
+
+import java.time.Instant;
+
+public class ServerTickHandler implements EventConsumer {
+ private static Instant lastTick;
+
+ @Override
+ public void consume(ServerTickEvent serverTickEvent) {
+ if(lastTick != null) {
+ Instant now = Instant.now();
+ long timeTaken = now.toEpochMilli() - lastTick.toEpochMilli();
+ GCGMPlugin.getWebSocketServer().broadcast(new WSData("tick", timeTaken));
+ lastTick = now;
+ } else {
+ lastTick = Instant.now();
+ }
+ }
+}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java
new file mode 100644
index 0000000..61d6e5f
--- /dev/null
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java
@@ -0,0 +1,34 @@
+package com.benj4.gcgm.server.websocket;
+
+import com.benj4.gcgm.server.websocket.json.WSData;
+import emu.grasscutter.Grasscutter;
+import express.Express;
+import io.javalin.websocket.WsContext;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class WebSocketServer {
+
+ //SocketIOServer socketIOServer;
+ private static Map userUsernameMap = new ConcurrentHashMap<>();
+
+ public void start(Express app) {
+ app.ws("/gm", ws -> {
+ ws.onConnect(ctx -> {
+ String username = "Not logged in";
+ userUsernameMap.put(ctx, username);
+ Grasscutter.getLogger().info("[GCGM] User logged in to panel");
+ });
+ });
+ }
+
+ public void broadcast(WSData data) {
+ userUsernameMap.keySet().stream().filter(ctx -> ctx.session.isOpen()).forEach(session -> {
+ session.send(data);
+ });
+ }
+}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java
new file mode 100644
index 0000000..e36a8bd
--- /dev/null
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java
@@ -0,0 +1,11 @@
+package com.benj4.gcgm.server.websocket.json;
+
+public class WSData {
+ public String eventName;
+ public Object object;
+
+ public WSData(String eventName, Object object) {
+ this.eventName = eventName;
+ this.object = object;
+ }
+}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java
new file mode 100644
index 0000000..51e3740
--- /dev/null
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java
@@ -0,0 +1,31 @@
+package com.benj4.gcgm.utils;
+
+import com.benj4.gcgm.GCGMPlugin;
+import emu.grasscutter.Grasscutter;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+public class GCGMUtils {
+
+ public static String GetDispatchAddress() {
+ return "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" +
+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) +
+ ":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0 ? Grasscutter.getConfig().getDispatchOptions().PublicPort : Grasscutter.getConfig().getDispatchOptions().Port);
+ }
+
+ public static boolean CopyFile(String resourceName, String copyLocation) {
+ try {
+ Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'");
+ Files.copy(GCGMPlugin.GetInstance().getResource(resourceName), Paths.get(new File(copyLocation).toURI()), StandardCopyOption.REPLACE_EXISTING);
+ return true;
+ } catch (IOException e) {
+ Grasscutter.getLogger().error(String.format("[GCGM] An error occurred while trying to copy '%s' to '%s'", resourceName, copyLocation));
+ e.printStackTrace();
+ return false;
+ }
+ }
+}
diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java
new file mode 100644
index 0000000..442a9a3
--- /dev/null
+++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java
@@ -0,0 +1,20 @@
+package com.benj4.gcgm.utils.web;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.utils.Utils;
+import express.Express;
+import io.javalin.http.staticfiles.Location;
+
+import java.io.File;
+import java.util.HashMap;
+
+public class WebUtils {
+
+ public static final String PAGE_ROOT = "/gm";
+
+ public static void addStaticFiles(Express app, File staticRoot) {
+ app.raw().config.precompressStaticFiles = false; // MUST BE SET TO FALSE OR FILES SUCH AS IMAGES WILL APPEAR CORRUPTED
+ app.raw().config.addStaticFiles(PAGE_ROOT, staticRoot.getAbsolutePath(), Location.EXTERNAL);
+ app.raw().config.addSinglePageRoot(PAGE_ROOT, Utils.toFilePath(staticRoot.getPath() + "/index.html"), Location.EXTERNAL);
+ }
+}
diff --git a/web-interface/package-lock.json b/web-interface/package-lock.json
index 7a7e84b..1df991e 100644
--- a/web-interface/package-lock.json
+++ b/web-interface/package-lock.json
@@ -14,10 +14,13 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
+ "chart.js": "^3.7.1",
"cross-env": "^7.0.3",
"react": "^18.0.0",
+ "react-chartjs-2": "^4.1.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1",
+ "react-use-websocket": "^3.0.0",
"web-vitals": "^2.1.4"
}
},
@@ -5115,6 +5118,11 @@
"node": ">=6"
}
},
+ "node_modules/chart.js": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
+ "integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
+ },
"node_modules/check-types": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
@@ -13321,6 +13329,15 @@
"node": ">=14"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
+ "integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
+ "peerDependencies": {
+ "chart.js": "^3.5.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -13540,6 +13557,15 @@
}
}
},
+ "node_modules/react-use-websocket": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz",
+ "integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==",
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -19858,6 +19884,11 @@
"resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz",
"integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ=="
},
+ "chart.js": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.1.tgz",
+ "integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
+ },
"check-types": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz",
@@ -25652,6 +25683,12 @@
"whatwg-fetch": "^3.6.2"
}
},
+ "react-chartjs-2": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
+ "integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
+ "requires": {}
+ },
"react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -25817,6 +25854,12 @@
"workbox-webpack-plugin": "^6.4.1"
}
},
+ "react-use-websocket": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-3.0.0.tgz",
+ "integrity": "sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==",
+ "requires": {}
+ },
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
diff --git a/web-interface/package.json b/web-interface/package.json
index 0d82e7f..fa48038 100644
--- a/web-interface/package.json
+++ b/web-interface/package.json
@@ -9,10 +9,13 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
+ "chart.js": "^3.7.1",
"cross-env": "^7.0.3",
"react": "^18.0.0",
+ "react-chartjs-2": "^4.1.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1",
+ "react-use-websocket": "^3.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/web-interface/src/App.css b/web-interface/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/web-interface/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/web-interface/src/App.js b/web-interface/src/App.js
index 4cbc81d..a2e6d61 100644
--- a/web-interface/src/App.js
+++ b/web-interface/src/App.js
@@ -1,4 +1,3 @@
-import './App.css';
import Dashboard from './Dashboard';
function App() {
diff --git a/web-interface/src/Components/NavigationButton.js b/web-interface/src/Components/Dashboard/Sidepanel/NavigationButton.js
similarity index 100%
rename from web-interface/src/Components/NavigationButton.js
rename to web-interface/src/Components/Dashboard/Sidepanel/NavigationButton.js
diff --git a/web-interface/src/Components/Sidepanel.css b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.css
similarity index 100%
rename from web-interface/src/Components/Sidepanel.css
rename to web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.css
diff --git a/web-interface/src/Components/Sidepanel.js b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js
similarity index 94%
rename from web-interface/src/Components/Sidepanel.js
rename to web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js
index 1fe96c9..0572a31 100644
--- a/web-interface/src/Components/Sidepanel.js
+++ b/web-interface/src/Components/Dashboard/Sidepanel/Sidepanel.js
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
-import gclogo from "../img/grasscutter-icon.png"
+import gclogo from "../../../img/grasscutter-icon.png"
import NavigationButton from './NavigationButton'
import SimpleButton from './SimpleButton'
import { faArrowLeft, faCogs, faGamepad, faHome, faNetworkWired } from '@fortawesome/free-solid-svg-icons'
diff --git a/web-interface/src/Components/SimpleButton.js b/web-interface/src/Components/Dashboard/Sidepanel/SimpleButton.js
similarity index 100%
rename from web-interface/src/Components/SimpleButton.js
rename to web-interface/src/Components/Dashboard/Sidepanel/SimpleButton.js
diff --git a/web-interface/src/Components/Dashboard/StatsWidget.css b/web-interface/src/Components/Dashboard/StatsWidget.css
new file mode 100644
index 0000000..9046264
--- /dev/null
+++ b/web-interface/src/Components/Dashboard/StatsWidget.css
@@ -0,0 +1,12 @@
+.graph {
+ position: absolute;
+ transform: translate(-50%, 0%);
+ top: 6%;
+ left: 50%;
+ border-color: grey;
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 8px;
+ width: 800px;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/web-interface/src/Components/Dashboard/StatsWidget.js b/web-interface/src/Components/Dashboard/StatsWidget.js
new file mode 100644
index 0000000..d6170c4
--- /dev/null
+++ b/web-interface/src/Components/Dashboard/StatsWidget.js
@@ -0,0 +1,94 @@
+import React, { useState, useEffect } from "react";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+ } from 'chart.js';
+import { useWebSocket, ReadyState } from "react-use-websocket/dist/lib/use-websocket";
+import { Line } from "react-chartjs-2";
+
+import "./StatsWidget.css";
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ );
+
+ const CHART_COLORS = {
+ red: 'rgb(255, 99, 132)',
+ orange: 'rgb(255, 159, 64)',
+ yellow: 'rgb(255, 205, 86)',
+ green: 'rgb(75, 192, 192)',
+ blue: 'rgb(54, 162, 235)',
+ purple: 'rgb(153, 102, 255)',
+ grey: 'rgb(201, 203, 207)'
+ };
+
+const TICK_COUNT = 50;
+const labels = [];
+for (let i = 0; i <= TICK_COUNT; ++i) {
+ labels.push(i.toString());
+}
+
+export default function StatsWidget() {
+ const protocol = window.location.protocol === "https:" ? "wss" : "wss"
+ const host = window.location.host;
+ const port = window.location.port;
+
+ const { sendMessage, lastMessage, readyState } = useWebSocket(protocol + "://"+ host +":" + port + "/gm");
+ const [graphHistory, setGraphHistory] = useState([]);
+
+ useEffect(() => {
+ if (lastMessage !== null) {
+ const data = JSON.parse(lastMessage.data);
+ var newGraph = graphHistory;
+ newGraph.push({ time: new Date().getTime(), tick: data.object});
+ if(newGraph.length > TICK_COUNT) {
+ newGraph.splice(0, 1);
+ }
+ setGraphHistory(newGraph);
+ }
+ }, [lastMessage, setGraphHistory]);
+
+ /*const connectionStatus = {
+ [ReadyState.CONNECTING]: "Connecting",
+ [ReadyState.OPEN]: "Open",
+ [ReadyState.CLOSING]: "Closing",
+ [ReadyState.CLOSED]: "Closed",
+ [ReadyState.UNINSTANTIATED]: "Uninstantiated",
+ }[readyState];*/
+
+ return (
+
+
Server Performance (Ticks)
+
l.tick),
+ borderColor: CHART_COLORS.red,
+ fill: false,
+ cubicInterpolationMode: 'monotone',
+ tension: 0.4
+ }
+ ]
+ }}
+ options={{
+ y: {
+ suggestedMin: 995,
+ suggestedMax: 1005
+ }
+ }}/>
+
+ );
+}
diff --git a/web-interface/src/Dashboard.js b/web-interface/src/Dashboard.js
index 3bd5c7f..49663df 100644
--- a/web-interface/src/Dashboard.js
+++ b/web-interface/src/Dashboard.js
@@ -1,5 +1,6 @@
import React, { Component } from 'react'
-import Sidepanel from './Components/Sidepanel'
+import Sidepanel from './Components/Dashboard/Sidepanel/Sidepanel'
+import StatsWidget from './Components/Dashboard/StatsWidget'
import './Dashboard.css'
@@ -8,6 +9,7 @@ export default class Dashboard extends Component {
return (
+
)
}