Godot-System-Tray.gd
| @@ -10,7 +10,7 @@ func setup_tray() -> void: | |||
| 10 | 10 | # 1. Initialize the StatusIndicator | |
| 11 | 11 | tray_icon = StatusIndicator.new() | |
| 12 | 12 | tray_icon.icon = load("res://ship-icon-256.png") # Path to your 16x16 or 32x32 icon | |
| 13 | - | tray_icon.tooltip = "My Godot App" | |
| 13 | + | tray_icon.tooltip = "EnvoyOS" | |
| 14 | 14 | add_child(tray_icon) | |
| 15 | 15 | ||
| 16 | 16 | # 2. Bind the click event to restore the window | |
Godot-Clipboard-Sync.gd
| @@ -26,7 +26,7 @@ var port_input: LineEdit | |||
| 26 | 26 | var key_input: LineEdit | |
| 27 | 27 | ||
| 28 | 28 | func _ready() -> void: | |
| 29 | - | line_edit.position = Vector2(50, 210) | |
| 29 | + | line_edit.position = Vector2(50, 195) | |
| 30 | 30 | line_edit.custom_minimum_size = Vector2(400, 40) | |
| 31 | 31 | line_edit.placeholder_text = "Type and press Enter to securely sync..." | |
| 32 | 32 | line_edit.text_submitted.connect(_on_text_submitted) | |
| @@ -116,12 +116,12 @@ func _on_disconnected() -> void: | |||
| 116 | 116 | print("[Network Status] Disconnected from host. Text changed to YELLOW. Starting 10s retry cadence.") | |
| 117 | 117 | queue_redraw() | |
| 118 | 118 | ||
| 119 | - | func _on_peer_connected(id: int) -> void: | |
| 120 | - | if is_server_mode:_ | |
| 119 | + | func _on_peer_connected(_id: int) -> void: | |
| 120 | + | if is_server_mode: | |
| 121 | 121 | is_connected_to_network = true | |
| 122 | 122 | queue_redraw() | |
| 123 | 123 | ||
| 124 | - | func _on_peer_disconnected(id: int) -> void: | |
| 124 | + | func _on_peer_disconnected(_id: int) -> void: | |
| 125 | 125 | if is_server_mode and multiplayer.get_peers().size() == 0: | |
| 126 | 126 | is_connected_to_network = false | |
| 127 | 127 | queue_redraw() | |
Godot-Clipboard-Sync.gd(файл создан)
| @@ -0,0 +1,278 @@ | |||
| 1 | + | extends Node2D | |
| 2 | + | ||
| 3 | + | @onready var line_edit: LineEdit = $LineEdit | |
| 4 | + | @onready var synchronizer: MultiplayerSynchronizer = $MultiplayerSynchronizer | |
| 5 | + | ||
| 6 | + | var server_ip: String = "127.0.0.1" | |
| 7 | + | var port_number: int = 8443 | |
| 8 | + | var secret_key: String = "ChangeMe123!" | |
| 9 | + | ||
| 10 | + | @export var clipboard_text: String = "Waiting for connection..." | |
| 11 | + | ||
| 12 | + | var is_connected_to_network: bool = false | |
| 13 | + | var is_server_mode: bool = false | |
| 14 | + | ||
| 15 | + | # Reconnection and Rate-Limiting Tracker variables | |
| 16 | + | var is_reconnecting: bool = false | |
| 17 | + | var retry_timer: float = 0.0 | |
| 18 | + | const RETRY_INTERVAL: float = 10.0 # Reconnect attempt frequency in seconds | |
| 19 | + | var last_log_time: float = 0.0 | |
| 20 | + | const LOG_LIMIT_INTERVAL: float = 10.0 # Suppresses error logs within this timeframe | |
| 21 | + | ||
| 22 | + | const CONFIG_PATH = "user://network_settings.cfg" | |
| 23 | + | ||
| 24 | + | var ip_input: LineEdit | |
| 25 | + | var port_input: LineEdit | |
| 26 | + | var key_input: LineEdit | |
| 27 | + | ||
| 28 | + | func _ready() -> void: | |
| 29 | + | line_edit.position = Vector2(50, 210) | |
| 30 | + | line_edit.custom_minimum_size = Vector2(400, 40) | |
| 31 | + | line_edit.placeholder_text = "Type and press Enter to securely sync..." | |
| 32 | + | line_edit.text_submitted.connect(_on_text_submitted) | |
| 33 | + | ||
| 34 | + | # Connect Godot's network lifecycle signals | |
| 35 | + | multiplayer.connected_to_server.connect(_on_connected_ok) | |
| 36 | + | multiplayer.connection_failed.connect(_on_connection_failed) | |
| 37 | + | multiplayer.server_disconnected.connect(_on_disconnected) | |
| 38 | + | multiplayer.peer_connected.connect(_on_peer_connected) | |
| 39 | + | multiplayer.peer_disconnected.connect(_on_peer_disconnected) | |
| 40 | + | ||
| 41 | + | create_network_interface() | |
| 42 | + | ||
| 43 | + | var has_config = load_network_config() | |
| 44 | + | if has_config: | |
| 45 | + | print("[Autoconnect] Secure config file loaded. Instantiating auto-client...") | |
| 46 | + | start_client() | |
| 47 | + | else: | |
| 48 | + | print("[Autoconnect] Initial default config file created.") | |
| 49 | + | ||
| 50 | + | func _process(delta: float) -> void: | |
| 51 | + | # 1. Manage Clipboard Synchronization | |
| 52 | + | var current_sys_clip = DisplayServer.clipboard_get() | |
| 53 | + | if current_sys_clip != clipboard_text: | |
| 54 | + | if multiplayer.is_server() or line_edit.has_focus() or current_sys_clip != "": | |
| 55 | + | var encrypted_payload = encrypt_string(current_sys_clip) | |
| 56 | + | rpc("receive_secure_clipboard", encrypted_payload) | |
| 57 | + | ||
| 58 | + | # 2. Manage Automatic Client Reconnection Loop | |
| 59 | + | if is_reconnecting and not is_server_mode: | |
| 60 | + | retry_timer += delta | |
| 61 | + | if retry_timer >= RETRY_INTERVAL: | |
| 62 | + | retry_timer = 0.0 | |
| 63 | + | rate_limited_log("[Network Recovery] 10 seconds elapsed. Attempting background reconnect...") | |
| 64 | + | silent_client_reconnect() | |
| 65 | + | ||
| 66 | + | @rpc("any_peer", "call_local", "reliable") | |
| 67 | + | func receive_secure_clipboard(encrypted_data: PackedByteArray) -> void: | |
| 68 | + | var decrypted_text = decrypt_string(encrypted_data) | |
| 69 | + | clipboard_text = decrypted_text | |
| 70 | + | DisplayServer.clipboard_set(decrypted_text) | |
| 71 | + | queue_redraw() | |
| 72 | + | ||
| 73 | + | func _on_text_submitted(new_text: String) -> void: | |
| 74 | + | var encrypted_payload = encrypt_string(new_text) | |
| 75 | + | rpc("receive_secure_clipboard", encrypted_payload) | |
| 76 | + | line_edit.text = "" | |
| 77 | + | ||
| 78 | + | func _draw() -> void: | |
| 79 | + | draw_rect(Rect2(50, 270, 500, 80), Color(0.15, 0.15, 0.15, 1.0)) | |
| 80 | + | var system_font = ThemeDB.fallback_font | |
| 81 | + | var draw_position = Vector2(60, 320) | |
| 82 | + | ||
| 83 | + | var network_status = "Mode: Server" if is_server_mode else "Mode: Client" | |
| 84 | + | if multiplayer.multiplayer_peer == null: | |
| 85 | + | network_status = "Mode: Offline" | |
| 86 | + | elif is_reconnecting and not is_server_mode: | |
| 87 | + | network_status = "Mode: Client (Reconnecting...)" | |
| 88 | + | ||
| 89 | + | var current_text_color = Color.GREEN if is_connected_to_network else Color.YELLOW | |
| 90 | + | draw_string(system_font, draw_position, network_status + " | Secure Clip: " + clipboard_text, HORIZONTAL_ALIGNMENT_LEFT, -1, 18, current_text_color) | |
| 91 | + | ||
| 92 | + | # ========================================== | |
| 93 | + | # MULTIPLAYER SIGNAL & RECONNECT CALLBACKS | |
| 94 | + | # ========================================== | |
| 95 | + | ||
| 96 | + | func _on_connected_ok() -> void: | |
| 97 | + | is_connected_to_network = true | |
| 98 | + | is_reconnecting = false | |
| 99 | + | retry_timer = 0.0 | |
| 100 | + | print("[Network Status] Connection successful. Text changed to GREEN.") | |
| 101 | + | queue_redraw() | |
| 102 | + | ||
| 103 | + | func _on_connection_failed() -> void: | |
| 104 | + | is_connected_to_network = false | |
| 105 | + | # Trigger the reconnection routine cleanly if we aren't already hunting a link | |
| 106 | + | if not is_reconnecting: | |
| 107 | + | is_reconnecting = true | |
| 108 | + | retry_timer = 0.0 | |
| 109 | + | rate_limited_log("[Network Error] Initial handshake failed. Suppressing cascade logs; retrying in 10s.") | |
| 110 | + | queue_redraw() | |
| 111 | + | ||
| 112 | + | func _on_disconnected() -> void: | |
| 113 | + | is_connected_to_network = false | |
| 114 | + | is_reconnecting = true | |
| 115 | + | retry_timer = 0.0 | |
| 116 | + | print("[Network Status] Disconnected from host. Text changed to YELLOW. Starting 10s retry cadence.") | |
| 117 | + | queue_redraw() | |
| 118 | + | ||
| 119 | + | func _on_peer_connected(id: int) -> void: | |
| 120 | + | if is_server_mode:_ | |
| 121 | + | is_connected_to_network = true | |
| 122 | + | queue_redraw() | |
| 123 | + | ||
| 124 | + | func _on_peer_disconnected(id: int) -> void: | |
| 125 | + | if is_server_mode and multiplayer.get_peers().size() == 0: | |
| 126 | + | is_connected_to_network = false | |
| 127 | + | queue_redraw() | |
| 128 | + | ||
| 129 | + | func silent_client_reconnect() -> void: | |
| 130 | + | # Explicitly clean up old hanging connection references before attempting a new socket allocation | |
| 131 | + | multiplayer.multiplayer_peer = null | |
| 132 | + | ||
| 133 | + | var peer = ENetMultiplayerPeer.new() | |
| 134 | + | var error = peer.create_client(server_ip, port_number) | |
| 135 | + | if error == OK: | |
| 136 | + | multiplayer.multiplayer_peer = peer | |
| 137 | + | else: | |
| 138 | + | rate_limited_log("[Network Error] Peer creation failed silently: " + str(error)) | |
| 139 | + | ||
| 140 | + | # Custom logger to ensure your output terminal stays pristine instead of printing millions of loops | |
| 141 | + | func rate_limited_log(message: String) -> void: | |
| 142 | + | var current_time = Time.get_ticks_msec() / 1000.0 | |
| 143 | + | if current_time - last_log_time >= LOG_LIMIT_INTERVAL: | |
| 144 | + | print(message) | |
| 145 | + | last_log_time = current_time | |
| 146 | + | ||
| 147 | + | # ========================================== | |
| 148 | + | # AES-256 ENCRYPTION & DECRYPTION LOGIC | |
| 149 | + | # ========================================== | |
| 150 | + | ||
| 151 | + | func get_hashed_key() -> PackedByteArray: | |
| 152 | + | var ctx = HashingContext.new() | |
| 153 | + | ctx.start(HashingContext.HASH_SHA256) | |
| 154 | + | ctx.update(secret_key.to_utf8_buffer()) | |
| 155 | + | return ctx.finish() | |
| 156 | + | ||
| 157 | + | func encrypt_string(plain_text: String) -> PackedByteArray: | |
| 158 | + | var aes = AESContext.new() | |
| 159 | + | var key = get_hashed_key() | |
| 160 | + | var data_bytes = plain_text.to_utf8_buffer() | |
| 161 | + | var padding_needed = 16 - (data_bytes.size() % 16) | |
| 162 | + | for i in range(padding_needed): | |
| 163 | + | data_bytes.append(padding_needed) | |
| 164 | + | ||
| 165 | + | aes.start(AESContext.MODE_ECB_ENCRYPT, key) | |
| 166 | + | var encrypted = aes.update(data_bytes) | |
| 167 | + | aes.finish() | |
| 168 | + | return encrypted | |
| 169 | + | ||
| 170 | + | func decrypt_string(encrypted_bytes: PackedByteArray) -> String: | |
| 171 | + | if encrypted_bytes.is_empty(): return "" | |
| 172 | + | var aes = AESContext.new() | |
| 173 | + | var key = get_hashed_key() | |
| 174 | + | ||
| 175 | + | aes.start(AESContext.MODE_ECB_DECRYPT, key) | |
| 176 | + | var decrypted = aes.update(encrypted_bytes) | |
| 177 | + | aes.finish() | |
| 178 | + | ||
| 179 | + | if decrypted.size() > 0: | |
| 180 | + | var padding_count = decrypted[decrypted.size() - 1] | |
| 181 | + | if padding_count > 0 and padding_count <= 16: | |
| 182 | + | decrypted = decrypted.slice(0, decrypted.size() - padding_count) | |
| 183 | + | return decrypted.get_string_from_utf8() | |
| 184 | + | ||
| 185 | + | # ========================================== | |
| 186 | + | # CONFIGURATION FILE SAVE & LOAD LOGIC | |
| 187 | + | # ========================================== | |
| 188 | + | ||
| 189 | + | func load_network_config() -> bool: | |
| 190 | + | var config = ConfigFile.new() | |
| 191 | + | var error = config.load(CONFIG_PATH) | |
| 192 | + | if error == OK: | |
| 193 | + | server_ip = config.get_value("Network", "server_ip", "127.0.0.1") | |
| 194 | + | port_number = config.get_value("Network", "port_number", 8443) | |
| 195 | + | secret_key = config.get_value("Network", "secret_key", "ChangeMe123!") | |
| 196 | + | if ip_input and port_input and key_input: | |
| 197 | + | ip_input.text = server_ip | |
| 198 | + | port_input.text = str(port_number) | |
| 199 | + | key_input.text = secret_key | |
| 200 | + | return true | |
| 201 | + | else: | |
| 202 | + | save_network_config("127.0.0.1", 8443, "ChangeMe123!") | |
| 203 | + | return false | |
| 204 | + | ||
| 205 | + | func save_network_config(ip: String, port: int, key: String) -> void: | |
| 206 | + | var config = ConfigFile.new() | |
| 207 | + | config.set_value("Network", "server_ip", ip) | |
| 208 | + | config.set_value("Network", "port_number", port) | |
| 209 | + | config.set_value("Network", "secret_key", key) | |
| 210 | + | if config.save(CONFIG_PATH) == OK: | |
| 211 | + | server_ip = ip | |
| 212 | + | port_number = port | |
| 213 | + | secret_key = key | |
| 214 | + | ||
| 215 | + | # ========================================== | |
| 216 | + | # INTERFACE IMPLEMENTATION | |
| 217 | + | # ========================================== | |
| 218 | + | ||
| 219 | + | func create_network_interface() -> void: | |
| 220 | + | var btn_server = Button.new() | |
| 221 | + | btn_server.text = "Start Server" | |
| 222 | + | btn_server.position = Vector2(50, 40) | |
| 223 | + | btn_server.pressed.connect(start_server) | |
| 224 | + | add_child(btn_server) | |
| 225 | + | ||
| 226 | + | var btn_client = Button.new() | |
| 227 | + | btn_client.text = "Connect Client" | |
| 228 | + | btn_client.position = Vector2(160, 40) | |
| 229 | + | btn_client.pressed.connect(start_client) | |
| 230 | + | add_child(btn_client) | |
| 231 | + | ||
| 232 | + | ip_input = LineEdit.new() | |
| 233 | + | ip_input.text = server_ip | |
| 234 | + | ip_input.placeholder_text = "Target Server IP" | |
| 235 | + | ip_input.position = Vector2(50, 100) | |
| 236 | + | ip_input.custom_minimum_size = Vector2(180, 35) | |
| 237 | + | add_child(ip_input) | |
| 238 | + | ||
| 239 | + | port_input = LineEdit.new() | |
| 240 | + | port_input.text = str(port_number) | |
| 241 | + | port_input.placeholder_text = "Port" | |
| 242 | + | port_input.position = Vector2(250, 100) | |
| 243 | + | port_input.custom_minimum_size = Vector2(100, 35) | |
| 244 | + | add_child(port_input) | |
| 245 | + | ||
| 246 | + | key_input = LineEdit.new() | |
| 247 | + | key_input.text = secret_key | |
| 248 | + | key_input.secret = true | |
| 249 | + | key_input.placeholder_text = "Shared Secret Key Passphrase" | |
| 250 | + | key_input.position = Vector2(50, 150) | |
| 251 | + | key_input.custom_minimum_size = Vector2(300, 35) | |
| 252 | + | add_child(key_input) | |
| 253 | + | ||
| 254 | + | func start_server() -> void: | |
| 255 | + | is_connected_to_network = false | |
| 256 | + | is_reconnecting = false | |
| 257 | + | is_server_mode = true | |
| 258 | + | var active_port = int(port_input.text) | |
| 259 | + | save_network_config(ip_input.text, active_port, key_input.text) | |
| 260 | + | var peer = ENetMultiplayerPeer.new() | |
| 261 | + | if peer.create_server(active_port) != OK: return | |
| 262 | + | multiplayer.multiplayer_peer = peer | |
| 263 | + | print("[Network] Server active on port: ", active_port) | |
| 264 | + | queue_redraw() | |
| 265 | + | ||
| 266 | + | func start_client() -> void: | |
| 267 | + | is_connected_to_network = false | |
| 268 | + | is_server_mode = false | |
| 269 | + | var active_ip = ip_input.text | |
| 270 | + | var active_port = int(port_input.text) | |
| 271 | + | save_network_config(active_ip, active_port, key_input.text) | |
| 272 | + | ||
| 273 | + | var peer = ENetMultiplayerPeer.new() | |
| 274 | + | var error = peer.create_client(active_ip, active_port) | |
| 275 | + | if error == OK: | |
| 276 | + | multiplayer.multiplayer_peer = peer | |
| 277 | + | is_reconnecting = true # Instantly engage our automated monitoring pipeline | |
| 278 | + | queue_redraw() | |
Godot-System-Tray.gd
| @@ -9,7 +9,7 @@ func _ready() -> void: | |||
| 9 | 9 | func setup_tray() -> void: | |
| 10 | 10 | # 1. Initialize the StatusIndicator | |
| 11 | 11 | tray_icon = StatusIndicator.new() | |
| 12 | - | tray_icon.icon = load("res://icon.svg") # Path to your 16x16 or 32x32 icon | |
| 12 | + | tray_icon.icon = load("res://ship-icon-256.png") # Path to your 16x16 or 32x32 icon | |
| 13 | 13 | tray_icon.tooltip = "My Godot App" | |
| 14 | 14 | add_child(tray_icon) | |
| 15 | 15 | ||
Godot-System-Tray.gdscript переименован в Godot-System-Tray.gd
Файл переименован без изменений
Godot-System-Tray.gdscript(файл создан)
| @@ -0,0 +1,55 @@ | |||
| 1 | + | extends Node | |
| 2 | + | ||
| 3 | + | var tray_icon: StatusIndicator | |
| 4 | + | var tray_menu: PopupMenu | |
| 5 | + | ||
| 6 | + | func _ready() -> void: | |
| 7 | + | setup_tray() | |
| 8 | + | ||
| 9 | + | func setup_tray() -> void: | |
| 10 | + | # 1. Initialize the StatusIndicator | |
| 11 | + | tray_icon = StatusIndicator.new() | |
| 12 | + | tray_icon.icon = load("res://icon.svg") # Path to your 16x16 or 32x32 icon | |
| 13 | + | tray_icon.tooltip = "My Godot App" | |
| 14 | + | add_child(tray_icon) | |
| 15 | + | ||
| 16 | + | # 2. Bind the click event to restore the window | |
| 17 | + | tray_icon.pressed.connect(_on_tray_icon_pressed) | |
| 18 | + | ||
| 19 | + | # 3. Optional: Create a right-click context menu | |
| 20 | + | tray_menu = PopupMenu.new() | |
| 21 | + | tray_menu.add_item("Show Window", 0) | |
| 22 | + | tray_menu.add_item("Quit", 1) | |
| 23 | + | tray_menu.id_pressed.connect(_on_menu_item_pressed) | |
| 24 | + | add_child(tray_menu) | |
| 25 | + | ||
| 26 | + | # Assign menu to the tray icon | |
| 27 | + | tray_icon.menu = tray_menu.get_path() | |
| 28 | + | ||
| 29 | + | # Handle window close interception | |
| 30 | + | func _notification(what: int) -> void: | |
| 31 | + | if what == NOTIFICATION_WM_CLOSE_REQUEST: | |
| 32 | + | minimize_to_tray() | |
| 33 | + | ||
| 34 | + | # Hides the application entirely from desktop view | |
| 35 | + | func minimize_to_tray() -> void: | |
| 36 | + | # Hide window from taskbar and ignore standard user inputs | |
| 37 | + | DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, true) | |
| 38 | + | # Hide the actual window frame | |
| 39 | + | DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) | |
| 40 | + | ||
| 41 | + | # Restores the application window back to normal | |
| 42 | + | func restore_from_tray() -> void: | |
| 43 | + | DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, false) | |
| 44 | + | DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) | |
| 45 | + | ||
| 46 | + | # Handle clicking directly on the tray icon | |
| 47 | + | func _on_tray_icon_pressed(mouse_button: int, _device_id: int) -> void: | |
| 48 | + | if mouse_button == MOUSE_BUTTON_LEFT: | |
| 49 | + | restore_from_tray() | |
| 50 | + | ||
| 51 | + | # Handle context menu actions | |
| 52 | + | func _on_menu_item_pressed(id: int) -> void: | |
| 53 | + | match id: | |
| 54 | + | 0: restore_from_tray() | |
| 55 | + | 1: get_tree().quit() # Ensure clean termination when explicitly chosen | |