From ef52a0feb0830709c473547a595af59415ebed47 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Tue, 13 Jan 2026 01:43:40 -0600 Subject: [PATCH 01/19] New version. --- .gitignore | 2 + src/icon.svg | 172 +++++++ src/icon.svg.import | 43 ++ src/project.godot | 74 +++ src/resources/icons/account.svg | 42 ++ src/resources/icons/account.svg.import | 43 ++ src/scenes/levels/home.tscn | 75 +++ src/scenes/managers/app/network_manager.gd | 216 +++++++++ .../managers/app/network_manager.gd.uid | 1 + src/scenes/managers/app/network_manager.tscn | 6 + src/scenes/managers/app/scene_manager.gd | 44 ++ src/scenes/managers/app/scene_manager.gd.uid | 1 + src/scenes/managers/app/scene_manager.tscn | 6 + .../managers/level/multiplayer_manager.gd | 29 ++ .../managers/level/multiplayer_manager.gd.uid | 1 + .../managers/level/multiplayer_manager.tscn | 6 + src/scenes/master.tscn | 19 + src/scenes/players/player.gd | 149 ++++++ src/scenes/players/player.gd.uid | 1 + src/scenes/players/player.tscn | 50 ++ src/scripts/Files.gd | 41 ++ src/scripts/Files.gd.uid | 1 + src/scripts/LaunchArguments.gd | 14 + src/scripts/LaunchArguments.gd.uid | 1 + src/scripts/Logger.gd | 73 +++ src/scripts/Logger.gd.uid | 1 + src/scripts/Util.gd | 10 + src/scripts/Util.gd.uid | 1 + src/scripts/network_compression.gd | 165 +++++++ src/scripts/network_compression.gd.uid | 1 + src/userinterface/hud.gd | 22 + src/userinterface/hud.gd.uid | 1 + src/userinterface/hud.tscn | 453 ++++++++++++++++++ 33 files changed, 1764 insertions(+) create mode 100644 .gitignore create mode 100644 src/icon.svg create mode 100644 src/icon.svg.import create mode 100644 src/project.godot create mode 100644 src/resources/icons/account.svg create mode 100644 src/resources/icons/account.svg.import create mode 100644 src/scenes/levels/home.tscn create mode 100644 src/scenes/managers/app/network_manager.gd create mode 100644 src/scenes/managers/app/network_manager.gd.uid create mode 100644 src/scenes/managers/app/network_manager.tscn create mode 100644 src/scenes/managers/app/scene_manager.gd create mode 100644 src/scenes/managers/app/scene_manager.gd.uid create mode 100644 src/scenes/managers/app/scene_manager.tscn create mode 100644 src/scenes/managers/level/multiplayer_manager.gd create mode 100644 src/scenes/managers/level/multiplayer_manager.gd.uid create mode 100644 src/scenes/managers/level/multiplayer_manager.tscn create mode 100644 src/scenes/master.tscn create mode 100644 src/scenes/players/player.gd create mode 100644 src/scenes/players/player.gd.uid create mode 100644 src/scenes/players/player.tscn create mode 100644 src/scripts/Files.gd create mode 100644 src/scripts/Files.gd.uid create mode 100644 src/scripts/LaunchArguments.gd create mode 100644 src/scripts/LaunchArguments.gd.uid create mode 100644 src/scripts/Logger.gd create mode 100644 src/scripts/Logger.gd.uid create mode 100644 src/scripts/Util.gd create mode 100644 src/scripts/Util.gd.uid create mode 100644 src/scripts/network_compression.gd create mode 100644 src/scripts/network_compression.gd.uid create mode 100644 src/userinterface/hud.gd create mode 100644 src/userinterface/hud.gd.uid create mode 100644 src/userinterface/hud.tscn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfad5d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/src/.godot +/src/android/ \ No newline at end of file diff --git a/src/icon.svg b/src/icon.svg new file mode 100644 index 0000000..6df617e --- /dev/null +++ b/src/icon.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icon.svg.import b/src/icon.svg.import new file mode 100644 index 0000000..bd0ca1a --- /dev/null +++ b/src/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://du0bhthwac604" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/project.godot b/src/project.godot new file mode 100644 index 0000000..2181155 --- /dev/null +++ b/src/project.godot @@ -0,0 +1,74 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="PrismLens" +run/main_scene="uid://cxk6c0uipjjpo" +config/features=PackedStringArray("4.5", "Forward Plus") +run/max_fps=144 +boot_splash/image="/home/adragon/Pictures/PrismLens_1_background.png" +config/icon="uid://cdukvnkmlkl0e" +boot_splash/minimum_display_time=1000 + +[autoload] + +LaunchArguments="*res://scripts/LaunchArguments.gd" +GlobalLogger="*res://scripts/Logger.gd" +FileManager="*res://scripts/Files.gd" +Util="*res://scripts/Util.gd" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 + +[input] + +jump={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} +forward={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} +backward={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} +left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} +context={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) +] +} +escape={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +sprint={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/src/resources/icons/account.svg b/src/resources/icons/account.svg new file mode 100644 index 0000000..a0afd11 --- /dev/null +++ b/src/resources/icons/account.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/resources/icons/account.svg.import b/src/resources/icons/account.svg.import new file mode 100644 index 0000000..35ce56d --- /dev/null +++ b/src/resources/icons/account.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://coqi7w7inqyv1" +path="res://.godot/imported/account.svg-4dd6f4e3bd6806ac9bb3535ef447dd9a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/account.svg" +dest_files=["res://.godot/imported/account.svg-4dd6f4e3bd6806ac9bb3535ef447dd9a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/scenes/levels/home.tscn b/src/scenes/levels/home.tscn new file mode 100644 index 0000000..be55a20 --- /dev/null +++ b/src/scenes/levels/home.tscn @@ -0,0 +1,75 @@ +[gd_scene load_steps=4 format=3 uid="uid://b3t1dk4vpjk6p"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_dbach"] +sky_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1) +ground_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1) + +[sub_resource type="Sky" id="Sky_ikf4c"] +sky_material = SubResource("ProceduralSkyMaterial_dbach") + +[sub_resource type="Environment" id="Environment_q28r8"] +background_mode = 2 +sky = SubResource("Sky_ikf4c") +tonemap_mode = 2 +glow_enabled = true + +[node name="Home" type="Node3D"] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(-0.8660254, -0.43301278, 0.25, 0, 0.49999997, 0.86602545, -0.50000006, 0.75, -0.43301266, 0, 0, 0) +shadow_enabled = true + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_q28r8") + +[node name="Node3D" type="Node3D" parent="."] + +[node name="CSGBox3D" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, -0.5, 0) +use_collision = true +size = Vector3(25, 1, 10) + +[node name="CSGBox3D9" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 3, 0) +use_collision = true +size = Vector3(25, 0.1, 10) + +[node name="CSGBox3D2" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -12.45) +use_collision = true +size = Vector3(10, 1, 0.1) + +[node name="CSGBox3D5" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 1.5, -9.35) +use_collision = true +size = Vector3(4, 3, 0.1) + +[node name="CSGBox3D8" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 12.45) +use_collision = true +size = Vector3(10, 3, 0.1) + +[node name="CSGBox3D6" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 1.5, -9.35) +use_collision = true +size = Vector3(4, 3, 0.1) + +[node name="CSGBox3D3" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.95, 1.5, 1.6) +use_collision = true +size = Vector3(0.1, 3, 21.8) + +[node name="CSGBox3D7" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.95, 1.5, 1.6) +use_collision = true +size = Vector3(0.1, 3, 21.8) + +[node name="CSGBox3D4" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.95, 0.5, -10.9) +use_collision = true +size = Vector3(0.1, 1, 3) + +[node name="CSGBox3D10" type="CSGBox3D" parent="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.95, 0.5, -10.9) +use_collision = true +size = Vector3(0.1, 1, 3) diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd new file mode 100644 index 0000000..61b881c --- /dev/null +++ b/src/scenes/managers/app/network_manager.gd @@ -0,0 +1,216 @@ +extends Node + +const n_c = preload("res://scripts/network_compression.gd") + +# TODO: Bandwidth toggles +@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") +@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") + +# This file contains all of the session management and client communication. +# Anything that goes through the network should first route through here at some point. +enum server_privacy {PRIVATE, INVITE, FRIENDS, PUBLIC} + +var status = { + "hosting": false, + "client": false +} + +var config = { + "port": 20205, + "max_clients": 4, + "privacy": 0, + + "networking": { + "use_steam": false, + "use_lan": false + } +} + +var info = { + "level": "res://scenes/levels/home.tscn", + "level_node_name": "", + "clients": [] +} + +func _ready(): + multiplayer.peer_connected.connect(_on_peer_connected) + multiplayer.peer_disconnected.connect(_on_peer_disconnected) + + multiplayer.connected_to_server.connect(_on_connected) + multiplayer.connection_failed.connect(_on_connection_failed) + +func start_server(port: int = config.port, max_clients: int = config.max_clients) -> void: + if status.hosting: + # This ideally should not trigger + GlobalLogger.log_string("Can not start server: Server is already running.", 2) + status.hosting = false + status.client = false + return + + var new_peer = ENetMultiplayerPeer.new() + # FIXME: Error handling is required here + var err = new_peer.create_server(port, max_clients) + # FIXME: This client append is happening too early, this is a debug position + info.clients.append({"display_name": "Me!", "multiplayer_id": 1}) + if err != OK: + GlobalLogger.log_string("Failed to start server.", 3) + status.hosting = false + status.client = false + return + + multiplayer.multiplayer_peer = new_peer + GlobalLogger.log_string("Successfully started server.", 1) + + while status.hosting == false: + await get_tree().process_frame + status.hosting = true + status.client = false + + +func close_server(): + # Disconnect all players. + # Remove listings from all used networking. + # Update server config. + # TODO: OfflineMultiplayerPeer is a test. Check to see if this actually works. + multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() + status.hosting = false + status.client = false + + return + +func update_server(): + # Update our config. + # Submit a update to any active networking service. + return + +func join_server(ip: String = "", port: int = config.port) -> void: + # Client connects to a server. + if ip.is_empty(): + GlobalLogger.log_string("No IP to connect to.", 2) + return + + if status.hosting: + # This ideally should not trigger + GlobalLogger.log_string("Can not join server: We are currently hosting a server.", 2) + return + + var new_peer = ENetMultiplayerPeer.new() + new_peer.create_client(ip, port) + multiplayer.multiplayer_peer = new_peer + + status.hosting = false + status.client = true + GlobalLogger.log_string("Connected to the server.", 1) + return + +func kick_player(player_id: int, reason: String = "No reason specified"): + # Server kicks a player from the session. + return + +func ban_player(): + # Server permanatly bans a user. + return + +func _on_connected(): + # We are connected to the server. + GlobalLogger.log_string("Connected to the server as '%s'." % multiplayer.get_unique_id(), 1) + return + +func _on_connection_failed(): + GlobalLogger.log_string("Connection to server failed.", 1) + return + +func _on_peer_connected(client_id): + info.level_node_name = get_tree().current_scene.get_node("Scenes").get_child(0).name + + # A client has been connected to our server. + if multiplayer.is_server() == false: + return + + # TODO: Preform validation to determine if the player is allowed to be here + + GlobalLogger.log_string("'%s' connected to us. Sending our server info." % multiplayer.get_unique_id(), 1) + _receive_server_info.rpc_id(client_id, info) + + return + +func _on_peer_disconnected(): + return + +func set_networking_config(options: Dictionary) -> void: + if !options: + GlobalLogger.log_string("Tried to set networking config without options", 2) + return + + # LAN connections + if options.lan == true: + config.use_lan = true + else: + config.use_lan = false + + # Steam connections + if options.steam == true: + config.use_steam = true + else: + config.use_steam = false + +@rpc("authority", "reliable") +func _receive_server_info(server_info: Dictionary): + GlobalLogger.log_string("Received server information.") + print(server_info) + + if server_info.level: + scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) + + _send_player_info({"display_name": "Client"}) + +@rpc("any_peer", "reliable") +func _receive_player_info(player_info: Dictionary): + GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) + + if multiplayer.is_server() == false: + return + + # TODO: Preform validation to determine if the player is allowed to be here + # TODO: Preform validation to determine if the player supplied cridentials are good, where they need to be. + + player_info = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) + + info.clients.append(player_info) + + # Spawn player + multiplayer_manager.spawn_player(player_info.multiplayer_id) + multiplayer_manager.rpc("spawn_player", player_info.multiplayer_id) + multiplayer_manager.rpc("spawn_player", 1) + send_server_session_info() + +func _send_player_info(player_info: Dictionary): + GlobalLogger.log_string("Starting server handshake: Sending information about ourself.") + _receive_player_info.rpc_id(1, player_info) + +func send_server_session_info() -> void: + rpc("received_server_session_info", info) + +@rpc("authority", "reliable") +func received_server_session_info(received_info: Dictionary) -> void: + GlobalLogger.log_string("Session information updated.") + info = received_info + return + +# TODO: Handle kick from server +# TODO: Handle ban from server +# TODO: Add item to player inventory +# TODO: Remove item from player inventory +# TODO: Check if item exists in player inventory +# TODO: Get player inventory + +func _sanity_check_player_info(player_info: Dictionary, multiplayer_id: int) -> Dictionary: + var sane_player_info = { + "display_name": "", + "multiplayer_id": "" + } + + sane_player_info.display_name = str(player_info.display_name) + sane_player_info.multiplayer_id = int(multiplayer_id) + + return sane_player_info diff --git a/src/scenes/managers/app/network_manager.gd.uid b/src/scenes/managers/app/network_manager.gd.uid new file mode 100644 index 0000000..3935f11 --- /dev/null +++ b/src/scenes/managers/app/network_manager.gd.uid @@ -0,0 +1 @@ +uid://cc0ei8wuetrvh diff --git a/src/scenes/managers/app/network_manager.tscn b/src/scenes/managers/app/network_manager.tscn new file mode 100644 index 0000000..a76ebca --- /dev/null +++ b/src/scenes/managers/app/network_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://5v8rbnp716b0"] + +[ext_resource type="Script" uid="uid://cc0ei8wuetrvh" path="res://scenes/managers/app/network_manager.gd" id="1_daels"] + +[node name="NetworkManager" type="Node"] +script = ExtResource("1_daels") diff --git a/src/scenes/managers/app/scene_manager.gd b/src/scenes/managers/app/scene_manager.gd new file mode 100644 index 0000000..b8e366c --- /dev/null +++ b/src/scenes/managers/app/scene_manager.gd @@ -0,0 +1,44 @@ +extends Node + +# Game managers +@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") +@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") + +# +@onready var scene_work_root = get_tree().current_scene.get_node("Scenes") +@onready var player_home_scene: PackedScene = load("res://scenes/levels/home.tscn") + +var server_init: bool = false + +func _ready(): + await network_manager.start_server() + var session_name = Util.random_string(6) + var new_home = player_home_scene.instantiate() + new_home.name = session_name + multiplayer_manager.active_session = session_name + scene_work_root.add_child(new_home) + + _spawn_host_player() + +func load_multiplayer_scene(scene_dir: String, scene_name: String): + await _clean_scene_work_root() + var scene_packed: PackedScene = load(scene_dir) + + var scene = scene_packed.instantiate() + scene.name = scene_name + scene_work_root.add_child(scene) + multiplayer_manager.active_session = scene_name + +func _clean_scene_work_root(): + multiplayer_manager.active_session = "" + var nodes_to_destroy = scene_work_root.get_children() + for node in nodes_to_destroy: + node.queue_free() + await get_tree().process_frame + return + +func _spawn_host_player(): + multiplayer_manager.spawn_player(1) + +func get_current_session_node(): + return get_tree().current_scene.get_node("Scenes").get_child(0) diff --git a/src/scenes/managers/app/scene_manager.gd.uid b/src/scenes/managers/app/scene_manager.gd.uid new file mode 100644 index 0000000..44c2b63 --- /dev/null +++ b/src/scenes/managers/app/scene_manager.gd.uid @@ -0,0 +1 @@ +uid://bwe2gsnip66cr diff --git a/src/scenes/managers/app/scene_manager.tscn b/src/scenes/managers/app/scene_manager.tscn new file mode 100644 index 0000000..435d9de --- /dev/null +++ b/src/scenes/managers/app/scene_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://cmknpdx5ba15o"] + +[ext_resource type="Script" uid="uid://bwe2gsnip66cr" path="res://scenes/managers/app/scene_manager.gd" id="1_i8qdd"] + +[node name="SceneManager" type="Node"] +script = ExtResource("1_i8qdd") diff --git a/src/scenes/managers/level/multiplayer_manager.gd b/src/scenes/managers/level/multiplayer_manager.gd new file mode 100644 index 0000000..e27c701 --- /dev/null +++ b/src/scenes/managers/level/multiplayer_manager.gd @@ -0,0 +1,29 @@ +extends Node + +@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") +var n_c = preload("res://scripts/network_compression.gd").new() +var active_session: String = "" + +func _ready(): + return + +@rpc("authority", "reliable") +func spawn_player(id: int): + var player_scene: PackedScene = load("res://scenes/players/player.tscn") + GlobalLogger.log_string("Spawning player %s" % id) + var new_player = player_scene.instantiate() + new_player.name = str(id) + new_player.position = Vector3(0, 0, 0) + scene_manager.get_current_session_node().call_deferred("add_child", new_player) + +@rpc("any_peer", "reliable") +func player_position(info: PackedByteArray): + var target_node = scene_manager.get_current_session_node().get_node_or_null(str(multiplayer.get_remote_sender_id())) + + if target_node == null: + return + + # HACK: The rotation data is hacked on here. This needs to be addressed at some point. + target_node.position = n_c.d_16_pos(info) + target_node.rotation = n_c.d_16_vec3(info.slice(12)) + return diff --git a/src/scenes/managers/level/multiplayer_manager.gd.uid b/src/scenes/managers/level/multiplayer_manager.gd.uid new file mode 100644 index 0000000..5aee08e --- /dev/null +++ b/src/scenes/managers/level/multiplayer_manager.gd.uid @@ -0,0 +1 @@ +uid://qp3jkmif6h85 diff --git a/src/scenes/managers/level/multiplayer_manager.tscn b/src/scenes/managers/level/multiplayer_manager.tscn new file mode 100644 index 0000000..394ab67 --- /dev/null +++ b/src/scenes/managers/level/multiplayer_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://g37f2dfffc8o"] + +[ext_resource type="Script" uid="uid://qp3jkmif6h85" path="res://scenes/managers/level/multiplayer_manager.gd" id="1_qo074"] + +[node name="MultiplayerManager" type="Node"] +script = ExtResource("1_qo074") diff --git a/src/scenes/master.tscn b/src/scenes/master.tscn new file mode 100644 index 0000000..df70c16 --- /dev/null +++ b/src/scenes/master.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=5 format=3 uid="uid://cxk6c0uipjjpo"] + +[ext_resource type="PackedScene" uid="uid://g37f2dfffc8o" path="res://scenes/managers/level/multiplayer_manager.tscn" id="1_h2qy3"] +[ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network_manager.tscn" id="1_jooxx"] +[ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene_manager.tscn" id="2_h2qy3"] +[ext_resource type="PackedScene" uid="uid://bdsc5kvle3jgd" path="res://userinterface/hud.tscn" id="2_rnotf"] + +[node name="Master" type="Node3D"] + +[node name="MultiplayerManager" parent="." instance=ExtResource("1_h2qy3")] + +[node name="NetworkManager" parent="." instance=ExtResource("1_jooxx")] + +[node name="SceneManager" parent="." instance=ExtResource("2_h2qy3")] + +[node name="Hud" parent="." instance=ExtResource("2_rnotf")] +visible = false + +[node name="Scenes" type="Node3D" parent="."] diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd new file mode 100644 index 0000000..b1c2841 --- /dev/null +++ b/src/scenes/players/player.gd @@ -0,0 +1,149 @@ +extends CharacterBody3D + +var speed = 5.0 + +@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") +@onready var hud = get_tree().current_scene.get_node("Hud") + +var n_c = preload("res://scripts/network_compression.gd").new() + +@onready var body = $"." +@onready var head = $Head +@onready var camera = $Head/Camera3D +@onready var interaction_ray = $Head/Camera3D/InteractionRay +@export var mouse_sensitivity: float = 1.5 + +const base_fov = 90.0 +const fov_change = 1.1 + +# Player speed +const SPRINT_SPEED = 6.0 +const WALK_SPEED = 3.0 +const CROUCH_SPEED = 1.5 +const PRONE_SPEED = 0.5 + +const JUMP_VELOCITY = 4.5 +const SENSITIVITY = 1.5 + +# Player statuses +var mouse_captured: bool = false + +# TODO: Rotate climbing collider as you move WASD + +# Get the gravity from the project settings to be synced with RigidBody nodes. +var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") + +func _enter_tree(): + set_multiplayer_authority(name.to_int()) + +func _ready(): + camera.fov = base_fov + camera.current = is_multiplayer_authority() + +func _input(event): + if is_multiplayer_authority() == false: + return + if Input.is_action_just_pressed("escape"): + if mouse_captured: + capture_mouse(false) + hud.set_active_state(true) + else: + capture_mouse(true) + hud.set_active_state(false) + return + + if event is InputEventMouseMotion && mouse_captured: + body.rotate_y(-event.relative.x * mouse_sensitivity * 0.001) + camera.rotate_x(-event.relative.y * mouse_sensitivity * 0.001) + camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-85), deg_to_rad(89)) + +func _physics_process(delta): + if is_multiplayer_authority() == false: + return + # Add the gravity. + check_if_interaction_ray_is_colliding() + + if not is_on_floor(): + velocity.y -= gravity * delta + 0.05 + + + if Input.is_action_pressed("sprint"): + speed = lerp(speed, SPRINT_SPEED, delta * 7.0) + var pos = Vector3.ZERO + pos.y = 1.7 + pos.z = -0.15 + else: + speed = lerp(speed, WALK_SPEED, delta * 7.0) + var pos = Vector3.ZERO + pos.y = 1.7 + pos.z = -0.15 + head.transform.origin = lerp(head.transform.origin, pos, delta * 7.0) + + if !is_on_floor(): + speed = speed / 1.1 + + # Get the input direction and handle the movement/deceleration. + if mouse_captured == true: + var input_dir = Input.get_vector("left", "right", "forward", "backward") + var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() + if direction: + velocity.x = direction.x * speed + velocity.z = direction.z * speed + else: + velocity.x = lerp(velocity.x, direction.x * speed, delta * 20.0) + velocity.z = lerp(velocity.z, direction.z * speed, delta * 20.0) + + var velocity_clamped = clamp(velocity.length(), 0.5, SPRINT_SPEED * 2) + var target_fov = base_fov + fov_change * velocity_clamped + camera.fov = lerp(camera.fov, target_fov, delta * 8.0) + + if Input.is_action_just_pressed("jump") and is_on_floor(): + velocity.y = JUMP_VELOCITY + + move_and_slide() + _send_player_synchronization_info() + +func round_to_dec(num, digit): + return round(num * pow(10.0, digit)) / pow(10.0, digit) + +# User interaction ray +func check_if_interaction_ray_is_colliding(): + if interaction_ray.is_colliding(): + var subscene_root = get_subscene_root(interaction_ray.get_collider()); + if subscene_root == null: + return + + if !subscene_root.is_in_group("interactable"): + return + + # Interact + if Input.is_action_just_pressed("interact"): + subscene_root.interact() + +func get_subscene_root(node: Node) -> Node: + var current_node = node + if current_node.get_parent() != null: + return current_node + else: + return null + +func capture_mouse(to_capture: bool): + if to_capture == false: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + mouse_captured = false + else: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + mouse_captured = true + return + +func _send_player_synchronization_info(): + if is_multiplayer_authority() == false: + return + + var compressed_position = n_c.c_16_pos(position) + var compressed_rotation = n_c.c_16_vec3(rotation) + + # HACK: We are just appending the rotation bits at the end here. It should probably be more efficient somewhere else. + compressed_position.append_array(compressed_rotation) + + multiplayer_manager.rpc("player_position", compressed_position) \ No newline at end of file diff --git a/src/scenes/players/player.gd.uid b/src/scenes/players/player.gd.uid new file mode 100644 index 0000000..b427d2c --- /dev/null +++ b/src/scenes/players/player.gd.uid @@ -0,0 +1 @@ +uid://dxa60xi5uelay diff --git a/src/scenes/players/player.tscn b/src/scenes/players/player.tscn new file mode 100644 index 0000000..c7adb76 --- /dev/null +++ b/src/scenes/players/player.tscn @@ -0,0 +1,50 @@ +[gd_scene load_steps=5 format=3 uid="uid://dvx1vs2ig7st4"] + +[ext_resource type="Script" uid="uid://dxa60xi5uelay" path="res://scenes/players/player.gd" id="1_plyga"] + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_p1mvi"] + +[sub_resource type="CylinderShape3D" id="CylinderShape3D_plyga"] + +[sub_resource type="SeparationRayShape3D" id="SeparationRayShape3D_xrm3l"] +length = 0.25 + +[node name="Player" type="CharacterBody3D" groups=["Players"]] +script = ExtResource("1_plyga") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("CapsuleMesh_p1mvi") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("CylinderShape3D_plyga") + +[node name="Head" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, -0.15) + +[node name="Camera3D" type="Camera3D" parent="Head"] +fov = 40.0 + +[node name="InteractionRay" type="RayCast3D" parent="Head/Camera3D"] +target_position = Vector3(0, 0, -2) +collision_mask = 2 +hit_back_faces = false + +[node name="AimSight" type="RayCast3D" parent="Head/Camera3D"] +transform = Transform3D(1, 0, 0, 0, -1, -8.74228e-08, 0, 8.74228e-08, -1, 0, 0, 0) +target_position = Vector3(0, 0, 3000) +hit_back_faces = false + +[node name="Hands" type="Node3D" parent="Head/Camera3D"] +transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, 0, 0, 0) + +[node name="StairStep" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0.3, -0.5194253) +shape = SubResource("SeparationRayShape3D_xrm3l") + +[node name="PlayerMovement" type="Node" parent="."] + +[node name="PlayerInput" type="Node" parent="."] + +[node name="PlayerInventory" type="Node" parent="."] diff --git a/src/scripts/Files.gd b/src/scripts/Files.gd new file mode 100644 index 0000000..129ac22 --- /dev/null +++ b/src/scripts/Files.gd @@ -0,0 +1,41 @@ +# This library provides an interface for file handling +# This handles housekeeping related to files including file creation, deletion, and some modifications. + +extends Node + +# TODO: Every 7 days, zip all log files and compress them. Delete the original files. +# TODO: After zip file is a month old, delete it. + +const app_name: String = "Open Wound" + +func log_file_exists() -> bool: + var today_log_filename = get_today_log_file_name() + var docs_path = OS.get_user_data_dir() + var does_log_file_exist = FileAccess.file_exists("%s/logs/%s.%s" % [docs_path, app_name, today_log_filename]) + return does_log_file_exist + +func get_today_log_file_name() -> String: + var current_timestring = Time.get_datetime_string_from_system() + return sanitize_log_file_name(current_timestring) + +func sanitize_log_file_name(file_name: String) -> String: + return file_name.replace("-", "_").replace("T", "-").replace(":", "_") + +func parse_log_file_name(file_name: String) -> Dictionary: + var date = file_name.split(".")[1].split("-") + var year = date[0].split("_")[0] + var month = date[0].split("_")[1] + var day = date[0].split("_")[2] + var hour = date[1].split("_")[0] + var minute = date[1].split("_")[1] + var second = date[1].split("_")[2] + var time_dictionary = Time.get_datetime_dict_from_datetime_string("%s-%s-%sT%s:%s:%s" % [year, month, day, hour, minute, second], true) + return time_dictionary + +func create_log_file() -> String: + var today_log_filename = get_today_log_file_name() + var docs_path = OS.get_user_data_dir() + var path = "%s/logs/%s.%s" % [docs_path, app_name, today_log_filename] + var file = FileAccess.open(path, FileAccess.WRITE) + file.close() + return path diff --git a/src/scripts/Files.gd.uid b/src/scripts/Files.gd.uid new file mode 100644 index 0000000..6ed3a18 --- /dev/null +++ b/src/scripts/Files.gd.uid @@ -0,0 +1 @@ +uid://be7s67g7qwhfe diff --git a/src/scripts/LaunchArguments.gd b/src/scripts/LaunchArguments.gd new file mode 100644 index 0000000..4771c4e --- /dev/null +++ b/src/scripts/LaunchArguments.gd @@ -0,0 +1,14 @@ +extends Node + +var arguments = {} + +func get_command_line_args() -> Dictionary: + for argument in OS.get_cmdline_args(): + if argument.contains("="): + var key_value = argument.split("=") + arguments[key_value[0].trim_prefix("--")] = key_value[1] + else: + # Options without an argument will be present in the dictionary, + # with the value set to an empty string. + arguments[argument.trim_prefix("--")] = "" + return arguments diff --git a/src/scripts/LaunchArguments.gd.uid b/src/scripts/LaunchArguments.gd.uid new file mode 100644 index 0000000..29d7da6 --- /dev/null +++ b/src/scripts/LaunchArguments.gd.uid @@ -0,0 +1 @@ +uid://8oa8u1aicxac diff --git a/src/scripts/Logger.gd b/src/scripts/Logger.gd new file mode 100644 index 0000000..83c12c1 --- /dev/null +++ b/src/scripts/Logger.gd @@ -0,0 +1,73 @@ +extends Node + +var console_logging_enabled: bool = true +var file_logging_enabled: bool = true +var log_file: FileAccess +var log_file_initialized = false +var log_file_path = "" + +var log_level_colors = { + 0: "lightblue", + 1: "green", + 2: "yellow", + 3: "red" +} +var log_level_names = { + 0: "Debug", + 1: "Info", + 2: "Warning", + 3: "Error" +} + +func _ready(): + var launch_arguments: Dictionary = LaunchArguments.get_command_line_args() + console_logging_enabled = !launch_arguments.has("console_log") || launch_arguments.console_log == "True" + file_logging_enabled = !launch_arguments.has("file_log") || launch_arguments.file_log == "True" + _initialize_log_file() + set_console_logging(true) + set_file_logging(true) + log_string("Logger initialized") + +func _initialize_log_file(): + if not FileManager.log_file_exists(): + log_file_path = FileManager.create_log_file() + log_file = FileAccess.open(log_file_path, FileAccess.WRITE) + log_file_initialized = true + log_string("Opened log file at %s" % log_file_path, 0) + +## Logs a message to both file and console (if enabled). +## @param message: The message string to log. If omitted, defaults to an empty string. +## @param level: The log level indicating the severity. Must be an integer: +## 0 -> Debug +## 1 -> Info +## 2 -> Warning +## 3 -> Error +## Defaults to 0 (Debug). +func log_string(message: String = "", level: int = 0): + _log_to_file(message, level) + + if console_logging_enabled: + print_rich("[[color=%s]%s[/color]] %s" % [log_level_colors[level], log_level_names[level], message]) + pass + +func _log_to_file(message: String = "", level: int = 0): + if file_logging_enabled: + var formatted_log = "[%s] %s" % [log_level_names[level], message] + # log_file.store_line(formatted_log) + # log_file.flush() + +func set_console_logging(enabled: bool): + if enabled: + console_logging_enabled = true + log_string("Console logging enabled for this session.", 1) + else: + console_logging_enabled = false + log_string("Console logging disabled for this session.", 1) + +func set_file_logging(enabled: bool): + if enabled: + file_logging_enabled = true + log_string("File logging enabled for this session.", 1) + else: + file_logging_enabled = false + log_string("File logging disabled for this session.", 1) diff --git a/src/scripts/Logger.gd.uid b/src/scripts/Logger.gd.uid new file mode 100644 index 0000000..ac95df2 --- /dev/null +++ b/src/scripts/Logger.gd.uid @@ -0,0 +1 @@ +uid://dgmfafi41y1nk diff --git a/src/scripts/Util.gd b/src/scripts/Util.gd new file mode 100644 index 0000000..02868bb --- /dev/null +++ b/src/scripts/Util.gd @@ -0,0 +1,10 @@ +extends Node + +func random_string(length: int = 6): + var rng = RandomNumberGenerator.new() + rng.randomize() + var chars = "0123456789abcdef" + var out = "" + for i in length: + out += chars[rng.randi_range(0, chars.length() - 1)] + return out \ No newline at end of file diff --git a/src/scripts/Util.gd.uid b/src/scripts/Util.gd.uid new file mode 100644 index 0000000..d2816f6 --- /dev/null +++ b/src/scripts/Util.gd.uid @@ -0,0 +1 @@ +uid://blph0tkw30w0i diff --git a/src/scripts/network_compression.gd b/src/scripts/network_compression.gd new file mode 100644 index 0000000..64f6faa --- /dev/null +++ b/src/scripts/network_compression.gd @@ -0,0 +1,165 @@ +extends Node + +# TODO: Positional data can further be reduced by using raw bytes instead of the given 16 bits. Maybe shoot for 10 bytes? + +## Compresses a Vector3 to a PackedByteArray using 32 bit precision +## @returns PackedByteArray +func c_32_vec3(provided_data: Vector3) -> PackedByteArray: + var data = PackedByteArray() + data.resize(12) + data.encode_s32(0, _float_to_int(provided_data.x)) + data.encode_s32(4, _float_to_int(provided_data.y)) + data.encode_s32(8, _float_to_int(provided_data.z)) + + return data + +## Decompress a PackedByteArray to a Vector3 using 32 bit precision +## @returns Vector3 +func d_32_vec3(provided_data: PackedByteArray) -> Vector3: + # Validate array size + if provided_data.size() < 12: + GlobalLogger.log_string("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + return Vector3() + + var x = _int_to_float(provided_data.decode_s32(0)) + var y = _int_to_float(provided_data.decode_s32(4)) + var z = _int_to_float(provided_data.decode_s32(8)) + + return Vector3(x, y, z) + +## Compresses a Vector3 to a PackedByteArray using 16 bit precision +## @returns PackedByteArray +func c_16_vec3(provided_data: Vector3) -> PackedByteArray: + var data = PackedByteArray() + data.resize(6) + data.encode_s16(0, _float_to_int(provided_data.x)) + data.encode_s16(2, _float_to_int(provided_data.y)) + data.encode_s16(4, _float_to_int(provided_data.z)) + + return data + +## Decompress a PackedByteArray to a Vector3 using 16 bit precision +## @returns Vector3 +func d_16_vec3(provided_data: PackedByteArray) -> Vector3: + # Validate array size + if provided_data.size() < 6: + GlobalLogger.log_string("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + return Vector3() + + var x = _int_to_float(provided_data.decode_s16(0)) + var y = _int_to_float(provided_data.decode_s16(2)) + var z = _int_to_float(provided_data.decode_s16(4)) + + return Vector3(x, y, z) + +## Compresses a user position with octree position to a PackedByteArray using 32 bit precision +## @returns PackedByteArray +func c_32_pos(provided_data: Vector3) -> PackedByteArray: + const OCTREE_OCTANT_SIZE: int = 1000 + + # Get the octree position + # HINT: o_x = octree_x_position + # FIXME: Unsure if we even need to round, or if this is the correct operation + var o_x = int(provided_data.x / OCTREE_OCTANT_SIZE) + var o_y = int(provided_data.y / OCTREE_OCTANT_SIZE) + var o_z = int(provided_data.z / OCTREE_OCTANT_SIZE) + + # HINT: octree_compressed_position + var o_c_pos = c_32_vec3(Vector3(o_x, o_y, o_z)) + + # Get the position in that octree + # HINT: i_x = internal_x_position + var i_x = _float_to_int(fmod(provided_data.x, OCTREE_OCTANT_SIZE)) + var i_y = _float_to_int(fmod(provided_data.y, OCTREE_OCTANT_SIZE)) + var i_z = _float_to_int(fmod(provided_data.z, OCTREE_OCTANT_SIZE)) + + var i_c_pos = c_32_vec3(Vector3(i_x, i_y, i_z)) + + # HINT: packed_compressed_position + # FIXME: Appending the array does not work for some reason. Try and figure that out + var p_c_pos = PackedByteArray() + p_c_pos.resize(24) + p_c_pos.encode_s32(0, o_x) + p_c_pos.encode_s32(4, o_y) + p_c_pos.encode_s32(8, o_z) + p_c_pos.encode_s32(12, i_x) + p_c_pos.encode_s32(16, i_y) + p_c_pos.encode_s32(20, i_z) + + return p_c_pos + +## Decompress a user position with octree position to a Vector3 using 32 bit precision +## @returns Vector3 +func d_32_pos(provided_data: PackedByteArray) -> Vector3: + # NOTE: See compression function for variable name hints. + const OCTREE_OCTANT_SIZE: int = 1000 + + var o_x = provided_data.decode_s32(0) + var o_y = provided_data.decode_s32(4) + var o_z = provided_data.decode_s32(8) + + var i_x = _int_to_float(provided_data.decode_s32(12)) + var i_y = _int_to_float(provided_data.decode_s32(16)) + var i_z = _int_to_float(provided_data.decode_s32(20)) + + var g_pos_x = (float(o_x) * OCTREE_OCTANT_SIZE) + i_x + var g_pos_y = (float(o_y) * OCTREE_OCTANT_SIZE) + i_y + var g_pos_z = (float(o_z) * OCTREE_OCTANT_SIZE) + i_z + + var global_position = Vector3(g_pos_x, g_pos_y, g_pos_z) + + return global_position + +## Compresses a user position with octree position to a PackedByteArray using 16 bit precision +## @returns PackedByteArray +func c_16_pos(provided_data: Vector3) -> PackedByteArray: + const OCTREE_OCTANT_SIZE: int = 1000 + + var o_x = int(provided_data.x / OCTREE_OCTANT_SIZE) + var o_y = int(provided_data.y / OCTREE_OCTANT_SIZE) + var o_z = int(provided_data.z / OCTREE_OCTANT_SIZE) + + var i_x = _float_to_int(fmod(provided_data.x, OCTREE_OCTANT_SIZE)) + var i_y = _float_to_int(fmod(provided_data.y, OCTREE_OCTANT_SIZE)) + var i_z = _float_to_int(fmod(provided_data.z, OCTREE_OCTANT_SIZE)) + + var p_c_pos = PackedByteArray() + p_c_pos.resize(12) + p_c_pos.encode_s16(0, o_x) + p_c_pos.encode_s16(2, o_y) + p_c_pos.encode_s16(4, o_z) + p_c_pos.encode_s16(6, i_x) + p_c_pos.encode_s16(8, i_y) + p_c_pos.encode_s16(10, i_z) + + return p_c_pos + +## Decompress a user position with octree position to a Vector3 using 16 bit precision +## @returns Vector3 +func d_16_pos(provided_data: PackedByteArray) -> Vector3: + # NOTE: See compression function for variable name hints. + const OCTREE_OCTANT_SIZE: int = 1000 + + var o_x = provided_data.decode_s16(0) + var o_y = provided_data.decode_s16(2) + var o_z = provided_data.decode_s16(4) + + var i_x = _int_to_float(provided_data.decode_s16(6)) + var i_y = _int_to_float(provided_data.decode_s16(8)) + var i_z = _int_to_float(provided_data.decode_s16(10)) + + var g_pos_x = (float(o_x) * OCTREE_OCTANT_SIZE) + i_x + var g_pos_y = (float(o_y) * OCTREE_OCTANT_SIZE) + i_y + var g_pos_z = (float(o_z) * OCTREE_OCTANT_SIZE) + i_z + + var global_position = Vector3(g_pos_x, g_pos_y, g_pos_z) + + return global_position + +func _float_to_int(val: float) -> int: + const FLOAT_PRECISION: int = 1000 + return int(val * FLOAT_PRECISION) + +func _int_to_float(val: int) -> float: + const FLOAT_PRECISION: int = 1000 + return float(val) / FLOAT_PRECISION \ No newline at end of file diff --git a/src/scripts/network_compression.gd.uid b/src/scripts/network_compression.gd.uid new file mode 100644 index 0000000..53bf2f3 --- /dev/null +++ b/src/scripts/network_compression.gd.uid @@ -0,0 +1 @@ +uid://b2iq75uom64x2 diff --git a/src/userinterface/hud.gd b/src/userinterface/hud.gd new file mode 100644 index 0000000..d4fe7fd --- /dev/null +++ b/src/userinterface/hud.gd @@ -0,0 +1,22 @@ +extends Control + +@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") + +func _ready(): + while true: + await get_tree().create_timer(1).timeout + _update_hud_state() + +func _on_join_pressed(): + await network_manager.join_server("localhost") + +func set_active_state(state: bool = false): + visible = state + +func _update_hud_state(): + var user_list_formatted = network_manager.info.clients.map(func(elem): return elem.display_name) + %HostingBool.text = "Host: %s" % network_manager.status.hosting + %SessionHost.text = "Server Host: %s" % network_manager.info.clients[0].display_name + %ClientBool.text = "Client: %s" % network_manager.status.client + %ConnectedUserCount.text = "Total Users: %s" % len(network_manager.info.clients) + %UserList.text = "User List: %s" % ", ".join(user_list_formatted) diff --git a/src/userinterface/hud.gd.uid b/src/userinterface/hud.gd.uid new file mode 100644 index 0000000..599cb9f --- /dev/null +++ b/src/userinterface/hud.gd.uid @@ -0,0 +1 @@ +uid://7j31we0siswq diff --git a/src/userinterface/hud.tscn b/src/userinterface/hud.tscn new file mode 100644 index 0000000..4ad1ea4 --- /dev/null +++ b/src/userinterface/hud.tscn @@ -0,0 +1,453 @@ +[gd_scene load_steps=5 format=3 uid="uid://bdsc5kvle3jgd"] + +[ext_resource type="Script" uid="uid://7j31we0siswq" path="res://userinterface/hud.gd" id="1_go5o5"] +[ext_resource type="Texture2D" uid="uid://coqi7w7inqyv1" path="res://resources/icons/account.svg" id="2_lco6c"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tfojp"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lco6c"] + +[node name="Hud" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_go5o5") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 50 +theme_override_constants/margin_top = 50 +theme_override_constants/margin_right = 50 +theme_override_constants/margin_bottom = 50 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/SignIn"] +custom_minimum_size = Vector2(450, 250) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Username" + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +clear_button_enabled = true +caret_blink = true + +[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Password" + +[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +clear_button_enabled = true +secret = true + +[node name="Label3" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="LoginButton" type="Button" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Login" + +[node name="Sessions" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +layout_mode = 2 + +[node name="Search" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer"] +custom_minimum_size = Vector2(350, 50) +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Search"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Search/MarginContainer"] +layout_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxEmpty_tfojp") +placeholder_text = "Search for users" + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer"] +custom_minimum_size = Vector2(350, 0) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel"] +custom_minimum_size = Vector2(350, 0) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UserContainer" type="Button" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 75) +layout_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxEmpty_lco6c") +text = " +" + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] +custom_minimum_size = Vector2(65, 65) +layout_mode = 2 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 2 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 2 + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer"] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 +mouse_filter = 1 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel/MarginContainer"] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +texture = ExtResource("2_lco6c") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 +text = "Myself" + +[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="TextContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer2"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] +layout_mode = 2 +text = "Send" + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +custom_minimum_size = Vector2(350, 0) +layout_mode = 2 + +[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Inventory" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 + +[node name="Join" type="Button" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/VBoxContainer"] +layout_mode = 2 +text = "Join" + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="ServerList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Debug" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HostingBool" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +unique_name_in_owner = true +layout_mode = 2 +text = "Hosting: ?" + +[node name="SessionHost" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +unique_name_in_owner = true +layout_mode = 2 +text = "Server Host: ?" + +[node name="ClientBool" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +unique_name_in_owner = true +layout_mode = 2 +text = "Client: ?" + +[node name="ConnectedUserCount" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +unique_name_in_owner = true +layout_mode = 2 +text = "Connected Users: ?" + +[node name="UserList" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +unique_name_in_owner = true +layout_mode = 2 +text = "User List: [?]" + +[node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel2"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer"] +layout_mode = 2 + +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Home" + +[node name="Button3" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Sessions" + +[node name="Button5" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Contacts" + +[node name="Button4" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Inventory" + +[node name="Button6" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "DEBUG" + +[node name="Button2" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Exit" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer/LoginButton" to="." method="_on_login_button_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/VBoxContainer/Join" to="." method="_on_join_pressed"] From 556573b1f4c829d9f3c32180dba53d00af1f2301 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 14 Jan 2026 02:12:16 -0600 Subject: [PATCH 02/19] Fix icon. --- src/project.godot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.godot b/src/project.godot index 2181155..9c96377 100644 --- a/src/project.godot +++ b/src/project.godot @@ -15,7 +15,7 @@ run/main_scene="uid://cxk6c0uipjjpo" config/features=PackedStringArray("4.5", "Forward Plus") run/max_fps=144 boot_splash/image="/home/adragon/Pictures/PrismLens_1_background.png" -config/icon="uid://cdukvnkmlkl0e" +config/icon="uid://du0bhthwac604" boot_splash/minimum_display_time=1000 [autoload] From 20ebfbeb32645b03de0f593708b9f6fad12d6ebb Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 14 Jan 2026 02:30:15 -0600 Subject: [PATCH 03/19] Fixed player spawning. The issue was player controllers had collisions. When they were spawned on the client, they were inside of the client on the client side. This caused the client to clip into the floor due to the client-side collision. --- src/scenes/players/player.gd | 3 +-- src/scenes/players/player.tscn | 8 +------- src/scripts/network_compression.gd | 4 ++-- src/userinterface/hud.tscn | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd index b1c2841..887edae 100644 --- a/src/scenes/players/player.gd +++ b/src/scenes/players/player.gd @@ -35,7 +35,7 @@ var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") func _enter_tree(): set_multiplayer_authority(name.to_int()) - + func _ready(): camera.fov = base_fov camera.current = is_multiplayer_authority() @@ -66,7 +66,6 @@ func _physics_process(delta): if not is_on_floor(): velocity.y -= gravity * delta + 0.05 - if Input.is_action_pressed("sprint"): speed = lerp(speed, SPRINT_SPEED, delta * 7.0) var pos = Vector3.ZERO diff --git a/src/scenes/players/player.tscn b/src/scenes/players/player.tscn index c7adb76..40d82e5 100644 --- a/src/scenes/players/player.tscn +++ b/src/scenes/players/player.tscn @@ -1,11 +1,9 @@ -[gd_scene load_steps=5 format=3 uid="uid://dvx1vs2ig7st4"] +[gd_scene load_steps=4 format=3 uid="uid://dvx1vs2ig7st4"] [ext_resource type="Script" uid="uid://dxa60xi5uelay" path="res://scenes/players/player.gd" id="1_plyga"] [sub_resource type="CapsuleMesh" id="CapsuleMesh_p1mvi"] -[sub_resource type="CylinderShape3D" id="CylinderShape3D_plyga"] - [sub_resource type="SeparationRayShape3D" id="SeparationRayShape3D_xrm3l"] length = 0.25 @@ -16,10 +14,6 @@ script = ExtResource("1_plyga") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) mesh = SubResource("CapsuleMesh_p1mvi") -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) -shape = SubResource("CylinderShape3D_plyga") - [node name="Head" type="Node3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, -0.15) diff --git a/src/scripts/network_compression.gd b/src/scripts/network_compression.gd index 64f6faa..0fcb805 100644 --- a/src/scripts/network_compression.gd +++ b/src/scripts/network_compression.gd @@ -65,7 +65,7 @@ func c_32_pos(provided_data: Vector3) -> PackedByteArray: var o_z = int(provided_data.z / OCTREE_OCTANT_SIZE) # HINT: octree_compressed_position - var o_c_pos = c_32_vec3(Vector3(o_x, o_y, o_z)) + # var o_c_pos = c_32_vec3(Vector3(o_x, o_y, o_z)) # Get the position in that octree # HINT: i_x = internal_x_position @@ -73,7 +73,7 @@ func c_32_pos(provided_data: Vector3) -> PackedByteArray: var i_y = _float_to_int(fmod(provided_data.y, OCTREE_OCTANT_SIZE)) var i_z = _float_to_int(fmod(provided_data.z, OCTREE_OCTANT_SIZE)) - var i_c_pos = c_32_vec3(Vector3(i_x, i_y, i_z)) + # var i_c_pos = c_32_vec3(Vector3(i_x, i_y, i_z)) # HINT: packed_compressed_position # FIXME: Appending the array does not work for some reason. Try and figure that out diff --git a/src/userinterface/hud.tscn b/src/userinterface/hud.tscn index 4ad1ea4..8cb886a 100644 --- a/src/userinterface/hud.tscn +++ b/src/userinterface/hud.tscn @@ -50,6 +50,7 @@ theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 [node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -320,7 +321,6 @@ theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 [node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 From c632c86dce343c5da5c8df90ba25da843e281006 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 14 Jan 2026 03:29:49 -0600 Subject: [PATCH 04/19] Allow multiple clients to see each other. Previously, only the host could see everyone. This means a third person could join and the two non-hosts could not see each other. --- src/scenes/managers/app/network_manager.gd | 12 +++++++++--- src/scenes/managers/app/scene_manager.gd | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd index 61b881c..b0db41a 100644 --- a/src/scenes/managers/app/network_manager.gd +++ b/src/scenes/managers/app/network_manager.gd @@ -66,7 +66,6 @@ func start_server(port: int = config.port, max_clients: int = config.max_clients status.hosting = true status.client = false - func close_server(): # Disconnect all players. # Remove listings from all used networking. @@ -160,7 +159,7 @@ func _receive_server_info(server_info: Dictionary): print(server_info) if server_info.level: - scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) + await scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) _send_player_info({"display_name": "Client"}) @@ -169,6 +168,7 @@ func _receive_player_info(player_info: Dictionary): GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) if multiplayer.is_server() == false: + # We are a client. We should not process any farther. return # TODO: Preform validation to determine if the player is allowed to be here @@ -181,7 +181,13 @@ func _receive_player_info(player_info: Dictionary): # Spawn player multiplayer_manager.spawn_player(player_info.multiplayer_id) multiplayer_manager.rpc("spawn_player", player_info.multiplayer_id) - multiplayer_manager.rpc("spawn_player", 1) + + # Spawn all connected clients on the new client + for client in info.clients: + if client.multiplayer_id == player_info.multiplayer_id: + continue + multiplayer_manager.rpc_id(player_info.multiplayer_id, "spawn_player", client.multiplayer_id) + send_server_session_info() func _send_player_info(player_info: Dictionary): diff --git a/src/scenes/managers/app/scene_manager.gd b/src/scenes/managers/app/scene_manager.gd index b8e366c..0609dee 100644 --- a/src/scenes/managers/app/scene_manager.gd +++ b/src/scenes/managers/app/scene_manager.gd @@ -1,10 +1,12 @@ extends Node +# TODO: Allow multiple sessions +# Right now only one session is allowed and is destroyed when the player joins another session. + # Game managers @onready var network_manager = get_tree().current_scene.get_node("NetworkManager") @onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") -# @onready var scene_work_root = get_tree().current_scene.get_node("Scenes") @onready var player_home_scene: PackedScene = load("res://scenes/levels/home.tscn") @@ -28,6 +30,8 @@ func load_multiplayer_scene(scene_dir: String, scene_name: String): scene.name = scene_name scene_work_root.add_child(scene) multiplayer_manager.active_session = scene_name + await get_tree().process_frame + return func _clean_scene_work_root(): multiplayer_manager.active_session = "" From 90e33315c2b43ee0c07cad9438d48fa8202276d0 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 14 Jan 2026 03:41:49 -0600 Subject: [PATCH 05/19] Add dashboard page switching. --- src/userinterface/hud.gd | 29 ++++++++ src/userinterface/hud.tscn | 132 +++++++++++++++++++------------------ 2 files changed, 98 insertions(+), 63 deletions(-) diff --git a/src/userinterface/hud.gd b/src/userinterface/hud.gd index d4fe7fd..ee0c119 100644 --- a/src/userinterface/hud.gd +++ b/src/userinterface/hud.gd @@ -20,3 +20,32 @@ func _update_hud_state(): %ClientBool.text = "Client: %s" % network_manager.status.client %ConnectedUserCount.text = "Total Users: %s" % len(network_manager.info.clients) %UserList.text = "User List: %s" % ", ".join(user_list_formatted) + + +func _on_nav_exit_pressed(): + _show_dashboard_page("Exit") + +func _on_nav_debug_pressed(): + _show_dashboard_page("Debug") + +func _on_nav_inventory_pressed(): + _show_dashboard_page("Inventory") + +func _on_nav_contacts_pressed(): + _show_dashboard_page("Contacts") + +func _on_nav_sessions_pressed(): + _show_dashboard_page("Sessions") + +func _on_nav_home_pressed(): + _show_dashboard_page("Home") + +func _show_dashboard_page(page_name: String = "Home"): + if page_name not in ["Home", "Sessions", "Contacts", "Inventory", "Debug", "Exit"]: + GlobalLogger.log_string("Tried to switch to an invalid dashboard page: '%s'" % page_name) + return + + for page in get_node("MarginContainer/VBoxContainer/PrimaryDashboard/").get_children(): + page.visible = false + + get_node("MarginContainer/VBoxContainer/PrimaryDashboard/%s" % page_name).visible = true \ No newline at end of file diff --git a/src/userinterface/hud.tscn b/src/userinterface/hud.tscn index 8cb886a..1e48f26 100644 --- a/src/userinterface/hud.tscn +++ b/src/userinterface/hud.tscn @@ -31,11 +31,11 @@ theme_override_constants/margin_bottom = 50 [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] layout_mode = 2 -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer"] +[node name="PrimaryDashboard" type="Panel" parent="MarginContainer/VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 -[node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -49,7 +49,7 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -63,13 +63,13 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/SignIn"] +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn"] custom_minimum_size = Vector2(450, 250) layout_mode = 2 size_flags_horizontal = 4 size_flags_vertical = 4 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -81,39 +81,39 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer"] custom_minimum_size = Vector2(400, 0) layout_mode = 2 size_flags_horizontal = 4 size_flags_vertical = 4 -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 text = "Username" -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 clear_button_enabled = true caret_blink = true -[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 text = "Password" -[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 clear_button_enabled = true secret = true -[node name="Label3" type="Label" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="Label3" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 -[node name="LoginButton" type="Button" parent="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="LoginButton" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 text = "Login" -[node name="Sessions" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Sessions" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -127,7 +127,7 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -141,17 +141,17 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts"] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts"] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] layout_mode = 2 -[node name="Search" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer"] +[node name="Search" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer"] custom_minimum_size = Vector2(350, 50) layout_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Search"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Search"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -163,17 +163,17 @@ theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 theme_override_constants/margin_bottom = 5 -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Search/MarginContainer"] +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Search/MarginContainer"] layout_mode = 2 theme_override_styles/focus = SubResource("StyleBoxEmpty_tfojp") placeholder_text = "Search for users" -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer"] +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer"] custom_minimum_size = Vector2(350, 0) layout_mode = 2 size_flags_vertical = 3 -[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel"] +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel"] custom_minimum_size = Vector2(350, 0) layout_mode = 1 anchors_preset = 15 @@ -182,7 +182,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer"] layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/margin_left = 5 @@ -190,18 +190,18 @@ theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 theme_override_constants/margin_bottom = 5 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="UserContainer" type="Button" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer"] +[node name="UserContainer" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer"] custom_minimum_size = Vector2(0, 75) layout_mode = 2 theme_override_styles/focus = SubResource("StyleBoxEmpty_lco6c") text = " " -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -213,10 +213,10 @@ theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 theme_override_constants/margin_bottom = 5 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer"] layout_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] custom_minimum_size = Vector2(65, 65) layout_mode = 2 theme_override_constants/margin_left = 2 @@ -224,12 +224,12 @@ theme_override_constants/margin_top = 2 theme_override_constants/margin_right = 2 theme_override_constants/margin_bottom = 2 -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer"] +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer"] custom_minimum_size = Vector2(50, 50) layout_mode = 2 mouse_filter = 1 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -237,7 +237,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel/MarginContainer"] +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel/MarginContainer"] custom_minimum_size = Vector2(50, 50) layout_mode = 2 size_flags_horizontal = 4 @@ -246,15 +246,15 @@ texture = ExtResource("2_lco6c") expand_mode = 5 stretch_mode = 4 -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] layout_mode = 2 text = "Myself" -[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2"] +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -264,7 +264,7 @@ grow_vertical = 2 size_flags_horizontal = 3 theme_override_constants/separation = 4 -[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2"] +[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2"] layout_mode = 2 size_flags_vertical = 3 theme_override_constants/margin_left = 5 @@ -272,33 +272,33 @@ theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 theme_override_constants/margin_bottom = 5 -[node name="TextContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer2"] +[node name="TextContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer2"] layout_mode = 2 size_flags_vertical = 3 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2"] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 theme_override_constants/margin_bottom = 5 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer"] layout_mode = 2 -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] layout_mode = 2 text = "Send" -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer"] +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] custom_minimum_size = Vector2(350, 0) layout_mode = 2 -[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Contacts/HBoxContainer/Panel"] +[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -306,7 +306,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="Inventory" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Inventory" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -320,7 +320,7 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -333,18 +333,18 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug"] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug"] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] custom_minimum_size = Vector2(200, 0) layout_mode = 2 -[node name="Join" type="Button" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/VBoxContainer"] +[node name="Join" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/VBoxContainer"] layout_mode = 2 text = "Join" -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/margin_left = 10 @@ -352,40 +352,40 @@ theme_override_constants/margin_top = 10 theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 -[node name="ServerList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/MarginContainer"] +[node name="ServerList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/MarginContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="Debug" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer"] +[node name="Debug" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="HostingBool" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +[node name="HostingBool" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] unique_name_in_owner = true layout_mode = 2 text = "Hosting: ?" -[node name="SessionHost" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +[node name="SessionHost" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] unique_name_in_owner = true layout_mode = 2 text = "Server Host: ?" -[node name="ClientBool" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +[node name="ClientBool" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] unique_name_in_owner = true layout_mode = 2 text = "Client: ?" -[node name="ConnectedUserCount" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +[node name="ConnectedUserCount" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] unique_name_in_owner = true layout_mode = 2 text = "Connected Users: ?" -[node name="UserList" type="Label" parent="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/Debug"] +[node name="UserList" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] unique_name_in_owner = true layout_mode = 2 text = "User List: [?]" -[node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel"] +[node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] visible = false layout_mode = 1 anchors_preset = 15 @@ -419,35 +419,41 @@ theme_override_constants/margin_bottom = 10 [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer"] layout_mode = 2 -[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavHome" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Home" -[node name="Button3" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavSessions" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Sessions" -[node name="Button5" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavContacts" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Contacts" -[node name="Button4" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavInventory" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Inventory" -[node name="Button6" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavDebug" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "DEBUG" -[node name="Button2" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] +[node name="NavExit" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Exit" -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel/SignIn/Panel/MarginContainer/VBoxContainer/LoginButton" to="." method="_on_login_button_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel/Debug/HBoxContainer/VBoxContainer/Join" to="." method="_on_join_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/LoginButton" to="." method="_on_login_button_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/VBoxContainer/Join" to="." method="_on_join_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavHome" to="." method="_on_nav_home_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavSessions" to="." method="_on_nav_sessions_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavContacts" to="." method="_on_nav_contacts_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavInventory" to="." method="_on_nav_inventory_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavDebug" to="." method="_on_nav_debug_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavExit" to="." method="_on_nav_exit_pressed"] From d898f6b016cbc2fd2b28bb0ae056db6162776d82 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Thu, 22 Jan 2026 04:40:10 -0600 Subject: [PATCH 06/19] Feature/http manager (#53) * Basic http manager. Basic login functionality. * Created basic credential store. * Updated url for device authentication. * Device authentication. --- src/project.godot | 1 + src/scripts/credential_store.gd | 16 ++++++ src/scripts/credential_store.gd.uid | 1 + src/scripts/http.gd | 75 +++++++++++++++++++++++++++++ src/scripts/http.gd.uid | 1 + src/scripts/keys.gd | 55 +++++++++++++++++++++ src/scripts/keys.gd.uid | 1 + src/userinterface/hud.gd | 35 +++++++++++++- src/userinterface/hud.tscn | 6 +-- 9 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 src/scripts/credential_store.gd create mode 100644 src/scripts/credential_store.gd.uid create mode 100644 src/scripts/http.gd create mode 100644 src/scripts/http.gd.uid create mode 100644 src/scripts/keys.gd create mode 100644 src/scripts/keys.gd.uid diff --git a/src/project.godot b/src/project.godot index 9c96377..d5ed840 100644 --- a/src/project.godot +++ b/src/project.godot @@ -24,6 +24,7 @@ LaunchArguments="*res://scripts/LaunchArguments.gd" GlobalLogger="*res://scripts/Logger.gd" FileManager="*res://scripts/Files.gd" Util="*res://scripts/Util.gd" +CredentialStore="*res://scripts/credential_store.gd" [display] diff --git a/src/scripts/credential_store.gd b/src/scripts/credential_store.gd new file mode 100644 index 0000000..977fa46 --- /dev/null +++ b/src/scripts/credential_store.gd @@ -0,0 +1,16 @@ +# FIXME: IMPORTANT! +# This script is more of a placeholder until a more secure method of storing these credentials becomes available. +# Due to security issues revolving around how Godot handles scripts umong other things, any object can be made accessible by anything else. +# As a result, there just isn't a safe spot to store this data. + +extends Node + +var info = { + "token": "", + "expire_time": "" +} + +func set_account_credential(credentials: PackedStringArray = []): + info.token = credentials[0] + info.expire_time = credentials[1] + GlobalLogger.log_string("Saved JWT to memory.") diff --git a/src/scripts/credential_store.gd.uid b/src/scripts/credential_store.gd.uid new file mode 100644 index 0000000..d1ea891 --- /dev/null +++ b/src/scripts/credential_store.gd.uid @@ -0,0 +1 @@ +uid://cs4c0ctis2flp diff --git a/src/scripts/http.gd b/src/scripts/http.gd new file mode 100644 index 0000000..f36aa2a --- /dev/null +++ b/src/scripts/http.gd @@ -0,0 +1,75 @@ +extends Node + +signal _completed(result: Dictionary) + +# TODO: When the http client fails to connect to server, no error appears. + +func req(method: HTTPClient.Method, host: String, path: String = "/", port: int = 443, headers: PackedStringArray = [], body: String = "") -> Dictionary: + var thread := Thread.new() + var params := { + "method": method, + "host": host, + "path": path, + "port": port, + "headers": headers, + "body": body, + "thread": thread + } + thread.start(_thread_main.bind(params)) + + return await _completed + +func _thread_main(params: Dictionary) -> void: + var client := HTTPClient.new() + var result := { + "ok": false + } + + var err := client.connect_to_host(params.host, params.port) + + # Could not connect to host + if err != OK: + result.error = "Connection failed" + _finish(params, result) + return + + # Wait for connection + while client.get_status() == HTTPClient.STATUS_CONNECTING: + client.poll() + OS.delay_msec(10) + + # TODO: Change the retry attempts? + while client.get_status() != HTTPClient.STATUS_CONNECTED: + client.poll() + OS.delay_msec(10) + + client.request(params.method, params.path, params.headers, params.body) + + while not client.has_response(): + client.poll() + OS.delay_msec(10) + + result.status_code = client.get_response_code() + result.response_headers = client.get_response_headers_as_dictionary() + + var response_body := "" + while client.get_status() == HTTPClient.STATUS_BODY: + client.poll() + var chunk := client.read_response_body_chunk() + if chunk.size() > 0: + response_body += chunk.get_string_from_utf8() + OS.delay_msec(10) + + client.close() + + result.ok = true + result.body = response_body + + _finish(params, result) + +func _finish(params: Dictionary, result: Dictionary) -> void: + call_deferred("_emit_completed", params.thread, result) + +func _emit_completed(thread: Thread, result: Dictionary) -> void: + emit_signal("_completed", result) + thread.wait_to_finish() diff --git a/src/scripts/http.gd.uid b/src/scripts/http.gd.uid new file mode 100644 index 0000000..cce1109 --- /dev/null +++ b/src/scripts/http.gd.uid @@ -0,0 +1 @@ +uid://d3cnfdwjxopsx diff --git a/src/scripts/keys.gd b/src/scripts/keys.gd new file mode 100644 index 0000000..188e57a --- /dev/null +++ b/src/scripts/keys.gd @@ -0,0 +1,55 @@ +extends Node + +var keys = { + "public": "", + "private": "" +} + +# TODO: Move this to Files.gd script. +func read_keys_from_disk(username: String) -> PackedStringArray: + var pubKeyPath = "user://accounts/%s/keys/pubKey.pem" % username + var privKeyPath = "user://accounts/%s/keys/privKey.pem" % username + + var pubKey = "" + var privKey = "" + + var keys_exist = FileAccess.file_exists(pubKeyPath) && FileAccess.file_exists(privKeyPath) + + if keys_exist: + GlobalLogger.log_string("Using saved account key.") + pubKey = FileAccess.open(pubKeyPath, FileAccess.READ).get_as_text() + privKey = FileAccess.open(privKeyPath, FileAccess.READ).get_as_text() + + return [pubKey, privKey] + + GlobalLogger.log_string("No key available. Generating a new one!") + _generate_keys() + _write_keys_to_disk(username) + + return [keys.public, keys.private] + +func _write_keys_to_disk(username): + # Make sure directory exists + var dir = DirAccess.open("user://") + dir.make_dir_recursive("user://accounts/%s/keys" % username) + + # Write keys to disk + var pubKeyPath = "user://accounts/%s/keys/pubKey.pem" % username + var privKeyPath = "user://accounts/%s/keys/privKey.pem" % username + + var pubKeyFile = FileAccess.open(pubKeyPath, FileAccess.WRITE) + pubKeyFile.store_string(keys.public) + + var privKeyFile = FileAccess.open(privKeyPath, FileAccess.WRITE) + privKeyFile.store_string(keys.private) + return + +func _generate_keys(): + var crypto = Crypto.new() + + # keys.private = crypto.generate_rsa(2048) + var generated_keys = crypto.generate_rsa(2048) + + keys.private = generated_keys.save_to_string(false) + keys.public = generated_keys.save_to_string(true) + return \ No newline at end of file diff --git a/src/scripts/keys.gd.uid b/src/scripts/keys.gd.uid new file mode 100644 index 0000000..3c9bf4e --- /dev/null +++ b/src/scripts/keys.gd.uid @@ -0,0 +1 @@ +uid://beb6jfgnx0gyc diff --git a/src/userinterface/hud.gd b/src/userinterface/hud.gd index ee0c119..b9f5b0a 100644 --- a/src/userinterface/hud.gd +++ b/src/userinterface/hud.gd @@ -1,6 +1,8 @@ extends Control @onready var network_manager = get_tree().current_scene.get_node("NetworkManager") +var http = preload("res://scripts/http.gd").new() +var keys = preload("res://scripts/keys.gd").new() func _ready(): while true: @@ -48,4 +50,35 @@ func _show_dashboard_page(page_name: String = "Home"): for page in get_node("MarginContainer/VBoxContainer/PrimaryDashboard/").get_children(): page.visible = false - get_node("MarginContainer/VBoxContainer/PrimaryDashboard/%s" % page_name).visible = true \ No newline at end of file + get_node("MarginContainer/VBoxContainer/PrimaryDashboard/%s" % page_name).visible = true + +func _on_login_button_pressed(): + var username_field_value = $"MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/UsernameField".text + var password_field_value = $"MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/PasswordField".text + var data = {"username": username_field_value, "password": password_field_value, "scope": "appAuth"} + # TODO: Make sure we have a keypair generated + + var keyPair = keys.read_keys_from_disk(username_field_value) + data["pubKey"] = keyPair[0] + + # TODO: Replace localhost with proper account server url + port + var response = await http.req(HTTPClient.Method.METHOD_POST, "http://localhost", "/api/v1/device/auth", 40400, ["Accept: application/json", "Content-Type: application/json"], JSON.stringify(data)) + + if response["ok"] == false: + GlobalLogger.log_string("Response failed for unknown reason.", 1) + return + + if response["body"] == null: + GlobalLogger.log_string("No body provided for login request.", 3) + return + + var res_body = JSON.parse_string(response["body"]) + + if "error" in res_body.keys(): + GlobalLogger.log_string("Login request returned an error. '%s'" % res_body["error"], 1) + return + + var token = response["response_headers"]["Set-Cookie"].split("; ") + token[0] = token[0].replace("token=", "") + CredentialStore.set_account_credential(token) + _show_dashboard_page("Home") \ No newline at end of file diff --git a/src/userinterface/hud.tscn b/src/userinterface/hud.tscn index 1e48f26..91755c5 100644 --- a/src/userinterface/hud.tscn +++ b/src/userinterface/hud.tscn @@ -50,7 +50,6 @@ theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 [node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -91,7 +90,7 @@ size_flags_vertical = 4 layout_mode = 2 text = "Username" -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="UsernameField" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 clear_button_enabled = true caret_blink = true @@ -100,7 +99,7 @@ caret_blink = true layout_mode = 2 text = "Password" -[node name="LineEdit2" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] +[node name="PasswordField" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] layout_mode = 2 clear_button_enabled = true secret = true @@ -321,6 +320,7 @@ theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 10 [node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 From 3a7988f0bd03cb482e93a097f21e3f731b301bdf Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Tue, 27 Jan 2026 05:23:47 -0600 Subject: [PATCH 07/19] Cleint authentication (#56) * Work. Just saving progress. * Disconnect when JWT verification fails. --- src/project.godot | 1 + src/scenes/managers/app/network_manager.gd | 69 ++++++++++++----- src/scripts/account_servers.gd | 23 ++++++ src/scripts/account_servers.gd.uid | 1 + src/scripts/jwt.gd | 86 ++++++++++++++++++++++ src/scripts/jwt.gd.uid | 1 + src/scripts/keys.gd | 3 +- 7 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 src/scripts/account_servers.gd create mode 100644 src/scripts/account_servers.gd.uid create mode 100644 src/scripts/jwt.gd create mode 100644 src/scripts/jwt.gd.uid diff --git a/src/project.godot b/src/project.godot index d5ed840..3b3e761 100644 --- a/src/project.godot +++ b/src/project.godot @@ -25,6 +25,7 @@ GlobalLogger="*res://scripts/Logger.gd" FileManager="*res://scripts/Files.gd" Util="*res://scripts/Util.gd" CredentialStore="*res://scripts/credential_store.gd" +AccountServers="*res://scripts/account_servers.gd" [display] diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd index b0db41a..51d5af3 100644 --- a/src/scenes/managers/app/network_manager.gd +++ b/src/scenes/managers/app/network_manager.gd @@ -1,6 +1,8 @@ extends Node -const n_c = preload("res://scripts/network_compression.gd") +var n_c = preload("res://scripts/network_compression.gd").new() +var jwt = preload("res://scripts/jwt.gd").new() +var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") # TODO: Bandwidth toggles @onready var scene_manager = get_tree().current_scene.get_node("SceneManager") @@ -156,41 +158,55 @@ func set_networking_config(options: Dictionary) -> void: @rpc("authority", "reliable") func _receive_server_info(server_info: Dictionary): GlobalLogger.log_string("Received server information.") - print(server_info) + # TODO: Do not change scene until connection is finalized. if server_info.level: await scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) - _send_player_info({"display_name": "Client"}) + _send_player_info(CredentialStore.info.token) @rpc("any_peer", "reliable") -func _receive_player_info(player_info: Dictionary): - GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) - +func _receive_player_info(player_info: String): + # TODO: Error checks for JWT if multiplayer.is_server() == false: # We are a client. We should not process any farther. return + + GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) # TODO: Preform validation to determine if the player is allowed to be here # TODO: Preform validation to determine if the player supplied cridentials are good, where they need to be. - player_info = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) + # Preform validation of JWT token + var player_info_dic = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) + var player_decoded_jwt = jwt.decode_jwt(player_info_dic.jwt) - info.clients.append(player_info) + # TODO: util function to break down a url to the key parts. + var url_parts = parse_url(player_decoded_jwt.payload.issuer) + var host_pub_key = await AccountServers._request_server_pem(url_parts.host, url_parts.port) + + var jwt_is_valid = jwt.verify(player_info_dic.jwt, host_pub_key) + + if jwt_is_valid == false: + # TODO: Refuse connection + multiplayer.multiplayer_peer.disconnect_peer(multiplayer.get_remote_sender_id()) + return + + info.clients.append(player_info_dic) # Spawn player - multiplayer_manager.spawn_player(player_info.multiplayer_id) - multiplayer_manager.rpc("spawn_player", player_info.multiplayer_id) + multiplayer_manager.spawn_player(player_info_dic.multiplayer_id) + multiplayer_manager.rpc("spawn_player", player_info_dic.multiplayer_id) # Spawn all connected clients on the new client for client in info.clients: - if client.multiplayer_id == player_info.multiplayer_id: + if client.multiplayer_id == player_info_dic.multiplayer_id: continue - multiplayer_manager.rpc_id(player_info.multiplayer_id, "spawn_player", client.multiplayer_id) + multiplayer_manager.rpc_id(player_info_dic.multiplayer_id, "spawn_player", client.multiplayer_id) send_server_session_info() -func _send_player_info(player_info: Dictionary): +func _send_player_info(player_info: String): GlobalLogger.log_string("Starting server handshake: Sending information about ourself.") _receive_player_info.rpc_id(1, player_info) @@ -210,13 +226,32 @@ func received_server_session_info(received_info: Dictionary) -> void: # TODO: Check if item exists in player inventory # TODO: Get player inventory -func _sanity_check_player_info(player_info: Dictionary, multiplayer_id: int) -> Dictionary: +func _sanity_check_player_info(player_info: String, multiplayer_id: int) -> Dictionary: var sane_player_info = { - "display_name": "", - "multiplayer_id": "" + "jwt": "", + "multiplayer_id": "", + "display_name": "Greetings!" } - sane_player_info.display_name = str(player_info.display_name) + sane_player_info.jwt = str(player_info) sane_player_info.multiplayer_id = int(multiplayer_id) return sane_player_info + + +func parse_url(url: String) -> Dictionary: + var result = { + "scheme": "", + "host": "", + "port": 0, + "path": "" + } + + var matches = url_regex.search(url) + if matches: + result["scheme"] = matches.get_string(1).to_lower() + result["host"] = matches.get_string(2) + result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) + result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" + + return result \ No newline at end of file diff --git a/src/scripts/account_servers.gd b/src/scripts/account_servers.gd new file mode 100644 index 0000000..c6c141d --- /dev/null +++ b/src/scripts/account_servers.gd @@ -0,0 +1,23 @@ +extends Node +var http = preload("res://scripts/http.gd").new() +var database = {} +# TODO: Open metadata file, and keep it opened + +# TODO: Validate RSA PEM key +func get_pem(host: String, port: int) -> String: + # TODO: Check if we have the key saved + return "" + +func _request_server_pem(host: String, port: int = 443) -> String: + var key = await http.req(HTTPClient.METHOD_GET, host, "/public_key", port) + if key.ok == true: + return key.body + return "" + + +func _ready(): + _request_server_pem("http://localhost", 40400) + +func _open_or_create_database(): + var dir = DirAccess.open("user://") + dir.make_dir_recursive("user://account_servers/database.json") \ No newline at end of file diff --git a/src/scripts/account_servers.gd.uid b/src/scripts/account_servers.gd.uid new file mode 100644 index 0000000..3be5ba9 --- /dev/null +++ b/src/scripts/account_servers.gd.uid @@ -0,0 +1 @@ +uid://c6heul4elcmbk diff --git a/src/scripts/jwt.gd b/src/scripts/jwt.gd new file mode 100644 index 0000000..fd78598 --- /dev/null +++ b/src/scripts/jwt.gd @@ -0,0 +1,86 @@ +# This provides basic JWT features +extends Node + +func verify(jwt_string: String = "", signature_pem: String = "") -> bool: + # TODO: Error checks + var crypto: Crypto = Crypto.new() + var public_key: CryptoKey = _signature_pem_to_cryptokey(signature_pem) + var jwt_parts: Dictionary = _get_jwt_parts(jwt_string) + var formatted_payload: Dictionary = _format_jwt_payload(jwt_parts.head, jwt_parts.payload) + + return crypto.verify( + HashingContext.HASH_SHA256, + formatted_payload.payload_bytes, + Marshalls.base64_to_raw(jwt_parts.signature), + public_key + ) + +func decode_jwt(jwt_string: String) -> Dictionary: + # TODO: Error checks + var return_dict = {"head": {}, "payload": {}} + + var jwt_parts = _get_jwt_parts(jwt_string) + + jwt_parts.head = _base64url_to_base64(jwt_parts.head) + jwt_parts.head = Marshalls.base64_to_utf8(jwt_parts.head) + return_dict.head = JSON.parse_string(jwt_parts.head) + + jwt_parts.payload = _base64url_to_base64(jwt_parts.payload) + jwt_parts.payload = Marshalls.base64_to_utf8(jwt_parts.payload) + return_dict.payload = JSON.parse_string(jwt_parts.payload) + + return return_dict + +func _base64url_to_base64(base64url: String): + # TODO: Error checks + var fixed: String = base64url + + fixed = fixed.replace("_", "/").replace("-", "+") + var padding = 4 - (fixed.length() % 4) + + if padding < 4: + fixed += "=".repeat(padding) + + return fixed + +func _signature_pem_to_cryptokey(signature_pem: String = "") -> CryptoKey: + # TODO: Error checks + var public_key := CryptoKey.new() + if public_key.load_from_string(signature_pem, true) != OK: + GlobalLogger.log_string("Failed to load signature", 3) + return null + + return public_key + +func _get_jwt_parts(jwt_string: String = "") -> Dictionary: + # TODO: Error checks + var return_dict = {"ok": false, "head": "", "payload": "", "signature": ""} + + var jwt_split = jwt_string.split(".") + + if len(jwt_split) != 3: + GlobalLogger.log_string("JWT token is not formatted correctly.", 2) + return return_dict + + return_dict.head = jwt_split[0] + return_dict.payload = jwt_split[1] + return_dict.signature = _base64url_to_base64(jwt_split[2]) + return_dict.ok = true + + return return_dict + +func _format_jwt_payload(head: String, payload: String) -> Dictionary: + # TODO: Error checks + var return_dict = {"ok": false, "payload_bytes": []} + + var formatted_payload = head + "." + payload + var payload_bytes = formatted_payload.to_utf8_buffer() + + var hasher: HashingContext = HashingContext.new() + hasher.start(HashingContext.HASH_SHA256) + hasher.update(payload_bytes) + + return_dict.payload_bytes = hasher.finish() + return_dict.ok = true + + return return_dict \ No newline at end of file diff --git a/src/scripts/jwt.gd.uid b/src/scripts/jwt.gd.uid new file mode 100644 index 0000000..5809512 --- /dev/null +++ b/src/scripts/jwt.gd.uid @@ -0,0 +1 @@ +uid://bt5gufaix02sa diff --git a/src/scripts/keys.gd b/src/scripts/keys.gd index 188e57a..d9d02b2 100644 --- a/src/scripts/keys.gd +++ b/src/scripts/keys.gd @@ -47,9 +47,8 @@ func _write_keys_to_disk(username): func _generate_keys(): var crypto = Crypto.new() - # keys.private = crypto.generate_rsa(2048) var generated_keys = crypto.generate_rsa(2048) keys.private = generated_keys.save_to_string(false) keys.public = generated_keys.save_to_string(true) - return \ No newline at end of file + return From 6f1670c858b69f1a68ce934aa1ea2a9a54a5bd45 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Tue, 27 Jan 2026 05:35:52 -0600 Subject: [PATCH 08/19] Update Godot engine to 4.6. (#57) --- src/project.godot | 6 +++++- src/scenes/master.tscn | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/project.godot b/src/project.godot index 3b3e761..ed7d6f3 100644 --- a/src/project.godot +++ b/src/project.godot @@ -8,11 +8,15 @@ config_version=5 +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + [application] config/name="PrismLens" run/main_scene="uid://cxk6c0uipjjpo" -config/features=PackedStringArray("4.5", "Forward Plus") +config/features=PackedStringArray("4.6", "Forward Plus") run/max_fps=144 boot_splash/image="/home/adragon/Pictures/PrismLens_1_background.png" config/icon="uid://du0bhthwac604" diff --git a/src/scenes/master.tscn b/src/scenes/master.tscn index df70c16..721382a 100644 --- a/src/scenes/master.tscn +++ b/src/scenes/master.tscn @@ -1,19 +1,19 @@ -[gd_scene load_steps=5 format=3 uid="uid://cxk6c0uipjjpo"] +[gd_scene format=3 uid="uid://cxk6c0uipjjpo"] [ext_resource type="PackedScene" uid="uid://g37f2dfffc8o" path="res://scenes/managers/level/multiplayer_manager.tscn" id="1_h2qy3"] [ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network_manager.tscn" id="1_jooxx"] [ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene_manager.tscn" id="2_h2qy3"] [ext_resource type="PackedScene" uid="uid://bdsc5kvle3jgd" path="res://userinterface/hud.tscn" id="2_rnotf"] -[node name="Master" type="Node3D"] +[node name="Master" type="Node3D" unique_id=420526444] -[node name="MultiplayerManager" parent="." instance=ExtResource("1_h2qy3")] +[node name="MultiplayerManager" parent="." unique_id=1925994240 instance=ExtResource("1_h2qy3")] -[node name="NetworkManager" parent="." instance=ExtResource("1_jooxx")] +[node name="NetworkManager" parent="." unique_id=1960146969 instance=ExtResource("1_jooxx")] -[node name="SceneManager" parent="." instance=ExtResource("2_h2qy3")] +[node name="SceneManager" parent="." unique_id=5477810 instance=ExtResource("2_h2qy3")] -[node name="Hud" parent="." instance=ExtResource("2_rnotf")] +[node name="Hud" parent="." unique_id=1305621179 instance=ExtResource("2_rnotf")] visible = false -[node name="Scenes" type="Node3D" parent="."] +[node name="Scenes" type="Node3D" parent="." unique_id=308239834] From 75558aa9d434058bf9ebcd2264035a091a7bd5bb Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Thu, 5 Feb 2026 10:18:42 -0600 Subject: [PATCH 09/19] Refactor 20260127 (#61) * Refactored JWT script. * Refactored files.gd. Log to a file. Changed Global log function name. Print_stack on error. * Refactored account_servers.gd * Moved credential_store. * Moved the other files. * RPC Manager. This contains a lot of dev functions. Another round of refactoring is required. * Removed multiplayer_manager and merged into network_manager. --- src/project.godot | 12 +- src/scenes/managers/app/network_manager.gd | 181 +++++------------- src/scenes/managers/app/rpc_manager.gd | 16 ++ src/scenes/managers/app/rpc_manager.gd.uid | 1 + src/scenes/managers/app/rpc_manager.tscn | 6 + src/scenes/managers/app/scene_manager.gd | 11 +- .../managers/level/multiplayer_manager.gd | 29 --- .../managers/level/multiplayer_manager.gd.uid | 1 - .../managers/level/multiplayer_manager.tscn | 6 - src/scenes/master.tscn | 4 +- src/scenes/players/player.gd | 9 +- src/scripts/Files.gd | 41 ---- src/scripts/Files.gd.uid | 1 - src/scripts/LaunchArguments.gd.uid | 1 - src/scripts/Util.gd.uid | 1 - src/scripts/account_servers.gd | 23 --- src/scripts/account_servers.gd.uid | 1 - src/scripts/crypto/aes.gd | 1 + src/scripts/crypto/aes.gd.uid | 1 + src/scripts/{ => crypto}/credential_store.gd | 6 +- .../{ => crypto}/credential_store.gd.uid | 0 src/scripts/{ => crypto}/jwt.gd | 74 ++++--- src/scripts/crypto/jwt.gd.uid | 1 + src/scripts/{ => crypto}/keys.gd | 4 +- src/scripts/{ => crypto}/keys.gd.uid | 0 src/scripts/crypto/rsa.gd | 49 +++++ src/scripts/crypto/rsa.gd.uid | 1 + src/scripts/jwt.gd.uid | 1 - src/scripts/{Logger.gd => logger.gd} | 40 ++-- src/scripts/{Logger.gd.uid => logger.gd.uid} | 0 src/scripts/network/account_servers.gd | 27 +++ src/scripts/network/account_servers.gd.uid | 1 + src/scripts/{ => network}/http.gd | 30 ++- src/scripts/{ => network}/http.gd.uid | 0 .../{ => network}/network_compression.gd | 4 +- .../{ => network}/network_compression.gd.uid | 0 src/scripts/rpc/client.gd | 29 +++ src/scripts/rpc/client.gd.uid | 1 + src/scripts/rpc/common.gd | 39 ++++ src/scripts/rpc/common.gd.uid | 1 + src/scripts/rpc/server.gd | 90 +++++++++ src/scripts/rpc/server.gd.uid | 1 + src/scripts/utils/files.gd | 65 +++++++ src/scripts/utils/files.gd.uid | 1 + .../launch_arguments.gd} | 3 + src/scripts/utils/launch_arguments.gd.uid | 1 + src/scripts/{Util.gd => utils/random.gd} | 2 +- src/scripts/utils/random.gd.uid | 1 + src/userinterface/hud.gd | 18 +- 49 files changed, 496 insertions(+), 340 deletions(-) create mode 100644 src/scenes/managers/app/rpc_manager.gd create mode 100644 src/scenes/managers/app/rpc_manager.gd.uid create mode 100644 src/scenes/managers/app/rpc_manager.tscn delete mode 100644 src/scenes/managers/level/multiplayer_manager.gd delete mode 100644 src/scenes/managers/level/multiplayer_manager.gd.uid delete mode 100644 src/scenes/managers/level/multiplayer_manager.tscn delete mode 100644 src/scripts/Files.gd delete mode 100644 src/scripts/Files.gd.uid delete mode 100644 src/scripts/LaunchArguments.gd.uid delete mode 100644 src/scripts/Util.gd.uid delete mode 100644 src/scripts/account_servers.gd delete mode 100644 src/scripts/account_servers.gd.uid create mode 100644 src/scripts/crypto/aes.gd create mode 100644 src/scripts/crypto/aes.gd.uid rename src/scripts/{ => crypto}/credential_store.gd (80%) rename src/scripts/{ => crypto}/credential_store.gd.uid (100%) rename src/scripts/{ => crypto}/jwt.gd (58%) create mode 100644 src/scripts/crypto/jwt.gd.uid rename src/scripts/{ => crypto}/keys.gd (92%) rename src/scripts/{ => crypto}/keys.gd.uid (100%) create mode 100644 src/scripts/crypto/rsa.gd create mode 100644 src/scripts/crypto/rsa.gd.uid delete mode 100644 src/scripts/jwt.gd.uid rename src/scripts/{Logger.gd => logger.gd} (64%) rename src/scripts/{Logger.gd.uid => logger.gd.uid} (100%) create mode 100644 src/scripts/network/account_servers.gd create mode 100644 src/scripts/network/account_servers.gd.uid rename src/scripts/{ => network}/http.gd (69%) rename src/scripts/{ => network}/http.gd.uid (100%) rename src/scripts/{ => network}/network_compression.gd (96%) rename src/scripts/{ => network}/network_compression.gd.uid (100%) create mode 100644 src/scripts/rpc/client.gd create mode 100644 src/scripts/rpc/client.gd.uid create mode 100644 src/scripts/rpc/common.gd create mode 100644 src/scripts/rpc/common.gd.uid create mode 100644 src/scripts/rpc/server.gd create mode 100644 src/scripts/rpc/server.gd.uid create mode 100644 src/scripts/utils/files.gd create mode 100644 src/scripts/utils/files.gd.uid rename src/scripts/{LaunchArguments.gd => utils/launch_arguments.gd} (91%) create mode 100644 src/scripts/utils/launch_arguments.gd.uid rename src/scripts/{Util.gd => utils/random.gd} (94%) create mode 100644 src/scripts/utils/random.gd.uid diff --git a/src/project.godot b/src/project.godot index ed7d6f3..b3ab4a7 100644 --- a/src/project.godot +++ b/src/project.godot @@ -24,12 +24,12 @@ boot_splash/minimum_display_time=1000 [autoload] -LaunchArguments="*res://scripts/LaunchArguments.gd" -GlobalLogger="*res://scripts/Logger.gd" -FileManager="*res://scripts/Files.gd" -Util="*res://scripts/Util.gd" -CredentialStore="*res://scripts/credential_store.gd" -AccountServers="*res://scripts/account_servers.gd" +LaunchArguments="*uid://c45jrfmrjtnyn" +GlobalLogger="*uid://dgmfafi41y1nk" +FileManager="*uid://d2s50p717g3n" +AccountServers="*uid://bpysjoq7n0ytu" +CredentialStore="*uid://cs4c0ctis2flp" +Random="*uid://1js68qt8w0mv" [display] diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd index 51d5af3..1c2df24 100644 --- a/src/scenes/managers/app/network_manager.gd +++ b/src/scenes/managers/app/network_manager.gd @@ -1,12 +1,15 @@ extends Node -var n_c = preload("res://scripts/network_compression.gd").new() -var jwt = preload("res://scripts/jwt.gd").new() +var n_c = preload("res://scripts/network/network_compression.gd").new() +var jwt = preload("res://scripts/crypto/jwt.gd").new() +var rsa = preload("res://scripts/crypto/rsa.gd").new() var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") # TODO: Bandwidth toggles @onready var scene_manager = get_tree().current_scene.get_node("SceneManager") -@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") +@onready var rpc_lib = get_tree().current_scene.get_node("RpcManager") + +var active_session = "" # This file contains all of the session management and client communication. # Anything that goes through the network should first route through here at some point. @@ -34,40 +37,47 @@ var info = { "clients": [] } -func _ready(): - multiplayer.peer_connected.connect(_on_peer_connected) - multiplayer.peer_disconnected.connect(_on_peer_disconnected) - - multiplayer.connected_to_server.connect(_on_connected) - multiplayer.connection_failed.connect(_on_connection_failed) - -func start_server(port: int = config.port, max_clients: int = config.max_clients) -> void: +func start_server(port: int = config.port, max_clients: int = config.max_clients, ignore_port: bool = false) -> void: + # TODO: In ignore_port = true, keep trying to make a server until it succeeds. if status.hosting: # This ideally should not trigger - GlobalLogger.log_string("Can not start server: Server is already running.", 2) + GlobalLogger.logs("Can not start server: Server is already running.", 2) status.hosting = false status.client = false return var new_peer = ENetMultiplayerPeer.new() # FIXME: Error handling is required here + info.clients.append({"username": "Me!", "multiplayer_id": 1}) var err = new_peer.create_server(port, max_clients) # FIXME: This client append is happening too early, this is a debug position - info.clients.append({"display_name": "Me!", "multiplayer_id": 1}) + + if err == 20: + # Port is in use + GlobalLogger.logs("Failed to start server: Is the port in use?", 1) + # FIXME: HACK: Just try again with the default port + 1. + err = new_peer.create_server(port + 1, max_clients) + status.hosting = false + status.client = false + if err != OK: - GlobalLogger.log_string("Failed to start server.", 3) + GlobalLogger.logs("Failed to start server. Error: '%s'" % err, 1) status.hosting = false status.client = false return + multiplayer.multiplayer_peer = new_peer - GlobalLogger.log_string("Successfully started server.", 1) + GlobalLogger.logs("Successfully started server.", 1) while status.hosting == false: await get_tree().process_frame status.hosting = true status.client = false - + + # TODO: Hardcoded spawn host value, is there a better way? + rpc_lib.com.on_spawn_player(1) + func close_server(): # Disconnect all players. # Remove listings from all used networking. @@ -82,18 +92,20 @@ func close_server(): func update_server(): # Update our config. # Submit a update to any active networking service. + GlobalLogger.logs("Not implemented.", 3) return func join_server(ip: String = "", port: int = config.port) -> void: # Client connects to a server. if ip.is_empty(): - GlobalLogger.log_string("No IP to connect to.", 2) + GlobalLogger.logs("No IP to connect to.", 2) return if status.hosting: # This ideally should not trigger - GlobalLogger.log_string("Can not join server: We are currently hosting a server.", 2) - return + GlobalLogger.logs("Can not join server: We are currently hosting a server.", 2) + close_server() + # return var new_peer = ENetMultiplayerPeer.new() new_peer.create_client(ip, port) @@ -101,46 +113,22 @@ func join_server(ip: String = "", port: int = config.port) -> void: status.hosting = false status.client = true - GlobalLogger.log_string("Connected to the server.", 1) + GlobalLogger.logs("Connected to the server.", 1) return func kick_player(player_id: int, reason: String = "No reason specified"): # Server kicks a player from the session. + GlobalLogger.logs("Not implemented.", 3) return func ban_player(): # Server permanatly bans a user. - return - -func _on_connected(): - # We are connected to the server. - GlobalLogger.log_string("Connected to the server as '%s'." % multiplayer.get_unique_id(), 1) - return - -func _on_connection_failed(): - GlobalLogger.log_string("Connection to server failed.", 1) - return - -func _on_peer_connected(client_id): - info.level_node_name = get_tree().current_scene.get_node("Scenes").get_child(0).name - - # A client has been connected to our server. - if multiplayer.is_server() == false: - return - - # TODO: Preform validation to determine if the player is allowed to be here - - GlobalLogger.log_string("'%s' connected to us. Sending our server info." % multiplayer.get_unique_id(), 1) - _receive_server_info.rpc_id(client_id, info) - - return - -func _on_peer_disconnected(): + GlobalLogger.logs("Not implemented.", 3) return func set_networking_config(options: Dictionary) -> void: if !options: - GlobalLogger.log_string("Tried to set networking config without options", 2) + GlobalLogger.logs("Tried to set networking config without options", 2) return # LAN connections @@ -155,90 +143,6 @@ func set_networking_config(options: Dictionary) -> void: else: config.use_steam = false -@rpc("authority", "reliable") -func _receive_server_info(server_info: Dictionary): - GlobalLogger.log_string("Received server information.") - # TODO: Do not change scene until connection is finalized. - - if server_info.level: - await scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) - - _send_player_info(CredentialStore.info.token) - -@rpc("any_peer", "reliable") -func _receive_player_info(player_info: String): - # TODO: Error checks for JWT - if multiplayer.is_server() == false: - # We are a client. We should not process any farther. - return - - GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) - - # TODO: Preform validation to determine if the player is allowed to be here - # TODO: Preform validation to determine if the player supplied cridentials are good, where they need to be. - - # Preform validation of JWT token - var player_info_dic = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) - var player_decoded_jwt = jwt.decode_jwt(player_info_dic.jwt) - - # TODO: util function to break down a url to the key parts. - var url_parts = parse_url(player_decoded_jwt.payload.issuer) - var host_pub_key = await AccountServers._request_server_pem(url_parts.host, url_parts.port) - - var jwt_is_valid = jwt.verify(player_info_dic.jwt, host_pub_key) - - if jwt_is_valid == false: - # TODO: Refuse connection - multiplayer.multiplayer_peer.disconnect_peer(multiplayer.get_remote_sender_id()) - return - - info.clients.append(player_info_dic) - - # Spawn player - multiplayer_manager.spawn_player(player_info_dic.multiplayer_id) - multiplayer_manager.rpc("spawn_player", player_info_dic.multiplayer_id) - - # Spawn all connected clients on the new client - for client in info.clients: - if client.multiplayer_id == player_info_dic.multiplayer_id: - continue - multiplayer_manager.rpc_id(player_info_dic.multiplayer_id, "spawn_player", client.multiplayer_id) - - send_server_session_info() - -func _send_player_info(player_info: String): - GlobalLogger.log_string("Starting server handshake: Sending information about ourself.") - _receive_player_info.rpc_id(1, player_info) - -func send_server_session_info() -> void: - rpc("received_server_session_info", info) - -@rpc("authority", "reliable") -func received_server_session_info(received_info: Dictionary) -> void: - GlobalLogger.log_string("Session information updated.") - info = received_info - return - -# TODO: Handle kick from server -# TODO: Handle ban from server -# TODO: Add item to player inventory -# TODO: Remove item from player inventory -# TODO: Check if item exists in player inventory -# TODO: Get player inventory - -func _sanity_check_player_info(player_info: String, multiplayer_id: int) -> Dictionary: - var sane_player_info = { - "jwt": "", - "multiplayer_id": "", - "display_name": "Greetings!" - } - - sane_player_info.jwt = str(player_info) - sane_player_info.multiplayer_id = int(multiplayer_id) - - return sane_player_info - - func parse_url(url: String) -> Dictionary: var result = { "scheme": "", @@ -254,4 +158,17 @@ func parse_url(url: String) -> Dictionary: result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" - return result \ No newline at end of file + return result + +func spawn_player(player): + # FIXME: Placeholder for refactor + while scene_manager.get_current_session_node() == null: + await get_tree().process_frame + + scene_manager.get_current_session_node().call_deferred("add_child", player) + +func player_exists(name: String) -> Node3D: + # FIXME: Placeholder for refactor + var target_node = scene_manager.get_current_session_node().get_node_or_null(name) + + return target_node diff --git a/src/scenes/managers/app/rpc_manager.gd b/src/scenes/managers/app/rpc_manager.gd new file mode 100644 index 0000000..b8ee3ca --- /dev/null +++ b/src/scenes/managers/app/rpc_manager.gd @@ -0,0 +1,16 @@ +extends Node + +var c := preload("res://scripts/rpc/client.gd").new() +var s := preload("res://scripts/rpc/server.gd").new() +var com := preload("res://scripts/rpc/common.gd").new() + +func _ready(): + # RPCs can not be called from outside of the scene tree, we are required to add them. + add_child(c) + add_child(s) + add_child(com) + + multiplayer.peer_connected.connect(s.on_peer_connected) + multiplayer.peer_disconnected.connect(s.on_peer_disconnected) + multiplayer.connected_to_server.connect(c.connected_to_server) + multiplayer.connection_failed.connect(c.connection_failed) \ No newline at end of file diff --git a/src/scenes/managers/app/rpc_manager.gd.uid b/src/scenes/managers/app/rpc_manager.gd.uid new file mode 100644 index 0000000..a7c8542 --- /dev/null +++ b/src/scenes/managers/app/rpc_manager.gd.uid @@ -0,0 +1 @@ +uid://x23lqbiivx1q diff --git a/src/scenes/managers/app/rpc_manager.tscn b/src/scenes/managers/app/rpc_manager.tscn new file mode 100644 index 0000000..bcdecb5 --- /dev/null +++ b/src/scenes/managers/app/rpc_manager.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://by0vghgshvbhd"] + +[ext_resource type="Script" uid="uid://x23lqbiivx1q" path="res://scenes/managers/app/rpc_manager.gd" id="1_6timl"] + +[node name="RpcManager" type="Node" unique_id=1836544617] +script = ExtResource("1_6timl") diff --git a/src/scenes/managers/app/scene_manager.gd b/src/scenes/managers/app/scene_manager.gd index 0609dee..6bfbab2 100644 --- a/src/scenes/managers/app/scene_manager.gd +++ b/src/scenes/managers/app/scene_manager.gd @@ -5,7 +5,6 @@ extends Node # Game managers @onready var network_manager = get_tree().current_scene.get_node("NetworkManager") -@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") @onready var scene_work_root = get_tree().current_scene.get_node("Scenes") @onready var player_home_scene: PackedScene = load("res://scenes/levels/home.tscn") @@ -14,10 +13,10 @@ var server_init: bool = false func _ready(): await network_manager.start_server() - var session_name = Util.random_string(6) + var session_name = Random.random_string(6) var new_home = player_home_scene.instantiate() new_home.name = session_name - multiplayer_manager.active_session = session_name + network_manager.active_session = session_name scene_work_root.add_child(new_home) _spawn_host_player() @@ -29,12 +28,12 @@ func load_multiplayer_scene(scene_dir: String, scene_name: String): var scene = scene_packed.instantiate() scene.name = scene_name scene_work_root.add_child(scene) - multiplayer_manager.active_session = scene_name + network_manager.active_session = scene_name await get_tree().process_frame return func _clean_scene_work_root(): - multiplayer_manager.active_session = "" + network_manager.active_session = "" var nodes_to_destroy = scene_work_root.get_children() for node in nodes_to_destroy: node.queue_free() @@ -42,7 +41,7 @@ func _clean_scene_work_root(): return func _spawn_host_player(): - multiplayer_manager.spawn_player(1) + network_manager.spawn_player(1) func get_current_session_node(): return get_tree().current_scene.get_node("Scenes").get_child(0) diff --git a/src/scenes/managers/level/multiplayer_manager.gd b/src/scenes/managers/level/multiplayer_manager.gd deleted file mode 100644 index e27c701..0000000 --- a/src/scenes/managers/level/multiplayer_manager.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends Node - -@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") -var n_c = preload("res://scripts/network_compression.gd").new() -var active_session: String = "" - -func _ready(): - return - -@rpc("authority", "reliable") -func spawn_player(id: int): - var player_scene: PackedScene = load("res://scenes/players/player.tscn") - GlobalLogger.log_string("Spawning player %s" % id) - var new_player = player_scene.instantiate() - new_player.name = str(id) - new_player.position = Vector3(0, 0, 0) - scene_manager.get_current_session_node().call_deferred("add_child", new_player) - -@rpc("any_peer", "reliable") -func player_position(info: PackedByteArray): - var target_node = scene_manager.get_current_session_node().get_node_or_null(str(multiplayer.get_remote_sender_id())) - - if target_node == null: - return - - # HACK: The rotation data is hacked on here. This needs to be addressed at some point. - target_node.position = n_c.d_16_pos(info) - target_node.rotation = n_c.d_16_vec3(info.slice(12)) - return diff --git a/src/scenes/managers/level/multiplayer_manager.gd.uid b/src/scenes/managers/level/multiplayer_manager.gd.uid deleted file mode 100644 index 5aee08e..0000000 --- a/src/scenes/managers/level/multiplayer_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://qp3jkmif6h85 diff --git a/src/scenes/managers/level/multiplayer_manager.tscn b/src/scenes/managers/level/multiplayer_manager.tscn deleted file mode 100644 index 394ab67..0000000 --- a/src/scenes/managers/level/multiplayer_manager.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://g37f2dfffc8o"] - -[ext_resource type="Script" uid="uid://qp3jkmif6h85" path="res://scenes/managers/level/multiplayer_manager.gd" id="1_qo074"] - -[node name="MultiplayerManager" type="Node"] -script = ExtResource("1_qo074") diff --git a/src/scenes/master.tscn b/src/scenes/master.tscn index 721382a..a0fe73a 100644 --- a/src/scenes/master.tscn +++ b/src/scenes/master.tscn @@ -1,13 +1,13 @@ [gd_scene format=3 uid="uid://cxk6c0uipjjpo"] -[ext_resource type="PackedScene" uid="uid://g37f2dfffc8o" path="res://scenes/managers/level/multiplayer_manager.tscn" id="1_h2qy3"] +[ext_resource type="PackedScene" uid="uid://by0vghgshvbhd" path="res://scenes/managers/app/rpc_manager.tscn" id="1_h2qy3"] [ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network_manager.tscn" id="1_jooxx"] [ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene_manager.tscn" id="2_h2qy3"] [ext_resource type="PackedScene" uid="uid://bdsc5kvle3jgd" path="res://userinterface/hud.tscn" id="2_rnotf"] [node name="Master" type="Node3D" unique_id=420526444] -[node name="MultiplayerManager" parent="." unique_id=1925994240 instance=ExtResource("1_h2qy3")] +[node name="RpcManager" parent="." unique_id=1836544617 instance=ExtResource("1_h2qy3")] [node name="NetworkManager" parent="." unique_id=1960146969 instance=ExtResource("1_jooxx")] diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd index 887edae..0b61580 100644 --- a/src/scenes/players/player.gd +++ b/src/scenes/players/player.gd @@ -1,11 +1,12 @@ extends CharacterBody3D +@onready var rpc_lib = get_tree().current_scene.get_node("RpcManager") + var speed = 5.0 -@onready var multiplayer_manager = get_tree().current_scene.get_node("MultiplayerManager") @onready var hud = get_tree().current_scene.get_node("Hud") -var n_c = preload("res://scripts/network_compression.gd").new() +var n_c = preload("res://scripts/network/network_compression.gd").new() @onready var body = $"." @onready var head = $Head @@ -144,5 +145,5 @@ func _send_player_synchronization_info(): # HACK: We are just appending the rotation bits at the end here. It should probably be more efficient somewhere else. compressed_position.append_array(compressed_rotation) - - multiplayer_manager.rpc("player_position", compressed_position) \ No newline at end of file + + rpc_lib.com.rpc("on_player_transform", compressed_position) diff --git a/src/scripts/Files.gd b/src/scripts/Files.gd deleted file mode 100644 index 129ac22..0000000 --- a/src/scripts/Files.gd +++ /dev/null @@ -1,41 +0,0 @@ -# This library provides an interface for file handling -# This handles housekeeping related to files including file creation, deletion, and some modifications. - -extends Node - -# TODO: Every 7 days, zip all log files and compress them. Delete the original files. -# TODO: After zip file is a month old, delete it. - -const app_name: String = "Open Wound" - -func log_file_exists() -> bool: - var today_log_filename = get_today_log_file_name() - var docs_path = OS.get_user_data_dir() - var does_log_file_exist = FileAccess.file_exists("%s/logs/%s.%s" % [docs_path, app_name, today_log_filename]) - return does_log_file_exist - -func get_today_log_file_name() -> String: - var current_timestring = Time.get_datetime_string_from_system() - return sanitize_log_file_name(current_timestring) - -func sanitize_log_file_name(file_name: String) -> String: - return file_name.replace("-", "_").replace("T", "-").replace(":", "_") - -func parse_log_file_name(file_name: String) -> Dictionary: - var date = file_name.split(".")[1].split("-") - var year = date[0].split("_")[0] - var month = date[0].split("_")[1] - var day = date[0].split("_")[2] - var hour = date[1].split("_")[0] - var minute = date[1].split("_")[1] - var second = date[1].split("_")[2] - var time_dictionary = Time.get_datetime_dict_from_datetime_string("%s-%s-%sT%s:%s:%s" % [year, month, day, hour, minute, second], true) - return time_dictionary - -func create_log_file() -> String: - var today_log_filename = get_today_log_file_name() - var docs_path = OS.get_user_data_dir() - var path = "%s/logs/%s.%s" % [docs_path, app_name, today_log_filename] - var file = FileAccess.open(path, FileAccess.WRITE) - file.close() - return path diff --git a/src/scripts/Files.gd.uid b/src/scripts/Files.gd.uid deleted file mode 100644 index 6ed3a18..0000000 --- a/src/scripts/Files.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://be7s67g7qwhfe diff --git a/src/scripts/LaunchArguments.gd.uid b/src/scripts/LaunchArguments.gd.uid deleted file mode 100644 index 29d7da6..0000000 --- a/src/scripts/LaunchArguments.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8oa8u1aicxac diff --git a/src/scripts/Util.gd.uid b/src/scripts/Util.gd.uid deleted file mode 100644 index d2816f6..0000000 --- a/src/scripts/Util.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://blph0tkw30w0i diff --git a/src/scripts/account_servers.gd b/src/scripts/account_servers.gd deleted file mode 100644 index c6c141d..0000000 --- a/src/scripts/account_servers.gd +++ /dev/null @@ -1,23 +0,0 @@ -extends Node -var http = preload("res://scripts/http.gd").new() -var database = {} -# TODO: Open metadata file, and keep it opened - -# TODO: Validate RSA PEM key -func get_pem(host: String, port: int) -> String: - # TODO: Check if we have the key saved - return "" - -func _request_server_pem(host: String, port: int = 443) -> String: - var key = await http.req(HTTPClient.METHOD_GET, host, "/public_key", port) - if key.ok == true: - return key.body - return "" - - -func _ready(): - _request_server_pem("http://localhost", 40400) - -func _open_or_create_database(): - var dir = DirAccess.open("user://") - dir.make_dir_recursive("user://account_servers/database.json") \ No newline at end of file diff --git a/src/scripts/account_servers.gd.uid b/src/scripts/account_servers.gd.uid deleted file mode 100644 index 3be5ba9..0000000 --- a/src/scripts/account_servers.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6heul4elcmbk diff --git a/src/scripts/crypto/aes.gd b/src/scripts/crypto/aes.gd new file mode 100644 index 0000000..61679fd --- /dev/null +++ b/src/scripts/crypto/aes.gd @@ -0,0 +1 @@ +extends Node \ No newline at end of file diff --git a/src/scripts/crypto/aes.gd.uid b/src/scripts/crypto/aes.gd.uid new file mode 100644 index 0000000..71277cb --- /dev/null +++ b/src/scripts/crypto/aes.gd.uid @@ -0,0 +1 @@ +uid://cnr57wo33psve diff --git a/src/scripts/credential_store.gd b/src/scripts/crypto/credential_store.gd similarity index 80% rename from src/scripts/credential_store.gd rename to src/scripts/crypto/credential_store.gd index 977fa46..405bf3e 100644 --- a/src/scripts/credential_store.gd +++ b/src/scripts/crypto/credential_store.gd @@ -2,7 +2,6 @@ # This script is more of a placeholder until a more secure method of storing these credentials becomes available. # Due to security issues revolving around how Godot handles scripts umong other things, any object can be made accessible by anything else. # As a result, there just isn't a safe spot to store this data. - extends Node var info = { @@ -13,4 +12,7 @@ var info = { func set_account_credential(credentials: PackedStringArray = []): info.token = credentials[0] info.expire_time = credentials[1] - GlobalLogger.log_string("Saved JWT to memory.") + GlobalLogger.logs("Saved JWT to memory.") + +# TODO: Save account credentials to disk. +# TODO: Read account credentials from disk. \ No newline at end of file diff --git a/src/scripts/credential_store.gd.uid b/src/scripts/crypto/credential_store.gd.uid similarity index 100% rename from src/scripts/credential_store.gd.uid rename to src/scripts/crypto/credential_store.gd.uid diff --git a/src/scripts/jwt.gd b/src/scripts/crypto/jwt.gd similarity index 58% rename from src/scripts/jwt.gd rename to src/scripts/crypto/jwt.gd index fd78598..2fb531c 100644 --- a/src/scripts/jwt.gd +++ b/src/scripts/crypto/jwt.gd @@ -1,12 +1,32 @@ -# This provides basic JWT features extends Node -func verify(jwt_string: String = "", signature_pem: String = "") -> bool: - # TODO: Error checks +# NOTE: To keep things consistent, please keep the signature always in base64. Only convert it where it will be used. + +func decode(jwt_string: String = "") -> Dictionary: + var return_dict = {"ok": false, "data": {}} + + var jwt_parts = _get_jwt_parts(jwt_string) + + return_dict.data.head = JSON.parse_string(Marshalls.base64_to_utf8(_base64url_to_base64(jwt_parts.head))) + return_dict.data.payload = JSON.parse_string(Marshalls.base64_to_utf8(_base64url_to_base64(jwt_parts.payload))) + # The signature should not be converted from base64 + return_dict.data.signature = _base64url_to_base64(jwt_parts.signature) + return_dict.ok = true + return return_dict + +func verify(jwt_string: String, public_key: CryptoKey) -> bool: var crypto: Crypto = Crypto.new() - var public_key: CryptoKey = _signature_pem_to_cryptokey(signature_pem) var jwt_parts: Dictionary = _get_jwt_parts(jwt_string) + if jwt_parts.ok != true: + GlobalLogger.logs("Failed to deconstruct jwt when verifying jwt.", 2) + GlobalLogger.logs(str(jwt_string), 0) + return false + var formatted_payload: Dictionary = _format_jwt_payload(jwt_parts.head, jwt_parts.payload) + if formatted_payload.ok != true: + GlobalLogger.logs("Failed to format the jwt payload when verifying jwt.", 2) + GlobalLogger.logs(str(jwt_parts), 0) + return false return crypto.verify( HashingContext.HASH_SHA256, @@ -15,19 +35,20 @@ func verify(jwt_string: String = "", signature_pem: String = "") -> bool: public_key ) -func decode_jwt(jwt_string: String) -> Dictionary: +# Private functions +func _format_jwt_payload(head: String, payload: String) -> Dictionary: # TODO: Error checks - var return_dict = {"head": {}, "payload": {}} + var return_dict = {"ok": false, "payload_bytes": []} - var jwt_parts = _get_jwt_parts(jwt_string) + var formatted_payload = head + "." + payload + var payload_bytes = formatted_payload.to_utf8_buffer() - jwt_parts.head = _base64url_to_base64(jwt_parts.head) - jwt_parts.head = Marshalls.base64_to_utf8(jwt_parts.head) - return_dict.head = JSON.parse_string(jwt_parts.head) + var hasher: HashingContext = HashingContext.new() + hasher.start(HashingContext.HASH_SHA256) + hasher.update(payload_bytes) - jwt_parts.payload = _base64url_to_base64(jwt_parts.payload) - jwt_parts.payload = Marshalls.base64_to_utf8(jwt_parts.payload) - return_dict.payload = JSON.parse_string(jwt_parts.payload) + return_dict.payload_bytes = hasher.finish() + return_dict.ok = true return return_dict @@ -43,15 +64,6 @@ func _base64url_to_base64(base64url: String): return fixed -func _signature_pem_to_cryptokey(signature_pem: String = "") -> CryptoKey: - # TODO: Error checks - var public_key := CryptoKey.new() - if public_key.load_from_string(signature_pem, true) != OK: - GlobalLogger.log_string("Failed to load signature", 3) - return null - - return public_key - func _get_jwt_parts(jwt_string: String = "") -> Dictionary: # TODO: Error checks var return_dict = {"ok": false, "head": "", "payload": "", "signature": ""} @@ -59,7 +71,7 @@ func _get_jwt_parts(jwt_string: String = "") -> Dictionary: var jwt_split = jwt_string.split(".") if len(jwt_split) != 3: - GlobalLogger.log_string("JWT token is not formatted correctly.", 2) + GlobalLogger.logs("JWT token is not formatted correctly.", 2) return return_dict return_dict.head = jwt_split[0] @@ -68,19 +80,3 @@ func _get_jwt_parts(jwt_string: String = "") -> Dictionary: return_dict.ok = true return return_dict - -func _format_jwt_payload(head: String, payload: String) -> Dictionary: - # TODO: Error checks - var return_dict = {"ok": false, "payload_bytes": []} - - var formatted_payload = head + "." + payload - var payload_bytes = formatted_payload.to_utf8_buffer() - - var hasher: HashingContext = HashingContext.new() - hasher.start(HashingContext.HASH_SHA256) - hasher.update(payload_bytes) - - return_dict.payload_bytes = hasher.finish() - return_dict.ok = true - - return return_dict \ No newline at end of file diff --git a/src/scripts/crypto/jwt.gd.uid b/src/scripts/crypto/jwt.gd.uid new file mode 100644 index 0000000..7d89294 --- /dev/null +++ b/src/scripts/crypto/jwt.gd.uid @@ -0,0 +1 @@ +uid://buxmy0sbla4sq diff --git a/src/scripts/keys.gd b/src/scripts/crypto/keys.gd similarity index 92% rename from src/scripts/keys.gd rename to src/scripts/crypto/keys.gd index d9d02b2..507bb66 100644 --- a/src/scripts/keys.gd +++ b/src/scripts/crypto/keys.gd @@ -16,13 +16,13 @@ func read_keys_from_disk(username: String) -> PackedStringArray: var keys_exist = FileAccess.file_exists(pubKeyPath) && FileAccess.file_exists(privKeyPath) if keys_exist: - GlobalLogger.log_string("Using saved account key.") + GlobalLogger.logs("Using saved account key.") pubKey = FileAccess.open(pubKeyPath, FileAccess.READ).get_as_text() privKey = FileAccess.open(privKeyPath, FileAccess.READ).get_as_text() return [pubKey, privKey] - GlobalLogger.log_string("No key available. Generating a new one!") + GlobalLogger.logs("No key available. Generating a new one!") _generate_keys() _write_keys_to_disk(username) diff --git a/src/scripts/keys.gd.uid b/src/scripts/crypto/keys.gd.uid similarity index 100% rename from src/scripts/keys.gd.uid rename to src/scripts/crypto/keys.gd.uid diff --git a/src/scripts/crypto/rsa.gd b/src/scripts/crypto/rsa.gd new file mode 100644 index 0000000..d578d5e --- /dev/null +++ b/src/scripts/crypto/rsa.gd @@ -0,0 +1,49 @@ +extends Node + +var jwt = preload("res://scripts/crypto/jwt.gd").new() + +## Generates a RSA keypair at a specific bit length +## @returns Dictionary +func generate_keypair(level: int = 0) -> Dictionary: + # TODO: Error checks + var return_dictionary = {"public": "", "private": ""} + var _target_bits = 0 + + match level: + 0: + _target_bits = 2048 + _: + _target_bits = 4096 + + var crypto = Crypto.new() + var generated_keys = crypto.generate_rsa(_target_bits) + + return_dictionary.private = generated_keys.save_to_string(false) + return_dictionary.public = generated_keys.save_to_string(true) + return return_dictionary + +## Verify a signature with a provided public key +## @returns bool +func verify_jwt_signature(jwt_string: String = "", signature_pem: String = "") -> bool: + var crypto: Crypto = Crypto.new() + var sig: CryptoKey = pem_to_cryptokey(signature_pem) + var jwt_parts: Dictionary = jwt._get_jwt_parts(jwt_string) + var formatted_payload = jwt._format_jwt_payload(jwt_parts.head, jwt_parts.payload) + + return crypto.verify( + HashingContext.HASH_SHA256, + formatted_payload.payload_bytes, + Marshalls.base64_to_raw(jwt_parts.signature), + sig + ) + +## Turns a pem into a CryptoKey +## @returns CryptoKey +func pem_to_cryptokey(pem: String = "") -> CryptoKey: + # TODO: Error checks + var public_key := CryptoKey.new() + if public_key.load_from_string(pem, true) != OK: + GlobalLogger.logs("Failed to load public key", 3) + return null + + return public_key \ No newline at end of file diff --git a/src/scripts/crypto/rsa.gd.uid b/src/scripts/crypto/rsa.gd.uid new file mode 100644 index 0000000..eb71e2c --- /dev/null +++ b/src/scripts/crypto/rsa.gd.uid @@ -0,0 +1 @@ +uid://dkgyyc7tlbvln diff --git a/src/scripts/jwt.gd.uid b/src/scripts/jwt.gd.uid deleted file mode 100644 index 5809512..0000000 --- a/src/scripts/jwt.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bt5gufaix02sa diff --git a/src/scripts/Logger.gd b/src/scripts/logger.gd similarity index 64% rename from src/scripts/Logger.gd rename to src/scripts/logger.gd index 83c12c1..b2ae399 100644 --- a/src/scripts/Logger.gd +++ b/src/scripts/logger.gd @@ -26,14 +26,13 @@ func _ready(): _initialize_log_file() set_console_logging(true) set_file_logging(true) - log_string("Logger initialized") + logs("Logger initialized") func _initialize_log_file(): - if not FileManager.log_file_exists(): - log_file_path = FileManager.create_log_file() - log_file = FileAccess.open(log_file_path, FileAccess.WRITE) - log_file_initialized = true - log_string("Opened log file at %s" % log_file_path, 0) + log_file_path = FileManager.create_log_file() + log_file = FileAccess.open(log_file_path, FileAccess.WRITE) + log_file_initialized = true + logs("Opened log file at %s" % log_file_path, 0) ## Logs a message to both file and console (if enabled). ## @param message: The message string to log. If omitted, defaults to an empty string. @@ -43,31 +42,44 @@ func _initialize_log_file(): ## 2 -> Warning ## 3 -> Error ## Defaults to 0 (Debug). -func log_string(message: String = "", level: int = 0): +func logs(message: String = "", level: int = 0): _log_to_file(message, level) if console_logging_enabled: print_rich("[[color=%s]%s[/color]] %s" % [log_level_colors[level], log_level_names[level], message]) + if level == 3: + # We are skipping the first frame, otherwise this function will be logged. + var stack := get_stack() + + for i in range(1, stack.size()): + var frame = stack[i] + print( + "%s:%d @ %s()" % [ + frame.source, + frame.line, + frame.function + ] + ) pass func _log_to_file(message: String = "", level: int = 0): - if file_logging_enabled: + if file_logging_enabled && log_file: var formatted_log = "[%s] %s" % [log_level_names[level], message] - # log_file.store_line(formatted_log) - # log_file.flush() + log_file.store_line(formatted_log) + log_file.flush() func set_console_logging(enabled: bool): if enabled: console_logging_enabled = true - log_string("Console logging enabled for this session.", 1) + logs("Console logging enabled for this session.", 1) else: console_logging_enabled = false - log_string("Console logging disabled for this session.", 1) + logs("Console logging disabled for this session.", 1) func set_file_logging(enabled: bool): if enabled: file_logging_enabled = true - log_string("File logging enabled for this session.", 1) + logs("File logging enabled for this session.", 1) else: file_logging_enabled = false - log_string("File logging disabled for this session.", 1) + logs("File logging disabled for this session.", 1) diff --git a/src/scripts/Logger.gd.uid b/src/scripts/logger.gd.uid similarity index 100% rename from src/scripts/Logger.gd.uid rename to src/scripts/logger.gd.uid diff --git a/src/scripts/network/account_servers.gd b/src/scripts/network/account_servers.gd new file mode 100644 index 0000000..a0126e7 --- /dev/null +++ b/src/scripts/network/account_servers.gd @@ -0,0 +1,27 @@ +extends Node + +var _http = preload("res://scripts/network/http.gd").new() +var _database = {} + +# TODO: Save public account server keys to disk + +func get_public_key(host: String, port: int) -> String: + if host in _database: + return _database[host] + + var response: Dictionary = await _request_server_pem(host, port) + if response.ok == true: + _database[host] = response.data + return response.data + + return "" + +func _request_server_pem(host: String, port: int = 443) -> Dictionary: + GlobalLogger.logs("Requesting server '%s:%s'." % [host, port]) + var return_dict = {"ok": false, "data": ""} + var key = await _http.req(HTTPClient.METHOD_GET, host, "/public_key", port) + if key.ok == true: + return_dict.data = key.body + return_dict.ok = true + return return_dict + return return_dict \ No newline at end of file diff --git a/src/scripts/network/account_servers.gd.uid b/src/scripts/network/account_servers.gd.uid new file mode 100644 index 0000000..1fbfc67 --- /dev/null +++ b/src/scripts/network/account_servers.gd.uid @@ -0,0 +1 @@ +uid://bpysjoq7n0ytu diff --git a/src/scripts/http.gd b/src/scripts/network/http.gd similarity index 69% rename from src/scripts/http.gd rename to src/scripts/network/http.gd index f36aa2a..c804400 100644 --- a/src/scripts/http.gd +++ b/src/scripts/network/http.gd @@ -5,8 +5,8 @@ signal _completed(result: Dictionary) # TODO: When the http client fails to connect to server, no error appears. func req(method: HTTPClient.Method, host: String, path: String = "/", port: int = 443, headers: PackedStringArray = [], body: String = "") -> Dictionary: - var thread := Thread.new() - var params := { + var thread: Thread = Thread.new() + var params: Dictionary = { "method": method, "host": host, "path": path, @@ -20,17 +20,15 @@ func req(method: HTTPClient.Method, host: String, path: String = "/", port: int return await _completed func _thread_main(params: Dictionary) -> void: - var client := HTTPClient.new() - var result := { - "ok": false - } + var client: HTTPClient = HTTPClient.new() + var return_dict: Dictionary = {"ok": false, "body": ""} - var err := client.connect_to_host(params.host, params.port) + var err: int = client.connect_to_host(params.host, params.port) # Could not connect to host if err != OK: - result.error = "Connection failed" - _finish(params, result) + return_dict.error = "Connection failed" + _finish(params, return_dict) return # Wait for connection @@ -49,23 +47,23 @@ func _thread_main(params: Dictionary) -> void: client.poll() OS.delay_msec(10) - result.status_code = client.get_response_code() - result.response_headers = client.get_response_headers_as_dictionary() + return_dict.status_code = client.get_response_code() + return_dict.response_headers = client.get_response_headers_as_dictionary() - var response_body := "" + var response_body: String = "" while client.get_status() == HTTPClient.STATUS_BODY: client.poll() - var chunk := client.read_response_body_chunk() + var chunk: PackedByteArray = client.read_response_body_chunk() if chunk.size() > 0: response_body += chunk.get_string_from_utf8() OS.delay_msec(10) client.close() - result.ok = true - result.body = response_body + return_dict.ok = true + return_dict.body = response_body - _finish(params, result) + _finish(params, return_dict) func _finish(params: Dictionary, result: Dictionary) -> void: call_deferred("_emit_completed", params.thread, result) diff --git a/src/scripts/http.gd.uid b/src/scripts/network/http.gd.uid similarity index 100% rename from src/scripts/http.gd.uid rename to src/scripts/network/http.gd.uid diff --git a/src/scripts/network_compression.gd b/src/scripts/network/network_compression.gd similarity index 96% rename from src/scripts/network_compression.gd rename to src/scripts/network/network_compression.gd index 0fcb805..8335692 100644 --- a/src/scripts/network_compression.gd +++ b/src/scripts/network/network_compression.gd @@ -18,7 +18,7 @@ func c_32_vec3(provided_data: Vector3) -> PackedByteArray: func d_32_vec3(provided_data: PackedByteArray) -> Vector3: # Validate array size if provided_data.size() < 12: - GlobalLogger.log_string("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) return Vector3() var x = _int_to_float(provided_data.decode_s32(0)) @@ -43,7 +43,7 @@ func c_16_vec3(provided_data: Vector3) -> PackedByteArray: func d_16_vec3(provided_data: PackedByteArray) -> Vector3: # Validate array size if provided_data.size() < 6: - GlobalLogger.log_string("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) return Vector3() var x = _int_to_float(provided_data.decode_s16(0)) diff --git a/src/scripts/network_compression.gd.uid b/src/scripts/network/network_compression.gd.uid similarity index 100% rename from src/scripts/network_compression.gd.uid rename to src/scripts/network/network_compression.gd.uid diff --git a/src/scripts/rpc/client.gd b/src/scripts/rpc/client.gd new file mode 100644 index 0000000..15490b3 --- /dev/null +++ b/src/scripts/rpc/client.gd @@ -0,0 +1,29 @@ +extends Node + +# Join server +# Leave server +# Kicked from server +# Banned from server + +@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") +@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") + +func connected_to_server(): + return + +func connection_failed(): + return + +@rpc("authority", "reliable") +func on_receive_server_info(info): + GlobalLogger.logs("Got server info!") + if info.level: + await scene_manager.load_multiplayer_scene(info.level, info.level_node_name) + get_parent().s.rpc_id(1, "on_receive_player_info", CredentialStore.info.token) + return + +@rpc("authority", "reliable") +func received_server_session_info(received_info: Dictionary) -> void: + GlobalLogger.logs("Session information updated.") + network_manager.info = received_info + return \ No newline at end of file diff --git a/src/scripts/rpc/client.gd.uid b/src/scripts/rpc/client.gd.uid new file mode 100644 index 0000000..44f228a --- /dev/null +++ b/src/scripts/rpc/client.gd.uid @@ -0,0 +1 @@ +uid://dsbv3frxchi71 diff --git a/src/scripts/rpc/common.gd b/src/scripts/rpc/common.gd new file mode 100644 index 0000000..4eb90cc --- /dev/null +++ b/src/scripts/rpc/common.gd @@ -0,0 +1,39 @@ +extends Node + +var n_c = preload("res://scripts/network/network_compression.gd").new() +@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") + +@rpc("any_peer", "unreliable") +func on_player_transform(info): + # TODO: Authenticate + var target_node = network_manager.player_exists(str(multiplayer.get_remote_sender_id())) + + if target_node == null: + return + + # HACK: The rotation data is hacked on here. This needs to be addressed at some point. + target_node.position = n_c.d_16_pos(info) + target_node.rotation = n_c.d_16_vec3(info.slice(12)) + return + + +@rpc("any_peer", "unreliable") +func on_node_transform() -> void: + # Handles changing positions of a node. + # This should only be used when a node is moving, and not to position a node on spawn. + # TODO: Authenticate + return + +@rpc("authority", "reliable") +func on_spawn_player(id) -> void: + var player_scene: PackedScene = load("res://scenes/players/player.tscn") + GlobalLogger.logs("Spawning player %s" % id) + var new_player = player_scene.instantiate() + new_player.name = str(id) + new_player.position = Vector3(0, 0, 0) + network_manager.spawn_player(new_player) + return + +@rpc("authority", "reliable") +func on_spawn_node() -> void: + return diff --git a/src/scripts/rpc/common.gd.uid b/src/scripts/rpc/common.gd.uid new file mode 100644 index 0000000..896d644 --- /dev/null +++ b/src/scripts/rpc/common.gd.uid @@ -0,0 +1 @@ +uid://8cnojblfb8ly diff --git a/src/scripts/rpc/server.gd b/src/scripts/rpc/server.gd new file mode 100644 index 0000000..1b90f06 --- /dev/null +++ b/src/scripts/rpc/server.gd @@ -0,0 +1,90 @@ +extends Node + +var n_c = preload("res://scripts/network/network_compression.gd").new() +var jwt = preload("res://scripts/crypto/jwt.gd").new() +var rsa = preload("res://scripts/crypto/rsa.gd").new() +var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") + +@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") + +# Create server +# Update server +# Close server + +# On player connecting +# On player connected +# On player leaving +# On player kicked +# On player banned + +func on_peer_connected(peer_id): + if multiplayer.is_server() == false: + return + + GlobalLogger.logs("[%s] Peer connected: '%s'. Sending server info." % [multiplayer.get_unique_id(), peer_id]) + get_parent().c.rpc_id(peer_id, "on_receive_server_info", network_manager.info) + +func on_peer_disconnected(): + return + +@rpc("any_peer", "reliable") +func on_receive_player_info(info) -> void: + if multiplayer.is_server() == false: + return + var sender_id = multiplayer.get_remote_sender_id() + GlobalLogger.logs("Got client info!") + var player_info = jwt.decode(info) + + if player_info.ok != true: + GlobalLogger.logs("Unknown error decoding player JWT.", 3) + + player_info = player_info.data + var url_parts = _parse_url(player_info.payload.issuer) + var host_pub_key = await AccountServers._request_server_pem(url_parts.host, url_parts.port) + + if host_pub_key.ok != true: + GlobalLogger.logs("Unknown error retrieving account server Public PEM.", 3) + + host_pub_key = host_pub_key.data + var jwt_is_valid = rsa.verify_jwt_signature(info, host_pub_key) + + if jwt_is_valid == false: + GlobalLogger.logs("JWT signature did not match.", 1) + multiplayer.multiplayer_peer.disconnect_peer(sender_id) + # TODO: Send a message before kicking the user. + return + + player_info.payload["multiplayer_id"] = sender_id + + network_manager.info.clients.append(player_info.payload) + + get_parent().com.on_spawn_player(sender_id) + get_parent().com.rpc("on_spawn_player", sender_id) + + for client in network_manager.info.clients: + if client.multiplayer_id == sender_id: + continue + get_parent().com.rpc_id(sender_id, "on_spawn_player", client.multiplayer_id) + + send_server_info() + +func send_server_info(): + get_parent().c.rpc("received_server_session_info", network_manager.info) + return + +func _parse_url(url: String) -> Dictionary: + var result = { + "scheme": "", + "host": "", + "port": 0, + "path": "" + } + + var matches = url_regex.search(url) + if matches: + result["scheme"] = matches.get_string(1).to_lower() + result["host"] = matches.get_string(2) + result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) + result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" + + return result diff --git a/src/scripts/rpc/server.gd.uid b/src/scripts/rpc/server.gd.uid new file mode 100644 index 0000000..4976943 --- /dev/null +++ b/src/scripts/rpc/server.gd.uid @@ -0,0 +1 @@ +uid://c6wrk7xh520cr diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd new file mode 100644 index 0000000..be14e5e --- /dev/null +++ b/src/scripts/utils/files.gd @@ -0,0 +1,65 @@ +extends Node + +## Create a config file at a given relative directory. +## Example: /system/cool.json +## @returns void +func create_config_file(dir: String) -> void: + GlobalLogger.logs("Creating '%s'" % dir, 0) + _maybe_make_directory("user://config/") + # TODO: Sanataize param + # TODO: Error checks + var dir_access = DirAccess.open("user://config/") + dir_access.make_dir_recursive("user://config/%s" % dir) + return + +## Read a config file from a directory. +## Example: /system/cool.json +## @returns String +func read_config_file(dir: String) -> String: + # TODO: Error checks + GlobalLogger.logs("Reading '%s'" % dir, 0) + dir = "user://config/%s" % dir + var file_contents = FileAccess.open(dir, FileAccess.READ).get_as_text() + return file_contents + +## Create a config file at a given relative directory. +## Example: /system/cool.json +## @returns String +func create_log_file() -> String: + GlobalLogger.logs("Creating a log file for this session.", 0) + _maybe_make_directory("user://logs/") + # TODO: Sanataize param + # TODO: Error checks + # TODO: Try to create a different file if one already exists with that name. + var log_file_name = _get_today_log_file_name() + var log_file_path = "user://logs/%s.%s" % [ProjectSettings.get_setting("application/config/name"), log_file_name] + var file = FileAccess.open(log_file_path, FileAccess.WRITE) + file.close() + GlobalLogger.logs("Log file '%s' created." % log_file_path, 0) + return log_file_path + +func create_client_file(_dir: String) -> void: + # Create a file to store in-game user data. This is for in-game data storage! + # IMPORTANT: DO NOT STORE PRIVATE DATA IN THIS DIRECTORY AS IT IS INTENDED TO BE READ AND WRITTEN TO FREELY! + GlobalLogger.logs("Not implemented.", 3) + return + +func _maybe_make_directory(dir: String): + var dir_access = DirAccess.open("user://") + dir_access.make_dir_recursive(dir) + +func _parse_log_file_name(file_name: String) -> Dictionary: + var date = file_name.split(".")[1].split("-") + var year = date[0].split("_")[0] + var month = date[0].split("_")[1] + var day = date[0].split("_")[2] + var hour = date[1].split("_")[0] + var minute = date[1].split("_")[1] + var second = date[1].split("_")[2] + var time_dictionary = Time.get_datetime_dict_from_datetime_string("%s-%s-%sT%s:%s:%s" % [year, month, day, hour, minute, second], true) + return time_dictionary + +func _get_today_log_file_name() -> String: + var current_timestring = Time.get_datetime_string_from_system() + var file_name = current_timestring.replace("-", "_").replace("T", "-").replace(":", "_") + return file_name diff --git a/src/scripts/utils/files.gd.uid b/src/scripts/utils/files.gd.uid new file mode 100644 index 0000000..805a1b2 --- /dev/null +++ b/src/scripts/utils/files.gd.uid @@ -0,0 +1 @@ +uid://d2s50p717g3n diff --git a/src/scripts/LaunchArguments.gd b/src/scripts/utils/launch_arguments.gd similarity index 91% rename from src/scripts/LaunchArguments.gd rename to src/scripts/utils/launch_arguments.gd index 4771c4e..05235d3 100644 --- a/src/scripts/LaunchArguments.gd +++ b/src/scripts/utils/launch_arguments.gd @@ -12,3 +12,6 @@ func get_command_line_args() -> Dictionary: # with the value set to an empty string. arguments[argument.trim_prefix("--")] = "" return arguments + +func _ready(): + get_command_line_args() \ No newline at end of file diff --git a/src/scripts/utils/launch_arguments.gd.uid b/src/scripts/utils/launch_arguments.gd.uid new file mode 100644 index 0000000..4dda50b --- /dev/null +++ b/src/scripts/utils/launch_arguments.gd.uid @@ -0,0 +1 @@ +uid://c45jrfmrjtnyn diff --git a/src/scripts/Util.gd b/src/scripts/utils/random.gd similarity index 94% rename from src/scripts/Util.gd rename to src/scripts/utils/random.gd index 02868bb..8d2064c 100644 --- a/src/scripts/Util.gd +++ b/src/scripts/utils/random.gd @@ -7,4 +7,4 @@ func random_string(length: int = 6): var out = "" for i in length: out += chars[rng.randi_range(0, chars.length() - 1)] - return out \ No newline at end of file + return out diff --git a/src/scripts/utils/random.gd.uid b/src/scripts/utils/random.gd.uid new file mode 100644 index 0000000..72994f6 --- /dev/null +++ b/src/scripts/utils/random.gd.uid @@ -0,0 +1 @@ +uid://1js68qt8w0mv diff --git a/src/userinterface/hud.gd b/src/userinterface/hud.gd index b9f5b0a..d159931 100644 --- a/src/userinterface/hud.gd +++ b/src/userinterface/hud.gd @@ -1,8 +1,8 @@ extends Control @onready var network_manager = get_tree().current_scene.get_node("NetworkManager") -var http = preload("res://scripts/http.gd").new() -var keys = preload("res://scripts/keys.gd").new() +var http = preload("res://scripts/network/http.gd").new() +var keys = preload("res://scripts/crypto/keys.gd").new() func _ready(): while true: @@ -16,9 +16,9 @@ func set_active_state(state: bool = false): visible = state func _update_hud_state(): - var user_list_formatted = network_manager.info.clients.map(func(elem): return elem.display_name) + var user_list_formatted = network_manager.info.clients.map(func(elem): return elem.username) %HostingBool.text = "Host: %s" % network_manager.status.hosting - %SessionHost.text = "Server Host: %s" % network_manager.info.clients[0].display_name + %SessionHost.text = "Server Host: %s" % network_manager.info.clients[0].username %ClientBool.text = "Client: %s" % network_manager.status.client %ConnectedUserCount.text = "Total Users: %s" % len(network_manager.info.clients) %UserList.text = "User List: %s" % ", ".join(user_list_formatted) @@ -44,7 +44,7 @@ func _on_nav_home_pressed(): func _show_dashboard_page(page_name: String = "Home"): if page_name not in ["Home", "Sessions", "Contacts", "Inventory", "Debug", "Exit"]: - GlobalLogger.log_string("Tried to switch to an invalid dashboard page: '%s'" % page_name) + GlobalLogger.logs("Tried to switch to an invalid dashboard page: '%s'" % page_name) return for page in get_node("MarginContainer/VBoxContainer/PrimaryDashboard/").get_children(): @@ -65,20 +65,20 @@ func _on_login_button_pressed(): var response = await http.req(HTTPClient.Method.METHOD_POST, "http://localhost", "/api/v1/device/auth", 40400, ["Accept: application/json", "Content-Type: application/json"], JSON.stringify(data)) if response["ok"] == false: - GlobalLogger.log_string("Response failed for unknown reason.", 1) + GlobalLogger.logs("Response failed for unknown reason.", 1) return if response["body"] == null: - GlobalLogger.log_string("No body provided for login request.", 3) + GlobalLogger.logs("No body provided for login request.", 3) return var res_body = JSON.parse_string(response["body"]) if "error" in res_body.keys(): - GlobalLogger.log_string("Login request returned an error. '%s'" % res_body["error"], 1) + GlobalLogger.logs("Login request returned an error. '%s'" % res_body["error"], 1) return var token = response["response_headers"]["Set-Cookie"].split("; ") token[0] = token[0].replace("token=", "") CredentialStore.set_account_credential(token) - _show_dashboard_page("Home") \ No newline at end of file + _show_dashboard_page("Home") From 2b8e9a36bd270b672deb3893625f0856e4f30ac0 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Tue, 31 Mar 2026 20:29:43 -0500 Subject: [PATCH 10/19] User dashboard + account handling. * Basic handling for multiple accounts. Improve security by utilizing two unique keys. One for Client to AS, the other for Client to Client/Server. * Refactored signin.gd. * Properly send public key to identity server. * Multi Account support. * Base work for libraries. * Remove dead code. * Account authentication status. * Passport handling. Changes how passports are obtained. Select an account to use. Remove dead code. * Dashboard current account selection. * Created placeholder jwt lib file. * Changed how responses are verified. Fixed passport sending to server. Fixed timing issue requesting passport from account server. * Change the dashboard to something more presentable. (#64) * New dashboard work. * Changed sessions icon. Added instance option to manage the current instance. * Disable dashboard buttons when page is not present. * Storage display. * Add account list page. Improved display_dashboard... function. * Created placeholder theme for panels. * Basic account list haneling. * Better create account functionality. * Account login. * Replaced original dashboard. Scroll to bottom of messages when contacts are open. * Use OAuth authentication for the game client. (#67) * Initial work. Lots of hard coded values, but this is functional ground work for OAuth support from the game client. * Generate a random key for the pkce. * Made the auth code non-global. * Refresh token and access token. * Added TODOs. * Added file header. psioniq File Header extension config. * Reimplemented JWT library. * Set oauth variables. * Removed hardcoded authentication server values. * Use existing random_string in random library. * Base64 to Base64uri function in jwt library. * Token introspection and refreshing. * Placeholder TCPResponse to successful token capture. The browser will no longer hang continuiously. * Cleanup. * Added multiple login options. This changes the login flow to have the user specify which login they are intending on using. * Created url library. This will centralize url parsing making it easier to handle urls. * Removed passport handling. This will likely be reimplemented later. * Improved create account flow. * Disabled usernamepassword login. * Use URL Parser library. * Removed unused AES file. Right now I do not need this here. * Removed duplicate library. Removed unused references. * Stop checking OAuth server after successful connection. * Change the dashboard handling to use a singal bus. (#78) * Dev upload public key to the server. I need to make some changes to the UIX so I am making a checkpoint here. * Change structure of hut to use signalbus. * OAuth flow. * Go back from account creation page. * Refresh account list on change. * Move to account list after creating a local account. * Check to see if account is valid before logging in. * Remove unused account data display. * Disable navigation to unknown pages. * Exit page. * Removed old dash files. --- .gitignore | 3 +- .vscode/settings.json | 53 + src/openminerva_darkpanel.tres | 4 + src/openminerva_default.tres | 19 + src/project.godot | 15 +- src/resources/icons/account.svg.import | 2 +- src/resources/icons/apps.svg | 1 + src/resources/icons/apps.svg.import | 43 + src/resources/icons/art.svg | 1 + src/resources/icons/art.svg.import | 43 + src/resources/icons/circus.svg | 1 + src/resources/icons/circus.svg.import | 43 + src/resources/icons/contacts.svg | 1 + src/resources/icons/contacts.svg.import | 43 + src/resources/icons/debug.svg | 1 + src/resources/icons/debug.svg.import | 43 + src/resources/icons/dummy16-9.webp | Bin 0 -> 6394 bytes src/resources/icons/dummy16-9.webp.import | 40 + src/resources/icons/edit.svg | 1 + src/resources/icons/edit.svg.import | 43 + src/resources/icons/environment.svg | 1 + src/resources/icons/environment.svg.import | 43 + src/resources/icons/exit.svg | 1 + src/resources/icons/exit.svg.import | 43 + src/resources/icons/flowchart.svg | 1 + src/resources/icons/flowchart.svg.import | 43 + src/resources/icons/home.svg | 37 + src/resources/icons/home.svg.import | 43 + src/resources/icons/inventory.svg | 1 + src/resources/icons/inventory.svg.import | 43 + src/resources/icons/science.svg | 1 + src/resources/icons/science.svg.import | 43 + src/resources/icons/search.svg | 1 + src/resources/icons/search.svg.import | 43 + src/resources/icons/send.svg | 1 + src/resources/icons/send.svg.import | 43 + src/resources/icons/settings.svg | 1 + src/resources/icons/settings.svg.import | 43 + src/resources/icons/subtitles.svg | 1 + src/resources/icons/subtitles.svg.import | 43 + src/resources/icons/world.svg | 1 + src/resources/icons/world.svg.import | 43 + src/resources/shaders/grid_shader.gdshader | 49 + .../shaders/grid_shader.gdshader.uid | 1 + src/scenes/levels/home.tscn | 69 +- src/scenes/managers/app/network_manager.gd | 10 +- src/scenes/managers/app/scene_manager.gd | 2 +- src/scenes/master.tscn | 4 +- src/scenes/players/player.gd | 5 +- src/scenes/players/player.tscn | 24 +- src/scripts/crypto/aes.gd | 1 - src/scripts/crypto/aes.gd.uid | 1 - src/scripts/crypto/credential_store.gd | 18 - src/scripts/crypto/credential_store.gd.uid | 1 - src/scripts/crypto/jwt.gd | 82 - src/scripts/crypto/jwt.gd.uid | 1 - src/scripts/crypto/rsa.gd | 26 +- src/scripts/libs/account.gd | 220 ++ src/scripts/libs/account.gd.uid | 1 + src/scripts/libs/account_server.gd | 20 + src/scripts/libs/account_server.gd.uid | 1 + src/scripts/libs/jwt.gd | 112 + src/scripts/libs/jwt.gd.uid | 1 + src/scripts/libs/oauth.gd | 148 + src/scripts/libs/oauth.gd.uid | 1 + src/scripts/libs/time.gd | 32 + src/scripts/libs/time.gd.uid | 1 + src/scripts/rpc/client.gd | 2 +- src/scripts/rpc/server.gd | 11 +- src/scripts/signal_bus.gd | 20 + src/scripts/signal_bus.gd.uid | 1 + src/scripts/utils/files.gd | 26 +- src/scripts/utils/random.gd | 4 +- src/userinterface/dash/account_create.gd | 60 + src/userinterface/dash/account_create.gd.uid | 1 + src/userinterface/dash/account_list.gd | 69 + src/userinterface/dash/account_list.gd.uid | 1 + src/userinterface/dash/apps.gd | 10 + src/userinterface/dash/apps.gd.uid | 1 + src/userinterface/dash/contacts.gd | 10 + src/userinterface/dash/contacts.gd.uid | 1 + src/userinterface/dash/debug.gd | 10 + src/userinterface/dash/debug.gd.uid | 1 + src/userinterface/dash/exit.gd | 29 + src/userinterface/dash/exit.gd.uid | 1 + src/userinterface/dash/home.gd | 37 + src/userinterface/dash/home.gd.uid | 1 + src/userinterface/dash/hud.tscn | 2851 +++++++++++++++++ src/userinterface/dash/instance.gd | 10 + src/userinterface/dash/instance.gd.uid | 1 + src/userinterface/dash/inventory.gd | 10 + src/userinterface/dash/inventory.gd.uid | 1 + src/userinterface/dash/master.gd | 59 + src/userinterface/dash/master.gd.uid | 1 + src/userinterface/dash/sessions.gd | 10 + src/userinterface/dash/sessions.gd.uid | 1 + src/userinterface/dash/settings.gd | 10 + src/userinterface/dash/settings.gd.uid | 1 + src/userinterface/hud.gd | 84 - src/userinterface/hud.gd.uid | 1 - src/userinterface/hud.tscn | 459 --- 101 files changed, 4778 insertions(+), 768 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/openminerva_darkpanel.tres create mode 100644 src/openminerva_default.tres create mode 100644 src/resources/icons/apps.svg create mode 100644 src/resources/icons/apps.svg.import create mode 100644 src/resources/icons/art.svg create mode 100644 src/resources/icons/art.svg.import create mode 100644 src/resources/icons/circus.svg create mode 100644 src/resources/icons/circus.svg.import create mode 100644 src/resources/icons/contacts.svg create mode 100644 src/resources/icons/contacts.svg.import create mode 100644 src/resources/icons/debug.svg create mode 100644 src/resources/icons/debug.svg.import create mode 100644 src/resources/icons/dummy16-9.webp create mode 100644 src/resources/icons/dummy16-9.webp.import create mode 100644 src/resources/icons/edit.svg create mode 100644 src/resources/icons/edit.svg.import create mode 100644 src/resources/icons/environment.svg create mode 100644 src/resources/icons/environment.svg.import create mode 100644 src/resources/icons/exit.svg create mode 100644 src/resources/icons/exit.svg.import create mode 100644 src/resources/icons/flowchart.svg create mode 100644 src/resources/icons/flowchart.svg.import create mode 100644 src/resources/icons/home.svg create mode 100644 src/resources/icons/home.svg.import create mode 100644 src/resources/icons/inventory.svg create mode 100644 src/resources/icons/inventory.svg.import create mode 100644 src/resources/icons/science.svg create mode 100644 src/resources/icons/science.svg.import create mode 100644 src/resources/icons/search.svg create mode 100644 src/resources/icons/search.svg.import create mode 100644 src/resources/icons/send.svg create mode 100644 src/resources/icons/send.svg.import create mode 100644 src/resources/icons/settings.svg create mode 100644 src/resources/icons/settings.svg.import create mode 100644 src/resources/icons/subtitles.svg create mode 100644 src/resources/icons/subtitles.svg.import create mode 100644 src/resources/icons/world.svg create mode 100644 src/resources/icons/world.svg.import create mode 100644 src/resources/shaders/grid_shader.gdshader create mode 100644 src/resources/shaders/grid_shader.gdshader.uid delete mode 100644 src/scripts/crypto/aes.gd delete mode 100644 src/scripts/crypto/aes.gd.uid delete mode 100644 src/scripts/crypto/credential_store.gd delete mode 100644 src/scripts/crypto/credential_store.gd.uid delete mode 100644 src/scripts/crypto/jwt.gd delete mode 100644 src/scripts/crypto/jwt.gd.uid create mode 100644 src/scripts/libs/account.gd create mode 100644 src/scripts/libs/account.gd.uid create mode 100644 src/scripts/libs/account_server.gd create mode 100644 src/scripts/libs/account_server.gd.uid create mode 100644 src/scripts/libs/jwt.gd create mode 100644 src/scripts/libs/jwt.gd.uid create mode 100644 src/scripts/libs/oauth.gd create mode 100644 src/scripts/libs/oauth.gd.uid create mode 100644 src/scripts/libs/time.gd create mode 100644 src/scripts/libs/time.gd.uid create mode 100644 src/scripts/signal_bus.gd create mode 100644 src/scripts/signal_bus.gd.uid create mode 100644 src/userinterface/dash/account_create.gd create mode 100644 src/userinterface/dash/account_create.gd.uid create mode 100644 src/userinterface/dash/account_list.gd create mode 100644 src/userinterface/dash/account_list.gd.uid create mode 100644 src/userinterface/dash/apps.gd create mode 100644 src/userinterface/dash/apps.gd.uid create mode 100644 src/userinterface/dash/contacts.gd create mode 100644 src/userinterface/dash/contacts.gd.uid create mode 100644 src/userinterface/dash/debug.gd create mode 100644 src/userinterface/dash/debug.gd.uid create mode 100644 src/userinterface/dash/exit.gd create mode 100644 src/userinterface/dash/exit.gd.uid create mode 100644 src/userinterface/dash/home.gd create mode 100644 src/userinterface/dash/home.gd.uid create mode 100644 src/userinterface/dash/hud.tscn create mode 100644 src/userinterface/dash/instance.gd create mode 100644 src/userinterface/dash/instance.gd.uid create mode 100644 src/userinterface/dash/inventory.gd create mode 100644 src/userinterface/dash/inventory.gd.uid create mode 100644 src/userinterface/dash/master.gd create mode 100644 src/userinterface/dash/master.gd.uid create mode 100644 src/userinterface/dash/sessions.gd create mode 100644 src/userinterface/dash/sessions.gd.uid create mode 100644 src/userinterface/dash/settings.gd create mode 100644 src/userinterface/dash/settings.gd.uid delete mode 100644 src/userinterface/hud.gd delete mode 100644 src/userinterface/hud.gd.uid delete mode 100644 src/userinterface/hud.tscn diff --git a/.gitignore b/.gitignore index bfad5d6..26c0f41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /src/.godot -/src/android/ \ No newline at end of file +/src/android/ +/src/addons \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..40c3f0c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,53 @@ +{ + "psi-header.config": { + "forceToTop": true, + "blankLinesAfter": 1, + "spacesBetweenYears": false, + "license": "MIT", + "company": "OpenMinerva", + "creationDateZero": "asIs", + }, + "psi-header.lang-config": [ + { + "language": "gdscript", + "begin": "# --- License", + "prefix": "# ", + "lineLength": 80, + "end": "# --- License", + "forceToTop": true, + "blankLinesAfter": 1, + "beforeHeader": [], + "afterHeader": [], + "rootDirFileName": "client", + "modAuthor": "Modified By:", + "modDate": "Last Modified:", + "modDateFormat": "dd/MM/yyyy hh:nn:ss", + "replace": [ + "Filename:", + "Project" + ], + "ignoreLines": [] + } + ], + "psi-header.templates": [ + { + "language": "gdscript", + "template": [ + "File: <>", + "Project: <>", + "Created Date: <>", + "Copyright (c) <> <>", + "License: <>", + "Authors: <>", + ], + "changeLogCaption": "HISTORY", + "changeLogHeaderLineCount": 2, + "changeLogEntryTemplate": [ + "", + "<>\t<>\t" + ], + "changeLogNaturalOrder": false, + "changeLogFooterLineCount": 0 + } + ], +} \ No newline at end of file diff --git a/src/openminerva_darkpanel.tres b/src/openminerva_darkpanel.tres new file mode 100644 index 0000000..a6890af --- /dev/null +++ b/src/openminerva_darkpanel.tres @@ -0,0 +1,4 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://cxx1q037xaswi"] + +[resource] +bg_color = Color(0.0625, 0.0625, 0.0625, 1) diff --git a/src/openminerva_default.tres b/src/openminerva_default.tres new file mode 100644 index 0000000..4a9a156 --- /dev/null +++ b/src/openminerva_default.tres @@ -0,0 +1,19 @@ +[gd_resource type="Theme" format=3 uid="uid://bg2nbganyysst"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_31r6k"] +bg_color = Color(0, 0, 0, 0.6) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f2aq3"] +bg_color = Color(0.14117648, 0.39215687, 0.5686275, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fbptr"] +bg_color = Color(0.02, 0.02, 0.02, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fq2hb"] +bg_color = Color(0.01773237, 0.0177324, 0.017732374, 0.8) + +[resource] +Button/styles/normal = SubResource("StyleBoxFlat_31r6k") +Button/styles/pressed = SubResource("StyleBoxFlat_f2aq3") +Panel/styles/normal = SubResource("StyleBoxFlat_fbptr") +Panel/styles/panel = SubResource("StyleBoxFlat_fq2hb") diff --git a/src/project.godot b/src/project.godot index b3ab4a7..0823541 100644 --- a/src/project.godot +++ b/src/project.godot @@ -28,13 +28,22 @@ LaunchArguments="*uid://c45jrfmrjtnyn" GlobalLogger="*uid://dgmfafi41y1nk" FileManager="*uid://d2s50p717g3n" AccountServers="*uid://bpysjoq7n0ytu" -CredentialStore="*uid://cs4c0ctis2flp" Random="*uid://1js68qt8w0mv" +GlobalAccount="*uid://dtlb70kxvbtvn" +Events="*uid://c656spc3ppdlw" +UrlParser="*uid://budprjmmpally" +OAuth="*uid://jd7qlsley1no" [display] window/size/viewport_width=1920 window/size/viewport_height=1080 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/openminerva.urlparser/plugin.cfg") [input] @@ -78,3 +87,7 @@ sprint={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } + +[rendering] + +textures/default_filters/use_nearest_mipmap_filter=true diff --git a/src/resources/icons/account.svg.import b/src/resources/icons/account.svg.import index 35ce56d..2c4211b 100644 --- a/src/resources/icons/account.svg.import +++ b/src/resources/icons/account.svg.import @@ -38,6 +38,6 @@ process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 detect_3d/compress_to=1 -svg/scale=1.0 +svg/scale=5.0 editor/scale_with_editor_scale=false editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/apps.svg b/src/resources/icons/apps.svg new file mode 100644 index 0000000..60a8cc2 --- /dev/null +++ b/src/resources/icons/apps.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/apps.svg.import b/src/resources/icons/apps.svg.import new file mode 100644 index 0000000..41f2d7d --- /dev/null +++ b/src/resources/icons/apps.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dp5u16rwxd0bp" +path="res://.godot/imported/apps.svg-062353f68bab189f9e392acee8f22033.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/apps.svg" +dest_files=["res://.godot/imported/apps.svg-062353f68bab189f9e392acee8f22033.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/art.svg b/src/resources/icons/art.svg new file mode 100644 index 0000000..247db39 --- /dev/null +++ b/src/resources/icons/art.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/art.svg.import b/src/resources/icons/art.svg.import new file mode 100644 index 0000000..bbecb1d --- /dev/null +++ b/src/resources/icons/art.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://crrjk7c2g4q55" +path="res://.godot/imported/art.svg-c5ead3145c8db393dce7fc50a2521349.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/art.svg" +dest_files=["res://.godot/imported/art.svg-c5ead3145c8db393dce7fc50a2521349.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/circus.svg b/src/resources/icons/circus.svg new file mode 100644 index 0000000..575b677 --- /dev/null +++ b/src/resources/icons/circus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/circus.svg.import b/src/resources/icons/circus.svg.import new file mode 100644 index 0000000..9296b75 --- /dev/null +++ b/src/resources/icons/circus.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b0qo67fgifqe8" +path="res://.godot/imported/circus.svg-055ea0232929f7da2d9389531cde2519.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/circus.svg" +dest_files=["res://.godot/imported/circus.svg-055ea0232929f7da2d9389531cde2519.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/contacts.svg b/src/resources/icons/contacts.svg new file mode 100644 index 0000000..ee85868 --- /dev/null +++ b/src/resources/icons/contacts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/contacts.svg.import b/src/resources/icons/contacts.svg.import new file mode 100644 index 0000000..c212229 --- /dev/null +++ b/src/resources/icons/contacts.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dfckge3u00boi" +path="res://.godot/imported/contacts.svg-cfa1710a5ebd0d6d19edfd359b5c2f4e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/contacts.svg" +dest_files=["res://.godot/imported/contacts.svg-cfa1710a5ebd0d6d19edfd359b5c2f4e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/debug.svg b/src/resources/icons/debug.svg new file mode 100644 index 0000000..cd86722 --- /dev/null +++ b/src/resources/icons/debug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/debug.svg.import b/src/resources/icons/debug.svg.import new file mode 100644 index 0000000..48d27b8 --- /dev/null +++ b/src/resources/icons/debug.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8fed1h3aqw1s" +path="res://.godot/imported/debug.svg-73aced604934a13740a0d50d04426de7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/debug.svg" +dest_files=["res://.godot/imported/debug.svg-73aced604934a13740a0d50d04426de7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/dummy16-9.webp b/src/resources/icons/dummy16-9.webp new file mode 100644 index 0000000000000000000000000000000000000000..b50f87f5c8eb7b98d3d34a7179e8687d7905dc3b GIT binary patch literal 6394 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 \ No newline at end of file diff --git a/src/resources/icons/edit.svg.import b/src/resources/icons/edit.svg.import new file mode 100644 index 0000000..46a84a5 --- /dev/null +++ b/src/resources/icons/edit.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckoajckje5mq2" +path="res://.godot/imported/edit.svg-eca169d9395b96f7bd6a43743745a869.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/edit.svg" +dest_files=["res://.godot/imported/edit.svg-eca169d9395b96f7bd6a43743745a869.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/environment.svg b/src/resources/icons/environment.svg new file mode 100644 index 0000000..55b530c --- /dev/null +++ b/src/resources/icons/environment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/environment.svg.import b/src/resources/icons/environment.svg.import new file mode 100644 index 0000000..0044528 --- /dev/null +++ b/src/resources/icons/environment.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cnhy47i7saehq" +path="res://.godot/imported/environment.svg-e5081c4d8b4cb57b48121ddccbf78859.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/environment.svg" +dest_files=["res://.godot/imported/environment.svg-e5081c4d8b4cb57b48121ddccbf78859.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/exit.svg b/src/resources/icons/exit.svg new file mode 100644 index 0000000..c06c85c --- /dev/null +++ b/src/resources/icons/exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/exit.svg.import b/src/resources/icons/exit.svg.import new file mode 100644 index 0000000..6686168 --- /dev/null +++ b/src/resources/icons/exit.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://8fwxg1ipf0fp" +path="res://.godot/imported/exit.svg-b4526ff8097e7b69e7a3907e51f5cfd2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/exit.svg" +dest_files=["res://.godot/imported/exit.svg-b4526ff8097e7b69e7a3907e51f5cfd2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=8 +process/channel_remap/blue=8 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/flowchart.svg b/src/resources/icons/flowchart.svg new file mode 100644 index 0000000..933c7e5 --- /dev/null +++ b/src/resources/icons/flowchart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/flowchart.svg.import b/src/resources/icons/flowchart.svg.import new file mode 100644 index 0000000..46b7a33 --- /dev/null +++ b/src/resources/icons/flowchart.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddchrocw45muh" +path="res://.godot/imported/flowchart.svg-1dfadc4d5729c8ef52a1ba1899b04c1f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/flowchart.svg" +dest_files=["res://.godot/imported/flowchart.svg-1dfadc4d5729c8ef52a1ba1899b04c1f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/home.svg b/src/resources/icons/home.svg new file mode 100644 index 0000000..47c1005 --- /dev/null +++ b/src/resources/icons/home.svg @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/src/resources/icons/home.svg.import b/src/resources/icons/home.svg.import new file mode 100644 index 0000000..01010db --- /dev/null +++ b/src/resources/icons/home.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://7mgxvhy58nhp" +path="res://.godot/imported/home.svg-2b476043e26df33e5e1ff306597ac95d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/home.svg" +dest_files=["res://.godot/imported/home.svg-2b476043e26df33e5e1ff306597ac95d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/inventory.svg b/src/resources/icons/inventory.svg new file mode 100644 index 0000000..360351c --- /dev/null +++ b/src/resources/icons/inventory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/inventory.svg.import b/src/resources/icons/inventory.svg.import new file mode 100644 index 0000000..9f5a813 --- /dev/null +++ b/src/resources/icons/inventory.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://br6hpysqvuhek" +path="res://.godot/imported/inventory.svg-4df2a15583a1909dfe17d480c9308b85.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/inventory.svg" +dest_files=["res://.godot/imported/inventory.svg-4df2a15583a1909dfe17d480c9308b85.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/science.svg b/src/resources/icons/science.svg new file mode 100644 index 0000000..44ef30a --- /dev/null +++ b/src/resources/icons/science.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/science.svg.import b/src/resources/icons/science.svg.import new file mode 100644 index 0000000..37a6512 --- /dev/null +++ b/src/resources/icons/science.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://fkfiymss8p57" +path="res://.godot/imported/science.svg-cafdac4bdbb3c0808227ace8b523bf6d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/science.svg" +dest_files=["res://.godot/imported/science.svg-cafdac4bdbb3c0808227ace8b523bf6d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/search.svg b/src/resources/icons/search.svg new file mode 100644 index 0000000..1de92db --- /dev/null +++ b/src/resources/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/search.svg.import b/src/resources/icons/search.svg.import new file mode 100644 index 0000000..bb324b1 --- /dev/null +++ b/src/resources/icons/search.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rdp5i1i18jus" +path="res://.godot/imported/search.svg-82a26f800f89de19aeb402de255cb51a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/search.svg" +dest_files=["res://.godot/imported/search.svg-82a26f800f89de19aeb402de255cb51a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/send.svg b/src/resources/icons/send.svg new file mode 100644 index 0000000..826ac4c --- /dev/null +++ b/src/resources/icons/send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/send.svg.import b/src/resources/icons/send.svg.import new file mode 100644 index 0000000..2bc6aec --- /dev/null +++ b/src/resources/icons/send.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dc24ve1rlrlpq" +path="res://.godot/imported/send.svg-bb321525a8b68976793a8352e1678c24.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/send.svg" +dest_files=["res://.godot/imported/send.svg-bb321525a8b68976793a8352e1678c24.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/settings.svg b/src/resources/icons/settings.svg new file mode 100644 index 0000000..87e6549 --- /dev/null +++ b/src/resources/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/settings.svg.import b/src/resources/icons/settings.svg.import new file mode 100644 index 0000000..ce424d0 --- /dev/null +++ b/src/resources/icons/settings.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://basvqob80u3l6" +path="res://.godot/imported/settings.svg-0b4f826fa9066b225cd3dbb381f4b59c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/settings.svg" +dest_files=["res://.godot/imported/settings.svg-0b4f826fa9066b225cd3dbb381f4b59c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/subtitles.svg b/src/resources/icons/subtitles.svg new file mode 100644 index 0000000..b128d91 --- /dev/null +++ b/src/resources/icons/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/subtitles.svg.import b/src/resources/icons/subtitles.svg.import new file mode 100644 index 0000000..3cdb66e --- /dev/null +++ b/src/resources/icons/subtitles.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d2yhyfcuop7gx" +path="res://.godot/imported/subtitles.svg-9f1342bf97e0fea8eeb93e4877d4aa61.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/subtitles.svg" +dest_files=["res://.godot/imported/subtitles.svg-9f1342bf97e0fea8eeb93e4877d4aa61.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/icons/world.svg b/src/resources/icons/world.svg new file mode 100644 index 0000000..5d5101b --- /dev/null +++ b/src/resources/icons/world.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/icons/world.svg.import b/src/resources/icons/world.svg.import new file mode 100644 index 0000000..db923a2 --- /dev/null +++ b/src/resources/icons/world.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7ofnclcof0ud" +path="res://.godot/imported/world.svg-e1d8d3b0524fba3a4befec8a9a4380bd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/world.svg" +dest_files=["res://.godot/imported/world.svg-e1d8d3b0524fba3a4befec8a9a4380bd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=5.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/resources/shaders/grid_shader.gdshader b/src/resources/shaders/grid_shader.gdshader new file mode 100644 index 0000000..955d78a --- /dev/null +++ b/src/resources/shaders/grid_shader.gdshader @@ -0,0 +1,49 @@ +shader_type spatial; + +uniform float line_width : hint_range(0.001, 0.5, 0.001) = 0.005; + +// Controls how far the grid is visible +uniform float fade_start : hint_range(0, 100, 1) = 5.0; +uniform float fade_end : hint_range(0, 100, 1) = 20.0; + +varying vec3 world_position; +varying vec3 world_normal; + +void vertex() { + vec4 world_pos = MODEL_MATRIX * vec4(VERTEX, 1.0); + world_position = world_pos.xyz; + world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz); +} + +void fragment() { + // 1. Calculate the basic grid (Hard lines, no AA) + vec2 grid_xz = fract(world_position.xz); + vec2 grid_xy = fract(world_position.xy); + vec2 grid_yz = fract(world_position.yz); + + float line_xz = step(grid_xz.x, line_width) + (1.0 - step(grid_xz.x, 1.0 - line_width)) + + step(grid_xz.y, line_width) + (1.0 - step(grid_xz.y, 1.0 - line_width)); + float line_xy = step(grid_xy.x, line_width) + (1.0 - step(grid_xy.x, 1.0 - line_width)) + + step(grid_xy.y, line_width) + (1.0 - step(grid_xy.y, 1.0 - line_width)); + float line_yz = step(grid_yz.x, line_width) + (1.0 - step(grid_yz.x, 1.0 - line_width)) + + step(grid_yz.y, line_width) + (1.0 - step(grid_yz.y, 1.0 - line_width)); + + // 2. Calculate distance to camera + // CAMERA_POSITION_WORLD is a built-in in Godot 4 fragment shaders + float dist = distance(CAMERA_POSITION_WORLD, world_position); + + // 3. Calculate fade factor (1.0 = visible, 0.0 = invisible) + // smoothstep transitions smoothly from fade_start to fade_end + float fade = 1.0 - smoothstep(fade_start, fade_end, dist); + + // 4. Determine visible grid based on normals + float w_xz = abs(world_normal.y); + float w_xy = abs(world_normal.z); + float w_yz = abs(world_normal.x); + + float grid = max(line_xz * w_xz, max(line_xy * w_xy, line_yz * w_yz)); + + // 5. Mix colors, applying the fade + // As 'fade' drops to 0, the white lines mix into the black background + ALBEDO = mix(vec3(0.0), vec3(1.0), step(0.5, grid) * fade); +} diff --git a/src/resources/shaders/grid_shader.gdshader.uid b/src/resources/shaders/grid_shader.gdshader.uid new file mode 100644 index 0000000..d4a6ee4 --- /dev/null +++ b/src/resources/shaders/grid_shader.gdshader.uid @@ -0,0 +1 @@ +uid://bh250q6rfv615 diff --git a/src/scenes/levels/home.tscn b/src/scenes/levels/home.tscn index be55a20..e87e279 100644 --- a/src/scenes/levels/home.tscn +++ b/src/scenes/levels/home.tscn @@ -1,4 +1,6 @@ -[gd_scene load_steps=4 format=3 uid="uid://b3t1dk4vpjk6p"] +[gd_scene format=3 uid="uid://b3t1dk4vpjk6p"] + +[ext_resource type="Shader" uid="uid://bh250q6rfv615" path="res://resources/shaders/grid_shader.gdshader" id="1_ikf4c"] [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_dbach"] sky_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1) @@ -13,63 +15,24 @@ sky = SubResource("Sky_ikf4c") tonemap_mode = 2 glow_enabled = true -[node name="Home" type="Node3D"] +[sub_resource type="ShaderMaterial" id="ShaderMaterial_q28r8"] +render_priority = 0 +shader = ExtResource("1_ikf4c") +shader_parameter/line_width = 0.010000000475 +shader_parameter/fade_start = 5.0 +shader_parameter/fade_end = 50.0 + +[node name="Home" type="Node3D" unique_id=53262658] -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1916430820] transform = Transform3D(-0.8660254, -0.43301278, 0.25, 0, 0.49999997, 0.86602545, -0.50000006, 0.75, -0.43301266, 0, 0, 0) shadow_enabled = true -[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1457348223] environment = SubResource("Environment_q28r8") -[node name="Node3D" type="Node3D" parent="."] - -[node name="CSGBox3D" type="CSGBox3D" parent="Node3D"] +[node name="CSGBox3D" type="CSGBox3D" parent="." unique_id=533069808] transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, -0.5, 0) use_collision = true -size = Vector3(25, 1, 10) - -[node name="CSGBox3D9" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 3, 0) -use_collision = true -size = Vector3(25, 0.1, 10) - -[node name="CSGBox3D2" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -12.45) -use_collision = true -size = Vector3(10, 1, 0.1) - -[node name="CSGBox3D5" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 1.5, -9.35) -use_collision = true -size = Vector3(4, 3, 0.1) - -[node name="CSGBox3D8" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 12.45) -use_collision = true -size = Vector3(10, 3, 0.1) - -[node name="CSGBox3D6" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 1.5, -9.35) -use_collision = true -size = Vector3(4, 3, 0.1) - -[node name="CSGBox3D3" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.95, 1.5, 1.6) -use_collision = true -size = Vector3(0.1, 3, 21.8) - -[node name="CSGBox3D7" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.95, 1.5, 1.6) -use_collision = true -size = Vector3(0.1, 3, 21.8) - -[node name="CSGBox3D4" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.95, 0.5, -10.9) -use_collision = true -size = Vector3(0.1, 1, 3) - -[node name="CSGBox3D10" type="CSGBox3D" parent="Node3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.95, 0.5, -10.9) -use_collision = true -size = Vector3(0.1, 1, 3) +size = Vector3(1000, 0.01, 1000) +material = SubResource("ShaderMaterial_q28r8") diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd index 1c2df24..5def165 100644 --- a/src/scenes/managers/app/network_manager.gd +++ b/src/scenes/managers/app/network_manager.gd @@ -1,7 +1,15 @@ +# --- License +# File: /client/src/scenes/managers/app/network_manager.gd +# Project: OpenMinerva +# Created Date: 05 February 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + extends Node var n_c = preload("res://scripts/network/network_compression.gd").new() -var jwt = preload("res://scripts/crypto/jwt.gd").new() var rsa = preload("res://scripts/crypto/rsa.gd").new() var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") diff --git a/src/scenes/managers/app/scene_manager.gd b/src/scenes/managers/app/scene_manager.gd index 6bfbab2..e316389 100644 --- a/src/scenes/managers/app/scene_manager.gd +++ b/src/scenes/managers/app/scene_manager.gd @@ -13,7 +13,7 @@ var server_init: bool = false func _ready(): await network_manager.start_server() - var session_name = Random.random_string(6) + var session_name = Random.random_string(6, true) var new_home = player_home_scene.instantiate() new_home.name = session_name network_manager.active_session = session_name diff --git a/src/scenes/master.tscn b/src/scenes/master.tscn index a0fe73a..33d08b2 100644 --- a/src/scenes/master.tscn +++ b/src/scenes/master.tscn @@ -3,7 +3,7 @@ [ext_resource type="PackedScene" uid="uid://by0vghgshvbhd" path="res://scenes/managers/app/rpc_manager.tscn" id="1_h2qy3"] [ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network_manager.tscn" id="1_jooxx"] [ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene_manager.tscn" id="2_h2qy3"] -[ext_resource type="PackedScene" uid="uid://bdsc5kvle3jgd" path="res://userinterface/hud.tscn" id="2_rnotf"] +[ext_resource type="PackedScene" uid="uid://ckl5gw0xbduiv" path="res://userinterface/dash/hud.tscn" id="5_q3f5g"] [node name="Master" type="Node3D" unique_id=420526444] @@ -13,7 +13,7 @@ [node name="SceneManager" parent="." unique_id=5477810 instance=ExtResource("2_h2qy3")] -[node name="Hud" parent="." unique_id=1305621179 instance=ExtResource("2_rnotf")] +[node name="Hud" parent="." unique_id=1053137144 instance=ExtResource("5_q3f5g")] visible = false [node name="Scenes" type="Node3D" parent="." unique_id=308239834] diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd index 0b61580..9bf7309 100644 --- a/src/scenes/players/player.gd +++ b/src/scenes/players/player.gd @@ -44,13 +44,14 @@ func _ready(): func _input(event): if is_multiplayer_authority() == false: return + if Input.is_action_just_pressed("escape"): if mouse_captured: capture_mouse(false) - hud.set_active_state(true) + Events.emit_signal("dash_set_state", true) else: capture_mouse(true) - hud.set_active_state(false) + Events.emit_signal("dash_set_state", false) return if event is InputEventMouseMotion && mouse_captured: diff --git a/src/scenes/players/player.tscn b/src/scenes/players/player.tscn index 40d82e5..5f1219a 100644 --- a/src/scenes/players/player.tscn +++ b/src/scenes/players/player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=4 format=3 uid="uid://dvx1vs2ig7st4"] +[gd_scene format=3 uid="uid://dvx1vs2ig7st4"] [ext_resource type="Script" uid="uid://dxa60xi5uelay" path="res://scenes/players/player.gd" id="1_plyga"] @@ -7,38 +7,38 @@ [sub_resource type="SeparationRayShape3D" id="SeparationRayShape3D_xrm3l"] length = 0.25 -[node name="Player" type="CharacterBody3D" groups=["Players"]] +[node name="Player" type="CharacterBody3D" unique_id=823362684 groups=["Players"]] script = ExtResource("1_plyga") -[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1660137540] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) mesh = SubResource("CapsuleMesh_p1mvi") -[node name="Head" type="Node3D" parent="."] +[node name="Head" type="Node3D" parent="." unique_id=401421687] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, -0.15) -[node name="Camera3D" type="Camera3D" parent="Head"] +[node name="Camera3D" type="Camera3D" parent="Head" unique_id=1141552766] fov = 40.0 -[node name="InteractionRay" type="RayCast3D" parent="Head/Camera3D"] +[node name="InteractionRay" type="RayCast3D" parent="Head/Camera3D" unique_id=1005810842] target_position = Vector3(0, 0, -2) collision_mask = 2 hit_back_faces = false -[node name="AimSight" type="RayCast3D" parent="Head/Camera3D"] +[node name="AimSight" type="RayCast3D" parent="Head/Camera3D" unique_id=1914541974] transform = Transform3D(1, 0, 0, 0, -1, -8.74228e-08, 0, 8.74228e-08, -1, 0, 0, 0) target_position = Vector3(0, 0, 3000) hit_back_faces = false -[node name="Hands" type="Node3D" parent="Head/Camera3D"] +[node name="Hands" type="Node3D" parent="Head/Camera3D" unique_id=553484413] transform = Transform3D(-4.37114e-08, 0, 1, 0, 1, 0, -1, 0, -4.37114e-08, 0, 0, 0) -[node name="StairStep" type="CollisionShape3D" parent="."] +[node name="StairStep" type="CollisionShape3D" parent="." unique_id=1633399245] transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0.3, -0.5194253) shape = SubResource("SeparationRayShape3D_xrm3l") -[node name="PlayerMovement" type="Node" parent="."] +[node name="PlayerMovement" type="Node" parent="." unique_id=1713886755] -[node name="PlayerInput" type="Node" parent="."] +[node name="PlayerInput" type="Node" parent="." unique_id=903642472] -[node name="PlayerInventory" type="Node" parent="."] +[node name="PlayerInventory" type="Node" parent="." unique_id=1423667068] diff --git a/src/scripts/crypto/aes.gd b/src/scripts/crypto/aes.gd deleted file mode 100644 index 61679fd..0000000 --- a/src/scripts/crypto/aes.gd +++ /dev/null @@ -1 +0,0 @@ -extends Node \ No newline at end of file diff --git a/src/scripts/crypto/aes.gd.uid b/src/scripts/crypto/aes.gd.uid deleted file mode 100644 index 71277cb..0000000 --- a/src/scripts/crypto/aes.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cnr57wo33psve diff --git a/src/scripts/crypto/credential_store.gd b/src/scripts/crypto/credential_store.gd deleted file mode 100644 index 405bf3e..0000000 --- a/src/scripts/crypto/credential_store.gd +++ /dev/null @@ -1,18 +0,0 @@ -# FIXME: IMPORTANT! -# This script is more of a placeholder until a more secure method of storing these credentials becomes available. -# Due to security issues revolving around how Godot handles scripts umong other things, any object can be made accessible by anything else. -# As a result, there just isn't a safe spot to store this data. -extends Node - -var info = { - "token": "", - "expire_time": "" -} - -func set_account_credential(credentials: PackedStringArray = []): - info.token = credentials[0] - info.expire_time = credentials[1] - GlobalLogger.logs("Saved JWT to memory.") - -# TODO: Save account credentials to disk. -# TODO: Read account credentials from disk. \ No newline at end of file diff --git a/src/scripts/crypto/credential_store.gd.uid b/src/scripts/crypto/credential_store.gd.uid deleted file mode 100644 index d1ea891..0000000 --- a/src/scripts/crypto/credential_store.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cs4c0ctis2flp diff --git a/src/scripts/crypto/jwt.gd b/src/scripts/crypto/jwt.gd deleted file mode 100644 index 2fb531c..0000000 --- a/src/scripts/crypto/jwt.gd +++ /dev/null @@ -1,82 +0,0 @@ -extends Node - -# NOTE: To keep things consistent, please keep the signature always in base64. Only convert it where it will be used. - -func decode(jwt_string: String = "") -> Dictionary: - var return_dict = {"ok": false, "data": {}} - - var jwt_parts = _get_jwt_parts(jwt_string) - - return_dict.data.head = JSON.parse_string(Marshalls.base64_to_utf8(_base64url_to_base64(jwt_parts.head))) - return_dict.data.payload = JSON.parse_string(Marshalls.base64_to_utf8(_base64url_to_base64(jwt_parts.payload))) - # The signature should not be converted from base64 - return_dict.data.signature = _base64url_to_base64(jwt_parts.signature) - return_dict.ok = true - return return_dict - -func verify(jwt_string: String, public_key: CryptoKey) -> bool: - var crypto: Crypto = Crypto.new() - var jwt_parts: Dictionary = _get_jwt_parts(jwt_string) - if jwt_parts.ok != true: - GlobalLogger.logs("Failed to deconstruct jwt when verifying jwt.", 2) - GlobalLogger.logs(str(jwt_string), 0) - return false - - var formatted_payload: Dictionary = _format_jwt_payload(jwt_parts.head, jwt_parts.payload) - if formatted_payload.ok != true: - GlobalLogger.logs("Failed to format the jwt payload when verifying jwt.", 2) - GlobalLogger.logs(str(jwt_parts), 0) - return false - - return crypto.verify( - HashingContext.HASH_SHA256, - formatted_payload.payload_bytes, - Marshalls.base64_to_raw(jwt_parts.signature), - public_key - ) - -# Private functions -func _format_jwt_payload(head: String, payload: String) -> Dictionary: - # TODO: Error checks - var return_dict = {"ok": false, "payload_bytes": []} - - var formatted_payload = head + "." + payload - var payload_bytes = formatted_payload.to_utf8_buffer() - - var hasher: HashingContext = HashingContext.new() - hasher.start(HashingContext.HASH_SHA256) - hasher.update(payload_bytes) - - return_dict.payload_bytes = hasher.finish() - return_dict.ok = true - - return return_dict - -func _base64url_to_base64(base64url: String): - # TODO: Error checks - var fixed: String = base64url - - fixed = fixed.replace("_", "/").replace("-", "+") - var padding = 4 - (fixed.length() % 4) - - if padding < 4: - fixed += "=".repeat(padding) - - return fixed - -func _get_jwt_parts(jwt_string: String = "") -> Dictionary: - # TODO: Error checks - var return_dict = {"ok": false, "head": "", "payload": "", "signature": ""} - - var jwt_split = jwt_string.split(".") - - if len(jwt_split) != 3: - GlobalLogger.logs("JWT token is not formatted correctly.", 2) - return return_dict - - return_dict.head = jwt_split[0] - return_dict.payload = jwt_split[1] - return_dict.signature = _base64url_to_base64(jwt_split[2]) - return_dict.ok = true - - return return_dict diff --git a/src/scripts/crypto/jwt.gd.uid b/src/scripts/crypto/jwt.gd.uid deleted file mode 100644 index 7d89294..0000000 --- a/src/scripts/crypto/jwt.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://buxmy0sbla4sq diff --git a/src/scripts/crypto/rsa.gd b/src/scripts/crypto/rsa.gd index d578d5e..e0064c2 100644 --- a/src/scripts/crypto/rsa.gd +++ b/src/scripts/crypto/rsa.gd @@ -1,6 +1,13 @@ -extends Node +# --- License +# File: /client/src/scripts/crypto/rsa.gd +# Project: OpenMinerva +# Created Date: 05 February 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License -var jwt = preload("res://scripts/crypto/jwt.gd").new() +extends Node ## Generates a RSA keypair at a specific bit length ## @returns Dictionary @@ -22,21 +29,6 @@ func generate_keypair(level: int = 0) -> Dictionary: return_dictionary.public = generated_keys.save_to_string(true) return return_dictionary -## Verify a signature with a provided public key -## @returns bool -func verify_jwt_signature(jwt_string: String = "", signature_pem: String = "") -> bool: - var crypto: Crypto = Crypto.new() - var sig: CryptoKey = pem_to_cryptokey(signature_pem) - var jwt_parts: Dictionary = jwt._get_jwt_parts(jwt_string) - var formatted_payload = jwt._format_jwt_payload(jwt_parts.head, jwt_parts.payload) - - return crypto.verify( - HashingContext.HASH_SHA256, - formatted_payload.payload_bytes, - Marshalls.base64_to_raw(jwt_parts.signature), - sig - ) - ## Turns a pem into a CryptoKey ## @returns CryptoKey func pem_to_cryptokey(pem: String = "") -> CryptoKey: diff --git a/src/scripts/libs/account.gd b/src/scripts/libs/account.gd new file mode 100644 index 0000000..4b0f45c --- /dev/null +++ b/src/scripts/libs/account.gd @@ -0,0 +1,220 @@ +# --- License +# File: /client/src/scripts/libs/account.gd +# Project: OpenMinerva +# Created Date: 26 February 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +var http = preload("res://scripts/network/http.gd").new() +var time_lib = preload("res://scripts/libs/time.gd").new() +var random_lib = preload("res://scripts/utils/random.gd").new() +var rsa_lib = preload("res://scripts/crypto/rsa.gd").new() + +const ACCOUNT_DATABASE_DIRECTORY: String = "user://database/accounts.bin" + +# TODO: Create proper encryption of the account database +# https://github.com/OpenMinerva/client/issues/59 +var stop_connection_timer = false + +var active_account = {} +var _database = [] + +func _ready(): + _load_account_database() + +## Get a list of all accounts and return their information. +func get_all() -> Array: + return _database + +## Adds an account to the account database. +func create(account: Dictionary, type: String) -> Dictionary: + # Make sure we are only recording data we are intending on. + var account_formatted: Dictionary = {} + + if type == "oauth": + account_formatted = _create_oauth(account) + + if len(account_formatted.keys()) == 0: + GlobalLogger.logs("Tried to create an account, but there was nothing to save.", 3) + return {"ok": false, "error": "No account formatted.", "id": null} + + _database.append(account_formatted) + + _save_account_database() + + Events.emit_signal("dash_account_list_loaded") + return {"ok": true, "id": account_formatted.id} + +func _create_oauth(account) -> Dictionary: + var _account_keys = rsa_lib.generate_keypair() + + var _clean_account = {} + _clean_account.id = random_lib.random_string(6, true) + _clean_account.display_name = account.get("display_name", null) + _clean_account.account_server = account.get("account_server", null) + _clean_account.private_device_key = _account_keys.private + _clean_account.public_device_key = _account_keys.public + + _clean_account.access_token = "" + _clean_account.refresh_token = "" + _clean_account.id_token = "" + _clean_account.access_token_expiry = 0 + + _clean_account.type = "oauth" + return _clean_account + +## Removes an account from the account database. +func remove(id: String) -> Dictionary: + GlobalLogger.logs("Attempting to remove account '%s'" % id) + var target_entry = _database.find_custom(func(entry): return entry.get("id") == id) + _database.remove_at(target_entry) + _save_account_database() + + Events.emit_signal("dash_account_list_loaded") + + return {"ok": true} + +## Sets an account as the active account. +func use(id: String) -> void: + GlobalLogger.logs("Setting active account to '%s'." % id, 1) + + var _account = _get_account_by_id(id) + if _account.type == "oauth": + if await OAuth.validate_token(_account) == false: + await authenticate_oauth(id) + + active_account = _account + Events.emit_signal("dash_active_account_changed", active_account) + return + +## Signs out of the active account. +func clear() -> void: + active_account = {} + return + +func update(id: String, data: Dictionary) -> void: + var target_entry = _database.find_custom(func(entry): return entry.get("id") == id) + var account = _get_account_by_id(id) + + var _database_keys = account.keys() + var _data_keys = data.keys() + + for key in _data_keys: + if key not in _database_keys: + GlobalLogger.logs("Tried to update an invalid key in an account, '%s'." % key) + continue + + account[key] = data[key] + + _database[target_entry] = account + _save_account_database() + return + +func authenticate_oauth(id: String, remember_me: bool = false) -> void: + # TODO: Error checks + GlobalLogger.logs("Attempting to connect account '%s' using oauth." % id) + var account = _get_account_by_id(id) + + # TODO: Check if account is still valid without trying to sign in. + var oauth_tokens = await OAuth.authenticate(account.account_server + "/oauth/authorize") + + update(id, oauth_tokens) + +## Save the current account database we have in memory to the disk. +func _save_account_database() -> void: + GlobalLogger.logs("Saving account database to disk.") + DirAccess.open("user://").make_dir_recursive("user://database") + + var file = FileAccess.open(ACCOUNT_DATABASE_DIRECTORY, FileAccess.WRITE) + if file: + file.store_var(_database) # Serializes variable to binary + file.close() + return + +## Read the account database from the config file on our disk. +func _load_account_database() -> Array: + GlobalLogger.logs("Loading the local account database.", 1) + + var account_file_exists = FileAccess.file_exists(ACCOUNT_DATABASE_DIRECTORY) + if account_file_exists == false: + GlobalLogger.logs("Account database does not exist, creating one now.", 1) + _save_account_database() + + var file = FileAccess.open(ACCOUNT_DATABASE_DIRECTORY, FileAccess.READ) + + var account_data + + if file: + account_data = file.get_var() # Deserializes variable back + file.close() + + _database = account_data + return account_data + +func _get_account_by_id(id: String) -> Dictionary: + var index = _database.find_custom(func(entry): return entry.get("id") == id) + + if index > -1: + return _database[index] + + return {} + +func _update_account_by_key(id: String, key: String, value: Variant) -> void: + var index = _database.find_custom(func(entry): return entry.get("id") == id) + # TODO: Check if key is a valid key. + _database[index][key] = value + _save_account_database() + return + +func get_account_authentication_status(id) -> Dictionary: + var status = { + "valid_passport": false, + "valid_private_jwt": false + } + + var _account = _get_account_by_id(id) + + status.valid_passport = _account.get("public_account_server_passport", {"expires": 0}).get("expires", 0) > int(Time.get_unix_time_from_system()) + status.valid_private_jwt = _account.get("private_account_server_jwt", {"expires": 0}).get("expires", 0) > int(Time.get_unix_time_from_system()) + + return status + +func _handle_response(response: Dictionary) -> Dictionary: + var response_data = {"ok": false, "error": "", "body": {}} + + if response.get("ok", false) == false: + response_data.error = "Request failed for unknown reason." + GlobalLogger.logs(response_data.error, 3) + return response_data + + if response.get("body", null) == null: + response_data.error = "No body provided from the request." + GlobalLogger.logs(response_data.error, 3) + return response_data + + response_data.ok = true + response_data.body = JSON.parse_string(response.get("body")) + + return response_data + +# DEV: Upload public key to the server. +# func test_upload_public_key_to_server(): +# GlobalLogger.logs("Registering the device public key to the account server.") +# var body = { +# "public_key": active_account.public_device_key +# } +# var url_parts = UrlParser.deconstruct(active_account.account_server) +# if url_parts.ok == false: +# GlobalLogger.logs("Unhandled error registering the public device key to the account server. '%s'" % url_parts.error, 3) +# return +# url_parts = url_parts.data + +# print(url_parts) + +# var public_key_response = await http.req(HTTPClient.Method.METHOD_POST, url_parts.host, "/api/v1/device_key", url_parts.port, ["Accept: application/json", "Content-Type: application/json", "authorization: Bearer %s" % oauth_lib.access_token], JSON.stringify(body)) +# print(public_key_response) +# return diff --git a/src/scripts/libs/account.gd.uid b/src/scripts/libs/account.gd.uid new file mode 100644 index 0000000..51e0263 --- /dev/null +++ b/src/scripts/libs/account.gd.uid @@ -0,0 +1 @@ +uid://dtlb70kxvbtvn diff --git a/src/scripts/libs/account_server.gd b/src/scripts/libs/account_server.gd new file mode 100644 index 0000000..644f41f --- /dev/null +++ b/src/scripts/libs/account_server.gd @@ -0,0 +1,20 @@ +extends Node + +var _http = preload("res://scripts/network/http.gd").new() +var _database = {} + +func get_public_key(url: String): + GlobalLogger.logs("Requesting public key from account server '%s'" % url, 1) + + # Check if we already have the public key in our database + + # If we do, return that data. + # Otherwise, http request the account server. + # Validate the response is valid. + # Add extra metadata to the database entry. + # Save to the database. + return + +func reset_database(): + GlobalLogger.logs("Clearing the account server database.", 1) + _database = {} \ No newline at end of file diff --git a/src/scripts/libs/account_server.gd.uid b/src/scripts/libs/account_server.gd.uid new file mode 100644 index 0000000..8de3a64 --- /dev/null +++ b/src/scripts/libs/account_server.gd.uid @@ -0,0 +1 @@ +uid://dm1co8xfx6i2x diff --git a/src/scripts/libs/jwt.gd b/src/scripts/libs/jwt.gd new file mode 100644 index 0000000..55e9803 --- /dev/null +++ b/src/scripts/libs/jwt.gd @@ -0,0 +1,112 @@ +# --- License +# File: /client/src/scripts/libs/jwt.gd +# Project: OpenMinerva +# Created Date: 24 February 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +# NOTE: To keep things consistent, please keep the signature always in base64. Only convert it where it will be used. +# NOTE: Currently the account server provides JWT that are Base64url encoded. We want to change this to Base64 in this application. + +func decode(jwt_string: String = ""): + var return_dict = {"ok": false, "data": {"head": "", "payload": "", "signature": ""}} + var jwt_parts = _get_parts(jwt_string) + + return_dict.data.head = JSON.parse_string(Marshalls.base64_to_utf8(base64url_to_base64(jwt_parts.head))) + return_dict.data.payload = JSON.parse_string(Marshalls.base64_to_utf8(base64url_to_base64(jwt_parts.payload))) + return_dict.data.signature = base64url_to_base64(jwt_parts.head) + return_dict.ok = true + + return return_dict + +func encode(): + return + +func validate(jwt_string: String, public_spki: String): + var crypto: Crypto = Crypto.new() + var jwt_parts: Dictionary = _get_parts(jwt_string) + var public_key: CryptoKey = pem_to_cryptokey(public_spki) + if jwt_parts.ok != true: + GlobalLogger.logs("Failed to deconstruct jwt when verifying jwt.", 2) + GlobalLogger.logs(str(jwt_string), 0) + return false + + var formatted_payload: Dictionary = _format_payload_for_verification(jwt_parts.head, jwt_parts.payload) + if formatted_payload.ok != true: + GlobalLogger.logs("Failed to format the jwt payload when verifying jwt.", 2) + GlobalLogger.logs(str(jwt_parts), 0) + return false + + return crypto.verify( + HashingContext.HASH_SHA256, + formatted_payload.payload_bytes, + Marshalls.base64_to_raw(jwt_parts.signature), + public_key + ) + +func base64url_to_base64(input_value: String): + var fixed: String = input_value + + fixed = fixed.replace("_", "/").replace("-", "+") + var padding = 4 - (fixed.length() % 4) + + if padding < 4: + fixed += "=".repeat(padding) + + return fixed + +func base64_to_base64url(input_value: String): + var base64_str = input_value.replace("+", "-") + base64_str = base64_str.replace("/", "_") + + while base64_str.ends_with("="): + base64_str = base64_str.substr(0, base64_str.length() - 1) + + return base64_str + +func _get_parts(input_value: String): + # TODO: Error checks + var return_dict = {"ok": false, "error": "", "head": "", "payload": "", "signature": ""} + + var jwt_split = input_value.split(".") + + if len(jwt_split) != 3: + GlobalLogger.logs("JWT is not formatted correctly.", 2) + return_dict.error = "JWT is not formatted correctly." + return return_dict + + return_dict.head = jwt_split[0] + return_dict.payload = jwt_split[1] + return_dict.signature = base64url_to_base64(jwt_split[2]) + return_dict.ok = true + + return return_dict + +func _format_payload_for_verification(head: String, payload: String) -> Dictionary: + # TODO: Error checks + var return_dict = {"ok": false, "payload_bytes": []} + + var formatted_payload = head + "." + payload + var payload_bytes = formatted_payload.to_utf8_buffer() + + var hasher: HashingContext = HashingContext.new() + hasher.start(HashingContext.HASH_SHA256) + hasher.update(payload_bytes) + + return_dict.payload_bytes = hasher.finish() + return_dict.ok = true + + return return_dict + +func pem_to_cryptokey(pem: String = "") -> CryptoKey: + # TODO: Error checks + var public_key := CryptoKey.new() + if public_key.load_from_string(pem, true) != OK: + GlobalLogger.logs("Failed to load public key", 3) + return null + + return public_key \ No newline at end of file diff --git a/src/scripts/libs/jwt.gd.uid b/src/scripts/libs/jwt.gd.uid new file mode 100644 index 0000000..3f3604e --- /dev/null +++ b/src/scripts/libs/jwt.gd.uid @@ -0,0 +1 @@ +uid://owde4o55acn2 diff --git a/src/scripts/libs/oauth.gd b/src/scripts/libs/oauth.gd new file mode 100644 index 0000000..858ccf3 --- /dev/null +++ b/src/scripts/libs/oauth.gd @@ -0,0 +1,148 @@ +# --- License +# File: /client/src/scripts/libs/oauth.gd +# Project: OpenMinerva +# Created Date: 23 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +var http = preload("res://scripts/network/http.gd").new() +var jwt_lib = preload("res://scripts/libs/jwt.gd").new() +var random_lib = preload("res://scripts/utils/random.gd").new() + +var port: int = 54000 +var bind_address: String = "127.0.0.1" +var redirect_server = TCPServer.new() + +var _listen_for_oauth_connections: bool = false + +func authenticate(account_server: String) -> Dictionary: + var secret_pkce: String = random_lib.random_string(50) + var account_server_url = UrlParser.deconstruct(account_server) + + if account_server_url.ok == false: + return {} + + account_server_url = account_server_url.data + + GlobalLogger.logs("Starting OAuth flow.", 0) + + var uri_parts := [ + "client_id=%s" % "OpenMinerva-Game-Client", + "redirect_uri=http://%s:%s" % [bind_address, port], + "response_type=code", + "scope=openid offline_access", + "response_mode=query", + "code_challenge_method=S256", + "code_challenge=%s" % _get_code_challenge(secret_pkce), + "prompt=consent" + ] + + var uri = account_server + "?" + "&".join(uri_parts) + OS.shell_open(uri) + + GlobalLogger.logs("Starting OAuth redirect server.", 0) + redirect_server.listen(port, bind_address) + + _listen_for_oauth_connections = true + var _auth_code = await _wait_for_auth_code() + + GlobalLogger.logs("Closing OAuth redirect server.", 0) + redirect_server = TCPServer.new() + + GlobalLogger.logs("Exchanging retrieved auth code for a proper token.", 0) + var form_parts := [ + "client_id=%s" % "OpenMinerva-Game-Client", + "grant_type=authorization_code", + "code=%s" % _auth_code, + "redirect_uri=http://%s:%s" % [bind_address, port], + "code_challenge_method=S256", + "code_verifier=%s" % secret_pkce, + ] + var form_string: String = "&".join(form_parts) + var exchange_response = await http.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) + var token_data = JSON.parse_string(exchange_response.get("body")) + + return _get_tokens_from_response(token_data) + +func _get_code_challenge(verifier: String) -> String: + var ctx = HashingContext.new() + ctx.start(HashingContext.HASH_SHA256) + ctx.update(verifier.to_utf8_buffer()) + var hash_bytes = ctx.finish() + var base64_str = Marshalls.raw_to_base64(hash_bytes) + + base64_str = jwt_lib.base64_to_base64url(base64_str) + + return base64_str + +func _wait_for_auth_code() -> String: + var code: String = "" + + while code == "": + if redirect_server.is_connection_available(): + _listen_for_oauth_connections = false + code = _handle_auth_callback(redirect_server.take_connection()) + else: + await get_tree().process_frame + + return code + +func _handle_auth_callback(connection: StreamPeerTCP) -> String: + var request = connection.get_string(connection.get_available_bytes()) + + var temp_auth_code: String = request.split("code=")[1].split("&iss=")[0].strip_edges() + + GlobalLogger.logs("Got authentication code: '%s'." % temp_auth_code, 0) + + # Send success. + var html_response = "HTTP/1.1 200 OK\r\n" + html_response += "Content-Type: text/html\r\n" + html_response += "Connection: close\r\n\r\n" + html_response += "

Success!

You can close this window now.

" + + connection.put_data(html_response.to_utf8_buffer()) + connection.disconnect_from_host() + + return temp_auth_code + +func _exchange_code() -> Dictionary: + return {} + +func _get_tokens_from_response(response: Dictionary) -> Dictionary: + # TODO: Error checks to prevent overwriting with bad data. + var oauth_data = { + "access_token" = response.get("access_token"), + "refresh_token" = response.get("refresh_token"), + "id_token" = response.get("id_token"), + "access_token_expiry" = response.get("expires_in") + } + + return oauth_data + +func validate_token(account: Dictionary) -> bool: + if account.access_token == "": + return false + + var form_parts := [ + "client_id=%s" % "OpenMinerva-Game-Client", + "token=%s" % account.access_token, + ] + var form_string: String = "&".join(form_parts) + + var account_server_url = UrlParser.deconstruct(account.account_server) + if account_server_url.ok == false: + GlobalLogger.logs("Failed to parse account server url.", 3) + return false + account_server_url = account_server_url.data + + var introspect_response = await http.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token/introspection", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) + if introspect_response.ok == false: + GlobalLogger.logs("Unknown error parsing the introspection response.", 3) + return false + introspect_response = JSON.parse_string(introspect_response.body) + + return introspect_response.active diff --git a/src/scripts/libs/oauth.gd.uid b/src/scripts/libs/oauth.gd.uid new file mode 100644 index 0000000..d5e1ac6 --- /dev/null +++ b/src/scripts/libs/oauth.gd.uid @@ -0,0 +1 @@ +uid://jd7qlsley1no diff --git a/src/scripts/libs/time.gd b/src/scripts/libs/time.gd new file mode 100644 index 0000000..b5cf25d --- /dev/null +++ b/src/scripts/libs/time.gd @@ -0,0 +1,32 @@ +extends Node + +const MONTH_MAP = { + "Jan": 1, + "Feb": 2, + "Mar": 3, + "Apr": 4, + "May": 5, + "Jun": 6, + "Jul": 7, + "Aug": 8, + "Sep": 9, + "Oct": 10, + "Nov": 11, + "Dec": 12 +} + +func convert_jwt_timestamp_to_unix(timestamp: String) -> int: + var _split = timestamp.split(" ") + var _time_split = _split[4].split(":") + + var datetime_dict = { + "year": _split[3].to_int(), + "month": MONTH_MAP.get(_split[2], 1), + "day": _split[1].to_int(), + "hour": _time_split[0].to_int(), + "minute": _time_split[1].to_int(), + "second": _time_split[2].to_int() + } + + var unix_timestamp = Time.get_unix_time_from_datetime_dict(datetime_dict) + return unix_timestamp \ No newline at end of file diff --git a/src/scripts/libs/time.gd.uid b/src/scripts/libs/time.gd.uid new file mode 100644 index 0000000..5125d2f --- /dev/null +++ b/src/scripts/libs/time.gd.uid @@ -0,0 +1 @@ +uid://cfdqhdynesvon diff --git a/src/scripts/rpc/client.gd b/src/scripts/rpc/client.gd index 15490b3..971d404 100644 --- a/src/scripts/rpc/client.gd +++ b/src/scripts/rpc/client.gd @@ -19,7 +19,7 @@ func on_receive_server_info(info): GlobalLogger.logs("Got server info!") if info.level: await scene_manager.load_multiplayer_scene(info.level, info.level_node_name) - get_parent().s.rpc_id(1, "on_receive_player_info", CredentialStore.info.token) + get_parent().s.rpc_id(1, "on_receive_player_info", GlobalAccount.active_account.get("public_account_server_passport", {}).get("token", null)) return @rpc("authority", "reliable") diff --git a/src/scripts/rpc/server.gd b/src/scripts/rpc/server.gd index 1b90f06..0666354 100644 --- a/src/scripts/rpc/server.gd +++ b/src/scripts/rpc/server.gd @@ -1,7 +1,16 @@ +# --- License +# File: /client/src/scripts/rpc/server.gd +# Project: OpenMinerva +# Created Date: 05 February 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + extends Node var n_c = preload("res://scripts/network/network_compression.gd").new() -var jwt = preload("res://scripts/crypto/jwt.gd").new() +var jwt = preload("res://scripts/libs/jwt.gd").new() var rsa = preload("res://scripts/crypto/rsa.gd").new() var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") diff --git a/src/scripts/signal_bus.gd b/src/scripts/signal_bus.gd new file mode 100644 index 0000000..acdea3c --- /dev/null +++ b/src/scripts/signal_bus.gd @@ -0,0 +1,20 @@ +# --- License +# File: /client/src/scripts/signal_bus.gd +# Project: OpenMinerva +# Created Date: 28 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License +extends Node + +# Dashboard +signal dash_set_state(is_open: bool) +signal dash_switch_tab(page_name: String) +signal dash_active_account_changed(account: Dictionary) +signal dash_storage_changed(storage_data: Dictionary) +signal dash_session_changed(session_data: Dictionary) +signal dash_message_received(message: Dictionary) +signal dash_notification(notification: Dictionary) + +signal dash_account_list_loaded(account_list: PackedStringArray) \ No newline at end of file diff --git a/src/scripts/signal_bus.gd.uid b/src/scripts/signal_bus.gd.uid new file mode 100644 index 0000000..55a5805 --- /dev/null +++ b/src/scripts/signal_bus.gd.uid @@ -0,0 +1 @@ +uid://c656spc3ppdlw diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index be14e5e..963a26d 100644 --- a/src/scripts/utils/files.gd +++ b/src/scripts/utils/files.gd @@ -1,30 +1,6 @@ extends Node -## Create a config file at a given relative directory. -## Example: /system/cool.json -## @returns void -func create_config_file(dir: String) -> void: - GlobalLogger.logs("Creating '%s'" % dir, 0) - _maybe_make_directory("user://config/") - # TODO: Sanataize param - # TODO: Error checks - var dir_access = DirAccess.open("user://config/") - dir_access.make_dir_recursive("user://config/%s" % dir) - return - -## Read a config file from a directory. -## Example: /system/cool.json -## @returns String -func read_config_file(dir: String) -> String: - # TODO: Error checks - GlobalLogger.logs("Reading '%s'" % dir, 0) - dir = "user://config/%s" % dir - var file_contents = FileAccess.open(dir, FileAccess.READ).get_as_text() - return file_contents - -## Create a config file at a given relative directory. -## Example: /system/cool.json -## @returns String +## Creates a log file following the internal format. func create_log_file() -> String: GlobalLogger.logs("Creating a log file for this session.", 0) _maybe_make_directory("user://logs/") diff --git a/src/scripts/utils/random.gd b/src/scripts/utils/random.gd index 8d2064c..5efafee 100644 --- a/src/scripts/utils/random.gd +++ b/src/scripts/utils/random.gd @@ -1,9 +1,9 @@ extends Node -func random_string(length: int = 6): +func random_string(length: int = 6, hexa_encoding: bool = false): var rng = RandomNumberGenerator.new() rng.randomize() - var chars = "0123456789abcdef" + var chars = "0123456789abcdef" if hexa_encoding else "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" var out = "" for i in length: out += chars[rng.randi_range(0, chars.length() - 1)] diff --git a/src/userinterface/dash/account_create.gd b/src/userinterface/dash/account_create.gd new file mode 100644 index 0000000..19d6233 --- /dev/null +++ b/src/userinterface/dash/account_create.gd @@ -0,0 +1,60 @@ +# --- License +# File: /client/src/userinterface/dash/account_create.gd +# Project: OpenMinerva +# Created Date: 28 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control + +var _page_names = [] +@onready var create = get_node("Create") +@onready var select_oauth_btn = get_node("Create/SelectMethod/Container/OAuth") +@onready var create_oauth_btn = get_node("Create/OAuth/Create/HBoxContainer/ConfirmCreateAccount") +@onready var create_oauth_back_btn = get_node("Create/OAuth/Create/HBoxContainer/CreateAccountBack") + +func _ready(): + _get_pages() + select_oauth_btn.pressed.connect(_display_login_route.bind("OAuth")) + + create_oauth_btn.pressed.connect(_create_oauth) + create_oauth_back_btn.pressed.connect(_display_login_route.bind("SelectMethod")) + return + +func _display_oauth(): + return + +func _display_login_route(page_name: String): + if page_name not in _page_names: + GlobalLogger.logs("Tried to display an invalid login route.", 3) + return + + for page in create.get_children(): + if page.name == page_name: + page.visible = true + continue + + page.visible = false + return + +func _get_pages() -> void: + for page in create.get_children(): + _page_names.append(page.name) + return + +func _create_oauth() -> void: + var display_name = get_node("Create/OAuth/Create/VBoxContainer3/CADisplayName").text + var account_server = get_node("Create/OAuth/Create/VBoxContainer3/CAAccountServer").text + + var account = { + "display_name": display_name, + "account_server": account_server + } + + var res = GlobalAccount.create(account, "oauth") + + Events.emit_signal("dash_switch_tab", "AccountDisplay") + + return diff --git a/src/userinterface/dash/account_create.gd.uid b/src/userinterface/dash/account_create.gd.uid new file mode 100644 index 0000000..1866a83 --- /dev/null +++ b/src/userinterface/dash/account_create.gd.uid @@ -0,0 +1 @@ +uid://cchqs4c1p1prd diff --git a/src/userinterface/dash/account_list.gd b/src/userinterface/dash/account_list.gd new file mode 100644 index 0000000..14e47aa --- /dev/null +++ b/src/userinterface/dash/account_list.gd @@ -0,0 +1,69 @@ +# --- License +# File: /client/src/userinterface/dash/account_list.gd +# Project: OpenMinerva +# Created Date: 28 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control + +@onready var _account_template = get_node("Templates/Account") +@onready var _account_list = get_node("List/ScrollContainer/AccountList") +@onready var _create_account_button = get_node("List/HBoxContainer/NewAccount") + +func _ready(): + _display_account_lists() + _create_account_button.pressed.connect(Events.emit_signal.bind("dash_switch_tab", "AccountCreate")) + + Events.dash_set_state.connect(_handle_dash_set_state) + Events.dash_account_list_loaded.connect(_handle_account_list_loaded) + return + +func _clear_account_listings() -> void: + for child in _account_list.get_children(): + child.queue_free() + return + +func _handle_dash_set_state(state: bool) -> void: + if state == false: + return + + _clear_account_listings() + _display_account_lists() + return + +func _handle_account_list_loaded() -> void: + _clear_account_listings() + _display_account_lists() + return + +func _display_account_lists() -> void: + var _list = GlobalAccount.get_all() + + if len(_list) == 0: + GlobalLogger.logs("No accounts to display.") + return + + for account in _list: + var _account_listing = _account_template.duplicate() + + var _username_node = _account_listing.get_node("MarginContainer/HBoxContainer/VBoxContainer/Username") + var _account_server_node = _account_listing.get_node("MarginContainer/HBoxContainer/VBoxContainer/AccountServer") + var _login_button = _account_listing.get_node("MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/Login") + var _configure_button = _account_listing.get_node("MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/Configure") + var _remove_button = _account_listing.get_node("MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/Remove") + + # Display + if account.type == "oauth": + _username_node.text = account.display_name + _account_server_node.text = account.account_server + + # Event listeners + _login_button.pressed.connect(GlobalAccount.use.bind(account.id)) + _remove_button.pressed.connect(GlobalAccount.remove.bind(account.id)) + + _account_list.add_child(_account_listing) + GlobalLogger.logs("Added account '%s' to the login list." % account.id) + return diff --git a/src/userinterface/dash/account_list.gd.uid b/src/userinterface/dash/account_list.gd.uid new file mode 100644 index 0000000..e8d0fca --- /dev/null +++ b/src/userinterface/dash/account_list.gd.uid @@ -0,0 +1 @@ +uid://ftk8qksc10qg diff --git a/src/userinterface/dash/apps.gd b/src/userinterface/dash/apps.gd new file mode 100644 index 0000000..2f8863f --- /dev/null +++ b/src/userinterface/dash/apps.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/apps.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control \ No newline at end of file diff --git a/src/userinterface/dash/apps.gd.uid b/src/userinterface/dash/apps.gd.uid new file mode 100644 index 0000000..9526fc4 --- /dev/null +++ b/src/userinterface/dash/apps.gd.uid @@ -0,0 +1 @@ +uid://di7li0digjpa8 diff --git a/src/userinterface/dash/contacts.gd b/src/userinterface/dash/contacts.gd new file mode 100644 index 0000000..c54ac2a --- /dev/null +++ b/src/userinterface/dash/contacts.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/contacts.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control diff --git a/src/userinterface/dash/contacts.gd.uid b/src/userinterface/dash/contacts.gd.uid new file mode 100644 index 0000000..5468a3a --- /dev/null +++ b/src/userinterface/dash/contacts.gd.uid @@ -0,0 +1 @@ +uid://crlujtfv2bh1w diff --git a/src/userinterface/dash/debug.gd b/src/userinterface/dash/debug.gd new file mode 100644 index 0000000..3c4ba87 --- /dev/null +++ b/src/userinterface/dash/debug.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/debug.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control \ No newline at end of file diff --git a/src/userinterface/dash/debug.gd.uid b/src/userinterface/dash/debug.gd.uid new file mode 100644 index 0000000..669c34b --- /dev/null +++ b/src/userinterface/dash/debug.gd.uid @@ -0,0 +1 @@ +uid://bwcvodv6u21jm diff --git a/src/userinterface/dash/exit.gd b/src/userinterface/dash/exit.gd new file mode 100644 index 0000000..4bfd48b --- /dev/null +++ b/src/userinterface/dash/exit.gd @@ -0,0 +1,29 @@ +# --- License +# File: /client/src/userinterface/dash/exit.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control +@onready var _cancel_button = get_node("VBoxContainer/HBoxContainer/Cancel") +@onready var _exit_button = get_node("VBoxContainer/HBoxContainer/Exit") + +func _ready(): + _cancel_button.pressed.connect(_handle_cancel_pressed) + _exit_button.pressed.connect(_handle_exit_pressed) + return + +func _handle_cancel_pressed(): + Events.emit_signal("dash_switch_tab", "Home") + return + +func _handle_exit_pressed(): + # TODO: Save? + # TODO: Sync? + # TODO: Validate database? + # TODO: Prune cache? + get_tree().quit() + return \ No newline at end of file diff --git a/src/userinterface/dash/exit.gd.uid b/src/userinterface/dash/exit.gd.uid new file mode 100644 index 0000000..6bf2d79 --- /dev/null +++ b/src/userinterface/dash/exit.gd.uid @@ -0,0 +1 @@ +uid://e1djdo6st2a7 diff --git a/src/userinterface/dash/home.gd b/src/userinterface/dash/home.gd new file mode 100644 index 0000000..e765a11 --- /dev/null +++ b/src/userinterface/dash/home.gd @@ -0,0 +1,37 @@ +# --- License +# File: /client/src/userinterface/dash/home.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control + +@onready var account_card_container = get_node("HBoxContainer/VBoxContainer/AccountDisplay") +@onready var storage_card_container = get_node("HBoxContainer/VBoxContainer/StorageDisplay") +@onready var session_card_container = get_node("HBoxContainer/VBoxContainer3/SessionDisplay") + +func _ready(): + account_card_container.get_node("Button").pressed.connect(Events.emit_signal.bind("dash_switch_tab", "AccountDisplay")) + + Events.connect("dash_active_account_changed", _handle_active_account_changed) + Events.connect("dash_storage_changed", _handle_storage_changed) + Events.connect("dash_session_changed", _handle_session_changed) + + return + +func _handle_active_account_changed(account: Dictionary) -> void: + account_card_container.get_node("MarginContainer/HBoxContainer/VBoxContainer/Username").text = account.get("username") if account.get("username") else account.get("display_name") + account_card_container.get_node("MarginContainer/HBoxContainer/VBoxContainer/AccountServer").text = account.account_server + return + +func _handle_storage_changed(storage_data: Dictionary) -> void: + storage_card_container.get_node("MarginContainer/VBoxContainer/ProgressBar").value = storage_data.used_percent + storage_card_container.get_node("MarginContainer/VBoxContainer/Label2").text = "%s GiB used of %s GiB" % [storage_data.used_gigs, storage_data.total_gigs] + return + +func _handle_session_changed(session_data: Dictionary) -> void: + session_card_container.get_node("MarginContainer/HBoxContainer/VBoxContainer/SessionName").text = session_data.session_name + return diff --git a/src/userinterface/dash/home.gd.uid b/src/userinterface/dash/home.gd.uid new file mode 100644 index 0000000..b2c4911 --- /dev/null +++ b/src/userinterface/dash/home.gd.uid @@ -0,0 +1 @@ +uid://bwcgqn33pn62o diff --git a/src/userinterface/dash/hud.tscn b/src/userinterface/dash/hud.tscn new file mode 100644 index 0000000..4077f9c --- /dev/null +++ b/src/userinterface/dash/hud.tscn @@ -0,0 +1,2851 @@ +[gd_scene format=3 uid="uid://ckl5gw0xbduiv"] + +[ext_resource type="Texture2D" uid="uid://7mgxvhy58nhp" path="res://resources/icons/home.svg" id="1_3b2ci"] +[ext_resource type="Theme" uid="uid://bg2nbganyysst" path="res://openminerva_default.tres" id="1_cx7w0"] +[ext_resource type="Script" uid="uid://cbl7rmnjxarba" path="res://userinterface/dash/master.gd" id="1_ugu7k"] +[ext_resource type="Texture2D" uid="uid://coqi7w7inqyv1" path="res://resources/icons/account.svg" id="2_bucoy"] +[ext_resource type="Texture2D" uid="uid://crrjk7c2g4q55" path="res://resources/icons/art.svg" id="3_6vaiq"] +[ext_resource type="Script" uid="uid://bwcgqn33pn62o" path="res://userinterface/dash/home.gd" id="3_bnkjl"] +[ext_resource type="Texture2D" uid="uid://c7ofnclcof0ud" path="res://resources/icons/world.svg" id="3_v4ccx"] +[ext_resource type="StyleBox" uid="uid://cxx1q037xaswi" path="res://openminerva_darkpanel.tres" id="4_6rlgq"] +[ext_resource type="Texture2D" uid="uid://dfckge3u00boi" path="res://resources/icons/contacts.svg" id="4_cyduv"] +[ext_resource type="Texture2D" uid="uid://cnhy47i7saehq" path="res://resources/icons/environment.svg" id="4_tkvwg"] +[ext_resource type="Texture2D" uid="uid://rdp5i1i18jus" path="res://resources/icons/search.svg" id="4_wfski"] +[ext_resource type="Texture2D" uid="uid://fkfiymss8p57" path="res://resources/icons/science.svg" id="5_hu6eh"] +[ext_resource type="Texture2D" uid="uid://br6hpysqvuhek" path="res://resources/icons/inventory.svg" id="5_r82dk"] +[ext_resource type="Texture2D" uid="uid://blkl4og334med" path="res://resources/icons/dummy16-9.webp" id="5_tkvwg"] +[ext_resource type="Script" uid="uid://ftk8qksc10qg" path="res://userinterface/dash/account_list.gd" id="5_upsw6"] +[ext_resource type="Script" uid="uid://cchqs4c1p1prd" path="res://userinterface/dash/account_create.gd" id="6_3rk53"] +[ext_resource type="Texture2D" uid="uid://dp5u16rwxd0bp" path="res://resources/icons/apps.svg" id="6_6ybpt"] +[ext_resource type="Texture2D" uid="uid://b0qo67fgifqe8" path="res://resources/icons/circus.svg" id="6_hu6eh"] +[ext_resource type="Texture2D" uid="uid://basvqob80u3l6" path="res://resources/icons/settings.svg" id="7_tixj4"] +[ext_resource type="Script" uid="uid://ccd2fw88a52mp" path="res://userinterface/dash/sessions.gd" id="8_4ahgm"] +[ext_resource type="Texture2D" uid="uid://b8fed1h3aqw1s" path="res://resources/icons/debug.svg" id="8_fgqsp"] +[ext_resource type="Texture2D" uid="uid://8fwxg1ipf0fp" path="res://resources/icons/exit.svg" id="9_amc1p"] +[ext_resource type="Texture2D" uid="uid://ckoajckje5mq2" path="res://resources/icons/edit.svg" id="9_xdslj"] +[ext_resource type="Texture2D" uid="uid://ddchrocw45muh" path="res://resources/icons/flowchart.svg" id="12_hq11n"] +[ext_resource type="Script" uid="uid://crlujtfv2bh1w" path="res://userinterface/dash/contacts.gd" id="15_gll50"] +[ext_resource type="Script" uid="uid://e1djdo6st2a7" path="res://userinterface/dash/exit.gd" id="17_3rk53"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kgckq"] +draw_center = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6vaiq"] +bg_color = Color(0, 0, 0, 1) +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wfski"] +bg_color = Color(0, 0.73333335, 1, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bucoy"] +bg_color = Color(1, 0, 0, 1) +corner_radius_top_left = 100 +corner_radius_top_right = 100 +corner_radius_bottom_right = 100 +corner_radius_bottom_left = 100 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dr1q3"] +bg_color = Color(0, 0.73333335, 1, 1) +corner_radius_top_left = 100 +corner_radius_top_right = 100 +corner_radius_bottom_right = 100 +corner_radius_bottom_left = 100 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xdslj"] +bg_color = Color(2.466701e-07, 0.13600668, 0.20061767, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6rlgq"] +bg_color = Color(0.099985994, 0.09998601, 0.099985965, 1) + +[sub_resource type="Theme" id="Theme_q6jcb"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hq11n"] +bg_color = Color(0.60038817, 0.60038817, 0.60038817, 0) +draw_center = false + +[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_hq11n"] + +[node name="Hud" type="Control" unique_id=1053137144] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ugu7k") + +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1988156212] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1376103708] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Master" type="Panel" parent="MarginContainer/VBoxContainer" unique_id=911098732] +layout_mode = 2 +size_flags_vertical = 3 +theme = ExtResource("1_cx7w0") + +[node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=675278383] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("3_bnkjl") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home" unique_id=581087957] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer" unique_id=2138031361] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="AccountDisplay" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer" unique_id=383858349] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay" unique_id=2024761038] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer" unique_id=133275778] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Container" type="CenterContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer" unique_id=539177141] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer/Container" unique_id=1941945009] +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer/Container/AspectRatioContainer" unique_id=1382039916] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 +stretch_mode = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer" unique_id=1966375622] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Username" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer/VBoxContainer" unique_id=321531249] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_font_sizes/normal_font_size = 20 +bbcode_enabled = true +text = "[color=gray]No Account[/color]" +fit_content = true + +[node name="AccountServer" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay/MarginContainer/HBoxContainer/VBoxContainer" unique_id=881790720] +layout_mode = 2 +theme_override_colors/font_color = Color(0.7977378, 0.7977378, 0.7977378, 1) +theme_override_font_sizes/font_size = 14 +text = "https://accounts.openminerva.org" + +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay" unique_id=58113697] +layout_mode = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_kgckq") + +[node name="StorageDisplay" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer" unique_id=1169784328] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/StorageDisplay" unique_id=1944750319] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/StorageDisplay/MarginContainer" unique_id=1039592827] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/StorageDisplay/MarginContainer/VBoxContainer" unique_id=21628856] +layout_mode = 2 +text = "Used Storage" +horizontal_alignment = 1 + +[node name="ProgressBar" type="ProgressBar" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/StorageDisplay/MarginContainer/VBoxContainer" unique_id=1535856991] +layout_mode = 2 +theme_override_styles/background = SubResource("StyleBoxFlat_6vaiq") +theme_override_styles/fill = SubResource("StyleBoxFlat_wfski") +value = 25.0 + +[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/StorageDisplay/MarginContainer/VBoxContainer" unique_id=749951536] +layout_mode = 2 +text = "2.5 GiB / 5 GiB" +horizontal_alignment = 1 + +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer" unique_id=1873874176] +custom_minimum_size = Vector2(1000, 0) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer" unique_id=1863434109] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SessionDisplay" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3" unique_id=368133605] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay" unique_id=993828895] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer" unique_id=1881644944] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer" unique_id=2054814657] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="SessionName" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1586628075] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Session Name" + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1575315261] +layout_mode = 2 +theme_override_constants/separation = 25 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=408001748] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/HBoxContainer" unique_id=770415475] +custom_minimum_size = Vector2(20, 20) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_bucoy") + +[node name="AccountServer2" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/HBoxContainer" unique_id=415378036] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Spectator" + +[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=1848382474] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/HBoxContainer2" unique_id=1133926083] +custom_minimum_size = Vector2(20, 20) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_dr1q3") + +[node name="AccountServer2" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer3/SessionDisplay/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/HBoxContainer2" unique_id=36948057] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "157 ms" + +[node name="AccountDisplay" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=1554068867] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("5_upsw6") + +[node name="List" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay" unique_id=1462685937] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/List" unique_id=1122975855] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="AccountList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/List/ScrollContainer" unique_id=156940286] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/List" unique_id=1849303399] +layout_mode = 2 + +[node name="NewAccount" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/List/HBoxContainer" unique_id=1573695902] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +size_flags_horizontal = 3 +text = "New Account" + +[node name="Templates" type="Control" parent="MarginContainer/VBoxContainer/Master/AccountDisplay" unique_id=1889394597] +visible = false +layout_mode = 2 + +[node name="Account" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates" unique_id=1270586344] +layout_mode = 0 +offset_left = 690.0 +offset_right = 1190.0 +offset_bottom = 106.0 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account" unique_id=1691936720] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer" unique_id=877904178] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="AspectRatioContainer2" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer" unique_id=905758712] +custom_minimum_size = Vector2(75, 50) +layout_mode = 2 + +[node name="ProfilePicture" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/AspectRatioContainer2" unique_id=1092421664] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer" unique_id=1076341750] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Username" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1381439920] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Account username" + +[node name="AccountServer" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1458428898] +layout_mode = 2 +theme_override_colors/font_color = Color(0.54, 0.54, 0.54, 1) +theme_override_font_sizes/font_size = 14 +text = "https://accounts.openminerva.org" + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1566413473] +layout_mode = 2 + +[node name="Login" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=249423624] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Login" + +[node name="Configure" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=1395695382] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Configure" + +[node name="Remove" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountDisplay/Templates/Account/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer" unique_id=1778567672] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Remove" + +[node name="AccountCreate" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=476222424] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("6_3rk53") + +[node name="Create" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate" unique_id=1338768232] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_styles/panel = ExtResource("4_6rlgq") + +[node name="UsernamePassword" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create" unique_id=83231436] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Create" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword" unique_id=1027080501] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_constants/separation = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=44400410] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer" unique_id=1919638633] +layout_mode = 2 +text = "Username" + +[node name="CAUsername" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer" unique_id=925907901] +layout_mode = 2 +text = "u" +placeholder_text = "Username" + +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=1696617473] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer2" unique_id=1480122353] +layout_mode = 2 +text = "Password" + +[node name="CAPassword" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer2" unique_id=8617011] +layout_mode = 2 +text = "i" +placeholder_text = "Password" +secret = true + +[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=2026306086] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer3" unique_id=252202476] +layout_mode = 2 +text = "Account Server" + +[node name="CAAccountServer" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/VBoxContainer3" unique_id=783276815] +layout_mode = 2 +text = "http://localhost" +placeholder_text = "https://accounts.openminerva.org" + +[node name="CARememberMe" type="CheckButton" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=1627203869] +layout_mode = 2 +button_pressed = true +text = "Remember Me" +flat = true + +[node name="CALocalAccount" type="CheckButton" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=869022482] +layout_mode = 2 +text = "Local Account" +flat = true + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create" unique_id=700361537] +layout_mode = 2 + +[node name="CreateAccountBack" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/HBoxContainer" unique_id=2040294501] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Back" + +[node name="ConfirmCreateAccount" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/UsernamePassword/Create/HBoxContainer" unique_id=1706811869] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Create" + +[node name="OAuth" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create" unique_id=1033385490] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Create" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth" unique_id=1431764238] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_constants/separation = 10 + +[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create" unique_id=1971820391] +layout_mode = 2 + +[node name="DisplayName" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/VBoxContainer3" unique_id=1545576882] +layout_mode = 2 +text = "Display Name" + +[node name="CADisplayName" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/VBoxContainer3" unique_id=1868732970] +layout_mode = 2 +text = "Anonymous" +placeholder_text = "Display Name" + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/VBoxContainer3" unique_id=1010954308] +layout_mode = 2 +text = "Account Server" + +[node name="CAAccountServer" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/VBoxContainer3" unique_id=404007920] +layout_mode = 2 +text = "http://localhost:40400" +placeholder_text = "https://accounts.openminerva.org" + +[node name="CARememberMe" type="CheckButton" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create" unique_id=1759228467] +layout_mode = 2 +button_pressed = true +text = "Remember Me" +flat = true + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create" unique_id=162707587] +layout_mode = 2 + +[node name="CreateAccountBack" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/HBoxContainer" unique_id=729130015] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Back" + +[node name="ConfirmCreateAccount" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/OAuth/Create/HBoxContainer" unique_id=840318543] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Sign In" + +[node name="SelectMethod" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create" unique_id=1416964800] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="Container" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/SelectMethod" unique_id=1882060538] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_constants/separation = 10 + +[node name="OAuth" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/SelectMethod/Container" unique_id=2044513720] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +text = "OAuth2" + +[node name="UsernamePassword" type="Button" parent="MarginContainer/VBoxContainer/Master/AccountCreate/Create/SelectMethod/Container" unique_id=1669994196] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +disabled = true +text = "Username + Password" + +[node name="Sessions" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=2135749463] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("8_4ahgm") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions" unique_id=1830116526] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer" unique_id=687228975] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="Art" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer" unique_id=8787651] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Art" unique_id=1704752065] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Art/MarginContainer" unique_id=723755098] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Art/MarginContainer/HBoxContainer" unique_id=1631358950] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Art/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=421186792] +layout_mode = 2 +texture = ExtResource("3_6vaiq") +expand_mode = 1 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Art/MarginContainer/HBoxContainer" unique_id=1515682350] +layout_mode = 2 +text = "Art" + +[node name="Environment" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer" unique_id=1460467440] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Environment" unique_id=1960994802] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Environment/MarginContainer" unique_id=1078127539] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Environment/MarginContainer/HBoxContainer" unique_id=43665461] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Environment/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1826253390] +layout_mode = 2 +texture = ExtResource("4_tkvwg") +expand_mode = 1 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Environment/MarginContainer/HBoxContainer" unique_id=1155183315] +layout_mode = 2 +text = "Environment" + +[node name="Science" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer" unique_id=508195085] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Science" unique_id=1375241018] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Science/MarginContainer" unique_id=1476810827] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Science/MarginContainer/HBoxContainer" unique_id=588549540] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Science/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1091425181] +layout_mode = 2 +texture = ExtResource("5_hu6eh") +expand_mode = 1 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Science/MarginContainer/HBoxContainer" unique_id=1662831340] +layout_mode = 2 +text = "Science" + +[node name="Games" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer" unique_id=369382614] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Games" unique_id=1896102711] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Games/MarginContainer" unique_id=1907169258] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Games/MarginContainer/HBoxContainer" unique_id=2118476409] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Games/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1606136766] +layout_mode = 2 +texture = ExtResource("6_hu6eh") +expand_mode = 1 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer/Games/MarginContainer/HBoxContainer" unique_id=1728001482] +layout_mode = 2 +text = "Games" + +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer" unique_id=275712777] +custom_minimum_size = Vector2(1000, 0) +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 15 + +[node name="Search" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2" unique_id=917704848] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/Search" unique_id=726470197] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/Search/MarginContainer" unique_id=65014822] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/Search/MarginContainer/HBoxContainer" unique_id=903609424] +custom_minimum_size = Vector2(30, 0) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/Search/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1429839469] +layout_mode = 2 +texture = ExtResource("4_wfski") +expand_mode = 4 + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/Search/MarginContainer/HBoxContainer" unique_id=670617857] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Search..." +emoji_menu_enabled = false +clear_button_enabled = true +middle_mouse_paste_enabled = false +flat = true +caret_blink = true +icon_expand_mode = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2" unique_id=242318771] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="GridContainer" type="GridContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer" unique_id=164218999] +layout_mode = 2 +theme_override_constants/h_separation = 15 +theme_override_constants/v_separation = 15 +columns = 4 + +[node name="WorldListing" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=72097487] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing" unique_id=1805185846] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer" unique_id=1942216951] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer/AspectRatioContainer" unique_id=467585571] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer" unique_id=1711734675] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer/MarginContainer" unique_id=72959204] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=536349374] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2" unique_id=1668182649] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer" unique_id=2059453721] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer/AspectRatioContainer" unique_id=1701263601] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer" unique_id=969705443] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer/MarginContainer" unique_id=925302443] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1068422016] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3" unique_id=1171639239] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer" unique_id=1823498012] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer/AspectRatioContainer" unique_id=224427286] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer" unique_id=1983590670] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer/MarginContainer" unique_id=1182281080] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1272654877] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4" unique_id=44004522] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer" unique_id=684739377] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer/AspectRatioContainer" unique_id=775612232] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer" unique_id=2076427482] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer/MarginContainer" unique_id=1542369134] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing5" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=502721862] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5" unique_id=1422523028] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer" unique_id=2104633633] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer/AspectRatioContainer" unique_id=1340850999] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer" unique_id=1085130516] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer/MarginContainer" unique_id=1235572658] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing6" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=886702167] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6" unique_id=571121164] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer" unique_id=1647835786] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer/AspectRatioContainer" unique_id=644952396] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer" unique_id=630480358] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer/MarginContainer" unique_id=1591667899] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing7" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1793074987] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7" unique_id=1047423560] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer" unique_id=514583078] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer/AspectRatioContainer" unique_id=1677760804] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer" unique_id=1909700952] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer/MarginContainer" unique_id=732288817] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing8" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=774422407] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8" unique_id=1603622578] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer" unique_id=910974868] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer/AspectRatioContainer" unique_id=2089434396] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer" unique_id=118183461] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer/MarginContainer" unique_id=1355563408] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing9" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=41675036] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9" unique_id=1960680163] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer" unique_id=1178627687] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer/AspectRatioContainer" unique_id=1776544938] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer" unique_id=186850142] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer/MarginContainer" unique_id=1030586810] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing10" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=2084109082] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10" unique_id=225495079] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer" unique_id=376527793] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer/AspectRatioContainer" unique_id=1875807404] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer" unique_id=1306717136] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer/MarginContainer" unique_id=1142312026] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing11" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1953856482] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11" unique_id=1886181112] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer" unique_id=248643867] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer/AspectRatioContainer" unique_id=251176411] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer" unique_id=209004049] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer/MarginContainer" unique_id=1801139647] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing12" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=497402337] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12" unique_id=602024177] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer" unique_id=437664178] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer/AspectRatioContainer" unique_id=625417018] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer" unique_id=1397258169] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer/MarginContainer" unique_id=954774566] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing13" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1177446667] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13" unique_id=226169753] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer" unique_id=1228661081] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer/AspectRatioContainer" unique_id=1369580510] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer" unique_id=1763383998] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer/MarginContainer" unique_id=1551226535] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing14" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1483002573] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14" unique_id=1690659101] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer" unique_id=926780909] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer/AspectRatioContainer" unique_id=541609792] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer" unique_id=838350665] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer/MarginContainer" unique_id=516725602] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing15" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=422150807] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15" unique_id=1056594161] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer" unique_id=122088462] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer/AspectRatioContainer" unique_id=339376271] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer" unique_id=1196852953] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer/MarginContainer" unique_id=263662711] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing16" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=570486665] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16" unique_id=1216909486] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer" unique_id=1663707523] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer/AspectRatioContainer" unique_id=1455460820] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer" unique_id=2097737858] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer/MarginContainer" unique_id=199551998] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing17" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1480341877] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17" unique_id=173416367] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer" unique_id=1856884492] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer/AspectRatioContainer" unique_id=755706003] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer" unique_id=1890260584] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer/MarginContainer" unique_id=942789936] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing18" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=431108879] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18" unique_id=1854699654] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer" unique_id=147007928] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer/AspectRatioContainer" unique_id=834337291] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer" unique_id=195884175] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer/MarginContainer" unique_id=1704660755] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing19" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=113973206] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19" unique_id=856575337] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer" unique_id=2030015789] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer/AspectRatioContainer" unique_id=1203134025] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer" unique_id=796654597] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer/MarginContainer" unique_id=1022681998] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="WorldListing20" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1373407292] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20" unique_id=616061903] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer" unique_id=1272360847] +custom_minimum_size = Vector2(350, 210) +layout_mode = 2 +ratio = 1.7778 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer/AspectRatioContainer" unique_id=1244955368] +layout_mode = 2 +texture = ExtResource("5_tkvwg") +expand_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer" unique_id=1231885986] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 15 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer/MarginContainer" unique_id=7766220] +custom_minimum_size = Vector2(200, 20) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "Example test world that actuall really exists. Also another example of something being really cool!" +autowrap_mode = 3 +text_overrun_behavior = 3 +max_lines_visible = 2 + +[node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=1935681612] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("15_gll50") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts" unique_id=609852715] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="PeopleList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer" unique_id=495048038] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 + +[node name="Search" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList" unique_id=1778670362] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/Search" unique_id=1330638996] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/Search/MarginContainer" unique_id=552152395] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/Search/MarginContainer/HBoxContainer" unique_id=1034644707] +custom_minimum_size = Vector2(30, 0) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/Search/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1362549755] +layout_mode = 2 +texture = ExtResource("4_wfski") +expand_mode = 4 + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/Search/MarginContainer/HBoxContainer" unique_id=532774583] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Search..." +emoji_menu_enabled = false +clear_button_enabled = true +middle_mouse_paste_enabled = false +flat = true +caret_blink = true +icon_expand_mode = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList" unique_id=1230683167] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="PeopleContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer" unique_id=1138665112] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Contact" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1376762730] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact" unique_id=1957164778] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer" unique_id=530564555] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer/HBoxContainer" unique_id=2037557771] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=213812101] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer/HBoxContainer" unique_id=123418873] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1963379470] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1790589234] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1917384234] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2" unique_id=248931094] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer" unique_id=1105136801] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer/HBoxContainer" unique_id=1042937322] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=945263631] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer/HBoxContainer" unique_id=72060891] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1790458992] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact2/MarginContainer/HBoxContainer/VBoxContainer" unique_id=655308943] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1002559792] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3" unique_id=2110329168] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer" unique_id=587970321] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer/HBoxContainer" unique_id=1155569374] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=363334467] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer/HBoxContainer" unique_id=1291298320] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer/HBoxContainer/VBoxContainer" unique_id=58329549] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact3/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1652527413] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1117539841] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4" unique_id=882936545] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer" unique_id=1983831238] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer/HBoxContainer" unique_id=1797575450] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1904659656] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer/HBoxContainer" unique_id=1195268824] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1911785678] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact4/MarginContainer/HBoxContainer/VBoxContainer" unique_id=673745301] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact5" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=785320451] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5" unique_id=488007093] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer" unique_id=1702189485] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer/HBoxContainer" unique_id=181066723] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=858326702] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer/HBoxContainer" unique_id=1622164735] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1525652223] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact5/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1764447998] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact6" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1223741083] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6" unique_id=1886365158] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer" unique_id=1000938433] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer/HBoxContainer" unique_id=1412410708] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1838408715] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer/HBoxContainer" unique_id=602696783] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer/HBoxContainer/VBoxContainer" unique_id=266752489] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact6/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1455339639] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact7" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=285355074] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7" unique_id=496124961] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer" unique_id=2125957011] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer/HBoxContainer" unique_id=1679355086] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1263285591] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer/HBoxContainer" unique_id=1944026472] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1929228588] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact7/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1247873814] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact8" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1645072851] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8" unique_id=778625893] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer" unique_id=2038217778] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer/HBoxContainer" unique_id=1122556879] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1092189046] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer/HBoxContainer" unique_id=1922829072] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer/HBoxContainer/VBoxContainer" unique_id=584730902] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact8/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1752589710] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact9" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=2101167709] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9" unique_id=618543179] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer" unique_id=1966106535] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer/HBoxContainer" unique_id=2145573781] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1400837203] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer/HBoxContainer" unique_id=300879442] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1887730979] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact9/MarginContainer/HBoxContainer/VBoxContainer" unique_id=591472536] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact10" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1338233000] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10" unique_id=1193164329] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer" unique_id=1302175277] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer/HBoxContainer" unique_id=547714522] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=207327638] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer/HBoxContainer" unique_id=292040170] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer/HBoxContainer/VBoxContainer" unique_id=800144392] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact10/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1643413008] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact11" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1413470422] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11" unique_id=2134138405] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer" unique_id=1392258180] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer/HBoxContainer" unique_id=643752280] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1015674972] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer/HBoxContainer" unique_id=157488074] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1826758548] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact11/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1006838147] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact12" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=758373763] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12" unique_id=182319459] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer" unique_id=1225511965] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer/HBoxContainer" unique_id=833329707] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=288160845] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer/HBoxContainer" unique_id=956311638] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer/HBoxContainer/VBoxContainer" unique_id=291815739] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact12/MarginContainer/HBoxContainer/VBoxContainer" unique_id=142605013] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact13" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=110651033] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13" unique_id=722964887] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer" unique_id=1681556310] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer/HBoxContainer" unique_id=1064996853] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=273745914] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer/HBoxContainer" unique_id=4372347] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1378094667] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact13/MarginContainer/HBoxContainer/VBoxContainer" unique_id=959498741] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact14" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=1765333158] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14" unique_id=1834888416] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer" unique_id=1667692749] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer/HBoxContainer" unique_id=296732179] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=246164239] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer/HBoxContainer" unique_id=1729440618] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1697000745] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact14/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1424300167] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact15" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=608987246] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15" unique_id=68254041] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer" unique_id=799897191] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer/HBoxContainer" unique_id=1562246438] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1995651580] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer/HBoxContainer" unique_id=1071607904] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1418212751] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact15/MarginContainer/HBoxContainer/VBoxContainer" unique_id=345858069] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Contact16" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer" unique_id=976707612] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16" unique_id=1155713213] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer" unique_id=1444756375] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer/HBoxContainer" unique_id=1973316902] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=235764742] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer/HBoxContainer" unique_id=550050191] +layout_mode = 2 + +[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1145402858] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +text = "Contact Username" + +[node name="OnlineStatus" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/PeopleList/ScrollContainer/PeopleContainer/Contact16/MarginContainer/HBoxContainer/VBoxContainer" unique_id=1572016901] +layout_mode = 2 +theme_override_colors/font_color = Color(0.48570347, 0.48570353, 0.48570347, 1) +theme_override_font_sizes/font_size = 14 +text = "Offline" + +[node name="Messages" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer" unique_id=847433634] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 10 + +[node name="ColorRect" type="ColorRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages" unique_id=478337139] +layout_mode = 2 +size_flags_vertical = 3 +color = Color(0, 0, 0, 0.1) + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect" unique_id=2057620318] +process_mode = 3 +process_thread_group = 2 +process_thread_group_order = 0 +process_thread_messages = 0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer" unique_id=385628332] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer" unique_id=87127683] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 10 + +[node name="OurMessage" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=2017134431] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage" unique_id=227036190] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage/MarginContainer" unique_id=1039151222] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="OurMessage2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=890209952] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage2" unique_id=1387435291] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage2/MarginContainer" unique_id=309283686] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="ContactMessage3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=1175570338] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_styles/panel = SubResource("StyleBoxFlat_6rlgq") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage3" unique_id=1197627479] +layout_mode = 2 +size_flags_horizontal = 0 +theme = SubResource("Theme_q6jcb") +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage3/MarginContainer" unique_id=1326683365] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="ContactMessage4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=374331530] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_styles/panel = SubResource("StyleBoxFlat_6rlgq") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage4" unique_id=624597713] +layout_mode = 2 +size_flags_horizontal = 0 +theme = SubResource("Theme_q6jcb") +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage4/MarginContainer" unique_id=1957773675] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="OurMessage3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=1739574120] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage3" unique_id=1666305266] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage3/MarginContainer" unique_id=1972401865] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="OurMessage4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=851557100] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage4" unique_id=16272845] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage4/MarginContainer" unique_id=1788643846] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="ContactMessage5" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=1504758173] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_styles/panel = SubResource("StyleBoxFlat_6rlgq") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage5" unique_id=1387589042] +layout_mode = 2 +size_flags_horizontal = 0 +theme = SubResource("Theme_q6jcb") +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/ContactMessage5/MarginContainer" unique_id=493655798] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="OurMessage5" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=1120772320] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage5" unique_id=1684659846] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage5/MarginContainer" unique_id=303214310] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="OurMessage6" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer" unique_id=903773050] +layout_mode = 2 +size_flags_horizontal = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_xdslj") + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage6" unique_id=200480297] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/ColorRect/ScrollContainer/MarginContainer/VBoxContainer/OurMessage6/MarginContainer" unique_id=1921158391] +custom_minimum_size = Vector2(800, 0) +layout_mode = 2 +size_flags_horizontal = 0 +focus_mode = 2 +bbcode_enabled = true +text = "This is a dummy text message made to simulate an actual message... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula." +fit_content = true +scroll_active = false +selection_enabled = true + +[node name="Entry" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages" unique_id=657008232] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/Entry" unique_id=1730343636] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/Entry/MarginContainer" unique_id=2107826533] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/Entry/MarginContainer/HBoxContainer" unique_id=1490241753] +custom_minimum_size = Vector2(30, 0) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/Entry/MarginContainer/HBoxContainer/AspectRatioContainer" unique_id=1577549022] +layout_mode = 2 +texture = ExtResource("9_xdslj") +expand_mode = 4 + +[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Messages/Entry/MarginContainer/HBoxContainer" unique_id=1536400606] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/focus = SubResource("StyleBoxFlat_hq11n") +placeholder_text = "Send a message..." +emoji_menu_enabled = false +clear_button_enabled = true +middle_mouse_paste_enabled = false +flat = true +caret_blink = true +icon_expand_mode = 1 + +[node name="Bio" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer" unique_id=1343094501] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio" unique_id=501129495] +layout_mode = 2 + +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/HBoxContainer" unique_id=584912787] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/HBoxContainer/AspectRatioContainer" unique_id=917984378] +layout_mode = 2 +texture = ExtResource("2_bucoy") +expand_mode = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/HBoxContainer" unique_id=1940231142] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/HBoxContainer/VBoxContainer" unique_id=1117926921] +layout_mode = 2 +theme_override_font_sizes/font_size = 22 +text = "My contact" + +[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/HBoxContainer/VBoxContainer" unique_id=1343091322] +layout_mode = 2 +theme_override_colors/font_color = Color(0.6651851, 0.6651851, 0.6651851, 1) +text = "https://accounts.openminerva.org" + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio" unique_id=526473658] +layout_mode = 2 +size_flags_vertical = 3 +text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vulputate enim at scelerisque accumsan. Ut aliquam sapien sem, venenatis ullamcorper felis pellentesque vitae. Nulla blandit dictum turpis, id viverra metus fermentum quis. Nulla dictum eros at lorem porta mattis. Nam rutrum et lacus vitae vehicula. Quisque dictum diam nec felis aliquet auctor. Morbi placerat enim at finibus convallis. Curabitur non convallis mi, interdum ultricies augue. + +Ut eget varius libero, vitae porta ipsum. Integer sapien dui, ornare et semper et, sagittis sed libero. Nullam volutpat tempus nunc, id semper orci. Maecenas egestas tincidunt justo at ultricies. Vivamus ut nisi vestibulum, cursus dui non, ullamcorper sem. Cras sed ligula ante. Morbi eget pretium mi. Nullam iaculis sed ligula ut fermentum. Aenean cursus felis nibh, nec malesuada ante bibendum sed. Sed sed congue nunc. Duis purus odio, efficitur in eros sit amet, aliquam laoreet est. Interdum et malesuada fames ac ante ipsum primis in faucibus. Morbi fringilla mattis pharetra. Suspendisse ornare sagittis velit, sit amet rhoncus velit. " +fit_content = true + +[node name="GridContainer" type="GridContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio" unique_id=485642944] +layout_mode = 2 +theme_override_constants/h_separation = 10 +theme_override_constants/v_separation = 10 +columns = 2 + +[node name="Button" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer" unique_id=1404765838] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button" unique_id=1507499146] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button/MarginContainer" unique_id=89047822] +layout_mode = 2 +text = "Add Contact" +horizontal_alignment = 1 + +[node name="Button2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer" unique_id=873002479] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button2" unique_id=1360585472] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button2/MarginContainer" unique_id=155640440] +layout_mode = 2 +text = "Add Friend" +horizontal_alignment = 1 + +[node name="Button3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer" unique_id=343372836] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button3" unique_id=1968315918] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button3/MarginContainer" unique_id=1104766121] +layout_mode = 2 +text = "Block User" +horizontal_alignment = 1 + +[node name="Button4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer" unique_id=2134122873] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button4" unique_id=193076607] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Contacts/HBoxContainer/Bio/GridContainer/Button4/MarginContainer" unique_id=1437723678] +layout_mode = 2 +text = "Block Avatar" +horizontal_alignment = 1 + +[node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=719734468] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("17_3rk53") + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Exit" unique_id=143476327] +layout_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Master/Exit/VBoxContainer" unique_id=1484282659] +layout_mode = 2 +size_flags_vertical = 3 +bbcode_enabled = true +text = "[font_size=40]Thank you for using OpenMinerva![/font_size] + +[font_size=30]Come again soon![/font_size]" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Exit/VBoxContainer" unique_id=611012192] +custom_minimum_size = Vector2(0, 100) +layout_mode = 2 + +[node name="Cancel" type="Button" parent="MarginContainer/VBoxContainer/Master/Exit/VBoxContainer/HBoxContainer" unique_id=1332748794] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 26 +text = "Cancel" + +[node name="Exit" type="Button" parent="MarginContainer/VBoxContainer/Master/Exit/VBoxContainer/HBoxContainer" unique_id=1558278195] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 26 +text = "Exit" + +[node name="NavBar" type="Panel" parent="MarginContainer/VBoxContainer" unique_id=1428960291] +custom_minimum_size = Vector2(0, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/NavBar" unique_id=1728990786] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -37.5 +offset_top = -37.5 +offset_right = 37.5 +offset_bottom = 37.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 10 + +[node name="Home" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=344464958] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home" unique_id=2077541755] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer" unique_id=2146637607] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer/VBoxContainer" unique_id=95810640] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer/VBoxContainer/CenterContainer" unique_id=1070773413] +layout_mode = 2 +texture = ExtResource("1_3b2ci") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer/VBoxContainer" unique_id=1142003763] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer/VBoxContainer/CenterContainer2" unique_id=862944763] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Home/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1897372055] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Home" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Sessions" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=1779312894] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions" unique_id=1414128034] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer" unique_id=1222935685] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer/VBoxContainer" unique_id=1794611439] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer/VBoxContainer/CenterContainer" unique_id=1479063914] +layout_mode = 2 +texture = ExtResource("12_hq11n") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer/VBoxContainer" unique_id=739110456] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1283313696] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Sessions/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1726576248] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Sessions" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Instance" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=1199197758] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance" unique_id=1968696261] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer" unique_id=524033554] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer/VBoxContainer" unique_id=1551725689] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer/VBoxContainer/CenterContainer" unique_id=1325848905] +layout_mode = 2 +texture = ExtResource("3_v4ccx") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer/VBoxContainer" unique_id=1007113927] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1992088652] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Instance/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1993490483] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Instance" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Contacts" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=2052926333] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts" unique_id=247880075] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer" unique_id=1493676986] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer/VBoxContainer" unique_id=993865409] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer/VBoxContainer/CenterContainer" unique_id=939575854] +layout_mode = 2 +texture = ExtResource("4_cyduv") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer/VBoxContainer" unique_id=327218435] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1753921793] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Contacts/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1239993332] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Contacts" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Inventory" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=1154242515] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory" unique_id=477611878] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer" unique_id=1029784273] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer/VBoxContainer" unique_id=1434684623] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer/VBoxContainer/CenterContainer" unique_id=355063981] +layout_mode = 2 +texture = ExtResource("5_r82dk") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer/VBoxContainer" unique_id=150216954] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer/VBoxContainer/CenterContainer2" unique_id=2129474918] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Inventory/CenterContainer/VBoxContainer/CenterContainer2" unique_id=436784442] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Inventory" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Apps" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=2035455386] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps" unique_id=1234174942] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer" unique_id=2137480982] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer/VBoxContainer" unique_id=1172100987] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer/VBoxContainer/CenterContainer" unique_id=1104376878] +layout_mode = 2 +texture = ExtResource("6_6ybpt") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer/VBoxContainer" unique_id=1849886021] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer/VBoxContainer/CenterContainer2" unique_id=573515185] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Apps/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1090534310] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Apps" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Settings" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=326520049] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings" unique_id=803794329] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer" unique_id=639771866] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer/VBoxContainer" unique_id=42256113] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer/VBoxContainer/CenterContainer" unique_id=121107602] +layout_mode = 2 +texture = ExtResource("7_tixj4") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer/VBoxContainer" unique_id=301796775] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1389137151] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Settings/CenterContainer/VBoxContainer/CenterContainer2" unique_id=463454723] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Settings" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Debug" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=239205402] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug" unique_id=2128143430] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer" unique_id=189425462] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer/VBoxContainer" unique_id=1205603038] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer/VBoxContainer/CenterContainer" unique_id=1576187531] +layout_mode = 2 +texture = ExtResource("8_fgqsp") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer/VBoxContainer" unique_id=1369920063] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer/VBoxContainer/CenterContainer2" unique_id=229165988] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Debug/CenterContainer/VBoxContainer/CenterContainer2" unique_id=305966204] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Debug" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Exit" type="Button" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer" unique_id=1604895221] +custom_minimum_size = Vector2(75, 75) +layout_mode = 2 +theme = ExtResource("1_cx7w0") +toggle_mode = true + +[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit" unique_id=1519067308] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer" unique_id=36849769] +layout_mode = 2 +theme_override_constants/separation = 2 + +[node name="CenterContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer/VBoxContainer" unique_id=1840012224] +custom_minimum_size = Vector2(50, 50) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer/VBoxContainer/CenterContainer" unique_id=806908628] +layout_mode = 2 +texture = ExtResource("9_amc1p") +expand_mode = 1 + +[node name="CenterContainer2" type="CenterContainer" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer/VBoxContainer" unique_id=817841049] +custom_minimum_size = Vector2(0, 15) +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer/VBoxContainer/CenterContainer2" unique_id=71500259] +layout_mode = 2 +texture = SubResource("CompressedTexture2D_hq11n") +expand_mode = 5 +stretch_mode = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/NavBar/HBoxContainer/Exit/CenterContainer/VBoxContainer/CenterContainer2" unique_id=1611628376] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +text = "Exit" +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/src/userinterface/dash/instance.gd b/src/userinterface/dash/instance.gd new file mode 100644 index 0000000..4e048d2 --- /dev/null +++ b/src/userinterface/dash/instance.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/instance.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control \ No newline at end of file diff --git a/src/userinterface/dash/instance.gd.uid b/src/userinterface/dash/instance.gd.uid new file mode 100644 index 0000000..1f83515 --- /dev/null +++ b/src/userinterface/dash/instance.gd.uid @@ -0,0 +1 @@ +uid://dpo2x4ddv3ftt diff --git a/src/userinterface/dash/inventory.gd b/src/userinterface/dash/inventory.gd new file mode 100644 index 0000000..25fef36 --- /dev/null +++ b/src/userinterface/dash/inventory.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/inventory.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control \ No newline at end of file diff --git a/src/userinterface/dash/inventory.gd.uid b/src/userinterface/dash/inventory.gd.uid new file mode 100644 index 0000000..68f6055 --- /dev/null +++ b/src/userinterface/dash/inventory.gd.uid @@ -0,0 +1 @@ +uid://c25o2e0f3y6yx diff --git a/src/userinterface/dash/master.gd b/src/userinterface/dash/master.gd new file mode 100644 index 0000000..1cc8927 --- /dev/null +++ b/src/userinterface/dash/master.gd @@ -0,0 +1,59 @@ +# --- License +# File: /client/src/userinterface/dash/master.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control + +var dashboard_tabs = [] +var dashboard_tab_names = [] + +@onready var dash_tab_master_container = get_node("MarginContainer/VBoxContainer/Master") +@onready var dash_nav_master_container = get_node("MarginContainer/VBoxContainer/NavBar/HBoxContainer") + +func _ready(): + _build_page_list() + + Events.connect("dash_set_state", _handle_set_dash_state) + Events.connect("dash_switch_tab", _handle_switch_tab) + + Events.emit_signal("dash_switch_tab", "Home") + +func _build_page_list(): + for child in get_node("MarginContainer/VBoxContainer/Master").get_children(): + child.add_to_group("_dashboard_pages") + dashboard_tab_names.append(child.name) + + for button in get_node("MarginContainer/VBoxContainer/NavBar/HBoxContainer").get_children(): + if button.name not in dashboard_tab_names: + button.disabled = true + continue + button.pressed.connect(_handle_switch_tab.bind(button.name)) + +func _handle_set_dash_state(is_open: bool) -> void: + GlobalLogger.logs("Changing dashboard state: '%s'" % is_open) + visible = is_open + +func _handle_switch_tab(target_name: String) -> void: + GlobalLogger.logs("Switching dashboard to page '%s'" % target_name) + + for dash_tab in dash_tab_master_container.get_children(): + dash_tab.visible = false + + for dash_nav_button in dash_nav_master_container.get_children(): + dash_nav_button.button_pressed = false + + if target_name not in dashboard_tab_names: + GlobalLogger.logs("Tried to switch to an invalid dashboard page: '%s'" % target_name, 2) + return + + dash_tab_master_container.get_node(target_name).visible = true + + if dash_nav_master_container.get_node(target_name): + dash_nav_master_container.get_node(target_name).button_pressed = true + + return diff --git a/src/userinterface/dash/master.gd.uid b/src/userinterface/dash/master.gd.uid new file mode 100644 index 0000000..73601b6 --- /dev/null +++ b/src/userinterface/dash/master.gd.uid @@ -0,0 +1 @@ +uid://cbl7rmnjxarba diff --git a/src/userinterface/dash/sessions.gd b/src/userinterface/dash/sessions.gd new file mode 100644 index 0000000..8610b15 --- /dev/null +++ b/src/userinterface/dash/sessions.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/sessions.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control diff --git a/src/userinterface/dash/sessions.gd.uid b/src/userinterface/dash/sessions.gd.uid new file mode 100644 index 0000000..5cf606c --- /dev/null +++ b/src/userinterface/dash/sessions.gd.uid @@ -0,0 +1 @@ +uid://ccd2fw88a52mp diff --git a/src/userinterface/dash/settings.gd b/src/userinterface/dash/settings.gd new file mode 100644 index 0000000..49aad29 --- /dev/null +++ b/src/userinterface/dash/settings.gd @@ -0,0 +1,10 @@ +# --- License +# File: /client/src/userinterface/dash/settings.gd +# Project: OpenMinerva +# Created Date: 27 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Control \ No newline at end of file diff --git a/src/userinterface/dash/settings.gd.uid b/src/userinterface/dash/settings.gd.uid new file mode 100644 index 0000000..238acd9 --- /dev/null +++ b/src/userinterface/dash/settings.gd.uid @@ -0,0 +1 @@ +uid://cq26hbvdqb4sf diff --git a/src/userinterface/hud.gd b/src/userinterface/hud.gd deleted file mode 100644 index d159931..0000000 --- a/src/userinterface/hud.gd +++ /dev/null @@ -1,84 +0,0 @@ -extends Control - -@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") -var http = preload("res://scripts/network/http.gd").new() -var keys = preload("res://scripts/crypto/keys.gd").new() - -func _ready(): - while true: - await get_tree().create_timer(1).timeout - _update_hud_state() - -func _on_join_pressed(): - await network_manager.join_server("localhost") - -func set_active_state(state: bool = false): - visible = state - -func _update_hud_state(): - var user_list_formatted = network_manager.info.clients.map(func(elem): return elem.username) - %HostingBool.text = "Host: %s" % network_manager.status.hosting - %SessionHost.text = "Server Host: %s" % network_manager.info.clients[0].username - %ClientBool.text = "Client: %s" % network_manager.status.client - %ConnectedUserCount.text = "Total Users: %s" % len(network_manager.info.clients) - %UserList.text = "User List: %s" % ", ".join(user_list_formatted) - - -func _on_nav_exit_pressed(): - _show_dashboard_page("Exit") - -func _on_nav_debug_pressed(): - _show_dashboard_page("Debug") - -func _on_nav_inventory_pressed(): - _show_dashboard_page("Inventory") - -func _on_nav_contacts_pressed(): - _show_dashboard_page("Contacts") - -func _on_nav_sessions_pressed(): - _show_dashboard_page("Sessions") - -func _on_nav_home_pressed(): - _show_dashboard_page("Home") - -func _show_dashboard_page(page_name: String = "Home"): - if page_name not in ["Home", "Sessions", "Contacts", "Inventory", "Debug", "Exit"]: - GlobalLogger.logs("Tried to switch to an invalid dashboard page: '%s'" % page_name) - return - - for page in get_node("MarginContainer/VBoxContainer/PrimaryDashboard/").get_children(): - page.visible = false - - get_node("MarginContainer/VBoxContainer/PrimaryDashboard/%s" % page_name).visible = true - -func _on_login_button_pressed(): - var username_field_value = $"MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/UsernameField".text - var password_field_value = $"MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/PasswordField".text - var data = {"username": username_field_value, "password": password_field_value, "scope": "appAuth"} - # TODO: Make sure we have a keypair generated - - var keyPair = keys.read_keys_from_disk(username_field_value) - data["pubKey"] = keyPair[0] - - # TODO: Replace localhost with proper account server url + port - var response = await http.req(HTTPClient.Method.METHOD_POST, "http://localhost", "/api/v1/device/auth", 40400, ["Accept: application/json", "Content-Type: application/json"], JSON.stringify(data)) - - if response["ok"] == false: - GlobalLogger.logs("Response failed for unknown reason.", 1) - return - - if response["body"] == null: - GlobalLogger.logs("No body provided for login request.", 3) - return - - var res_body = JSON.parse_string(response["body"]) - - if "error" in res_body.keys(): - GlobalLogger.logs("Login request returned an error. '%s'" % res_body["error"], 1) - return - - var token = response["response_headers"]["Set-Cookie"].split("; ") - token[0] = token[0].replace("token=", "") - CredentialStore.set_account_credential(token) - _show_dashboard_page("Home") diff --git a/src/userinterface/hud.gd.uid b/src/userinterface/hud.gd.uid deleted file mode 100644 index 599cb9f..0000000 --- a/src/userinterface/hud.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://7j31we0siswq diff --git a/src/userinterface/hud.tscn b/src/userinterface/hud.tscn deleted file mode 100644 index 91755c5..0000000 --- a/src/userinterface/hud.tscn +++ /dev/null @@ -1,459 +0,0 @@ -[gd_scene load_steps=5 format=3 uid="uid://bdsc5kvle3jgd"] - -[ext_resource type="Script" uid="uid://7j31we0siswq" path="res://userinterface/hud.gd" id="1_go5o5"] -[ext_resource type="Texture2D" uid="uid://coqi7w7inqyv1" path="res://resources/icons/account.svg" id="2_lco6c"] - -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_tfojp"] - -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lco6c"] - -[node name="Hud" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -script = ExtResource("1_go5o5") - -[node name="MarginContainer" type="MarginContainer" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/margin_left = 50 -theme_override_constants/margin_top = 50 -theme_override_constants/margin_right = 50 -theme_override_constants/margin_bottom = 50 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] -layout_mode = 2 - -[node name="PrimaryDashboard" type="Panel" parent="MarginContainer/VBoxContainer"] -layout_mode = 2 -size_flags_vertical = 3 - -[node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="SignIn" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn"] -custom_minimum_size = Vector2(450, 250) -layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer"] -custom_minimum_size = Vector2(400, 0) -layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "Username" - -[node name="UsernameField" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -layout_mode = 2 -clear_button_enabled = true -caret_blink = true - -[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "Password" - -[node name="PasswordField" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -layout_mode = 2 -clear_button_enabled = true -secret = true - -[node name="Label3" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -layout_mode = 2 - -[node name="LoginButton" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer"] -unique_name_in_owner = true -layout_mode = 2 -text = "Login" - -[node name="Sessions" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts"] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] -layout_mode = 2 - -[node name="Search" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer"] -custom_minimum_size = Vector2(350, 50) -layout_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Search"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Search/MarginContainer"] -layout_mode = 2 -theme_override_styles/focus = SubResource("StyleBoxEmpty_tfojp") -placeholder_text = "Search for users" - -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer"] -custom_minimum_size = Vector2(350, 0) -layout_mode = 2 -size_flags_vertical = 3 - -[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel"] -custom_minimum_size = Vector2(350, 0) -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="UserContainer" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer"] -custom_minimum_size = Vector2(0, 75) -layout_mode = 2 -theme_override_styles/focus = SubResource("StyleBoxEmpty_lco6c") -text = " -" - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer"] -layout_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] -custom_minimum_size = Vector2(65, 65) -layout_mode = 2 -theme_override_constants/margin_left = 2 -theme_override_constants/margin_top = 2 -theme_override_constants/margin_right = 2 -theme_override_constants/margin_bottom = 2 - -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer"] -custom_minimum_size = Vector2(50, 50) -layout_mode = 2 -mouse_filter = 1 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer/MarginContainer/Panel/MarginContainer"] -custom_minimum_size = Vector2(50, 50) -layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 -texture = ExtResource("2_lco6c") -expand_mode = 5 -stretch_mode = 4 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/VBoxContainer/Panel/ScrollContainer/MarginContainer/VBoxContainer/UserContainer/MarginContainer/HBoxContainer"] -layout_mode = 2 -text = "Myself" - -[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -theme_override_constants/separation = 4 - -[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2"] -layout_mode = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="TextContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer2"] -layout_mode = 2 -size_flags_vertical = 3 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2"] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 5 - -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer"] -layout_mode = 2 - -[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel2/VBoxContainer2/MarginContainer/HBoxContainer"] -layout_mode = 2 -text = "Send" - -[node name="Panel" type="Panel" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer"] -custom_minimum_size = Vector2(350, 0) -layout_mode = 2 - -[node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Contacts/HBoxContainer/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="Inventory" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="Debug" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug"] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] -custom_minimum_size = Vector2(200, 0) -layout_mode = 2 - -[node name="Join" type="Button" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/VBoxContainer"] -layout_mode = 2 -text = "Join" - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="ServerList" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/MarginContainer"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Debug" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="HostingBool" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] -unique_name_in_owner = true -layout_mode = 2 -text = "Hosting: ?" - -[node name="SessionHost" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] -unique_name_in_owner = true -layout_mode = 2 -text = "Server Host: ?" - -[node name="ClientBool" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] -unique_name_in_owner = true -layout_mode = 2 -text = "Client: ?" - -[node name="ConnectedUserCount" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] -unique_name_in_owner = true -layout_mode = 2 -text = "Connected Users: ?" - -[node name="UserList" type="Label" parent="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/Debug"] -unique_name_in_owner = true -layout_mode = 2 -text = "User List: [?]" - -[node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/PrimaryDashboard"] -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="Panel2" type="Panel" parent="MarginContainer/VBoxContainer"] -custom_minimum_size = Vector2(0, 50) -layout_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Panel2"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_vertical = 3 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 - -[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer"] -layout_mode = 2 - -[node name="NavHome" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Home" - -[node name="NavSessions" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Sessions" - -[node name="NavContacts" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Contacts" - -[node name="NavInventory" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Inventory" - -[node name="NavDebug" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "DEBUG" - -[node name="NavExit" type="Button" parent="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer"] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Exit" - -[connection signal="pressed" from="MarginContainer/VBoxContainer/PrimaryDashboard/SignIn/Panel/MarginContainer/VBoxContainer/LoginButton" to="." method="_on_login_button_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/PrimaryDashboard/Debug/HBoxContainer/VBoxContainer/Join" to="." method="_on_join_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavHome" to="." method="_on_nav_home_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavSessions" to="." method="_on_nav_sessions_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavContacts" to="." method="_on_nav_contacts_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavInventory" to="." method="_on_nav_inventory_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavDebug" to="." method="_on_nav_debug_pressed"] -[connection signal="pressed" from="MarginContainer/VBoxContainer/Panel2/MarginContainer/HBoxContainer/NavExit" to="." method="_on_nav_exit_pressed"] From 6b11f354a8ab5917850ee31e08758ef74d10377a Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Tue, 31 Mar 2026 21:58:18 -0500 Subject: [PATCH 11/19] Fix player movement with an open menu. (#80) * Change movement to always calculate vector. This change makes it so that when the menu is opened, the player will stop being able to control the player, but the player can still slow down to a stop. * Remove movement FOV. * Fix dashboard not opening sometimes, or getting stuck. * Removed placeholder url. --- src/scenes/players/player.gd | 46 ++++++++++++++++++--------------- src/userinterface/dash/hud.tscn | 3 +-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd index 9bf7309..0b29c67 100644 --- a/src/scenes/players/player.gd +++ b/src/scenes/players/player.gd @@ -45,19 +45,24 @@ func _input(event): if is_multiplayer_authority() == false: return - if Input.is_action_just_pressed("escape"): + if event is InputEventMouseMotion && mouse_captured: + body.rotate_y(-event.relative.x * mouse_sensitivity * 0.001) + camera.rotate_x(-event.relative.y * mouse_sensitivity * 0.001) + camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-85), deg_to_rad(89)) + +func _unhandled_input(event): + if is_multiplayer_authority() == false: + return + + if event.is_action_pressed("escape"): if mouse_captured: capture_mouse(false) Events.emit_signal("dash_set_state", true) else: capture_mouse(true) Events.emit_signal("dash_set_state", false) - return - if event is InputEventMouseMotion && mouse_captured: - body.rotate_y(-event.relative.x * mouse_sensitivity * 0.001) - camera.rotate_x(-event.relative.y * mouse_sensitivity * 0.001) - camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-85), deg_to_rad(89)) + get_viewport().set_input_as_handled() func _physics_process(delta): if is_multiplayer_authority() == false: @@ -84,22 +89,21 @@ func _physics_process(delta): speed = speed / 1.1 # Get the input direction and handle the movement/deceleration. + var input_dir: Vector2 = Vector2(0, 0) + if mouse_captured == true: - var input_dir = Input.get_vector("left", "right", "forward", "backward") - var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() - if direction: - velocity.x = direction.x * speed - velocity.z = direction.z * speed - else: - velocity.x = lerp(velocity.x, direction.x * speed, delta * 20.0) - velocity.z = lerp(velocity.z, direction.z * speed, delta * 20.0) - - var velocity_clamped = clamp(velocity.length(), 0.5, SPRINT_SPEED * 2) - var target_fov = base_fov + fov_change * velocity_clamped - camera.fov = lerp(camera.fov, target_fov, delta * 8.0) - - if Input.is_action_just_pressed("jump") and is_on_floor(): - velocity.y = JUMP_VELOCITY + input_dir = Input.get_vector("left", "right", "forward", "backward") + + var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() + if direction: + velocity.x = direction.x * speed + velocity.z = direction.z * speed + else: + velocity.x = lerp(velocity.x, direction.x * speed, delta * 20.0) + velocity.z = lerp(velocity.z, direction.z * speed, delta * 20.0) + + if Input.is_action_just_pressed("jump") and is_on_floor(): + velocity.y = JUMP_VELOCITY move_and_slide() _send_player_synchronization_info() diff --git a/src/userinterface/dash/hud.tscn b/src/userinterface/dash/hud.tscn index 4077f9c..faebdd9 100644 --- a/src/userinterface/dash/hud.tscn +++ b/src/userinterface/dash/hud.tscn @@ -99,7 +99,6 @@ size_flags_vertical = 3 theme = ExtResource("1_cx7w0") [node name="Home" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=675278383] -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -164,7 +163,6 @@ fit_content = true layout_mode = 2 theme_override_colors/font_color = Color(0.7977378, 0.7977378, 0.7977378, 1) theme_override_font_sizes/font_size = 14 -text = "https://accounts.openminerva.org" [node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/AccountDisplay" unique_id=58113697] layout_mode = 2 @@ -2394,6 +2392,7 @@ text = "Block Avatar" horizontal_alignment = 1 [node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=719734468] +visible = false layout_mode = 0 anchor_right = 1.0 anchor_bottom = 1.0 From 9483d0a3a92aa4b59fc3414ff0a06e1ba1016e16 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Fri, 1 May 2026 13:45:20 -0500 Subject: [PATCH 12/19] Added README.md. --- README.md | 38 ++++++++++++++++++++++++++++++++++-- docs/logos/om-logo-big.webp | Bin 0 -> 153698 bytes 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 docs/logos/om-logo-big.webp diff --git a/README.md b/README.md index ad739ae..5f21aea 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ -# Prism -VR Social platform with a focus on Education +> [!WARNING] +> This repository is currently pre-alpha, breaking changes happen frequently, there have not been any security audits, and data loss is expected. + +

+ +

+ +# Client + +Client is the interface used to connect and interact with the virtual world of OpenMinerva. + +## Development Quick Start + +### 1: Download source code +Installation and running on Linux. +```bash +# Clone this repository. +git clone https://github.com/OpenMinerva/client +``` + +### 2: Import project into Godot. +2.a: Open your copy of the [Godot Engine](https://godotengine.org/). + +2.b: Click on "Import" near the top of the application window. + +2.c: Navigate to the location where the source code was downloaded to. + +2.d: Navigate to the subdirectory `/src`. + +2.e: Select `project.godot` to import this project. + +### 3: Open +Double click on the added entry. + +## Contributing +See [CONTRIBUTING.md](https://github.com/OpenMinerva/client/blob/alpha/CONTRIBUTING.md) for guidelines, and information. diff --git a/docs/logos/om-logo-big.webp b/docs/logos/om-logo-big.webp new file mode 100644 index 0000000000000000000000000000000000000000..04fe460692d54cab09766a004168a5d70e75d2b2 GIT binary patch literal 153698 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 literal 0 HcmV?d00001 From fffdfbe5a71c4728bd3b5cf8e748ea32b3b0e0c2 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Fri, 1 May 2026 15:27:50 -0500 Subject: [PATCH 13/19] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5f21aea..6c4197e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@

+[![](https://dcbadge.limes.pink/api/server/https://discord.gg/Kx6avB52gK)](https://discord.gg/Kx6avB52gK) + # Client Client is the interface used to connect and interact with the virtual world of OpenMinerva. From c7f5f446be95ff2fab5a866f139bad8d82af3342 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Mon, 4 May 2026 15:48:54 -0500 Subject: [PATCH 14/19] Multiplayer session hosting (#87) * Session listing function. * Basic session querying. * Fixed text capturing. * Instance HUD. * Added active session display. New network_manager to manage multiple sessions at once. * Added debug bug fix for crashing the game when not logged in when viewing the sessions tab. * Session advertising. * New application manager files. (#83) * Created new manager files. * Created base server scene. Created create_master_scene function. * Start multiplayer server. * Fixed dashboard error. Could not find active sessions because there was not a function. * Load scene root. * Force spawn the host. * Start server managers. * Added function name to logger. * Removed old files. * Fixed port finder bug. The function would never find a port, woopsie! * Initial session advertising work. * Removed unneeded Enum. * Initial heartbeat work. * Session Updating. Sessions can now update settings on the fly. * Session delisting. Remove sessions from the session server. * Settings placeholder. List session servers. * Remove session server from config. * Add session servers. * Don't add empty session servers. * Hide session server dialog in settings. * Use session server list to advertise sessions to. * Error fixes. Makes the debugger not angry with me. * Properly destroy master scene when creating a server failed. * Error handling. Removed some out of date TODOs. * Stop scene when replacing root. * Removed hardcoded localhost values used for debugging and development. * Moved TODO to it's own issue. https://github.com/OpenMinerva/client/issues/85 * Fixed heartbeats using incorrect ID. * Fixed port issues. * Remove dead code. * Attempt fix for rare network issue. * Do not request new key if we have key already. * Basic connection to multiplayer sessions. * Bootstrap for managing startup and shutdown. * Close and leave servers on shutdown. * Moved functions from app manager to session manager. * Add multiplayer support (#89) * Spawn player on connect. * Basic connected session switching. * Synchronization testing. * Maybe fix desync of loaded players? * Removed interaction ray from player controller. * Set active session from network. * Kick players when a session is closing. * Fixed all loggers to use Enums instead of magic numbers. * NetworkCompression autoload instead of hard referenced library. * Removed now unused RPC handlers. * Made HTTP library a global. * Fix errors. * Added session disconnect button. Fixed leaving world. * Minor refactor. * Fix player losing control of other sessions when connecting. * Clean up peer when they disconnect. * Changed node IDs. This was causing errors or something? * Change module active variable name from "active" to "module_active" to prevent confusion. * Fix controlling incorrect player when joining session. --- src/openminerva_default.tres | 2 +- src/project.godot | 8 +- src/scenes/levels/base.tscn | 22 + src/scenes/levels/{home.tscn => grid.tscn} | 2 +- src/scenes/managers/app/network.gd | 463 +++++++++ src/scenes/managers/app/network.gd.uid | 1 + src/scenes/managers/app/network.tscn | 6 + src/scenes/managers/app/network_manager.gd | 182 ---- .../managers/app/network_manager.gd.uid | 1 - src/scenes/managers/app/network_manager.tscn | 6 - src/scenes/managers/app/rpc_manager.gd | 16 - src/scenes/managers/app/rpc_manager.gd.uid | 1 - src/scenes/managers/app/rpc_manager.tscn | 6 - src/scenes/managers/app/scene.gd | 183 ++++ src/scenes/managers/app/scene.gd.uid | 1 + src/scenes/managers/app/scene.tscn | 6 + src/scenes/managers/app/scene_manager.gd | 47 - src/scenes/managers/app/scene_manager.gd.uid | 1 - src/scenes/managers/app/scene_manager.tscn | 6 - src/scenes/managers/scene/entity.gd | 1 + src/scenes/managers/scene/entity.gd.uid | 1 + src/scenes/managers/scene/network.gd | 92 ++ src/scenes/managers/scene/network.gd.uid | 1 + src/scenes/managers/scene/player.gd | 69 ++ src/scenes/managers/scene/player.gd.uid | 1 + src/scenes/managers/scene/signalbus.gd | 12 + src/scenes/managers/scene/signalbus.gd.uid | 1 + src/scenes/master.tscn | 7 +- src/scenes/players/player.gd | 56 +- src/scripts/crypto/rsa.gd | 4 +- src/scripts/enum.gd | 42 + src/scripts/enum.gd.uid | 1 + src/scripts/libs/account.gd | 44 +- src/scripts/libs/account_server.gd | 20 - src/scripts/libs/account_server.gd.uid | 1 - src/scripts/libs/bootstrap.gd | 27 + src/scripts/libs/bootstrap.gd.uid | 1 + src/scripts/libs/jwt.gd | 18 +- src/scripts/libs/oauth.gd | 21 +- src/scripts/logger.gd | 22 +- src/scripts/managers/settings.gd | 93 ++ src/scripts/managers/settings.gd.uid | 1 + src/scripts/network/account_servers.gd | 7 +- src/scripts/network/network_compression.gd | 10 +- src/scripts/rpc/client.gd | 29 - src/scripts/rpc/client.gd.uid | 1 - src/scripts/rpc/common.gd | 39 - src/scripts/rpc/common.gd.uid | 1 - src/scripts/rpc/server.gd | 99 -- src/scripts/rpc/server.gd.uid | 1 - src/scripts/signal_bus.gd | 22 +- src/scripts/utils/files.gd | 15 +- src/userinterface/dash/account_create.gd | 6 +- src/userinterface/dash/exit.gd | 22 +- src/userinterface/dash/home.gd | 36 +- src/userinterface/dash/hud.tscn | 935 ++++++++---------- src/userinterface/dash/instance.gd | 98 +- src/userinterface/dash/master.gd | 4 +- src/userinterface/dash/sessions.gd | 60 ++ src/userinterface/dash/settings.gd | 66 +- src/userinterface/session_query.gd | 95 ++ src/userinterface/session_query.gd.uid | 1 + 62 files changed, 1911 insertions(+), 1132 deletions(-) create mode 100644 src/scenes/levels/base.tscn rename src/scenes/levels/{home.tscn => grid.tscn} (98%) create mode 100644 src/scenes/managers/app/network.gd create mode 100644 src/scenes/managers/app/network.gd.uid create mode 100644 src/scenes/managers/app/network.tscn delete mode 100644 src/scenes/managers/app/network_manager.gd delete mode 100644 src/scenes/managers/app/network_manager.gd.uid delete mode 100644 src/scenes/managers/app/network_manager.tscn delete mode 100644 src/scenes/managers/app/rpc_manager.gd delete mode 100644 src/scenes/managers/app/rpc_manager.gd.uid delete mode 100644 src/scenes/managers/app/rpc_manager.tscn create mode 100644 src/scenes/managers/app/scene.gd create mode 100644 src/scenes/managers/app/scene.gd.uid create mode 100644 src/scenes/managers/app/scene.tscn delete mode 100644 src/scenes/managers/app/scene_manager.gd delete mode 100644 src/scenes/managers/app/scene_manager.gd.uid delete mode 100644 src/scenes/managers/app/scene_manager.tscn create mode 100644 src/scenes/managers/scene/entity.gd create mode 100644 src/scenes/managers/scene/entity.gd.uid create mode 100644 src/scenes/managers/scene/network.gd create mode 100644 src/scenes/managers/scene/network.gd.uid create mode 100644 src/scenes/managers/scene/player.gd create mode 100644 src/scenes/managers/scene/player.gd.uid create mode 100644 src/scenes/managers/scene/signalbus.gd create mode 100644 src/scenes/managers/scene/signalbus.gd.uid create mode 100644 src/scripts/enum.gd create mode 100644 src/scripts/enum.gd.uid delete mode 100644 src/scripts/libs/account_server.gd delete mode 100644 src/scripts/libs/account_server.gd.uid create mode 100644 src/scripts/libs/bootstrap.gd create mode 100644 src/scripts/libs/bootstrap.gd.uid create mode 100644 src/scripts/managers/settings.gd create mode 100644 src/scripts/managers/settings.gd.uid delete mode 100644 src/scripts/rpc/client.gd delete mode 100644 src/scripts/rpc/client.gd.uid delete mode 100644 src/scripts/rpc/common.gd delete mode 100644 src/scripts/rpc/common.gd.uid delete mode 100644 src/scripts/rpc/server.gd delete mode 100644 src/scripts/rpc/server.gd.uid create mode 100644 src/userinterface/session_query.gd create mode 100644 src/userinterface/session_query.gd.uid diff --git a/src/openminerva_default.tres b/src/openminerva_default.tres index 4a9a156..a390c0e 100644 --- a/src/openminerva_default.tres +++ b/src/openminerva_default.tres @@ -1,7 +1,7 @@ [gd_resource type="Theme" format=3 uid="uid://bg2nbganyysst"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_31r6k"] -bg_color = Color(0, 0, 0, 0.6) +bg_color = Color(0, 0, 0, 0.78431374) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f2aq3"] bg_color = Color(0.14117648, 0.39215687, 0.5686275, 1) diff --git a/src/project.godot b/src/project.godot index 0823541..b5bcad5 100644 --- a/src/project.godot +++ b/src/project.godot @@ -27,12 +27,18 @@ boot_splash/minimum_display_time=1000 LaunchArguments="*uid://c45jrfmrjtnyn" GlobalLogger="*uid://dgmfafi41y1nk" FileManager="*uid://d2s50p717g3n" +SettingsManager="*uid://bj4giyk05v042" AccountServers="*uid://bpysjoq7n0ytu" Random="*uid://1js68qt8w0mv" GlobalAccount="*uid://dtlb70kxvbtvn" Events="*uid://c656spc3ppdlw" -UrlParser="*uid://budprjmmpally" OAuth="*uid://jd7qlsley1no" +SessionQuery="*uid://dbrabs53sf0x5" +Enum="*uid://dgpvcem71xdbn" +Bootstrap="*uid://cvj10vdw6hlqw" +NetworkCompression="*uid://b2iq75uom64x2" +HTTP="*uid://d3cnfdwjxopsx" +UrlParser="*uid://budprjmmpally" [display] diff --git a/src/scenes/levels/base.tscn b/src/scenes/levels/base.tscn new file mode 100644 index 0000000..6d3b923 --- /dev/null +++ b/src/scenes/levels/base.tscn @@ -0,0 +1,22 @@ +[gd_scene format=3 uid="uid://bysrhijnau31g"] + +[ext_resource type="Script" uid="uid://bx6abcqetw04c" path="res://scenes/managers/scene/entity.gd" id="1_5on25"] +[ext_resource type="Script" uid="uid://c13l6ywhxq6n5" path="res://scenes/managers/scene/player.gd" id="1_c3hqj"] +[ext_resource type="Script" uid="uid://ckxuws4l4alod" path="res://scenes/managers/scene/network.gd" id="1_fundk"] +[ext_resource type="Script" uid="uid://cxbdjoelope42" path="res://scenes/managers/scene/signalbus.gd" id="2_fundk"] + +[node name="Base" type="Node3D" unique_id=2084163635] + +[node name="EntityManager" type="Node" parent="." unique_id=1378414814] +script = ExtResource("1_5on25") + +[node name="NetworkManager" type="Node" parent="." unique_id=2144728320] +script = ExtResource("1_fundk") + +[node name="PlayerManager" type="Node" parent="." unique_id=412138074] +script = ExtResource("1_c3hqj") + +[node name="SignalBus" type="Node" parent="." unique_id=165926833] +script = ExtResource("2_fundk") + +[node name="root" type="Node3D" parent="." unique_id=345927695] diff --git a/src/scenes/levels/home.tscn b/src/scenes/levels/grid.tscn similarity index 98% rename from src/scenes/levels/home.tscn rename to src/scenes/levels/grid.tscn index e87e279..e598ffa 100644 --- a/src/scenes/levels/home.tscn +++ b/src/scenes/levels/grid.tscn @@ -32,7 +32,7 @@ shadow_enabled = true environment = SubResource("Environment_q28r8") [node name="CSGBox3D" type="CSGBox3D" parent="." unique_id=533069808] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, -0.5, 0) +transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 0) use_collision = true size = Vector3(1000, 0.01, 1000) material = SubResource("ShaderMaterial_q28r8") diff --git a/src/scenes/managers/app/network.gd b/src/scenes/managers/app/network.gd new file mode 100644 index 0000000..6b44e50 --- /dev/null +++ b/src/scenes/managers/app/network.gd @@ -0,0 +1,463 @@ +# --- License +# File: /client/src/scenes/managers/app/network.gd +# Project: OpenMinerva +# Created Date: 13 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +const MAX_CLIENTS = 1000 +const MINIMUM_INCREMENTAL_PORT = 20205 + +var url_regex: RegEx = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") + +@onready var scene_m = get_node("../SceneManager") + +var _database = { + "heartbeats": {}, + "sessions_id": {}, + "sessions": {}, + "sessions_api": {} +} + +const _instance_database_template = { + "id": "", + "name": "", + "description": "", + "port": 0, + "max_connected_users": 1, + "privacy": null, + "active": false, + + "connected_players": [], + "start_time": 0, + + "networking": { + "use_steam": false, + "use_lan": false + } +} + +func start_server(port: int = 0, root_scene: Enum.BaseLevel = Enum.BaseLevel.GRID) -> Dictionary: + var response_dict = {"ok": false, "error": null, "data": null} + GlobalLogger.logs("Starting a new server.") + + # Get an available port. If port was defined, force that port or fail. + if port != 0: + GlobalLogger.logs("Forcing port '%s'" % port) + var port_available = !_is_port_in_use(port) + if !port_available: + response_dict.error = "Port is not available." + return response_dict + else: + port = _find_available_port() + + # Create server master scene. + var _scene: String = scene_m.create_master_scene() + var _instance = _instance_database_template.duplicate() + _instance.id = _scene + _instance.name = _scene + _instance.start_time = int(Time.get_unix_time_from_system()) + _instance.privacy = Enum.PrivacyLevel.INVITE + _instance.port = port + _instance.type = "host" + + # Create a new peer. + var _mp_api = SceneMultiplayer.new() + var _session_peer = ENetMultiplayerPeer.new() + var _create_server_response = _session_peer.create_server(port, MAX_CLIENTS) + _mp_api.multiplayer_peer = _session_peer + + var master_scene = scene_m.get_master_scene(_scene) + get_tree().set_multiplayer(_mp_api, master_scene.get_path()) + _mp_api.set_root_path(master_scene.get_path()) + var net_manager = master_scene.get_node("NetworkManager") + net_manager.setup_connection(_mp_api, _scene) + + _database.sessions_api.set(_scene, _mp_api) + _database.sessions.set(_scene, _instance) + + if _create_server_response != OK: + GlobalLogger.logs("Failed to start server. Error: '%s'" % _create_server_response, Enum.LogLevel.INFO) + response_dict.error = str(_create_server_response) + + _database.sessions_api.erase(_scene) + _database.sessions.erase(_scene) + + scene_m.destroy_master_scene(_scene) + # HACK: Retry creating a server again. + if _create_server_response == 20: + # Port is in use + return start_server(0, root_scene) + + return response_dict + + # Create server root scene. + if root_scene: + scene_m.set_master_root_from_program(_scene, root_scene) + else: + scene_m.set_master_root_from_program(_scene, Enum.BaseLevel.GRID) + + scene_m.start_master_scene(_scene) + + # DEV: Force spawn the host. + scene_m.get_master_scene(_scene).get_node("PlayerManager").add_player(1) + scene_m.set_active_session(_scene) + + return response_dict + +func stop_server(id: String): + var database_has_sessions: bool = _database.sessions.has(id) + var database_has_sessions_api: bool = _database.sessions_api.has(id) + GlobalLogger.logs("Stopping server '%s'." % id) + + # TODO: Disable join requests to server + + if !database_has_sessions && !database_has_sessions_api: + GlobalLogger.logs("Session '%s' does not exist, cannot stop the server." % id, Enum.LogLevel.WARNING) + return + + if database_has_sessions_api: + var mp_api: SceneMultiplayer = _database.sessions_api.get(id) + var all_peers = mp_api.get_peers() + + # Kick all players + for _peer in all_peers: + kick_player(id, _peer, "Server Closing") + + # Close the server + mp_api.multiplayer_peer.close() + mp_api.multiplayer_peer = null + + # Application cleanup + scene_m.stop_master_scene(id) + scene_m.destroy_master_scene(id) + + # Database cleanup + _database.sessions_api.erase(id) + _database.sessions.erase(id) + return + +func update_server(id: String, server_info: Dictionary): + GlobalLogger.logs("Updating server '%s'." % id) + var _saved_session_servers = SettingsManager.get_session_servers() + + if server_info.privacy > Enum.PrivacyLevel.INVITE: + for _server in _saved_session_servers: + if _database.heartbeats.has(id): + GlobalLogger.logs("Session '%s' is already advertised. Updating instead." % id) + await _update_session_server_listing(server_info, _server.url) + else: + var advertise_response = await _advertise_session(server_info, _server.url) + + if advertise_response.ok == true: + _database.sessions_id.set(server_info.id, advertise_response.data.id) + _create_heartbeat_timer(server_info.id, _server.url) + + if server_info.privacy == Enum.PrivacyLevel.INVITE: + if _database.heartbeats.has(id): + GlobalLogger.logs("Destroying session heartbeat for '%s'" % id) + _database.heartbeats.erase(id) + + for _server in _saved_session_servers: + _remove_session_from_server(id, _server.url) + + Events.emit_signal("instance_updated") + return + +func join_server(ip: String, port: int): + var response_dict = {"ok": false, "error": null, "data": null} + + GlobalLogger.logs("Joining server at '%s:%s'" % [ip, port], Enum.LogLevel.INFO) + var _port_is_valid = port > 0 && port < 65535 + + if ip.is_empty() || !_port_is_valid: + GlobalLogger.logs("Server information is invalid '%s:%s'." % [ip, port], Enum.LogLevel.INFO) + response_dict.error = "Server information is invalid." + return response_dict + + # Create server master scene. + var _scene: String = scene_m.create_master_scene() + var _instance = _instance_database_template.duplicate() + _instance.id = _scene + _instance.name = _scene + _instance.start_time = int(Time.get_unix_time_from_system()) + _instance.privacy = Enum.PrivacyLevel.INVITE + _instance.port = port + _instance.type = "client" + + var _mp_api = SceneMultiplayer.new() + var _session_peer = ENetMultiplayerPeer.new() + var connect_error = _session_peer.create_client(ip, port) + + if connect_error != OK: + GlobalLogger.logs("Failed to join server. Error: '%s'" % connect_error, Enum.LogLevel.INFO) + response_dict.error = "Failed to join server. Error: '%s'" % connect_error + return response_dict + + _mp_api.multiplayer_peer = _session_peer + + var master_scene = scene_m.get_master_scene(_scene) + get_tree().set_multiplayer(_mp_api, master_scene.get_path()) + _mp_api.set_root_path(master_scene.get_path()) + var net_manager = master_scene.get_node("NetworkManager") + net_manager.setup_connection(_mp_api, _scene) + + _database.sessions_api.set(_scene, _mp_api) + _database.sessions.set(_scene, _instance) + + Events.emit_signal("session_joined") + return + +func leave_server(id: String): + GlobalLogger.logs("Trying to leave server '%s'." % id) + var database_has_sessions: bool = _database.sessions.has(id) + var database_has_sessions_api: bool = _database.sessions_api.has(id) + + if !database_has_sessions && !database_has_sessions_api: + GlobalLogger.logs("Session '%s' does not exist, cannot disconnect." % id, Enum.LogLevel.WARNING) + return + + if database_has_sessions_api: + var mp_api: SceneMultiplayer = _database.sessions_api.get(id) + + if mp_api.multiplayer_peer: + mp_api.multiplayer_peer.close() + GlobalLogger.logs("Disconnected from session '%s'." % id, Enum.LogLevel.DEBUG) + + scene_m.set_active_session(get_connected_sessions()[0].id) + + scene_m.stop_master_scene(id) + scene_m.destroy_master_scene(id) + + _database.sessions_api.erase(id) + _database.sessions.erase(id) + + GlobalLogger.logs("Successfully disconnected from session '%s' and cleaned up." % id, Enum.LogLevel.DEBUG) + Events.emit_signal("session_left") + return + +func kick_player(server_id:String, peer_id: int, reason: String): + GlobalLogger.logs("Kicking peer '%s' from '%s' for reason '%s'" % [peer_id, server_id, reason], Enum.LogLevel.DEBUG) + var database_has_sessions_api: bool = _database.sessions_api.has(server_id) + # TODO: Check if peer exists + if database_has_sessions_api: + var mp_api: SceneMultiplayer = _database.sessions_api.get(server_id) + # TODO: Notify user of kick + mp_api.disconnect_peer(peer_id) + return + +func get_connected_sessions(): + var result = [] + + for session_id in _database.sessions.keys(): + result.append(_database.sessions[session_id].merged({"id": session_id})) + + return result + +func set_active_session(id: String): + if _database.sessions.has(id): + GlobalLogger.logs("Tried to mark an invalid session as active: '%s'" % id, Enum.LogLevel.WARNING) + return + + for session_id in _database.sessions.keys(): + var my_id = _database.sessions_api[session_id].multiplayer.get_unique_id() + _database.sessions[session_id].active = false + scene_m.get_master_root(session_id).get_node("PlayerManager").players.get(my_id).get("node").camera.current = false + + var my_id = _database.sessions_api[id].multiplayer.get_unique_id() + scene_m.get_master_root(id).get_node("PlayerManager").players.get(my_id).get("node").camera.current = true + _database.sessions[id].active = true + scene_m.set_active_session(id) + return + +func _update_session_server_listing(session_info: Dictionary, session_server: String) -> Dictionary: + var response_dict = {"ok": false, "error": null, "data": null} + + GlobalLogger.logs("Updating session '%s' to the server '%s'" % [session_info.id, session_server]) + var url = UrlParser.deconstruct("%s/api/v1/updateSession" % session_server) + + if url.ok != true: + GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [session_server, url.error]) + response_dict.error = url.error + return response_dict + + url = url.data + + var _body = { + "id": _database.sessions_id.get(session_info.id), + "session_name": session_info.name, + "session_description": session_info.description, + "session_privacy": session_info.privacy, + } + + var _update_response = await HTTP.req( + HTTPClient.Method.METHOD_POST, + url.host, + url.path, + url.port, + ["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key], + JSON.stringify(_body) + ) + + return response_dict + +func _remove_session_from_server(server_id: String, session_server: String) -> Dictionary: + var response_dict = {"ok": false, "error": null, "data": {}} + var _full_url = "%s/api/v1/deleteSession" % session_server + + GlobalLogger.logs("Removing session '%s' to the server '%s'" % [server_id, session_server]) + var url = UrlParser.deconstruct(_full_url) + + if url.ok != true: + GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, url.error]) + response_dict.error = url.error + return response_dict + + url = url.data + + var _body = { + "id": _database.sessions_id.get(server_id), + } + + var _removal_response = await HTTP.req( + HTTPClient.Method.METHOD_DELETE, + url.host, + url.path, + url.port, + ["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key], + JSON.stringify(_body) + ) + + if _removal_response.ok: + response_dict.ok = true + response_dict.data = JSON.parse_string(_removal_response.body) + return response_dict + + response_dict.error = _removal_response.error + return response_dict + +func _find_available_port(target_port: int = MINIMUM_INCREMENTAL_PORT) -> int: + GlobalLogger.logs("Trying to find an available port starting at '%s'." % target_port) + var _found_port = null + var _is_found = false + + while _is_found == false: + var port_available = !_is_port_in_use(target_port) + if port_available: + _found_port = target_port + _is_found = true + break + target_port = target_port + 1 + + GlobalLogger.logs("Port found: '%s'" % target_port) + + return _found_port + +func _is_port_in_use(port: int) -> bool: + var udp_server = UDPServer.new() + var err_udp = udp_server.listen(port, "*") + var tcp_server = TCPServer.new() + var err_tcp = tcp_server.listen(port, "*") + + if err_udp == OK && err_tcp == OK: + udp_server.stop() + tcp_server.stop() + return false + + udp_server.stop() + tcp_server.stop() + return true + +func _create_heartbeat_timer(session_id: String, session_server_url: String): + GlobalLogger.logs("Creating a heartbeat timer for server '%s'" % session_id) + # FIXME: Hardcoded time for timer. + var timer = get_tree().create_timer(20) + + _database.heartbeats[session_id] = timer + + timer.timeout.connect(_heartbeat_timer_timeout.bind(session_id, session_server_url)) + return + +func _heartbeat_timer_timeout(session_id: String, session_server_url: String): + GlobalLogger.logs("Sending a heartbeat for server '%s'" % session_id) + if _database.heartbeats.has(session_id) == false: + GlobalLogger.logs("Server '%s' does not exist anymore, not sending a heartbeat." % session_id) + return + + _heartbeat_session(session_id, session_server_url) + + _create_heartbeat_timer(session_id, session_server_url) + return + +func _heartbeat_session(session_id: String, session_server_url: String) -> void: + var _full_url = "%s/api/v1/heartbeatSession" % session_server_url + var _url = UrlParser.deconstruct(_full_url) + + if _url.ok != true: + GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, _url.error]) + return + + _url = _url.data + var body = {"session_id": _database.sessions_id.get(session_id)} + + var response = await HTTP.req( + HTTPClient.Method.METHOD_POST, + _url.host, + _url.path, + _url.port, + ["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key], + JSON.stringify(body) + ) + + if response and response.get("ok"): + GlobalLogger.logs("Heartbeat sent for session '%s'" % session_id) + return + +func _advertise_session(session_info: Dictionary, session_server: String) -> Dictionary: + var response_dict = {"ok": false, "error": null, "data": null} + GlobalLogger.logs("Advertising session '%s' to the server '%s'" % [session_info.id, session_server]) + var _full_url = "%s/api/v1/postSession" % session_server + var url = UrlParser.deconstruct(_full_url) + + if url.ok != true: + GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, url.error]) + response_dict.error = url.error + return response_dict + + url = url.data + + var _body = { + "session_name": session_info.name, + "session_description": session_info.description, + "session_privacy": session_info.privacy, + "session_port": session_info.port, + } + + var advertise_response = await HTTP.req( + HTTPClient.Method.METHOD_POST, + url.host, + url.path, + url.port, + ["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key], + JSON.stringify(_body) + ) + + # FIXME: What is this flow? This is bad? + if advertise_response.ok != true: + response_dict.error = advertise_response.error + return response_dict + + advertise_response = JSON.parse_string(advertise_response.body) + if advertise_response.ok == false: + response_dict.error = advertise_response.error + return response_dict + + advertise_response = advertise_response.data + + response_dict.ok = true + response_dict.data = advertise_response + return response_dict diff --git a/src/scenes/managers/app/network.gd.uid b/src/scenes/managers/app/network.gd.uid new file mode 100644 index 0000000..701a03c --- /dev/null +++ b/src/scenes/managers/app/network.gd.uid @@ -0,0 +1 @@ +uid://r51flk0bjypx diff --git a/src/scenes/managers/app/network.tscn b/src/scenes/managers/app/network.tscn new file mode 100644 index 0000000..6344fef --- /dev/null +++ b/src/scenes/managers/app/network.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://5v8rbnp716b0"] + +[ext_resource type="Script" uid="uid://r51flk0bjypx" path="res://scenes/managers/app/network.gd" id="1_daels"] + +[node name="NetworkManager" type="Node"] +script = ExtResource("1_daels") diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd deleted file mode 100644 index 5def165..0000000 --- a/src/scenes/managers/app/network_manager.gd +++ /dev/null @@ -1,182 +0,0 @@ -# --- License -# File: /client/src/scenes/managers/app/network_manager.gd -# Project: OpenMinerva -# Created Date: 05 February 2026 -# Copyright (c) 2026 OpenMinerva -# License: MIT License -# Authors: Armored Dragon -# --- License - -extends Node - -var n_c = preload("res://scripts/network/network_compression.gd").new() -var rsa = preload("res://scripts/crypto/rsa.gd").new() -var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") - -# TODO: Bandwidth toggles -@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") -@onready var rpc_lib = get_tree().current_scene.get_node("RpcManager") - -var active_session = "" - -# This file contains all of the session management and client communication. -# Anything that goes through the network should first route through here at some point. -enum server_privacy {PRIVATE, INVITE, FRIENDS, PUBLIC} - -var status = { - "hosting": false, - "client": false -} - -var config = { - "port": 20205, - "max_clients": 4, - "privacy": 0, - - "networking": { - "use_steam": false, - "use_lan": false - } -} - -var info = { - "level": "res://scenes/levels/home.tscn", - "level_node_name": "", - "clients": [] -} - -func start_server(port: int = config.port, max_clients: int = config.max_clients, ignore_port: bool = false) -> void: - # TODO: In ignore_port = true, keep trying to make a server until it succeeds. - if status.hosting: - # This ideally should not trigger - GlobalLogger.logs("Can not start server: Server is already running.", 2) - status.hosting = false - status.client = false - return - - var new_peer = ENetMultiplayerPeer.new() - # FIXME: Error handling is required here - info.clients.append({"username": "Me!", "multiplayer_id": 1}) - var err = new_peer.create_server(port, max_clients) - # FIXME: This client append is happening too early, this is a debug position - - if err == 20: - # Port is in use - GlobalLogger.logs("Failed to start server: Is the port in use?", 1) - # FIXME: HACK: Just try again with the default port + 1. - err = new_peer.create_server(port + 1, max_clients) - status.hosting = false - status.client = false - - if err != OK: - GlobalLogger.logs("Failed to start server. Error: '%s'" % err, 1) - status.hosting = false - status.client = false - return - - - multiplayer.multiplayer_peer = new_peer - GlobalLogger.logs("Successfully started server.", 1) - - while status.hosting == false: - await get_tree().process_frame - status.hosting = true - status.client = false - - # TODO: Hardcoded spawn host value, is there a better way? - rpc_lib.com.on_spawn_player(1) - -func close_server(): - # Disconnect all players. - # Remove listings from all used networking. - # Update server config. - # TODO: OfflineMultiplayerPeer is a test. Check to see if this actually works. - multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() - status.hosting = false - status.client = false - - return - -func update_server(): - # Update our config. - # Submit a update to any active networking service. - GlobalLogger.logs("Not implemented.", 3) - return - -func join_server(ip: String = "", port: int = config.port) -> void: - # Client connects to a server. - if ip.is_empty(): - GlobalLogger.logs("No IP to connect to.", 2) - return - - if status.hosting: - # This ideally should not trigger - GlobalLogger.logs("Can not join server: We are currently hosting a server.", 2) - close_server() - # return - - var new_peer = ENetMultiplayerPeer.new() - new_peer.create_client(ip, port) - multiplayer.multiplayer_peer = new_peer - - status.hosting = false - status.client = true - GlobalLogger.logs("Connected to the server.", 1) - return - -func kick_player(player_id: int, reason: String = "No reason specified"): - # Server kicks a player from the session. - GlobalLogger.logs("Not implemented.", 3) - return - -func ban_player(): - # Server permanatly bans a user. - GlobalLogger.logs("Not implemented.", 3) - return - -func set_networking_config(options: Dictionary) -> void: - if !options: - GlobalLogger.logs("Tried to set networking config without options", 2) - return - - # LAN connections - if options.lan == true: - config.use_lan = true - else: - config.use_lan = false - - # Steam connections - if options.steam == true: - config.use_steam = true - else: - config.use_steam = false - -func parse_url(url: String) -> Dictionary: - var result = { - "scheme": "", - "host": "", - "port": 0, - "path": "" - } - - var matches = url_regex.search(url) - if matches: - result["scheme"] = matches.get_string(1).to_lower() - result["host"] = matches.get_string(2) - result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) - result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" - - return result - -func spawn_player(player): - # FIXME: Placeholder for refactor - while scene_manager.get_current_session_node() == null: - await get_tree().process_frame - - scene_manager.get_current_session_node().call_deferred("add_child", player) - -func player_exists(name: String) -> Node3D: - # FIXME: Placeholder for refactor - var target_node = scene_manager.get_current_session_node().get_node_or_null(name) - - return target_node diff --git a/src/scenes/managers/app/network_manager.gd.uid b/src/scenes/managers/app/network_manager.gd.uid deleted file mode 100644 index 3935f11..0000000 --- a/src/scenes/managers/app/network_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cc0ei8wuetrvh diff --git a/src/scenes/managers/app/network_manager.tscn b/src/scenes/managers/app/network_manager.tscn deleted file mode 100644 index a76ebca..0000000 --- a/src/scenes/managers/app/network_manager.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://5v8rbnp716b0"] - -[ext_resource type="Script" uid="uid://cc0ei8wuetrvh" path="res://scenes/managers/app/network_manager.gd" id="1_daels"] - -[node name="NetworkManager" type="Node"] -script = ExtResource("1_daels") diff --git a/src/scenes/managers/app/rpc_manager.gd b/src/scenes/managers/app/rpc_manager.gd deleted file mode 100644 index b8ee3ca..0000000 --- a/src/scenes/managers/app/rpc_manager.gd +++ /dev/null @@ -1,16 +0,0 @@ -extends Node - -var c := preload("res://scripts/rpc/client.gd").new() -var s := preload("res://scripts/rpc/server.gd").new() -var com := preload("res://scripts/rpc/common.gd").new() - -func _ready(): - # RPCs can not be called from outside of the scene tree, we are required to add them. - add_child(c) - add_child(s) - add_child(com) - - multiplayer.peer_connected.connect(s.on_peer_connected) - multiplayer.peer_disconnected.connect(s.on_peer_disconnected) - multiplayer.connected_to_server.connect(c.connected_to_server) - multiplayer.connection_failed.connect(c.connection_failed) \ No newline at end of file diff --git a/src/scenes/managers/app/rpc_manager.gd.uid b/src/scenes/managers/app/rpc_manager.gd.uid deleted file mode 100644 index a7c8542..0000000 --- a/src/scenes/managers/app/rpc_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://x23lqbiivx1q diff --git a/src/scenes/managers/app/rpc_manager.tscn b/src/scenes/managers/app/rpc_manager.tscn deleted file mode 100644 index bcdecb5..0000000 --- a/src/scenes/managers/app/rpc_manager.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene format=3 uid="uid://by0vghgshvbhd"] - -[ext_resource type="Script" uid="uid://x23lqbiivx1q" path="res://scenes/managers/app/rpc_manager.gd" id="1_6timl"] - -[node name="RpcManager" type="Node" unique_id=1836544617] -script = ExtResource("1_6timl") diff --git a/src/scenes/managers/app/scene.gd b/src/scenes/managers/app/scene.gd new file mode 100644 index 0000000..439391c --- /dev/null +++ b/src/scenes/managers/app/scene.gd @@ -0,0 +1,183 @@ +# --- License +# File: /client/src/scenes/managers/app/scene_manager.gd +# Project: OpenMinerva +# Created Date: 13 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +# Game managers +@onready var network_m: Node = get_tree().current_scene.get_node("NetworkManager") +@onready var scene_container: Node3D = get_tree().current_scene.get_node("Scenes") +var active_session: String = "" + +func _ready(): + network_m.start_server() + return + +func create_master_scene(): + var _scene_id = Random.random_string() + var _base_scene = preload("res://scenes/levels/base.tscn") + + _base_scene = _base_scene.instantiate() + _base_scene.name = _scene_id + _base_scene.top_level = true + _base_scene.visible = false + + scene_container.add_child(_base_scene) + + return _scene_id + +func get_master_scene(id: String) -> Node3D: + var _scene = scene_container.get_node(id) + return _scene + +func destroy_master_scene(id: String): + var _scene = scene_container.get_node_or_null(id) + + if _scene == null: + GlobalLogger.logs("'%s' does not exist, could not delete." % id, Enum.LogLevel.WARNING) + return + + _scene.queue_free() + return + +func set_master_root_from_program(id: String, scene_type: Enum.BaseLevel) -> void: + var _scene = get_master_scene(id) + + var _root_scene: PackedScene = _get_scene_by_type(scene_type) + var _root_node = get_master_root(id) + + # Stop everything + stop_master_scene(id) + + # Remove everything + _scene.remove_child(_root_node) + _root_node.queue_free() + + # Get new scene + var _root_scene_node = _root_scene.instantiate() + _root_scene_node.name = "root" + + # Add new scene + _scene.add_child(_root_scene_node) + + # Start everything + start_master_scene(id) + + Events.emit_signal("instance_root_changed") + return + +func get_master_root(id: String) -> Node3D: + var _scene: Node3D = get_master_scene(id) + var _root = _scene.get_node_or_null("root") + return _root + +func set_master_root_from_inventory(_id: String, _scene_type: Enum.BaseLevel) -> bool: + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + # get_master_scene + # Find scene from inventory. + # Validate scene integrity. + # Find node "root". + # Destroy node. + # Replace with new scene. + return false + +func start_master_scene(id: String): + const MANAGERS = ["PlayerManager", "SignalBus"] + + var _scene = get_master_scene(id) + + for node_name in MANAGERS: + var _scene_manager = _scene.get_node_or_null(node_name) + if _scene_manager: + _scene_manager.module_active = true + GlobalLogger.logs("'%s' started in server '%s'" % [node_name, id]) + continue + + GlobalLogger.logs("Could not start invalid manager '%s' in server '%s'" % [node_name, id], Enum.LogLevel.ERROR) + return + +func stop_master_scene(id: String): + const MANAGERS = ["PlayerManager", "SignalBus"] + + var _scene = get_master_scene(id) + + for node_name in MANAGERS: + var _scene_manager = _scene.get_node_or_null(node_name) + if _scene_manager: + _scene_manager.module_active = true + GlobalLogger.logs("'%s' stopped in server '%s'" % [node_name, id]) + continue + + GlobalLogger.logs("Could not stop invalid manager '%s' in server '%s'" % [node_name, id], Enum.LogLevel.ERROR) + return + +func _get_scene_by_type(scene_type: Enum.BaseLevel) -> PackedScene: + var _scene_dir: String = "" + + match scene_type: + Enum.BaseLevel.DEBUG: + _scene_dir = "res://scenes/levels/debug.tscn" + Enum.BaseLevel.EMPTY: + _scene_dir = "res://scenes/levels/empty.tscn" + Enum.BaseLevel.GRID: + _scene_dir = "res://scenes/levels/grid.tscn" + _: + _scene_dir = "res://scenes/levels/debug.tscn" + + return load(_scene_dir) + +func set_active_session(session_id: String): + GlobalLogger.logs("Setting session '%s' active." % session_id) + + for _scene in network_m.get_connected_sessions(): + # Each session gets disabled + scene_container.get_node(_scene.id).visible = false + _set_camera_active_state(_scene.id, false) + _set_player_authority_state(_scene.id, false) + + # session_id gets enabled. + active_session = session_id + _set_camera_active_state(session_id, true) + scene_container.get_node(session_id).visible = true + _set_player_authority_state(session_id, true) + return + +func _set_camera_active_state(session_id, state: bool = false) -> void: + # TODO: check if session exists. + var my_id: String = str(network_m._database.sessions_api[session_id].get_unique_id()) + var master_scene: Node3D = get_master_scene(session_id) + # HACK: If my_id = 0, we get the desired result. This is not safe though. + if my_id == "0": + GlobalLogger.logs("Could not set active state for session '%s', is session open?" % [session_id], Enum.LogLevel.WARNING) + return + var player_manager: Node = master_scene.get_node("PlayerManager") + var player_database = player_manager.players + var my_database_entry = player_database.get(my_id) + var camera = my_database_entry.get("node").get_node("Head/Camera3D") + + camera.current = state + return + +func _set_player_authority_state(session_id, is_active: bool = false) -> void: + var my_id: String = str(network_m._database.sessions_api[session_id].get_unique_id()) + var master_scene: Node3D = get_master_scene(session_id) + # HACK: If my_id = 0, we get the desired result. This is not safe though. + if my_id == "0": + GlobalLogger.logs("Could not set player authority for session '%s', is session open?" % [session_id], Enum.LogLevel.WARNING) + return + var player_manager: Node = master_scene.get_node("PlayerManager") + var player_database = player_manager.players + var my_database_entry = player_database.get(my_id) + var player = my_database_entry.get("node") + + if is_active: + player.set_multiplayer_authority(int(my_id)) + return + + player.set_multiplayer_authority(0) + return diff --git a/src/scenes/managers/app/scene.gd.uid b/src/scenes/managers/app/scene.gd.uid new file mode 100644 index 0000000..02824f9 --- /dev/null +++ b/src/scenes/managers/app/scene.gd.uid @@ -0,0 +1 @@ +uid://3ylijxenr1bg diff --git a/src/scenes/managers/app/scene.tscn b/src/scenes/managers/app/scene.tscn new file mode 100644 index 0000000..d382583 --- /dev/null +++ b/src/scenes/managers/app/scene.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3 uid="uid://cmknpdx5ba15o"] + +[ext_resource type="Script" uid="uid://3ylijxenr1bg" path="res://scenes/managers/app/scene.gd" id="1_i8qdd"] + +[node name="SceneManager" type="Node"] +script = ExtResource("1_i8qdd") diff --git a/src/scenes/managers/app/scene_manager.gd b/src/scenes/managers/app/scene_manager.gd deleted file mode 100644 index e316389..0000000 --- a/src/scenes/managers/app/scene_manager.gd +++ /dev/null @@ -1,47 +0,0 @@ -extends Node - -# TODO: Allow multiple sessions -# Right now only one session is allowed and is destroyed when the player joins another session. - -# Game managers -@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") - -@onready var scene_work_root = get_tree().current_scene.get_node("Scenes") -@onready var player_home_scene: PackedScene = load("res://scenes/levels/home.tscn") - -var server_init: bool = false - -func _ready(): - await network_manager.start_server() - var session_name = Random.random_string(6, true) - var new_home = player_home_scene.instantiate() - new_home.name = session_name - network_manager.active_session = session_name - scene_work_root.add_child(new_home) - - _spawn_host_player() - -func load_multiplayer_scene(scene_dir: String, scene_name: String): - await _clean_scene_work_root() - var scene_packed: PackedScene = load(scene_dir) - - var scene = scene_packed.instantiate() - scene.name = scene_name - scene_work_root.add_child(scene) - network_manager.active_session = scene_name - await get_tree().process_frame - return - -func _clean_scene_work_root(): - network_manager.active_session = "" - var nodes_to_destroy = scene_work_root.get_children() - for node in nodes_to_destroy: - node.queue_free() - await get_tree().process_frame - return - -func _spawn_host_player(): - network_manager.spawn_player(1) - -func get_current_session_node(): - return get_tree().current_scene.get_node("Scenes").get_child(0) diff --git a/src/scenes/managers/app/scene_manager.gd.uid b/src/scenes/managers/app/scene_manager.gd.uid deleted file mode 100644 index 44c2b63..0000000 --- a/src/scenes/managers/app/scene_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwe2gsnip66cr diff --git a/src/scenes/managers/app/scene_manager.tscn b/src/scenes/managers/app/scene_manager.tscn deleted file mode 100644 index 435d9de..0000000 --- a/src/scenes/managers/app/scene_manager.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://cmknpdx5ba15o"] - -[ext_resource type="Script" uid="uid://bwe2gsnip66cr" path="res://scenes/managers/app/scene_manager.gd" id="1_i8qdd"] - -[node name="SceneManager" type="Node"] -script = ExtResource("1_i8qdd") diff --git a/src/scenes/managers/scene/entity.gd b/src/scenes/managers/scene/entity.gd new file mode 100644 index 0000000..61510e1 --- /dev/null +++ b/src/scenes/managers/scene/entity.gd @@ -0,0 +1 @@ +extends Node diff --git a/src/scenes/managers/scene/entity.gd.uid b/src/scenes/managers/scene/entity.gd.uid new file mode 100644 index 0000000..23fc5bf --- /dev/null +++ b/src/scenes/managers/scene/entity.gd.uid @@ -0,0 +1 @@ +uid://bx6abcqetw04c diff --git a/src/scenes/managers/scene/network.gd b/src/scenes/managers/scene/network.gd new file mode 100644 index 0000000..d3b2f5a --- /dev/null +++ b/src/scenes/managers/scene/network.gd @@ -0,0 +1,92 @@ +extends Node + +var _specific_api: SceneMultiplayer = null +var _my_id = 0 +var _server_id: String = "" + +@onready var player_m = get_node("../PlayerManager") +@onready var scene_m = get_tree().current_scene.get_node("SceneManager") +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") + +func _process(_delta): + if multiplayer: + multiplayer.poll() + return + +func setup_connection(api: SceneMultiplayer, id: String): + _specific_api = api + _server_id = id + + _specific_api.connected_to_server.connect(_on_connected_to_server) + _specific_api.peer_connected.connect(_on_peer_connected) + _specific_api.peer_disconnected.connect(_on_peer_disconnected) + _specific_api.server_disconnected.connect(_on_server_disconnected) + + _my_id = multiplayer.get_unique_id() + +func _on_connected_to_server(): + GlobalLogger.logs("[%s] I am connected to a server." % _my_id) + +func _on_server_disconnected(): + network_m.leave_server(_server_id) + return + +@rpc("authority", "unreliable") +func ban_player(): + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + return + +func on_kicked(): + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + return + +func on_banned(): + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + return + +func _on_peer_connected(peer_id: int): + if is_multiplayer_authority() == false: + return + + player_m.add_player(peer_id) + player_m.add_player.rpc(peer_id) + + GlobalLogger.logs("[%s] Peer '%s' connected to our server." % [_my_id, peer_id]) + rpc_id(peer_id, "set_root", Enum.BaseLevel.GRID) + rpc_id(peer_id, "add_players", player_m.players) + return + +func _on_peer_disconnected(peer_id: int) -> void: + if is_multiplayer_authority() == false: + return + + player_m.remove_player(peer_id) + player_m.remove_player.rpc(peer_id) + + GlobalLogger.logs("[%s] Peer '%s' disconnected to our server." % [_my_id, peer_id]) + return + +@rpc("authority", "reliable") +func set_root(scene_type: Enum.BaseLevel): + GlobalLogger.logs("[%s] Received root base scene." % [_my_id]) + scene_m.set_master_root_from_program(_server_id, scene_type) + +@rpc("authority", "reliable") +func add_players(players: Dictionary): + GlobalLogger.logs("[%s] Playerlist received." % [_my_id]) + for _player in players.keys(): + player_m.add_player(int(_player)) + +@rpc("authority", "reliable") +func spawn_entity(): + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + +@rpc("any_peer", "unreliable") +func entity_position(entity_id: int, position): + var caller_id = multiplayer.get_remote_sender_id() + if caller_id != entity_id: + return + var target_node = get_parent().get_node("root").get_node_or_null(str(entity_id)) + if target_node: + target_node.position = NetworkCompression.d_16_pos(position) + target_node.rotation = NetworkCompression.d_16_vec3(position.slice(12)) diff --git a/src/scenes/managers/scene/network.gd.uid b/src/scenes/managers/scene/network.gd.uid new file mode 100644 index 0000000..b4b7e6a --- /dev/null +++ b/src/scenes/managers/scene/network.gd.uid @@ -0,0 +1 @@ +uid://ckxuws4l4alod diff --git a/src/scenes/managers/scene/player.gd b/src/scenes/managers/scene/player.gd new file mode 100644 index 0000000..d91892a --- /dev/null +++ b/src/scenes/managers/scene/player.gd @@ -0,0 +1,69 @@ +# --- License +# File: /client/src/scenes/managers/scene/player.gd +# Project: OpenMinerva +# Created Date: 13 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +var module_active: bool = false + +var players = {} + +const PLAYER_TEMPLATE = { + "peer_id": 0, + "has_spawned": false, + "node": null +} + +@rpc("authority", "reliable") +func add_player(peer_id: int) -> void: + var caller_id = multiplayer.get_remote_sender_id() + var database_template = PLAYER_TEMPLATE.duplicate() + + GlobalLogger.logs("[%s] Adding peer '%s' to the player list" % [caller_id, peer_id]) + database_template.set("peer_id", peer_id) + players.set(str(peer_id), database_template) + + spawn_player(str(peer_id)) + +@rpc("authority", "reliable") +func remove_player(peer_id: int) -> void: + var caller_id = multiplayer.get_remote_sender_id() + + GlobalLogger.logs("[%s] Removing peer '%s' from the player list" % [caller_id, peer_id]) + players[str(peer_id)].get("node").queue_free() + players.erase(str(peer_id)) + +@rpc("authority", "unreliable") +func spawn_player(peer_id: String) -> void: + var caller_id = multiplayer.get_remote_sender_id() + + if players[peer_id].get("has_spawned") == true: + GlobalLogger.logs("[%s] Did not spawn peer '%s', already exists!" % [caller_id, peer_id], Enum.LogLevel.WARNING) + return + + GlobalLogger.logs("[%s] Spawning peer '%s'" % [caller_id, peer_id]) + + if module_active == false: + GlobalLogger.logs("[%s] Could not spawn peer '%s', module inactive." % [caller_id, peer_id]) + return + + var _player_scene: PackedScene = load("res://scenes/players/player.tscn") + var _new_player: Node3D = _player_scene.instantiate() + _new_player.name = str(peer_id) + _new_player.position = Vector3(0, 0, 0) + _new_player.set_multiplayer_authority(int(peer_id)) + get_node("../root").add_child( _new_player) + var player_node = get_node("../root").get_node(str(peer_id)) + GlobalLogger.logs("[%s] Spawned peer '%s'." % [caller_id, peer_id]) + players[peer_id].set("node", player_node) + players[peer_id].set("has_spawned", true) + return + +func kill_player() -> void: + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + return diff --git a/src/scenes/managers/scene/player.gd.uid b/src/scenes/managers/scene/player.gd.uid new file mode 100644 index 0000000..bb355fa --- /dev/null +++ b/src/scenes/managers/scene/player.gd.uid @@ -0,0 +1 @@ +uid://c13l6ywhxq6n5 diff --git a/src/scenes/managers/scene/signalbus.gd b/src/scenes/managers/scene/signalbus.gd new file mode 100644 index 0000000..b89b375 --- /dev/null +++ b/src/scenes/managers/scene/signalbus.gd @@ -0,0 +1,12 @@ +# --- License +# File: /client/src/scenes/managers/scene/signalbus.gd +# Project: OpenMinerva +# Created Date: 13 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +var module_active = false diff --git a/src/scenes/managers/scene/signalbus.gd.uid b/src/scenes/managers/scene/signalbus.gd.uid new file mode 100644 index 0000000..9814658 --- /dev/null +++ b/src/scenes/managers/scene/signalbus.gd.uid @@ -0,0 +1 @@ +uid://cxbdjoelope42 diff --git a/src/scenes/master.tscn b/src/scenes/master.tscn index 33d08b2..01ce876 100644 --- a/src/scenes/master.tscn +++ b/src/scenes/master.tscn @@ -1,14 +1,11 @@ [gd_scene format=3 uid="uid://cxk6c0uipjjpo"] -[ext_resource type="PackedScene" uid="uid://by0vghgshvbhd" path="res://scenes/managers/app/rpc_manager.tscn" id="1_h2qy3"] -[ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network_manager.tscn" id="1_jooxx"] -[ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene_manager.tscn" id="2_h2qy3"] +[ext_resource type="PackedScene" uid="uid://5v8rbnp716b0" path="res://scenes/managers/app/network.tscn" id="1_jooxx"] +[ext_resource type="PackedScene" uid="uid://cmknpdx5ba15o" path="res://scenes/managers/app/scene.tscn" id="2_h2qy3"] [ext_resource type="PackedScene" uid="uid://ckl5gw0xbduiv" path="res://userinterface/dash/hud.tscn" id="5_q3f5g"] [node name="Master" type="Node3D" unique_id=420526444] -[node name="RpcManager" parent="." unique_id=1836544617 instance=ExtResource("1_h2qy3")] - [node name="NetworkManager" parent="." unique_id=1960146969 instance=ExtResource("1_jooxx")] [node name="SceneManager" parent="." unique_id=5477810 instance=ExtResource("2_h2qy3")] diff --git a/src/scenes/players/player.gd b/src/scenes/players/player.gd index 0b29c67..bcdfd26 100644 --- a/src/scenes/players/player.gd +++ b/src/scenes/players/player.gd @@ -1,17 +1,19 @@ extends CharacterBody3D -@onready var rpc_lib = get_tree().current_scene.get_node("RpcManager") - var speed = 5.0 @onready var hud = get_tree().current_scene.get_node("Hud") +@onready var scene_m = get_tree().current_scene.get_node("SceneManager") -var n_c = preload("res://scripts/network/network_compression.gd").new() +# TODO: Mouse sensitivity from settings +# TODO: Replace interaction ray +# TODO: Add skeleton controller +# TODO: Mouse captured from HUD, not player controller +# TODO: Rotate climbing collider as you move WASD @onready var body = $"." @onready var head = $Head @onready var camera = $Head/Camera3D -@onready var interaction_ray = $Head/Camera3D/InteractionRay @export var mouse_sensitivity: float = 1.5 const base_fov = 90.0 @@ -29,17 +31,15 @@ const SENSITIVITY = 1.5 # Player statuses var mouse_captured: bool = false -# TODO: Rotate climbing collider as you move WASD - # Get the gravity from the project settings to be synced with RigidBody nodes. var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") func _enter_tree(): - set_multiplayer_authority(name.to_int()) - + set_multiplayer_authority(0) + func _ready(): camera.fov = base_fov - camera.current = is_multiplayer_authority() + camera.current = false func _input(event): if is_multiplayer_authority() == false: @@ -65,15 +65,15 @@ func _unhandled_input(event): get_viewport().set_input_as_handled() func _physics_process(delta): + # TODO: Simplify focus detection code from "mouse_captured". if is_multiplayer_authority() == false: return # Add the gravity. - check_if_interaction_ray_is_colliding() if not is_on_floor(): velocity.y -= gravity * delta + 0.05 - if Input.is_action_pressed("sprint"): + if Input.is_action_pressed("sprint") && mouse_captured == true: speed = lerp(speed, SPRINT_SPEED, delta * 7.0) var pos = Vector3.ZERO pos.y = 1.7 @@ -84,7 +84,7 @@ func _physics_process(delta): pos.y = 1.7 pos.z = -0.15 head.transform.origin = lerp(head.transform.origin, pos, delta * 7.0) - + if !is_on_floor(): speed = speed / 1.1 @@ -101,29 +101,15 @@ func _physics_process(delta): else: velocity.x = lerp(velocity.x, direction.x * speed, delta * 20.0) velocity.z = lerp(velocity.z, direction.z * speed, delta * 20.0) - - if Input.is_action_just_pressed("jump") and is_on_floor(): + + if Input.is_action_just_pressed("jump") && is_on_floor() && mouse_captured == true: velocity.y = JUMP_VELOCITY move_and_slide() _send_player_synchronization_info() - + func round_to_dec(num, digit): return round(num * pow(10.0, digit)) / pow(10.0, digit) - -# User interaction ray -func check_if_interaction_ray_is_colliding(): - if interaction_ray.is_colliding(): - var subscene_root = get_subscene_root(interaction_ray.get_collider()); - if subscene_root == null: - return - - if !subscene_root.is_in_group("interactable"): - return - - # Interact - if Input.is_action_just_pressed("interact"): - subscene_root.interact() func get_subscene_root(node: Node) -> Node: var current_node = node @@ -131,7 +117,7 @@ func get_subscene_root(node: Node) -> Node: return current_node else: return null - + func capture_mouse(to_capture: bool): if to_capture == false: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) @@ -144,11 +130,11 @@ func capture_mouse(to_capture: bool): func _send_player_synchronization_info(): if is_multiplayer_authority() == false: return - - var compressed_position = n_c.c_16_pos(position) - var compressed_rotation = n_c.c_16_vec3(rotation) + + var compressed_position = NetworkCompression.c_16_pos(position) + var compressed_rotation = NetworkCompression.c_16_vec3(rotation) # HACK: We are just appending the rotation bits at the end here. It should probably be more efficient somewhere else. compressed_position.append_array(compressed_rotation) - - rpc_lib.com.rpc("on_player_transform", compressed_position) + + scene_m.get_master_scene(scene_m.active_session).get_node("NetworkManager").entity_position.rpc(int(name), compressed_position) diff --git a/src/scripts/crypto/rsa.gd b/src/scripts/crypto/rsa.gd index e0064c2..11b0fa3 100644 --- a/src/scripts/crypto/rsa.gd +++ b/src/scripts/crypto/rsa.gd @@ -35,7 +35,7 @@ func pem_to_cryptokey(pem: String = "") -> CryptoKey: # TODO: Error checks var public_key := CryptoKey.new() if public_key.load_from_string(pem, true) != OK: - GlobalLogger.logs("Failed to load public key", 3) + GlobalLogger.logs("Failed to load public key", Enum.LogLevel.ERROR) return null - return public_key \ No newline at end of file + return public_key diff --git a/src/scripts/enum.gd b/src/scripts/enum.gd new file mode 100644 index 0000000..347eb13 --- /dev/null +++ b/src/scripts/enum.gd @@ -0,0 +1,42 @@ +# --- License +# File: /client/src/scripts/enum.gd +# Project: OpenMinerva +# Created Date: 13 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +enum BaseLevel { + DEBUG = 0, + EMPTY = 1, + GRID = 2, +} + +enum PrivacyLevel { + INVITE = 0, + PUBLIC = 1, + CONTACTS_PLUS = 2, + CONTACTS = 3, + FRIENDS_PLUS = 4, + FRIENDS = 5 +} + +const Settings = { + Graphics = { + DisplayMode = { + FULLSCREEN = 0, + WINDOWED = 1, + BORDERLESS = 2 + } + } +} + +enum LogLevel { + DEBUG = 0, + INFO = 1, + WARNING = 2, + ERROR = 3, +} diff --git a/src/scripts/enum.gd.uid b/src/scripts/enum.gd.uid new file mode 100644 index 0000000..26a0b4d --- /dev/null +++ b/src/scripts/enum.gd.uid @@ -0,0 +1 @@ +uid://dgpvcem71xdbn diff --git a/src/scripts/libs/account.gd b/src/scripts/libs/account.gd index 4b0f45c..ce4d330 100644 --- a/src/scripts/libs/account.gd +++ b/src/scripts/libs/account.gd @@ -9,7 +9,6 @@ extends Node -var http = preload("res://scripts/network/http.gd").new() var time_lib = preload("res://scripts/libs/time.gd").new() var random_lib = preload("res://scripts/utils/random.gd").new() var rsa_lib = preload("res://scripts/crypto/rsa.gd").new() @@ -22,6 +21,7 @@ var stop_connection_timer = false var active_account = {} var _database = [] +var dev_session_server_api_key = "" func _ready(): _load_account_database() @@ -39,19 +39,19 @@ func create(account: Dictionary, type: String) -> Dictionary: account_formatted = _create_oauth(account) if len(account_formatted.keys()) == 0: - GlobalLogger.logs("Tried to create an account, but there was nothing to save.", 3) + GlobalLogger.logs("Tried to create an account, but there was nothing to save.", Enum.LogLevel.ERROR) return {"ok": false, "error": "No account formatted.", "id": null} _database.append(account_formatted) _save_account_database() - + Events.emit_signal("dash_account_list_loaded") return {"ok": true, "id": account_formatted.id} func _create_oauth(account) -> Dictionary: var _account_keys = rsa_lib.generate_keypair() - + var _clean_account = {} _clean_account.id = random_lib.random_string(6, true) _clean_account.display_name = account.get("display_name", null) @@ -63,7 +63,7 @@ func _create_oauth(account) -> Dictionary: _clean_account.refresh_token = "" _clean_account.id_token = "" _clean_account.access_token_expiry = 0 - + _clean_account.type = "oauth" return _clean_account @@ -80,7 +80,7 @@ func remove(id: String) -> Dictionary: ## Sets an account as the active account. func use(id: String) -> void: - GlobalLogger.logs("Setting active account to '%s'." % id, 1) + GlobalLogger.logs("Setting active account to '%s'." % id, Enum.LogLevel.INFO) var _account = _get_account_by_id(id) if _account.type == "oauth": @@ -114,7 +114,7 @@ func update(id: String, data: Dictionary) -> void: _save_account_database() return -func authenticate_oauth(id: String, remember_me: bool = false) -> void: +func authenticate_oauth(id: String, _remember_me: bool = false) -> void: # TODO: Error checks GlobalLogger.logs("Attempting to connect account '%s' using oauth." % id) var account = _get_account_by_id(id) @@ -137,11 +137,11 @@ func _save_account_database() -> void: ## Read the account database from the config file on our disk. func _load_account_database() -> Array: - GlobalLogger.logs("Loading the local account database.", 1) + GlobalLogger.logs("Loading the local account database.", Enum.LogLevel.INFO) var account_file_exists = FileAccess.file_exists(ACCOUNT_DATABASE_DIRECTORY) if account_file_exists == false: - GlobalLogger.logs("Account database does not exist, creating one now.", 1) + GlobalLogger.logs("Account database does not exist, creating one now.", Enum.LogLevel.INFO) _save_account_database() var file = FileAccess.open(ACCOUNT_DATABASE_DIRECTORY, FileAccess.READ) @@ -151,7 +151,7 @@ func _load_account_database() -> Array: if file: account_data = file.get_var() # Deserializes variable back file.close() - + _database = account_data return account_data @@ -165,7 +165,7 @@ func _get_account_by_id(id: String) -> Dictionary: func _update_account_by_key(id: String, key: String, value: Variant) -> void: var index = _database.find_custom(func(entry): return entry.get("id") == id) - # TODO: Check if key is a valid key. + # TODO: Check if key is a valid key. _database[index][key] = value _save_account_database() return @@ -188,33 +188,15 @@ func _handle_response(response: Dictionary) -> Dictionary: if response.get("ok", false) == false: response_data.error = "Request failed for unknown reason." - GlobalLogger.logs(response_data.error, 3) + GlobalLogger.logs(response_data.error, Enum.LogLevel.ERROR) return response_data if response.get("body", null) == null: response_data.error = "No body provided from the request." - GlobalLogger.logs(response_data.error, 3) + GlobalLogger.logs(response_data.error, Enum.LogLevel.ERROR) return response_data response_data.ok = true response_data.body = JSON.parse_string(response.get("body")) return response_data - -# DEV: Upload public key to the server. -# func test_upload_public_key_to_server(): -# GlobalLogger.logs("Registering the device public key to the account server.") -# var body = { -# "public_key": active_account.public_device_key -# } -# var url_parts = UrlParser.deconstruct(active_account.account_server) -# if url_parts.ok == false: -# GlobalLogger.logs("Unhandled error registering the public device key to the account server. '%s'" % url_parts.error, 3) -# return -# url_parts = url_parts.data - -# print(url_parts) - -# var public_key_response = await http.req(HTTPClient.Method.METHOD_POST, url_parts.host, "/api/v1/device_key", url_parts.port, ["Accept: application/json", "Content-Type: application/json", "authorization: Bearer %s" % oauth_lib.access_token], JSON.stringify(body)) -# print(public_key_response) -# return diff --git a/src/scripts/libs/account_server.gd b/src/scripts/libs/account_server.gd deleted file mode 100644 index 644f41f..0000000 --- a/src/scripts/libs/account_server.gd +++ /dev/null @@ -1,20 +0,0 @@ -extends Node - -var _http = preload("res://scripts/network/http.gd").new() -var _database = {} - -func get_public_key(url: String): - GlobalLogger.logs("Requesting public key from account server '%s'" % url, 1) - - # Check if we already have the public key in our database - - # If we do, return that data. - # Otherwise, http request the account server. - # Validate the response is valid. - # Add extra metadata to the database entry. - # Save to the database. - return - -func reset_database(): - GlobalLogger.logs("Clearing the account server database.", 1) - _database = {} \ No newline at end of file diff --git a/src/scripts/libs/account_server.gd.uid b/src/scripts/libs/account_server.gd.uid deleted file mode 100644 index 8de3a64..0000000 --- a/src/scripts/libs/account_server.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dm1co8xfx6i2x diff --git a/src/scripts/libs/bootstrap.gd b/src/scripts/libs/bootstrap.gd new file mode 100644 index 0000000..0b88636 --- /dev/null +++ b/src/scripts/libs/bootstrap.gd @@ -0,0 +1,27 @@ +# --- License +# File: /client/src/scripts/libs/bootstrap.gd +# Project: OpenMinerva +# Created Date: 21 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_CLOSE_REQUEST: + GlobalLogger.logs("Shutting down") + + # Shutdown / leave all servers. + for server in network_m.get_connected_sessions(): + if server.type == "host": + network_m.stop_server(server.id) + continue + if server.type == "client": + network_m.leave_server(server.id) + continue + + get_tree().quit() diff --git a/src/scripts/libs/bootstrap.gd.uid b/src/scripts/libs/bootstrap.gd.uid new file mode 100644 index 0000000..0343796 --- /dev/null +++ b/src/scripts/libs/bootstrap.gd.uid @@ -0,0 +1 @@ +uid://cvj10vdw6hlqw diff --git a/src/scripts/libs/jwt.gd b/src/scripts/libs/jwt.gd index 55e9803..495416b 100644 --- a/src/scripts/libs/jwt.gd +++ b/src/scripts/libs/jwt.gd @@ -31,14 +31,14 @@ func validate(jwt_string: String, public_spki: String): var jwt_parts: Dictionary = _get_parts(jwt_string) var public_key: CryptoKey = pem_to_cryptokey(public_spki) if jwt_parts.ok != true: - GlobalLogger.logs("Failed to deconstruct jwt when verifying jwt.", 2) - GlobalLogger.logs(str(jwt_string), 0) + GlobalLogger.logs("Failed to deconstruct jwt when verifying jwt.", Enum.LogLevel.WARNING) + GlobalLogger.logs(str(jwt_string)) return false var formatted_payload: Dictionary = _format_payload_for_verification(jwt_parts.head, jwt_parts.payload) if formatted_payload.ok != true: - GlobalLogger.logs("Failed to format the jwt payload when verifying jwt.", 2) - GlobalLogger.logs(str(jwt_parts), 0) + GlobalLogger.logs("Failed to format the jwt payload when verifying jwt.", Enum.LogLevel.WARNING) + GlobalLogger.logs(str(jwt_parts)) return false return crypto.verify( @@ -71,14 +71,14 @@ func base64_to_base64url(input_value: String): func _get_parts(input_value: String): # TODO: Error checks var return_dict = {"ok": false, "error": "", "head": "", "payload": "", "signature": ""} - + var jwt_split = input_value.split(".") if len(jwt_split) != 3: - GlobalLogger.logs("JWT is not formatted correctly.", 2) + GlobalLogger.logs("JWT is not formatted correctly.", Enum.LogLevel.WARNING) return_dict.error = "JWT is not formatted correctly." return return_dict - + return_dict.head = jwt_split[0] return_dict.payload = jwt_split[1] return_dict.signature = base64url_to_base64(jwt_split[2]) @@ -106,7 +106,7 @@ func pem_to_cryptokey(pem: String = "") -> CryptoKey: # TODO: Error checks var public_key := CryptoKey.new() if public_key.load_from_string(pem, true) != OK: - GlobalLogger.logs("Failed to load public key", 3) + GlobalLogger.logs("Failed to load public key", Enum.LogLevel.ERROR) return null - return public_key \ No newline at end of file + return public_key diff --git a/src/scripts/libs/oauth.gd b/src/scripts/libs/oauth.gd index 858ccf3..7caa97a 100644 --- a/src/scripts/libs/oauth.gd +++ b/src/scripts/libs/oauth.gd @@ -9,7 +9,6 @@ extends Node -var http = preload("res://scripts/network/http.gd").new() var jwt_lib = preload("res://scripts/libs/jwt.gd").new() var random_lib = preload("res://scripts/utils/random.gd").new() @@ -28,7 +27,7 @@ func authenticate(account_server: String) -> Dictionary: account_server_url = account_server_url.data - GlobalLogger.logs("Starting OAuth flow.", 0) + GlobalLogger.logs("Starting OAuth flow.") var uri_parts := [ "client_id=%s" % "OpenMinerva-Game-Client", @@ -44,16 +43,16 @@ func authenticate(account_server: String) -> Dictionary: var uri = account_server + "?" + "&".join(uri_parts) OS.shell_open(uri) - GlobalLogger.logs("Starting OAuth redirect server.", 0) + GlobalLogger.logs("Starting OAuth redirect server.") redirect_server.listen(port, bind_address) _listen_for_oauth_connections = true var _auth_code = await _wait_for_auth_code() - - GlobalLogger.logs("Closing OAuth redirect server.", 0) + + GlobalLogger.logs("Closing OAuth redirect server.") redirect_server = TCPServer.new() - GlobalLogger.logs("Exchanging retrieved auth code for a proper token.", 0) + GlobalLogger.logs("Exchanging retrieved auth code for a proper token.") var form_parts := [ "client_id=%s" % "OpenMinerva-Game-Client", "grant_type=authorization_code", @@ -63,7 +62,7 @@ func authenticate(account_server: String) -> Dictionary: "code_verifier=%s" % secret_pkce, ] var form_string: String = "&".join(form_parts) - var exchange_response = await http.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) + var exchange_response = await HTTP.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) var token_data = JSON.parse_string(exchange_response.get("body")) return _get_tokens_from_response(token_data) @@ -96,7 +95,7 @@ func _handle_auth_callback(connection: StreamPeerTCP) -> String: var temp_auth_code: String = request.split("code=")[1].split("&iss=")[0].strip_edges() - GlobalLogger.logs("Got authentication code: '%s'." % temp_auth_code, 0) + GlobalLogger.logs("Got authentication code: '%s'." % temp_auth_code) # Send success. var html_response = "HTTP/1.1 200 OK\r\n" @@ -135,13 +134,13 @@ func validate_token(account: Dictionary) -> bool: var account_server_url = UrlParser.deconstruct(account.account_server) if account_server_url.ok == false: - GlobalLogger.logs("Failed to parse account server url.", 3) + GlobalLogger.logs("Failed to parse account server url.", Enum.LogLevel.WARNING) return false account_server_url = account_server_url.data - var introspect_response = await http.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token/introspection", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) + var introspect_response = await HTTP.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token/introspection", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) if introspect_response.ok == false: - GlobalLogger.logs("Unknown error parsing the introspection response.", 3) + GlobalLogger.logs("Unknown error parsing the introspection response.", Enum.LogLevel.WARNING) return false introspect_response = JSON.parse_string(introspect_response.body) diff --git a/src/scripts/logger.gd b/src/scripts/logger.gd index b2ae399..f547841 100644 --- a/src/scripts/logger.gd +++ b/src/scripts/logger.gd @@ -32,25 +32,19 @@ func _initialize_log_file(): log_file_path = FileManager.create_log_file() log_file = FileAccess.open(log_file_path, FileAccess.WRITE) log_file_initialized = true - logs("Opened log file at %s" % log_file_path, 0) + logs("Opened log file at %s" % log_file_path) ## Logs a message to both file and console (if enabled). ## @param message: The message string to log. If omitted, defaults to an empty string. ## @param level: The log level indicating the severity. Must be an integer: -## 0 -> Debug -## 1 -> Info -## 2 -> Warning -## 3 -> Error -## Defaults to 0 (Debug). -func logs(message: String = "", level: int = 0): +func logs(message: String = "", level: Enum.LogLevel = Enum.LogLevel.DEBUG): _log_to_file(message, level) if console_logging_enabled: - print_rich("[[color=%s]%s[/color]] %s" % [log_level_colors[level], log_level_names[level], message]) + var stack := get_stack() + print_rich("[[color=%s]%s[/color]] %s [[color=lightyellow]%s[/color]]" % [log_level_colors[level], log_level_names[level], message, stack[1]["function"]]) if level == 3: # We are skipping the first frame, otherwise this function will be logged. - var stack := get_stack() - for i in range(1, stack.size()): var frame = stack[i] print( @@ -71,15 +65,15 @@ func _log_to_file(message: String = "", level: int = 0): func set_console_logging(enabled: bool): if enabled: console_logging_enabled = true - logs("Console logging enabled for this session.", 1) + logs("Console logging enabled for this session.", Enum.LogLevel.INFO) else: console_logging_enabled = false - logs("Console logging disabled for this session.", 1) + logs("Console logging disabled for this session.", Enum.LogLevel.INFO) func set_file_logging(enabled: bool): if enabled: file_logging_enabled = true - logs("File logging enabled for this session.", 1) + logs("File logging enabled for this session.", Enum.LogLevel.INFO) else: file_logging_enabled = false - logs("File logging disabled for this session.", 1) + logs("File logging disabled for this session.", Enum.LogLevel.INFO) diff --git a/src/scripts/managers/settings.gd b/src/scripts/managers/settings.gd new file mode 100644 index 0000000..842450c --- /dev/null +++ b/src/scripts/managers/settings.gd @@ -0,0 +1,93 @@ +# --- License +# File: /client/src/scripts/managers/settings.gd +# Project: OpenMinerva +# Created Date: 16 April 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +var _settings = {} + +func _ready(): + _load_settings() + +# Settings versioning +# Settings file upgrade + +# Get setting +func get_session_servers() -> Array: + var return_arr = [] + + return_arr = _settings.get("config", {}).get("session_servers", []) + + return return_arr + +func add_session_server(session_server: Dictionary) -> bool: + var _name = session_server.get("name", "") + var _url = session_server.get("url", "") + var _date_added = Time.get_unix_time_from_system() + + _settings.config.session_servers.append({"name": _name, "url": _url, "date_added": _date_added}) + _save_settings() + return true + +func remove_session_server(url: String) -> bool: + var _index = -1 + for i in range(_settings.config.session_servers.size()): + if _settings.config.session_servers[i]["url"] == url: + _index = i + break + + if _index != -1: + _settings.config.session_servers.remove_at(_index) + + _save_settings() + return false + +func _save_settings(): + var _file = FileAccess.open("user://settings/current.json", FileAccess.WRITE) + var _settings_string: String = JSON.stringify(_settings) + _file.store_string(_settings_string) + _file.close() + GlobalLogger.logs("Saved settings file.", Enum.LogLevel.INFO) + return + +func _load_settings() -> void: + var _settings_exist: bool = FileAccess.file_exists("user://settings/current.json") + if _settings_exist: + var _file = FileAccess.open("user://settings/current.json", FileAccess.READ) + var _content = _file.get_as_text() + var _parsed = JSON.parse_string(_content) + _settings = _parsed + GlobalLogger.logs("Settings have been loaded.", Enum.LogLevel.INFO) + return + + # TODO: Check if backup settings exist. + GlobalLogger.logs("Settings file does not exist, creating new settings file.", Enum.LogLevel.INFO) + FileManager.create_file("user://settings/", "current.json") + _settings = _templates.settings_file + _save_settings() + GlobalLogger.logs("Blank settings have been loaded.", Enum.LogLevel.INFO) + return + +const _templates = { + # The full settings file that is saved and stored. + "settings_file": { + "graphics": { + "display_mode": Enum.Settings.Graphics.DisplayMode.FULLSCREEN + }, + "config": { + "session_servers": [] + } + }, + + # Small templates that are duplicated and used to + "session_server": { + "name": "", + "url": "", + "date_added": int(0) + } +} diff --git a/src/scripts/managers/settings.gd.uid b/src/scripts/managers/settings.gd.uid new file mode 100644 index 0000000..ab6ac06 --- /dev/null +++ b/src/scripts/managers/settings.gd.uid @@ -0,0 +1 @@ +uid://bj4giyk05v042 diff --git a/src/scripts/network/account_servers.gd b/src/scripts/network/account_servers.gd index a0126e7..4227491 100644 --- a/src/scripts/network/account_servers.gd +++ b/src/scripts/network/account_servers.gd @@ -1,6 +1,5 @@ extends Node -var _http = preload("res://scripts/network/http.gd").new() var _database = {} # TODO: Save public account server keys to disk @@ -8,7 +7,7 @@ var _database = {} func get_public_key(host: String, port: int) -> String: if host in _database: return _database[host] - + var response: Dictionary = await _request_server_pem(host, port) if response.ok == true: _database[host] = response.data @@ -19,9 +18,9 @@ func get_public_key(host: String, port: int) -> String: func _request_server_pem(host: String, port: int = 443) -> Dictionary: GlobalLogger.logs("Requesting server '%s:%s'." % [host, port]) var return_dict = {"ok": false, "data": ""} - var key = await _http.req(HTTPClient.METHOD_GET, host, "/public_key", port) + var key = await HTTP.req(HTTPClient.METHOD_GET, host, "/public_key", port) if key.ok == true: return_dict.data = key.body return_dict.ok = true return return_dict - return return_dict \ No newline at end of file + return return_dict diff --git a/src/scripts/network/network_compression.gd b/src/scripts/network/network_compression.gd index 8335692..5fa51cf 100644 --- a/src/scripts/network/network_compression.gd +++ b/src/scripts/network/network_compression.gd @@ -13,12 +13,12 @@ func c_32_vec3(provided_data: Vector3) -> PackedByteArray: return data -## Decompress a PackedByteArray to a Vector3 using 32 bit precision +## Decompress a PackedByteArray to a Vector3 using 32 bit precision ## @returns Vector3 func d_32_vec3(provided_data: PackedByteArray) -> Vector3: # Validate array size if provided_data.size() < 12: - GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", Enum.LogLevel.WARNING) return Vector3() var x = _int_to_float(provided_data.decode_s32(0)) @@ -38,12 +38,12 @@ func c_16_vec3(provided_data: Vector3) -> PackedByteArray: return data -## Decompress a PackedByteArray to a Vector3 using 16 bit precision +## Decompress a PackedByteArray to a Vector3 using 16 bit precision ## @returns Vector3 func d_16_vec3(provided_data: PackedByteArray) -> Vector3: # Validate array size if provided_data.size() < 6: - GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", 2) + GlobalLogger.logs("'%s' contained invalid PackedByteArray size. Can not decode value.", Enum.LogLevel.WARNING) return Vector3() var x = _int_to_float(provided_data.decode_s16(0)) @@ -162,4 +162,4 @@ func _float_to_int(val: float) -> int: func _int_to_float(val: int) -> float: const FLOAT_PRECISION: int = 1000 - return float(val) / FLOAT_PRECISION \ No newline at end of file + return float(val) / FLOAT_PRECISION diff --git a/src/scripts/rpc/client.gd b/src/scripts/rpc/client.gd deleted file mode 100644 index 971d404..0000000 --- a/src/scripts/rpc/client.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends Node - -# Join server -# Leave server -# Kicked from server -# Banned from server - -@onready var scene_manager = get_tree().current_scene.get_node("SceneManager") -@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") - -func connected_to_server(): - return - -func connection_failed(): - return - -@rpc("authority", "reliable") -func on_receive_server_info(info): - GlobalLogger.logs("Got server info!") - if info.level: - await scene_manager.load_multiplayer_scene(info.level, info.level_node_name) - get_parent().s.rpc_id(1, "on_receive_player_info", GlobalAccount.active_account.get("public_account_server_passport", {}).get("token", null)) - return - -@rpc("authority", "reliable") -func received_server_session_info(received_info: Dictionary) -> void: - GlobalLogger.logs("Session information updated.") - network_manager.info = received_info - return \ No newline at end of file diff --git a/src/scripts/rpc/client.gd.uid b/src/scripts/rpc/client.gd.uid deleted file mode 100644 index 44f228a..0000000 --- a/src/scripts/rpc/client.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dsbv3frxchi71 diff --git a/src/scripts/rpc/common.gd b/src/scripts/rpc/common.gd deleted file mode 100644 index 4eb90cc..0000000 --- a/src/scripts/rpc/common.gd +++ /dev/null @@ -1,39 +0,0 @@ -extends Node - -var n_c = preload("res://scripts/network/network_compression.gd").new() -@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") - -@rpc("any_peer", "unreliable") -func on_player_transform(info): - # TODO: Authenticate - var target_node = network_manager.player_exists(str(multiplayer.get_remote_sender_id())) - - if target_node == null: - return - - # HACK: The rotation data is hacked on here. This needs to be addressed at some point. - target_node.position = n_c.d_16_pos(info) - target_node.rotation = n_c.d_16_vec3(info.slice(12)) - return - - -@rpc("any_peer", "unreliable") -func on_node_transform() -> void: - # Handles changing positions of a node. - # This should only be used when a node is moving, and not to position a node on spawn. - # TODO: Authenticate - return - -@rpc("authority", "reliable") -func on_spawn_player(id) -> void: - var player_scene: PackedScene = load("res://scenes/players/player.tscn") - GlobalLogger.logs("Spawning player %s" % id) - var new_player = player_scene.instantiate() - new_player.name = str(id) - new_player.position = Vector3(0, 0, 0) - network_manager.spawn_player(new_player) - return - -@rpc("authority", "reliable") -func on_spawn_node() -> void: - return diff --git a/src/scripts/rpc/common.gd.uid b/src/scripts/rpc/common.gd.uid deleted file mode 100644 index 896d644..0000000 --- a/src/scripts/rpc/common.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8cnojblfb8ly diff --git a/src/scripts/rpc/server.gd b/src/scripts/rpc/server.gd deleted file mode 100644 index 0666354..0000000 --- a/src/scripts/rpc/server.gd +++ /dev/null @@ -1,99 +0,0 @@ -# --- License -# File: /client/src/scripts/rpc/server.gd -# Project: OpenMinerva -# Created Date: 05 February 2026 -# Copyright (c) 2026 OpenMinerva -# License: MIT License -# Authors: Armored Dragon -# --- License - -extends Node - -var n_c = preload("res://scripts/network/network_compression.gd").new() -var jwt = preload("res://scripts/libs/jwt.gd").new() -var rsa = preload("res://scripts/crypto/rsa.gd").new() -var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") - -@onready var network_manager = get_tree().current_scene.get_node("NetworkManager") - -# Create server -# Update server -# Close server - -# On player connecting -# On player connected -# On player leaving -# On player kicked -# On player banned - -func on_peer_connected(peer_id): - if multiplayer.is_server() == false: - return - - GlobalLogger.logs("[%s] Peer connected: '%s'. Sending server info." % [multiplayer.get_unique_id(), peer_id]) - get_parent().c.rpc_id(peer_id, "on_receive_server_info", network_manager.info) - -func on_peer_disconnected(): - return - -@rpc("any_peer", "reliable") -func on_receive_player_info(info) -> void: - if multiplayer.is_server() == false: - return - var sender_id = multiplayer.get_remote_sender_id() - GlobalLogger.logs("Got client info!") - var player_info = jwt.decode(info) - - if player_info.ok != true: - GlobalLogger.logs("Unknown error decoding player JWT.", 3) - - player_info = player_info.data - var url_parts = _parse_url(player_info.payload.issuer) - var host_pub_key = await AccountServers._request_server_pem(url_parts.host, url_parts.port) - - if host_pub_key.ok != true: - GlobalLogger.logs("Unknown error retrieving account server Public PEM.", 3) - - host_pub_key = host_pub_key.data - var jwt_is_valid = rsa.verify_jwt_signature(info, host_pub_key) - - if jwt_is_valid == false: - GlobalLogger.logs("JWT signature did not match.", 1) - multiplayer.multiplayer_peer.disconnect_peer(sender_id) - # TODO: Send a message before kicking the user. - return - - player_info.payload["multiplayer_id"] = sender_id - - network_manager.info.clients.append(player_info.payload) - - get_parent().com.on_spawn_player(sender_id) - get_parent().com.rpc("on_spawn_player", sender_id) - - for client in network_manager.info.clients: - if client.multiplayer_id == sender_id: - continue - get_parent().com.rpc_id(sender_id, "on_spawn_player", client.multiplayer_id) - - send_server_info() - -func send_server_info(): - get_parent().c.rpc("received_server_session_info", network_manager.info) - return - -func _parse_url(url: String) -> Dictionary: - var result = { - "scheme": "", - "host": "", - "port": 0, - "path": "" - } - - var matches = url_regex.search(url) - if matches: - result["scheme"] = matches.get_string(1).to_lower() - result["host"] = matches.get_string(2) - result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) - result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" - - return result diff --git a/src/scripts/rpc/server.gd.uid b/src/scripts/rpc/server.gd.uid deleted file mode 100644 index 4976943..0000000 --- a/src/scripts/rpc/server.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6wrk7xh520cr diff --git a/src/scripts/signal_bus.gd b/src/scripts/signal_bus.gd index acdea3c..aa5b93b 100644 --- a/src/scripts/signal_bus.gd +++ b/src/scripts/signal_bus.gd @@ -8,13 +8,33 @@ # --- License extends Node + # Dashboard +@warning_ignore("unused_signal") signal dash_set_state(is_open: bool) +@warning_ignore("unused_signal") signal dash_switch_tab(page_name: String) +@warning_ignore("unused_signal") signal dash_active_account_changed(account: Dictionary) +@warning_ignore("unused_signal") signal dash_storage_changed(storage_data: Dictionary) +@warning_ignore("unused_signal") signal dash_session_changed(session_data: Dictionary) +@warning_ignore("unused_signal") signal dash_message_received(message: Dictionary) +@warning_ignore("unused_signal") signal dash_notification(notification: Dictionary) +@warning_ignore("unused_signal") +signal dash_account_list_loaded(account_list: PackedStringArray) + +# Instance +@warning_ignore("unused_signal") +signal instance_updated(instance: Dictionary) +@warning_ignore("unused_signal") +signal instance_root_changed(instance: Dictionary) -signal dash_account_list_loaded(account_list: PackedStringArray) \ No newline at end of file +# Multiplayer +@warning_ignore("unused_signal") +signal session_joined(instance: Dictionary) +@warning_ignore("unused_signal") +signal session_left(instance: Dictionary) \ No newline at end of file diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index 963a26d..b1d64df 100644 --- a/src/scripts/utils/files.gd +++ b/src/scripts/utils/files.gd @@ -2,7 +2,7 @@ extends Node ## Creates a log file following the internal format. func create_log_file() -> String: - GlobalLogger.logs("Creating a log file for this session.", 0) + GlobalLogger.logs("Creating a log file for this session.") _maybe_make_directory("user://logs/") # TODO: Sanataize param # TODO: Error checks @@ -11,14 +11,15 @@ func create_log_file() -> String: var log_file_path = "user://logs/%s.%s" % [ProjectSettings.get_setting("application/config/name"), log_file_name] var file = FileAccess.open(log_file_path, FileAccess.WRITE) file.close() - GlobalLogger.logs("Log file '%s' created." % log_file_path, 0) + GlobalLogger.logs("Log file '%s' created." % log_file_path) return log_file_path -func create_client_file(_dir: String) -> void: - # Create a file to store in-game user data. This is for in-game data storage! - # IMPORTANT: DO NOT STORE PRIVATE DATA IN THIS DIRECTORY AS IT IS INTENDED TO BE READ AND WRITTEN TO FREELY! - GlobalLogger.logs("Not implemented.", 3) - return +func create_file(dir: String, file_name: String) -> void: + _maybe_make_directory(dir) + # TODO: Sanitize name + var file = FileAccess.open("%s/%s" % [dir, file_name], FileAccess.WRITE) + GlobalLogger.logs("File '%s' created at '%s'." % [file_name, dir]) + file.close() func _maybe_make_directory(dir: String): var dir_access = DirAccess.open("user://") diff --git a/src/userinterface/dash/account_create.gd b/src/userinterface/dash/account_create.gd index 19d6233..81b7f4b 100644 --- a/src/userinterface/dash/account_create.gd +++ b/src/userinterface/dash/account_create.gd @@ -18,7 +18,7 @@ var _page_names = [] func _ready(): _get_pages() select_oauth_btn.pressed.connect(_display_login_route.bind("OAuth")) - + create_oauth_btn.pressed.connect(_create_oauth) create_oauth_back_btn.pressed.connect(_display_login_route.bind("SelectMethod")) return @@ -28,7 +28,7 @@ func _display_oauth(): func _display_login_route(page_name: String): if page_name not in _page_names: - GlobalLogger.logs("Tried to display an invalid login route.", 3) + GlobalLogger.logs("Tried to display an invalid login route.", Enum.LogLevel.WARNING) return for page in create.get_children(): @@ -53,7 +53,7 @@ func _create_oauth() -> void: "account_server": account_server } - var res = GlobalAccount.create(account, "oauth") + GlobalAccount.create(account, "oauth") Events.emit_signal("dash_switch_tab", "AccountDisplay") diff --git a/src/userinterface/dash/exit.gd b/src/userinterface/dash/exit.gd index 4bfd48b..75b8331 100644 --- a/src/userinterface/dash/exit.gd +++ b/src/userinterface/dash/exit.gd @@ -12,18 +12,18 @@ extends Control @onready var _exit_button = get_node("VBoxContainer/HBoxContainer/Exit") func _ready(): - _cancel_button.pressed.connect(_handle_cancel_pressed) - _exit_button.pressed.connect(_handle_exit_pressed) - return + _cancel_button.pressed.connect(_handle_cancel_pressed) + _exit_button.pressed.connect(_handle_exit_pressed) + return func _handle_cancel_pressed(): - Events.emit_signal("dash_switch_tab", "Home") - return + Events.emit_signal("dash_switch_tab", "Home") + return func _handle_exit_pressed(): - # TODO: Save? - # TODO: Sync? - # TODO: Validate database? - # TODO: Prune cache? - get_tree().quit() - return \ No newline at end of file + # TODO: Save? + # TODO: Sync? + # TODO: Validate database? + # TODO: Prune cache? + get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST) + return diff --git a/src/userinterface/dash/home.gd b/src/userinterface/dash/home.gd index e765a11..9166e48 100644 --- a/src/userinterface/dash/home.gd +++ b/src/userinterface/dash/home.gd @@ -9,17 +9,28 @@ extends Control +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") +@onready var scene_m = get_tree().current_scene.get_node("SceneManager") + @onready var account_card_container = get_node("HBoxContainer/VBoxContainer/AccountDisplay") @onready var storage_card_container = get_node("HBoxContainer/VBoxContainer/StorageDisplay") +@onready var active_sessions_container = get_node("HBoxContainer/VBoxContainer/ActiveSessions") @onready var session_card_container = get_node("HBoxContainer/VBoxContainer3/SessionDisplay") +@onready var active_session_template = get_node("Templates/ActiveSessionButton") + func _ready(): account_card_container.get_node("Button").pressed.connect(Events.emit_signal.bind("dash_switch_tab", "AccountDisplay")) - + Events.connect("dash_active_account_changed", _handle_active_account_changed) Events.connect("dash_storage_changed", _handle_storage_changed) Events.connect("dash_session_changed", _handle_session_changed) - + + Events.connect("session_joined", _display_active_sessions) + Events.connect("session_left", _display_active_sessions) + + _display_active_sessions() + return func _handle_active_account_changed(account: Dictionary) -> void: @@ -35,3 +46,24 @@ func _handle_storage_changed(storage_data: Dictionary) -> void: func _handle_session_changed(session_data: Dictionary) -> void: session_card_container.get_node("MarginContainer/HBoxContainer/VBoxContainer/SessionName").text = session_data.session_name return + +func _display_active_sessions() -> void: + for node in active_sessions_container.get_node("MarginContainer/VBoxContainer").get_children(): + if node is not Label: + node.queue_free() + + var _sessions = network_m.get_connected_sessions() + + for session in _sessions: + var _entry = active_session_template.duplicate() + var _entry_label = _entry.get_node("HBoxContainer/Join") + var _entry_close = _entry.get_node("HBoxContainer/Close") + + _entry_label.text = session.id + _entry_label.pressed.connect(scene_m.set_active_session.bind(session.id)) + _entry_close.pressed.connect(network_m.leave_server.bind(session.id)) + + active_sessions_container.get_node("MarginContainer/VBoxContainer").add_child(_entry) + + # TODO: When button is pressed, focus that session + return diff --git a/src/userinterface/dash/hud.tscn b/src/userinterface/dash/hud.tscn index faebdd9..415a470 100644 --- a/src/userinterface/dash/hud.tscn +++ b/src/userinterface/dash/hud.tscn @@ -13,7 +13,6 @@ [ext_resource type="Texture2D" uid="uid://rdp5i1i18jus" path="res://resources/icons/search.svg" id="4_wfski"] [ext_resource type="Texture2D" uid="uid://fkfiymss8p57" path="res://resources/icons/science.svg" id="5_hu6eh"] [ext_resource type="Texture2D" uid="uid://br6hpysqvuhek" path="res://resources/icons/inventory.svg" id="5_r82dk"] -[ext_resource type="Texture2D" uid="uid://blkl4og334med" path="res://resources/icons/dummy16-9.webp" id="5_tkvwg"] [ext_resource type="Script" uid="uid://ftk8qksc10qg" path="res://userinterface/dash/account_list.gd" id="5_upsw6"] [ext_resource type="Script" uid="uid://cchqs4c1p1prd" path="res://userinterface/dash/account_create.gd" id="6_3rk53"] [ext_resource type="Texture2D" uid="uid://dp5u16rwxd0bp" path="res://resources/icons/apps.svg" id="6_6ybpt"] @@ -24,8 +23,10 @@ [ext_resource type="Texture2D" uid="uid://8fwxg1ipf0fp" path="res://resources/icons/exit.svg" id="9_amc1p"] [ext_resource type="Texture2D" uid="uid://ckoajckje5mq2" path="res://resources/icons/edit.svg" id="9_xdslj"] [ext_resource type="Texture2D" uid="uid://ddchrocw45muh" path="res://resources/icons/flowchart.svg" id="12_hq11n"] +[ext_resource type="Script" uid="uid://dpo2x4ddv3ftt" path="res://userinterface/dash/instance.gd" id="14_3ff87"] [ext_resource type="Script" uid="uid://crlujtfv2bh1w" path="res://userinterface/dash/contacts.gd" id="15_gll50"] [ext_resource type="Script" uid="uid://e1djdo6st2a7" path="res://userinterface/dash/exit.gd" id="17_3rk53"] +[ext_resource type="Script" uid="uid://cq26hbvdqb4sf" path="res://userinterface/dash/settings.gd" id="17_shwvw"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kgckq"] draw_center = false @@ -54,6 +55,10 @@ corner_radius_top_right = 100 corner_radius_bottom_right = 100 corner_radius_bottom_left = 100 +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_shwvw"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3ff87"] + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xdslj"] bg_color = Color(2.466701e-07, 0.13600668, 0.20061767, 1) @@ -199,6 +204,26 @@ layout_mode = 2 text = "2.5 GiB / 5 GiB" horizontal_alignment = 1 +[node name="ActiveSessions" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer" unique_id=1874646451] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/ActiveSessions" unique_id=591364507] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/ActiveSessions/MarginContainer" unique_id=1770756004] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer/VBoxContainer/ActiveSessions/MarginContainer/VBoxContainer" unique_id=66943091] +layout_mode = 2 +text = "Active Sessions" +horizontal_alignment = 1 + [node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/HBoxContainer" unique_id=1873874176] custom_minimum_size = Vector2(1000, 0) layout_mode = 2 @@ -262,6 +287,29 @@ layout_mode = 2 theme_override_font_sizes/font_size = 12 text = "157 ms" +[node name="Templates" type="Control" parent="MarginContainer/VBoxContainer/Master/Home" unique_id=725523853] +visible = false +layout_mode = 2 + +[node name="ActiveSessionButton" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Home/Templates" unique_id=1090596649] +layout_mode = 0 +offset_left = 20.0 +offset_top = 274.0 +offset_right = 436.0 +offset_bottom = 297.0 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Home/Templates/ActiveSessionButton" unique_id=1736548450] +layout_mode = 2 + +[node name="Join" type="Button" parent="MarginContainer/VBoxContainer/Master/Home/Templates/ActiveSessionButton/HBoxContainer" unique_id=375184485] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Close" type="Button" parent="MarginContainer/VBoxContainer/Master/Home/Templates/ActiveSessionButton/HBoxContainer" unique_id=797785460] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +text = "X" + [node name="AccountDisplay" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=1554068867] visible = false layout_mode = 1 @@ -710,649 +758,271 @@ size_flags_vertical = 3 [node name="GridContainer" type="GridContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer" unique_id=164218999] layout_mode = 2 +size_flags_horizontal = 3 theme_override_constants/h_separation = 15 theme_override_constants/v_separation = 15 columns = 4 -[node name="WorldListing" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=72097487] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing" unique_id=1805185846] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer" unique_id=1942216951] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer/AspectRatioContainer" unique_id=467585571] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer" unique_id=1711734675] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing/VBoxContainer/MarginContainer" unique_id=72959204] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=536349374] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2" unique_id=1668182649] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer" unique_id=2059453721] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer/AspectRatioContainer" unique_id=1701263601] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer" unique_id=969705443] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing2/VBoxContainer/MarginContainer" unique_id=925302443] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing3" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1068422016] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3" unique_id=1171639239] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer" unique_id=1823498012] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer/AspectRatioContainer" unique_id=224427286] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer" unique_id=1983590670] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing3/VBoxContainer/MarginContainer" unique_id=1182281080] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing4" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1272654877] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4" unique_id=44004522] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer" unique_id=684739377] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer/AspectRatioContainer" unique_id=775612232] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer" unique_id=2076427482] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing4/VBoxContainer/MarginContainer" unique_id=1542369134] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing5" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=502721862] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5" unique_id=1422523028] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer" unique_id=2104633633] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer/AspectRatioContainer" unique_id=1340850999] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer" unique_id=1085130516] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing5/VBoxContainer/MarginContainer" unique_id=1235572658] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing6" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=886702167] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6" unique_id=571121164] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer" unique_id=1647835786] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer/AspectRatioContainer" unique_id=644952396] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer" unique_id=630480358] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing6/VBoxContainer/MarginContainer" unique_id=1591667899] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing7" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1793074987] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7" unique_id=1047423560] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer" unique_id=514583078] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer/AspectRatioContainer" unique_id=1677760804] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer" unique_id=1909700952] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing7/VBoxContainer/MarginContainer" unique_id=732288817] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing8" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=774422407] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8" unique_id=1603622578] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer" unique_id=910974868] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer/AspectRatioContainer" unique_id=2089434396] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer" unique_id=118183461] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing8/VBoxContainer/MarginContainer" unique_id=1355563408] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing9" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=41675036] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9" unique_id=1960680163] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer" unique_id=1178627687] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer/AspectRatioContainer" unique_id=1776544938] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer" unique_id=186850142] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing9/VBoxContainer/MarginContainer" unique_id=1030586810] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing10" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=2084109082] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10" unique_id=225495079] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer" unique_id=376527793] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer/AspectRatioContainer" unique_id=1875807404] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer" unique_id=1306717136] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing10/VBoxContainer/MarginContainer" unique_id=1142312026] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing11" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1953856482] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11" unique_id=1886181112] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer" unique_id=248643867] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer/AspectRatioContainer" unique_id=251176411] -layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 - -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer" unique_id=209004049] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing11/VBoxContainer/MarginContainer" unique_id=1801139647] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing12" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=497402337] -layout_mode = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12" unique_id=602024177] -layout_mode = 2 - -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer" unique_id=437664178] -custom_minimum_size = Vector2(350, 210) -layout_mode = 2 -ratio = 1.7778 - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer/AspectRatioContainer" unique_id=625417018] +[node name="Templates" type="Control" parent="MarginContainer/VBoxContainer/Master/Sessions" unique_id=2135764135] +visible = false layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer" unique_id=1397258169] -layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing12/VBoxContainer/MarginContainer" unique_id=954774566] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +[node name="WorldListing" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates" unique_id=1301965468] +custom_minimum_size = Vector2(350, 270) +layout_mode = 0 +offset_left = 420.0 +offset_top = 56.0 +offset_right = 770.0 +offset_bottom = 326.0 + +[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing" unique_id=265222407] +layout_mode = 2 -[node name="WorldListing13" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1177446667] +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer" unique_id=2080859872] layout_mode = 2 +mouse_behavior_recursive = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13" unique_id=226169753] +[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer/VBoxContainer" unique_id=490931647] +custom_minimum_size = Vector2(0, 200) layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 2 -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer" unique_id=1228661081] -custom_minimum_size = Vector2(350, 210) +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer/VBoxContainer/MarginContainer2" unique_id=1546006054] layout_mode = 2 +size_flags_vertical = 3 ratio = 1.7778 -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer/AspectRatioContainer" unique_id=1369580510] +[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer/VBoxContainer/MarginContainer2/AspectRatioContainer" unique_id=1678639094] layout_mode = 2 -texture = ExtResource("5_tkvwg") expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer" unique_id=1763383998] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer/VBoxContainer" unique_id=1720674229] layout_mode = 2 -theme_override_constants/margin_left = 5 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 +theme_override_constants/margin_right = 10 theme_override_constants/margin_bottom = 15 -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing13/VBoxContainer/MarginContainer" unique_id=1551226535] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing/PanelContainer/VBoxContainer/MarginContainer" unique_id=2066703758] custom_minimum_size = Vector2(200, 20) layout_mode = 2 +size_flags_vertical = 1 +mouse_filter = 1 theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" autowrap_mode = 3 text_overrun_behavior = 3 max_lines_visible = 2 -[node name="WorldListing14" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1483002573] +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Master/Sessions/Templates/WorldListing" unique_id=1779962200] +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_kgckq") + +[node name="Instance" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=919985343] +visible = false +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("14_3ff87") + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance" unique_id=413032466] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14" unique_id=1690659101] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer" unique_id=1137764538] layout_mode = 2 +size_flags_vertical = 3 -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer" unique_id=926780909] -custom_minimum_size = Vector2(350, 210) +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer" unique_id=90422462] layout_mode = 2 -ratio = 1.7778 +size_flags_horizontal = 3 -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer/AspectRatioContainer" unique_id=541609792] +[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2" unique_id=1857065201] layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer" unique_id=838350665] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer" unique_id=193436950] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing14/VBoxContainer/MarginContainer" unique_id=516725602] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +theme_override_constants/margin_bottom = 5 -[node name="WorldListing15" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=422150807] +[node name="InstanceName" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer/MarginContainer" unique_id=1986372326] layout_mode = 2 +size_flags_horizontal = 3 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15" unique_id=1056594161] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer/MarginContainer/InstanceName" unique_id=1002302148] layout_mode = 2 +text = "Instance Name" -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer" unique_id=122088462] -custom_minimum_size = Vector2(350, 210) +[node name="InstanceNameField" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer/MarginContainer/InstanceName" unique_id=2029691082] layout_mode = 2 -ratio = 1.7778 +text = "My Instance" +placeholder_text = "Instance Name" -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer/AspectRatioContainer" unique_id=339376271] +[node name="PanelContainer2" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2" unique_id=1338599011] layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer" unique_id=1196852953] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer2" unique_id=372944118] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 +theme_override_constants/margin_bottom = 5 -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing15/VBoxContainer/MarginContainer" unique_id=263662711] -custom_minimum_size = Vector2(200, 20) +[node name="InstanceDescriptionContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer2/MarginContainer" unique_id=2036761912] layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +size_flags_horizontal = 3 -[node name="WorldListing16" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=570486665] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer2/MarginContainer/InstanceDescriptionContainer" unique_id=1778162350] layout_mode = 2 +text = "Instance Description" -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16" unique_id=1216909486] +[node name="InstanceDescription" type="TextEdit" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer2/PanelContainer2/MarginContainer/InstanceDescriptionContainer" unique_id=1883764719] +custom_minimum_size = Vector2(0, 100) layout_mode = 2 +text = "A basic instance description" +placeholder_text = "Instance Description" -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer" unique_id=1663707523] -custom_minimum_size = Vector2(350, 210) +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer" unique_id=725794580] layout_mode = 2 -ratio = 1.7778 +size_flags_horizontal = 3 -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer/AspectRatioContainer" unique_id=1455460820] +[node name="InstanceSettings" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer" unique_id=1058028038] layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer" unique_id=2097737858] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceSettings" unique_id=1068744384] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing16/VBoxContainer/MarginContainer" unique_id=199551998] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +theme_override_constants/margin_bottom = 5 -[node name="WorldListing17" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1480341877] +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceSettings/MarginContainer" unique_id=91242726] layout_mode = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17" unique_id=173416367] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceSettings/MarginContainer/HBoxContainer" unique_id=1980940677] layout_mode = 2 +size_flags_horizontal = 3 +text = "Max connected users" -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer" unique_id=1856884492] -custom_minimum_size = Vector2(350, 210) +[node name="MaxConnectedUsers" type="SpinBox" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceSettings/MarginContainer/HBoxContainer" unique_id=218460777] layout_mode = 2 -ratio = 1.7778 +min_value = 1.0 +max_value = 1000.0 +value = 1.0 +suffix = "Users" -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer/AspectRatioContainer" unique_id=755706003] +[node name="InstancePrivacy" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer" unique_id=418692061] layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer" unique_id=1890260584] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy" unique_id=520825558] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing17/VBoxContainer/MarginContainer" unique_id=942789936] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 - -[node name="WorldListing18" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=431108879] -layout_mode = 2 +theme_override_constants/margin_bottom = 5 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18" unique_id=1854699654] +[node name="InstancePrivacyContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer" unique_id=44619055] layout_mode = 2 +size_flags_horizontal = 3 -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer" unique_id=147007928] -custom_minimum_size = Vector2(350, 210) +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=1242833055] layout_mode = 2 -ratio = 1.7778 +text = "Instance Privacy" -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer/AspectRatioContainer" unique_id=834337291] +[node name="Public" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=700002280] +custom_minimum_size = Vector2(0, 50) layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 +toggle_mode = true +text = "Public" -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer" unique_id=195884175] +[node name="Contacts+" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=1719321337] +visible = false +custom_minimum_size = Vector2(0, 50) layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 +toggle_mode = true +text = "Contacts+" -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing18/VBoxContainer/MarginContainer" unique_id=1704660755] -custom_minimum_size = Vector2(200, 20) +[node name="Contacts" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=1878668850] +custom_minimum_size = Vector2(0, 40) layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +toggle_mode = true +text = "Contacts" -[node name="WorldListing19" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=113973206] +[node name="Friends+" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=1834008150] +visible = false +custom_minimum_size = Vector2(0, 50) layout_mode = 2 +toggle_mode = true +text = "Friends+" -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19" unique_id=856575337] +[node name="Friends" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=4967885] +custom_minimum_size = Vector2(0, 40) layout_mode = 2 +toggle_mode = true +text = "Friends" -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer" unique_id=2030015789] -custom_minimum_size = Vector2(350, 210) +[node name="InviteOnly" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer" unique_id=1625370876] +custom_minimum_size = Vector2(0, 40) layout_mode = 2 -ratio = 1.7778 +toggle_mode = true +text = "Invite" -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer/AspectRatioContainer" unique_id=1203134025] +[node name="InstanceAdvertisement" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer" unique_id=1453042159] +visible = false layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer" unique_id=796654597] +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceAdvertisement" unique_id=710014992] layout_mode = 2 theme_override_constants/margin_left = 5 theme_override_constants/margin_top = 5 theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 - -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing19/VBoxContainer/MarginContainer" unique_id=1022681998] -custom_minimum_size = Vector2(200, 20) -layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +theme_override_constants/margin_bottom = 5 -[node name="WorldListing20" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer" unique_id=1373407292] +[node name="Instance Name" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceAdvertisement/MarginContainer" unique_id=339732048] layout_mode = 2 +size_flags_horizontal = 3 -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20" unique_id=616061903] +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceAdvertisement/MarginContainer/Instance Name" unique_id=924196938] layout_mode = 2 +text = "Instance Advertisement" -[node name="AspectRatioContainer" type="AspectRatioContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer" unique_id=1272360847] -custom_minimum_size = Vector2(350, 210) +[node name="CheckButton" type="CheckButton" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceAdvertisement/MarginContainer/Instance Name" unique_id=476328324] layout_mode = 2 -ratio = 1.7778 +theme = ExtResource("1_cx7w0") +text = "Enable instance advertisement" -[node name="TextureRect" type="TextureRect" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer/AspectRatioContainer" unique_id=1244955368] +[node name="ItemList" type="ItemList" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer/VBoxContainer/InstanceAdvertisement/MarginContainer/Instance Name" unique_id=645896803] +custom_minimum_size = Vector2(0, 100) layout_mode = 2 -texture = ExtResource("5_tkvwg") -expand_mode = 2 +size_flags_vertical = 3 +theme_override_styles/cursor_unfocused = SubResource("StyleBoxEmpty_shwvw") +theme_override_styles/cursor = SubResource("StyleBoxEmpty_3ff87") +select_mode = 2 +item_count = 3 +item_0/text = "sessions.openminerva.org" +item_1/text = "sessions.otherservice1.com" +item_2/text = "sessions.otherservice2.com" -[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer" unique_id=1231885986] +[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer" unique_id=1107704271] +custom_minimum_size = Vector2(0, 50) layout_mode = 2 -theme_override_constants/margin_left = 5 -theme_override_constants/margin_top = 5 -theme_override_constants/margin_right = 5 -theme_override_constants/margin_bottom = 15 -[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Sessions/HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer/WorldListing20/VBoxContainer/MarginContainer" unique_id=7766220] -custom_minimum_size = Vector2(200, 20) +[node name="SaveChanges" type="Button" parent="MarginContainer/VBoxContainer/Master/Instance/VBoxContainer/HBoxContainer2" unique_id=175903443] layout_mode = 2 -theme_override_font_sizes/font_size = 18 -text = "Example test world that actuall really exists. Also another example of something being really cool!" -autowrap_mode = 3 -text_overrun_behavior = 3 -max_lines_visible = 2 +size_flags_horizontal = 3 +text = "Save Changes" [node name="Contacts" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=1935681612] visible = false @@ -2391,6 +2061,215 @@ layout_mode = 2 text = "Block Avatar" horizontal_alignment = 1 +[node name="Settings" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=362656680] +visible = false +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 +script = ExtResource("17_shwvw") + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings" unique_id=1829621060] +layout_mode = 2 + +[node name="Nav" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer" unique_id=954760906] +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 + +[node name="General" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=985920354] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "General" + +[node name="Display" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=1395666760] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Display" + +[node name="Audio" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=38839733] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Audio" + +[node name="Controls" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=875250441] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Controls" + +[node name="Interface" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=540067912] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Interface" + +[node name="Network" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=1675516200] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Network" + +[node name="Config" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=1660210073] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Config" + +[node name="Security" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=1489360741] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Security" + +[node name="Misc" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=1234025146] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Misc" + +[node name="Advanced" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Nav" unique_id=378374610] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +text = "Advanced" + +[node name="Container" type="Control" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer" unique_id=444043284] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="General" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container" unique_id=351943332] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 + +[node name="Config" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container" unique_id=661151710] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 + +[node name="SessionServers" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config" unique_id=749605939] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers" unique_id=1413203981] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer" unique_id=1294718765] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/MarginContainer" unique_id=510822724] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/MarginContainer/HBoxContainer" unique_id=1248908459] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 20 +text = "Session Servers" + +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/MarginContainer/HBoxContainer" unique_id=2141833094] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Show List" + +[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer" unique_id=1524295507] +visible = false +layout_mode = 2 + +[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer" unique_id=1107486151] +custom_minimum_size = Vector2(0, 200) +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2" unique_id=90574433] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer" unique_id=1903245076] +layout_mode = 2 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer" unique_id=1621920772] +layout_mode = 2 +text = "Add Session Server" + +[node name="Name" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer" unique_id=351217801] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Friendly Name" + +[node name="URL" type="LineEdit" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer" unique_id=512184669] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "URL" + +[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer" unique_id=475127911] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Add" + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer" unique_id=1281162897] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/ScrollContainer" unique_id=1146094050] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/ScrollContainer/MarginContainer" unique_id=1455078587] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Description" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer" unique_id=1192735413] +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Settings/HBoxContainer/Description" unique_id=1672934352] +layout_mode = 2 +size_flags_vertical = 1 +theme_override_font_sizes/font_size = 20 +text = "This is some top text, next we may be able to afford some bottom text. For now we will just be able to afford some medium text." +autowrap_mode = 3 + +[node name="Templates" type="Control" parent="MarginContainer/VBoxContainer/Master/Settings" unique_id=717770235] +visible = false +layout_mode = 2 + +[node name="SessionServerListing" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Master/Settings/Templates" unique_id=877735623] +layout_mode = 0 +offset_left = 319.0 +offset_top = 57.0 +offset_right = 1553.0 +offset_bottom = 80.0 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Master/Settings/Templates/SessionServerListing" unique_id=22006714] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.73333335, 0.73333335, 0.73333335, 1) +text = "https://servers.openmierva.org" + +[node name="Remove" type="Button" parent="MarginContainer/VBoxContainer/Master/Settings/Templates/SessionServerListing" unique_id=725542881] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "X" + [node name="Exit" type="MarginContainer" parent="MarginContainer/VBoxContainer/Master" unique_id=719734468] visible = false layout_mode = 0 diff --git a/src/userinterface/dash/instance.gd b/src/userinterface/dash/instance.gd index 4e048d2..8b593dc 100644 --- a/src/userinterface/dash/instance.gd +++ b/src/userinterface/dash/instance.gd @@ -7,4 +7,100 @@ # Authors: Armored Dragon # --- License -extends Control \ No newline at end of file +extends Control + +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") + +@onready var instance_settings_root = get_node("VBoxContainer/HBoxContainer") +@onready var instance_name = instance_settings_root.get_node("VBoxContainer2/PanelContainer/MarginContainer/InstanceName/InstanceNameField") +@onready var instance_description = instance_settings_root.get_node("VBoxContainer2/PanelContainer2/MarginContainer/InstanceDescriptionContainer/InstanceDescription") +@onready var instance_max_users = instance_settings_root.get_node("VBoxContainer/InstanceSettings/MarginContainer/HBoxContainer/MaxConnectedUsers") + +@onready var instance_privacy_container = instance_settings_root.get_node("VBoxContainer/InstancePrivacy/MarginContainer/InstancePrivacyContainer") +@onready var instance_privacy_public_btn = instance_privacy_container.get_node("Public") +@onready var instance_privacy_contacts_btn = instance_privacy_container.get_node("Contacts") +@onready var instance_privacy_friends_btn = instance_privacy_container.get_node("Friends") +@onready var instance_privacy_invite_btn = instance_privacy_container.get_node("InviteOnly") + +@onready var save_changes_btn = get_node("VBoxContainer/HBoxContainer2/SaveChanges") + +var session_privacy: Enum.PrivacyLevel + +func _ready(): + instance_privacy_public_btn.pressed.connect(_update_instance_privacy_visual.bind(Enum.PrivacyLevel.PUBLIC)) + instance_privacy_contacts_btn.pressed.connect(_update_instance_privacy_visual.bind(Enum.PrivacyLevel.CONTACTS)) + instance_privacy_friends_btn.pressed.connect(_update_instance_privacy_visual.bind(Enum.PrivacyLevel.FRIENDS)) + instance_privacy_invite_btn.pressed.connect(_update_instance_privacy_visual.bind(Enum.PrivacyLevel.INVITE)) + + save_changes_btn.pressed.connect(_handle_save_session_info) + + Events.connect("instance_updated", update_instance) + + _update_instance_privacy_visual(Enum.PrivacyLevel.INVITE) + return + + +func _update_instance_privacy_visual(level): + if !is_multiplayer_authority(): + return + + GlobalLogger.logs("Updating instance privacy.") + + session_privacy = level + + for node in instance_privacy_container.get_children(): + if node is Button: + _privacy_button_disable(node) + match level: + Enum.PrivacyLevel.INVITE: + _privacy_button_enable(instance_privacy_invite_btn) + Enum.PrivacyLevel.PUBLIC: + _privacy_button_enable(instance_privacy_public_btn) + Enum.PrivacyLevel.CONTACTS: + _privacy_button_enable(instance_privacy_contacts_btn) + Enum.PrivacyLevel.FRIENDS: + _privacy_button_enable(instance_privacy_friends_btn) + + return + +func _privacy_button_disable(node) -> void: + node.button_pressed = false + node.custom_minimum_size = Vector2(0, 40) + return + +func _privacy_button_enable(node) -> void: + node.button_pressed = true + node.custom_minimum_size = Vector2(0, 50) + return + +func update_instance(_instance: Dictionary) -> void: + GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], Enum.LogLevel.WARNING) + # TODO Session Permissions: Admins can change instance settings. + # Publish changes to the session server. + # Update running server + return + +func _handle_save_session_info() -> void: + # TODO: Have this page know what instance it currently occupies. + var _sessions = network_m.get_connected_sessions() + + # Get current session info settings from the dashboard. + # TODO: Get the current session we are connected to. + # Update the database to the new settings. + var _current_session_settings = _get_server_settings() + + _sessions[0].set("name", _current_session_settings.name) + _sessions[0].set("description", _current_session_settings.description) + _sessions[0].set("max_connected_users", _current_session_settings.max_connected_users) + _sessions[0].set("privacy", _current_session_settings.privacy) + + network_m.update_server(_sessions[0].id, _sessions[0]) + return + +func _get_server_settings() -> Dictionary: + return { + "name": instance_name.text, + "description": instance_description.text, + "max_connected_users": int(instance_max_users.value), + "privacy": session_privacy, + } diff --git a/src/userinterface/dash/master.gd b/src/userinterface/dash/master.gd index 1cc8927..c904a18 100644 --- a/src/userinterface/dash/master.gd +++ b/src/userinterface/dash/master.gd @@ -32,7 +32,7 @@ func _build_page_list(): if button.name not in dashboard_tab_names: button.disabled = true continue - button.pressed.connect(_handle_switch_tab.bind(button.name)) + button.pressed.connect(Events.emit_signal.bind("dash_switch_tab", button.name)) func _handle_set_dash_state(is_open: bool) -> void: GlobalLogger.logs("Changing dashboard state: '%s'" % is_open) @@ -48,7 +48,7 @@ func _handle_switch_tab(target_name: String) -> void: dash_nav_button.button_pressed = false if target_name not in dashboard_tab_names: - GlobalLogger.logs("Tried to switch to an invalid dashboard page: '%s'" % target_name, 2) + GlobalLogger.logs("Tried to switch to an invalid dashboard page: '%s'" % target_name, Enum.LogLevel.WARNING) return dash_tab_master_container.get_node(target_name).visible = true diff --git a/src/userinterface/dash/sessions.gd b/src/userinterface/dash/sessions.gd index 8610b15..d2dca93 100644 --- a/src/userinterface/dash/sessions.gd +++ b/src/userinterface/dash/sessions.gd @@ -8,3 +8,63 @@ # --- License extends Control + +@onready var _template_world_listing = get_node("Templates/WorldListing") +@onready var _world_listing_grid = get_node("HBoxContainer/VBoxContainer2/ScrollContainer/GridContainer") +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") + +# TODO: Keep track of what is different from the current live settings +# When something changes, show icon or indicator of a change. + +func _ready(): + Events.dash_switch_tab.connect(_handle_page_opened) + return + +func _handle_page_opened(page_name: String) -> void: + if page_name != "Sessions": + return + + # TODO: Make a more robust active_account detection mechanism. + if GlobalAccount.active_account == {}: + return + + # TODO: Check if we need to authenticate, if so + for _session_server in SettingsManager.get_session_servers(): + await SessionQuery.authenticate(_session_server.url) + + # Get a list of all sessions from our saved sessions_list + var session_list = await SessionQuery.get_sessions() + + # Remove all entries in the list + _remove_all_listings() + + # In our flat array, add all sessions to the view + for session in session_list: + insert_world_into_session_listing(session) + return + +func insert_world_into_session_listing(world_data: Dictionary) -> void: + var _world = _template_world_listing.duplicate() + + var world_title = _world.get_node("PanelContainer/VBoxContainer/MarginContainer/Label") + var world_thumbnail = _world.get_node("PanelContainer/VBoxContainer/MarginContainer2/AspectRatioContainer/TextureRect") + + world_title.text = world_data.get("sessionName", "Unknown session name.") + world_thumbnail.set_texture(load(world_data.get("sessionThumbnail", "res://resources/icons/dummy16-9.webp"))) + + _world_listing_grid.add_child(_world) + + # Buttons + var _button = _world.get_node("Button") + + _button.pressed.connect(network_m.join_server.bind(world_data.url, world_data.port)) + + GlobalLogger.logs("Added a session to the session list.") + return + +func _remove_all_listings() -> void: + GlobalLogger.logs("Removed all listings from the session list.", Enum.LogLevel.INFO) + + for session_listing in _world_listing_grid.get_children(): + session_listing.queue_free() + return diff --git a/src/userinterface/dash/settings.gd b/src/userinterface/dash/settings.gd index 49aad29..79d6583 100644 --- a/src/userinterface/dash/settings.gd +++ b/src/userinterface/dash/settings.gd @@ -7,4 +7,68 @@ # Authors: Armored Dragon # --- License -extends Control \ No newline at end of file +extends Control + +@onready var _templates_session_server_listing = get_node("Templates/SessionServerListing") +@onready var _session_server_container = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer") + +@onready var _show_session_server_info_btn = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/MarginContainer/HBoxContainer/Button") +@onready var _session_server_info = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer") +@onready var _add_session_server_btn = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer/Button") +@onready var _add_session_server_name = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer/Name") +@onready var _add_session_server_url = get_node("HBoxContainer/Container/Config/SessionServers/VBoxContainer/PanelContainer/MarginContainer2/VBoxContainer/HBoxContainer/URL") + +func _ready(): + _load_session_servers() + + _show_session_server_info_btn.pressed.connect(_show_session_server_info_dialog) + _add_session_server_btn.pressed.connect(_add_session_server) + + Events.dash_switch_tab.connect(_handle_page_opened) + return + +func _handle_page_opened(page_name) -> void: + if page_name != "Settings": + return + + _load_session_servers() + return + +func _load_session_servers() -> void: + GlobalLogger.logs("Loading session servers.") + var _servers = SettingsManager.get_session_servers() + + for _existing_listing in _session_server_container.get_children(): + _existing_listing.queue_free() + + for server in _servers: + var _template = _templates_session_server_listing.duplicate() + _template.get_node("Label").text = server.url + _template.get_node("Remove").pressed.connect(_remove_session_server.bind(server.url)) + _session_server_container.add_child(_template) + return + +func _remove_session_server(url: String) -> void: + SettingsManager.remove_session_server(url) + _load_session_servers() + +func _add_session_server() -> void: + var _name = _add_session_server_name.text + var _url = _add_session_server_url.text + + if _name == "": + return + + if _url == "": + return + + SettingsManager.add_session_server({"name": _name, "url": _url}) + + _add_session_server_name.text = "" + _add_session_server_url.text = "" + _load_session_servers() + return + +func _show_session_server_info_dialog() -> void: + _session_server_info.visible = !_session_server_info.visible + return diff --git a/src/userinterface/session_query.gd b/src/userinterface/session_query.gd new file mode 100644 index 0000000..9b3cdd1 --- /dev/null +++ b/src/userinterface/session_query.gd @@ -0,0 +1,95 @@ +# --- License +# File: /client/src/userinterface/session_query.gd +# Project: OpenMinerva +# Created Date: 31 March 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License + +extends Node + +func authenticate(url: String) -> Dictionary: + var _return_dict: Dictionary = {"ok": false, "error": ""} + + # TODO: Do we need a new authentication key? + if GlobalAccount.dev_session_server_api_key == "": + var url_deconstructed = UrlParser.deconstruct(url) + if url_deconstructed.ok == false: + var ERROR_MESSAGE = "Failed to deconstruct the url '%s'" % url + GlobalLogger.logs(ERROR_MESSAGE, Enum.LogLevel.WARNING) + _return_dict.error = ERROR_MESSAGE + return _return_dict + url_deconstructed = url_deconstructed.data + + var body: Dictionary = { + "id_token": GlobalAccount.active_account.id_token, + "challenge": "challenge value" + } + var authentication_response = await HTTP.req(HTTPClient.Method.METHOD_POST, url_deconstructed.host, "/api/v1/getAuthenticationKey", url_deconstructed.port, ["Accept: application/json", "Content-Type: application/json"], JSON.stringify(body)) + + _authentication_request_received(url_deconstructed.host, authentication_response) + + # Get id_token of currently logged in user + # Sign challenge using private key + # Send request {id_token, challenge} + + # ... Server does its thing ... + + # Response contains api key, or error + + return _return_dict + +func get_sessions() -> Array: + var _return_arr = [] + var _session_servers = SettingsManager.get_session_servers() + + for server in _session_servers: + var search = "" + var tags = "" + + var url_deconstructed = UrlParser.deconstruct(server.url) + if url_deconstructed.ok == false: + GlobalLogger.logs("Failed to parse the session server URL.", Enum.LogLevel.INFO) + continue + url_deconstructed = url_deconstructed.data + + var form_parts := [ + "search=%s" % search, + "tags=%s" % tags, + ] + var form_string: String = "&".join(form_parts) + + var sessions_response = await HTTP.req(HTTPClient.Method.METHOD_GET, url_deconstructed.host, "/api/v1/getSessions?%s" % form_string, url_deconstructed.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key]) + + var sessions_in_server = _session_request_received(url_deconstructed.host, sessions_response) + # TODO: Validate request health + _return_arr.append_array(sessions_in_server.data) + + return _return_arr + +func _session_request_received(_host: String, response: Dictionary) -> Dictionary: + var _return_arr = {"ok": false, "error": "", "data": null} + + # TODO: If response.ok + # TODO: Validate is valid JSON + # TODO: Validate key exists + var response_parsed = JSON.parse_string(response.body) + + _return_arr.data = response_parsed.data + _return_arr.ok = true + + return _return_arr + +func _authentication_request_received(_host: String, response: Dictionary) -> Dictionary: + var _return_arr = {"ok": false, "error": "", "data": null} + + # TODO: If response.ok + # TODO: Validate is valid JSON + # TODO: Validate key exists + + GlobalAccount.dev_session_server_api_key = JSON.parse_string(response.body).key + + # If authentication succeeded, record data + # Else report error. + return _return_arr diff --git a/src/userinterface/session_query.gd.uid b/src/userinterface/session_query.gd.uid new file mode 100644 index 0000000..6002c34 --- /dev/null +++ b/src/userinterface/session_query.gd.uid @@ -0,0 +1 @@ +uid://dbrabs53sf0x5 From d5c6468033f604e39478d61b9547236b036bdc7d Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Wed, 6 May 2026 18:25:03 -0500 Subject: [PATCH 15/19] Utilize OAuth2Client library. (#90) * Remove local oauth library. Use Godot-OAuth2 library. * Adjustments with the current Alpha of godot-oauth2client library. * Added warning label to warning log. * Adjusted README.md to center Discord invite. --- README.md | 7 +- src/project.godot | 4 +- src/scripts/libs/account.gd | 40 ++++++++- src/scripts/libs/oauth.gd | 147 ---------------------------------- src/scripts/libs/oauth.gd.uid | 1 - src/scripts/logger.gd | 4 + 6 files changed, 47 insertions(+), 156 deletions(-) delete mode 100644 src/scripts/libs/oauth.gd delete mode 100644 src/scripts/libs/oauth.gd.uid diff --git a/README.md b/README.md index 6c4197e..d55228a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@

- -[![](https://dcbadge.limes.pink/api/server/https://discord.gg/Kx6avB52gK)](https://discord.gg/Kx6avB52gK) +

+ + Discord + +

# Client diff --git a/src/project.godot b/src/project.godot index b5bcad5..9478309 100644 --- a/src/project.godot +++ b/src/project.godot @@ -32,13 +32,13 @@ AccountServers="*uid://bpysjoq7n0ytu" Random="*uid://1js68qt8w0mv" GlobalAccount="*uid://dtlb70kxvbtvn" Events="*uid://c656spc3ppdlw" -OAuth="*uid://jd7qlsley1no" SessionQuery="*uid://dbrabs53sf0x5" Enum="*uid://dgpvcem71xdbn" Bootstrap="*uid://cvj10vdw6hlqw" NetworkCompression="*uid://b2iq75uom64x2" HTTP="*uid://d3cnfdwjxopsx" UrlParser="*uid://budprjmmpally" +OAuth2Client="*uid://c4b880pwgfrty" [display] @@ -49,7 +49,7 @@ window/stretch/aspect="expand" [editor_plugins] -enabled=PackedStringArray("res://addons/openminerva.urlparser/plugin.cfg") +enabled=PackedStringArray("res://addons/openminerva.oauth2client/plugin.cfg", "res://addons/openminerva.urlparser/plugin.cfg") [input] diff --git a/src/scripts/libs/account.gd b/src/scripts/libs/account.gd index ce4d330..0a2ba71 100644 --- a/src/scripts/libs/account.gd +++ b/src/scripts/libs/account.gd @@ -83,8 +83,25 @@ func use(id: String) -> void: GlobalLogger.logs("Setting active account to '%s'." % id, Enum.LogLevel.INFO) var _account = _get_account_by_id(id) + + # TODO: Error checking + var url = UrlParser.deconstruct(_account.account_server) + url = url.data + + var _oauth = OAuth2Client.new( + url.host, + url.port, + "OpenMinerva-Game-Client", + 54000, + GlobalLogger, + HTTP, + true + ) + if _account.type == "oauth": - if await OAuth.validate_token(_account) == false: + var _oauth_valid: Dictionary = await _oauth.validate(_account) + + if _oauth_valid.ok == OAuth2Client.OAUTH2_CLIENT_RESULT.OK && _oauth_valid.data == false: await authenticate_oauth(id) active_account = _account @@ -105,7 +122,7 @@ func update(id: String, data: Dictionary) -> void: for key in _data_keys: if key not in _database_keys: - GlobalLogger.logs("Tried to update an invalid key in an account, '%s'." % key) + GlobalLogger.logs("Tried to update an invalid key in an account, '%s'." % key, Enum.LogLevel.WARNING) continue account[key] = data[key] @@ -120,9 +137,24 @@ func authenticate_oauth(id: String, _remember_me: bool = false) -> void: var account = _get_account_by_id(id) # TODO: Check if account is still valid without trying to sign in. - var oauth_tokens = await OAuth.authenticate(account.account_server + "/oauth/authorize") + var url = UrlParser.deconstruct(account.account_server) + url = url.data + + var _oauth = OAuth2Client.new( + url.host, + url.port, + "OpenMinerva-Game-Client", + 54000, + GlobalLogger, + HTTP, + true + ) + + var oauth_tokens = await _oauth.authenticate() + if oauth_tokens.ok == OAuth2Client.OAUTH2_CLIENT_RESULT.OK: + update(id, oauth_tokens.data) - update(id, oauth_tokens) + return ## Save the current account database we have in memory to the disk. func _save_account_database() -> void: diff --git a/src/scripts/libs/oauth.gd b/src/scripts/libs/oauth.gd deleted file mode 100644 index 7caa97a..0000000 --- a/src/scripts/libs/oauth.gd +++ /dev/null @@ -1,147 +0,0 @@ -# --- License -# File: /client/src/scripts/libs/oauth.gd -# Project: OpenMinerva -# Created Date: 23 March 2026 -# Copyright (c) 2026 OpenMinerva -# License: MIT License -# Authors: Armored Dragon -# --- License - -extends Node - -var jwt_lib = preload("res://scripts/libs/jwt.gd").new() -var random_lib = preload("res://scripts/utils/random.gd").new() - -var port: int = 54000 -var bind_address: String = "127.0.0.1" -var redirect_server = TCPServer.new() - -var _listen_for_oauth_connections: bool = false - -func authenticate(account_server: String) -> Dictionary: - var secret_pkce: String = random_lib.random_string(50) - var account_server_url = UrlParser.deconstruct(account_server) - - if account_server_url.ok == false: - return {} - - account_server_url = account_server_url.data - - GlobalLogger.logs("Starting OAuth flow.") - - var uri_parts := [ - "client_id=%s" % "OpenMinerva-Game-Client", - "redirect_uri=http://%s:%s" % [bind_address, port], - "response_type=code", - "scope=openid offline_access", - "response_mode=query", - "code_challenge_method=S256", - "code_challenge=%s" % _get_code_challenge(secret_pkce), - "prompt=consent" - ] - - var uri = account_server + "?" + "&".join(uri_parts) - OS.shell_open(uri) - - GlobalLogger.logs("Starting OAuth redirect server.") - redirect_server.listen(port, bind_address) - - _listen_for_oauth_connections = true - var _auth_code = await _wait_for_auth_code() - - GlobalLogger.logs("Closing OAuth redirect server.") - redirect_server = TCPServer.new() - - GlobalLogger.logs("Exchanging retrieved auth code for a proper token.") - var form_parts := [ - "client_id=%s" % "OpenMinerva-Game-Client", - "grant_type=authorization_code", - "code=%s" % _auth_code, - "redirect_uri=http://%s:%s" % [bind_address, port], - "code_challenge_method=S256", - "code_verifier=%s" % secret_pkce, - ] - var form_string: String = "&".join(form_parts) - var exchange_response = await HTTP.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) - var token_data = JSON.parse_string(exchange_response.get("body")) - - return _get_tokens_from_response(token_data) - -func _get_code_challenge(verifier: String) -> String: - var ctx = HashingContext.new() - ctx.start(HashingContext.HASH_SHA256) - ctx.update(verifier.to_utf8_buffer()) - var hash_bytes = ctx.finish() - var base64_str = Marshalls.raw_to_base64(hash_bytes) - - base64_str = jwt_lib.base64_to_base64url(base64_str) - - return base64_str - -func _wait_for_auth_code() -> String: - var code: String = "" - - while code == "": - if redirect_server.is_connection_available(): - _listen_for_oauth_connections = false - code = _handle_auth_callback(redirect_server.take_connection()) - else: - await get_tree().process_frame - - return code - -func _handle_auth_callback(connection: StreamPeerTCP) -> String: - var request = connection.get_string(connection.get_available_bytes()) - - var temp_auth_code: String = request.split("code=")[1].split("&iss=")[0].strip_edges() - - GlobalLogger.logs("Got authentication code: '%s'." % temp_auth_code) - - # Send success. - var html_response = "HTTP/1.1 200 OK\r\n" - html_response += "Content-Type: text/html\r\n" - html_response += "Connection: close\r\n\r\n" - html_response += "

Success!

You can close this window now.

" - - connection.put_data(html_response.to_utf8_buffer()) - connection.disconnect_from_host() - - return temp_auth_code - -func _exchange_code() -> Dictionary: - return {} - -func _get_tokens_from_response(response: Dictionary) -> Dictionary: - # TODO: Error checks to prevent overwriting with bad data. - var oauth_data = { - "access_token" = response.get("access_token"), - "refresh_token" = response.get("refresh_token"), - "id_token" = response.get("id_token"), - "access_token_expiry" = response.get("expires_in") - } - - return oauth_data - -func validate_token(account: Dictionary) -> bool: - if account.access_token == "": - return false - - var form_parts := [ - "client_id=%s" % "OpenMinerva-Game-Client", - "token=%s" % account.access_token, - ] - var form_string: String = "&".join(form_parts) - - var account_server_url = UrlParser.deconstruct(account.account_server) - if account_server_url.ok == false: - GlobalLogger.logs("Failed to parse account server url.", Enum.LogLevel.WARNING) - return false - account_server_url = account_server_url.data - - var introspect_response = await HTTP.req(HTTPClient.Method.METHOD_POST, account_server_url.host, "/oauth/token/introspection", account_server_url.port, ["Accept: application/json", "Content-Type: application/x-www-form-urlencoded"], form_string) - if introspect_response.ok == false: - GlobalLogger.logs("Unknown error parsing the introspection response.", Enum.LogLevel.WARNING) - return false - introspect_response = JSON.parse_string(introspect_response.body) - - return introspect_response.active diff --git a/src/scripts/libs/oauth.gd.uid b/src/scripts/libs/oauth.gd.uid deleted file mode 100644 index d5e1ac6..0000000 --- a/src/scripts/libs/oauth.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://jd7qlsley1no diff --git a/src/scripts/logger.gd b/src/scripts/logger.gd index f547841..c399bbe 100644 --- a/src/scripts/logger.gd +++ b/src/scripts/logger.gd @@ -56,6 +56,10 @@ func logs(message: String = "", level: Enum.LogLevel = Enum.LogLevel.DEBUG): ) pass +# FIXME: Replace "logs" with "log" +func log(message: String = "", level: Enum.LogLevel = Enum.LogLevel.DEBUG) -> void: + logs(message, level) + func _log_to_file(message: String = "", level: int = 0): if file_logging_enabled && log_file: var formatted_log = "[%s] %s" % [log_level_names[level], message] From f58fc9ba9908d6635c0741dbcdf75d0e483f667f Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 6 May 2026 19:36:35 -0500 Subject: [PATCH 16/19] Use submodules for Addons. --- .gitignore | 1 - .gitmodules | 6 ++++++ README.md | 4 ++-- src/project.godot | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .gitmodules diff --git a/.gitignore b/.gitignore index 26c0f41..4760075 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /src/.godot /src/android/ -/src/addons \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d43ab76 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "src/addons/godot-oauth2client"] + path = src/addons/godot-oauth2client + url = https://github.com/OpenMinerva/godot-oauth2client.git +[submodule "src/addons/godot-urlparser"] + path = src/addons/godot-urlparser + url = https://github.com/OpenMinerva/godot-urlparser.git diff --git a/README.md b/README.md index d55228a..282669f 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ Client is the interface used to connect and interact with the virtual world of O ## Development Quick Start ### 1: Download source code -Installation and running on Linux. +For cloning this project on Linux: ```bash # Clone this repository. -git clone https://github.com/OpenMinerva/client +git clone --recurse-submodules https://github.com/OpenMinerva/client ``` ### 2: Import project into Godot. diff --git a/src/project.godot b/src/project.godot index 9478309..bf9fb10 100644 --- a/src/project.godot +++ b/src/project.godot @@ -37,8 +37,8 @@ Enum="*uid://dgpvcem71xdbn" Bootstrap="*uid://cvj10vdw6hlqw" NetworkCompression="*uid://b2iq75uom64x2" HTTP="*uid://d3cnfdwjxopsx" -UrlParser="*uid://budprjmmpally" OAuth2Client="*uid://c4b880pwgfrty" +UrlParser="*uid://budprjmmpally" [display] @@ -49,7 +49,7 @@ window/stretch/aspect="expand" [editor_plugins] -enabled=PackedStringArray("res://addons/openminerva.oauth2client/plugin.cfg", "res://addons/openminerva.urlparser/plugin.cfg") +enabled=PackedStringArray("res://addons/godot-oauth2client/plugin.cfg", "res://addons/godot-urlparser/plugin.cfg") [input] From 053c38770929373b117d7ffac31c40cf9c7c2824 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 6 May 2026 19:57:57 -0500 Subject: [PATCH 17/19] Actually include the addons. --- src/addons/godot-oauth2client | 1 + src/addons/godot-urlparser | 1 + 2 files changed, 2 insertions(+) create mode 160000 src/addons/godot-oauth2client create mode 160000 src/addons/godot-urlparser diff --git a/src/addons/godot-oauth2client b/src/addons/godot-oauth2client new file mode 160000 index 0000000..ae999da --- /dev/null +++ b/src/addons/godot-oauth2client @@ -0,0 +1 @@ +Subproject commit ae999da78e4e455691bd798eba0186b5b7032050 diff --git a/src/addons/godot-urlparser b/src/addons/godot-urlparser new file mode 160000 index 0000000..5063250 --- /dev/null +++ b/src/addons/godot-urlparser @@ -0,0 +1 @@ +Subproject commit 50632506d6a5a8a65bda52640339df98957fd0cd From d1999262960161fb6b7f62cc449389201ed04c9b Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Wed, 6 May 2026 22:49:41 -0500 Subject: [PATCH 18/19] Fix branding (#91) * Removed legacy media. Updated names. * Added branding icons. * Changed physics settings. --- src/addons/godot-oauth2client | 2 +- src/icon.svg | 172 ------------------ src/project.godot | 16 +- src/resources/icons/logos/logo-big.png | Bin 0 -> 35018 bytes src/resources/icons/logos/logo-big.png.import | 40 ++++ src/resources/icons/logos/logo.icns | Bin 0 -> 25984 bytes src/resources/icons/logos/logo.ico | Bin 0 -> 30092 bytes src/resources/icons/logos/logo.svg | 104 +++++++++++ .../icons/logos/logo.svg.import} | 8 +- src/resources/icons/logos/logo.webp | Bin 0 -> 15424 bytes src/resources/icons/logos/logo.webp.import | 36 ++++ 11 files changed, 197 insertions(+), 181 deletions(-) delete mode 100644 src/icon.svg create mode 100644 src/resources/icons/logos/logo-big.png create mode 100644 src/resources/icons/logos/logo-big.png.import create mode 100644 src/resources/icons/logos/logo.icns create mode 100644 src/resources/icons/logos/logo.ico create mode 100644 src/resources/icons/logos/logo.svg rename src/{icon.svg.import => resources/icons/logos/logo.svg.import} (77%) create mode 100644 src/resources/icons/logos/logo.webp create mode 100644 src/resources/icons/logos/logo.webp.import diff --git a/src/addons/godot-oauth2client b/src/addons/godot-oauth2client index ae999da..a835738 160000 --- a/src/addons/godot-oauth2client +++ b/src/addons/godot-oauth2client @@ -1 +1 @@ -Subproject commit ae999da78e4e455691bd798eba0186b5b7032050 +Subproject commit a835738a2674feb2679576a247169fc53c2d4682 diff --git a/src/icon.svg b/src/icon.svg deleted file mode 100644 index 6df617e..0000000 --- a/src/icon.svg +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/project.godot b/src/project.godot index bf9fb10..be75d1e 100644 --- a/src/project.godot +++ b/src/project.godot @@ -14,13 +14,16 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] -config/name="PrismLens" +config/name="OpenMinerva Client" run/main_scene="uid://cxk6c0uipjjpo" config/features=PackedStringArray("4.6", "Forward Plus") run/max_fps=144 -boot_splash/image="/home/adragon/Pictures/PrismLens_1_background.png" -config/icon="uid://du0bhthwac604" -boot_splash/minimum_display_time=1000 +boot_splash/stretch_mode=0 +boot_splash/image="uid://dpdryj57noc4c" +config/icon="res://resources/icons/logos/logo.webp" +config/macos_native_icon="res://resources/icons/logos/logo.icns" +config/windows_native_icon="res://resources/icons/logos/logo.ico" +boot_splash/minimum_display_time=1500 [autoload] @@ -94,6 +97,11 @@ sprint={ ] } +[physics] + +3d/run_on_separate_thread=true +3d/physics_engine="Jolt Physics" + [rendering] textures/default_filters/use_nearest_mipmap_filter=true diff --git a/src/resources/icons/logos/logo-big.png b/src/resources/icons/logos/logo-big.png new file mode 100644 index 0000000000000000000000000000000000000000..93238832b7d593426238977efe016d7bc98b9711 GIT binary patch literal 35018 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 literal 0 HcmV?d00001 diff --git a/src/resources/icons/logos/logo-big.png.import b/src/resources/icons/logos/logo-big.png.import new file mode 100644 index 0000000..87ddf99 --- /dev/null +++ b/src/resources/icons/logos/logo-big.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpdryj57noc4c" +path="res://.godot/imported/logo-big.png-8df91e22a8df12fa9010f3b5b5799846.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/logos/logo-big.png" +dest_files=["res://.godot/imported/logo-big.png-8df91e22a8df12fa9010f3b5b5799846.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/src/resources/icons/logos/logo.icns b/src/resources/icons/logos/logo.icns new file mode 100644 index 0000000000000000000000000000000000000000..4dcaa94477ed2e1f12aa785fa127869adfd95322 GIT binary patch literal 25984 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

8Ypq_-rycp z4Jhu{?+1gjDjAjXRk=|=AS{8}Y$)n30!6K0 zCM#kn$bRcm3dqtSpj>-$^Ooz6Vqn3a5IS2?Sb9ijbOESS(|i&dD~3b+Pbfib?jMMo zjRCJp{>ta)Ym(a|nwY9{Qt~4S(PUl_n>vT%jMcs$yxsX<74^I+>sL)UX~Vy_utUpZ zBx1heKaPkG@JkAW(A|agBiXg_1CZV?+lgR-8;o-rjx}^*65!o=p|LkkD-|dzunade z)w=!Z=F6KKk*gC!GoRvE%HW^ad6Y$3#J@h1OHE`TY4{@FeB9&E4E6jqs%9tDIp3w; zon^-Zz#C|Rf2z*;^L}R_GaMs5n}I|hg0S4@hVJWdK zc>c7zTgYv&f9D~0NzkziPC&uET0F*N&rV2P$ySgaeqs;b zET@0g1@@eQ>vmtOz;%czgQN~Wc`&Z_r$Y5yz6+tTjbR#$ur1y1;QS3GNOZNO*MxR7RK|pXn8p9TI{j^?p(tG zdd#TV7#k>?uTk)V2T)R5E{H;mv!!C}sQ2;L7*+M?P@qXgnSO28_8#)<9pAJ8jrGH& zWiGn#&Nh>PI$~+h7(L{Qzc_dBT+@aDn5|{LifPj+qk%usmjk z0Ifq92-c857U|dWa6`66hZsaD1+<^BisX+CGVm%~XmGaCL=m!s|$F4!I-g$e$WK4Lo!85^Q*T*u48n%B)-SphEZYobXRJ8H{6d1H|JS8jCS^J?WJDuO}- zBbWVehGyf(y?QGmHA1^)I?M=C zBMY>YzcxxhO!#SUyOFQeZa%)qbWVJWbMua0GAU};@NbOgcovSWwow?F^oB{-+0rXK$8YQv`ES zZ3Nvi_x&;AN-px#?xiLNb=tG#ISBVQVn{v8ZtbyEfpQcS!PKbP&trSb{NEygGoVErmTii>G-h+;Jjz{_S?_@LY35Ao?8S7yL?N(MxiX65w4Ch{)LEd6(}`t-XvrNilG^$7=B zva85`>(V>==*tvGd?3rC-pRTS`Ipdc*j-pv%0agL(wA&Z9JFpmkj;g@ff{;d?~QKo zVm_w9LhGCh&OUYih9o~Uy*OUdL=cwV2$0u0#jtdi2oqxED>zfo?8(qO&J4-}70XYT zDL~UzE`}P23XH3i6=H03BL=kN;9vh7&?b&Q@mj&pOUT}&;Kf2yV{-A~*Hv&IhDudF z+EtjwlDyb%Pif=336f*z(CMw6d{Zj$*5)_2PgkZ^2z{O*$Qs6QNH~qkj<|f=_yH5@ znwfknlc`8PbahZ`6aXeVvL!19+lY+B*$PpTo0H@G2@e^FhH%f6_}scMPy9wdk()(d zvS*Feqa73vk>o!Nn+oJ01sYHd3X)BgulGw@ygP#`I5l}e%uqs40veVEd*7NE zak;DVSGU=Es0=aA>d-8kY2uM?#?$^r=s8MSPnmK;4vX_ z?(Af2k08rk!+~w;129Oh9CQ8#$)R6k5>Ra`0(}dqT%=5a-0<5`Kwm-z)vIiPB&IZ1y z6k+rC3}+LcQ(Ap7)5J{l>B#x8n`82ALy^+vfV(-Ji@NjNOfVxEgbN{wxcn18s*T5J ze#kAM^HaP##pnXGBATaC&Iv)hTyZZA01+K=(K`{e|EgB-^DN&`eC%f6iLDLZXcv1B zCgPzaST2J=5js`meNk6SpXThdddfZ*G(;x->5xuv&O>}hy?a?ur_|wt^$sC4v1cdA3s0#)xi;Gn4<%HRomFt0+v{h!{3ADIxHy) z-+c!ZrAgNpa0*yA26ErR>OD4J7lSZE_Z~yPySw(V@!L;6J9#>urWta-yn**Dplyz}~n93&VXvfn)ld;Ol4HEJAifk_E>Qh?S1b1TFM4KZXNT!u(s z^^nl#rbeQs2ef}nji52OFrIAs=Bt->PDFWC6js&wI_dAEkP*^^e8hj@eS;)W9Wsuo zwE=pr(*J0HFpKbocYSOxe%>^TZOsrI^vt`AlL{ShKJXe)p!IMoZ@n43QVub~xdqRl z;(IS6*hqM>9!fT%SpXV=re?N;<_ zdcxtYaA=w*b2!j`aRz^mNVt5RAp&V7sm2%_f&mM)#Ex#Jt@r6h&qMWft$=4%|HmIZwPLR@FszX7PE zPMp%azb!Vw&YCM!qir_FrTC1FAxMx~Ke9*dv}GHCdZ0xS?VIJ}#@j&%1=CN{12jRHE%=EG6gC#$#V*_;^)VX| zxmD+p;@zf!RvG4qY=UgvbAIqy_=L5HD@A$h$KK25%$GJ2y|>tx*jG|{0<$6MXs{D4@Va<^pK;WE2Ar&A=%J`cRIm+vBJ%gm~!< zY4kjCE+KjErX~s}T=2+8z*|nWSH6wsf(J-(--~hj(Psu|RkFh<89FSYvf8q`f1s`= znl_%rUWpz`RG(DH@XB5`AOb7vUp1yEFDTak(ry|hjs6@V0)qhE66h=;k=iYV#&S%k zR8g*RbaV^W9{J@+wqk`_h*RZ9e|oOE>zx}eNolFqmco|UI@|#p#n+myl?d``9Q{SC zb35JbbHx{L9~rDkwX+5I8eD3k%oAvkuNK|Jr|N#iae|ayU}c_Zt@M5QH+6NAvt4yy zEIuW8x+!TPDyG>jO=+WpwT4hPzz+O;YX55A)9k{yoM-Lo_VWjPt&0 zatF8>eES2ll(Olv{Al8lY-R>w`DOq&mY_}E`O7?*Vl`@4Oj7sSE262uQ8>3Zb3J6; z{db6?bWr^qb_=!~ST3wq2C4A!C)4LlIumlVl1uBDR8_bCwQ@~GUMLH{*cIc_*fkjb{`x> zHcQ{c`o`vy&r8*+&Yi@|Z_|eUhXv?13>%@@Ne&W@#quDnB$;yvb+bjr<4&%1idWM6 za!WSBsP^ReU6P_INM12Be-M!bQyd>(gtw#XU+w1rCoB)ZBozv5Fi)%P$0H>{8#7%? zUGo(ar09UZL0_F;Q)Z(OJh60I=Y;RwYzyNeAb0R3g}@VPE2c_0$zmAIo-D%Dh&t39 znU!*ZpLLrsGJKK1GXU%t!OPuKpQ1Ghf%NG)mK4|qRbVyt5ID0Kvz))JPI+ov4{y+s zJ^e=hH%6qZim+Fhl=i8&sqr$&EpNy%Q?N+2J*~1k>{GUVBb4hx3u9mQb{;;Ah+qnK zg8w#Z#_!_g9}Ll+9ePBs`%wE2(3j$xt%3O7SoiXH*-yvTarntSV@`7Z*S)T672xI! zWoUmzp*-AcwD3!var~)9@FVv3Zfp%)@2?WRxgauOf4WVwPAZwY;luDORyR#mqO0fu z7)!hd`?Hjc_KKg^*MAyY(GR7Qfhwr5-8ALO3?~)s@wQt`-01DgDw|Hfdn zNJy8obV`Q;BHfBe3@Je*L^?JE0|Dud2`Hd6h%_4^qeHr$$sUSw;US?f!D4+V$~K)Q{QCKPHQGmh^$|Hr1l(gCe(X`S5>@*yr&8#537I z*Y2)Bm}*?HyPn2Mdan!zPa-8kq!&Xw{C(x|Kf@lCTNpEvk1ur%20-$RCeEZf#4nM; zl2dfi3wOh6>T~p0e8!hz?ym@7EZf%g`75#yb%{ zdBklq+b1PSgtvRbpOB_9#HP*B9a7Gd+GsA>8ljAIPvVf9EXSUPYweLUcPcfvOQ?wGEBZs|jb2omIs#%UJ-Bw{)8 zeJj(5$|D~L&XoCp-8@OK#8h)2WrdviK*jspy-RIp)yc&|7qZ9?^x9+=wH4sD?M;|d zC?oU1a_=qd6c+#LOWiHSCst?X=2SUIkUG(m2t&L8=#tRcnD#}Esh1WgJyL#1f;;X- ztkUDU_TD`NQD6`q-{)s8}`PYweC1_4Q~VP(MkpY*XZBr zSp+$S(u11V7+NVzPRUn+o*+_1d?;gEbFHDjrKa$xTeWSZt3EIbT>-+AmVmWIiO4$n zK1E=$bmf<4)L3NxSrIG-K_Q9y;j>eqHe-BkBW7iZ$O*ZC*}4rNr>J@ih(+|?&V52G>r^i)CPi&4ESq0tkGWHiZHO)seis%UqiMX?{g()r< z_c-whGoZ$qRk({+LDe`p7NPk+4t? zHl=KpyWpAk7&=2?Y^o%REG1uyJVtlwP1+qI(g8D*Nyd@mlXx^)C^3AbP_nIh}n?hSN zlooftH!Pq^g5d=sMlw;b#P~xZJ=|vB_(QisJcOw*2|g0Y$ArAX*0}MHsI0a?Y+GXx znn7D3i+-Bj$?>@l6M0I*bj_9kPtxj2_-` z`8c8Nm8EYY06IDDI`=vIJGp-^;fD=@te~eY`c2^5S674ecxviFWiRM?@ML9JkXuCiObAi{pDf~&C=7Qg;|r~?xYeEVNF9@L9+A(VcQN%!HSUYt zA+tHXNen*q&51OLGte?%UKov3x`Drt(NT;zD{?RD-o?yzB3$)th_%`;80do%&Q_{S z&OO4$OxV}Lv91?{qal@{im!dgOJO3BwDxK$rWRUVj8Tl%3`i$*ltxCB)!O3MY2o(^ z*9?u-{*ixqydBcE^Zi_P_e;mGLs&KgG1+w|y$R|9MAR3OSVR4+djgOM#_I=9JFZJx zY~)SA=jW~^Xyi7nE!GYzqITDob=1Q>H@0f0WT$Wtx(|{Lt^fh(zj`~Va2vwVxF{=q2#QpQ%vivw-0`3>^B*D)Sf3pS97OEsSc-Yj;GObd#%z5$m@* z|Bz!_44nETp7;E;#}DW4`@6RyI~Ay>&2aAM?Dnwu_A@3HD;Y7Sa3x8lxc0h8Yz4_; zQg4~MA}W}1YdJO^*=uq-b&UJxDtS(-5TD;i%u~A_vLb3;j-p+=O}A$E@}MI*JVM)x zSJVqWdd~jdw>09ILB#|VN?L1756za@LRMlLeIuME>uaBnQD@}dW1c@QP|JGln~jin z@}NQ?X6{6_Cuh9_t{%4$_E0~GSvoJXU`Pt%)@sb-y@(h8 z`yuq!82W(IdBx4Cl=SyJHPs{vJyWQfiNn-Z3QIK{`C>gm;icF|ga-q7M0B?+v+!_f%Rf!%=I(*a$<$Je$O8Dnb51q) zGKt`5}`nxZfnPJH@5z2G_7GJox-ULC@G zb$|E2-I5>=rCRfJtP0ms%7kAAe0<5~t@K0u6e4R}m*au+av}I4E@c=mctuxV6auif zE$qouFidk}`<=Y0Vopo|nVwi6MQu3j`cne5#zTUf9lFEyWfG2zsNQMmMVB~M;mLE( z!9KV1R%JH1NY8mp3nlPD)D_4rOPKt~)3Pb2Y%buS1;0KckYUt6sev&EUv;XwmB~b! zixOUocQjhAM=3%cN6)O0_diz7INW{hYY)MLQZKk5=V9*cP1A%IGwZU!!Qm9_8TmdG z-CXoX;rjIDS1)?Eq_LT?mf*$rBCP>>!{m|KQrQq2zwfLhDC!#F^opv!DG~T#X zOsby{o`Vl~^emiK@vc57sL)!SZVi7ctwA5v(m)$?jbF6>__>eYHmP@xaZaIS7?s5~ zJ$NaqM6bN1oZ24HU^#4UG#};6&e&jiGfUgp>d97n<9S4+w5%HMz@iTGg%v%JP}Sje z8}w+db19!k@S&s_4nF>AbZGcsgM)lZoZ^Kos;ui4< z1KzxV1)8|d63e9Qr_d>Jk~B!ASRix`)b>H!o=0@|bmw(Mb!LhloIHobKqL_PynQSF zU4aO51>b?d#3)_Gnjku#g*DySVRVkXf7tDbC5eVp0fkBUht<>5zjR*v!K4p@9~KN( z9@<6Es9=W%^!_*zY0!Azu6U_BarXE);SlxO?61DZs0e zU38# z*?E#zFFK<$)d;g!kKQsMZ92*Xxm*3aVdDlm;gM{`hV83NXUY~(CJ&xH&hh)~coy5N zdp^4p80IHp`t4nQ4cB8@Aj{-kJM7XCD{kaR7mxI5$(C#=b__EsM^Ubc-@!~1sgSow zRSRqTpZ~TSP}5_cU$}!(W?#EKV)LQ5!;1Jsp>9!MHgT0n47|cpTRq8#e8TqRvWax9 zkK?&9^L*UQDEh~9<7&X%1QzN%*Dc&796b1O%0Z3S=&u~3#GBK;WsGy9O9fqj#4m~( zp?K-$ly3eRDI>AtA<}_W6!5>~oJJDO&Ei957P+m)%iwI&u9?$*!I(-5&of{6*YEF_ zT$$=uX#i1^wjoi7_mA({7JnxLj<0Wm7w=%^t7nnXVQpE^X#qVpjq*OMO|i(tzdi0O-i2~ z9xUP&>3~XWKg!{VyaSt7Q4g;QzupiMeURzgkN+titS4-LFT> ze=)=~E?S%kQ+YnO6?*xchu&*4ow&w!pR|E9X0?d!B3pv)6YIrttzNr5k;V#j?LovI zRb8N)yYaP1K{tbcL~!udzBKRdJ;`4GNfhN)zg8T7)E{K=I_8luA-;Z<-1Pc4ViP{- zlpQ1|$r|)dorU8rvr3XX(sEPq*E|W|;b7>4dqLkL2@;a$d=9wQl(038oDfh(VxrWs zt$S;+d%@LiYpD`;T+o5ZbFt%T;P6^jZw?n7@OFB_m^Gcy1Errf9lC%Gu`iL=Y)3Vo z0_Dr^=3j7B-O}9NJ}$W7fRicc)RrP6Ny->!i=!em)f`DBN-nYACsp$|H zs7RL4^NmrFX*hyI;4|$5s=D!W(b}Z+Y4BB3f+d0$hWt)Q3sUI|^PJNUVy%@Si!5!~ zOLh-~=gg4}44@StTf<*S>?etVP_;+Kg^5V{XGnON{cu-mTHpk{ZqamLm1vub&-~?S z?!fs{^o&jAD|Lh~mx0&`#+9`Dbrw4y5{qtWI+cSaG3mk6Z`q`j8BYX87ENok=a~}b zzl1M!6gZMaBKy4Nep`E`83LihdLV0?%7dXqPehhZ9bSM&8-1HKZunL1wQulD15+&x z>D^QIX~BD2-FSa_4*{?Z&il>(O&;5LzKWatlVX%F=*Oy!CeRAsd)B8sW$>63_ z{^h3AfjCi85DHGBoMr7TRrel=nNSK4mMqm=zUg-R$#g zXP%FlIgg<1*?4_7_u55o%Ed#6qj2|j)jX6>S|34#MR*rzvH6b3;XB22TH>VmN9(02 zUs{}s>GBv1PWBBRA8dEE+dZObBrMJta4mu^+YHrh6B6-@Z+@uo*Z^bMuS1PN4~qpg zo?M(=#&tI0%nNr@9RE9n!C{2{bAe?3i+|xYn?e%0LiGI)W2nJB;BCB? z6p6-5N^kpn*3IwS!;l02o-=#gtf7I}b4U9!&61Za3`i%_iYTMgXtlWnsPcwl2TL8# zK{3)lXV0aYNdU7#(P#1*oRi%_te%Sg$I33~TndkTXT2*gsmvM3SBKqDa!>yhwro0J z^ocPvHU9;#`H}w)A6D)QS?x)fdQv0)25<@N38VSZld%I;`Z6;Z2X8hPU&J#rTJYp+ zS3dd+)LU&|vPk&ADy%OS?)hVj&M^Eg(Z}_uKZEG3*l+$)6K9JoH^AA^1=2WvSj%%- zY=0ZTq#DW---3G@R(NfJ?Z+H*Y*smmlOO1y*>a~qnhe;5_fs8$jEN71!r!*ae-meDsup1h{HYJ>wdD5nOueWa#&h?@+AmW3Hw&l&W3m8yJKFO5 zyZIxXq@4S8YRU&;oX~h$IW^D&h(J>giIJ@&-$4#^`g&Y9lobnn>aVr z$T0c8+-WGX_wQOtUU?L&FE?|QA@n=;pBwhQhe3nrpykq3@YMG-;vJkBG2g>|`}i4~ z4w$HYq@c_nf{3(V#j7h%)bK~`3u$bx#b(GK1B9x!66IaiZaD9(2JH4d1ECNO{i*0LXZPqO7dvLPg78*&E_3=YsA>%1 zYDTlzOH6QWH*%dCA{bfCq$y`B28dP}zr9e=-RWWt-~m*BI*ide8ZBMJGIF@~6T0?~ ztqv+N#Z1M;OlSwkDxdpeYU z-J*&e?5kv^-03VJLF@f&lkuc5BDO(jQYGvcD2{@bf|bvn^ymX9tl>{boubiMP!;KRtL!=s*XGut+_s3uZhpKIigt-5LJ%> z@@apk3JSB`9J(hf26<^h{Eww>F*QRBO~_{BLT@pO5CdoQ~Ke0w4jjg4cBlHK3kUB4}xJrHoB<$su0M1p}rjp3gGO5G!K5P=6Eiy;FrgygpNi zb;3yiFcR|~2SG4DsP$}rO*PApT4S7Ml_GjJ#Tu78`6c{EvsICj5@dZ)Ltnx{=$K6k zaGBOkSg-KyI<0yCgVZzr)Y5DxHYv2WkM}J_Tq>V@ z0~+?IcED__F|Rjy2P3NlG3Bv3$2>sb`NW-N)eMprAgQnW9^w4#y_icUR$yQiw6r@y zKk0D07}t(cQlb_Lu=nl4GxeVa|F{mEyn!lPd<8TQ*OE$VvJqte>-i0+F${=7p22wX zQA*$=c-1;X2X?RHxOy{)%NxCdF%~WlEs*nk4XDXBaq9kUY{3p?e8y>_$n+E_gb}P;^bS_&k%zd69d17XPv3o6|s!=bm@#A2G^j-H- z?}!;kSkq&+Nt*V?RCEi|VHr38c66%)3-%lLWNxU@a_eqI-{d?epd3bTM*ZQC&H#FH z@@43z1%Q}>7yBY_8LHpyo^_PNf(2M9+dq)vDNfE;;d6TivW&tfC>!p)aANPOwel8| zcKfb7q~PdYM}M^;YEW`>#4vvX$q!|Q65uq~p-+vS7bk8y&kbxT+mO_g zxt5r(H8(K333M5cf84-PYbl)qoaw?%Do#k-rVnMY8;b^Ca$K{tPg~kX_$i?|s(sk$ zYOSWsB#C&rh0oPg+**?tLW>GgRL}81hG{{+Y(X|+Cr^=6qFL#c`z4G;&mgn?tQ|2- zs;_#VVS!H*Q20^XHzM@tAkKPkrJ=(5k;k_mncWzxA5}Dzr8S|k3KA^yXW+Y^JL>2oDudSiieHakK_!l=EPH)Xz zHQ$c^3bI|KVvawx*gWsO|NDm9S?|S;TU=mancUD-*K*}{4p99 zX3wI#mFOO34~r-+4}sS9JMjT*yx^ny4qC|m^gGqwo_dD?ctnvmpjh_u6EA*BJiV?v z9pF>PlrW+kBw$~(wd0}@M$K(^&Ay*N%jg?qcol=aBAN!BG@1To{(2bZ#JfkA74B13 zTz*bFZ7ZDwP!LX^#``hk>Kq*GAv;dM0^Q`s(4&onrMk^s5$k2WD~LtW^yLlgngjne zh(RtA_`E>h)i(lwm1^qr4xXOW?&^3?A~7hS`6yi>SOuBb^ScNVAaLt%_;d< zMV2dJ*&3PuCVNV$-&9W}$);cmr9T9yoDdJz^-pBMkkl{a;1eFkp#vdM3Sb|?_d$<4naJhU|`-5HLs-Zr7 z8UB(Qm+@9i8NgE>Nt=LK*t1Da0+m-BGi-SwfchoUboBYdQr}u4rp#YpdNj1VI3O+I z`#j&DQ9u4+64bnPl~DZQE686>a`AMn>l(2&RHz7qtG-ZQWEH$J-YS;hS8-@+&hH-N z{r6n^G)M}Rl<2K~JM?=#jibL(j-9Wdg&8Vlpn?V(thbx)^FHO`SQNB4aue>}39tJv zOuV2#Y$9|*DM^9J*~4BQbVc{>ktNy+j$H|s@tZ#7h`?t#3zpO@V`GvN{Fgd9(YdV{#Am(24z_UF`{g_P-5GJzp&bZyl>lZmgXRL9#%K7eQmU2`c#Jm<)e*8Dqp#bpf_sM;S4zP}}V2KJ$O#9~qlO=^O;98cXiB zF|rD`z6~7C-Ii|@xErt-Qn5#Om{oWpR}j`XGHj<`^Dyi|@|~+0nk$Jsh|cf#@xZ8E zELnITQV~5f3&k^5#b1Zg^fPn_<}_&p>iBIvZ}4t+O4p!}Q+*5)g_=Ce| z&+f#|nVp&a4hRGWe*gOcgD60*E+7y-@EoSBD20yl4h48bmywoG0f8XEBLswu2>iKn zpM3xUFUv@LRCCWbUi0u)pPRdTofO$yPY+{{$s7Cg!zt5t4r9Jr9HO%)qSMe7-N=Zw ze<@_!&?tgERaV*9pk7|d^;5#8d_H%csDaNfJV2f^m6ItKJk}7$}E)%BH+JON>St zF`)^X2;zXP9WvF;X}+_BYpjPhfW~38M$a0C<=s_1zpyR)mhlMd5ZciOF3y?tpos27 zuTT-$(hQ;lP=X5V?qG8OJlUPbNm7Tzjy;BKq-4Zbqx-07_$$zjFw0Pm$3qK~87T>Z zT4(4gl7v~q+&=|z*Fse-N9a|TbwL(K%2RVp<%28A0>{6TtvQa~!eW1fd#r~>Q9SM% zAWs$}==gtzdBI4So!D;_DzxQCLo{StyXzhEJ~FCDqT~jCVO+I-2f~J(5<2lYP&**k z@GdpooA_WXfSvr8V9XVn2+ZBhw(|VyJe-B$VzsV9viIAAT)7{>Jo*?DzJ#5~-f#E_ z2_r?EvIikdehdD4wKv*E7|}EG=BV<*x;z4fD3Pv`vMzIdp6t}HBxo0m38NIbV_+tno|vBXYtaqXH;lq`!VJr?yfw1gAUW(+vw0^Pnai#<|JA`K98P znnqmn`*`;>2Lql=8i>AuXeE$&#R46jJGlL#>*Gb5QPl!q)cEj9(l8nZVR5G}nS}^0(yoIJDKwsD+54_pP z<8G*R!&SmS9L-??*#jZP3AF<}&=mQ*nP$qq`^ku8iLEhpJtXR@JfK_C7KCjILQCg=xo(io>VtDTWI>sqlm;-7?WBo9AqaV)FWk%lc z^1aI|-8=^(V0HQrVCCODxfmqHXr`^1_oOc$<&OLR0t=?(oOZh{$1Bg2te$F^F&Pj z&g|$4vnRz6vH|~Rnv$U*3in*I;uctJM_y1|fUM9}76*i9P;Bt#kwi0K+n3;uVMImSKEIB-`314`SG z%EM-Tl~amzLQ>ADW}H~?U^+*fkA}0SY(PjeEf+foC_GFfZ$};w zMj5-rUEVxv{z)G=jq-!W`5aTw^jBB$bolXE<$01hMo-Ef`^4{z=XPi2GLU6V(Rbb! zFtdfiAV*uSbc^9B;MT!BY}GJ|>V<-Pb4)5+It ze$e>4h+seng@g&1*TNy4aHQKc5;bG+ zZ^`qEJbZ}B7{StQN@|6{`p9zs^hL=}z<_<`r+PQXvac+mmof93_9W5?<{M8gZe54r z(Wu!ZO+GRdD#ZvlASX00a{hQT^7>liV1Ql_KregVf?ChPmL|FssgoL@*N3gFQik`W z=If$&i?%cp@S6kPG}@?(ZTxSCwv5ia++hAENyFG#x=2)<;$+iRflZs~t>a(OO}?ae zi5u-`1v>eSGUtv$_9P2XQ&fY$F@j}6|M6#H;sY6MLE=*tNYKZZ`#O1bB&CfoU~Yp{ zK&9?xA;9A#(xAgE?enQgvb-Yfnl;*q=?l?QRQ%c+Ion4hC|m%gbn7YZ;S6_PJnRB(?VhiFG<*?1rjFlZ* zVhS6r{hcXfLuf($mMY?G?5_}vpBQGLk^b&hxa&m;%boH9ZEUcR)__f2_Z#;j4XebQ z_XTpG{-PpEj9t4=!gqo?gNC0C2dxqv$nk_ggRA2~Ar5SCjE4Jp)*Sa2hLi@LEDp8K z^n$as(Pd4yrJtr8)uu*+JZOzRM29&|l`35v$^)*@&-qP`mb@=X`@9p!E?W@yg_G%0 z=F3B^8p5Et67tvSY5!ws#8F-u>eKCH!{q4C2oi>M;lZ}Tgr+ZIT1c;2?*;YC0=pQc zX&>(~?9ovPdda05NvTPHphVv^B24Eltt+=>TkpQS8m&argXHePj8zx(E>=ls zySmk@^p8X}oq4^Xa$>Q6zH~UCPv~;S6w56uJqvR*K)i{=*x|x9^BgcN#YlC&2?FH` z((+;2#N;$u=Q~r3kPE;m{}@o>CEPQ2KG$S?5)pZMC*Wl~HZGr%%NC9x3#|<{|3u1n z>OuG-yXe~`aL3eOn)-oSw;+Epsec*uygAuQ9qCW!R5t zc#3e^F%a*Is2E@Lb8tm~HsFj6b~)=gyY1HG2>pvEXF`pXS%5I(HZ?W}$pV%WX+^_V&LsJP z{1)#1JOzd%du!8+UFtd=&Vmjs{n_|$!%65Ep|6zjok>>P1>4H}$a6$E4G|fO>^YJG zXcTSyG7i?il5Osw&!cyLYO*5VEk=Fp7$usji~KjuS6Dm2B2^$Pr)m?fFfNl+z0wmJ zrO@8^m1>gJW>=IY_rPIJF_OWB-?+(+grfq9>Oq~o#*JUNEpe*ygW}l1s-Tk1a(*HA z=j|emq-rbkmonNc!}?gdce+{(iSz?<-763t+^|Dxy;}pLSZ9i|OJ_t1Yio1AFi{Zj zwV@EwGhlkJz1FW6X8h8$#~@xxe+$<(Qzx`_^isky{DHjE%6#I_wGf9riyND59*w+x z(C4aEjttS}fa=q=P`J=*bBAmcM%gE?rdrs*E;V`8YTps+!o`r+k^5KMa{Ck8J5&!0bd?fqM^KHyWMvw~pJMT^9M(-r|P@pNsq=|?a@C2v{~*p2u&{*c=in;Wv*Tym#h1Beu*QVC&D3e z0=(JL6<BB?+ zpC9HkookeFJlmyIt>>fPtAt4evkFBw*XkOWi;byRD z47B`7S-bz^3wN-_@$)cTYQMck+B+9|wMQ(4VeF6{-VS}G_|Q<~aag&dcY8+D zzFNU1uJ7{obEM4RNp~{lYShTIr2#{S_ZmK#rw50qPe<&y?=k5G=MuHpI+E6zn02`> zaN@Q};`q_0u;b|6h8p<-pL(stI@>)5>`vJ0-x`h-yNS_lz@4(4r6T9{PQSdT6~E21 z=OdF5Ao?q53$X(P1SpSR-E>nwWJ#E|?P ztcT?u&6z1u({(Un#K2a)((~Sh>1E*vUzd;vuGGn#sPj?FeNBlY7q8F_xJ$cPMj&D6 zuDj+mQq)5!glQ$L8y))8_xAn#gDENw+5}<2guUN?~sufsu-`e zagMqj2jKoOxR9~YIQlP(KXC03n=aZkk@0=e+MoP58IIlg>lGBePi8vb6IC&@9VqzW z{popfywrJMyyQ$Vr<$4l-25Nc*Yx9GdbEkjXY&Ya{^KNG z6PN7{UCzwbbRWRTZKjha!5p`sUlD%*?L|M4>fmp$*XHv-rWhyCp>*LV9uZ2O%?Fls zJl0tC)0*#Hk5{wn2{i|-Op^9)dEGD#rRLZs(&|N%tR>?-CST0=pjQk@eaVgfyBaSN zWinvYl1Xri*-!Mj!88pNwDdO=43r=gK45H%$uQifJaoA`AT(t{DNIAIdT{r3U!$)o zq|Y0kk=Q9x|Av_Jjr5aD_g&u#Gjnka7&L*BkOGt`)NpC)<8?6;X_7 zW_3R}J^=9pwYMhT1Orb92TiYWPx8-7!p)+h#7&|;jJ?pv zQ8vegE!6x6hp#d4N|T0@yYD9;u1@Hf8I5#%WV{&?bfY{3aU;L@4A-kgJJNLT_eOSE zeR<>8%Bc*AjnL*F<0l|N>9inYbyduP7LpR|sNN{I`=l;xIH-s*^2W7}#gQu9#I}hGS|D;`KFIKF7)Ru}348%ZQwW z^I>7R=I3-I+`n(Tyx9>Y(GMrSbl?ba+LN-hC}I%7r5yW$_}4g?cS^Vnw);b=x0MBR zxIo5#o8O>mA`ea#>c1vN8+%?`DiU(h$9%*qqV#EVTtPrO-#Puw=6>nXxhmXkOF#nq z*|}CA{)QVagWa6BNHgYiSze5p>`oyw+l+Ov5A`vS)Ob1@qd|C}6e1ZhFcn<-iVcUE z#i@eys<_ypga8$SqDiUY?$!=bF_}DFJaHV8{l=cejwa{&fuu+zBco|P`R5NE(@l?~ zVlu`j+iYMTxpLRRzSQpmiH}q&Usqztdt{K#vIhOad(U4ISAFMw(U@vTFT_nTpaM3c znKx(DaNf_GYxgr3;@@9sqtkt|%$|R|;X*wU*QdBj#%4s+yF@6cemv<}d7Tx1aJs4O zbr~0xdPG0y&n}lzrGKP}@^34Xs@wQ!Fr6zA$Kh~V+neg?@VeS1BmcB`!qI$ctm7f# z?TllT@|E{h0gQngL76l}_B`6%AJ}$L=lJ-gOzOGyt7VrRT7%U)e5tOChfKrO%rZ8# z-$6B~B&K{0T5K9DdZWkV{0R@-h&6Gy@PzC;Brd!12Hwhyhz+Cya9FfeZN%(~6+Wil zMJ}OQ+L1N`Cvu;|?wn2k62a|}@&>%|x=Fw&l%qXfioc8|&JBc;sU`F^d#?JDXe4c4 z;xT=^L-!_8{Kh*BnzaCqQ~bn0@ZP5!d1gf3teK@PLWkbc{KZN)j~7!$m9UiFKGmB) z=l?8e)uhz=Qrt%jKI?C>Z6MyV?uDpI#m><6q_nT?#O8BG-m`h0QW6C&hlpevUu5e3 zfzgCCiO1<@+C`%$bZ{gofC9z1^BP-0s+5>Se=5=i<1p-6~70y zFjM_Rs3D1I^HtxMmg;B3LKo4Hy}j}p^jnzUy1w(^zS8hkrRwFM@&UW+FqK%X7`iF9fsXZ<@H%qPGoNpG0y3>)&vs>v2l>58$7>0~qiFKN_-o zS6;;y9Cl;H>#H!IhBo;Hab&ob7+g{9z_vR0EySARs-2LMoM1KU z)5f@!s+en;?yQ23X1s7&nI;57ot zh+_-h_~9<_!`B|XYAH&+vhtOP(s1|~^lr*MqGNT9t{`;Q8}ebtnrLFXwNNA%un#8t z3lYt)V=__#+unTF6)Str18xz5!oi4q>IJs(E4y8_K5|6MY#A0)5nona_|8B}LC5VJ&(4>I zKhO>|Wn4B$M%V@I6bJn;|EkGix0Xx)5b3}k!KU)}kyxJKSP?Zl6Y2ZTz?bLkbbh{W zQr?8f=N?4%j2ho;!)I14((-K9qV4BX6TQk(@>vlF^k%@D^ryD_sAI?3&#mR9(D9*< zYeb1Zm)_J^gpDF+zF=mGe^(g!djsz4&Y+%iuLVwiOKx;f@sIk5w5qWe6pDUAIen8W?zyjzMLF9c)Ljbqyk0!*LBs8-eDrkrf+Ss0_Ek7!ZO@<4EiKV`c6_<(^dL3l zEJu&B+y)f@(Krr6P(ZezOGXB&4~aTizPy`PXc8Fl8ja^8@nX5!ZBUSwl}u#*h?BQ@ z6qSp(5r2p{%{x|eKs7u6LP4GM(~a?uAdw2MfH=KqyiyoY8r`lsBSFV(7M|lTz3PJKK^XN!@TDj zhDtts3I!0v^ae4YdihVY^Uid4xQhNOW?Pn7_e-=V!fMuGd)4?fVd=J?aQ89IZ--hy zbMym4YOhCs0+07??Q@fn@7!<~gei0PT(7w(n=r+SvYAyLsF93Kgkf*r+qzwRoF@iR zQNXA?5DSVz)#;k`ee4QRAl+}}XT87yd2ht9+9Saj80~AURj+V_!smFN1MrW!oWMb2 zkSzN4`y&WrK<*HUnF5fjJM0=h1YyH&q3*7JZ-)|VhwjK@F=yG@UMX0YHa>JCc8nS3 zuo^U`&+u6q$B%;W=2HPkd4YUWp0*_S^Ula+z%wbr8X%CoNZf~1_#6^KmfsE>fIP?p z@S@L{9v*(G!_H)oFTTr*?~c#t_ixnpXyQ|=pg1iN{$C$P-vb_JV_*S%Ozz!FyZ5h4 zSGScvHB>7ho2Q`wVyA@TSMx|uK3VCwKl1hu>tgoB49zl&KpN|195A;fcU^KgGe(yASNpkx34 zSb*B7`l|bop4F zo+C~%Esw07!~qunMt&Q0)T}w}+?wU7)q%3FS<%)bdg#XxD(ZKvJb-ZHDVxQwl=gBU z5Ys&O-icS~)ADN4?wpH9+UeTo>I$1e86`;2e0YQIza}@fm}lv|Wp1yL4|;k4(TyRP z?g!dx^>%z7YY3J`Iqx@P1(?@|dG{nqeMIES;PT76_T3h?gP_gF@&9z6ZGHIhE1Z$9 zCW(RmFOKRBrJc!8CSD{BgqlEGd(Ge(n~2M&_K{;a!nt)QBe@l1v)G-)$bIu<*JJJU zASP`wE~e)__hCO2)rV$^9Fv>o!0_Z70zTf|&e^dIN zOWP8QRZ{hwnO}{JnGl&a1s9g+FC(~8@7Z)skA`n2`0GP%xwNT1pg?Jw;8SW=GC&=} zDcCvLVr?F;N%m|8*|%qy=5<_7qTvscr*)N%`!Dug`tY&yxW08OME@&SfK%jOhI z$R?dQxdbbCug=|Gd&B+f0H+)XPmDY)Bm%fZ z_Wfn<7VGLWwD{Uy5H_IDdMU_U5iGS=g&(zb=A>z(Lij$4Shv~6Dr?~tocjm@acPxo zo#04y=VLWU3cSCstEEt_BPbt zIZ~?~(&tFLYJUaxEb*;0=;<+WzM~;Lvh!bXPsOcf-Iu%Q5cI5_In1vXlxi9SI*03e zJBD!YiNOdDlkfB2_;o!Xt>?w0K9J+vo6F=M>lTlkzPw)AFctKIHYckqr)EHuH3hX{ zD@k`+=^xFha%e#!Gw?9BZHs_Dq{Tg?>*1zej&s~Pw`9A`*oLj<*t0F}%Jw$rKp>`= zyxmiw1@9}3qlLLf0F{1UM}3e#apjYbvc0D*-qH1U3J!et^TZfP^-B{KzPp2l`yU3$ zqxAUIr?i-vSHCGqm;v@}$$$8Pr#Y^3d>@1I^p&Ko4JT)Ov5~xi@u!<-2hy%Kvvp5= zZfh$ehRF%-?hKx6CYKRhxI+dvNG>0+b;;j|*sy7wF$IH zy%wlmJsHZ0HBKB5l;XDjjz5(EV2f>TJpvu{GW{ zx86Y8t9oRxub9^cft5O1GAwwDpX{a0F~!_eDg`9(rJ>EfVJ#Tg>L55z<>DtV24ZJ6 zF{6s$E}(jWjO*KG_8TY(nx;AvBG_CU(pqO}zbBO}miPALh|sj@ zm+lg}7~r(iu9p#rq@Z?w12g<@CO2;PuOamdR%C43%jG?XF6 zhTJ1YLmy?R?Q@iZ-pgH&2}Ao|lLjomY!qKVY*XP>Yz@eu3x0u6mB*dFPTca=UIYS} zyD??c{r8UYS3BM1J~%eqaWT9fK$*77{Z4;ZjylMsx2D)MAqG$ufvO%i^O@HC7qb|OM> zdQbZhb132I<)Lw7RcREtC|~GOW$?wty^>AHD0xQjauLsg@_U<`P@9&&zY%Z;=p6A(0J88%gMXgeDuiCu? z4roIQ75CbfKgQN_JFlyKIJCT8?>zcLQz5`UiSLD?=AyO?e^?lNmn^1F2nbEWt4+(le5RZ5TL;lBmk%y#TEh@eBWsepf{+Uaf~ z1J6_cTN_Pz`rVCw9jfa%)WWyTl8a!2TUQRTsSf{z+XA8ZWyjWF4ZCOQZR%d5x)S(K z?mwYY;VBBJj^pYF-*D|Y4$hGOlqWdjvSn!(s*WuaG*{I$YI_zu+G?m&+u%q~-sKLt zHYNV?b$NTjV%Mo>%!3)@q>l0*s&%iY@v}cJJiu36Em025xohugyiW7gy_tOHtAd6d z@{1HzC3oUh6?A+r2x)yQiv9Q)!=G1&E8`p{xO!cEUyr-B1}^8RXTN{pZTZ_0)6?jA zYsL9If?x3)9wzmlldD*T3wQE$qx0=BqAFg$5t!_1E3=eEVzd7CM+%xc-t_YPwzB^s zyM1(-+52dN{35^2f?&K*C96T`^^{TZar@}Wvs?1Op~o7>Q40GrgI+A zU^Ipp>q_<4!f9Tm$%slbn?0eLte&{=;+&4Ht*7q%Ca*#MaS!s+Y7*hh7hB7><2Wx$ z;MkoZIRJH`MnXG^qH^dhB-c?TdbuC?Ugc@d@oM#EOR{T{YNq-d@@=d16`rI$v7}Ts zLiDBCW{ud23yDL1;zFgo*XnJz=X!Qins6%};Z!-=LYQ4~7hs*fmy3@Xue0yhnNL5l z@_Ust^xN8V_Z*I+9q1}f+R?;z5Q=2hwhKVH<1IRSUps#fg-_XCJ(3up1Z>3Il31O6 zG4Q0=|KHQn657ac$2M zZ!NlrThirp)bjcsZ!24O=<|^}hYAn40;PTF2MhSyBKES<7%7JxkhoT~Lu(Gyj}n1+0*<5qD^kt>C|~MgQ~E854pUztip^zB!LKY&{I~ z{^jE{pJNv|a=8+TriZkv;r#cqxw%hGnxLWXaQ;#FqYzX%t)lSe8GW|1%Rk;T$>D$i z(eZA-^LV}ZlXtcl=k`VSzbGNWrdZt#%dd92{-jn3`+;Aid!rf5PsTlE)Tk~1FwF8ZJn197HM%4E~%iWNLB8>9>dk8&hzCD=}s^iekF*{( z(qg&$m_2Qth9XP7jh;~8{%0UA>I3s4%x0WnT3|Oc3DrASH)BTXY7<5&Ovqmo=PNvV z@CE;D+5p>z(v-!9x2e`dpNpJ%qpzZ$xKy`34B-p^zyjWUpoYJ$a3!gt?Ofg_@&Cld zrA4{erW#-~|3+OEvM?Rcy@s99*3t+a5VG7KWhNjBvxkqR(HOql7iU=a68 zR55s0tgD?d5=ThG5xC8#Wi#xaG@mdyFptYpxis0|LpMTX;3uH}MBb8rVz=@8A6w%PO(hf+8UH zfCDR3>m0hGi;%WKQ0M~AH#)jE?4swc*N6)LJ8-#_kzmN+p5TlL&dgOux zZ1c)lG?=%qTarZE=g&Znr%mh)0kxv#BpeE~_(~LQFazoGoxKS1EYfc?fQIJq6&2Y$ z>rcz(MPR8emmjNdl#L3d6IHt752Q3qPIQ0_A_|C>oicbR?!IYQ0Jv;@nwc18Sl9su z3-2mj$E-Q*GE#6 zN%`CE_iuO#EZ0Wqx~x+;W!W9+ve%lStK|Pl#I@Gp0;+8)-}tF8YH~hHF)iK#*`p+o z&{JmW#7M{(0Vrsdmbe2^{4KM2xet1gK!xQK^DfvPe!WT$kF8KG!NX)uVgtt4>c#(G z86dau-6mdaBbNXwpaANYrC!Yx9TuRb128~8KSB*M&?(R^R0|5bCcdG`4T*@O1%0f& zAvJQ^JUJ8rv|}-RMV!9OD_`yhl#vsr?TG!|;OrN46d(yIZ4;x?m9JfL70&q*z5TK{ zemV7ai0+C7wrfZ^3Crq4wPwB0c#M~NXAYpcMz1;u5n`Hd{7>8y##$orM|1{vR(#+Z{JupR#Ha0o|mjWnY_I#5OYx!^9Ck0s3UHG6ZQE_j*M z02mzSL|vqwZd^SzC{$Yh@0xMjV?rD3=LG@pL?15^n_p80QgAG z8aH_JOKjuy2R{{SufMLZa6%*5tjX4GCBR)lec~nTAEwx@)eK-vvyFcrg3;zh1`h94 zP6)UaK-2}tLYqGSwzM6Aq1RoURE%p&TgPnOoaec&do+rjZV;<0oiCie}5o1cX&?y z->UKrL}e6YAju2Is6O`f(T_&U;HR~o88E|YtAd(czv9_hrNXK(fqiKYDsX8yZ6$3C zl!QI^HT^osfAR;$%h;HS6^6%NQ{RF;w7f*QUX{vx=70jMRo1P*d6w2=E0R>{AF9XI zE;*o{It)@vq6JMCMPK{1^)D+GUrFbv^!Mo;z;D{w@rf6ci37@JuemYEQ7Ca7+BX0d zhD!>B&2CZUpxd0qTkH9x(SmY~k|9dr+|_j;Sv(}0!Whsij*Xe3Na^GhJO!F{dH1ql z-*WO9Zi33kp&zu5_@kuql}Xlyw&FN`Q|Dv|Nk6!=6%U#HRD#EJ4)URLwKr$~-H-tG z$*fY!vpH#UGHQ>eB6}}$b3(qUIBomMD|7aS@BlQcVf_w(JqqPW6!@6t6uf^dRw;AP zLbV-_dm~0XfBM;W&ZHE$(>g#COZj??y=f1uqx6=s@QI?U?-u#%-z2^{SHM(g7c_M% zI*7Wt1eS2Xo)Vg+0mi}yJ00J;Gz1lSiGl%mzp?4bCCnxOEGBCKPGBS~<)PkS@YB5D z^t@0)1{UB^Ef?P8&Exp0;;EXW5)2!ao`0JB^%P>Y?X1HQ1ULdJn_{>K2~+6_o$^KMPVK@E|RxzsXF^ag3_hlLbZgLVE_EbBo4f78wb=^}^{i$P$-Q#jx`=qBHEGjch{CA=h?()|z}_uWD7Hx}R=hDOC!@DJ$cjXOF@Dh;See=Kukf|0rC8sFVzIulYZ zN*4e=V$0pYzCY|}ll(hyyXeY|{=8xV_6!?%%jU23=>vS>tYSgDu{3DDDfwBSh4jaR z_kVBMRlIHfEP$pWbACl%t&+RrVNCc0_Ki3?+$0AB;|Sd708CGxS_xjvm0V1w#S&Sz zl+({b}T9>E)501nFl=r@);#NYE?KN#i7yJh285U7VlVPp+x6t=voUch-8CmtFP~z)c}-6!XEi$5 z0H~V~z}0&;F3jdHz+x+JXv8mR^VGKF8iUQwk(%G8&v%RFT{WgT}wd?IDQTpgK`bQg;-%K!Na3LR_9GdgO~dX z0N*J9-`r<1HDmB(Nf^e;yio@FfTC33}2Omv2B`_%%YeFAoSF1 zQV^kj1cYL)6=(BAgFgZQ z!!I*6kcuYnf^kLr?9nrfjNbC<4~m50a)w^rapuo_u#Ky&4P631w-lA z#dU*(@DOLlZ-JsP?gCwxb2bV9j4Ayq89^9mci{V~kP6i$qv$e{#Y2vJ*-R3N9<^Bp*oqtaXaAZzS)^FBT6f{M9b+g*QHh z{t{7h<6vE`A%~Rrlq&}cwpN|7uIL~{UMM29sk0X#>fjY)Xm`HxK;GEQ?pwBx{WjO# z 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 e#^~Fv{2z + + + + + + + + + + + + + + diff --git a/src/icon.svg.import b/src/resources/icons/logos/logo.svg.import similarity index 77% rename from src/icon.svg.import rename to src/resources/icons/logos/logo.svg.import index bd0ca1a..d21bf7c 100644 --- a/src/icon.svg.import +++ b/src/resources/icons/logos/logo.svg.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://du0bhthwac604" -path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +uid="uid://bt4tkqkxr41ob" +path="res://.godot/imported/logo.svg-6ed21cb00320c581a372293d3be1caf2.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://icon.svg" -dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] +source_file="res://resources/icons/logos/logo.svg" +dest_files=["res://.godot/imported/logo.svg-6ed21cb00320c581a372293d3be1caf2.ctex"] [params] diff --git a/src/resources/icons/logos/logo.webp b/src/resources/icons/logos/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..8c42e030c2bd52d06a31a20da2529bf262510d1f GIT binary patch literal 15424 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 q?|wsV1#!k`u%HJ8oQ`RlHy0RVs0000>(#4_x literal 0 HcmV?d00001 diff --git a/src/resources/icons/logos/logo.webp.import b/src/resources/icons/logos/logo.webp.import new file mode 100644 index 0000000..64c377f --- /dev/null +++ b/src/resources/icons/logos/logo.webp.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://du7qkxsv0v82d" +valid=false + +[deps] + +source_file="res://resources/icons/logos/logo.webp" + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 From a7258be31d4293eb39af31c7dae1ede75bb0bb5d Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Wed, 6 May 2026 22:57:02 -0500 Subject: [PATCH 19/19] Create LICENSE I guess I forgot to include the actual license in the file. --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77033a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 OpenMinerva + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.