Naposledy aktivní 1786000335

Revize 8a595c9164d91fc71cca1475ee5002e416087f2e

clipboard_engine.md Raw

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!")

clipboard_ui.gd Raw
1extends Node
2class_name ClipboardUIHelper
3
4# Builds the Network tab content into a provided container page
5static 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
138static 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
142static func _on_start_client_pressed(engine: Node) -> void:
143 engine.start_client(str(engine.server_ip), int(engine.port_number))
144
145static 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
150static 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 Raw
1extends Control
2
3var tab_container: TabContainer
4var support_tab_container: TabContainer
5
6# Reference to the instantiated clipboard/network engine
7var clipboard_engine: Node
8
9func _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
45func 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
81func _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
91func _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.
99func _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.
110func _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
129func game_interface(_page_container: Node) -> void: pass
130func ai_interface(_page_container: Node) -> void: pass
131func video_settings() -> void: pass
132func audio_settings() -> void: pass
133func support_interface(_page_container: Node) -> void: pass
134