summary refs log tree commit diff stats
path: root/client/Archipelago
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
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')
-rw-r--r--client/Archipelago/client.gd378
-rw-r--r--client/Archipelago/door.gd38
-rw-r--r--client/Archipelago/gamedata.gd78
-rw-r--r--client/Archipelago/locationListener.gd20
-rw-r--r--client/Archipelago/manager.gd100
-rw-r--r--client/Archipelago/painting.gd38
-rw-r--r--client/Archipelago/player.gd42
-rw-r--r--client/Archipelago/settings_buttons.gd24
-rw-r--r--client/Archipelago/settings_screen.gd173
-rw-r--r--client/Archipelago/vendor/LICENSE21
-rw-r--r--client/Archipelago/vendor/uuid.gd195
11 files changed, 1107 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)
diff --git a/client/Archipelago/door.gd b/client/Archipelago/door.gd new file mode 100644 index 0000000..731eca4 --- /dev/null +++ b/client/Archipelago/door.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/door.gd"
2
3var item_id
4
5
6func _ready():
7 var node_path = String(
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 )
10
11 print("node: %s" % node_path)
12
13 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 var ap = global.get_node("Archipelago")
19 item_id = ap.get_item_id_for_door(door_id)
20
21 if item_id != null:
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.has_item(item_id):
38 handleTriggered()
diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd new file mode 100644 index 0000000..16368a9 --- /dev/null +++ b/client/Archipelago/gamedata.gd
@@ -0,0 +1,78 @@
1extends Node
2
3var SCRIPT_proto
4
5var objects
6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {}
8var door_id_by_ap_id = {}
9var map_id_by_name = {}
10
11
12func _init(proto_script):
13 SCRIPT_proto = proto_script
14
15
16func load(data_bytes):
17 objects = SCRIPT_proto.AllObjects.new()
18
19 var result_code = objects.from_bytes(data_bytes)
20 if result_code != SCRIPT_proto.PB_ERR.NO_ERRORS:
21 print("Could not load generated data: %d" % result_code)
22 return
23
24 for map in objects.get_maps():
25 map_id_by_name[map.get_name()] = map.get_id()
26
27 for door in objects.get_doors():
28 var map = objects.get_maps()[door.get_map_id()]
29
30 if not map.get_name() in door_id_by_map_node_path:
31 door_id_by_map_node_path[map.get_name()] = {}
32
33 var map_data = door_id_by_map_node_path[map.get_name()]
34 for receiver in door.get_receivers():
35 map_data[receiver] = door.get_id()
36
37 for painting_id in door.get_move_paintings():
38 var painting = objects.get_paintings()[painting_id]
39 map_data[painting.get_path()] = door.get_id()
40
41 if door.has_ap_id():
42 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
43
44 for painting in objects.get_paintings():
45 var room = objects.get_rooms()[painting.get_room_id()]
46 var map = objects.get_maps()[room.get_map_id()]
47
48 if not map.get_name() in painting_id_by_map_node_path:
49 painting_id_by_map_node_path[map.get_name()] = {}
50
51 var _map_data = painting_id_by_map_node_path[map.get_name()]
52
53
54func get_door_for_map_node_path(map_name, node_path):
55 if not door_id_by_map_node_path.has(map_name):
56 return null
57
58 var map_data = door_id_by_map_node_path[map_name]
59 return map_data.get(node_path, null)
60
61
62func get_door_ap_id(door_id):
63 var door = objects.get_doors()[door_id]
64 if door.has_ap_id():
65 return door.get_ap_id()
66 else:
67 return null
68
69
70func get_door_receivers(door_id):
71 var door = objects.get_doors()[door_id]
72 return door.get_receivers()
73
74
75func get_door_map_name(door_id):
76 var door = objects.get_doors()[door_id]
77 var map = objects.get_maps()[door.get_map_id()]
78 return map.get_name()
diff --git a/client/Archipelago/locationListener.gd b/client/Archipelago/locationListener.gd new file mode 100644 index 0000000..71792ed --- /dev/null +++ b/client/Archipelago/locationListener.gd
@@ -0,0 +1,20 @@
1extends Receiver
2
3var location_id
4
5
6func _ready():
7 super._ready()
8
9
10func handleTriggered():
11 triggered += 1
12 if triggered >= total:
13 var ap = global.get_node("Archipelago")
14 ap.send_location(location_id)
15
16
17func handleUntriggered():
18 triggered -= 1
19 if triggered < total:
20 pass
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd new file mode 100644 index 0000000..35fc8e3 --- /dev/null +++ b/client/Archipelago/manager.gd
@@ -0,0 +1,100 @@
1extends Node
2
3const my_version = "0.1.0"
4
5var SCRIPT_client
6var SCRIPT_locationListener
7var SCRIPT_uuid
8
9var ap_server = ""
10var ap_user = ""
11var ap_pass = ""
12var connection_history = []
13
14var client
15
16signal could_not_connect
17signal connect_status
18signal ap_connected
19
20
21func _ready():
22 client = SCRIPT_client.new()
23 client.SCRIPT_uuid = SCRIPT_uuid
24
25 client.connect("item_received", _process_item)
26 client.connect("could_not_connect", _client_could_not_connect)
27 client.connect("connect_status", _client_connect_status)
28 client.connect("client_connected", _client_connected)
29
30 add_child(client)
31
32
33func saveSettings():
34 pass
35
36
37func connectToServer():
38 client.connectToServer(ap_server, ap_user, ap_pass)
39
40
41func getSaveFileName():
42 return "zzAP_%s_%d" % [client._seed, client._slot]
43
44
45func disconnect_from_ap():
46 client.disconnect_from_ap()
47
48
49func get_item_id_for_door(door_id):
50 var gamedata = global.get_node("Gamedata")
51 var door = gamedata.objects.get_doors()[door_id]
52 if (
53 door.get_type() == gamedata.SCRIPT_proto.DoorType.EVENT
54 or door.get_type() == gamedata.SCRIPT_proto.DoorType.LOCATION_ONLY
55 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
56 ):
57 return null
58 return gamedata.get_door_ap_id(door_id)
59
60
61func has_item(item_id):
62 return client.hasItem(item_id)
63
64
65func _process_item(item_id, _index, _player, _flags):
66 var gamedata = global.get_node("Gamedata")
67 var door_id = gamedata.door_id_by_ap_id.get(item_id, null)
68 if door_id != null and gamedata.get_door_map_name(door_id) == global.map:
69 var receivers = gamedata.get_door_receivers(door_id)
70 var scene = get_tree().get_root().get_node_or_null("scene")
71 if scene != null:
72 for receiver in receivers:
73 var rnode = scene.get_node_or_null(receiver)
74 if rnode != null:
75 rnode.handleTriggered()
76 for painting_id in gamedata.objects.get_doors()[door_id].get_move_paintings():
77 var painting = gamedata.objects.get_paintings()[painting_id]
78 var pnode = scene.get_node_or_null(painting.get_path() + "/teleportListener")
79 if pnode != null:
80 pnode.handleTriggered()
81
82
83func _process_message(_message):
84 pass
85
86
87func _client_could_not_connect():
88 emit_signal("could_not_connect")
89
90
91func _client_connect_status(message):
92 emit_signal("connect_status", message)
93
94
95func _client_connected():
96 emit_signal("ap_connected")
97
98
99func send_location(loc_id):
100 client.sendLocation(loc_id)
diff --git a/client/Archipelago/painting.gd b/client/Archipelago/painting.gd new file mode 100644 index 0000000..17baeb5 --- /dev/null +++ b/client/Archipelago/painting.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/painting.gd"
2
3var item_id
4
5
6func _ready():
7 var node_path = String(
8 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
9 )
10
11 print("node: %s" % node_path)
12
13 var gamedata = global.get_node("Gamedata")
14 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
15 if door_id != null:
16 print("door_id: %d" % door_id)
17
18 self.senders = []
19 self.senderGroup = []
20 self.nested = false
21 self.complete_at = 0
22 self.max_length = 0
23 self.excludeSenders = []
24
25 var ap = global.get_node("Archipelago")
26 item_id = ap.get_item_id_for_door(door_id)
27
28 if item_id != null:
29 call_deferred("_readier")
30
31 super._ready()
32
33
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.has_item(item_id):
38 $teleportListener.handleTriggered()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd new file mode 100644 index 0000000..a84548a --- /dev/null +++ b/client/Archipelago/player.gd
@@ -0,0 +1,42 @@
1extends "res://scripts/nodes/player.gd"
2
3
4func _ready():
5 var ap = global.get_node("Archipelago")
6 var gamedata = global.get_node("Gamedata")
7
8 var map_id = gamedata.map_id_by_name.get(global.map)
9 for door in gamedata.objects.get_doors():
10 if door.get_map_id() != map_id:
11 continue
12
13 if not door.has_ap_id():
14 continue
15
16 if door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY:
17 continue
18
19 var locationListener = ap.SCRIPT_locationListener.new()
20 locationListener.location_id = door.get_ap_id()
21 locationListener.name = "locationListener_%d" % door.get_ap_id()
22
23 for panel_ref in door.get_panels():
24 # TODO: specific answers
25 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
26 locationListener.senders.append(NodePath("/root/scene/" + panel_data.get_path()))
27
28 get_parent().add_child.call_deferred(locationListener)
29
30 for letter in gamedata.objects.get_letters():
31 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
32 if room.get_map_id() != map_id:
33 continue
34
35 var locationListener = ap.SCRIPT_locationListener.new()
36 locationListener.location_id = letter.get_ap_id()
37 locationListener.name = "locationListener_%d" % letter.get_ap_id()
38 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
39
40 get_parent().add_child.call_deferred(locationListener)
41
42 super._ready()
diff --git a/client/Archipelago/settings_buttons.gd b/client/Archipelago/settings_buttons.gd new file mode 100644 index 0000000..9e61cb0 --- /dev/null +++ b/client/Archipelago/settings_buttons.gd
@@ -0,0 +1,24 @@
1extends Button
2
3
4func _ready():
5 pass
6
7
8func _connect_pressed():
9 self.disabled = true
10
11 var ap = global.get_node("Archipelago")
12 ap.ap_server = self.get_parent().get_node("server_box").text
13 ap.ap_user = self.get_parent().get_node("player_box").text
14 ap.ap_pass = self.get_parent().get_node("password_box").text
15 ap.saveSettings()
16
17 ap.connectToServer()
18
19
20func _back_pressed():
21 var ap = global.get_node("Archipelago")
22 ap.disconnect_from_ap()
23
24 get_tree().change_scene_to_file("res://objects/scenes/menus/main_menu.tscn")
diff --git a/client/Archipelago/settings_screen.gd b/client/Archipelago/settings_screen.gd new file mode 100644 index 0000000..90d5437 --- /dev/null +++ b/client/Archipelago/settings_screen.gd
@@ -0,0 +1,173 @@
1extends Node2D
2
3
4func _ready():
5 # Some helpful logging.
6 if Steam.isSubscribed():
7 global._print("Provisioning successful! Build ID: %d" % Steam.getAppBuildId())
8 else:
9 global._print("Provisioning failed.")
10
11 # Undo the load screen removing our cursor
12 get_tree().get_root().set_disable_input(false)
13 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
14
15 # Increase the WebSocket input buffer size so that we can download large
16 # data packages.
17 ProjectSettings.set_setting("network/limits/websocket_client/max_in_buffer_kb", 8192)
18
19 # Create the global AP manager, if it doesn't already exist.
20 if not global.has_node("Archipelago"):
21 var ap_script = ResourceLoader.load("user://maps/Archipelago/manager.gd")
22 var ap_instance = ap_script.new()
23 ap_instance.name = "Archipelago"
24
25 #apclient_instance.SCRIPT_doorControl = load("user://maps/Archipelago/doorControl.gd")
26 #apclient_instance.SCRIPT_effects = load("user://maps/Archipelago/effects.gd")
27 #apclient_instance.SCRIPT_location = load("user://maps/Archipelago/location.gd")
28 #apclient_instance.SCRIPT_mypainting = load("user://maps/Archipelago/mypainting.gd")
29 #apclient_instance.SCRIPT_panel = load("user://maps/Archipelago/panel.gd")
30 #apclient_instance.SCRIPT_textclient = load("user://maps/Archipelago/textclient.gd")
31
32 ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd")
33 ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd")
34 ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd")
35
36 global.add_child(ap_instance)
37
38 # Let's also inject any scripts we need to inject now.
39 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd"))
40 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
41 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
42
43 var proto_script = load("user://maps/Archipelago/generated/proto.gd")
44 var gamedata_script = load("user://maps/Archipelago/gamedata.gd")
45 var gamedata_instance = gamedata_script.new(proto_script)
46 gamedata_instance.load(
47 FileAccess.get_file_as_bytes("user://maps/Archipelago/generated/data.binpb")
48 )
49 gamedata_instance.name = "Gamedata"
50 global.add_child(gamedata_instance)
51
52 var ap = global.get_node("Archipelago")
53 ap.connect("ap_connected", connectionSuccessful)
54 ap.connect("could_not_connect", connectionUnsuccessful)
55 ap.connect("connect_status", connectionStatus)
56
57 # Populate textboxes with AP settings.
58 $Panel/server_box.text = ap.ap_server
59 $Panel/player_box.text = ap.ap_user
60 $Panel/password_box.text = ap.ap_pass
61
62 var history_box = $Panel/connection_history
63 if ap.connection_history.is_empty():
64 history_box.disabled = true
65 else:
66 history_box.disabled = false
67
68 var i = 0
69 for details in ap.connection_history:
70 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
71 i += 1
72
73 history_box.get_popup().connect("id_pressed", historySelected)
74
75 # Show client version.
76 $Panel/title.text = "ARCHIPELAGO (%s)" % ap.my_version
77
78 # Increase font size in text boxes.
79 $Panel/server_box.add_theme_font_size_override("font_size", 36)
80 $Panel/player_box.add_theme_font_size_override("font_size", 36)
81 $Panel/password_box.add_theme_font_size_override("font_size", 36)
82
83
84# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
85func installScriptExtension(childScript: Resource):
86 # Force Godot to compile the script now.
87 # We need to do this here to ensure that the inheritance chain is
88 # properly set up, and multiple mods can chain-extend the same
89 # class multiple times.
90 # This is also needed to make Godot instantiate the extended class
91 # when creating singletons.
92 # The actual instance is thrown away.
93 childScript.new()
94
95 var parentScript = childScript.get_base_script()
96 var parentScriptPath = parentScript.resource_path
97 global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
98 childScript.take_over_path(parentScriptPath)
99
100
101func connectionStatus(message):
102 var popup = self.get_node("Panel/AcceptDialog")
103 popup.title = "Connecting to Archipelago"
104 popup.dialog_text = message
105 popup.exclusive = true
106 popup.get_ok_button().visible = false
107 popup.popup_centered()
108
109
110func connectionSuccessful():
111 var ap = global.get_node("Archipelago")
112
113 # Save connection details
114 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
115 if ap.connection_history.has(connection_details):
116 ap.connection_history.erase(connection_details)
117 ap.connection_history.push_front(connection_details)
118 if ap.connection_history.size() > 10:
119 ap.connection_history.resize(10)
120 ap.saveSettings()
121
122 # Switch to the_entry
123 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
124 global.user = ap.getSaveFileName()
125 global.universe = "lingo"
126 global.map = "the_entry"
127
128 unlocks.resetKeys()
129 unlocks.resetCollectables()
130 unlocks.resetData()
131 unlocks.loadKeys()
132 unlocks.loadCollectables()
133 unlocks.loadData()
134 unlocks.unlockKey("capslock", 1)
135
136 clearResourceCache("res://objects/nodes/door.tscn")
137 clearResourceCache("res://objects/nodes/player.tscn")
138
139 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
140 if paintings_dir:
141 paintings_dir.list_dir_begin()
142 var file_name = paintings_dir.get_next()
143 while file_name != "":
144 if not paintings_dir.current_is_dir() and file_name.ends_with(".tscn"):
145 clearResourceCache("res://objects/meshes/paintings/" + file_name)
146 file_name = paintings_dir.get_next()
147
148 switcher.switch_map("res://objects/scenes/the_entry.tscn")
149
150
151func connectionUnsuccessful(error_message):
152 $Panel/connect_button.disabled = false
153
154 var popup = $Panel/AcceptDialog
155 popup.title = "Could not connect to Archipelago"
156 popup.dialog_text = error_message
157 popup.exclusive = true
158 popup.get_ok_button().visible = true
159 popup.popup_centered()
160
161
162func historySelected(index):
163 var ap = global.get_node("Archipelago")
164 var details = ap.connection_history[index]
165
166 $Panel/server_box.text = details[0]
167 $Panel/player_box.text = details[1]
168 $Panel/password_box.text = details[2]
169
170
171func clearResourceCache(path):
172 ResourceLoader.load_threaded_request(path, "", false, ResourceLoader.CACHE_MODE_REPLACE)
173 ResourceLoader.load_threaded_get(path)
diff --git a/client/Archipelago/vendor/LICENSE b/client/Archipelago/vendor/LICENSE new file mode 100644 index 0000000..115ba15 --- /dev/null +++ b/client/Archipelago/vendor/LICENSE
@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2023 Xavier Sellier
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE. \ No newline at end of file
diff --git a/client/Archipelago/vendor/uuid.gd b/client/Archipelago/vendor/uuid.gd new file mode 100644 index 0000000..b63fa04 --- /dev/null +++ b/client/Archipelago/vendor/uuid.gd
@@ -0,0 +1,195 @@
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