diff --git a/libs.xml b/libs.xml
index 570b451c2..258a6279f 100644
--- a/libs.xml
+++ b/libs.xml
@@ -13,6 +13,7 @@
+
diff --git a/project.xml b/project.xml
index 382ed9f65..efe8f52f3 100644
--- a/project.xml
+++ b/project.xml
@@ -182,6 +182,8 @@
+
+
diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx
index a5011d6d6..1e787754e 100644
--- a/source/funkin/backend/system/MainState.hx
+++ b/source/funkin/backend/system/MainState.hx
@@ -8,6 +8,7 @@ import funkin.menus.TitleState;
import funkin.menus.BetaWarningState;
import funkin.backend.chart.EventsData;
import flixel.FlxState;
+import funkin.backend.system.net.WebSocketPacket;
/**
* Simple state used for loading the game
@@ -21,6 +22,9 @@ class MainState extends FlxState {
Main.loadGameSettings();
initiated = true;
+ // Resetting to default because I doubt any modding the engine will reset it lmao
+ WebSocketPacket.packetTypes = WebSocketPacket.default_packetTypes;
+
#if sys
CoolUtil.deleteFolder('./.temp/'); // delete temp folder
#end
diff --git a/source/funkin/backend/system/macros/Macros.hx b/source/funkin/backend/system/macros/Macros.hx
index bb0b4972e..82f8abf10 100644
--- a/source/funkin/backend/system/macros/Macros.hx
+++ b/source/funkin/backend/system/macros/Macros.hx
@@ -23,9 +23,9 @@ class Macros {
#if VIDEO_CUTSCENES "hxvlc.flixel", "hxvlc.openfl", #end
#if NAPE_ENABLED "nape", "flixel.addons.nape", #end
// BASE HAXE
- "DateTools", "EReg", "Lambda", "StringBuf", "haxe.crypto", "haxe.display", "haxe.exceptions", "haxe.extern", "scripting"
+ "DateTools", "EReg", "Lambda", "StringBuf", "haxe.crypto", "haxe.display", "haxe.exceptions", "haxe.extern", "scripting",
])
- Compiler.include(inc);
+ Compiler.include(inc);
var isHl = Context.defined("hl");
@@ -40,6 +40,7 @@ class Macros {
}
}
+ Compiler.include("hx.ws", ["hx.ws.cs", "hx.ws.java", "hx.ws.nodejs"]);
Compiler.include("funkin", [#if !UPDATE_CHECKING 'funkin.backend.system.updating' #end]);
}
diff --git a/source/funkin/backend/system/net/WebSocketPacket.hx b/source/funkin/backend/system/net/WebSocketPacket.hx
new file mode 100644
index 000000000..f5d89da4a
--- /dev/null
+++ b/source/funkin/backend/system/net/WebSocketPacket.hx
@@ -0,0 +1,150 @@
+package funkin.backend.system.net;
+
+import funkin.backend.assets.ModsFolder;
+import funkin.backend.system.macros.GitCommitMacro;
+import funkin.backend.utils.DiscordUtil;
+
+import haxe.Serializer;
+
+import Date;
+import StringBuf;
+import String;
+import Type;
+
+/**
+* A data object that can be customized for `WebSocketUtil` to send data to the server.
+* You will need to handle the custom packet on your server yourself.
+*
+* GENERAL WEBSOCKET WARNING: Sending data to the server on `update` SHOULD NEVER BE DONE!!
+* It will be slow and generally not a good idea. It might overload the server and cause unforseen issues.
+*
+* Why use a packet class instead of sending your own data? Well this Serializes the data and handles it for you, so all you do is just send the class in the `WebSocketUtil.send` and thats it.
+**/
+class WebSocketPacket {
+
+ /**
+ * Packet Types that can be gathered from the server. Used by `WebSocketUtil`
+ * If your server doesn't use the Template ItsLJcool made, then you add your own here.
+ **/
+ public static var packetTypes:Map = [
+ "haxe" => {params: "!HXP", none: "!HXp"},
+ "javascript" => {params: "!JSP", none: "!JSp"},
+ "js" => {params: "!JSP", none: "!JSp"},
+ ];
+
+ @:dox(hide)
+ public static var default_packetTypes(default, never):Map = [
+ "haxe" => {params: "!HXP", none: "!HXp"},
+ "javascript" => {params: "!JSP", none: "!JSp"},
+ "js" => {params: "!JSP", none: "!JSp"},
+ ];
+
+ /**
+ * Just normal data that is being held for the packet to get stringified.
+ **/
+ private var packetData(default, set):Dynamic = {};
+ private function set_packetData(value:Dynamic):Dynamic {
+ if (value == null) return {};
+ if (value is String) value = haxe.Json.parse(value);
+ return this.packetData = value;
+ }
+
+ /**
+ * The name of the event the server handles.
+ * If null it won't be added in the packet.
+ **/
+ public var packetEventName(default, set):String;
+ private function set_packetEventName(value:Null):String {
+ if (value == null) return this.packetEventName = "";
+ return this.packetEventName = value;
+ }
+
+ @:dox(hide) private var add_meta_data:Bool = true;
+
+ /**
+ * @param packetName The name of the event the server handles.
+ * @param packetData The data that is being sent to the server. Can also be a stringified JSON.
+ * @param add_meta_data If true, adds metadata to the packet. This is useful for data like the time it was sent,
+ **/
+ public function new(packetName:Null, ?packetData:Null, ?_add_meta_data:Bool = true) {
+ this.packetEventName = packetName;
+ this.add_meta_data = _add_meta_data;
+
+ this.packetData = packetData;
+
+ if (this.add_meta_data) {
+ try {
+ if (ModsFolder.currentModFolder != null) this.packetData.__mod = ModsFolder.currentModFolder;
+ this.packetData.__commitHash = GitCommitMacro.commitHash; // for checking outdated action builds on the server. its gonna be peak trust.
+ // if Discord isn't active, dont send the metadata
+ if (DiscordUtil.ready) this.packetData.__discord = { username: DiscordUtil.user.username, globalName: DiscordUtil.user.globalName, premiumType: DiscordUtil.user.premiumType };
+ } catch (e:Dynamic) {
+ trace("Error adding metadata to packet: " + e);
+ }
+ }
+
+ }
+
+ /**
+ * Checks if the packet has the field.
+ * @param field The field to check for
+ * @return If the packet has the field.
+ **/
+ public function exists(field:String):Bool {
+ return Reflect.hasField(this.packetData, field);
+ }
+
+ /**
+ * Gets the packet field.
+ * @param field The field to get the value.
+ * @return the value of the field.
+ **/
+ public function get(field:String):Dynamic {
+ return Reflect.field(this.packetData, field);
+ }
+
+ /**
+ * Sets a value to the packet.
+ * @param field The field to get the value.
+ * @param value The value to set.
+ * @return the packet data as a JSON structure.
+ **/
+ public function set(field:String, value:Dynamic) {
+ Reflect.setField(this.packetData, field, value);
+ return this.packetData;
+ }
+
+ /**
+ * Converts the packet to a string. Uses `Serializer` to convert the packet to a data string
+ * @return The packet as a string.
+ **/
+ public function toString():String {
+ var buffer = new StringBuf();
+
+ // if no name is associated with packet, just serialize the data
+ if (this.packetEventName.trim() != "") {
+ buffer.add('!HXP');
+ buffer.add(this.packetEventName);
+ buffer.add('=>');
+ } else {
+ buffer.add('!HXp');
+ }
+
+ if (add_meta_data) this.packetData.__timestamp = Date.now();
+
+ var cerial = new Serializer();
+ if (this.packetData != {}) cerial.serialize(this.packetData);
+ return '${buffer.toString()}${cerial.toString()}';
+ }
+
+ public static function isServerPacket(data:Dynamic):Bool {
+ if ((data is ServerPacketData)) return true;
+ return false;
+ }
+}
+
+@:structInit
+class ServerPacketData {
+ public var name:String;
+ public var data:Dynamic;
+}
\ No newline at end of file
diff --git a/source/funkin/backend/system/net/WebSocketUtil.hx b/source/funkin/backend/system/net/WebSocketUtil.hx
new file mode 100644
index 000000000..a9e564d7e
--- /dev/null
+++ b/source/funkin/backend/system/net/WebSocketUtil.hx
@@ -0,0 +1,259 @@
+package funkin.backend.system.net;
+
+import hx.ws.*;
+
+import funkin.backend.system.net.WebSocketPacket.ServerPacketData;
+import funkin.backend.system.Logs;
+import haxe.Unserializer;
+
+import flixel.util.FlxTimer;
+
+/**
+* Basically a Utility for HScript to use WebSockets. Adds safeguards, error handling, and logging to debug your WebSockets.
+* YOU WILL NEED TO HANDLE HOW THE WEBSOCKETS ARE CLOSED!!! calling `destroy` will close the WebSocket.
+* ItsLJcool wanted to make CodenameEngine Online / Multiplayer. This will make it easier to do so.
+*
+* This does NOT support making a Server Side WebSocket. Its only for Client Side WebSockets. If you want to make a Server you need to that yourself.
+* I'd suggest using JavaScript for it. Though any program will do.
+*
+* Check out the WebSocket Server Template for Codename Engine here:
+* https://github.com/ItsLJcool/WebSocket-Server-Template-for-CNE
+**/
+class WebSocketUtil implements IFlxDestroyable {
+ /**
+ * Used for the `toggleLogging` function. this is more of a data handler for the function.
+ **/
+ static var loggingEnabled:Bool = false;
+
+ /**
+ * Call this function to toggle debugging for the WebSocket.
+ **/
+ static function toggleLogging(?INFO:Bool = true, ?DEBUG:Bool = true, ?DATA:Bool = true) {
+ loggingEnabled = !loggingEnabled;
+ if (!loggingEnabled) return Log.mask = 0;
+
+ var _mask = Log.mask;
+ if (INFO) _mask = _mask | Log.INFO;
+ if (DEBUG) _mask = _mask | Log.DEBUG;
+ if (DATA) _mask = _mask | Log.DATA;
+ Log.mask = _mask;
+
+ return Log.mask;
+ }
+
+ /**
+ * Function calls after the WebSocket has been opened.
+ * @param webSocket Returns the instance of the WebSocket.
+ **/
+ public var onOpen:WebSocketUtil->Void = (webSocket)->{};
+
+ /**
+ * Whenever the WebSocket receives a message sent from the server.
+ * @param message Returns the message sent from the server.
+ **/
+ public var onMessage:Dynamic->Void = (message)->{};
+
+ /**
+ * Runs whenever the WebSocket closes.
+ **/
+ public var onClose:Void->Void = ()->{};
+
+ /**
+ * Runs whenever the WebSocket encounters an error.
+ **/
+ public var onError(default, set):Dynamic->Void = (error)->{};
+ private function set_onError(_errorFunc):Dynamic->Void {
+ var func = (error)->{
+ Logs.traceColored([
+ Logs.logText("[WebSocket Error] ", RED),
+ Logs.logText('${error}'),
+ ], ERROR);
+ if (_errorFunc != null) _errorFunc(error);
+ if (this.closeOnError) this.close();
+ };
+ return this.onError = func;
+ }
+
+ /**
+ * If true, the WebSocket will close when an error occurs.
+ **/
+ public var closeOnError:Bool = false;
+
+ @:dox(hide) private var url:String;
+ @:dox(hide) private var webSocket:WebSocket;
+
+ /**
+ * If true, when you call `open` the WebSocket will attempt to connect in a new thread.
+ * Usefull for trying to connect to a WebSocket Server whilst the game is running.
+ * WARNING: CAN CAUSE ERRORS IN HSCRIPT!!
+ **/
+ public var _threadedConnection:Bool = false;
+
+ /**
+ * If true, when you call `send` the WebSocket will attempt to send in a new thread.
+ * WARNING: CAN CAUSE ERRORS IN HSCRIPT!!
+ **/
+ public var _threadedSend:Bool = false;
+
+ @:dox(hide) public var __packets:Array = [];
+
+ /**
+ * @param url The URL of the WebSocket. Usually `ws://localhost:port`.
+ * @param onOpen sets the `onOpen` function directly to the class.
+ * @param immediateOpen If true, the WebSocket will open immediately. Hence why `onOpen` is a function in the parameters.
+ **/
+ public function new(url:String, ?onOpen:WebSocketUtil->Void, ?immediateOpen:Bool = false) {
+ this.onOpen = (onOpen == null) ? this.onOpen : onOpen;
+ this.onError = this.onError;
+
+ this.url = url;
+ this.webSocket = new WebSocket(this.url, false);
+
+ this.webSocket.onopen = function() {
+ try {
+ this.onOpen(this);
+ } catch(error) {
+ this.onError(error);
+ }
+ };
+
+ this.webSocket.onmessage = function(message) {
+ var data:Dynamic = null;
+ try {
+ switch(message) {
+ case StrMessage(str):
+ data = str;
+ var _data = this.attemptDeserialize(data);
+ if (WebSocketPacket.isServerPacket(_data)) data = _data;
+ case BytesMessage(bytes):
+ data = bytes;
+ }
+ this.onMessage(data);
+ } catch(e) {
+ this.onError(e);
+ }
+ __packets.push(data);
+ };
+
+ this.webSocket.onclose = function() {
+ try {
+ this.onClose();
+ } catch(e) {
+ this.onError(e);
+ }
+ };
+
+ this.webSocket.onerror = this.onError;
+
+ if (immediateOpen) this.open();
+ }
+
+ public function getRecentPacket():Dynamic {
+ return __packets.shift();
+ }
+
+ /**
+ * @param rawData The raw data from the server
+ * @return The packet data if it was found, otherwise null and WebSocketUtil will handle it.
+ */
+ private function attemptDeserialize(rawData:String):Null {
+ if (!rawData.startsWith("!")) return null;
+
+ for (key=>value in WebSocketPacket.packetTypes) {
+ var hasPacketData = rawData.startsWith(value.params); // PREFIXname=>DATA
+ var hasPacketNone = rawData.startsWith(value.none); // PREFIX=>DATA
+
+ if (hasPacketNone) {
+ var data = rawData.substr(rawData.indexOf("=>") + 2);
+ var packetData:Dynamic = Unserializer.run(data);
+ if (packetData == null) packetData = {};
+ var packet:ServerPacketData = { name: null, data: packetData };
+ return packet;
+ }
+
+ if (!hasPacketData) continue;
+
+ try {
+ var data = rawData.substr(rawData.indexOf("=>") + 2);
+ var name = rawData.substring(value.params.length, rawData.indexOf("=>"));
+ var packetData:Dynamic = Unserializer.run(data);
+ if (packetData == null) packetData = {};
+ var packet:ServerPacketData = { name: name, data: packetData };
+ return packet;
+ } catch (e:Dynamic) {
+ trace('Error parsing packet: ${e}');
+ return null;
+ }
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Opens the WebSocket.
+ **/
+ public function open() {
+ Logs.traceColored([
+ Logs.logText("[WebSocket Connection] ", BLUE),
+ Logs.logText('Connecting to ${this.url}'),
+ ], INFO);
+
+ var _func = () -> {
+ try {
+ this.webSocket.open();
+ } catch(e) {
+ this.onError(e);
+ return;
+ }
+ Logs.traceColored([
+ Logs.logText("[WebSocket Connection] ", YELLOW),
+ Logs.logText('Connected to ${this.url}'),
+ ], INFO);
+ };
+ if (this._threadedConnection) Main.execAsync(_func);
+ else _func();
+ }
+
+ /**
+ * Closes the WebSocket.
+ **/
+ public function close() {
+ Logs.traceColored([
+ Logs.logText("[WebSocket Connection] ", BLUE),
+ Logs.logText('Closing connection to ${this.url}'),
+ ], INFO);
+
+ try {
+ this.webSocket.close();
+ this._isClosed = true;
+ } catch(e) {
+ this.onError(e);
+ }
+ }
+
+ /**
+ * Sends data to the server
+ **/
+ public function send(data) {
+ var _data = null;
+ if (data is WebSocketPacket) _data = data.toString();
+ else _data = data;
+ try {
+ this.webSocket.send(_data);
+ } catch(e) {
+ this.onError(e);
+ }
+ }
+
+ private var _isClosed:Bool = false;
+
+ /**
+ * Closes the WebSocket and destroys the class instance.
+ **/
+ public function destroy() {
+ if (this._isClosed) return;
+
+ this.close();
+ }
+}
\ No newline at end of file