about summary refs log tree commit diff stats
path: root/apworld/client/client.gd
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/client/client.gd')
-rw-r--r--apworld/client/client.gd381
1 files changed, 88 insertions, 293 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