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