diff --git a/building/libs.xml b/building/libs.xml
index 139cbaf8a..ee816439a 100644
--- a/building/libs.xml
+++ b/building/libs.xml
@@ -18,6 +18,7 @@
+
diff --git a/project.xml b/project.xml
index 3d6399a14..8d5da9ca8 100644
--- a/project.xml
+++ b/project.xml
@@ -127,6 +127,8 @@
+
+
diff --git a/source/funkin/backend/system/net/FunkinPacket.hx b/source/funkin/backend/system/net/FunkinPacket.hx
new file mode 100644
index 000000000..6c1c9c0d4
--- /dev/null
+++ b/source/funkin/backend/system/net/FunkinPacket.hx
@@ -0,0 +1,76 @@
+package funkin.backend.system.net;
+
+import flixel.util.typeLimit.OneOfTwo;
+
+import haxe.io.Bytes;
+import haxe.io.BytesOutput;
+
+class FunkinPacket {
+ public var status:Int = -1;
+ public var head:String;
+ public var fields:Map = new Map();
+ public var body:OneOfTwo;
+
+ public function new(_head:String, ?_body:OneOfTwo = "", ?_status:Int = -1) {
+ this.head = _head.trim();
+ this.body = (_body is String) ? _body.trim() : _body;
+ this.status = _status;
+ }
+
+ public function set(name:String, value:String):FunkinPacket { fields.set(name, value); return this; }
+ inline public function get(name:String):String { return fields.get(name); }
+ inline public function exists(name:String):Bool { return fields.exists(name); }
+ inline public function remove(name:String):Bool { return fields.remove(name); }
+ inline public function keys():Iterator { return fields.keys(); }
+
+ public function toString(?includeBody:Bool = true):String {
+ var str:String = '';
+ if (head.length > 0) str += '$head\r\n';
+ for (key in keys()) str += '$key: ${get(key)}\r\n';
+ if (!includeBody) return str;
+ if (body is String) str += body;
+ else str += (cast body : Bytes).toString();
+ return str;
+ }
+ public function toBytes():Bytes {
+ var bytes:BytesOutput = new BytesOutput();
+ bytes.writeString(toString(false)); // Absolute Cinema, thanks AbstractAndrew for the Revolutionary Idea 🔥🔥
+ if (body is String) bytes.writeString(body);
+ else if (body is Bytes) bytes.write(body);
+ return bytes.getBytes();
+ }
+
+ public static function fromBytes(bytes:Bytes):Null {
+ var status:Int = -1;
+
+ var header_length:Int = -1;
+ var header:String = "";
+
+ var body_is_string:Bool = false;
+ var body_length:Int = -1;
+ var body:OneOfTwo = null;
+
+ try {
+ var offset:Int = 0;
+ status = bytes.getInt32(0); offset += 4;
+ header_length = bytes.getInt32(offset); offset += 4;
+ header = bytes.getString(offset, header_length); offset += header_length;
+ body_is_string = (bytes.get(offset) == 1); offset += 1;
+ body_length = bytes.getInt32(offset); offset += 4;
+ if (body_is_string) body = bytes.getString(offset, body_length);
+ else body = bytes.sub(offset, body_length);
+ } catch(e) {
+ FlxG.log.error('FunkinPacket.fromBytes() failed to parse packet: $e');
+ return null;
+ }
+ var packet:FunkinPacket = new FunkinPacket(null, body, status);
+ for (line in header.split("\r\n")) {
+ var data = line.split(": ");
+ if (data.length < 2) continue;
+ var key:String = data.shift().trim();
+ var value:String = data.shift().trim();
+ packet.set(key, value);
+ }
+ return packet;
+ }
+}
\ 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 000000000..ef77fd360
--- /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 000000000..4214bcb56
--- /dev/null
+++ b/source/funkin/backend/system/net/FunkinWebSocket.hx
@@ -0,0 +1,180 @@
+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`.
+* By default, it assums the Connections is to a [CodenameEngine Template Server](https://github.com/ItsLJcool/CodenameEngine-Online-Template).
+* 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:// (SSH) 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; }
+
+ /**
+ * Since not all servers are going to be the Custom CodenameEngine Template Server, this allows you to receive the packet as raw Bytes, if you want to decode it yourself.
+ * Not all incomming data will be bytes, since Strings are just... strings, there is no reason to have special handling for them.
+ **/
+ public var AUTO_DECODE_PACKETS:Bool = true;
+
+ /**
+ * This is only called if the `Metrics` failed to update the bytes sent or received.
+ * So you can handle and update the data yourself.
+ * If Bool is true, the data was being sent. Otherwise it was being received.
+ **/
+ private var updateMetricCustom:(Metrics, Bool, Dynamic)->Void = null;
+
+ /**
+ * @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.dispatch();
+ this._ws.onmessage = (message:MessageType) -> {
+ var data:OneOfThree = "";
+ switch(message) {
+ case StrMessage(content):
+ data = content;
+ metrics.updateBytesReceived(Bytes.ofString(content).length);
+ case BytesMessage(buffer):
+ metrics.updateBytesReceived(buffer.length);
+ data = buffer.readAllAvailableBytes();
+ if (!AUTO_DECODE_PACKETS) return onMessage.dispatch(data);
+ var packet:FunkinPacket = FunkinPacket.fromBytes(data);
+ if (packet == null) return onMessage.dispatch(data);
+ data = packet;
+ }
+ onMessage.dispatch(data);
+ };
+ this._ws.onclose = () -> onClose.dispatch();
+ this._ws.onerror = (error) -> 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),
+ ]);
+ this._ws.open();
+ return this;
+ }
+
+ /**
+ * Sends data to the server.
+ * @param data The data to send.
+ **/
+ public function send(data:Dynamic):Bool {
+ try {
+ this._ws.send(data);
+ 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);
+ else if (metrics.IS_LOGGING && updateMetricCustom != null) updateMetricCustom(metrics, true, data);
+ 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 000000000..0e44aa76d
--- /dev/null
+++ b/source/funkin/backend/system/net/Metrics.hx
@@ -0,0 +1,29 @@
+package funkin.backend.system.net;
+
+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 function new() { }
+
+ public function updateBytesSent(amount:Int) {
+ if (!IS_LOGGING) return;
+ bytesSent += amount;
+ packetsSent++;
+ }
+
+ public function updateBytesReceived(amount:Int) {
+ if (!IS_LOGGING) return;
+ bytesReceived += amount;
+ packetsReceived++;
+ }
+
+ 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 19ebf9a11..000000000
--- 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