diff --git a/building/libs.xml b/building/libs.xml index 9eaf4de4e6..f78849581d 100644 --- a/building/libs.xml +++ b/building/libs.xml @@ -16,6 +16,7 @@ + diff --git a/project.xml b/project.xml index b129dbb377..3bcdd92c26 100644 --- a/project.xml +++ b/project.xml @@ -130,6 +130,8 @@ + + diff --git a/source/funkin/backend/system/net/FunkinPacket.hx b/source/funkin/backend/system/net/FunkinPacket.hx new file mode 100644 index 0000000000..a1de35e480 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinPacket.hx @@ -0,0 +1,65 @@ +package funkin.backend.system.net; + +import flixel.util.typeLimit.OneOfTwo; + +import haxe.io.Bytes; +import haxe.io.BytesOutput; + +class FunkinPacket implements haxe.Constraints.IMap { + // Status of the packet. 200 is an OK response. + public var status:Int = -1; + + // The JSON fields of the packet, as a Map. + private var fields:Map = []; + + // If the recieved data is binary or contains binary, this will contain the raw bytes. + public var bytes:Bytes = null; + + public function new() { } + + public static function fromJson(json:OneOfTwo>):Null { + var packet = new FunkinPacket(); + packet.appendJson(json); + return packet; + } + + public function appendJson(json:OneOfTwo>):Void { + var parsedJson:haxe.DynamicAccess = (json is String) ? haxe.Json.parse(json) : json; + if (parsedJson == null) return; + + for (key => value in parsedJson) this.set(key, value); + } + + public function toJson():haxe.DynamicAccess { + var json:haxe.DynamicAccess = {}; + for (key => value in fields) json[key] = value; + return json; + } + + inline public function stringify():String { return haxe.Json.stringify(toJson()); } + + public function toBytes():Bytes { + var output = new BytesOutput(); + output.writeString(haxe.Json.stringify(toJson())); + return output.getBytes(); + } + + inline public function get(key:String):Null { return fields.get(key); } + inline public function set(key:String, value:Dynamic):Void { fields.set(key, value); } + inline public function exists(key:String):Bool { return fields.exists(key); } + inline public function remove(key:String):Bool { return fields.remove(key); } + + inline public function keys():Iterator { return fields.keys(); } + inline public function iterator():Iterator { return fields.iterator(); } + inline public function keyValueIterator():KeyValueIterator { return fields.keyValueIterator(); } + + public function copy():FunkinPacket { + var copy = new FunkinPacket(); + copy.fields = this.fields.copy(); + copy.status = this.status; + return copy; + } + + inline public function clear():Void { fields.clear(); } + inline public function toString():String { return 'FunkinPacket (Status: $status)'; } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/FunkinSocket.hx b/source/funkin/backend/system/net/FunkinSocket.hx new file mode 100644 index 0000000000..ef77fd3602 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinSocket.hx @@ -0,0 +1,129 @@ +package funkin.backend.system.net; + +#if sys +import sys.net.Host; +import sys.net.Socket as SysSocket; +import haxe.io.Bytes; + +@:keep +class FunkinSocket implements IFlxDestroyable { + public var socket:SysSocket = new SysSocket(); + + public var metrics:Metrics = new Metrics(); + + public var FAST_SEND(default, set):Bool = true; + private function set_FAST_SEND(value:Bool):Bool { + FAST_SEND = value; + socket.setFastSend(value); + return value; + } + public var BLOCKING(default, set):Bool = false; + private function set_BLOCKING(value:Bool):Bool { + BLOCKING = value; + socket.setBlocking(value); + return value; + } + + public var host:Host; + public var port:Int; + + public function new(?_host:String = "127.0.0.1", ?_port:Int = 5000) { + FAST_SEND = true; + BLOCKING = false; + this.host = new Host(_host); + this.port = _port; + } + + // Reading Area + public function readAll():Null { + try { + var bytes = this.socket.input.readAll(); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function readLine():Null { + try { + var bytes = this.socket.input.readLine(); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function read(nBytes:Int):Null { + try { + var bytes = this.socket.input.read(nBytes); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function readBytes(bytes:Bytes):Int { + try { + var length = this.socket.input.readBytes(bytes, 0, bytes.length); + metrics.updateBytesReceived(length); + return length; + } catch(e) { } + return 0; + } + + // Writing Area + public function prepare(nbytes:Int):Void { socket.output.prepare(nbytes); } + public function write(bytes:Bytes):Bool { + try { + this.socket.output.write(bytes); + metrics.updateBytesSent(bytes.length); + return true; + } catch (e) { } + return false; + } + public function writeString(str:String):Bool { + try { + this.socket.output.writeString(str); + metrics.updateBytesSent(Bytes.ofString(str).length); + return true; + } catch(e) { } + return false; + } + + public function bind(?expectingConnections:Int = 1):FunkinSocket { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Binding to ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + socket.bind(host, port); + socket.listen(expectingConnections); + return this; + } + + public function connect():FunkinSocket { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Connecting to ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + socket.connect(host, port); + return this; + } + + public function close() { + try { + if (socket != null) socket.close(); + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Closing socket from ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + } catch(e) { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Failed to close socket: ${e}', NONE), + ]); + } + } + + public function destroy() { close(); } +} +#end \ No newline at end of file diff --git a/source/funkin/backend/system/net/FunkinWebSocket.hx b/source/funkin/backend/system/net/FunkinWebSocket.hx new file mode 100644 index 0000000000..109d49fe91 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinWebSocket.hx @@ -0,0 +1,228 @@ +package funkin.backend.system.net; + +import flixel.util.typeLimit.OneOfThree; + +import haxe.io.Bytes; + +import flixel.util.FlxSignal.FlxTypedSignal; +import hx.ws.Log as LogWs; +import hx.ws.WebSocket; +import hx.ws.Types.MessageType; + +/** +* This is a wrapper for hxWebSockets. Used in-tangem with `FunkinPacket` and `Metrics`. +* You can override how `haxe.io.Bytes` is decoded by setting `AUTO_DECODE_PACKETS`. By default it will attempt to deserialize the packet into a `FunkinPacket`. +* It also has `Metrics` which keeps track of the amount of bytes sent and received. +**/ +class FunkinWebSocket implements IFlxDestroyable { + /** + * This interacts with the hxWebSockets logging system, probably the best way to get the debug info. + * Although, it's not in the format of CodenameEngine's logs so it might look weird. + **/ + private static var LOG_INFO(default, set):Bool = false; + private static function set_LOG_INFO(value:Bool):Bool { + LOG_INFO = value; + if (LOG_INFO) LogWs.mask |= LogWs.INFO; + else LogWs.mask &= ~LogWs.INFO; + return value; + } + /** + * Ditto to LOG_INFO + **/ + private static var LOG_DEBUG(default, set):Bool = false; + private static function set_LOG_DEBUG(value:Bool):Bool { + LOG_DEBUG = value; + if (LOG_DEBUG) LogWs.mask |= LogWs.DEBUG; + else LogWs.mask &= ~LogWs.DEBUG; + return value; + } + /** + * Ditto to LOG_INFO + **/ + private static var LOG_DATA(default, set):Bool = false; + private static function set_LOG_DATA(value:Bool):Bool { + LOG_DATA = value; + if (LOG_DATA) LogWs.mask |= LogWs.DATA; + else LogWs.mask &= ~LogWs.DATA; + return value; + } + + @:dox(hide) private var _ws:WebSocket; + + /** + * This keeps track of the amount of bytes sent and received. + * You can set the logging state directly in the class. + **/ + public var metrics:Metrics = new Metrics(); + + /** + * This signal is only called once when the connection is opened. + **/ + public var onOpen:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** + * This signal is called every time a message is received. + * It can be one of three types: String, Bytes, or FunkinPacket. + * If you have AUTO_DECODE_PACKETS set to true, It will attempt to deserialize the packet into a FunkinPacket. + * If it fails to deserialize or AUTO_DECODE_PACKETS is false, it will just return the Bytes directly. + **/ + public var onMessage:FlxTypedSignal->Void> = new FlxTypedSignal->Void>(); // cursed 😭😭 + /** + * This signal is only called once when the connection is closed. + **/ + public var onClose:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** + * This signal is only called when an error occurs. Useful for debugging and letting the user know something has gone wrong. + **/ + public var onError:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + /** + * The URL to connect to, including the protocol (ws:// or wss://). Currently wss:// (Secure WebSocket) is not supported. + **/ + public var url:String; + + /** + * This just allows you to override or add custom headers when the handshake happens, as this is the first and last time we use proper HTTP Headers. + **/ + public var handshakeHeaders(get, null):Map; + public function get_handshakeHeaders():Map { return this._ws.additionalHeaders; } + + /** + * If true, the packets will be automatically deserialized into a `FunkinPacket` if possible. + * Otherwise you will handle everything manually. + **/ + public var AUTO_DECODE_PACKETS:Bool = true; + + /** + * @param _url The URL to connect to, including the protocol (ws:// or wss://). Currently wss:// (SSH) is not supported. + **/ + public function new(_url:String) { + this.url = _url; + + this._ws = new WebSocket(this.url, false); + this._ws.onopen = _onOpen; + this._ws.onmessage = _onMessage; + this._ws.onclose = _onClose; + this._ws.onerror = _onError; + } + + /** + * Internal function for handling the onMessage event. + * @param message The message to handle from hxWebSockets. + **/ + private function _onMessage(message:MessageType):Void { + var data:OneOfThree = ""; + + switch(message) { + case StrMessage(content): + data = content; + metrics.updateBytesReceived(Bytes.ofString(content).length); + // Can we just have a switch break in Haxe for the life of me bro 💔💔💔 + if (AUTO_DECODE_PACKETS) { + var packet:FunkinPacket = FunkinPacket.fromJson(cast(data, String)); + if (packet != null) { + packet.status = 200; + + data = packet; + } + } + case BytesMessage(buffer): + metrics.updateBytesReceived(buffer.length); + if (AUTO_DECODE_PACKETS) { + var packet:FunkinPacket = new FunkinPacket(); + packet.bytes = buffer.readAllAvailableBytes(); + packet.status = 200; + data = packet; + } else + data = buffer.readAllAvailableBytes(); + default: trace('Unknown message type: ${message}'); + } + + onMessage.dispatch(data); + } + + /** + * Internal function for handling the onOpen event. + **/ + private function _onOpen():Void { + onOpen.dispatch(); + } + + /** + * Internal function for handling the onClose event. + **/ + private function _onClose():Void { + onClose.dispatch(); + } + + /** + * Internal function for handling the onError event. + **/ + private function _onError(error:Dynamic):Void { + onError.dispatch(error); + } + + /** + * Opens the WebSocket connection. + **/ + public function open():FunkinWebSocket { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Opening WebSocket to ', NONE), Logs.logText(url, YELLOW), + ]); + try { + this._ws.open(); + } catch(e) { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Failed to open WebSocket: ${e}', NONE), + ]); + onError.dispatch(e); + } + return this; + } + + /** + * Sends data to the server. + * @param data The data to send. + * @return true if the data was sent successfully, false if it failed. + **/ + public function send(data:Dynamic):Bool { + try { + if (data is FunkinPacket) + this._ws.send(data.stringify()); + else + this._ws.send(data); + + if (metrics.IS_LOGGING) { + if (data is String) metrics.updateBytesSent(Bytes.ofString(data).length); + else if (data is Bytes) metrics.updateBytesSent(data.length); + else if (data is FunkinPacket) metrics.updateBytesSent(data.toBytes().length); + } + + return true; + } catch(e) { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Failed to send data: ${e}', NONE), + ]); + } + return false; + } + + /** + * Closes the WebSocket connection. + * Once you close the connection, you cannot reopen it, you must create a new instance. + **/ + public function close():Void { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Closing WebSocket from ', NONE), Logs.logText(url, YELLOW), + ]); + this._ws.close(); + } + + /** + * Basically the same as close(), but if a class is handling it and expects it to be IFlxDestroyable compatable, it will call this. + **/ + public function destroy():Void { close(); } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/Metrics.hx b/source/funkin/backend/system/net/Metrics.hx new file mode 100644 index 0000000000..a417974120 --- /dev/null +++ b/source/funkin/backend/system/net/Metrics.hx @@ -0,0 +1,40 @@ +package funkin.backend.system.net; + +import flixel.util.FlxSignal.FlxTypedSignal; + +class Metrics { + public var bytesSent:Int = 0; + public var bytesReceived:Int = 0; + + public var packetsSent:Int = 0; + public var packetsReceived:Int = 0; + + public var IS_LOGGING:Bool = true; + + public var onBytesSent:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + public var onBytesReceived:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + public function new() { } + + public function updateBytesSent(amount:Int) { + if (!IS_LOGGING) return; + + bytesSent += amount; + packetsSent++; + + onBytesSent.dispatch(amount); + } + + public function updateBytesReceived(amount:Int) { + if (!IS_LOGGING) return; + + bytesReceived += amount; + packetsReceived++; + + onBytesReceived.dispatch(amount); + } + + public function toString():String { + return '(Metrics) $bytesSent bytes sent | $bytesReceived bytes received | $packetsSent packets sent | $packetsReceived packets received'; + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/Socket.hx b/source/funkin/backend/system/net/Socket.hx deleted file mode 100644 index 19ebf9a116..0000000000 --- a/source/funkin/backend/system/net/Socket.hx +++ /dev/null @@ -1,65 +0,0 @@ -package funkin.backend.system.net; - -#if sys -import sys.net.Host; -import sys.net.Socket as SysSocket; - -@:keep -class Socket implements IFlxDestroyable { - public var socket:SysSocket; - - public function new(?socket:SysSocket) { - this.socket = socket; - if (this.socket == null) - this.socket = new SysSocket(); - this.socket.setFastSend(true); - this.socket.setBlocking(false); - } - - public function read():String { - try { - return this.socket.input.readUntil('\n'.code).replace("\\n", "\n"); - } catch(e) { - - } - return null; - } - - public function write(str:String):Bool { - try { - this.socket.output.writeString(str.replace("\n", "\\n")); - return true; - } catch(e) { - - } - return false; - } - - public function host(host:Host, port:Int, nbConnections:Int = 1) { - socket.bind(host, port); - socket.listen(nbConnections); - socket.setFastSend(true); - } - - public function hostAndWait(h:Host, port:Int) { - host(h, port); - return acceptConnection(); - } - - public function acceptConnection():Socket { - socket.setBlocking(true); - var accept = new Socket(socket.accept()); - socket.setBlocking(false); - return accept; - } - - public function connect(host:Host, port:Int) { - socket.connect(host, port); - } - - public function destroy() { - if (socket != null) - socket.close(); - } -} -#end \ No newline at end of file