From 1531505d2f3c9db67d57faab412fdbf45c2955f3 Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Tue, 18 Feb 2025 13:02:40 +0100 Subject: [PATCH] NOTICKET: Adding WS Structure and Example --- addons/JsonClassConverter/AssetIcon.jpg | Bin 0 -> 10598 bytes .../JsonClassConverter/AssetIcon.jpg.import | 34 ++ .../JsonClassConverter/JsonClassConverter.gd | 310 ++++++++++++++++++ .../JsonClassConverter.gd.uid | 1 + project.godot | 4 + scenes/theme_test.tscn | 29 +- scripts/channel/channel.gd | 19 ++ .../channel/connection/connection_channel.gd | 42 +++ .../provided_connection_token_message.gd | 9 + .../request_connection_token_message.gd | 8 + scripts/channel/message.gd | 30 ++ .../channel/time/current_unix_time_message.gd | 8 + scripts/ui/login.gd | 7 +- scripts/ui/websocket_time.gd | 56 +++- 14 files changed, 535 insertions(+), 22 deletions(-) create mode 100644 addons/JsonClassConverter/AssetIcon.jpg create mode 100644 addons/JsonClassConverter/AssetIcon.jpg.import create mode 100644 addons/JsonClassConverter/JsonClassConverter.gd create mode 100644 addons/JsonClassConverter/JsonClassConverter.gd.uid create mode 100644 scripts/channel/channel.gd create mode 100644 scripts/channel/connection/connection_channel.gd create mode 100644 scripts/channel/connection/provided_connection_token_message.gd create mode 100644 scripts/channel/connection/request_connection_token_message.gd create mode 100644 scripts/channel/message.gd create mode 100644 scripts/channel/time/current_unix_time_message.gd diff --git a/addons/JsonClassConverter/AssetIcon.jpg b/addons/JsonClassConverter/AssetIcon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..73f4827c9cc990b0bf16a081c44881d22895463c GIT binary patch literal 10598 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 asRpWvyFVnyo)7=wk$<`5{|AAcPyIKEGXLHH literal 0 HcmV?d00001 diff --git a/addons/JsonClassConverter/AssetIcon.jpg.import b/addons/JsonClassConverter/AssetIcon.jpg.import new file mode 100644 index 0000000..112628a --- /dev/null +++ b/addons/JsonClassConverter/AssetIcon.jpg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbt51sclatky7" +path="res://.godot/imported/AssetIcon.jpg-09518bcc644832efd4eedeb1abc08598.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/JsonClassConverter/AssetIcon.jpg" +dest_files=["res://.godot/imported/AssetIcon.jpg-09518bcc644832efd4eedeb1abc08598.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/JsonClassConverter/JsonClassConverter.gd b/addons/JsonClassConverter/JsonClassConverter.gd new file mode 100644 index 0000000..d81bab6 --- /dev/null +++ b/addons/JsonClassConverter/JsonClassConverter.gd @@ -0,0 +1,310 @@ +class_name JsonClassConverter + +# Flag to control whether to save nested resources as separate .tres files +static var save_temp_resources_tres: bool = false + +## Checks if the provided class is valid (not null) +static func _check_cast_class(castClass: GDScript) -> bool: + if typeof(castClass) == Variant.Type.TYPE_NIL: + printerr("The provided class is null.") + return false + return true + +## Checks if the directory for the given file path exists, creating it if necessary. +static func check_dir(file_path: String) -> void: + if !DirAccess.dir_exists_absolute(file_path.get_base_dir()): + DirAccess.make_dir_absolute(file_path.get_base_dir()) + +#region Json to Class + +## Loads a JSON file and parses it into a Dictionary. +## Supports optional decryption using a security key. +static func json_file_to_dict(file_path: String, security_key: String = "") -> Dictionary: + var file: FileAccess + if FileAccess.file_exists(file_path): + if security_key.length() == 0: + file = FileAccess.open(file_path, FileAccess.READ) + else: + file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.READ, security_key) + if not file: + printerr("Error opening file: ", file_path) + return {} + var parsed_results: Variant = JSON.parse_string(file.get_as_text()) + file.close() + if parsed_results is Dictionary or parsed_results is Array: + return parsed_results + return {} + +## Loads a JSON file and converts its contents into a Godot class instance. +## Uses the provided GDScript (castClass) as a template for the class. +static func json_file_to_class(castClass: GDScript, file_path: String, security_key: String = "") -> Object: + if not _check_cast_class(castClass): + printerr("The provided class is null.") + return null + var parsed_results = json_file_to_dict(file_path, security_key) + if parsed_results == null: + return castClass.new() + return json_to_class(castClass, parsed_results) + +## Converts a JSON string into a Godot class instance. +static func json_string_to_class(castClass: GDScript, json_string: String) -> Object: + if not _check_cast_class(castClass): + printerr("The provided class is null.") + return null + var json: JSON = JSON.new() + var parse_result: Error = json.parse(json_string) + if parse_result == Error.OK: + return json_to_class(castClass, json.data) + return castClass.new() + +## Converts a JSON dictionary into a Godot class instance. +## This is the core deserialization function. +static func json_to_class(castClass: GDScript, json: Dictionary) -> Object: + # Create an instance of the target class + var _class: Object = castClass.new() as Object + var properties: Array = _class.get_property_list() + + # Iterate through each key-value pair in the JSON dictionary + for key: String in json.keys(): + var value: Variant = json[key] + + # Special handling for Vector types (stored as strings in JSON) + if type_string(typeof(value)) == "String" and value.begins_with("Vector"): + value = str_to_var(value) + + # Find the matching property in the target class + for property: Dictionary in properties: + # Skip the 'script' property (built-in) + if property.name == "script": + continue + + # Get the current value of the property in the class instance + var property_value: Variant = _class.get(property.name) + + # If the property name matches the JSON key and is a script variable: + if property.name == key and property.usage >= PROPERTY_USAGE_SCRIPT_VARIABLE: + # Case 1: Property is an Object (not an array) + if not property_value is Array and property.type == TYPE_OBJECT: + var inner_class_path: String = "" + if property_value: + # If the property already holds an object, try to get its script path + for inner_property: Dictionary in property_value.get_property_list(): + if inner_property.has("hint_string") and inner_property["hint_string"].contains(".gd"): + inner_class_path = inner_property["hint_string"] + # Recursively deserialize nested objects + _class.set(property.name, json_to_class(load(inner_class_path), value)) + elif value: + var script_type: GDScript = null + # Determine the script type for the nested object + if value is Dictionary and value.has("script_inheritance"): + script_type = get_gdscript(value["script_inheritance"]) + else: + script_type = get_gdscript(property. class_name ) + + # If the value is a resource path, load the resource + if value is String and value.is_absolute_path(): + _class.set(property.name, ResourceLoader.load(get_main_tres_path(value))) + else: + # Recursively deserialize nested objects + _class.set(property.name, json_to_class(script_type, value)) + + # Case 2: Property is an Array + elif property_value is Array: + if property.has("hint_string"): + var class_hint: String = property["hint_string"] + if class_hint.contains(":"): + # Extract class name from hint string (e.g., "24/34:ClassName") + class_hint = class_hint.split(":")[1] + + # Recursively convert the JSON array to a Godot array + var arrayTemp: Array = convert_json_to_array(value, get_gdscript(class_hint)) + + # Handle Vector arrays (convert string elements back to Vectors) + if type_string(property_value.get_typed_builtin()).begins_with("Vector"): + for obj_array: Variant in arrayTemp: + _class.get(property.name).append(str_to_var(obj_array)) + else: + _class.get(property.name).assign(arrayTemp) + + # Case 3: Property is a simple type (not an object or array) + else: + # Special handling for Color type (stored as a hex string) + if property.type == TYPE_COLOR: + value = Color(value) + if property.type == TYPE_INT and property.hint == PROPERTY_HINT_ENUM: + var enum_strs: Array = property.hint_string.split(",") + var enum_value: int = 0 + for enum_str: String in enum_strs: + if enum_str.contains(":"): + var enum_keys: Array = enum_str.split(":") + for i: int in enum_keys.size(): + if enum_keys[i].to_lower() == value.to_lower(): + enum_value = int(enum_keys[i + 1]) + _class.set(property.name, enum_value) + else: + _class.set(property.name, value) + + # Return the fully deserialized class instance + return _class + +## Helper function to find a GDScript by its class name. +static func get_gdscript(hint_class: String) -> GDScript: + for className: Dictionary in ProjectSettings.get_global_class_list(): + if className. class == hint_class: + return load(className.path) + return null + +## Helper function to recursively convert JSON arrays to Godot arrays. +static func convert_json_to_array(json_array: Array, cast_class: GDScript = null) -> Array: + var godot_array: Array = [] + for element: Variant in json_array: + if typeof(element) == TYPE_DICTIONARY: + # If json element has a script_inheritance, get the script (for inheritance or for untyped array/dictionary) + if "script_inheritance" in element: + cast_class = get_gdscript(element["script_inheritance"]) + godot_array.append(json_to_class(cast_class, element)) + elif typeof(element) == TYPE_ARRAY: + godot_array.append(convert_json_to_array(element)) + else: + godot_array.append(element) + return godot_array + +#endregion + +#region Class to Json +## Stores a JSON dictionary to a file, optionally with encryption. +static func store_json_file(file_path: String, data: Dictionary, security_key: String = "") -> bool: + check_dir(file_path) + var file: FileAccess + if security_key.length() == 0: + file = FileAccess.open(file_path, FileAccess.WRITE) + else: + file = FileAccess.open_encrypted_with_pass(file_path, FileAccess.WRITE, security_key) + if not file: + printerr("Error writing to a file") + return false + var json_string: String = JSON.stringify(data, "\t") + file.store_string(json_string) + file.close() + return true + +## Converts a Godot class instance into a JSON string. +static func class_to_json_string(_class: Object, save_temp_res: bool = false) -> String: + return JSON.stringify(class_to_json(_class, save_temp_res)) + +## Converts a Godot class instance into a JSON dictionary. +## This is the core serialization function. +static func class_to_json(_class: Object, save_temp_res: bool = false, inheritance: bool = false) -> Dictionary: + var dictionary: Dictionary = {} + save_temp_resources_tres = save_temp_res + # Store the script name for reference during deserialization if inheritance exists + if inheritance: + dictionary["script_inheritance"] = _class.get_script().get_global_name() + var properties: Array = _class.get_property_list() + + # Iterate through each property of the class + for property: Dictionary in properties: + var property_name: String = property["name"] + # Skip the built-in 'script' property + if property_name == "script": + continue + var property_value: Variant = _class.get(property_name) + + # Only serialize properties that are exported or marked for storage + if not property_name.is_empty() and property.usage >= PROPERTY_USAGE_SCRIPT_VARIABLE and property.usage & PROPERTY_USAGE_STORAGE > 0: + if property_value is Array: + # Recursively convert arrays to JSON + dictionary[property_name] = convert_array_to_json(property_value) + elif property_value is Dictionary: + # Recursively convert dictionaries to JSON + dictionary[property_name] = convert_dictionary_to_json(property_value) + # If the property is a Resource: + elif property["type"] == TYPE_OBJECT and property_value != null and property_value.get_property_list(): + if property_value is Resource and ResourceLoader.exists(property_value.resource_path): + var main_src: String = get_main_tres_path(property_value.resource_path) + if main_src.get_extension() != "tres": + # Store the resource path if it's not a .tres file + dictionary[property.name] = property_value.resource_path + elif save_temp_resources_tres: + # Save the resource as a separate .tres file + var tempfile = "user://temp_resource/" + check_dir(tempfile) + var nodePath: String = get_node_tres_path(property_value.resource_path) + if not nodePath.is_empty(): + tempfile += nodePath + tempfile += ".tres" + else: + tempfile += property_value.resource_path.get_file() + dictionary[property.name] = tempfile + ResourceSaver.save(property_value, tempfile) + else: + # Recursively serialize the nested resource + dictionary[property.name] = class_to_json(property_value, save_temp_resources_tres) + else: + dictionary[property.name] = class_to_json(property_value, save_temp_resources_tres, property. class_name != property_value.get_script().get_global_name()) + # Special handling for Vector types (store as strings) + elif type_string(typeof(property_value)).begins_with("Vector"): + dictionary[property_name] = var_to_str(property_value) + elif property["type"] == TYPE_COLOR: + # Store Color as a hex string + dictionary[property_name] = property_value.to_html() + else: + # Store other basic types directly + if property.type == TYPE_INT and property.hint == PROPERTY_HINT_ENUM: + var enum_value: String = property.hint_string.split(",")[property_value] + if enum_value.contains(":"): + dictionary[property.name] = enum_value.split(":")[0] + else: + dictionary[property.name] = enum_value + else: + dictionary[property.name] = property_value + return dictionary + +## Extracts the main path from a resource path (removes node path if present). +static func get_main_tres_path(path: String) -> String: + var path_parts: PackedStringArray = path.split("::", true, 1) + if path_parts.size() > 0: + return path_parts[0] + else: + return path + +## Extracts the node path from a resource path. +static func get_node_tres_path(path: String) -> String: + var path_parts: PackedStringArray = path.split("::", true, 1) + if path_parts.size() > 1: + return path_parts[1] + else: + return "" + + +## Helper function to recursively convert Godot arrays to JSON arrays. +static func convert_array_to_json(array: Array) -> Array: + var json_array: Array = [] + for element: Variant in array: + if element is Object: + json_array.append(class_to_json(element, save_temp_resources_tres,!array.is_typed())) + elif element is Array: + json_array.append(convert_array_to_json(element)) + elif element is Dictionary: + json_array.append(convert_dictionary_to_json(element)) + elif type_string(typeof(element)).begins_with("Vector"): + json_array.append(var_to_str(element)) + else: + json_array.append(element) + return json_array + +## Helper function to recursively convert Godot dictionaries to JSON dictionaries. +static func convert_dictionary_to_json(dictionary: Dictionary) -> Dictionary: + var json_dictionary: Dictionary = {} + for key: Variant in dictionary.keys(): + var value: Variant = dictionary[key] + if value is Object: + json_dictionary[key] = class_to_json(value, save_temp_resources_tres) + elif value is Array: + json_dictionary[key] = convert_array_to_json(value) + elif value is Dictionary: + json_dictionary[key] = convert_dictionary_to_json(value) + else: + json_dictionary[key] = value + return json_dictionary +#endregion diff --git a/addons/JsonClassConverter/JsonClassConverter.gd.uid b/addons/JsonClassConverter/JsonClassConverter.gd.uid new file mode 100644 index 0000000..4b825d8 --- /dev/null +++ b/addons/JsonClassConverter/JsonClassConverter.gd.uid @@ -0,0 +1 @@ +uid://ch1r4c54yetfx diff --git a/project.godot b/project.godot index 788a8aa..27843ab 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://scenes/main_menu.tscn" config/features=PackedStringArray("4.3", "GL Compatibility") config/icon="res://textures/icon.svg" +[autoload] + +ConnectionChannel="*res://scripts/channel/connection/connection_channel.gd" + [editor_plugins] enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg") diff --git a/scenes/theme_test.tscn b/scenes/theme_test.tscn index 7f0cfaf..7964c82 100644 --- a/scenes/theme_test.tscn +++ b/scenes/theme_test.tscn @@ -1,7 +1,9 @@ -[gd_scene load_steps=7 format=3 uid="uid://ctqxikky2g0nj"] +[gd_scene load_steps=9 format=3 uid="uid://ctqxikky2g0nj"] [ext_resource type="Script" path="res://scripts/ui/switch_to_scene.gd" id="1_7goww"] [ext_resource type="Script" path="res://scripts/ui/websocket_time.gd" id="1_qy4dl"] +[ext_resource type="Resource" uid="uid://cdixdbu3sqgjn" path="res://config/api_config.tres" id="3_bphu2"] +[ext_resource type="Script" path="res://scripts/ui/login.gd" id="4_pkjxs"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ujhhp"] bg_color = Color(0.289228, 0.413265, 0.44363, 1) @@ -1612,8 +1614,31 @@ layout_mode = 2 text = "Important Video!" uri = "https://www.youtube.com/watch?v=PXqcHi2fkXI" -[node name="RichTextLabel" type="RichTextLabel" parent="HBoxContainer/ScrollContainer/VBoxContainer"] +[node name="UsernameInput" type="LineEdit" parent="HBoxContainer/ScrollContainer/VBoxContainer"] +layout_mode = 2 +text = "Player1" +placeholder_text = "Username" + +[node name="PasswordInput" type="LineEdit" parent="HBoxContainer/ScrollContainer/VBoxContainer"] +layout_mode = 2 +text = "1234" +placeholder_text = "Password" +secret = true + +[node name="LoginButton" type="Button" parent="HBoxContainer/ScrollContainer/VBoxContainer" node_paths=PackedStringArray("username_field", "password_field")] +layout_mode = 2 +text = "Login" +script = ExtResource("4_pkjxs") +username_field = NodePath("../UsernameInput") +password_field = NodePath("../PasswordInput") +api_config = ExtResource("3_bphu2") + +[node name="HTTPRequest" type="HTTPRequest" parent="HBoxContainer/ScrollContainer/VBoxContainer/LoginButton"] + +[node name="CurrentTimeDisplay" type="RichTextLabel" parent="HBoxContainer/ScrollContainer/VBoxContainer" node_paths=PackedStringArray("login")] layout_mode = 2 text = "..." fit_content = true script = ExtResource("1_qy4dl") +login = NodePath("../LoginButton") +api_config = ExtResource("3_bphu2") diff --git a/scripts/channel/channel.gd b/scripts/channel/channel.gd new file mode 100644 index 0000000..86b24de --- /dev/null +++ b/scripts/channel/channel.gd @@ -0,0 +1,19 @@ +class_name Channel +extends Node + +const SOCKET_FALLBACK_URL := "ws://localhost:8080/ws" + +var socket = WebSocketPeer.new() +var socket_url := OS.get_environment("TD_SERVER_WS") + + +func get_channel_location() -> String: + push_error("Not Implemented") + return "" + + +func connect_socket(token: String): + socket.handshake_headers = PackedStringArray(["Authorization: " + token]) + if socket_url == "": + socket_url = SOCKET_FALLBACK_URL + socket.connect_to_url(socket_url + "/" + get_channel_location()) diff --git a/scripts/channel/connection/connection_channel.gd b/scripts/channel/connection/connection_channel.gd new file mode 100644 index 0000000..d488750 --- /dev/null +++ b/scripts/channel/connection/connection_channel.gd @@ -0,0 +1,42 @@ +extends Channel + +signal on_channel_token_received(msg: ProvidedConnectionTokenMessage) + +var queue: Array[Message.Channels] + + +func get_channel_location() -> String: + return "connection" + + +func connect_to_channel(token: String) -> void: + if self.socket.get_ready_state() != WebSocketPeer.STATE_CLOSED: + return + self.connect_socket(token) + + +func request_channel_token(channel: Message.Channels) -> void: + if not queue.has(channel): + queue.push_back(channel) + + +func _process(_delta: float) -> void: + self.socket.poll() + if self.socket.get_ready_state() != WebSocketPeer.STATE_OPEN: + return + for i in queue.size(): + var msg := RequestConnectionTokenMessage.new() + msg.channel = queue[i] + self.socket.send_text(Message.serialize(msg)) + queue.remove_at(i) + while self.socket.get_available_packet_count(): + var msg: ProvidedConnectionTokenMessage = ( + Message + . deserialize( + self.socket.get_packet().get_string_from_utf8(), + [ProvidedConnectionTokenMessage], + ) + ) + if msg == null: + continue + on_channel_token_received.emit(msg) diff --git a/scripts/channel/connection/provided_connection_token_message.gd b/scripts/channel/connection/provided_connection_token_message.gd new file mode 100644 index 0000000..3a01ae8 --- /dev/null +++ b/scripts/channel/connection/provided_connection_token_message.gd @@ -0,0 +1,9 @@ +class_name ProvidedConnectionTokenMessage +extends Message + +@export var channel: Channels +var token: String + + +func get_message_id() -> String: + return "ProvidedConnectionToken" diff --git a/scripts/channel/connection/request_connection_token_message.gd b/scripts/channel/connection/request_connection_token_message.gd new file mode 100644 index 0000000..bb4371b --- /dev/null +++ b/scripts/channel/connection/request_connection_token_message.gd @@ -0,0 +1,8 @@ +class_name RequestConnectionTokenMessage +extends Message + +@export var channel: Channels + + +func get_message_id() -> String: + return "RequestConnectionToken" diff --git a/scripts/channel/message.gd b/scripts/channel/message.gd new file mode 100644 index 0000000..11e6250 --- /dev/null +++ b/scripts/channel/message.gd @@ -0,0 +1,30 @@ +class_name Message + +enum Channels { CONNECTION, TIME } + + +func get_message_id() -> String: + push_error("Not Implemented") + return "" + + +static func serialize(message: Message) -> String: + var msg: Dictionary = JsonClassConverter.class_to_json(message) + msg["$id"] = message.get_message_id() + return JSON.stringify(msg) + + +static func deserialize(payload: String, messages: Array[GDScript]) -> Message: + var json := JSON.new() + var err = json.parse(payload) + if err != OK: + return null + var data: Variant = json.data + if data == null: + return null + var msg_id: String = data.get("$id") + for msg in messages: + if msg_id != msg.new().get_message_id(): + continue + return JsonClassConverter.json_to_class(msg, data) + return null diff --git a/scripts/channel/time/current_unix_time_message.gd b/scripts/channel/time/current_unix_time_message.gd new file mode 100644 index 0000000..68a1ae7 --- /dev/null +++ b/scripts/channel/time/current_unix_time_message.gd @@ -0,0 +1,8 @@ +class_name CurrentUnixTimeMessage +extends Message + +var time: int + + +func get_message_id() -> String: + return "CurrentUnixTime" diff --git a/scripts/ui/login.gd b/scripts/ui/login.gd index c66809a..cdfee98 100644 --- a/scripts/ui/login.gd +++ b/scripts/ui/login.gd @@ -1,5 +1,8 @@ +class_name Login extends Button +signal login_successful(session: PlayerLoginSession) + @export var username_field: LineEdit @export var password_field: LineEdit @export var api_config: ApiConfig @@ -34,9 +37,7 @@ func login() -> void: func on_success(response: ApiResponse) -> void: - var session: PlayerLoginSession = response.data - print("username: ", session.username) - print("token: ", session.token.to_utf8_buffer()) + login_successful.emit(response.data) func on_error(error: ApiError) -> void: diff --git a/scripts/ui/websocket_time.gd b/scripts/ui/websocket_time.gd index 046778f..e25985a 100644 --- a/scripts/ui/websocket_time.gd +++ b/scripts/ui/websocket_time.gd @@ -1,28 +1,50 @@ extends RichTextLabel +@export var login: Login +@export var api_config: ApiConfig +var api := ServerApi.new(api_config) + # docs.redotengine.org/en/stable/tutorials/networking/websocket.html -@export var fallpack_websocket_url = "ws://localhost:8080/ws/server" -var websocket_url = OS.get_environment("TD_SERVER_WS") -var socket = WebSocketPeer.new() +var time_channel = WebSocketPeer.new() func _ready() -> void: - if websocket_url.is_empty(): - websocket_url = fallpack_websocket_url - var err = socket.connect_to_url(websocket_url) - if err != OK: - error_string(err) - set_process(false) + login.connect("login_successful", on_login) + ( + ConnectionChannel + . connect( + "on_channel_token_received", + on_channel_token_received, + ) + ) + + +func on_login(session: PlayerLoginSession): + ConnectionChannel.connect_to_channel(session.token) + ConnectionChannel.request_channel_token(Message.Channels.TIME) + + +func on_channel_token_received(msg: ProvidedConnectionTokenMessage) -> void: + if time_channel.get_ready_state() != WebSocketPeer.STATE_CLOSED: + return + if msg.channel != Message.Channels.TIME: + return + time_channel.handshake_headers = PackedStringArray( + ["Authorization: " + msg.token], + ) + time_channel.connect_to_url("ws://localhost:8080/ws/time") func _process(_delta: float) -> void: - socket.poll() - var state = socket.get_ready_state() + time_channel.poll() + var state = time_channel.get_ready_state() - if state == WebSocketPeer.STATE_CLOSED: - self.text = "Disconnected" + if state != WebSocketPeer.STATE_OPEN: return - - if state == WebSocketPeer.STATE_OPEN: - while socket.get_available_packet_count(): - self.text = "Current Unixtime: " + socket.get_packet().get_string_from_utf8() + while time_channel.get_available_packet_count(): + var msg: CurrentUnixTimeMessage = Message.deserialize( + time_channel.get_packet().get_string_from_utf8(), [CurrentUnixTimeMessage] + ) + if msg == null: + continue + self.text = str(msg.time)