ZZ revisou este gist . Ir para a revisão
1 file changed, 0 insertions, 0 deletions
clipboard_engine.md renomeado para clipboard_engine.gd
Arquivo renomeado sem alterações
ZZ revisou este gist . Ir para a revisão
3 files changed, 520 insertions
clipboard_engine.md(arquivo criado)
| @@ -0,0 +1,228 @@ | |||
| 1 | + | extends Node | |
| 2 | + | ||
| 3 | + | var synchronizer: MultiplayerSynchronizer # Declare variable to keep reference | |
| 4 | + | ||
| 5 | + | # Signal emitted when network status or clipboard text changes | |
| 6 | + | signal status_changed(text: String) | |
| 7 | + | ||
| 8 | + | var server_ip: String = "127.0.0.1" | |
| 9 | + | var port_number: int = 8443 | |
| 10 | + | var secret_key: String = "ChangeMe123!" | |
| 11 | + | var clipboard_text: String = "Waiting for connection..." | |
| 12 | + | ||
| 13 | + | var is_connected_to_network: bool = false | |
| 14 | + | var is_server_mode: bool = false | |
| 15 | + | ||
| 16 | + | # Reconnection and Rate-Limiting Tracker variables | |
| 17 | + | var is_reconnecting: bool = false | |
| 18 | + | var retry_timer: float = 0.0 | |
| 19 | + | const RETRY_INTERVAL: float = 10.0 | |
| 20 | + | var last_log_time: float = 0.0 | |
| 21 | + | const LOG_LIMIT_INTERVAL: float = 10.0 | |
| 22 | + | ||
| 23 | + | const CONFIG_PATH = "user://network_settings.cfg" | |
| 24 | + | ||
| 25 | + | func _ready() -> void: | |
| 26 | + | synchronizer = MultiplayerSynchronizer.new() | |
| 27 | + | #synchronizer.update_rate = 30.0 | |
| 28 | + | add_child(synchronizer) | |
| 29 | + | load_network_config() | |
| 30 | + | ||
| 31 | + | # Connect Godot's network lifecycle signals | |
| 32 | + | multiplayer.connected_to_server.connect(_on_connected_ok) | |
| 33 | + | multiplayer.connection_failed.connect(_on_connection_failed) | |
| 34 | + | multiplayer.server_disconnected.connect(_on_disconnected) | |
| 35 | + | multiplayer.peer_connected.connect(_on_peer_connected) | |
| 36 | + | multiplayer.peer_disconnected.connect(_on_peer_disconnected) | |
| 37 | + | ||
| 38 | + | update_status("DISCONNECTED | Secure Clip: Empty") | |
| 39 | + | ||
| 40 | + | func _process(delta: float) -> void: | |
| 41 | + | # 1. Manage Clipboard Synchronization | |
| 42 | + | var current_sys_clip = DisplayServer.clipboard_get() | |
| 43 | + | if current_sys_clip != clipboard_text: | |
| 44 | + | if multiplayer.is_server() or current_sys_clip != "": | |
| 45 | + | var encrypted_payload = encrypt_string(current_sys_clip) | |
| 46 | + | rpc("receive_secure_clipboard", encrypted_payload) | |
| 47 | + | ||
| 48 | + | # 2. Manage Automatic Client Reconnection Loop | |
| 49 | + | if is_reconnecting and not is_server_mode: | |
| 50 | + | retry_timer += delta | |
| 51 | + | if retry_timer >= RETRY_INTERVAL: | |
| 52 | + | retry_timer = 0.0 | |
| 53 | + | rate_limited_log("[Network Recovery] 10 seconds elapsed. Attempting background reconnect...") | |
| 54 | + | silent_client_reconnect() | |
| 55 | + | ||
| 56 | + | @rpc("any_peer", "call_local", "reliable") | |
| 57 | + | func receive_secure_clipboard(encrypted_data: PackedByteArray) -> void: | |
| 58 | + | var decrypted_text = decrypt_string(encrypted_data) | |
| 59 | + | clipboard_text = decrypted_text | |
| 60 | + | DisplayServer.clipboard_set(decrypted_text) | |
| 61 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 62 | + | ||
| 63 | + | func start_server(active_port: int) -> void: | |
| 64 | + | is_connected_to_network = false | |
| 65 | + | is_reconnecting = false | |
| 66 | + | is_server_mode = true | |
| 67 | + | ||
| 68 | + | var peer = ENetMultiplayerPeer.new() | |
| 69 | + | if peer.create_server(active_port) == OK: | |
| 70 | + | multiplayer.multiplayer_peer = peer | |
| 71 | + | save_network_config(server_ip, active_port, secret_key) | |
| 72 | + | print("[Network] Server active on port: ", active_port) | |
| 73 | + | else: | |
| 74 | + | rate_limited_log("[Network Error] Failed to start server.") | |
| 75 | + | ||
| 76 | + | func start_client(active_ip: String, active_port: int) -> void: | |
| 77 | + | is_connected_to_network = false | |
| 78 | + | is_server_mode = false | |
| 79 | + | ||
| 80 | + | var peer = ENetMultiplayerPeer.new() | |
| 81 | + | var error = peer.create_client(active_ip, active_port) | |
| 82 | + | if error == OK: | |
| 83 | + | multiplayer.multiplayer_peer = peer | |
| 84 | + | save_network_config(active_ip, active_port, secret_key) | |
| 85 | + | is_reconnecting = true | |
| 86 | + | else: | |
| 87 | + | rate_limited_log("[Network Error] Failed to create client. Error: %d" % error) | |
| 88 | + | ||
| 89 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 90 | + | ||
| 91 | + | # ========================================== | |
| 92 | + | # MULTIPLAYER SIGNAL & RECONNECT CALLBACKS | |
| 93 | + | # ========================================== | |
| 94 | + | ||
| 95 | + | func _on_connected_ok() -> void: | |
| 96 | + | is_connected_to_network = true | |
| 97 | + | is_reconnecting = false | |
| 98 | + | retry_timer = 0.0 | |
| 99 | + | print("[Network Status] Connection successful.") | |
| 100 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 101 | + | ||
| 102 | + | func _on_connection_failed() -> void: | |
| 103 | + | is_connected_to_network = false | |
| 104 | + | if not is_reconnecting: | |
| 105 | + | is_reconnecting = true | |
| 106 | + | retry_timer = 0.0 | |
| 107 | + | rate_limited_log("[Network Error] Initial handshake failed. Suppressing cascade logs; retrying in 10s.") | |
| 108 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 109 | + | ||
| 110 | + | func _on_disconnected() -> void: | |
| 111 | + | is_connected_to_network = false | |
| 112 | + | is_reconnecting = true | |
| 113 | + | retry_timer = 0.0 | |
| 114 | + | print("[Network Status] Disconnected from host. Text changed to YELLOW. Starting 10s retry cadence.") | |
| 115 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 116 | + | ||
| 117 | + | func _on_peer_connected(_id: int) -> void: | |
| 118 | + | if is_server_mode: | |
| 119 | + | is_connected_to_network = true | |
| 120 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 121 | + | ||
| 122 | + | func _on_peer_disconnected(_id: int) -> void: | |
| 123 | + | if is_server_mode and multiplayer.get_peers().size() == 0: | |
| 124 | + | is_connected_to_network = false | |
| 125 | + | update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text]) | |
| 126 | + | ||
| 127 | + | func silent_client_reconnect() -> void: | |
| 128 | + | multiplayer.multiplayer_peer = null | |
| 129 | + | ||
| 130 | + | var peer = ENetMultiplayerPeer.new() | |
| 131 | + | var error = peer.create_client(server_ip, port_number) | |
| 132 | + | if error == OK: | |
| 133 | + | multiplayer.multiplayer_peer = peer | |
| 134 | + | else: | |
| 135 | + | rate_limited_log("[Network Error] Peer creation failed silently: " + str(error)) | |
| 136 | + | ||
| 137 | + | # ========================================== | |
| 138 | + | # AES-256 ENCRYPTION & DECRYPTION LOGIC | |
| 139 | + | # ========================================== | |
| 140 | + | ||
| 141 | + | func get_hashed_key() -> PackedByteArray: | |
| 142 | + | var ctx = HashingContext.new() | |
| 143 | + | ctx.start(HashingContext.HASH_SHA256) | |
| 144 | + | ctx.update(secret_key.to_utf8_buffer()) | |
| 145 | + | return ctx.finish() | |
| 146 | + | ||
| 147 | + | func encrypt_string(plain_text: String) -> PackedByteArray: | |
| 148 | + | var aes = AESContext.new() | |
| 149 | + | var key = get_hashed_key() | |
| 150 | + | var data_bytes = plain_text.to_utf8_buffer() | |
| 151 | + | var padding_needed = 16 - (data_bytes.size() % 16) | |
| 152 | + | for i in range(padding_needed): | |
| 153 | + | data_bytes.append(padding_needed) | |
| 154 | + | ||
| 155 | + | aes.start(AESContext.MODE_ECB_ENCRYPT, key) | |
| 156 | + | var encrypted = aes.update(data_bytes) | |
| 157 | + | aes.finish() | |
| 158 | + | return encrypted | |
| 159 | + | ||
| 160 | + | func decrypt_string(encrypted_bytes: PackedByteArray) -> String: | |
| 161 | + | if encrypted_bytes.is_empty(): return "" | |
| 162 | + | var aes = AESContext.new() | |
| 163 | + | var key = get_hashed_key() | |
| 164 | + | ||
| 165 | + | aes.start(AESContext.MODE_ECB_DECRYPT, key) | |
| 166 | + | var decrypted = aes.update(encrypted_bytes) | |
| 167 | + | aes.finish() | |
| 168 | + | ||
| 169 | + | if decrypted.size() > 0: | |
| 170 | + | var padding_count = decrypted[decrypted.size() - 1] | |
| 171 | + | if padding_count > 0 and padding_count <= 16: | |
| 172 | + | decrypted = decrypted.slice(0, decrypted.size() - padding_count) | |
| 173 | + | return decrypted.get_string_from_utf8() | |
| 174 | + | ||
| 175 | + | # ========================================== | |
| 176 | + | # CONFIGURATION & UTILITIES | |
| 177 | + | # ========================================== | |
| 178 | + | ||
| 179 | + | func load_network_config() -> bool: | |
| 180 | + | var config = ConfigFile.new() | |
| 181 | + | var error = config.load(CONFIG_PATH) | |
| 182 | + | if error == OK: | |
| 183 | + | server_ip = config.get_value("Network", "server_ip", "127.0.0.1") | |
| 184 | + | port_number = config.get_value("Network", "port_number", 8443) | |
| 185 | + | secret_key = config.get_value("Network", "secret_key", "ChangeMe123!") | |
| 186 | + | return true | |
| 187 | + | else: | |
| 188 | + | save_network_config("127.0.0.1", 8443, "ChangeMe123!") | |
| 189 | + | return false | |
| 190 | + | ||
| 191 | + | func save_network_config(ip: String, port: int, key: String) -> void: | |
| 192 | + | var config = ConfigFile.new() | |
| 193 | + | config.set_value("Network", "server_ip", ip) | |
| 194 | + | config.set_value("Network", "port_number", port) | |
| 195 | + | config.set_value("Network", "secret_key", key) | |
| 196 | + | if config.save(CONFIG_PATH) == OK: | |
| 197 | + | server_ip = ip | |
| 198 | + | port_number = port | |
| 199 | + | secret_key = key | |
| 200 | + | ||
| 201 | + | func get_network_mode_text() -> String: | |
| 202 | + | var mode_text: String = "Mode: Server" if is_server_mode else "Mode: Client" | |
| 203 | + | if multiplayer.multiplayer_peer == null: | |
| 204 | + | mode_text = "Mode: Offline" | |
| 205 | + | elif is_reconnecting and not is_server_mode: | |
| 206 | + | mode_text = "Mode: Client (Reconnecting...)" | |
| 207 | + | return mode_text | |
| 208 | + | ||
| 209 | + | func update_status(text: String) -> void: | |
| 210 | + | status_changed.emit(text) | |
| 211 | + | ||
| 212 | + | func rate_limited_log(message: String) -> void: | |
| 213 | + | var current_time = Time.get_ticks_msec() / 1000.0 | |
| 214 | + | if current_time - last_log_time >= LOG_LIMIT_INTERVAL: | |
| 215 | + | print(message) | |
| 216 | + | last_log_time = current_time | |
| 217 | + | ||
| 218 | + | func generate_random_key(length: int) -> String: | |
| 219 | + | randomize() | |
| 220 | + | var chars: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | |
| 221 | + | var result: String = "" | |
| 222 | + | for i in range(length): | |
| 223 | + | result += chars[randi() % chars.length()] | |
| 224 | + | return result | |
| 225 | + | ||
| 226 | + | func copy_key_to_clipboard() -> void: | |
| 227 | + | DisplayServer.clipboard_set(secret_key) | |
| 228 | + | print("Copied to clipboard!") | |
clipboard_ui.gd(arquivo criado)
| @@ -0,0 +1,159 @@ | |||
| 1 | + | extends Node | |
| 2 | + | class_name ClipboardUIHelper | |
| 3 | + | ||
| 4 | + | # Builds the Network tab content into a provided container page | |
| 5 | + | static func build_network_ui(page_container: Node, engine: Node) -> void: | |
| 6 | + | ## 1. CLEANUP PREVIOUS RENDERS: Safely look for and wipe old structural layout node | |
| 7 | + | var old_wrapper = page_container.get_node_or_null("NetworkMarginWrapper") | |
| 8 | + | if old_wrapper: | |
| 9 | + | old_wrapper.queue_free() | |
| 10 | + | page_container.remove_child(old_wrapper) | |
| 11 | + | ||
| 12 | + | ## 2. MAIN PAGE LAYOUT: The root wrapper for this specific tab page | |
| 13 | + | var margin_wrapper = MarginContainer.new() | |
| 14 | + | margin_wrapper.name = "NetworkMarginWrapper" | |
| 15 | + | margin_wrapper.add_theme_constant_override("margin_left", 50) | |
| 16 | + | margin_wrapper.add_theme_constant_override("margin_top", 40) | |
| 17 | + | page_container.add_child(margin_wrapper) | |
| 18 | + | ||
| 19 | + | var main_stack = VBoxContainer.new() | |
| 20 | + | main_stack.add_theme_constant_override("separation", 15) | |
| 21 | + | margin_wrapper.add_child(main_stack) | |
| 22 | + | ||
| 23 | + | # ---------------------------------------------------- | |
| 24 | + | # ROW 1: Connection Actions (Start Server / Connect Client) | |
| 25 | + | # ---------------------------------------------------- | |
| 26 | + | var row_actions = HBoxContainer.new() | |
| 27 | + | row_actions.add_theme_constant_override("separation", 15) | |
| 28 | + | main_stack.add_child(row_actions) | |
| 29 | + | ||
| 30 | + | var btn_server = Button.new() | |
| 31 | + | btn_server.text = "Start Server" | |
| 32 | + | btn_server.pressed.connect(func(): _on_start_server_pressed(engine)) | |
| 33 | + | row_actions.add_child(btn_server) | |
| 34 | + | ||
| 35 | + | var btn_client = Button.new() | |
| 36 | + | btn_client.text = "Connect Client" | |
| 37 | + | btn_client.pressed.connect(func(): _on_start_client_pressed(engine)) | |
| 38 | + | row_actions.add_child(btn_client) | |
| 39 | + | ||
| 40 | + | # ---------------------------------------------------- | |
| 41 | + | # ROW 2: Address Info (IP Input & Port Input) | |
| 42 | + | # ---------------------------------------------------- | |
| 43 | + | var row_address = HBoxContainer.new() | |
| 44 | + | row_address.add_theme_constant_override("separation", 15) | |
| 45 | + | main_stack.add_child(row_address) | |
| 46 | + | ||
| 47 | + | var ip_input = LineEdit.new() | |
| 48 | + | ip_input.text = engine.server_ip | |
| 49 | + | ip_input.placeholder_text = "Target Server IP" | |
| 50 | + | ip_input.custom_minimum_size = Vector2(180, 35) | |
| 51 | + | row_address.add_child(ip_input) | |
| 52 | + | ||
| 53 | + | var port_input = LineEdit.new() | |
| 54 | + | port_input.text = str(engine.port_number) | |
| 55 | + | port_input.placeholder_text = "Port" | |
| 56 | + | port_input.custom_minimum_size = Vector2(100, 35) | |
| 57 | + | row_address.add_child(port_input) | |
| 58 | + | ||
| 59 | + | # ---------------------------------------------------- | |
| 60 | + | # ROW 3: Security & Utility (Passphrase, Generate, Copy) | |
| 61 | + | # ---------------------------------------------------- | |
| 62 | + | var row_security = HBoxContainer.new() | |
| 63 | + | row_security.add_theme_constant_override("separation", 15) | |
| 64 | + | main_stack.add_child(row_security) | |
| 65 | + | ||
| 66 | + | var key_input = LineEdit.new() | |
| 67 | + | key_input.text = engine.secret_key | |
| 68 | + | key_input.secret = true | |
| 69 | + | key_input.placeholder_text = "Shared Secret Key Passphrase" | |
| 70 | + | key_input.custom_minimum_size = Vector2(300, 35) | |
| 71 | + | row_security.add_child(key_input) | |
| 72 | + | ||
| 73 | + | var btn_generate = Button.new() | |
| 74 | + | btn_generate.text = "New Key" | |
| 75 | + | btn_generate.custom_minimum_size = Vector2(0, 35) | |
| 76 | + | btn_generate.pressed.connect(func(): _on_generate_key(engine, key_input)) | |
| 77 | + | row_security.add_child(btn_generate) | |
| 78 | + | ||
| 79 | + | var btn_copy = Button.new() | |
| 80 | + | btn_copy.text = "Copy Key" | |
| 81 | + | btn_copy.custom_minimum_size = Vector2(0, 35) | |
| 82 | + | btn_copy.pressed.connect(func(): engine.copy_key_to_clipboard()) | |
| 83 | + | row_security.add_child(btn_copy) | |
| 84 | + | ||
| 85 | + | # ---------------------------------------------------- | |
| 86 | + | # ROW 4: Manual Sync Input | |
| 87 | + | # ---------------------------------------------------- | |
| 88 | + | var row_input = HBoxContainer.new() | |
| 89 | + | row_address.add_theme_constant_override("separation", 15) | |
| 90 | + | main_stack.add_child(row_input) | |
| 91 | + | ||
| 92 | + | var line_edit = LineEdit.new() | |
| 93 | + | line_edit.custom_minimum_size = Vector2(400, 40) | |
| 94 | + | line_edit.placeholder_text = "Type and press Enter to securely sync..." | |
| 95 | + | # Wire the UI input directly to the engine's RPC method using encryption | |
| 96 | + | line_edit.text_submitted.connect(func(new_text): engine.receive_secure_clipboard(engine.encrypt_string(new_text))) | |
| 97 | + | row_input.add_child(line_edit) | |
| 98 | + | ||
| 99 | + | # ---------------------------------------------------- | |
| 100 | + | # ROW 5: Status Panel (Replaces draw_rect entirely) | |
| 101 | + | # ---------------------------------------------------- | |
| 102 | + | var row_status = HBoxContainer.new() | |
| 103 | + | row_status.add_theme_constant_override("separation", 15) | |
| 104 | + | main_stack.add_child(row_status) | |
| 105 | + | ||
| 106 | + | var status_card = PanelContainer.new() | |
| 107 | + | status_card.custom_minimum_size = Vector2(400, 80) | |
| 108 | + | status_card.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN | |
| 109 | + | ||
| 110 | + | var card_padding = MarginContainer.new() | |
| 111 | + | card_padding.add_theme_constant_override("margin_left", 30) | |
| 112 | + | card_padding.add_theme_constant_override("margin_right", 30) | |
| 113 | + | card_padding.add_theme_constant_override("margin_top", 15) | |
| 114 | + | card_padding.add_theme_constant_override("margin_bottom", 15) | |
| 115 | + | ||
| 116 | + | status_card.add_child(card_padding) | |
| 117 | + | ||
| 118 | + | var status_label = RichTextLabel.new() | |
| 119 | + | status_label.text = "Loading..." | |
| 120 | + | status_label.selection_enabled = true | |
| 121 | + | status_label.add_theme_font_size_override("font_size", 18) | |
| 122 | + | status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL | |
| 123 | + | card_padding.add_child(status_label) | |
| 124 | + | ||
| 125 | + | # StyleBoxFlat to mimic your old draw_rect background | |
| 126 | + | var custom_style = StyleBoxFlat.new() | |
| 127 | + | custom_style.bg_color = Color(0.13, 0.13, 0.13, 1.0) | |
| 128 | + | custom_style.set_corner_radius_all(4) | |
| 129 | + | status_card.add_theme_stylebox_override("panel", custom_style) | |
| 130 | + | ||
| 131 | + | main_stack.add_child(status_card) | |
| 132 | + | ||
| 133 | + | # Listen for engine status updates and wire them to the label | |
| 134 | + | engine.status_changed.connect(func(new_status_text): _update_status_label(status_label, new_status_text)) | |
| 135 | + | ||
| 136 | + | # -- Static Helper Functions for UI Wiring -- | |
| 137 | + | ||
| 138 | + | static func _on_start_server_pressed(engine: Node) -> void: | |
| 139 | + | # Reads current properties. In a real app you might pass inputs as arguments instead. | |
| 140 | + | engine.start_server(int(engine.port_number)) | |
| 141 | + | ||
| 142 | + | static func _on_start_client_pressed(engine: Node) -> void: | |
| 143 | + | engine.start_client(str(engine.server_ip), int(engine.port_number)) | |
| 144 | + | ||
| 145 | + | static func _on_generate_key(engine: Node, key_input: LineEdit) -> void: | |
| 146 | + | var new_key = engine.generate_random_key(16) | |
| 147 | + | engine.secret_key = new_key | |
| 148 | + | key_input.text = new_key | |
| 149 | + | ||
| 150 | + | static func _update_status_label(label: RichTextLabel, text: String) -> void: | |
| 151 | + | label.text = text | |
| 152 | + | # Calculate dynamic color state based on connection status | |
| 153 | + | var target_color: Color = Color.GREEN | |
| 154 | + | if "Disconnected" in text or "Offline" in text: | |
| 155 | + | target_color = Color.YELLOW | |
| 156 | + | elif "Reconnecting" in text: | |
| 157 | + | target_color = Color.ORANGE | |
| 158 | + | ||
| 159 | + | label.add_theme_color_override("default_color", target_color) | |
main.gd(arquivo criado)
| @@ -0,0 +1,133 @@ | |||
| 1 | + | extends Control | |
| 2 | + | ||
| 3 | + | var tab_container: TabContainer | |
| 4 | + | var support_tab_container: TabContainer | |
| 5 | + | ||
| 6 | + | # Reference to the instantiated clipboard/network engine | |
| 7 | + | var clipboard_engine: Node | |
| 8 | + | ||
| 9 | + | func _ready() -> void: | |
| 10 | + | # 1. Initialize the full-screen TabContainer | |
| 11 | + | tab_container = TabContainer.new() | |
| 12 | + | add_child(tab_container) | |
| 13 | + | tab_container.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) | |
| 14 | + | ||
| 15 | + | create_standard_tab("Game") | |
| 16 | + | create_standard_tab("Editor") | |
| 17 | + | create_standard_tab("Network") | |
| 18 | + | create_standard_tab("AI") | |
| 19 | + | create_standard_tab("Video") | |
| 20 | + | create_standard_tab("Audio") | |
| 21 | + | create_standard_tab("Support") | |
| 22 | + | ||
| 23 | + | # Connect the tab navigation signal natively | |
| 24 | + | tab_container.tab_changed.connect(_on_tab_changed) | |
| 25 | + | ||
| 26 | + | # Explicitly set the starting tab to ensure a clean state on launch | |
| 27 | + | tab_container.current_tab = 0 | |
| 28 | + | ||
| 29 | + | # Instantiate the clipboard/network engine as a child of the root Control. | |
| 30 | + | # This allows it to access the scene tree and multiplayer signals globally. | |
| 31 | + | var engine_script = preload("res://clipboard_engine.gd") | |
| 32 | + | clipboard_engine = engine_script.new() | |
| 33 | + | add_child(clipboard_engine) | |
| 34 | + | ||
| 35 | + | await get_tree().process_frame # Ensure child _ready runs | |
| 36 | + | ||
| 37 | + | # Bootup logic | |
| 38 | + | var has_config = clipboard_engine.load_network_config() | |
| 39 | + | if has_config: | |
| 40 | + | print("[Autoconnect] Secure config file loaded. Instantiating auto-client...") | |
| 41 | + | _switch_to_tab_by_name("Network") | |
| 42 | + | else: | |
| 43 | + | print("[Autoconnect] Initial default config file created.") | |
| 44 | + | ||
| 45 | + | func create_standard_tab(tab_name: String) -> void: | |
| 46 | + | # MarginContainer isolates your tab content from the very edge of the screen | |
| 47 | + | var tab_page := MarginContainer.new() | |
| 48 | + | tab_page.name = tab_name | |
| 49 | + | ||
| 50 | + | # Force the tab page to fill all space provided by the TabContainer | |
| 51 | + | tab_page.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 52 | + | tab_page.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 53 | + | ||
| 54 | + | # Apply standard UI padding (16px - 24px is standard for modern apps) | |
| 55 | + | var padding := 20 | |
| 56 | + | tab_page.add_theme_constant_override("margin_top", padding) | |
| 57 | + | tab_page.add_theme_constant_override("margin_left", padding) | |
| 58 | + | tab_page.add_theme_constant_override("margin_right", padding) | |
| 59 | + | tab_page.add_theme_constant_override("margin_bottom", padding) | |
| 60 | + | ||
| 61 | + | tab_container.add_child(tab_page) | |
| 62 | + | ||
| 63 | + | # Add a vertical layout to separate main content from the bottom bar | |
| 64 | + | var layout_vbox := VBoxContainer.new() | |
| 65 | + | layout_vbox.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 66 | + | layout_vbox.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 67 | + | tab_page.add_child(layout_vbox) | |
| 68 | + | ||
| 69 | + | # Placeholder for main content body (Expands to fill top area) | |
| 70 | + | var main_content := Control.new() | |
| 71 | + | main_content.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 72 | + | main_content.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 73 | + | layout_vbox.add_child(main_content) | |
| 74 | + | ||
| 75 | + | # Standard locked bottom node (Only fills width, sits neatly at bottom) | |
| 76 | + | var bottom_bar := PanelContainer.new() | |
| 77 | + | bottom_bar.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL | |
| 78 | + | bottom_bar.size_flags_vertical = Control.SIZE_FILL # Crucial: No expansion flag | |
| 79 | + | layout_vbox.add_child(bottom_bar) | |
| 80 | + | ||
| 81 | + | func _switch_to_tab_by_name(target_name: String) -> void: | |
| 82 | + | # Loop through tabs to match the node name | |
| 83 | + | for i in range(tab_container.get_tab_count()): | |
| 84 | + | var child_node = tab_container.get_child(i) | |
| 85 | + | if child_node.name == target_name: | |
| 86 | + | tab_container.current_tab = i | |
| 87 | + | return | |
| 88 | + | ||
| 89 | + | push_warning("Could not find a tab named: " + target_name) | |
| 90 | + | ||
| 91 | + | func _unhandled_input(event: InputEvent) -> void: | |
| 92 | + | # Handle input events efficiently without polling in _process | |
| 93 | + | if event.is_action_pressed("next_tab"): | |
| 94 | + | _cycle_tabs(1) | |
| 95 | + | elif event.is_action_pressed("previous_tab"): | |
| 96 | + | _cycle_tabs(-1) | |
| 97 | + | ||
| 98 | + | ## Safely increments or decrements the current tab with out-of-bounds protection. | |
| 99 | + | func _cycle_tabs(direction: int) -> void: | |
| 100 | + | var total_tabs: int = tab_container.get_tab_count() | |
| 101 | + | ||
| 102 | + | if total_tabs <= 1: | |
| 103 | + | return | |
| 104 | + | ||
| 105 | + | # posmod() prevents negative numbers and wraps index perfectly | |
| 106 | + | var target_index: int = posmod(tab_container.current_tab + direction, total_tabs) | |
| 107 | + | tab_container.current_tab = target_index | |
| 108 | + | ||
| 109 | + | ## Callback function triggered every time the tab changes. | |
| 110 | + | func _on_tab_changed(tab_index: int) -> void: | |
| 111 | + | var current_page: Node = tab_container.get_child(tab_index) | |
| 112 | + | ||
| 113 | + | match current_page.name: | |
| 114 | + | "Game": | |
| 115 | + | game_interface(current_page) | |
| 116 | + | "Network": | |
| 117 | + | # Pass the container and the engine instance to the UI builder | |
| 118 | + | load("res://clipboard_ui.gd").build_network_ui(current_page, clipboard_engine) | |
| 119 | + | "AI": | |
| 120 | + | ai_interface(current_page) | |
| 121 | + | "Video": | |
| 122 | + | video_settings() | |
| 123 | + | "Audio": | |
| 124 | + | audio_settings() | |
| 125 | + | "Support": | |
| 126 | + | support_interface(current_page) | |
| 127 | + | ||
| 128 | + | # Mock interfaces for other tabs | |
| 129 | + | func game_interface(_page_container: Node) -> void: pass | |
| 130 | + | func ai_interface(_page_container: Node) -> void: pass | |
| 131 | + | func video_settings() -> void: pass | |
| 132 | + | func audio_settings() -> void: pass | |
| 133 | + | func support_interface(_page_container: Node) -> void: pass | |