diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/addons/uuid/plugin.cfg b/addons/uuid/plugin.cfg new file mode 100644 index 0000000..b2a1c89 --- /dev/null +++ b/addons/uuid/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="godot-uuid" +description="Unique identifier generation v4 for Godot Engine" +author="Binogure Studio" +version="3.0.0" +script="plugin.gd" diff --git a/addons/uuid/plugin.gd b/addons/uuid/plugin.gd new file mode 100644 index 0000000..efa8033 --- /dev/null +++ b/addons/uuid/plugin.gd @@ -0,0 +1,10 @@ +@tool +extends EditorPlugin + +const AUTOLOAD_NAME = 'uuid' + +func _enable_plugin() -> void: + add_autoload_singleton(AUTOLOAD_NAME, 'res://addons/uuid/uuid.gd') + +func _disable_plugin() -> void: + remove_autoload_singleton(AUTOLOAD_NAME) diff --git a/addons/uuid/plugin.gd.uid b/addons/uuid/plugin.gd.uid new file mode 100644 index 0000000..123f32b --- /dev/null +++ b/addons/uuid/plugin.gd.uid @@ -0,0 +1 @@ +uid://by83vwyqmco65 diff --git a/addons/uuid/uuid.gd b/addons/uuid/uuid.gd new file mode 100644 index 0000000..9c65ef6 --- /dev/null +++ b/addons/uuid/uuid.gd @@ -0,0 +1,112 @@ +# Note: The code might not be as pretty it could be, since it's written +# in a way that maximizes performance. Methods are inlined and loops are avoided. +extends Node + +const BYTE_MASK: int = 0b11111111 + +static func uuidbin(): + # 16 random bytes with the bytes on index 6 and 8 modified + return [ + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK, + ((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + ] + +static func uuidbinrng(rng: RandomNumberGenerator): + return [ + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK, + ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + ] + +static func v4(): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbin() + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +static func v4_rng(rng: RandomNumberGenerator): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbinrng(rng) + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +var _uuid: Array + +func _init(rng := RandomNumberGenerator.new()): + _uuid = uuidbinrng(rng) + +func as_array(): + return _uuid.duplicate() + +func as_dict(big_endian := true): + if big_endian: + return { + "low" : (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8 ) + _uuid[3], + "mid" : (_uuid[4] << 8 ) + _uuid[5], + "hi" : (_uuid[6] << 8 ) + _uuid[7], + "clock": (_uuid[8] << 8 ) + _uuid[9], + "node" : (_uuid[10] << 40) + (_uuid[11] << 32) + (_uuid[12] << 24) + (_uuid[13] << 16) + (_uuid[14] << 8 ) + _uuid[15] + } + else: + return { + "low" : _uuid[0] + (_uuid[1] << 8 ) + (_uuid[2] << 16) + (_uuid[3] << 24), + "mid" : _uuid[4] + (_uuid[5] << 8 ), + "hi" : _uuid[6] + (_uuid[7] << 8 ), + "clock": _uuid[8] + (_uuid[9] << 8 ), + "node" : _uuid[10] + (_uuid[11] << 8 ) + (_uuid[12] << 16) + (_uuid[13] << 24) + (_uuid[14] << 32) + (_uuid[15] << 40) + } + +func as_string(): + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + _uuid[0], _uuid[1], _uuid[2], _uuid[3], + + # mid + _uuid[4], _uuid[5], + + # hi + _uuid[6], _uuid[7], + + # clock + _uuid[8], _uuid[9], + + # node + _uuid[10], _uuid[11], _uuid[12], _uuid[13], _uuid[14], _uuid[15] + ] + +func is_equal(other): + # Godot Engine compares Array recursively + # There's no need for custom comparison here. + return _uuid == other._uuid diff --git a/addons/uuid/uuid.gd.uid b/addons/uuid/uuid.gd.uid new file mode 100644 index 0000000..85da0c3 --- /dev/null +++ b/addons/uuid/uuid.gd.uid @@ -0,0 +1 @@ +uid://bafv8ihtmdpf3 diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..fdd8ada --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,42 @@ +[preset.0] + +name="Linux" +platform="Linux" +runnable=true +advanced_options=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../../builds/desktop/billiards/lobby_server.x86_64" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=false +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\") +rm -rf \"{temp_dir}\"" diff --git a/globals/globals.gd b/globals/globals.gd new file mode 100644 index 0000000..2589443 --- /dev/null +++ b/globals/globals.gd @@ -0,0 +1,7 @@ +extends Node + + +var game_data: GameData +var game_state: GameState +var lobby_data: LobbyData +var multiplayer_data: MultiplayerData diff --git a/globals/globals.gd.uid b/globals/globals.gd.uid new file mode 100644 index 0000000..5d3f638 --- /dev/null +++ b/globals/globals.gd.uid @@ -0,0 +1 @@ +uid://biu64hvcxb1su diff --git a/globals/instance_launcher.gd b/globals/instance_launcher.gd new file mode 100644 index 0000000..68579a6 --- /dev/null +++ b/globals/instance_launcher.gd @@ -0,0 +1,69 @@ +extends Node + + +var port_range_start: int = 5000 +var port_range_end: int = 5010 +var threads: Dictionary = Dictionary() + + +func _wait_for_child(id: String, port: String, mode: String) -> void: + if port == "-1": + call_deferred("_child_finished", id, -1) + return + + var output := [] + var exit_code := OS.execute( + "./game.sh", ["--", "server_" + port, "game", mode], + output, + true + ) + + call_deferred("_child_finished", id, exit_code) + +func _child_finished(id: String, exit_code: int) -> void: + var thread: Thread = threads.get(id) + if not thread: return + + push_warning("Game Server [ %s ] Exited: %d" % [id, exit_code]) + + thread.wait_to_finish() + threads.erase(id) + +func is_port_free(port: int) -> bool: + var server := TCPServer.new() + var state := server.listen(port) + + match state: + OK: + server.stop() + return true + _: + return false + +func find_free_port(start_port: int = 1024, end_port: int = 65535) -> int: + for port in range(start_port, end_port + 1): + if is_port_free(port): + return port + + return -1 + +func launch_game_server(mode: String) -> String: + var thread := Thread.new() + var thread_id := str( thread.get_instance_id() ) + var port := str( + find_free_port( + port_range_start, + port_range_end + ) + ) + + thread.start( + _wait_for_child.bind( + thread_id, + port, + mode + ) + ) + + threads.set(thread_id, thread) + return port diff --git a/globals/instance_launcher.gd.uid b/globals/instance_launcher.gd.uid new file mode 100644 index 0000000..4a179e0 --- /dev/null +++ b/globals/instance_launcher.gd.uid @@ -0,0 +1 @@ +uid://cru35twp27pa4 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..0463b6e --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b6xgtqmx8kjor" +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/project.godot b/project.godot new file mode 100644 index 0000000..09069b9 --- /dev/null +++ b/project.godot @@ -0,0 +1,30 @@ +; 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="Lobby Server" +run/main_scene="uid://b0cqo3qo62y6n" +config/features=PackedStringArray("4.5", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +Globals="*res://globals/globals.gd" +InstanceLauncher="*res://globals/instance_launcher.gd" + +[display] + +window/size/always_on_top=true + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/scenes/data_bridge/bridge.tscn b/scenes/data_bridge/bridge.tscn new file mode 100644 index 0000000..e06f40d --- /dev/null +++ b/scenes/data_bridge/bridge.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=5 format=3 uid="uid://dy51wny4x53f4"] + +[ext_resource type="Script" uid="uid://dbnl4tjahkoun" path="res://scenes/data_bridge/game_data.gd" id="1_l6w8l"] +[ext_resource type="Script" uid="uid://65x4bq2wsh8k" path="res://scenes/data_bridge/game_state.gd" id="2_6ash1"] +[ext_resource type="Script" uid="uid://dyq4p5e06ib14" path="res://scenes/data_bridge/lobby_data.gd" id="3_l6w8l"] +[ext_resource type="Script" uid="uid://uyimqfjmuc34" path="res://scenes/data_bridge/multiplayer_data.gd" id="4_gcm58"] + +[node name="bridge" type="Node"] + +[node name="game_data" type="Node" parent="."] +script = ExtResource("1_l6w8l") + +[node name="game_state" type="Node" parent="."] +script = ExtResource("2_6ash1") + +[node name="lobby_data" type="Node" parent="."] +script = ExtResource("3_l6w8l") + +[node name="multiplayer" type="Node" parent="."] +script = ExtResource("4_gcm58") diff --git a/scenes/data_bridge/game_data.gd b/scenes/data_bridge/game_data.gd new file mode 100644 index 0000000..73f8093 --- /dev/null +++ b/scenes/data_bridge/game_data.gd @@ -0,0 +1,14 @@ +class_name GameData extends Node + + +func _init() -> void: + Globals.game_data = self + + +@rpc("authority", "reliable") +func set_game_address(_address: String) -> void: + pass + +@rpc("authority", "reliable") +func set_game_port(_port: String) -> void: + pass diff --git a/scenes/data_bridge/game_data.gd.uid b/scenes/data_bridge/game_data.gd.uid new file mode 100644 index 0000000..84defd3 --- /dev/null +++ b/scenes/data_bridge/game_data.gd.uid @@ -0,0 +1 @@ +uid://dbnl4tjahkoun diff --git a/scenes/data_bridge/game_state.gd b/scenes/data_bridge/game_state.gd new file mode 100644 index 0000000..1a16c31 --- /dev/null +++ b/scenes/data_bridge/game_state.gd @@ -0,0 +1,23 @@ +class_name GameState extends Node + + +func _init() -> void: + Globals.game_state = self + + +func set_game_scene(scene_str: String) -> void: + # NOTE: 'node_pth_str' needs to match game node structure else server sync throws errors. + var node_pth_str = "billiards/scene" + var scene = load(scene_str).instantiate() + var target = get_tree().root.get_node(node_pth_str) + scene.name = "lobby_screen" + + clear_game_scene(target) + + target.add_child(scene) + +func clear_game_scene(container: Node) -> void: + for child in container.get_children(): + child.queue_free() + + await get_tree().process_frame diff --git a/scenes/data_bridge/game_state.gd.uid b/scenes/data_bridge/game_state.gd.uid new file mode 100644 index 0000000..1408600 --- /dev/null +++ b/scenes/data_bridge/game_state.gd.uid @@ -0,0 +1 @@ +uid://65x4bq2wsh8k diff --git a/scenes/data_bridge/lobby_data.gd b/scenes/data_bridge/lobby_data.gd new file mode 100644 index 0000000..df5dbb2 --- /dev/null +++ b/scenes/data_bridge/lobby_data.gd @@ -0,0 +1,183 @@ +class_name LobbyData extends Node + + +var uuid = null +var lobby_ui: Node = null + +var IS_VALID_CHARS_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z0-9]+$") +const MATCH_TYPES: Array = ["8ball", "9ball", "snooker"] +const CLIENT_DICT: Dictionary = { + "id": "", + "partner_id": "", + "in_match": false, + "match_id": "" +} +const MATCH_POOL_ENTRY: Dictionary = { + "player1": "", + "player2": "", + "data": {} +} + +var active_match_pool: Dictionary = {} +var match_pool: Dictionary = {} +var client_pool: Dictionary = {} + + +func _init() -> void: + Globals.lobby_data = self + + uuid = load('res://addons/uuid/uuid.gd') + + +func _delete_from_match_pool(match_id: String) -> Dictionary: + if not match_pool.get(match_id): + return {} + + var to_delete: Dictionary = match_pool[match_id].data + match_pool.erase(match_id) + + return to_delete + +func _signal_match_list_remove_entry(match_id: String) -> void: + lobby_ui.rpc_signals.rpc_id(1, "match_list_remove_entry", match_id) + + for client in client_pool.values(): + if client.in_match: continue + + lobby_ui.rpc_signals.rpc_id(client.id, "match_list_remove_entry", match_id) + +func _launch_game_server( + match_entry: Dictionary, + client1: Dictionary, client2: Dictionary +) -> void: + var port: String = InstanceLauncher.launch_game_server(match_entry.data.type) + if port == "-1": + # TODO: Alert clients that the game wont be created due to + # server reaching number of allowed instances + return + + Globals.game_data.rpc_id(client1.id, "set_game_address", "127.0.0.1") + Globals.game_data.rpc_id(client2.id, "set_game_address", "127.0.0.1") + Globals.game_data.rpc_id(client1.id, "set_game_port", port) + Globals.game_data.rpc_id(client2.id, "set_game_port", port) + rpc_id(client1.id, "connect_to_game") + rpc_id(client2.id, "connect_to_game") + +@rpc("any_peer", "reliable") +func match_list_add_entry(match_entry: Dictionary) -> void: + var client1 = client_pool.get( multiplayer.get_remote_sender_id() ) + if not client1: return + + if not client1.match_id.is_empty(): return + if match_entry.name.length() < 1 or match_entry.name.length() > 12: return + if not IS_VALID_CHARS_REGEX.search(match_entry.name): return + if not match_entry.type in MATCH_TYPES: return + + match_entry["match_id"] = uuid.v4() + match_pool[match_entry.match_id] = MATCH_POOL_ENTRY.duplicate_deep() + match_pool[match_entry.match_id].player1 = client1.id + match_pool[match_entry.match_id].data = match_entry + client1.match_id = match_entry.match_id + + lobby_ui.rpc_signals.rpc_id(1, "match_list_add_entry", match_entry) + for client in client_pool.values(): + if client.in_match: continue + lobby_ui.rpc_signals.rpc_id(client.id, "match_list_add_entry", match_entry) + + lobby_ui.rpc_signals.rpc_id(client1.id, "match_list_activate_entry", match_entry.match_id) + + +@rpc("any_peer", "reliable") +func match_list_remove_entry(match_id: String) -> void: + var client1 = client_pool.get( multiplayer.get_remote_sender_id() ) + if not client1: return + if not client1.match_id == match_id: return + + client1.match_id = "" + _delete_from_match_pool(match_id) + _signal_match_list_remove_entry(match_id) + +@rpc("any_peer", "reliable") +func match_list_join_entry(match_id: String) -> void: + var match_entry = match_pool.get(match_id) + if not match_entry: return + + var client1 = client_pool.get( match_entry.player1 ) + var client2 = client_pool.get( multiplayer.get_remote_sender_id() ) + + if not client1: return + if not client2: return + + match_entry.player2 = client2.id + + client1.in_match = true + client1.partner_id = client2.id + + client2.match_id = match_id + client2.in_match = true + client2.partner_id = client1.id + + active_match_pool[match_id] = match_entry + + match_pool.erase(match_id) + client_pool.erase(client1.id) + client_pool.erase(client2.id) + + _signal_match_list_remove_entry(match_id) + _launch_game_server(match_entry, client1, client2) + +# NOTE: Needed for hash check to pass between client and server but isn't used by server... +@rpc("authority", "reliable") +func connect_to_game() -> void: + pass + +@rpc("authority", "call_local", "reliable") +func load_game_scene(_match_entry: Dictionary, _is_player_1: bool) -> void: + pass + +@rpc("authority", "reliable") +func go_back_to_match_screen() -> void: + pass + + +func server_client_connected(id: int) -> void: + push_warning("Client Connected... ID: ", id) + + client_pool[id] = CLIENT_DICT.duplicate_deep() + client_pool[id].id = id + + var matches: Array = [] + for match_entry in match_pool.values(): + matches.append(match_entry.data) + + lobby_ui.rpc_signals.rpc_id(id, "receive_match_list", matches) + +func server_client_disconnected(id: int) -> void: + push_warning("Client Disconnected... ID: ", id) + + var client1 = client_pool.get(id) + if not client1: return + + var client2 = client_pool.get( client1.partner_id ) + var match_entry = _delete_from_match_pool(client1.match_id) + + if not client1.match_id.is_empty() and not match_entry.is_empty(): + lobby_ui.rpc_signals.rpc_id(1, "match_list_remove_entry", match_entry.match_id) + + for client in client_pool.values(): + if client.in_match: continue + if client.id == id: continue + + lobby_ui.rpc_signals.rpc_id(client.id, "match_list_remove_entry", match_entry.match_id) + + match_pool.erase(client1.match_id) + + client_pool.erase(client1.id) + if not client2: return + + if active_match_pool.get(client2.match_id): + match_pool.erase(client2.match_id) + + var _id = client2.id + client2 = CLIENT_DICT.duplicate_deep() + client2.id = _id diff --git a/scenes/data_bridge/lobby_data.gd.uid b/scenes/data_bridge/lobby_data.gd.uid new file mode 100644 index 0000000..52129b9 --- /dev/null +++ b/scenes/data_bridge/lobby_data.gd.uid @@ -0,0 +1 @@ +uid://dyq4p5e06ib14 diff --git a/scenes/data_bridge/multiplayer_data.gd b/scenes/data_bridge/multiplayer_data.gd new file mode 100644 index 0000000..55b7c08 --- /dev/null +++ b/scenes/data_bridge/multiplayer_data.gd @@ -0,0 +1,10 @@ +class_name MultiplayerData extends Node + + +@rpc("authority", "reliable") +func set_multiplayer_active() -> void: + pass + +@rpc("authority", "call_local", "reliable") +func set_active_player_id(_id: int) -> void: + pass diff --git a/scenes/data_bridge/multiplayer_data.gd.uid b/scenes/data_bridge/multiplayer_data.gd.uid new file mode 100644 index 0000000..2ede73f --- /dev/null +++ b/scenes/data_bridge/multiplayer_data.gd.uid @@ -0,0 +1 @@ +uid://uyimqfjmuc34 diff --git a/scenes/screens/lobby_screen/lobby.gd b/scenes/screens/lobby_screen/lobby.gd new file mode 100644 index 0000000..a2c363c --- /dev/null +++ b/scenes/screens/lobby_screen/lobby.gd @@ -0,0 +1,42 @@ +class_name Lobby extends LobbyBase + + +@onready var rpc_signals: Node = $rpc_signals + + +func _ready() -> void: + Globals.lobby_data.lobby_ui = self + setup_signals() + +func _exit_tree() -> void: + if server.peer: + server.close_connection() + +func setup_signals() -> void: + rpc_signals.server_host_started.connect(_server_host_started) + rpc_signals.match_list_entry_added.connect(_on_match_list_entry_added) + rpc_signals.match_list_entry_removed.connect(_on_match_list_entry_removed) + +func _server_host_started() -> void: + host_start_bttn.visible = false + host_stop_bttn.visible = true + +func _on_match_list_entry_added(match_entry: Dictionary) -> void: + push_warning("Client: Added Match List Entry... ", match_entry) + + _create_match_entry(match_entry) + +func _on_match_list_entry_removed(match_id: String) -> void: + push_warning("Client: Removed Match List Entry... ", match_id) + + for match_entry in match_list.get_children(): + if not match_id == match_entry.match_id: continue + match_entry.queue_free() + +func _create_match_entry(match_entry: Dictionary) -> void: + var container = MATCH_ENTRY.instantiate() + match_list.add_child(container) + + container.name_lbl.text = match_entry.name + container.type_lbl.text = match_entry.type + container.match_id = match_entry.match_id diff --git a/scenes/screens/lobby_screen/lobby.gd.uid b/scenes/screens/lobby_screen/lobby.gd.uid new file mode 100644 index 0000000..bc1baf7 --- /dev/null +++ b/scenes/screens/lobby_screen/lobby.gd.uid @@ -0,0 +1 @@ +uid://dk7afnf7ol7dy diff --git a/scenes/screens/lobby_screen/lobby.tscn b/scenes/screens/lobby_screen/lobby.tscn new file mode 100644 index 0000000..02208f2 --- /dev/null +++ b/scenes/screens/lobby_screen/lobby.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=5 format=3 uid="uid://dkes5cdu45q6e"] + +[ext_resource type="Script" uid="uid://dk7afnf7ol7dy" path="res://scenes/screens/lobby_screen/lobby.gd" id="1_hhiik"] +[ext_resource type="PackedScene" uid="uid://rb6av5eg4u0" path="res://scenes/ui_controls/lobby_screen/lobby_ui.tscn" id="2_lqt48"] +[ext_resource type="Script" uid="uid://6sfwe1hwgf0g" path="res://scenes/screens/lobby_screen/lobby_rpc.gd" id="3_rurl6"] +[ext_resource type="Script" uid="uid://bvqtgf3vbsoj4" path="res://scripts/server_networking.gd" id="4_glu7g"] + +[node name="lobby_screen" 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_hhiik") + +[node name="ui" parent="." instance=ExtResource("2_lqt48")] +layout_mode = 1 +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="rpc_signals" type="Node" parent="."] +script = ExtResource("3_rurl6") + +[node name="server" type="Node" parent="."] +script = ExtResource("4_glu7g") + +[connection signal="pressed" from="ui/body/left_body/server_host_hbox/host_start_bttn" to="." method="_on_host_start_bttn_pressed"] +[connection signal="pressed" from="ui/body/left_body/server_host_hbox/host_stop_bttn" to="." method="_on_host_stop_bttn_pressed"] +[connection signal="button_up" from="ui/body/left_body/search_vbox/search_hbox/match_search_bttn" to="." method="_on_match_search_bttn_button_up"] + +[editable path="ui"] diff --git a/scenes/screens/lobby_screen/lobby_base.gd b/scenes/screens/lobby_screen/lobby_base.gd new file mode 100644 index 0000000..c59bd4e --- /dev/null +++ b/scenes/screens/lobby_screen/lobby_base.gd @@ -0,0 +1,39 @@ +class_name LobbyBase extends Control + + +const MATCH_ENTRY = preload("res://scenes/screens/lobby_screen/match_entry.tscn") + +@onready var host_start_bttn: Button = $ui/body/left_body/server_host_hbox/host_start_bttn +@onready var host_stop_bttn: Button = $ui/body/left_body/server_host_hbox/host_stop_bttn +@onready var host_address_input: LineEdit = $ui/body/left_body/server_host_hbox/host_address_input +@onready var host_port_range: SpinBox = $ui/body/left_body/server_host_hbox/host_port_range + +@onready var search_vbox: VBoxContainer = $ui/body/left_body/search_vbox +@onready var match_search_input: LineEdit = $ui/body/left_body/search_vbox/search_hbox/match_search_input +@onready var match_list: VBoxContainer = $ui/body/left_body/search_vbox/scroll_container/match_list + +@onready var server: Node = $server + + + +func _on_home_bttn_pressed() -> void: + Globals.game_state.set_game_scene( + "res://scenes/screens/start_screen/start.tscn" + ) + +func _on_host_start_bttn_pressed() -> void: + server.start_server(host_address_input.text, int(host_port_range.value)) + +func _on_host_stop_bttn_pressed() -> void: + if not server.peer: return + + server.close_connection() + + host_start_bttn.visible = true + host_stop_bttn.visible = false + +func _on_search_bttn_pressed() -> void: + pass + +func _on_match_search_bttn_button_up() -> void: + pass diff --git a/scenes/screens/lobby_screen/lobby_base.gd.uid b/scenes/screens/lobby_screen/lobby_base.gd.uid new file mode 100644 index 0000000..72c884f --- /dev/null +++ b/scenes/screens/lobby_screen/lobby_base.gd.uid @@ -0,0 +1 @@ +uid://5tvhtoe6fdu1 diff --git a/scenes/screens/lobby_screen/lobby_rpc.gd b/scenes/screens/lobby_screen/lobby_rpc.gd new file mode 100644 index 0000000..0424a31 --- /dev/null +++ b/scenes/screens/lobby_screen/lobby_rpc.gd @@ -0,0 +1,29 @@ +extends Node + + +@warning_ignore_start("unused_signal") +signal server_host_started() +signal match_list_entry_added(match_entry: Dictionary) +signal match_list_entry_removed(match_id: String) +@warning_ignore_restore("unused_signal") + + +@rpc("authority", "call_local", "reliable") +func match_list_add_entry(match_entry: Dictionary) -> void: + push_warning("Received new match entry...") + + emit_signal("match_list_entry_added", match_entry) + +@rpc("authority", "call_local", "reliable") +func match_list_remove_entry(match_id: String) -> void: + push_warning("Received delete match entry...") + + emit_signal("match_list_entry_removed", match_id) + +@rpc("authority", "reliable") +func receive_match_list(_match_list: Array) -> void: + pass + +@rpc("authority", "reliable") +func match_list_activate_entry(_match_id: String) -> void: + pass diff --git a/scenes/screens/lobby_screen/lobby_rpc.gd.uid b/scenes/screens/lobby_screen/lobby_rpc.gd.uid new file mode 100644 index 0000000..fa8f290 --- /dev/null +++ b/scenes/screens/lobby_screen/lobby_rpc.gd.uid @@ -0,0 +1 @@ +uid://6sfwe1hwgf0g diff --git a/scenes/screens/lobby_screen/match_entry.gd b/scenes/screens/lobby_screen/match_entry.gd new file mode 100644 index 0000000..db47039 --- /dev/null +++ b/scenes/screens/lobby_screen/match_entry.gd @@ -0,0 +1,8 @@ +class_name MatchEntry extends HBoxContainer + + +@onready var name_lbl: Label = $name_lbl +@onready var type_lbl: Label = $type_lbl +@onready var view_bttn: Button = $view_bttn + +var match_id: String = "" diff --git a/scenes/screens/lobby_screen/match_entry.gd.uid b/scenes/screens/lobby_screen/match_entry.gd.uid new file mode 100644 index 0000000..c1ecc7c --- /dev/null +++ b/scenes/screens/lobby_screen/match_entry.gd.uid @@ -0,0 +1 @@ +uid://ck663xy8rqumo diff --git a/scenes/screens/lobby_screen/match_entry.tscn b/scenes/screens/lobby_screen/match_entry.tscn new file mode 100644 index 0000000..1a18a82 --- /dev/null +++ b/scenes/screens/lobby_screen/match_entry.tscn @@ -0,0 +1,23 @@ +[gd_scene load_steps=2 format=3 uid="uid://b5b18vkfe3caa"] + +[ext_resource type="Script" uid="uid://ck663xy8rqumo" path="res://scenes/screens/lobby_screen/match_entry.gd" id="1_kw53p"] + +[node name="match_entry" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_kw53p") + +[node name="name_lbl" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="type_lbl" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="view_bttn" type="Button" parent="."] +layout_mode = 2 +text = "Join" diff --git a/scenes/ui_controls/lobby_screen/lobby_ui.tscn b/scenes/ui_controls/lobby_screen/lobby_ui.tscn new file mode 100644 index 0000000..b06fc86 --- /dev/null +++ b/scenes/ui_controls/lobby_screen/lobby_ui.tscn @@ -0,0 +1,107 @@ +[gd_scene load_steps=3 format=3 uid="uid://rb6av5eg4u0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e03fk"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.22352941, 0.47058824, 0.23137255, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lqt48"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.8460855, 0.21924442, 0.24864262, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[node name="margin" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="body" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="left_body" type="VBoxContainer" parent="body"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="server_host_hbox" type="HBoxContainer" parent="body/left_body"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="host_address_input" type="LineEdit" parent="body/left_body/server_host_hbox"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "127.0.0.1" +placeholder_text = "Server Address: (Eg. 0.0.0.0)" +alignment = 1 +max_length = 12 +emoji_menu_enabled = false +clear_button_enabled = true +caret_blink = true + +[node name="host_port_range" type="SpinBox" parent="body/left_body/server_host_hbox"] +layout_mode = 2 +min_value = 1.0 +max_value = 65535.0 +page = 10.0 +value = 31315.0 +rounded = true +alignment = 1 + +[node name="host_start_bttn" type="Button" parent="body/left_body/server_host_hbox"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_e03fk") +text = "Start Host" + +[node name="host_stop_bttn" type="Button" parent="body/left_body/server_host_hbox"] +visible = false +layout_mode = 2 +mouse_default_cursor_shape = 2 +theme_override_styles/normal = SubResource("StyleBoxFlat_lqt48") +text = "Stop Host" + +[node name="search_vbox" type="VBoxContainer" parent="body/left_body"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="search_hbox" type="HBoxContainer" parent="body/left_body/search_vbox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="match_search_input" type="LineEdit" parent="body/left_body/search_vbox/search_hbox"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Search Match List... " +max_length = 12 +emoji_menu_enabled = false +clear_button_enabled = true +caret_blink = true + +[node name="match_search_bttn" type="Button" parent="body/left_body/search_vbox/search_hbox"] +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Search" + +[node name="scroll_container" type="ScrollContainer" parent="body/left_body/search_vbox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="match_list" type="VBoxContainer" parent="body/left_body/search_vbox/scroll_container"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/scripts/server_networking.gd b/scripts/server_networking.gd new file mode 100644 index 0000000..4f90165 --- /dev/null +++ b/scripts/server_networking.gd @@ -0,0 +1,51 @@ +extends Node + + +var peer: ENetMultiplayerPeer + + +func _ready() -> void: + pass + + +func start_server(address: String = "0.0.0.0", port: int = 8080) -> void: + if peer: return + + push_warning("Server Starting... Address: ", address, " Port: ", port) + + peer = ENetMultiplayerPeer.new() + peer.set_bind_ip(address) + + var state = peer.create_server(port) + match state: + OK: + push_warning("Server Started: ", state) + + multiplayer.multiplayer_peer = peer + multiplayer.peer_connected.connect(server_client_connected) + multiplayer.peer_disconnected.connect(server_client_disconnected) + + get_parent()._server_host_started() + ERR_ALREADY_IN_USE: + peer.close() + peer = null + start_server(address, port) + ERR_CANT_CREATE: + push_warning("Couldn't create server...") + +func close_connection() -> void: + if not peer: return + + push_warning("Server Closed...") + + multiplayer.peer_connected.disconnect(server_client_connected) + multiplayer.peer_disconnected.disconnect(server_client_disconnected) + + peer.close() + peer = null + +func server_client_connected(id: int) -> void: + Globals.lobby_data.server_client_connected(id) + +func server_client_disconnected(id: int) -> void: + Globals.lobby_data.server_client_disconnected(id) diff --git a/scripts/server_networking.gd.uid b/scripts/server_networking.gd.uid new file mode 100644 index 0000000..f8317d7 --- /dev/null +++ b/scripts/server_networking.gd.uid @@ -0,0 +1 @@ +uid://bvqtgf3vbsoj4 diff --git a/server.gd b/server.gd new file mode 100644 index 0000000..ffdb836 --- /dev/null +++ b/server.gd @@ -0,0 +1,50 @@ +extends Node + + +# +# +# Note that the lobby server is looking for a script local to it titled 'game.sh' +# +# Editor -> Debug > Customize Run Instance +# +# +# -- name_billiards server game_port_range 5000:5010 +# +# +# CLI +# +# ./lobby_server.sh -- name_billiards server game_port_range 5000:5010 +# +# + + +func _ready() -> void: + for arg in OS.get_cmdline_user_args(): + if "name_" in arg: + name_(arg) + + match arg: + "server": + server(arg) + "game_port_range": + game_port_range(arg) + _: + pass + + +func name_(arg: String) -> void: + var game_name = arg.split("_")[-1] + self.name = game_name + +func server(_arg: String) -> void: + Globals.game_state.set_game_scene( + "res://scenes/screens/lobby_screen/lobby.tscn" + ) + + $scene/lobby_screen.find_child("host_start_bttn", true, false).emit_signal("pressed") + +func game_port_range(_arg: String) -> void: + var port_range = OS.get_cmdline_user_args()[-1].split(":") + + InstanceLauncher.port_range_start = port_range[0] + InstanceLauncher.port_range_end = port_range[1] diff --git a/server.gd.uid b/server.gd.uid new file mode 100644 index 0000000..837984a --- /dev/null +++ b/server.gd.uid @@ -0,0 +1 @@ +uid://chu1htrspx5su diff --git a/server.tscn b/server.tscn new file mode 100644 index 0000000..222bec1 --- /dev/null +++ b/server.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=3 uid="uid://b0cqo3qo62y6n"] + +[ext_resource type="Script" uid="uid://chu1htrspx5su" path="res://server.gd" id="1_ob30c"] +[ext_resource type="PackedScene" uid="uid://dy51wny4x53f4" path="res://scenes/data_bridge/bridge.tscn" id="2_h8t3n"] + +[node name="server" type="Node"] +editor_description = "Note: The root node needs to match game's client root node in order for proper sync talk." +script = ExtResource("1_ob30c") + +[node name="bridge" parent="." instance=ExtResource("2_h8t3n")] + +[node name="scene" type="Node" parent="."]