Merge pull request '[Feature]:' (!4) from task/ws-setup into trunk
All checks were successful
Quality Check / Linting (push) Successful in 6s
All checks were successful
Quality Check / Linting (push) Successful in 6s
Reviewed-on: #4 Reviewed-by: SZUT-Kevin <kevin.schmidt9101@gmail.com>
This commit is contained in:
commit
38a2991198
14 changed files with 535 additions and 22 deletions
BIN
addons/JsonClassConverter/AssetIcon.jpg
Normal file
BIN
addons/JsonClassConverter/AssetIcon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
34
addons/JsonClassConverter/AssetIcon.jpg.import
Normal file
34
addons/JsonClassConverter/AssetIcon.jpg.import
Normal file
|
@ -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
|
310
addons/JsonClassConverter/JsonClassConverter.gd
Normal file
310
addons/JsonClassConverter/JsonClassConverter.gd
Normal file
|
@ -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
|
1
addons/JsonClassConverter/JsonClassConverter.gd.uid
Normal file
1
addons/JsonClassConverter/JsonClassConverter.gd.uid
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uid://ch1r4c54yetfx
|
|
@ -15,6 +15,10 @@ run/main_scene="res://scenes/main_menu.tscn"
|
||||||
config/features=PackedStringArray("4.3", "GL Compatibility")
|
config/features=PackedStringArray("4.3", "GL Compatibility")
|
||||||
config/icon="res://textures/icon.svg"
|
config/icon="res://textures/icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
ConnectionChannel="*res://scripts/channel/connection/connection_channel.gd"
|
||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg")
|
enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg")
|
||||||
|
|
|
@ -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/switch_to_scene.gd" id="1_7goww"]
|
||||||
[ext_resource type="Script" path="res://scripts/ui/websocket_time.gd" id="1_qy4dl"]
|
[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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ujhhp"]
|
||||||
bg_color = Color(0.289228, 0.413265, 0.44363, 1)
|
bg_color = Color(0.289228, 0.413265, 0.44363, 1)
|
||||||
|
@ -1612,8 +1614,31 @@ layout_mode = 2
|
||||||
text = "Important Video!"
|
text = "Important Video!"
|
||||||
uri = "https://www.youtube.com/watch?v=PXqcHi2fkXI"
|
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
|
layout_mode = 2
|
||||||
text = "..."
|
text = "..."
|
||||||
fit_content = true
|
fit_content = true
|
||||||
script = ExtResource("1_qy4dl")
|
script = ExtResource("1_qy4dl")
|
||||||
|
login = NodePath("../LoginButton")
|
||||||
|
api_config = ExtResource("3_bphu2")
|
||||||
|
|
19
scripts/channel/channel.gd
Normal file
19
scripts/channel/channel.gd
Normal file
|
@ -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())
|
42
scripts/channel/connection/connection_channel.gd
Normal file
42
scripts/channel/connection/connection_channel.gd
Normal file
|
@ -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)
|
|
@ -0,0 +1,9 @@
|
||||||
|
class_name ProvidedConnectionTokenMessage
|
||||||
|
extends Message
|
||||||
|
|
||||||
|
@export var channel: Channels
|
||||||
|
var token: String
|
||||||
|
|
||||||
|
|
||||||
|
func get_message_id() -> String:
|
||||||
|
return "ProvidedConnectionToken"
|
|
@ -0,0 +1,8 @@
|
||||||
|
class_name RequestConnectionTokenMessage
|
||||||
|
extends Message
|
||||||
|
|
||||||
|
@export var channel: Channels
|
||||||
|
|
||||||
|
|
||||||
|
func get_message_id() -> String:
|
||||||
|
return "RequestConnectionToken"
|
30
scripts/channel/message.gd
Normal file
30
scripts/channel/message.gd
Normal file
|
@ -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
|
8
scripts/channel/time/current_unix_time_message.gd
Normal file
8
scripts/channel/time/current_unix_time_message.gd
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class_name CurrentUnixTimeMessage
|
||||||
|
extends Message
|
||||||
|
|
||||||
|
var time: int
|
||||||
|
|
||||||
|
|
||||||
|
func get_message_id() -> String:
|
||||||
|
return "CurrentUnixTime"
|
|
@ -1,5 +1,8 @@
|
||||||
|
class_name Login
|
||||||
extends Button
|
extends Button
|
||||||
|
|
||||||
|
signal login_successful(session: PlayerLoginSession)
|
||||||
|
|
||||||
@export var username_field: LineEdit
|
@export var username_field: LineEdit
|
||||||
@export var password_field: LineEdit
|
@export var password_field: LineEdit
|
||||||
@export var api_config: ApiConfig
|
@export var api_config: ApiConfig
|
||||||
|
@ -34,9 +37,7 @@ func login() -> void:
|
||||||
|
|
||||||
|
|
||||||
func on_success(response: ApiResponse) -> void:
|
func on_success(response: ApiResponse) -> void:
|
||||||
var session: PlayerLoginSession = response.data
|
login_successful.emit(response.data)
|
||||||
print("username: ", session.username)
|
|
||||||
print("token: ", session.token.to_utf8_buffer())
|
|
||||||
|
|
||||||
|
|
||||||
func on_error(error: ApiError) -> void:
|
func on_error(error: ApiError) -> void:
|
||||||
|
|
|
@ -1,28 +1,50 @@
|
||||||
extends RichTextLabel
|
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
|
# docs.redotengine.org/en/stable/tutorials/networking/websocket.html
|
||||||
@export var fallpack_websocket_url = "ws://localhost:8080/ws/server"
|
var time_channel = WebSocketPeer.new()
|
||||||
var websocket_url = OS.get_environment("TD_SERVER_WS")
|
|
||||||
var socket = WebSocketPeer.new()
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if websocket_url.is_empty():
|
login.connect("login_successful", on_login)
|
||||||
websocket_url = fallpack_websocket_url
|
(
|
||||||
var err = socket.connect_to_url(websocket_url)
|
ConnectionChannel
|
||||||
if err != OK:
|
. connect(
|
||||||
error_string(err)
|
"on_channel_token_received",
|
||||||
set_process(false)
|
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:
|
func _process(_delta: float) -> void:
|
||||||
socket.poll()
|
time_channel.poll()
|
||||||
var state = socket.get_ready_state()
|
var state = time_channel.get_ready_state()
|
||||||
|
|
||||||
if state == WebSocketPeer.STATE_CLOSED:
|
if state != WebSocketPeer.STATE_OPEN:
|
||||||
self.text = "Disconnected"
|
|
||||||
return
|
return
|
||||||
|
while time_channel.get_available_packet_count():
|
||||||
if state == WebSocketPeer.STATE_OPEN:
|
var msg: CurrentUnixTimeMessage = Message.deserialize(
|
||||||
while socket.get_available_packet_count():
|
time_channel.get_packet().get_string_from_utf8(), [CurrentUnixTimeMessage]
|
||||||
self.text = "Current Unixtime: " + socket.get_packet().get_string_from_utf8()
|
)
|
||||||
|
if msg == null:
|
||||||
|
continue
|
||||||
|
self.text = str(msg.time)
|
||||||
|
|
Loading…
Add table
Reference in a new issue