clipboard_engine.gd
· 7.8 KiB · GDScript3
Ham
extends Node
var synchronizer: MultiplayerSynchronizer # Declare variable to keep reference
# Signal emitted when network status or clipboard text changes
signal status_changed(text: String)
var server_ip: String = "127.0.0.1"
var port_number: int = 8443
var secret_key: String = "ChangeMe123!"
var clipboard_text: String = "Waiting for connection..."
var is_connected_to_network: bool = false
var is_server_mode: bool = false
# Reconnection and Rate-Limiting Tracker variables
var is_reconnecting: bool = false
var retry_timer: float = 0.0
const RETRY_INTERVAL: float = 10.0
var last_log_time: float = 0.0
const LOG_LIMIT_INTERVAL: float = 10.0
const CONFIG_PATH = "user://network_settings.cfg"
func _ready() -> void:
synchronizer = MultiplayerSynchronizer.new()
#synchronizer.update_rate = 30.0
add_child(synchronizer)
load_network_config()
# Connect Godot's network lifecycle signals
multiplayer.connected_to_server.connect(_on_connected_ok)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_disconnected)
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
update_status("DISCONNECTED | Secure Clip: Empty")
func _process(delta: float) -> void:
# 1. Manage Clipboard Synchronization
var current_sys_clip = DisplayServer.clipboard_get()
if current_sys_clip != clipboard_text:
if multiplayer.is_server() or current_sys_clip != "":
var encrypted_payload = encrypt_string(current_sys_clip)
rpc("receive_secure_clipboard", encrypted_payload)
# 2. Manage Automatic Client Reconnection Loop
if is_reconnecting and not is_server_mode:
retry_timer += delta
if retry_timer >= RETRY_INTERVAL:
retry_timer = 0.0
rate_limited_log("[Network Recovery] 10 seconds elapsed. Attempting background reconnect...")
silent_client_reconnect()
@rpc("any_peer", "call_local", "reliable")
func receive_secure_clipboard(encrypted_data: PackedByteArray) -> void:
var decrypted_text = decrypt_string(encrypted_data)
clipboard_text = decrypted_text
DisplayServer.clipboard_set(decrypted_text)
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func start_server(active_port: int) -> void:
is_connected_to_network = false
is_reconnecting = false
is_server_mode = true
var peer = ENetMultiplayerPeer.new()
if peer.create_server(active_port) == OK:
multiplayer.multiplayer_peer = peer
save_network_config(server_ip, active_port, secret_key)
print("[Network] Server active on port: ", active_port)
else:
rate_limited_log("[Network Error] Failed to start server.")
func start_client(active_ip: String, active_port: int) -> void:
is_connected_to_network = false
is_server_mode = false
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client(active_ip, active_port)
if error == OK:
multiplayer.multiplayer_peer = peer
save_network_config(active_ip, active_port, secret_key)
is_reconnecting = true
else:
rate_limited_log("[Network Error] Failed to create client. Error: %d" % error)
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
# ==========================================
# MULTIPLAYER SIGNAL & RECONNECT CALLBACKS
# ==========================================
func _on_connected_ok() -> void:
is_connected_to_network = true
is_reconnecting = false
retry_timer = 0.0
print("[Network Status] Connection successful.")
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func _on_connection_failed() -> void:
is_connected_to_network = false
if not is_reconnecting:
is_reconnecting = true
retry_timer = 0.0
rate_limited_log("[Network Error] Initial handshake failed. Suppressing cascade logs; retrying in 10s.")
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func _on_disconnected() -> void:
is_connected_to_network = false
is_reconnecting = true
retry_timer = 0.0
print("[Network Status] Disconnected from host. Text changed to YELLOW. Starting 10s retry cadence.")
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func _on_peer_connected(_id: int) -> void:
if is_server_mode:
is_connected_to_network = true
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func _on_peer_disconnected(_id: int) -> void:
if is_server_mode and multiplayer.get_peers().size() == 0:
is_connected_to_network = false
update_status("%s | Secure Clip: %s" % [get_network_mode_text(), clipboard_text])
func silent_client_reconnect() -> void:
multiplayer.multiplayer_peer = null
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client(server_ip, port_number)
if error == OK:
multiplayer.multiplayer_peer = peer
else:
rate_limited_log("[Network Error] Peer creation failed silently: " + str(error))
# ==========================================
# AES-256 ENCRYPTION & DECRYPTION LOGIC
# ==========================================
func get_hashed_key() -> PackedByteArray:
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(secret_key.to_utf8_buffer())
return ctx.finish()
func encrypt_string(plain_text: String) -> PackedByteArray:
var aes = AESContext.new()
var key = get_hashed_key()
var data_bytes = plain_text.to_utf8_buffer()
var padding_needed = 16 - (data_bytes.size() % 16)
for i in range(padding_needed):
data_bytes.append(padding_needed)
aes.start(AESContext.MODE_ECB_ENCRYPT, key)
var encrypted = aes.update(data_bytes)
aes.finish()
return encrypted
func decrypt_string(encrypted_bytes: PackedByteArray) -> String:
if encrypted_bytes.is_empty(): return ""
var aes = AESContext.new()
var key = get_hashed_key()
aes.start(AESContext.MODE_ECB_DECRYPT, key)
var decrypted = aes.update(encrypted_bytes)
aes.finish()
if decrypted.size() > 0:
var padding_count = decrypted[decrypted.size() - 1]
if padding_count > 0 and padding_count <= 16:
decrypted = decrypted.slice(0, decrypted.size() - padding_count)
return decrypted.get_string_from_utf8()
# ==========================================
# CONFIGURATION & UTILITIES
# ==========================================
func load_network_config() -> bool:
var config = ConfigFile.new()
var error = config.load(CONFIG_PATH)
if error == OK:
server_ip = config.get_value("Network", "server_ip", "127.0.0.1")
port_number = config.get_value("Network", "port_number", 8443)
secret_key = config.get_value("Network", "secret_key", "ChangeMe123!")
return true
else:
save_network_config("127.0.0.1", 8443, "ChangeMe123!")
return false
func save_network_config(ip: String, port: int, key: String) -> void:
var config = ConfigFile.new()
config.set_value("Network", "server_ip", ip)
config.set_value("Network", "port_number", port)
config.set_value("Network", "secret_key", key)
if config.save(CONFIG_PATH) == OK:
server_ip = ip
port_number = port
secret_key = key
func get_network_mode_text() -> String:
var mode_text: String = "Mode: Server" if is_server_mode else "Mode: Client"
if multiplayer.multiplayer_peer == null:
mode_text = "Mode: Offline"
elif is_reconnecting and not is_server_mode:
mode_text = "Mode: Client (Reconnecting...)"
return mode_text
func update_status(text: String) -> void:
status_changed.emit(text)
func rate_limited_log(message: String) -> void:
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - last_log_time >= LOG_LIMIT_INTERVAL:
print(message)
last_log_time = current_time
func generate_random_key(length: int) -> String:
randomize()
var chars: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var result: String = ""
for i in range(length):
result += chars[randi() % chars.length()]
return result
func copy_key_to_clipboard() -> void:
DisplayServer.clipboard_set(secret_key)
print("Copied to clipboard!")
| 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!") |
| 229 |
clipboard_ui.gd
· 6.2 KiB · GDScript3
Ham
extends Node
class_name ClipboardUIHelper
# Builds the Network tab content into a provided container page
static func build_network_ui(page_container: Node, engine: Node) -> void:
## 1. CLEANUP PREVIOUS RENDERS: Safely look for and wipe old structural layout node
var old_wrapper = page_container.get_node_or_null("NetworkMarginWrapper")
if old_wrapper:
old_wrapper.queue_free()
page_container.remove_child(old_wrapper)
## 2. MAIN PAGE LAYOUT: The root wrapper for this specific tab page
var margin_wrapper = MarginContainer.new()
margin_wrapper.name = "NetworkMarginWrapper"
margin_wrapper.add_theme_constant_override("margin_left", 50)
margin_wrapper.add_theme_constant_override("margin_top", 40)
page_container.add_child(margin_wrapper)
var main_stack = VBoxContainer.new()
main_stack.add_theme_constant_override("separation", 15)
margin_wrapper.add_child(main_stack)
# ----------------------------------------------------
# ROW 1: Connection Actions (Start Server / Connect Client)
# ----------------------------------------------------
var row_actions = HBoxContainer.new()
row_actions.add_theme_constant_override("separation", 15)
main_stack.add_child(row_actions)
var btn_server = Button.new()
btn_server.text = "Start Server"
btn_server.pressed.connect(func(): _on_start_server_pressed(engine))
row_actions.add_child(btn_server)
var btn_client = Button.new()
btn_client.text = "Connect Client"
btn_client.pressed.connect(func(): _on_start_client_pressed(engine))
row_actions.add_child(btn_client)
# ----------------------------------------------------
# ROW 2: Address Info (IP Input & Port Input)
# ----------------------------------------------------
var row_address = HBoxContainer.new()
row_address.add_theme_constant_override("separation", 15)
main_stack.add_child(row_address)
var ip_input = LineEdit.new()
ip_input.text = engine.server_ip
ip_input.placeholder_text = "Target Server IP"
ip_input.custom_minimum_size = Vector2(180, 35)
row_address.add_child(ip_input)
var port_input = LineEdit.new()
port_input.text = str(engine.port_number)
port_input.placeholder_text = "Port"
port_input.custom_minimum_size = Vector2(100, 35)
row_address.add_child(port_input)
# ----------------------------------------------------
# ROW 3: Security & Utility (Passphrase, Generate, Copy)
# ----------------------------------------------------
var row_security = HBoxContainer.new()
row_security.add_theme_constant_override("separation", 15)
main_stack.add_child(row_security)
var key_input = LineEdit.new()
key_input.text = engine.secret_key
key_input.secret = true
key_input.placeholder_text = "Shared Secret Key Passphrase"
key_input.custom_minimum_size = Vector2(300, 35)
row_security.add_child(key_input)
var btn_generate = Button.new()
btn_generate.text = "New Key"
btn_generate.custom_minimum_size = Vector2(0, 35)
btn_generate.pressed.connect(func(): _on_generate_key(engine, key_input))
row_security.add_child(btn_generate)
var btn_copy = Button.new()
btn_copy.text = "Copy Key"
btn_copy.custom_minimum_size = Vector2(0, 35)
btn_copy.pressed.connect(func(): engine.copy_key_to_clipboard())
row_security.add_child(btn_copy)
# ----------------------------------------------------
# ROW 4: Manual Sync Input
# ----------------------------------------------------
var row_input = HBoxContainer.new()
row_address.add_theme_constant_override("separation", 15)
main_stack.add_child(row_input)
var line_edit = LineEdit.new()
line_edit.custom_minimum_size = Vector2(400, 40)
line_edit.placeholder_text = "Type and press Enter to securely sync..."
# Wire the UI input directly to the engine's RPC method using encryption
line_edit.text_submitted.connect(func(new_text): engine.receive_secure_clipboard(engine.encrypt_string(new_text)))
row_input.add_child(line_edit)
# ----------------------------------------------------
# ROW 5: Status Panel (Replaces draw_rect entirely)
# ----------------------------------------------------
var row_status = HBoxContainer.new()
row_status.add_theme_constant_override("separation", 15)
main_stack.add_child(row_status)
var status_card = PanelContainer.new()
status_card.custom_minimum_size = Vector2(400, 80)
status_card.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
var card_padding = MarginContainer.new()
card_padding.add_theme_constant_override("margin_left", 30)
card_padding.add_theme_constant_override("margin_right", 30)
card_padding.add_theme_constant_override("margin_top", 15)
card_padding.add_theme_constant_override("margin_bottom", 15)
status_card.add_child(card_padding)
var status_label = RichTextLabel.new()
status_label.text = "Loading..."
status_label.selection_enabled = true
status_label.add_theme_font_size_override("font_size", 18)
status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card_padding.add_child(status_label)
# StyleBoxFlat to mimic your old draw_rect background
var custom_style = StyleBoxFlat.new()
custom_style.bg_color = Color(0.13, 0.13, 0.13, 1.0)
custom_style.set_corner_radius_all(4)
status_card.add_theme_stylebox_override("panel", custom_style)
main_stack.add_child(status_card)
# Listen for engine status updates and wire them to the label
engine.status_changed.connect(func(new_status_text): _update_status_label(status_label, new_status_text))
# -- Static Helper Functions for UI Wiring --
static func _on_start_server_pressed(engine: Node) -> void:
# Reads current properties. In a real app you might pass inputs as arguments instead.
engine.start_server(int(engine.port_number))
static func _on_start_client_pressed(engine: Node) -> void:
engine.start_client(str(engine.server_ip), int(engine.port_number))
static func _on_generate_key(engine: Node, key_input: LineEdit) -> void:
var new_key = engine.generate_random_key(16)
engine.secret_key = new_key
key_input.text = new_key
static func _update_status_label(label: RichTextLabel, text: String) -> void:
label.text = text
# Calculate dynamic color state based on connection status
var target_color: Color = Color.GREEN
if "Disconnected" in text or "Offline" in text:
target_color = Color.YELLOW
elif "Reconnecting" in text:
target_color = Color.ORANGE
label.add_theme_color_override("default_color", target_color)
| 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) |
| 160 |
main.gd
· 4.8 KiB · GDScript3
Ham
extends Control
var tab_container: TabContainer
var support_tab_container: TabContainer
# Reference to the instantiated clipboard/network engine
var clipboard_engine: Node
func _ready() -> void:
# 1. Initialize the full-screen TabContainer
tab_container = TabContainer.new()
add_child(tab_container)
tab_container.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
create_standard_tab("Game")
create_standard_tab("Editor")
create_standard_tab("Network")
create_standard_tab("AI")
create_standard_tab("Video")
create_standard_tab("Audio")
create_standard_tab("Support")
# Connect the tab navigation signal natively
tab_container.tab_changed.connect(_on_tab_changed)
# Explicitly set the starting tab to ensure a clean state on launch
tab_container.current_tab = 0
# Instantiate the clipboard/network engine as a child of the root Control.
# This allows it to access the scene tree and multiplayer signals globally.
var engine_script = preload("res://clipboard_engine.gd")
clipboard_engine = engine_script.new()
add_child(clipboard_engine)
await get_tree().process_frame # Ensure child _ready runs
# Bootup logic
var has_config = clipboard_engine.load_network_config()
if has_config:
print("[Autoconnect] Secure config file loaded. Instantiating auto-client...")
_switch_to_tab_by_name("Network")
else:
print("[Autoconnect] Initial default config file created.")
func create_standard_tab(tab_name: String) -> void:
# MarginContainer isolates your tab content from the very edge of the screen
var tab_page := MarginContainer.new()
tab_page.name = tab_name
# Force the tab page to fill all space provided by the TabContainer
tab_page.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL
tab_page.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL
# Apply standard UI padding (16px - 24px is standard for modern apps)
var padding := 20
tab_page.add_theme_constant_override("margin_top", padding)
tab_page.add_theme_constant_override("margin_left", padding)
tab_page.add_theme_constant_override("margin_right", padding)
tab_page.add_theme_constant_override("margin_bottom", padding)
tab_container.add_child(tab_page)
# Add a vertical layout to separate main content from the bottom bar
var layout_vbox := VBoxContainer.new()
layout_vbox.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL
layout_vbox.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL
tab_page.add_child(layout_vbox)
# Placeholder for main content body (Expands to fill top area)
var main_content := Control.new()
main_content.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL
main_content.size_flags_vertical = Control.SIZE_EXPAND | Control.SIZE_FILL
layout_vbox.add_child(main_content)
# Standard locked bottom node (Only fills width, sits neatly at bottom)
var bottom_bar := PanelContainer.new()
bottom_bar.size_flags_horizontal = Control.SIZE_EXPAND | Control.SIZE_FILL
bottom_bar.size_flags_vertical = Control.SIZE_FILL # Crucial: No expansion flag
layout_vbox.add_child(bottom_bar)
func _switch_to_tab_by_name(target_name: String) -> void:
# Loop through tabs to match the node name
for i in range(tab_container.get_tab_count()):
var child_node = tab_container.get_child(i)
if child_node.name == target_name:
tab_container.current_tab = i
return
push_warning("Could not find a tab named: " + target_name)
func _unhandled_input(event: InputEvent) -> void:
# Handle input events efficiently without polling in _process
if event.is_action_pressed("next_tab"):
_cycle_tabs(1)
elif event.is_action_pressed("previous_tab"):
_cycle_tabs(-1)
## Safely increments or decrements the current tab with out-of-bounds protection.
func _cycle_tabs(direction: int) -> void:
var total_tabs: int = tab_container.get_tab_count()
if total_tabs <= 1:
return
# posmod() prevents negative numbers and wraps index perfectly
var target_index: int = posmod(tab_container.current_tab + direction, total_tabs)
tab_container.current_tab = target_index
## Callback function triggered every time the tab changes.
func _on_tab_changed(tab_index: int) -> void:
var current_page: Node = tab_container.get_child(tab_index)
match current_page.name:
"Game":
game_interface(current_page)
"Network":
# Pass the container and the engine instance to the UI builder
load("res://clipboard_ui.gd").build_network_ui(current_page, clipboard_engine)
"AI":
ai_interface(current_page)
"Video":
video_settings()
"Audio":
audio_settings()
"Support":
support_interface(current_page)
# Mock interfaces for other tabs
func game_interface(_page_container: Node) -> void: pass
func ai_interface(_page_container: Node) -> void: pass
func video_settings() -> void: pass
func audio_settings() -> void: pass
func support_interface(_page_container: Node) -> void: pass
| 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 |
| 134 |