about summary refs log tree commit diff stats
path: root/apworld/client
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/client')
-rw-r--r--apworld/client/client.gd381
-rw-r--r--apworld/client/main.gd16
-rw-r--r--apworld/client/manager.gd201
-rw-r--r--apworld/client/panel.gd4
-rw-r--r--apworld/client/player.gd20
-rw-r--r--apworld/client/textclient.gd2
-rw-r--r--apworld/client/vendor/LICENSE28
-rw-r--r--apworld/client/vendor/WebSocketServer.gd173
-rw-r--r--apworld/client/vendor/uuid.gd195
9 files changed, 369 insertions, 651 deletions
diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 843647d..67edf29 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd
@@ -2,20 +2,10 @@ extends Node
2 2
3const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"} 3const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"}
4 4
5var SCRIPT_uuid 5var SCRIPT_websocketserver
6 6
7var _ws = WebSocketPeer.new() 7var _server
8var _should_process = false 8var _should_process = false
9var _initiated_disconnect = false
10var _try_wss = false
11var _has_connected = false
12
13var _datapackages = {}
14var _pending_packages = []
15var _item_id_to_name = {} # All games
16var _location_id_to_name = {} # All games
17var _item_name_to_id = {} # Lingo 2 only
18var _location_name_to_id = {} # Lingo 2 only
19 9
20var _remote_version = {"major": 0, "minor": 0, "build": 0} 10var _remote_version = {"major": 0, "minor": 0, "build": 0}
21var _gen_version = {"major": 0, "minor": 0, "build": 0} 11var _gen_version = {"major": 0, "minor": 0, "build": 0}
@@ -24,13 +14,9 @@ var ap_server = ""
24var ap_user = "" 14var ap_user = ""
25var ap_pass = "" 15var ap_pass = ""
26 16
27var _authenticated = false
28var _seed = "" 17var _seed = ""
29var _team = 0 18var _team = 0
30var _slot = 0 19var _slot = 0
31var _players = []
32var _player_name_by_slot = {}
33var _game_by_player = {}
34var _checked_locations = [] 20var _checked_locations = []
35var _received_indexes = [] 21var _received_indexes = []
36var _received_items = {} 22var _received_items = {}
@@ -39,317 +25,135 @@ var _slot_data = {}
39signal could_not_connect 25signal could_not_connect
40signal connect_status 26signal connect_status
41signal client_connected(slot_data) 27signal client_connected(slot_data)
42signal item_received(item_id, index, player, flags, amount) 28signal item_received(item, amount)
43signal message_received(message) 29signal location_scout_received(location_id, item_name, player_name, flags, for_self)
44signal location_scout_received(item_id, location_id, player, flags) 30signal text_message_received(message)
31signal item_sent_notification(message)
32signal hint_received(message)
45 33
46 34
47func _init(): 35func _init():
48 set_process_mode(Node.PROCESS_MODE_ALWAYS) 36 set_process_mode(Node.PROCESS_MODE_ALWAYS)
49 37
50 _ws.inbound_buffer_size = 8388608
51
52 global._print("Instantiated APClient") 38 global._print("Instantiated APClient")
53 39
54 # Read AP datapackages from file, if there are any
55 if FileAccess.file_exists("user://ap_datapackages"):
56 var file = FileAccess.open("user://ap_datapackages", FileAccess.READ)
57 var data = file.get_var(true)
58 file.close()
59
60 if typeof(data) != TYPE_DICTIONARY:
61 global._print("AP datapackages file is corrupted")
62 data = {}
63
64 _datapackages = data
65
66 processDatapackages()
67
68 40
69func _ready(): 41func _ready():
70 pass 42 _server = SCRIPT_websocketserver.new()
71 #_ws.connect("connection_closed", _closed) 43 _server.client_connected.connect(_on_web_socket_server_client_connected)
72 #_ws.connect("connection_failed", _closed) 44 _server.client_disconnected.connect(_on_web_socket_server_client_disconnected)
73 #_ws.connect("server_disconnected", _closed) 45 _server.message_received.connect(_on_web_socket_server_message_received)
74 #_ws.connect("connection_error", _errored) 46 add_child(_server)
75 #_ws.connect("connection_established", _connected) 47 _server.listen(43182)
76 48
77 49
78func _reset_state(): 50func _reset_state():
79 _should_process = false 51 _should_process = false
80 _authenticated = false
81 _try_wss = false
82 _has_connected = false
83 _received_items = {} 52 _received_items = {}
84 _received_indexes = [] 53 _received_indexes = []
85 54
86 55
87func _errored(): 56func disconnect_from_ap():
88 if _try_wss: 57 sendMessage([{"cmd": "Disconnect"}])
89 global._print("Could not connect to AP with ws://, now trying wss://")
90 connectToServer(ap_server, ap_user, ap_pass)
91 else:
92 global._print("AP connection failed")
93 _reset_state()
94 58
95 emit_signal(
96 "could_not_connect",
97 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information."
98 )
99 59
60func _on_web_socket_server_client_connected(peer_id: int) -> void:
61 var peer: WebSocketPeer = _server.peers[peer_id]
62 print("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()])
63 _server.send(-peer_id, "[%d] connected" % peer_id)
100 64
101func _closed(_was_clean = true):
102 global._print("Connection closed")
103 _reset_state()
104 65
105 if not _initiated_disconnect: 66func _on_web_socket_server_client_disconnected(peer_id: int) -> void:
106 emit_signal("could_not_connect", "Disconnected from Archipelago") 67 var peer: WebSocketPeer = _server.peers[peer_id]
68 print(
69 (
70 "Remote client disconnected: %d. Code: %d, Reason: %s"
71 % [peer_id, peer.get_close_code(), peer.get_close_reason()]
72 )
73 )
74 _server.send(-peer_id, "[%d] disconnected" % peer_id)
107 75
108 _initiated_disconnect = false
109 76
77func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> void:
78 global._print("Got data from server: " + packet)
79 var json = JSON.new()
80 var jserror = json.parse(packet)
81 if jserror != OK:
82 global._print("Error parsing packet from AP: " + jserror.error_string)
83 return
110 84
111func _connected(_proto = ""): 85 for message in json.data:
112 global._print("Connected!") 86 var cmd = message["cmd"]
113 _try_wss = false 87 global._print("Received command: " + cmd)
114 88
89 if cmd == "Connected":
90 _seed = message["seed_name"]
91 _remote_version = message["version"]
92 _gen_version = message["generator_version"]
93 _team = message["team"]
94 _slot = message["slot"]
95 _checked_locations = message["checked_locations"]
96 _slot_data = message["slot_data"]
115 97
116func disconnect_from_ap(): 98 client_connected.emit(_slot_data)
117 _initiated_disconnect = true
118 _ws.close()
119
120
121func _process(_delta):
122 if _should_process:
123 _ws.poll()
124
125 var state = _ws.get_ready_state()
126 if state == WebSocketPeer.STATE_OPEN:
127 if not _has_connected:
128 _has_connected = true
129
130 _connected()
131
132 while _ws.get_available_packet_count():
133 var packet = _ws.get_packet()
134 global._print("Got data from server: " + packet.get_string_from_utf8())
135 var json = JSON.new()
136 var jserror = json.parse(packet.get_string_from_utf8())
137 if jserror != OK:
138 global._print("Error parsing packet from AP: " + jserror.error_string)
139 return
140
141 for message in json.data:
142 var cmd = message["cmd"]
143 global._print("Received command: " + cmd)
144
145 if cmd == "RoomInfo":
146 _seed = message["seed_name"]
147 _remote_version = message["version"]
148 _gen_version = message["generator_version"]
149
150 var needed_games = []
151 for game in message["datapackage_checksums"].keys():
152 if (
153 !_datapackages.has(game)
154 or (
155 _datapackages[game]["checksum"]
156 != message["datapackage_checksums"][game]
157 )
158 ):
159 needed_games.append(game)
160
161 if !needed_games.is_empty():
162 _pending_packages = needed_games
163 var cur_needed = _pending_packages.pop_front()
164 requestDatapackages([cur_needed])
165 else:
166 connectToRoom()
167
168 elif cmd == "DataPackage":
169 for game in message["data"]["games"].keys():
170 _datapackages[game] = message["data"]["games"][game]
171 saveDatapackages()
172
173 if !_pending_packages.is_empty():
174 var cur_needed = _pending_packages.pop_front()
175 requestDatapackages([cur_needed])
176 else:
177 processDatapackages()
178 connectToRoom()
179
180 elif cmd == "Connected":
181 _authenticated = true
182 _team = message["team"]
183 _slot = message["slot"]
184 _players = message["players"]
185 _checked_locations = message["checked_locations"]
186 _slot_data = message["slot_data"]
187
188 for player in _players:
189 _player_name_by_slot[player["slot"]] = player["alias"]
190 _game_by_player[player["slot"]] = message["slot_info"][str(
191 player["slot"]
192 )]["game"]
193
194 emit_signal("client_connected", _slot_data)
195
196 elif cmd == "ConnectionRefused":
197 var error_message = ""
198 for error in message["errors"]:
199 var submsg = ""
200 if error == "InvalidSlot":
201 submsg = "Invalid player name."
202 elif error == "InvalidGame":
203 submsg = "The specified player is not playing Lingo."
204 elif error == "IncompatibleVersion":
205 submsg = (
206 "The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d."
207 % [
208 ap_version["major"],
209 ap_version["minor"],
210 ap_version["build"],
211 _remote_version["major"],
212 _remote_version["minor"],
213 _remote_version["build"]
214 ]
215 )
216 elif error == "InvalidPassword":
217 submsg = "Incorrect password."
218 elif error == "InvalidItemsHandling":
219 submsg = "Invalid item handling flag. This is a bug with the client."
220
221 if submsg != "":
222 if error_message != "":
223 error_message += " "
224 error_message += submsg
225
226 if error_message == "":
227 error_message = "Unknown error."
228
229 _initiated_disconnect = true
230 _ws.close()
231
232 emit_signal("could_not_connect", error_message)
233 global._print("Connection to AP refused")
234 global._print(message)
235
236 elif cmd == "ReceivedItems":
237 var i = 0
238 for item in message["items"]:
239 var index = int(message["index"] + i)
240 i += 1
241
242 if _received_indexes.has(index):
243 # Do not re-process items.
244 continue
245
246 _received_indexes.append(index)
247
248 var item_id = int(item["item"])
249 _received_items[item_id] = _received_items.get(item_id, 0) + 1
250
251 emit_signal(
252 "item_received",
253 item_id,
254 index,
255 int(item["player"]),
256 int(item["flags"]),
257 _received_items[item_id]
258 )
259
260 elif cmd == "PrintJSON":
261 emit_signal("message_received", message)
262
263 elif cmd == "LocationInfo":
264 for loc in message["locations"]:
265 emit_signal(
266 "location_scout_received",
267 int(loc["item"]),
268 int(loc["location"]),
269 int(loc["player"]),
270 int(loc["flags"])
271 )
272
273 elif state == WebSocketPeer.STATE_CLOSED:
274 if _has_connected:
275 _closed()
276 else:
277 _errored()
278
279
280func saveDatapackages():
281 # Save the AP datapackages to disk.
282 var file = FileAccess.open("user://ap_datapackages", FileAccess.WRITE)
283 file.store_var(_datapackages, true)
284 file.close()
285 99
100 elif cmd == "ConnectionRefused":
101 could_not_connect.emit(message["text"])
102 global._print("Connection to AP refused")
286 103
287func connectToServer(server, un, pw): 104 elif cmd == "ItemReceived":
288 ap_server = server 105 for item in message["items"]:
289 ap_user = un 106 var index = int(item["index"])
290 ap_pass = pw 107 if _received_indexes.has(index):
108 # Do not re-process items.
109 continue
291 110
292 _initiated_disconnect = false 111 _received_indexes.append(index)
293
294 var url = ""
295 if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"):
296 url = ap_server
297 _try_wss = false
298 elif _try_wss:
299 url = "wss://" + ap_server
300 _try_wss = false
301 else:
302 url = "ws://" + ap_server
303 _try_wss = true
304
305 var err = _ws.connect_to_url(url)
306 if err != OK:
307 emit_signal(
308 "could_not_connect",
309 (
310 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d."
311 % err
312 )
313 )
314 global._print("Could not connect to AP: %d" % err)
315 return
316 _should_process = true
317 112
318 emit_signal("connect_status", "Connecting...") 113 var item_id = int(item["id"])
114 _received_items[item_id] = _received_items.get(item_id, 0) + 1
319 115
116 item_received.emit(item, _received_items[item_id])
320 117
321func sendMessage(msg): 118 elif cmd == "TextMessage":
322 var payload = JSON.stringify(msg) 119 text_message_received.emit(message["data"])
323 _ws.send_text(payload) 120
121 elif cmd == "ItemSentNotif":
122 item_sent_notification.emit(message)
324 123
124 elif cmd == "HintReceived":
125 hint_received.emit(message)
325 126
326func requestDatapackages(games): 127 elif cmd == "LocationInfo":
327 emit_signal("connect_status", "Downloading %s data package..." % games[0]) 128 for loc in message["locations"]:
129 location_scout_received.emit(
130 int(loc["id"]),
131 loc["item"],
132 loc["player"],
133 int(loc["flags"]),
134 int(loc["for_self"])
135 )
328 136
329 sendMessage([{"cmd": "GetDataPackage", "games": games}])
330 137
138func connectToServer(server, un, pw):
139 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
140
141 ap_server = server
142 ap_user = un
143 ap_pass = pw
331 144
332func processDatapackages(): 145 _should_process = true
333 _item_id_to_name = {}
334 _location_id_to_name = {}
335 for game in _datapackages.keys():
336 var package = _datapackages[game]
337 146
338 _item_id_to_name[game] = {} 147 connect_status.emit("Connecting...")
339 for item_name in package["item_name_to_id"].keys():
340 _item_id_to_name[game][int(package["item_name_to_id"][item_name])] = item_name
341 148
342 _location_id_to_name[game] = {}
343 for location_name in package["location_name_to_id"].keys():
344 _location_id_to_name[game][int(package["location_name_to_id"][location_name])] = location_name
345 149
346 if _datapackages.has("Lingo 2"): 150func sendMessage(msg):
347 _item_name_to_id = _datapackages["Lingo 2"]["item_name_to_id"] 151 var payload = JSON.stringify(msg)
348 _location_name_to_id = _datapackages["Lingo 2"]["location_name_to_id"] 152 _server.send(0, payload)
349 153
350 154
351func connectToRoom(): 155func connectToRoom():
352 emit_signal("connect_status", "Authenticating...") 156 connect_status.emit("Authenticating...")
353 157
354 sendMessage( 158 sendMessage(
355 [ 159 [
@@ -358,20 +162,11 @@ func connectToRoom():
358 "password": ap_pass, 162 "password": ap_pass,
359 "game": "Lingo 2", 163 "game": "Lingo 2",
360 "name": ap_user, 164 "name": ap_user,
361 "uuid": SCRIPT_uuid.v4(),
362 "version": ap_version,
363 "items_handling": 0b111, # always receive our items
364 "tags": [],
365 "slot_data": true
366 } 165 }
367 ] 166 ]
368 ) 167 )
369 168
370 169
371func sendConnectUpdate(tags):
372 sendMessage([{"cmd": "ConnectUpdate", "tags": tags}])
373
374
375func requestSync(): 170func requestSync():
376 sendMessage([{"cmd": "Sync"}]) 171 sendMessage([{"cmd": "Sync"}])
377 172
diff --git a/apworld/client/main.gd b/apworld/client/main.gd index cff92bc..9d66358 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd
@@ -30,8 +30,8 @@ func _ready():
30 ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd") 30 ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd")
31 ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd") 31 ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd")
32 ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd") 32 ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd")
33 ap_instance.SCRIPT_uuid = runtime.load_script("vendor/uuid.gd")
34 ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd") 33 ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd")
34 ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd")
35 35
36 global.add_child(ap_instance) 36 global.add_child(ap_instance)
37 37
@@ -79,9 +79,9 @@ func _ready():
79 79
80 var ap = global.get_node("Archipelago") 80 var ap = global.get_node("Archipelago")
81 var gamedata = global.get_node("Gamedata") 81 var gamedata = global.get_node("Gamedata")
82 ap.connect("ap_connected", connectionSuccessful) 82 ap.ap_connected.connect(connectionSuccessful)
83 ap.connect("could_not_connect", connectionUnsuccessful) 83 ap.could_not_connect.connect(connectionUnsuccessful)
84 ap.connect("connect_status", connectionStatus) 84 ap.connect_status.connect(connectionStatus)
85 85
86 # Populate textboxes with AP settings. 86 # Populate textboxes with AP settings.
87 get_node("../Panel/server_box").text = ap.ap_server 87 get_node("../Panel/server_box").text = ap.ap_server
@@ -99,7 +99,7 @@ func _ready():
99 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i) 99 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
100 i += 1 100 i += 1
101 101
102 history_box.get_popup().connect("id_pressed", historySelected) 102 history_box.get_popup().id_pressed.connect(historySelected)
103 103
104 # Show client version. 104 # Show client version.
105 get_node("../Panel/title").text = ( 105 get_node("../Panel/title").text = (
@@ -112,14 +112,14 @@ func _ready():
112 get_node("../Panel/password_box").add_theme_font_size_override("font_size", 36) 112 get_node("../Panel/password_box").add_theme_font_size_override("font_size", 36)
113 113
114 # Set up version mismatch dialog. 114 # Set up version mismatch dialog.
115 get_node("../Panel/VersionMismatch").connect("confirmed", startGame) 115 get_node("../Panel/VersionMismatch").confirmed.connect(startGame)
116 get_node("../Panel/VersionMismatch").get_cancel_button().pressed.connect( 116 get_node("../Panel/VersionMismatch").get_cancel_button().pressed.connect(
117 versionMismatchDeclined 117 versionMismatchDeclined
118 ) 118 )
119 119
120 # Set up buttons. 120 # Set up buttons.
121 get_node("../Panel/connect_button").connect("pressed", _connect_pressed) 121 get_node("../Panel/connect_button").pressed.connect(_connect_pressed)
122 get_node("../Panel/quit_button").connect("pressed", _back_pressed) 122 get_node("../Panel/quit_button").pressed.connect(_back_pressed)
123 123
124 124
125func _connect_pressed(): 125func _connect_pressed():
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index b170c77..46c5456 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd
@@ -6,8 +6,8 @@ var SCRIPT_client
6var SCRIPT_keyboard 6var SCRIPT_keyboard
7var SCRIPT_locationListener 7var SCRIPT_locationListener
8var SCRIPT_minimap 8var SCRIPT_minimap
9var SCRIPT_uuid
10var SCRIPT_victoryListener 9var SCRIPT_victoryListener
10var SCRIPT_websocketserver
11 11
12var ap_server = "" 12var ap_server = ""
13var ap_user = "" 13var ap_user = ""
@@ -92,14 +92,17 @@ func _init():
92 92
93func _ready(): 93func _ready():
94 client = SCRIPT_client.new() 94 client = SCRIPT_client.new()
95 client.SCRIPT_uuid = SCRIPT_uuid 95 client.SCRIPT_websocketserver = SCRIPT_websocketserver
96 96
97 client.connect("item_received", _process_item) 97 client.item_received.connect(_process_item)
98 client.connect("message_received", _process_message) 98 client.location_scout_received.connect(_process_location_scout)
99 client.connect("location_scout_received", _process_location_scout) 99 client.text_message_received.connect(_process_text_message)
100 client.connect("could_not_connect", _client_could_not_connect) 100 client.item_sent_notification.connect(_process_item_sent_notification)
101 client.connect("connect_status", _client_connect_status) 101 client.hint_received.connect(_process_hint_received)
102 client.connect("client_connected", _client_connected) 102
103 client.could_not_connect.connect(_client_could_not_connect)
104 client.connect_status.connect(_client_connect_status)
105 client.client_connected.connect(_client_connected)
103 106
104 add_child(client) 107 add_child(client)
105 108
@@ -163,20 +166,17 @@ func get_item_id_for_door(door_id):
163 return _item_locks.get(door_id, null) 166 return _item_locks.get(door_id, null)
164 167
165 168
166func _process_item(item, index, from, flags, amount): 169func _process_item(item, amount):
167 var item_name = "Unknown"
168 if client._item_id_to_name["Lingo 2"].has(item):
169 item_name = client._item_id_to_name["Lingo 2"][item]
170
171 var gamedata = global.get_node("Gamedata") 170 var gamedata = global.get_node("Gamedata")
172 171
172 var item_id = int(item["id"])
173 var prog_id = null 173 var prog_id = null
174 if _inverse_item_locks.has(item): 174 if _inverse_item_locks.has(item_id):
175 for lock in _inverse_item_locks.get(item): 175 for lock in _inverse_item_locks.get(item_id):
176 if lock[1] != amount: 176 if lock[1] != amount:
177 continue 177 continue
178 178
179 if gamedata.progressive_id_by_ap_id.has(item): 179 if gamedata.progressive_id_by_ap_id.has(item_id):
180 prog_id = lock[0] 180 prog_id = lock[0]
181 181
182 if gamedata.get_door_map_name(lock[0]) != global.map: 182 if gamedata.get_door_map_name(lock[0]) != global.map:
@@ -190,38 +190,35 @@ func _process_item(item, index, from, flags, amount):
190 if rnode != null: 190 if rnode != null:
191 rnode.handleTriggered() 191 rnode.handleTriggered()
192 192
193 var letter_id = gamedata.letter_id_by_ap_id.get(item, null) 193 var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null)
194 if letter_id != null: 194 if letter_id != null:
195 var letter = gamedata.objects.get_letters()[letter_id] 195 var letter = gamedata.objects.get_letters()[letter_id]
196 if not letter.has_level2() or not letter.get_level2(): 196 if not letter.has_level2() or not letter.get_level2():
197 _process_key_item(letter.get_key(), amount) 197 _process_key_item(letter.get_key(), amount)
198 198
199 if gamedata.symbol_item_ids.has(item): 199 if gamedata.symbol_item_ids.has(item_id):
200 var player = get_tree().get_root().get_node_or_null("scene/player") 200 var player = get_tree().get_root().get_node_or_null("scene/player")
201 if player != null: 201 if player != null:
202 player.emit_signal("evaluate_solvability") 202 player.evaluate_solvability.emit()
203 203
204 # Show a message about the item if it's new. 204 # Show a message about the item if it's new.
205 if index != null and index > _last_new_item: 205 if int(item["index"]) > _last_new_item:
206 _last_new_item = index 206 _last_new_item = int(item["index"])
207 saveLocaldata() 207 saveLocaldata()
208 208
209 var player_name = "Unknown" 209 var full_item_name = item["text"]
210 if client._player_name_by_slot.has(float(from)):
211 player_name = client._player_name_by_slot[float(from)]
212
213 var full_item_name = item_name
214 if prog_id != null: 210 if prog_id != null:
215 var door = gamedata.objects.get_doors()[prog_id] 211 var door = gamedata.objects.get_doors()[prog_id]
216 full_item_name = "%s (%s)" % [item_name, door.get_name()] 212 full_item_name = "%s (%s)" % [full_item_name, door.get_name()]
217 213
218 var message 214 var message
219 if from == client._slot: 215 if "sender" in item:
220 message = "Found %s" % wrapInItemColorTags(full_item_name, flags)
221 else:
222 message = ( 216 message = (
223 "Received %s from %s" % [wrapInItemColorTags(full_item_name, flags), player_name] 217 "Received %s from %s"
218 % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]]
224 ) 219 )
220 else:
221 message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"])
225 222
226 if gamedata.anti_trap_ids.has(item): 223 if gamedata.anti_trap_ids.has(item):
227 keyboard.block_letter(gamedata.anti_trap_ids[item]) 224 keyboard.block_letter(gamedata.anti_trap_ids[item])
@@ -231,112 +228,68 @@ func _process_item(item, index, from, flags, amount):
231 global.get_node("Messages").showMessage(message) 228 global.get_node("Messages").showMessage(message)
232 229
233 230
234func _process_message(message): 231func _process_item_sent_notification(message):
235 parse_printjson_for_textclient(message) 232 var sentMsg = (
236 233 "Sent %s to %s"
237 if ( 234 % [
238 !message.has("receiving") 235 wrapInItemColorTags(message["item_name"], message["item_flags"]),
239 or !message.has("item") 236 message["receiver_name"]
240 or message["item"]["player"] != client._slot 237 ]
241 ): 238 )
242 return 239 #if _hinted_locations.has(message["item"]["location"]):
243 240 # sentMsg += " ([color=#fafad2]Hinted![/color])"
244 var item_name = "Unknown" 241 global.get_node("Messages").showMessage(sentMsg)
245 var item_player_game = client._game_by_player[message["receiving"]] 242
246 if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])): 243
247 item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])] 244func _process_hint_received(message):
248 245 var is_for = ""
249 var location_name = "Unknown" 246 if message["self"] == 0:
250 var location_player_game = client._game_by_player[message["item"]["player"]] 247 is_for = " for %s" % message["receiver_name"]
251 if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])): 248
252 location_name = (client._location_id_to_name[location_player_game][int( 249 global.get_node("Messages").showMessage(
253 message["item"]["location"] 250 (
254 )]) 251 "Hint: %s%s is on %s"
255 252 % [
256 var player_name = "Unknown" 253 wrapInItemColorTags(message["item_name"], message["item_flags"]),
257 if client._player_name_by_slot.has(message["receiving"]): 254 is_for,
258 player_name = client._player_name_by_slot[message["receiving"]] 255 message["location_name"]
259 256 ]
260 var item_color = colorForItemType(message["item"]["flags"]) 257 )
261 258 )
262 if message["type"] == "Hint": 259
263 var is_for = "" 260
264 if message["receiving"] != client._slot: 261func _process_text_message(message):
265 is_for = " for %s" % player_name
266 if !message.has("found") || !message["found"]:
267 global.get_node("Messages").showMessage(
268 (
269 "Hint: %s%s is on %s"
270 % [
271 wrapInItemColorTags(item_name, message["item"]["flags"]),
272 is_for,
273 location_name
274 ]
275 )
276 )
277 else:
278 if message["receiving"] != client._slot:
279 var sentMsg = (
280 "Sent %s to %s"
281 % [wrapInItemColorTags(item_name, message["item"]["flags"]), player_name]
282 )
283 #if _hinted_locations.has(message["item"]["location"]):
284 # sentMsg += " ([color=#fafad2]Hinted![/color])"
285 global.get_node("Messages").showMessage(sentMsg)
286
287
288func parse_printjson_for_textclient(message):
289 var parts = [] 262 var parts = []
290 for message_part in message["data"]: 263 for message_part in message:
291 if !message_part.has("type") and message_part.has("text"): 264 if message_part["type"] == "text":
292 parts.append(message_part["text"]) 265 parts.append(message_part["text"])
293 elif message_part["type"] == "player_id": 266 elif message_part["type"] == "player":
294 if int(message_part["text"]) == client._slot: 267 if message_part["self"] == 1:
295 parts.append( 268 parts.append("[color=#ee00ee]%s[/color]" % message_part["text"])
296 "[color=#ee00ee]%s[/color]" % client._player_name_by_slot[client._slot]
297 )
298 else: 269 else:
299 var from = float(message_part["text"]) 270 parts.append("[color=#fafad2]%s[/color]" % message_part["text"])
300 parts.append("[color=#fafad2]%s[/color]" % client._player_name_by_slot[from]) 271 elif message_part["type"] == "item":
301 elif message_part["type"] == "item_id": 272 parts.append(wrapInItemColorTags(message_part["text"], message_part["flags"]))
302 var item_name = "Unknown" 273 elif message_part["type"] == "location":
303 var item_player_game = client._game_by_player[message_part["player"]] 274 parts.append("[color=#00ff7f]%s[/color]" % message_part["text"])
304 if client._item_id_to_name[item_player_game].has(int(message_part["text"])):
305 item_name = client._item_id_to_name[item_player_game][int(message_part["text"])]
306
307 parts.append(wrapInItemColorTags(item_name, message_part["flags"]))
308 elif message_part["type"] == "location_id":
309 var location_name = "Unknown"
310 var location_player_game = client._game_by_player[message_part["player"]]
311 if client._location_id_to_name[location_player_game].has(int(message_part["text"])):
312 location_name = client._location_id_to_name[location_player_game][int(
313 message_part["text"]
314 )]
315
316 parts.append("[color=#00ff7f]%s[/color]" % location_name)
317 elif message_part.has("text"):
318 parts.append(message_part["text"])
319 275
320 var textclient_node = global.get_node("Textclient") 276 var textclient_node = global.get_node("Textclient")
321 if textclient_node != null: 277 if textclient_node != null:
322 textclient_node.parse_printjson("".join(parts)) 278 textclient_node.parse_printjson("".join(parts))
323 279
324 280
325func _process_location_scout(item_id, location_id, player, flags): 281func _process_location_scout(location_id, item_name, player_name, flags, for_self):
326 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags} 282 _location_scouts[location_id] = {
283 "item": item_name, "player": player_name, "flags": flags, "for_self": for_self
284 }
327 285
328 if player == client._slot and flags & 4 != 0: 286 if for_self and flags & 4 != 0:
329 # This is a trap for us, so let's not display it. 287 # This is a trap for us, so let's not display it.
330 return 288 return
331 289
332 var gamedata = global.get_node("Gamedata") 290 var gamedata = global.get_node("Gamedata")
333 var map_id = gamedata.map_id_by_name.get(global.map) 291 var map_id = gamedata.map_id_by_name.get(global.map)
334 292
335 var item_name = "Unknown"
336 var item_player_game = client._game_by_player[float(player)]
337 if client._item_id_to_name[item_player_game].has(item_id):
338 item_name = client._item_id_to_name[item_player_game][item_id]
339
340 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null) 293 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
341 if letter_id != null: 294 if letter_id != null:
342 var letter = gamedata.objects.get_letters()[letter_id] 295 var letter = gamedata.objects.get_letters()[letter_id]
@@ -350,11 +303,11 @@ func _process_location_scout(item_id, location_id, player, flags):
350 303
351 304
352func _client_could_not_connect(message): 305func _client_could_not_connect(message):
353 emit_signal("could_not_connect", message) 306 could_not_connect.emit(message)
354 307
355 308
356func _client_connect_status(message): 309func _client_connect_status(message):
357 emit_signal("connect_status", message) 310 connect_status.emit(message)
358 311
359 312
360func _client_connected(slot_data): 313func _client_connected(slot_data):
@@ -463,7 +416,7 @@ func _client_connected(slot_data):
463 416
464 _inverse_item_locks[lock[0]].append([door_id, lock[1]]) 417 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
465 418
466 emit_signal("ap_connected") 419 ap_connected.emit()
467 420
468 421
469func start_batching_locations(): 422func start_batching_locations():
diff --git a/apworld/client/panel.gd b/apworld/client/panel.gd index fdaaf0e..2cef28e 100644 --- a/apworld/client/panel.gd +++ b/apworld/client/panel.gd
@@ -29,8 +29,8 @@ func _ready():
29 checkSymbolSolvable() 29 checkSymbolSolvable()
30 30
31 if not symbol_solvable: 31 if not symbol_solvable:
32 get_tree().get_root().get_node("scene/player").connect( 32 get_tree().get_root().get_node("scene/player").evaluate_solvability.connect(
33 "evaluate_solvability", evaluateSolvability 33 evaluateSolvability
34 ) 34 )
35 35
36 36
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index e58f1bc..fb88880 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd
@@ -105,20 +105,12 @@ func _ready():
105 != ap.kLETTER_BEHAVIOR_VANILLA 105 != ap.kLETTER_BEHAVIOR_VANILLA
106 ): 106 ):
107 var scout = ap.scout_location(letter.get_ap_id()) 107 var scout = ap.scout_location(letter.get_ap_id())
108 if ( 108 if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0):
109 scout != null 109 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
110 and not (scout["player"] == ap.client._slot and scout["flags"] & 4 != 0) 110 letter.get_path()
111 ): 111 )
112 var item_name = "Unknown" 112 if collectable != null:
113 var item_player_game = ap.client._game_by_player[float(scout["player"])] 113 collectable.setScoutedText.call_deferred(scout["item"])
114 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
115 item_name = ap.client._item_id_to_name[item_player_game][scout["item"]]
116
117 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
118 letter.get_path()
119 )
120 if collectable != null:
121 collectable.setScoutedText.call_deferred(item_name)
122 114
123 # Set up mastery locations. 115 # Set up mastery locations.
124 for mastery in gamedata.objects.get_masteries(): 116 for mastery in gamedata.objects.get_masteries():
diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 26831b4..9841063 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd
@@ -47,7 +47,7 @@ func _ready():
47 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1)) 47 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
48 entry.add_theme_stylebox_override("focus", entry_style) 48 entry.add_theme_stylebox_override("focus", entry_style)
49 panel.add_child(entry) 49 panel.add_child(entry)
50 entry.connect("text_submitted", text_entered) 50 entry.text_submitted.connect(text_entered)
51 51
52 52
53func _input(event): 53func _input(event):
diff --git a/apworld/client/vendor/LICENSE b/apworld/client/vendor/LICENSE index 115ba15..12763b1 100644 --- a/apworld/client/vendor/LICENSE +++ b/apworld/client/vendor/LICENSE
@@ -1,21 +1,21 @@
1MIT License 1WebSocketServer.gd:
2 2
3Copyright (c) 2023 Xavier Sellier 3Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014
4Juan Linietsky, Ariel Manzur.
4 5
5Permission is hereby granted, free of charge, to any person obtaining a copy 6Permission is hereby granted, free of charge, to any person obtaining a copy of
6of this software and associated documentation files (the "Software"), to deal 7this software and associated documentation files (the "Software"), to deal in
7in the Software without restriction, including without limitation the rights 8the Software without restriction, including without limitation the rights to
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9copies of the Software, and to permit persons to whom the Software is 10the Software, and to permit persons to whom the Software is furnished to do so,
10furnished to do so, subject to the following conditions: 11subject to the following conditions:
11 12
12The above copyright notice and this permission notice shall be included in all 13The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software. 14copies or substantial portions of the Software.
14 15
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21SOFTWARE. \ No newline at end of file
diff --git a/apworld/client/vendor/WebSocketServer.gd b/apworld/client/vendor/WebSocketServer.gd new file mode 100644 index 0000000..2cee494 --- /dev/null +++ b/apworld/client/vendor/WebSocketServer.gd
@@ -0,0 +1,173 @@
1class_name WebSocketServer
2extends Node
3
4signal message_received(peer_id: int, message: String)
5signal client_connected(peer_id: int)
6signal client_disconnected(peer_id: int)
7
8@export var handshake_headers := PackedStringArray()
9@export var supported_protocols := PackedStringArray()
10@export var handshake_timout := 3000
11@export var use_tls := false
12@export var tls_cert: X509Certificate
13@export var tls_key: CryptoKey
14@export var refuse_new_connections := false:
15 set(refuse):
16 if refuse:
17 pending_peers.clear()
18
19
20class PendingPeer:
21 var connect_time: int
22 var tcp: StreamPeerTCP
23 var connection: StreamPeer
24 var ws: WebSocketPeer
25
26 func _init(p_tcp: StreamPeerTCP) -> void:
27 tcp = p_tcp
28 connection = p_tcp
29 connect_time = Time.get_ticks_msec()
30
31
32var tcp_server := TCPServer.new()
33var pending_peers: Array[PendingPeer] = []
34var peers: Dictionary
35
36
37func listen(port: int) -> int:
38 assert(not tcp_server.is_listening())
39 return tcp_server.listen(port)
40
41
42func stop() -> void:
43 tcp_server.stop()
44 pending_peers.clear()
45 peers.clear()
46
47
48func send(peer_id: int, message: String) -> int:
49 var type := typeof(message)
50 if peer_id <= 0:
51 # Send to multiple peers, (zero = broadcast, negative = exclude one).
52 for id: int in peers:
53 if id == -peer_id:
54 continue
55 if type == TYPE_STRING:
56 peers[id].send_text(message)
57 else:
58 peers[id].put_packet(message)
59 return OK
60
61 assert(peers.has(peer_id))
62 var socket: WebSocketPeer = peers[peer_id]
63 if type == TYPE_STRING:
64 return socket.send_text(message)
65 return socket.send(var_to_bytes(message))
66
67
68func get_message(peer_id: int) -> Variant:
69 assert(peers.has(peer_id))
70 var socket: WebSocketPeer = peers[peer_id]
71 if socket.get_available_packet_count() < 1:
72 return null
73 var pkt: PackedByteArray = socket.get_packet()
74 if socket.was_string_packet():
75 return pkt.get_string_from_utf8()
76 return bytes_to_var(pkt)
77
78
79func has_message(peer_id: int) -> bool:
80 assert(peers.has(peer_id))
81 return peers[peer_id].get_available_packet_count() > 0
82
83
84func _create_peer() -> WebSocketPeer:
85 var ws := WebSocketPeer.new()
86 ws.supported_protocols = supported_protocols
87 ws.handshake_headers = handshake_headers
88 return ws
89
90
91func poll() -> void:
92 if not tcp_server.is_listening():
93 return
94
95 while not refuse_new_connections and tcp_server.is_connection_available():
96 var conn: StreamPeerTCP = tcp_server.take_connection()
97 assert(conn != null)
98 pending_peers.append(PendingPeer.new(conn))
99
100 var to_remove := []
101
102 for p in pending_peers:
103 if not _connect_pending(p):
104 if p.connect_time + handshake_timout < Time.get_ticks_msec():
105 # Timeout.
106 to_remove.append(p)
107 continue # Still pending.
108
109 to_remove.append(p)
110
111 for r: RefCounted in to_remove:
112 pending_peers.erase(r)
113
114 to_remove.clear()
115
116 for id: int in peers:
117 var p: WebSocketPeer = peers[id]
118 p.poll()
119
120 if p.get_ready_state() != WebSocketPeer.STATE_OPEN:
121 client_disconnected.emit(id)
122 to_remove.append(id)
123 continue
124
125 while p.get_available_packet_count():
126 message_received.emit(id, get_message(id))
127
128 for r: int in to_remove:
129 peers.erase(r)
130 to_remove.clear()
131
132
133func _connect_pending(p: PendingPeer) -> bool:
134 if p.ws != null:
135 # Poll websocket client if doing handshake.
136 p.ws.poll()
137 var state := p.ws.get_ready_state()
138 if state == WebSocketPeer.STATE_OPEN:
139 var id := randi_range(2, 1 << 30)
140 peers[id] = p.ws
141 client_connected.emit(id)
142 return true # Success.
143 elif state != WebSocketPeer.STATE_CONNECTING:
144 return true # Failure.
145 return false # Still connecting.
146 elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED:
147 return true # TCP disconnected.
148 elif not use_tls:
149 # TCP is ready, create WS peer.
150 p.ws = _create_peer()
151 p.ws.accept_stream(p.tcp)
152 return false # WebSocketPeer connection is pending.
153
154 else:
155 if p.connection == p.tcp:
156 assert(tls_key != null and tls_cert != null)
157 var tls := StreamPeerTLS.new()
158 tls.accept_stream(p.tcp, TLSOptions.server(tls_key, tls_cert))
159 p.connection = tls
160 p.connection.poll()
161 var status: StreamPeerTLS.Status = p.connection.get_status()
162 if status == StreamPeerTLS.STATUS_CONNECTED:
163 p.ws = _create_peer()
164 p.ws.accept_stream(p.connection)
165 return false # WebSocketPeer connection is pending.
166 if status != StreamPeerTLS.STATUS_HANDSHAKING:
167 return true # Failure.
168
169 return false
170
171
172func _process(_delta: float) -> void:
173 poll()
diff --git a/apworld/client/vendor/uuid.gd b/apworld/client/vendor/uuid.gd deleted file mode 100644 index b63fa04..0000000 --- a/apworld/client/vendor/uuid.gd +++ /dev/null
@@ -1,195 +0,0 @@
1# Note: The code might not be as pretty it could be, since it's written
2# in a way that maximizes performance. Methods are inlined and loops are avoided.
3extends Node
4
5const BYTE_MASK: int = 0b11111111
6
7
8static func uuidbin():
9 randomize()
10 # 16 random bytes with the bytes on index 6 and 8 modified
11 return [
12 randi() & BYTE_MASK,
13 randi() & BYTE_MASK,
14 randi() & BYTE_MASK,
15 randi() & BYTE_MASK,
16 randi() & BYTE_MASK,
17 randi() & BYTE_MASK,
18 ((randi() & BYTE_MASK) & 0x0f) | 0x40,
19 randi() & BYTE_MASK,
20 ((randi() & BYTE_MASK) & 0x3f) | 0x80,
21 randi() & BYTE_MASK,
22 randi() & BYTE_MASK,
23 randi() & BYTE_MASK,
24 randi() & BYTE_MASK,
25 randi() & BYTE_MASK,
26 randi() & BYTE_MASK,
27 randi() & BYTE_MASK,
28 ]
29
30
31static func uuidbinrng(rng: RandomNumberGenerator):
32 rng.randomize()
33 return [
34 rng.randi() & BYTE_MASK,
35 rng.randi() & BYTE_MASK,
36 rng.randi() & BYTE_MASK,
37 rng.randi() & BYTE_MASK,
38 rng.randi() & BYTE_MASK,
39 rng.randi() & BYTE_MASK,
40 ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40,
41 rng.randi() & BYTE_MASK,
42 ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80,
43 rng.randi() & BYTE_MASK,
44 rng.randi() & BYTE_MASK,
45 rng.randi() & BYTE_MASK,
46 rng.randi() & BYTE_MASK,
47 rng.randi() & BYTE_MASK,
48 rng.randi() & BYTE_MASK,
49 rng.randi() & BYTE_MASK,
50 ]
51
52
53static func v4():
54 # 16 random bytes with the bytes on index 6 and 8 modified
55 var b = uuidbin()
56
57 return (
58 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
59 % [
60 # low
61 b[0],
62 b[1],
63 b[2],
64 b[3],
65 # mid
66 b[4],
67 b[5],
68 # hi
69 b[6],
70 b[7],
71 # clock
72 b[8],
73 b[9],
74 # clock
75 b[10],
76 b[11],
77 b[12],
78 b[13],
79 b[14],
80 b[15]
81 ]
82 )
83
84
85static func v4_rng(rng: RandomNumberGenerator):
86 # 16 random bytes with the bytes on index 6 and 8 modified
87 var b = uuidbinrng(rng)
88
89 return (
90 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
91 % [
92 # low
93 b[0],
94 b[1],
95 b[2],
96 b[3],
97 # mid
98 b[4],
99 b[5],
100 # hi
101 b[6],
102 b[7],
103 # clock
104 b[8],
105 b[9],
106 # clock
107 b[10],
108 b[11],
109 b[12],
110 b[13],
111 b[14],
112 b[15]
113 ]
114 )
115
116
117var _uuid: Array
118
119
120func _init(rng := RandomNumberGenerator.new()) -> void:
121 _uuid = uuidbinrng(rng)
122
123
124func as_array() -> Array:
125 return _uuid.duplicate()
126
127
128func as_dict(big_endian := true) -> Dictionary:
129 if big_endian:
130 return {
131 "low": (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8) + _uuid[3],
132 "mid": (_uuid[4] << 8) + _uuid[5],
133 "hi": (_uuid[6] << 8) + _uuid[7],
134 "clock": (_uuid[8] << 8) + _uuid[9],
135 "node":
136 (
137 (_uuid[10] << 40)
138 + (_uuid[11] << 32)
139 + (_uuid[12] << 24)
140 + (_uuid[13] << 16)
141 + (_uuid[14] << 8)
142 + _uuid[15]
143 )
144 }
145 else:
146 return {
147 "low": _uuid[0] + (_uuid[1] << 8) + (_uuid[2] << 16) + (_uuid[3] << 24),
148 "mid": _uuid[4] + (_uuid[5] << 8),
149 "hi": _uuid[6] + (_uuid[7] << 8),
150 "clock": _uuid[8] + (_uuid[9] << 8),
151 "node":
152 (
153 _uuid[10]
154 + (_uuid[11] << 8)
155 + (_uuid[12] << 16)
156 + (_uuid[13] << 24)
157 + (_uuid[14] << 32)
158 + (_uuid[15] << 40)
159 )
160 }
161
162
163func as_string() -> String:
164 return (
165 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
166 % [
167 # low
168 _uuid[0],
169 _uuid[1],
170 _uuid[2],
171 _uuid[3],
172 # mid
173 _uuid[4],
174 _uuid[5],
175 # hi
176 _uuid[6],
177 _uuid[7],
178 # clock
179 _uuid[8],
180 _uuid[9],
181 # node
182 _uuid[10],
183 _uuid[11],
184 _uuid[12],
185 _uuid[13],
186 _uuid[14],
187 _uuid[15]
188 ]
189 )
190
191
192func is_equal(other) -> bool:
193 # Godot Engine compares Array recursively
194 # There's no need for custom comparison here.
195 return _uuid == other._uuid