最終更新 1785695986

Systray app behavior: Go to Project > Project Settings > Engine > Config > Uncheck Auto Accept Quit

修正履歴 d1be8e2e53c1132c529410e0d9903fcc7c93a6e5

Godot-Clipboard-Sync.gd Raw
1extends Node2D
2
3@onready var line_edit: LineEdit = $LineEdit
4@onready var synchronizer: MultiplayerSynchronizer = $MultiplayerSynchronizer
5
6var server_ip: String = "127.0.0.1"
7var port_number: int = 8443
8var secret_key: String = "ChangeMe123!"
9
10@export var clipboard_text: String = "Waiting for connection..."
11
12var is_connected_to_network: bool = false
13var is_server_mode: bool = false
14
15# Reconnection and Rate-Limiting Tracker variables
16var is_reconnecting: bool = false
17var retry_timer: float = 0.0
18const RETRY_INTERVAL: float = 10.0 # Reconnect attempt frequency in seconds
19var last_log_time: float = 0.0
20const LOG_LIMIT_INTERVAL: float = 10.0 # Suppresses error logs within this timeframe
21
22const CONFIG_PATH = "user://network_settings.cfg"
23
24var ip_input: LineEdit
25var port_input: LineEdit
26var key_input: LineEdit
27
28func _ready() -> void:
29 line_edit.position = Vector2(50, 195)
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
51func _process(delta: float) -> void:
52 # 1. Manage Clipboard Synchronization
53 var current_sys_clip = DisplayServer.clipboard_get()
54 if current_sys_clip != clipboard_text:
55 if multiplayer.is_server() or line_edit.has_focus() or current_sys_clip != "":
56 var encrypted_payload = encrypt_string(current_sys_clip)
57 rpc("receive_secure_clipboard", encrypted_payload)
58
59 # 2. Manage Automatic Client Reconnection Loop
60 if is_reconnecting and not is_server_mode:
61 retry_timer += delta
62 if retry_timer >= RETRY_INTERVAL:
63 retry_timer = 0.0
64 rate_limited_log("[Network Recovery] 10 seconds elapsed. Attempting background reconnect...")
65 silent_client_reconnect()
66
67@rpc("any_peer", "call_local", "reliable")
68func receive_secure_clipboard(encrypted_data: PackedByteArray) -> void:
69 var decrypted_text = decrypt_string(encrypted_data)
70 clipboard_text = decrypted_text
71 DisplayServer.clipboard_set(decrypted_text)
72 queue_redraw()
73
74func _on_text_submitted(new_text: String) -> void:
75 var encrypted_payload = encrypt_string(new_text)
76 rpc("receive_secure_clipboard", encrypted_payload)
77 line_edit.text = ""
78
79func _draw() -> void:
80 draw_rect(Rect2(50, 270, 500, 80), Color(0.15, 0.15, 0.15, 1.0))
81 var system_font = ThemeDB.fallback_font
82 var draw_position = Vector2(60, 320)
83
84 var network_status = "Mode: Server" if is_server_mode else "Mode: Client"
85 if multiplayer.multiplayer_peer == null:
86 network_status = "Mode: Offline"
87 elif is_reconnecting and not is_server_mode:
88 network_status = "Mode: Client (Reconnecting...)"
89
90 var current_text_color = Color.GREEN if is_connected_to_network else Color.YELLOW
91 draw_string(system_font, draw_position, network_status + " | Secure Clip: " + clipboard_text, HORIZONTAL_ALIGNMENT_LEFT, -1, 18, current_text_color)
92
93# ==========================================
94# MULTIPLAYER SIGNAL & RECONNECT CALLBACKS
95# ==========================================
96
97func _on_connected_ok() -> void:
98 is_connected_to_network = true
99 is_reconnecting = false
100 retry_timer = 0.0
101 print("[Network Status] Connection successful. Text changed to GREEN.")
102 queue_redraw()
103
104func _on_connection_failed() -> void:
105 is_connected_to_network = false
106 # Trigger the reconnection routine cleanly if we aren't already hunting a link
107 if not is_reconnecting:
108 is_reconnecting = true
109 retry_timer = 0.0
110 rate_limited_log("[Network Error] Initial handshake failed. Suppressing cascade logs; retrying in 10s.")
111 queue_redraw()
112
113func _on_disconnected() -> void:
114 is_connected_to_network = false
115 is_reconnecting = true
116 retry_timer = 0.0
117 print("[Network Status] Disconnected from host. Text changed to YELLOW. Starting 10s retry cadence.")
118 queue_redraw()
119
120func _on_peer_connected(_id: int) -> void:
121 if is_server_mode:
122 is_connected_to_network = true
123 queue_redraw()
124
125func _on_peer_disconnected(_id: int) -> void:
126 if is_server_mode and multiplayer.get_peers().size() == 0:
127 is_connected_to_network = false
128 queue_redraw()
129
130func silent_client_reconnect() -> void:
131 # Explicitly clean up old hanging connection references before attempting a new socket allocation
132 multiplayer.multiplayer_peer = null
133
134 var peer = ENetMultiplayerPeer.new()
135 var error = peer.create_client(server_ip, port_number)
136 if error == OK:
137 multiplayer.multiplayer_peer = peer
138 else:
139 rate_limited_log("[Network Error] Peer creation failed silently: " + str(error))
140
141# Custom logger to ensure your output terminal stays pristine instead of printing millions of loops
142func rate_limited_log(message: String) -> void:
143 var current_time = Time.get_ticks_msec() / 1000.0
144 if current_time - last_log_time >= LOG_LIMIT_INTERVAL:
145 print(message)
146 last_log_time = current_time
147
148# ==========================================
149# AES-256 ENCRYPTION & DECRYPTION LOGIC
150# ==========================================
151
152func get_hashed_key() -> PackedByteArray:
153 var ctx = HashingContext.new()
154 ctx.start(HashingContext.HASH_SHA256)
155 ctx.update(secret_key.to_utf8_buffer())
156 return ctx.finish()
157
158func encrypt_string(plain_text: String) -> PackedByteArray:
159 var aes = AESContext.new()
160 var key = get_hashed_key()
161 var data_bytes = plain_text.to_utf8_buffer()
162 var padding_needed = 16 - (data_bytes.size() % 16)
163 for i in range(padding_needed):
164 data_bytes.append(padding_needed)
165
166 aes.start(AESContext.MODE_ECB_ENCRYPT, key)
167 var encrypted = aes.update(data_bytes)
168 aes.finish()
169 return encrypted
170
171func decrypt_string(encrypted_bytes: PackedByteArray) -> String:
172 if encrypted_bytes.is_empty(): return ""
173 var aes = AESContext.new()
174 var key = get_hashed_key()
175
176 aes.start(AESContext.MODE_ECB_DECRYPT, key)
177 var decrypted = aes.update(encrypted_bytes)
178 aes.finish()
179
180 if decrypted.size() > 0:
181 var padding_count = decrypted[decrypted.size() - 1]
182 if padding_count > 0 and padding_count <= 16:
183 decrypted = decrypted.slice(0, decrypted.size() - padding_count)
184 return decrypted.get_string_from_utf8()
185
186# ==========================================
187# CONFIGURATION FILE SAVE & LOAD LOGIC
188# ==========================================
189
190func load_network_config() -> bool:
191 var config = ConfigFile.new()
192 var error = config.load(CONFIG_PATH)
193 if error == OK:
194 server_ip = config.get_value("Network", "server_ip", "127.0.0.1")
195 port_number = config.get_value("Network", "port_number", 8443)
196 secret_key = config.get_value("Network", "secret_key", "ChangeMe123!")
197 if ip_input and port_input and key_input:
198 ip_input.text = server_ip
199 port_input.text = str(port_number)
200 key_input.text = secret_key
201 return true
202 else:
203 save_network_config("127.0.0.1", 8443, "ChangeMe123!")
204 return false
205
206func save_network_config(ip: String, port: int, key: String) -> void:
207 var config = ConfigFile.new()
208 config.set_value("Network", "server_ip", ip)
209 config.set_value("Network", "port_number", port)
210 config.set_value("Network", "secret_key", key)
211 if config.save(CONFIG_PATH) == OK:
212 server_ip = ip
213 port_number = port
214 secret_key = key
215
216# ==========================================
217# INTERFACE IMPLEMENTATION
218# ==========================================
219
220func create_network_interface() -> void:
221 var btn_server = Button.new()
222 btn_server.text = "Start Server"
223 btn_server.position = Vector2(50, 40)
224 btn_server.pressed.connect(start_server)
225 add_child(btn_server)
226
227 var btn_client = Button.new()
228 btn_client.text = "Connect Client"
229 btn_client.position = Vector2(160, 40)
230 btn_client.pressed.connect(start_client)
231 add_child(btn_client)
232
233 ip_input = LineEdit.new()
234 ip_input.text = server_ip
235 ip_input.placeholder_text = "Target Server IP"
236 ip_input.position = Vector2(50, 100)
237 ip_input.custom_minimum_size = Vector2(180, 35)
238 add_child(ip_input)
239
240 port_input = LineEdit.new()
241 port_input.text = str(port_number)
242 port_input.placeholder_text = "Port"
243 port_input.position = Vector2(250, 100)
244 port_input.custom_minimum_size = Vector2(100, 35)
245 add_child(port_input)
246
247 key_input = LineEdit.new()
248 key_input.text = secret_key
249 key_input.secret = true
250 key_input.placeholder_text = "Shared Secret Key Passphrase"
251 key_input.position = Vector2(50, 150)
252 key_input.custom_minimum_size = Vector2(300, 35)
253 add_child(key_input)
254
255 key_input = LineEdit.new()
256 key_input.text = secret_key
257 key_input.secret = true
258 key_input.placeholder_text = "Shared Secret Key Passphrase"
259 key_input.position = Vector2(50, 150)
260 key_input.custom_minimum_size = Vector2(300, 35)
261 add_child(key_input)
262
263 var btn_generate = Button.new()
264 btn_generate.text = "New Key"
265 btn_generate.position = Vector2(358, 150)
266 btn_generate.size.y = 34
267 btn_generate.pressed.connect(_on_generate_pressed)
268 add_child(btn_generate)
269
270 var btn_copy = Button.new()
271 btn_copy.text = "Copy Key"
272 btn_copy.position = Vector2(440, 150)
273 btn_copy.size.y = 34
274 btn_copy.pressed.connect(_on_copy_pressed)
275 add_child(btn_copy)
276
277
278func start_server() -> void:
279 is_connected_to_network = false
280 is_reconnecting = false
281 is_server_mode = true
282 var active_port = int(port_input.text)
283 save_network_config(ip_input.text, active_port, key_input.text)
284 var peer = ENetMultiplayerPeer.new()
285 if peer.create_server(active_port) != OK: return
286 multiplayer.multiplayer_peer = peer
287 print("[Network] Server active on port: ", active_port)
288 queue_redraw()
289
290func start_client() -> void:
291 is_connected_to_network = false
292 is_server_mode = false
293 var active_ip = ip_input.text
294 var active_port = int(port_input.text)
295 save_network_config(active_ip, active_port, key_input.text)
296
297 var peer = ENetMultiplayerPeer.new()
298 var error = peer.create_client(active_ip, active_port)
299 if error == OK:
300 multiplayer.multiplayer_peer = peer
301 is_reconnecting = true # Instantly engage our automated monitoring pipeline
302 queue_redraw()
303
304func _on_generate_pressed() -> void:
305 key_input.text = generate_random_key(16)
306
307func _on_copy_pressed() -> void:
308 if key_input.text.is_empty():
309 return
310 DisplayServer.clipboard_set(key_input.text)
311 print("Copied to clipboard!")
312
313func generate_random_key(length: int) -> String:
314 var chars: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
315 var result: String = ""
316 for i in range(length):
317 result += chars[randi() % chars.length()]
318 return result
319
Godot-System-Tray.gd Raw
1extends Node
2
3var tray_icon: StatusIndicator
4var tray_menu: PopupMenu
5
6func _ready() -> void:
7 setup_tray()
8
9func setup_tray() -> void:
10 # 1. Initialize the StatusIndicator
11 tray_icon = StatusIndicator.new()
12 tray_icon.icon = load("res://ship-icon-256.png") # Path to your 16x16 or 32x32 icon
13 tray_icon.tooltip = "EnvoyOS"
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
30func _notification(what: int) -> void:
31 if what == NOTIFICATION_WM_CLOSE_REQUEST:
32 minimize_to_tray()
33
34# Hides the application entirely from desktop view
35func 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
42func 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
47func _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
52func _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
Godot-Z1-README.md Raw

Layout

◯ Node2D - Godot-Clipboard-Sync.gd
└── LineEdit
└── ◯ Node - Godot-System-Tray.gd
└── MultiplayerSynchronizer

Features

  • Godot systray app
  • Clipboard synchronization copy and paste manager for Linux and Windows
    • Autoconnect to server on start
    • Encrypted Clipboard Sync
    • Save server address and secret key to App config file
    • 💡 Unicode character support