summary refs log tree commit diff stats
path: root/client/Archipelago/client.gd
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-08-28 14:25:50 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-08-28 14:25:50 -0400
commit6fd6d493cd16b41bf88742ff6f4b7635ec3fa67c (patch)
tree92bc0bc1559e550e48b507c0925d28f93962d06a /client/Archipelago/client.gd
parentaaeb0a4074a58c906b26dac6ee610266062d88be (diff)
downloadlingo2-archipelago-6fd6d493cd16b41bf88742ff6f4b7635ec3fa67c.tar.gz
lingo2-archipelago-6fd6d493cd16b41bf88742ff6f4b7635ec3fa67c.tar.bz2
lingo2-archipelago-6fd6d493cd16b41bf88742ff6f4b7635ec3fa67c.zip
Client is starting to work!
Diffstat (limited to 'client/Archipelago/client.gd')
-rw-r--r--client/Archipelago/client.gd378
1 files changed, 378 insertions, 0 deletions
diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd new file mode 100644 index 0000000..5c4bc51 --- /dev/null +++ b/client/Archipelago/client.gd
@@ -0,0 +1,378 @@
1extends Node
2
3const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"}
4
5var SCRIPT_uuid
6
7var _ws = WebSocketPeer.new()
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
20var _remote_version = {"major": 0, "minor": 0, "build": 0}
21var _gen_version = {"major": 0, "minor": 0, "build": 0}
22
23var ap_server = ""
24var ap_user = ""
25var ap_pass = ""
26
27var _authenticated = false
28var _seed = ""
29var _team = 0
30var _slot = 0
31var _players = []
32var _player_name_by_slot = {}
33var _game_by_player = {}
34var _checked_locations = []
35var _received_items = []
36var _slot_data = {}
37
38signal could_not_connect
39signal connect_status
40signal client_connected
41signal item_received(item_id, index, player, flags)
42signal message_received(message)
43
44
45func _init():
46 global._print("Instantiated APClient")
47
48 # Read AP settings from file, if there are any
49 if FileAccess.file_exists("user://ap_datapackages"):
50 var file = FileAccess.open("user://ap_datapackages", FileAccess.READ)
51 var data = file.get_var(true)
52 file.close()
53
54 if typeof(data) != TYPE_DICTIONARY:
55 global._print("AP datapackages file is corrupted")
56 data = {}
57
58 _datapackages = data
59
60 processDatapackages()
61
62
63func _ready():
64 pass
65 #_ws.connect("connection_closed", _closed)
66 #_ws.connect("connection_failed", _closed)
67 #_ws.connect("server_disconnected", _closed)
68 #_ws.connect("connection_error", _errored)
69 #_ws.connect("connection_established", _connected)
70
71
72func _reset_state():
73 _should_process = false
74 _authenticated = false
75 _try_wss = false
76 _has_connected = false
77
78
79func _errored():
80 if _try_wss:
81 global._print("Could not connect to AP with ws://, now trying wss://")
82 connectToServer(ap_server, ap_user, ap_pass)
83 else:
84 global._print("AP connection failed")
85 _reset_state()
86
87 emit_signal(
88 "could_not_connect",
89 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information."
90 )
91
92
93func _closed(_was_clean = true):
94 global._print("Connection closed")
95 _reset_state()
96
97 if not _initiated_disconnect:
98 emit_signal("could_not_connect", "Disconnected from Archipelago")
99
100 _initiated_disconnect = false
101
102
103func _connected(_proto = ""):
104 global._print("Connected!")
105 _try_wss = false
106
107
108func disconnect_from_ap():
109 _initiated_disconnect = true
110 _ws.close()
111
112
113func _process(_delta):
114 if _should_process:
115 _ws.poll()
116
117 var state = _ws.get_ready_state()
118 if state == WebSocketPeer.STATE_OPEN:
119 if not _has_connected:
120 _has_connected = true
121
122 _connected()
123
124 while _ws.get_available_packet_count():
125 var packet = _ws.get_packet()
126 global._print("Got data from server: " + packet.get_string_from_utf8())
127 var json = JSON.new()
128 var jserror = json.parse(packet.get_string_from_utf8())
129 if jserror != OK:
130 global._print("Error parsing packet from AP: " + jserror.error_string)
131 return
132
133 for message in json.data:
134 var cmd = message["cmd"]
135 global._print("Received command: " + cmd)
136
137 if cmd == "RoomInfo":
138 _seed = message["seed_name"]
139 _remote_version = message["version"]
140 _gen_version = message["generator_version"]
141
142 var needed_games = []
143 for game in message["datapackage_checksums"].keys():
144 if (
145 !_datapackages.has(game)
146 or (
147 _datapackages[game]["checksum"]
148 != message["datapackage_checksums"][game]
149 )
150 ):
151 needed_games.append(game)
152
153 if !needed_games.is_empty():
154 _pending_packages = needed_games
155 var cur_needed = _pending_packages.pop_front()
156 requestDatapackages([cur_needed])
157 else:
158 connectToRoom()
159
160 elif cmd == "DataPackage":
161 for game in message["data"]["games"].keys():
162 _datapackages[game] = message["data"]["games"][game]
163 saveDatapackages()
164
165 if !_pending_packages.is_empty():
166 var cur_needed = _pending_packages.pop_front()
167 requestDatapackages([cur_needed])
168 else:
169 processDatapackages()
170 connectToRoom()
171
172 elif cmd == "Connected":
173 _authenticated = true
174 _team = message["team"]
175 _slot = message["slot"]
176 _players = message["players"]
177 _checked_locations = message["checked_locations"]
178 _slot_data = message["slot_data"]
179
180 for player in _players:
181 _player_name_by_slot[player["slot"]] = player["alias"]
182 _game_by_player[player["slot"]] = message["slot_info"][str(
183 player["slot"]
184 )]["game"]
185
186 emit_signal("client_connected")
187
188 elif cmd == "ConnectionRefused":
189 var error_message = ""
190 for error in message["errors"]:
191 var submsg = ""
192 if error == "InvalidSlot":
193 submsg = "Invalid player name."
194 elif error == "InvalidGame":
195 submsg = "The specified player is not playing Lingo."
196 elif error == "IncompatibleVersion":
197 submsg = (
198 "The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d."
199 % [
200 ap_version["major"],
201 ap_version["minor"],
202 ap_version["build"],
203 _remote_version["major"],
204 _remote_version["minor"],
205 _remote_version["build"]
206 ]
207 )
208 elif error == "InvalidPassword":
209 submsg = "Incorrect password."
210 elif error == "InvalidItemsHandling":
211 submsg = "Invalid item handling flag. This is a bug with the client."
212
213 if submsg != "":
214 if error_message != "":
215 error_message += " "
216 error_message += submsg
217
218 if error_message == "":
219 error_message = "Unknown error."
220
221 _initiated_disconnect = true
222 _ws.disconnect_from_host()
223
224 emit_signal("could_not_connect", error_message)
225 global._print("Connection to AP refused")
226 global._print(message)
227
228 elif cmd == "ReceivedItems":
229 var i = 0
230 for item in message["items"]:
231 if not _received_items.has(int(item["item"])):
232 _received_items.append(int(item["item"]))
233
234 emit_signal(
235 "item_received",
236 int(item["item"]),
237 int(message["index"]) + i,
238 int(item["player"]),
239 int(item["flags"])
240 )
241 i += 1
242
243 elif cmd == "PrintJSON":
244 emit_signal("message_received", message)
245
246 elif state == WebSocketPeer.STATE_CLOSED:
247 if _has_connected:
248 _closed()
249 else:
250 _errored()
251
252
253func saveDatapackages():
254 # Save the AP datapackages to disk.
255 var file = FileAccess.open("user://ap_datapackages", FileAccess.WRITE)
256 file.store_var(_datapackages, true)
257 file.close()
258
259
260func connectToServer(server, un, pw):
261 ap_server = server
262 ap_user = un
263 ap_pass = pw
264
265 _initiated_disconnect = false
266
267 var url = ""
268 if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"):
269 url = ap_server
270 _try_wss = false
271 elif _try_wss:
272 url = "wss://" + ap_server
273 _try_wss = false
274 else:
275 url = "ws://" + ap_server
276 _try_wss = true
277
278 var err = _ws.connect_to_url(url)
279 if err != OK:
280 emit_signal(
281 "could_not_connect",
282 (
283 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d."
284 % err
285 )
286 )
287 global._print("Could not connect to AP: " + err)
288 return
289 _should_process = true
290
291 emit_signal("connect_status", "Connecting...")
292
293
294func sendMessage(msg):
295 var payload = JSON.stringify(msg)
296 _ws.send_text(payload)
297
298
299func requestDatapackages(games):
300 emit_signal("connect_status", "Downloading %s data package..." % games[0])
301
302 sendMessage([{"cmd": "GetDataPackage", "games": games}])
303
304
305func processDatapackages():
306 _item_id_to_name = {}
307 _location_id_to_name = {}
308 for game in _datapackages.keys():
309 var package = _datapackages[game]
310
311 _item_id_to_name[game] = {}
312 for item_name in package["item_name_to_id"].keys():
313 _item_id_to_name[game][package["item_name_to_id"][item_name]] = item_name
314
315 _location_id_to_name[game] = {}
316 for location_name in package["location_name_to_id"].keys():
317 _location_id_to_name[game][package["location_name_to_id"][location_name]] = location_name
318
319 if _datapackages.has("Lingo 2"):
320 _item_name_to_id = _datapackages["Lingo 2"]["item_name_to_id"]
321 _location_name_to_id = _datapackages["Lingo 2"]["location_name_to_id"]
322
323
324func connectToRoom():
325 emit_signal("connect_status", "Authenticating...")
326
327 sendMessage(
328 [
329 {
330 "cmd": "Connect",
331 "password": ap_pass,
332 "game": "Lingo 2",
333 "name": ap_user,
334 "uuid": SCRIPT_uuid.v4(),
335 "version": ap_version,
336 "items_handling": 0b111, # always receive our items
337 "tags": [],
338 "slot_data": true
339 }
340 ]
341 )
342
343
344func sendConnectUpdate(tags):
345 sendMessage([{"cmd": "ConnectUpdate", "tags": tags}])
346
347
348func requestSync():
349 sendMessage([{"cmd": "Sync"}])
350
351
352func sendLocation(loc_id):
353 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
354
355
356func setValue(key, value, operation = "replace"):
357 sendMessage(
358 [
359 {
360 "cmd": "Set",
361 "key": "Lingo2_%d_%s" % [_slot, key],
362 "want_reply": false,
363 "operations": [{"operation": operation, "value": value}]
364 }
365 ]
366 )
367
368
369func say(textdata):
370 sendMessage([{"cmd": "Say", "text": textdata}])
371
372
373func completedGoal():
374 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
375
376
377func hasItem(item_id):
378 return _received_items.has(item_id)