about summary refs log tree commit diff stats
path: root/client/Archipelago
diff options
context:
space:
mode:
Diffstat (limited to 'client/Archipelago')
-rw-r--r--client/Archipelago/animationListener.gd38
-rw-r--r--client/Archipelago/client.gd415
-rw-r--r--client/Archipelago/collectable.gd16
-rw-r--r--client/Archipelago/door.gd38
-rw-r--r--client/Archipelago/gamedata.gd133
-rw-r--r--client/Archipelago/keyHolder.gd38
-rw-r--r--client/Archipelago/keyHolderChecker.gd24
-rw-r--r--client/Archipelago/keyHolderResetterListener.gd8
-rw-r--r--client/Archipelago/keyboard.gd178
-rw-r--r--client/Archipelago/locationListener.gd20
-rw-r--r--client/Archipelago/manager.gd525
-rw-r--r--client/Archipelago/messages.gd61
-rw-r--r--client/Archipelago/painting.gd38
-rw-r--r--client/Archipelago/panel.gd101
-rw-r--r--client/Archipelago/pauseMenu.gd12
-rw-r--r--client/Archipelago/player.gd238
-rw-r--r--client/Archipelago/saver.gd9
-rw-r--r--client/Archipelago/settings_buttons.gd24
-rw-r--r--client/Archipelago/settings_screen.gd241
-rw-r--r--client/Archipelago/teleport.gd38
-rw-r--r--client/Archipelago/teleportListener.gd49
-rw-r--r--client/Archipelago/textclient.gd86
-rw-r--r--client/Archipelago/vendor/LICENSE21
-rw-r--r--client/Archipelago/vendor/uuid.gd195
-rw-r--r--client/Archipelago/victoryListener.gd20
-rw-r--r--client/Archipelago/visibilityListener.gd38
-rw-r--r--client/Archipelago/worldportListener.gd8
27 files changed, 2612 insertions, 0 deletions
diff --git a/client/Archipelago/animationListener.gd b/client/Archipelago/animationListener.gd new file mode 100644 index 0000000..c3b26db --- /dev/null +++ b/client/Archipelago/animationListener.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/listeners/animationListener.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
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.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd new file mode 100644 index 0000000..2e080fd --- /dev/null +++ b/client/Archipelago/client.gd
@@ -0,0 +1,415 @@
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_indexes = []
36var _received_items = {}
37var _slot_data = {}
38
39signal could_not_connect
40signal connect_status
41signal client_connected(slot_data)
42signal item_received(item_id, index, player, flags, amount)
43signal message_received(message)
44signal location_scout_received(item_id, location_id, player, flags)
45
46
47func _init():
48 set_process_mode(Node.PROCESS_MODE_ALWAYS)
49
50 global._print("Instantiated APClient")
51
52 # Read AP datapackages from file, if there are any
53 if FileAccess.file_exists("user://ap_datapackages"):
54 var file = FileAccess.open("user://ap_datapackages", FileAccess.READ)
55 var data = file.get_var(true)
56 file.close()
57
58 if typeof(data) != TYPE_DICTIONARY:
59 global._print("AP datapackages file is corrupted")
60 data = {}
61
62 _datapackages = data
63
64 processDatapackages()
65
66
67func _ready():
68 pass
69 #_ws.connect("connection_closed", _closed)
70 #_ws.connect("connection_failed", _closed)
71 #_ws.connect("server_disconnected", _closed)
72 #_ws.connect("connection_error", _errored)
73 #_ws.connect("connection_established", _connected)
74
75
76func _reset_state():
77 _should_process = false
78 _authenticated = false
79 _try_wss = false
80 _has_connected = false
81 _received_items = {}
82 _received_indexes = []
83
84
85func _errored():
86 if _try_wss:
87 global._print("Could not connect to AP with ws://, now trying wss://")
88 connectToServer(ap_server, ap_user, ap_pass)
89 else:
90 global._print("AP connection failed")
91 _reset_state()
92
93 emit_signal(
94 "could_not_connect",
95 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information."
96 )
97
98
99func _closed(_was_clean = true):
100 global._print("Connection closed")
101 _reset_state()
102
103 if not _initiated_disconnect:
104 emit_signal("could_not_connect", "Disconnected from Archipelago")
105
106 _initiated_disconnect = false
107
108
109func _connected(_proto = ""):
110 global._print("Connected!")
111 _try_wss = false
112
113
114func disconnect_from_ap():
115 _initiated_disconnect = true
116 _ws.close()
117
118
119func _process(_delta):
120 if _should_process:
121 _ws.poll()
122
123 var state = _ws.get_ready_state()
124 if state == WebSocketPeer.STATE_OPEN:
125 if not _has_connected:
126 _has_connected = true
127
128 _connected()
129
130 while _ws.get_available_packet_count():
131 var packet = _ws.get_packet()
132 global._print("Got data from server: " + packet.get_string_from_utf8())
133 var json = JSON.new()
134 var jserror = json.parse(packet.get_string_from_utf8())
135 if jserror != OK:
136 global._print("Error parsing packet from AP: " + jserror.error_string)
137 return
138
139 for message in json.data:
140 var cmd = message["cmd"]
141 global._print("Received command: " + cmd)
142
143 if cmd == "RoomInfo":
144 _seed = message["seed_name"]
145 _remote_version = message["version"]
146 _gen_version = message["generator_version"]
147
148 var needed_games = []
149 for game in message["datapackage_checksums"].keys():
150 if (
151 !_datapackages.has(game)
152 or (
153 _datapackages[game]["checksum"]
154 != message["datapackage_checksums"][game]
155 )
156 ):
157 needed_games.append(game)
158
159 if !needed_games.is_empty():
160 _pending_packages = needed_games
161 var cur_needed = _pending_packages.pop_front()
162 requestDatapackages([cur_needed])
163 else:
164 connectToRoom()
165
166 elif cmd == "DataPackage":
167 for game in message["data"]["games"].keys():
168 _datapackages[game] = message["data"]["games"][game]
169 saveDatapackages()
170
171 if !_pending_packages.is_empty():
172 var cur_needed = _pending_packages.pop_front()
173 requestDatapackages([cur_needed])
174 else:
175 processDatapackages()
176 connectToRoom()
177
178 elif cmd == "Connected":
179 _authenticated = true
180 _team = message["team"]
181 _slot = message["slot"]
182 _players = message["players"]
183 _checked_locations = message["checked_locations"]
184 _slot_data = message["slot_data"]
185
186 for player in _players:
187 _player_name_by_slot[player["slot"]] = player["alias"]
188 _game_by_player[player["slot"]] = message["slot_info"][str(
189 player["slot"]
190 )]["game"]
191
192 emit_signal("client_connected", _slot_data)
193
194 elif cmd == "ConnectionRefused":
195 var error_message = ""
196 for error in message["errors"]:
197 var submsg = ""
198 if error == "InvalidSlot":
199 submsg = "Invalid player name."
200 elif error == "InvalidGame":
201 submsg = "The specified player is not playing Lingo."
202 elif error == "IncompatibleVersion":
203 submsg = (
204 "The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d."
205 % [
206 ap_version["major"],
207 ap_version["minor"],
208 ap_version["build"],
209 _remote_version["major"],
210 _remote_version["minor"],
211 _remote_version["build"]
212 ]
213 )
214 elif error == "InvalidPassword":
215 submsg = "Incorrect password."
216 elif error == "InvalidItemsHandling":
217 submsg = "Invalid item handling flag. This is a bug with the client."
218
219 if submsg != "":
220 if error_message != "":
221 error_message += " "
222 error_message += submsg
223
224 if error_message == "":
225 error_message = "Unknown error."
226
227 _initiated_disconnect = true
228 _ws.disconnect_from_host()
229
230 emit_signal("could_not_connect", error_message)
231 global._print("Connection to AP refused")
232 global._print(message)
233
234 elif cmd == "ReceivedItems":
235 var i = 0
236 for item in message["items"]:
237 var index = int(message["index"] + i)
238 i += 1
239
240 if _received_indexes.has(index):
241 # Do not re-process items.
242 continue
243
244 _received_indexes.append(index)
245
246 var item_id = int(item["item"])
247 _received_items[item_id] = _received_items.get(item_id, 0) + 1
248
249 emit_signal(
250 "item_received",
251 item_id,
252 index,
253 int(item["player"]),
254 int(item["flags"]),
255 _received_items[item_id]
256 )
257
258 elif cmd == "PrintJSON":
259 emit_signal("message_received", message)
260
261 elif cmd == "LocationInfo":
262 for loc in message["locations"]:
263 emit_signal(
264 "location_scout_received",
265 int(loc["item"]),
266 int(loc["location"]),
267 int(loc["player"]),
268 int(loc["flags"])
269 )
270
271 elif state == WebSocketPeer.STATE_CLOSED:
272 if _has_connected:
273 _closed()
274 else:
275 _errored()
276
277
278func saveDatapackages():
279 # Save the AP datapackages to disk.
280 var file = FileAccess.open("user://ap_datapackages", FileAccess.WRITE)
281 file.store_var(_datapackages, true)
282 file.close()
283
284
285func connectToServer(server, un, pw):
286 ap_server = server
287 ap_user = un
288 ap_pass = pw
289
290 _initiated_disconnect = false
291
292 var url = ""
293 if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"):
294 url = ap_server
295 _try_wss = false
296 elif _try_wss:
297 url = "wss://" + ap_server
298 _try_wss = false
299 else:
300 url = "ws://" + ap_server
301 _try_wss = true
302
303 var err = _ws.connect_to_url(url)
304 if err != OK:
305 emit_signal(
306 "could_not_connect",
307 (
308 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d."
309 % err
310 )
311 )
312 global._print("Could not connect to AP: " + err)
313 return
314 _should_process = true
315
316 emit_signal("connect_status", "Connecting...")
317
318
319func sendMessage(msg):
320 var payload = JSON.stringify(msg)
321 _ws.send_text(payload)
322
323
324func requestDatapackages(games):
325 emit_signal("connect_status", "Downloading %s data package..." % games[0])
326
327 sendMessage([{"cmd": "GetDataPackage", "games": games}])
328
329
330func processDatapackages():
331 _item_id_to_name = {}
332 _location_id_to_name = {}
333 for game in _datapackages.keys():
334 var package = _datapackages[game]
335
336 _item_id_to_name[game] = {}
337 for item_name in package["item_name_to_id"].keys():
338 _item_id_to_name[game][int(package["item_name_to_id"][item_name])] = item_name
339
340 _location_id_to_name[game] = {}
341 for location_name in package["location_name_to_id"].keys():
342 _location_id_to_name[game][int(package["location_name_to_id"][location_name])] = location_name
343
344 if _datapackages.has("Lingo 2"):
345 _item_name_to_id = _datapackages["Lingo 2"]["item_name_to_id"]
346 _location_name_to_id = _datapackages["Lingo 2"]["location_name_to_id"]
347
348
349func connectToRoom():
350 emit_signal("connect_status", "Authenticating...")
351
352 sendMessage(
353 [
354 {
355 "cmd": "Connect",
356 "password": ap_pass,
357 "game": "Lingo 2",
358 "name": ap_user,
359 "uuid": SCRIPT_uuid.v4(),
360 "version": ap_version,
361 "items_handling": 0b111, # always receive our items
362 "tags": [],
363 "slot_data": true
364 }
365 ]
366 )
367
368
369func sendConnectUpdate(tags):
370 sendMessage([{"cmd": "ConnectUpdate", "tags": tags}])
371
372
373func requestSync():
374 sendMessage([{"cmd": "Sync"}])
375
376
377func sendLocation(loc_id):
378 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
379
380
381func sendLocations(loc_ids):
382 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
383
384
385func setValue(key, value, operation = "replace"):
386 sendMessage(
387 [
388 {
389 "cmd": "Set",
390 "key": "Lingo2_%d_%s" % [_slot, key],
391 "want_reply": false,
392 "operations": [{"operation": operation, "value": value}]
393 }
394 ]
395 )
396
397
398func say(textdata):
399 sendMessage([{"cmd": "Say", "text": textdata}])
400
401
402func completedGoal():
403 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
404
405
406func scoutLocations(loc_ids):
407 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
408
409
410func hasItem(item_id):
411 return _received_items.has(item_id)
412
413
414func getItemAmount(item_id):
415 return _received_items.get(item_id, 0)
diff --git a/client/Archipelago/collectable.gd b/client/Archipelago/collectable.gd new file mode 100644 index 0000000..4a17a2a --- /dev/null +++ b/client/Archipelago/collectable.gd
@@ -0,0 +1,16 @@
1extends "res://scripts/nodes/collectable.gd"
2
3
4func pickedUp():
5 if unlock_type == "key":
6 var ap = global.get_node("Archipelago")
7 if ap.get_letter_behavior(unlock_key, level == 2) == ap.kLETTER_BEHAVIOR_VANILLA:
8 ap.keyboard.collect_local_letter(unlock_key, level)
9 else:
10 ap.keyboard.update_unlocks()
11
12 super.pickedUp()
13
14
15func setScoutedText(text):
16 get_node("MeshInstance3D").mesh.text = text.replace(" ", "\n")
diff --git a/client/Archipelago/door.gd b/client/Archipelago/door.gd new file mode 100644 index 0000000..fead818 --- /dev/null +++ b/client/Archipelago/door.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/door.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
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.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd new file mode 100644 index 0000000..d8d16ed --- /dev/null +++ b/client/Archipelago/gamedata.gd
@@ -0,0 +1,133 @@
1extends Node
2
3var SCRIPT_proto
4
5var objects
6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {}
8var panel_id_by_map_node_path = {}
9var door_id_by_ap_id = {}
10var map_id_by_name = {}
11var progressive_id_by_ap_id = {}
12var letter_id_by_ap_id = {}
13var symbol_item_ids = []
14
15var kSYMBOL_ITEMS
16
17
18func _init(proto_script):
19 SCRIPT_proto = proto_script
20
21 kSYMBOL_ITEMS = {
22 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
23 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
24 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
25 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
26 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
27 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
28 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
29 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
30 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
31 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
32 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
33 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
34 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
35 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
36 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
37 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
38 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
39 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
40 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
41 }
42
43
44func load(data_bytes):
45 objects = SCRIPT_proto.AllObjects.new()
46
47 var result_code = objects.from_bytes(data_bytes)
48 if result_code != SCRIPT_proto.PB_ERR.NO_ERRORS:
49 print("Could not load generated data: %d" % result_code)
50 return
51
52 for map in objects.get_maps():
53 map_id_by_name[map.get_name()] = map.get_id()
54
55 for door in objects.get_doors():
56 var map = objects.get_maps()[door.get_map_id()]
57
58 if not map.get_name() in door_id_by_map_node_path:
59 door_id_by_map_node_path[map.get_name()] = {}
60
61 var map_data = door_id_by_map_node_path[map.get_name()]
62 for receiver in door.get_receivers():
63 map_data[receiver] = door.get_id()
64
65 for painting_id in door.get_move_paintings():
66 var painting = objects.get_paintings()[painting_id]
67 map_data[painting.get_path()] = door.get_id()
68
69 if door.has_ap_id():
70 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
71
72 for painting in objects.get_paintings():
73 var room = objects.get_rooms()[painting.get_room_id()]
74 var map = objects.get_maps()[room.get_map_id()]
75
76 if not map.get_name() in painting_id_by_map_node_path:
77 painting_id_by_map_node_path[map.get_name()] = {}
78
79 var _map_data = painting_id_by_map_node_path[map.get_name()]
80
81 for progressive in objects.get_progressives():
82 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
83
84 for letter in objects.get_letters():
85 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
86
87 for panel in objects.get_panels():
88 var room = objects.get_rooms()[panel.get_room_id()]
89 var map = objects.get_maps()[room.get_map_id()]
90
91 if not map.get_name() in panel_id_by_map_node_path:
92 panel_id_by_map_node_path[map.get_name()] = {}
93
94 var map_data = panel_id_by_map_node_path[map.get_name()]
95 map_data[panel.get_path()] = panel.get_id()
96
97 for symbol_name in kSYMBOL_ITEMS.values():
98 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
99
100
101func get_door_for_map_node_path(map_name, node_path):
102 if not door_id_by_map_node_path.has(map_name):
103 return null
104
105 var map_data = door_id_by_map_node_path[map_name]
106 return map_data.get(node_path, null)
107
108
109func get_panel_for_map_node_path(map_name, node_path):
110 if not panel_id_by_map_node_path.has(map_name):
111 return null
112
113 var map_data = panel_id_by_map_node_path[map_name]
114 return map_data.get(node_path, null)
115
116
117func get_door_ap_id(door_id):
118 var door = objects.get_doors()[door_id]
119 if door.has_ap_id():
120 return door.get_ap_id()
121 else:
122 return null
123
124
125func get_door_receivers(door_id):
126 var door = objects.get_doors()[door_id]
127 return door.get_receivers()
128
129
130func get_door_map_name(door_id):
131 var door = objects.get_doors()[door_id]
132 var map = objects.get_maps()[door.get_map_id()]
133 return map.get_name()
diff --git a/client/Archipelago/keyHolder.gd b/client/Archipelago/keyHolder.gd new file mode 100644 index 0000000..3c037ff --- /dev/null +++ b/client/Archipelago/keyHolder.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/keyHolder.gd"
2
3
4func setFromAp(key, level):
5 if level > 0:
6 has_key = true
7 is_complete = "%s%d" % [key, level]
8 held_key = key
9 held_level = level
10 get_node("Hinge/Letter").mesh.text = held_key
11 get_node("Hinge/Letter2").mesh.text = held_key
12 setMaterial()
13 emit_signal("trigger")
14 else:
15 has_key = false
16 held_key = ""
17 held_level = 0
18 setMaterial()
19 get_node("Hinge/Letter").mesh.text = "-"
20 get_node("Hinge/Letter2").mesh.text = "-"
21 is_complete = ""
22 emit_signal("untrigger")
23
24
25func addKey(key):
26 var node_path = String(
27 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
28 )
29 var ap = global.get_node("Archipelago")
30 ap.keyboard.put_in_keyholder(key, global.map, node_path)
31
32
33func removeKey():
34 var node_path = String(
35 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
36 )
37 var ap = global.get_node("Archipelago")
38 ap.keyboard.remove_from_keyholder(held_key, global.map, node_path)
diff --git a/client/Archipelago/keyHolderChecker.gd b/client/Archipelago/keyHolderChecker.gd new file mode 100644 index 0000000..a75a9e4 --- /dev/null +++ b/client/Archipelago/keyHolderChecker.gd
@@ -0,0 +1,24 @@
1extends "res://scripts/nodes/listeners/keyHolderChecker.gd"
2
3
4func check():
5 var ap = global.get_node("Archipelago")
6 var matches = []
7 for map in ap.keyboard.keyholder_state.keys():
8 var nodes = ap.keyboard.keyholder_state[map]
9 for node in nodes.keys():
10 matches.append([nodes[node], 1, map, "/root/scene/%s" % node])
11
12 var count = 0
13 for key_match in matches:
14 var active = (
15 key_match[2] + String(key_match[3]).replace("/root/scene/Components/KeyHolders/", ".")
16 )
17 if map[active] == key_match[0]:
18 emit_signal("trigger_letter", key_match[0], true)
19 count += 1
20 else:
21 emit_signal("trigger_letter", key_match[0], false)
22
23 if count > 25:
24 emit_signal("trigger")
diff --git a/client/Archipelago/keyHolderResetterListener.gd b/client/Archipelago/keyHolderResetterListener.gd new file mode 100644 index 0000000..d5300f3 --- /dev/null +++ b/client/Archipelago/keyHolderResetterListener.gd
@@ -0,0 +1,8 @@
1extends "res://scripts/nodes/listeners/keyHolderResetterListener.gd"
2
3
4func reset():
5 var ap = global.get_node("Archipelago")
6 var was_removed = ap.keyboard.reset_keyholders()
7 if was_removed:
8 sfxPlayer.sfx_play("pickup")
diff --git a/client/Archipelago/keyboard.gd b/client/Archipelago/keyboard.gd new file mode 100644 index 0000000..600a047 --- /dev/null +++ b/client/Archipelago/keyboard.gd
@@ -0,0 +1,178 @@
1extends Node
2
3const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz"
4
5var letters_saved = {}
6var letters_in_keyholders = []
7var letters_dynamic = {}
8var keyholder_state = {}
9
10var filename = ""
11
12
13func _init():
14 reset()
15
16
17func reset():
18 letters_saved.clear()
19 letters_in_keyholders.clear()
20 letters_dynamic.clear()
21 keyholder_state.clear()
22
23
24func load_seed():
25 var ap = global.get_node("Archipelago")
26
27 reset()
28
29 filename = "user://archipelago_keys/%s_%d" % [ap.client._seed, ap.client._slot]
30
31 if FileAccess.file_exists(filename):
32 var ap_file = FileAccess.open(filename, FileAccess.READ)
33 var localdata = []
34 if ap_file != null:
35 localdata = ap_file.get_var(true)
36 ap_file.close()
37
38 if typeof(localdata) != TYPE_ARRAY:
39 print("AP keyboard file is corrupted")
40 localdata = []
41
42 if localdata.size() > 0:
43 letters_saved = localdata[0]
44 if localdata.size() > 1:
45 letters_in_keyholders = localdata[1]
46 if localdata.size() > 2:
47 keyholder_state = localdata[2]
48
49 for k in kALL_LETTERS:
50 var level = 0
51
52 if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED:
53 level += 1
54 if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED:
55 level += 1
56
57 letters_dynamic[k] = level
58
59 update_unlocks()
60
61
62func save():
63 var dir = DirAccess.open("user://")
64 var folder = "archipelago_keys"
65 if not dir.dir_exists(folder):
66 dir.make_dir(folder)
67
68 var file = FileAccess.open(filename, FileAccess.WRITE)
69
70 var data = [
71 letters_saved,
72 letters_in_keyholders,
73 keyholder_state,
74 ]
75 file.store_var(data, true)
76 file.close()
77
78
79func update_unlocks():
80 unlocks.resetKeys()
81
82 var has_doubles = false
83
84 for k in kALL_LETTERS:
85 var level = 0
86
87 if not letters_in_keyholders.has(k):
88 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
89
90 if level >= 2:
91 level = 2
92 has_doubles = true
93
94 unlocks.unlockKey(k, level)
95
96 if has_doubles and unlocks.data["double_letters"] != "unlocked":
97 var ap = global.get_node("Archipelago")
98 if ap.cyan_door_behavior == ap.kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER:
99 unlocks.setData("double_letters", "unlocked")
100
101
102func collect_local_letter(key, level):
103 if level < 0 or level > 2 or level < letters_saved.get(key, 0):
104 return
105
106 letters_saved[key] = level
107
108 update_unlocks()
109 save()
110
111
112func collect_remote_letter(key, level):
113 if level < 0 or level > 2 or level < letters_dynamic.get(key, 0):
114 return
115
116 letters_dynamic[key] = level
117
118 update_unlocks()
119 save()
120
121
122func put_in_keyholder(key, map, kh_path):
123 if not keyholder_state.has(map):
124 keyholder_state[map] = {}
125
126 keyholder_state[map][kh_path] = key
127 letters_in_keyholders.append(key)
128
129 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(
130 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
131 )
132
133 update_unlocks()
134 save()
135
136
137func remove_from_keyholder(key, map, kh_path):
138 if not keyholder_state.has(map):
139 # This... shouldn't happen.
140 keyholder_state[map] = {}
141
142 keyholder_state[map].erase(kh_path)
143 letters_in_keyholders.erase(key)
144
145 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0)
146
147 update_unlocks()
148 save()
149
150
151func load_keyholders(map):
152 if keyholder_state.has(map):
153 var khs = keyholder_state[map]
154
155 for path in khs.keys():
156 var key = khs[path]
157 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
158 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
159 )
160
161
162func reset_keyholders():
163 if letters_in_keyholders.is_empty():
164 return false
165
166 if keyholder_state.has(global.map):
167 for path in keyholder_state[global.map]:
168 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
169 keyholder_state[global.map][path], 0
170 )
171
172 keyholder_state.clear()
173 letters_in_keyholders.clear()
174
175 update_unlocks()
176 save()
177
178 return true
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..8a15728 --- /dev/null +++ b/client/Archipelago/manager.gd
@@ -0,0 +1,525 @@
1extends Node
2
3const MOD_VERSION = 2
4
5var SCRIPT_client
6var SCRIPT_keyboard
7var SCRIPT_locationListener
8var SCRIPT_uuid
9var SCRIPT_victoryListener
10
11var ap_server = ""
12var ap_user = ""
13var ap_pass = ""
14var connection_history = []
15
16var client
17var keyboard
18
19var _localdata_file = ""
20var _last_new_item = -1
21var _batch_locations = false
22var _held_locations = []
23var _held_location_scouts = []
24var _location_scouts = {}
25var _item_locks = {}
26var _inverse_item_locks = {}
27var _held_letters = {}
28var _letters_setup = false
29
30const kSHUFFLE_LETTERS_VANILLA = 0
31const kSHUFFLE_LETTERS_UNLOCKED = 1
32const kSHUFFLE_LETTERS_PROGRESSIVE = 2
33const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
34const kSHUFFLE_LETTERS_ITEM_CYAN = 4
35
36const kLETTER_BEHAVIOR_VANILLA = 0
37const kLETTER_BEHAVIOR_ITEM = 1
38const kLETTER_BEHAVIOR_UNLOCKED = 2
39
40const kCYAN_DOOR_BEHAVIOR_H2 = 0
41const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
42const kCYAN_DOOR_BEHAVIOR_ITEM = 2
43
44var apworld_version = [0, 0]
45var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
46var daedalus_roof_access = false
47var keyholder_sanity = false
48var shuffle_control_center_colors = false
49var shuffle_doors = false
50var shuffle_gallery_paintings = false
51var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
52var shuffle_symbols = false
53var victory_condition = -1
54
55signal could_not_connect
56signal connect_status
57signal ap_connected
58
59
60func _init():
61 # Read AP settings from file, if there are any
62 if FileAccess.file_exists("user://ap_settings"):
63 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
64 var data = file.get_var(true)
65 file.close()
66
67 if typeof(data) != TYPE_ARRAY:
68 global._print("AP settings file is corrupted")
69 data = []
70
71 if data.size() > 0:
72 ap_server = data[0]
73
74 if data.size() > 1:
75 ap_user = data[1]
76
77 if data.size() > 2:
78 ap_pass = data[2]
79
80 if data.size() > 3:
81 connection_history = data[3]
82
83
84func _ready():
85 client = SCRIPT_client.new()
86 client.SCRIPT_uuid = SCRIPT_uuid
87
88 client.connect("item_received", _process_item)
89 client.connect("message_received", _process_message)
90 client.connect("location_scout_received", _process_location_scout)
91 client.connect("could_not_connect", _client_could_not_connect)
92 client.connect("connect_status", _client_connect_status)
93 client.connect("client_connected", _client_connected)
94
95 add_child(client)
96
97 keyboard = SCRIPT_keyboard.new()
98 add_child(keyboard)
99
100
101func saveSettings():
102 # Save the AP settings to disk.
103 var path = "user://ap_settings"
104 var file = FileAccess.open(path, FileAccess.WRITE)
105
106 var data = [
107 ap_server,
108 ap_user,
109 ap_pass,
110 connection_history,
111 ]
112 file.store_var(data, true)
113 file.close()
114
115
116func saveLocaldata():
117 # Save the MW/slot specific settings to disk.
118 var dir = DirAccess.open("user://")
119 var folder = "archipelago_data"
120 if not dir.dir_exists(folder):
121 dir.make_dir(folder)
122
123 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
124
125 var data = [
126 _last_new_item,
127 ]
128 file.store_var(data, true)
129 file.close()
130
131
132func connectToServer():
133 _last_new_item = -1
134 _batch_locations = false
135 _held_locations = []
136 _held_location_scouts = []
137 _location_scouts = {}
138 _letters_setup = false
139 _held_letters = {}
140
141 client.connectToServer(ap_server, ap_user, ap_pass)
142
143
144func getSaveFileName():
145 return "zzAP_%s_%d" % [client._seed, client._slot]
146
147
148func disconnect_from_ap():
149 client.disconnect_from_ap()
150
151
152func get_item_id_for_door(door_id):
153 return _item_locks.get(door_id, null)
154
155
156func _process_item(item, index, from, flags, amount):
157 var item_name = "Unknown"
158 if client._item_id_to_name["Lingo 2"].has(item):
159 item_name = client._item_id_to_name["Lingo 2"][item]
160
161 var gamedata = global.get_node("Gamedata")
162
163 var prog_id = null
164 if _inverse_item_locks.has(item):
165 for lock in _inverse_item_locks.get(item):
166 if lock[1] != amount:
167 continue
168
169 if gamedata.progressive_id_by_ap_id.has(item):
170 prog_id = lock[0]
171
172 if gamedata.get_door_map_name(lock[0]) != global.map:
173 continue
174
175 var receivers = gamedata.get_door_receivers(lock[0])
176 var scene = get_tree().get_root().get_node_or_null("scene")
177 if scene != null:
178 for receiver in receivers:
179 var rnode = scene.get_node_or_null(receiver)
180 if rnode != null:
181 rnode.handleTriggered()
182
183 var letter_id = gamedata.letter_id_by_ap_id.get(item, null)
184 if letter_id != null:
185 var letter = gamedata.objects.get_letters()[letter_id]
186 if not letter.has_level2() or not letter.get_level2():
187 _process_key_item(letter.get_key(), amount)
188
189 if gamedata.symbol_item_ids.has(item):
190 var player = get_tree().get_root().get_node_or_null("scene/player")
191 if player != null:
192 player.emit_signal("evaluate_solvability")
193
194 # Show a message about the item if it's new.
195 if index != null and index > _last_new_item:
196 _last_new_item = index
197 saveLocaldata()
198
199 var player_name = "Unknown"
200 if client._player_name_by_slot.has(float(from)):
201 player_name = client._player_name_by_slot[float(from)]
202
203 var item_color = colorForItemType(flags)
204
205 var full_item_name = item_name
206 if prog_id != null:
207 var door = gamedata.objects.get_doors()[prog_id]
208 full_item_name = "%s (%s)" % [item_name, door.get_name()]
209
210 var message
211 if from == client._slot:
212 message = "Found [color=%s]%s[/color]" % [item_color, full_item_name]
213 else:
214 message = (
215 "Received [color=%s]%s[/color] from %s" % [item_color, full_item_name, player_name]
216 )
217
218 global._print(message)
219
220 global.get_node("Messages").showMessage(message)
221
222
223func _process_message(message):
224 parse_printjson_for_textclient(message)
225
226 if (
227 !message.has("receiving")
228 or !message.has("item")
229 or message["item"]["player"] != client._slot
230 ):
231 return
232
233 var item_name = "Unknown"
234 var item_player_game = client._game_by_player[message["receiving"]]
235 if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])):
236 item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])]
237
238 var location_name = "Unknown"
239 var location_player_game = client._game_by_player[message["item"]["player"]]
240 if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])):
241 location_name = (client._location_id_to_name[location_player_game][int(
242 message["item"]["location"]
243 )])
244
245 var player_name = "Unknown"
246 if client._player_name_by_slot.has(message["receiving"]):
247 player_name = client._player_name_by_slot[message["receiving"]]
248
249 var item_color = colorForItemType(message["item"]["flags"])
250
251 if message["type"] == "Hint":
252 var is_for = ""
253 if message["receiving"] != client._slot:
254 is_for = " for %s" % player_name
255 if !message.has("found") || !message["found"]:
256 global.get_node("Messages").showMessage(
257 (
258 "Hint: [color=%s]%s[/color]%s is on %s"
259 % [item_color, item_name, is_for, location_name]
260 )
261 )
262 else:
263 if message["receiving"] != client._slot:
264 var sentMsg = "Sent [color=%s]%s[/color] to %s" % [item_color, item_name, player_name]
265 #if _hinted_locations.has(message["item"]["location"]):
266 # sentMsg += " ([color=#fafad2]Hinted![/color])"
267 global.get_node("Messages").showMessage(sentMsg)
268
269
270func parse_printjson_for_textclient(message):
271 var parts = []
272 for message_part in message["data"]:
273 if !message_part.has("type") and message_part.has("text"):
274 parts.append(message_part["text"])
275 elif message_part["type"] == "player_id":
276 if int(message_part["text"]) == client._slot:
277 parts.append(
278 "[color=#ee00ee]%s[/color]" % client._player_name_by_slot[client._slot]
279 )
280 else:
281 var from = float(message_part["text"])
282 parts.append("[color=#fafad2]%s[/color]" % client._player_name_by_slot[from])
283 elif message_part["type"] == "item_id":
284 var item_name = "Unknown"
285 var item_player_game = client._game_by_player[message_part["player"]]
286 if client._item_id_to_name[item_player_game].has(int(message_part["text"])):
287 item_name = client._item_id_to_name[item_player_game][int(message_part["text"])]
288
289 parts.append(
290 "[color=%s]%s[/color]" % [colorForItemType(message_part["flags"]), item_name]
291 )
292 elif message_part["type"] == "location_id":
293 var location_name = "Unknown"
294 var location_player_game = client._game_by_player[message_part["player"]]
295 if client._location_id_to_name[location_player_game].has(int(message_part["text"])):
296 location_name = client._location_id_to_name[location_player_game][int(
297 message_part["text"]
298 )]
299
300 parts.append("[color=#00ff7f]%s[/color]" % location_name)
301 elif message_part.has("text"):
302 parts.append(message_part["text"])
303
304 var textclient_node = global.get_node("Textclient")
305 if textclient_node != null:
306 textclient_node.parse_printjson("".join(parts))
307
308
309func _process_location_scout(item_id, location_id, player, flags):
310 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
311
312 var gamedata = global.get_node("Gamedata")
313 var map_id = gamedata.map_id_by_name.get(global.map)
314
315 var item_name = "Unknown"
316 var item_player_game = client._game_by_player[float(player)]
317 if client._item_id_to_name[item_player_game].has(item_id):
318 item_name = client._item_id_to_name[item_player_game][item_id]
319
320 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
321 if letter_id != null:
322 var letter = gamedata.objects.get_letters()[letter_id]
323 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
324 if room.get_map_id() == map_id:
325 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
326 letter.get_path()
327 )
328 if collectable != null:
329 collectable.setScoutedText(item_name)
330
331
332func _client_could_not_connect():
333 emit_signal("could_not_connect")
334
335
336func _client_connect_status(message):
337 emit_signal("connect_status", message)
338
339
340func _client_connected(slot_data):
341 var gamedata = global.get_node("Gamedata")
342
343 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
344 _last_new_item = -1
345
346 if FileAccess.file_exists(_localdata_file):
347 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
348 var localdata = []
349 if ap_file != null:
350 localdata = ap_file.get_var(true)
351 ap_file.close()
352
353 if typeof(localdata) != TYPE_ARRAY:
354 print("AP localdata file is corrupted")
355 localdata = []
356
357 if localdata.size() > 0:
358 _last_new_item = localdata[0]
359
360 # Read slot data.
361 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
362 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
363 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
364 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
365 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
366 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
367 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
368 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
369 victory_condition = int(slot_data.get("victory_condition", 0))
370
371 if slot_data.has("version"):
372 apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])]
373
374 # Set up item locks.
375 _item_locks = {}
376
377 if shuffle_doors:
378 for door in gamedata.objects.get_doors():
379 if (
380 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
381 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
382 ):
383 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
384
385 for progressive in gamedata.objects.get_progressives():
386 for i in range(0, progressive.get_doors().size()):
387 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
388 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
389
390 for door_group in gamedata.objects.get_door_groups():
391 if (
392 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
393 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
394 ):
395 for door in door_group.get_doors():
396 _item_locks[door] = [door_group.get_ap_id(), 1]
397
398 if shuffle_control_center_colors:
399 for door in gamedata.objects.get_doors():
400 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
401 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
402
403 for door_group in gamedata.objects.get_door_groups():
404 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
405 for door in door_group.get_doors():
406 _item_locks[door] = [door_group.get_ap_id(), 1]
407
408 if shuffle_gallery_paintings:
409 for door in gamedata.objects.get_doors():
410 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
411 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
412
413 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
414 for door_group in gamedata.objects.get_door_groups():
415 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
416 for door in door_group.get_doors():
417 if not _item_locks.has(door):
418 _item_locks[door] = [door_group.get_ap_id(), 1]
419
420 # Create a reverse item locks map for processing items.
421 _inverse_item_locks = {}
422
423 for door_id in _item_locks.keys():
424 var lock = _item_locks.get(door_id)
425
426 if not _inverse_item_locks.has(lock[0]):
427 _inverse_item_locks[lock[0]] = []
428
429 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
430
431 emit_signal("ap_connected")
432
433
434func start_batching_locations():
435 _batch_locations = true
436
437
438func send_location(loc_id):
439 if _batch_locations:
440 _held_locations.append(loc_id)
441 else:
442 client.sendLocation(loc_id)
443
444
445func scout_location(loc_id):
446 if _location_scouts.has(loc_id):
447 return _location_scouts.get(loc_id)
448
449 if _batch_locations:
450 _held_location_scouts.append(loc_id)
451 else:
452 client.scoutLocation(loc_id)
453
454 return null
455
456
457func stop_batching_locations():
458 _batch_locations = false
459
460 if not _held_locations.is_empty():
461 client.sendLocations(_held_locations)
462 _held_locations.clear()
463
464 if not _held_location_scouts.is_empty():
465 client.scoutLocations(_held_location_scouts)
466 _held_location_scouts.clear()
467
468
469func colorForItemType(flags):
470 var int_flags = int(flags)
471 if int_flags & 1: # progression
472 if int_flags & 2: # proguseful
473 return "#f0d200"
474 else:
475 return "#bc51e0"
476 elif int_flags & 2: # useful
477 return "#2b67ff"
478 elif int_flags & 4: # trap
479 return "#d63a22"
480 else: # filler
481 return "#14de9e"
482
483
484func get_letter_behavior(key, level2):
485 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
486 return kLETTER_BEHAVIOR_UNLOCKED
487
488 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
489 if level2:
490 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
491 return kLETTER_BEHAVIOR_VANILLA
492 else:
493 return kLETTER_BEHAVIOR_ITEM
494 else:
495 return kLETTER_BEHAVIOR_UNLOCKED
496
497 if not level2 and ["h", "i", "n", "t"].has(key):
498 # This differs from the equivalent function in the apworld. Logically it is
499 # the same as UNLOCKED since they are in the starting room, but VANILLA
500 # means the player still has to actually pick up the letters.
501 return kLETTER_BEHAVIOR_VANILLA
502
503 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
504 return kLETTER_BEHAVIOR_ITEM
505
506 return kLETTER_BEHAVIOR_VANILLA
507
508
509func setup_keys():
510 keyboard.load_seed()
511
512 _letters_setup = true
513
514 for k in _held_letters.keys():
515 _process_key_item(k, _held_letters[k])
516
517 _held_letters.clear()
518
519
520func _process_key_item(key, level):
521 if not _letters_setup:
522 _held_letters[key] = max(_held_letters.get(key, 0), level)
523 return
524
525 keyboard.collect_remote_letter(key, level)
diff --git a/client/Archipelago/messages.gd b/client/Archipelago/messages.gd new file mode 100644 index 0000000..52f38b9 --- /dev/null +++ b/client/Archipelago/messages.gd
@@ -0,0 +1,61 @@
1extends CanvasLayer
2
3var _message_queue = []
4var _font
5var _container
6var _ordered_labels = []
7
8
9func _ready():
10 _container = VBoxContainer.new()
11 _container.set_name("Container")
12 _container.anchor_bottom = 1
13 _container.offset_left = 20.0
14 _container.offset_right = 1920.0
15 _container.offset_top = 0.0
16 _container.offset_bottom = -20.0
17 _container.alignment = BoxContainer.ALIGNMENT_END
18 _container.mouse_filter = Control.MOUSE_FILTER_IGNORE
19 self.add_child(_container)
20
21 _font = load("res://assets/fonts/Lingo2.ttf")
22
23
24func _add_message(text):
25 var new_label = RichTextLabel.new()
26 new_label.push_font(_font)
27 new_label.push_font_size(36)
28 new_label.push_outline_color(Color(0, 0, 0, 1))
29 new_label.push_outline_size(2)
30 new_label.append_text(text)
31 new_label.fit_content = true
32
33 _container.add_child(new_label)
34 _ordered_labels.push_back(new_label)
35
36
37func showMessage(text):
38 if _ordered_labels.size() >= 9:
39 _message_queue.append(text)
40 return
41
42 _add_message(text)
43
44 if _ordered_labels.size() > 1:
45 return
46
47 var timeout = 10.0
48 while !_ordered_labels.is_empty():
49 await get_tree().create_timer(timeout).timeout
50
51 var to_remove = _ordered_labels.pop_front()
52 var to_tween = get_tree().create_tween().bind_node(to_remove)
53 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5)
54 to_tween.tween_callback(to_remove.queue_free)
55
56 if !_message_queue.is_empty():
57 var next_msg = _message_queue.pop_front()
58 _add_message(next_msg)
59
60 if timeout > 4:
61 timeout -= 3
diff --git a/client/Archipelago/painting.gd b/client/Archipelago/painting.gd new file mode 100644 index 0000000..276d4eb --- /dev/null +++ b/client/Archipelago/painting.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/painting.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
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.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/panel.gd b/client/Archipelago/panel.gd new file mode 100644 index 0000000..fdaaf0e --- /dev/null +++ b/client/Archipelago/panel.gd
@@ -0,0 +1,101 @@
1extends "res://scripts/nodes/panel.gd"
2
3var panel_logic = null
4var symbol_solvable = true
5
6var black = load("res://assets/materials/black.material")
7
8
9func _ready():
10 super._ready()
11
12 var node_path = String(
13 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
14 )
15
16 var gamedata = global.get_node("Gamedata")
17 var panel_id = gamedata.get_panel_for_map_node_path(global.map, node_path)
18 if panel_id != null:
19 var ap = global.get_node("Archipelago")
20 if ap.shuffle_symbols:
21 if global.map == "the_entry" and node_path == "Panels/Entry/front_1":
22 clue = "i"
23 symbol = ""
24
25 setField("clue", clue)
26 setField("symbol", symbol)
27
28 panel_logic = gamedata.objects.get_panels()[panel_id]
29 checkSymbolSolvable()
30
31 if not symbol_solvable:
32 get_tree().get_root().get_node("scene/player").connect(
33 "evaluate_solvability", evaluateSolvability
34 )
35
36
37func checkSymbolSolvable():
38 var old_solvable = symbol_solvable
39 symbol_solvable = true
40
41 if panel_logic == null:
42 # There's no logic for this panel.
43 return
44
45 var ap = global.get_node("Archipelago")
46 if not ap.shuffle_symbols:
47 # Symbols aren't item-locked.
48 return
49
50 var gamedata = global.get_node("Gamedata")
51 for symbol in panel_logic.get_symbols():
52 var item_name = gamedata.kSYMBOL_ITEMS.get(symbol)
53 var item_id = gamedata.objects.get_special_ids()[item_name]
54 if ap.client.getItemAmount(item_id) < 1:
55 symbol_solvable = false
56 break
57
58 if symbol_solvable != old_solvable:
59 if symbol_solvable:
60 setField("clue", clue)
61 setField("symbol", symbol)
62 setField("answer", answer)
63 else:
64 quad_mesh.surface_set_material(0, black)
65 get_node("Hinge/clue").text = "missing"
66 get_node("Hinge/answer").text = "symbols"
67
68
69func checkSolvable(key):
70 checkSymbolSolvable()
71 if not symbol_solvable:
72 return false
73
74 return super.checkSolvable(key)
75
76
77func evaluateSolvability():
78 checkSolvable("")
79
80
81func passedInput(key, skip_focus_check = false):
82 if not symbol_solvable:
83 return
84
85 super.passedInput(key, skip_focus_check)
86
87
88func focus():
89 if not symbol_solvable:
90 has_focus = false
91 return
92
93 super.focus()
94
95
96func unfocus():
97 if not symbol_solvable:
98 has_focus = false
99 return
100
101 super.unfocus()
diff --git a/client/Archipelago/pauseMenu.gd b/client/Archipelago/pauseMenu.gd new file mode 100644 index 0000000..5da114a --- /dev/null +++ b/client/Archipelago/pauseMenu.gd
@@ -0,0 +1,12 @@
1extends "res://scripts/ui/pauseMenu.gd"
2
3
4func _pause_game():
5 global.get_node("Textclient").dismiss()
6 super._pause_game()
7
8
9func _main_menu():
10 global.loaded = false
11 global.get_node("Archipelago").disconnect_from_ap()
12 super._main_menu()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd new file mode 100644 index 0000000..9de3e07 --- /dev/null +++ b/client/Archipelago/player.gd
@@ -0,0 +1,238 @@
1extends "res://scripts/nodes/player.gd"
2
3const kEndingNameByVictoryValue = {
4 0: "GRAY",
5 1: "PURPLE",
6 2: "MINT",
7 3: "BLACK",
8 4: "BLUE",
9 5: "CYAN",
10 6: "RED",
11 7: "PLUM",
12 8: "ORANGE",
13 9: "GOLD",
14 10: "YELLOW",
15 11: "GREEN",
16 12: "WHITE",
17}
18
19signal evaluate_solvability
20
21
22func _ready():
23 var khl_script = load("res://scripts/nodes/keyHolderListener.gd")
24
25 var ap = global.get_node("Archipelago")
26 var gamedata = global.get_node("Gamedata")
27
28 ap.start_batching_locations()
29
30 # Set up door locations.
31 var map_id = gamedata.map_id_by_name.get(global.map)
32 for door in gamedata.objects.get_doors():
33 if door.get_map_id() != map_id:
34 continue
35
36 if not door.has_ap_id():
37 continue
38
39 if door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY:
40 continue
41
42 var locationListener = ap.SCRIPT_locationListener.new()
43 locationListener.location_id = door.get_ap_id()
44 locationListener.name = "locationListener_%d" % door.get_ap_id()
45
46 for panel_ref in door.get_panels():
47 var panel_data = gamedata.objects.get_panels()[panel_ref.get_panel()]
48 var panel_path = panel_data.get_path()
49
50 if panel_ref.has_answer():
51 for proxy in panel_data.get_proxies():
52 if proxy.get_answer() == panel_ref.get_answer():
53 panel_path = proxy.get_path()
54 break
55
56 locationListener.senders.append(NodePath("/root/scene/" + panel_path))
57
58 for keyholder_ref in door.get_keyholders():
59 var keyholder_data = gamedata.objects.get_keyholders()[keyholder_ref.get_keyholder()]
60
61 var khl = khl_script.new()
62 khl.name = (
63 "location_%d_keyholder_%d" % [door.get_ap_id(), keyholder_ref.get_keyholder()]
64 )
65 khl.answer = keyholder_ref.get_key()
66 khl.senders.append(NodePath("/root/scene/" + keyholder_data.get_path()))
67 get_parent().add_child.call_deferred(khl)
68
69 locationListener.senders.append(NodePath("../" + khl.name))
70
71 get_parent().add_child.call_deferred(locationListener)
72
73 # Set up letter locations.
74 for letter in gamedata.objects.get_letters():
75 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
76 if room.get_map_id() != map_id:
77 continue
78
79 var locationListener = ap.SCRIPT_locationListener.new()
80 locationListener.location_id = letter.get_ap_id()
81 locationListener.name = "locationListener_%d" % letter.get_ap_id()
82 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
83
84 get_parent().add_child.call_deferred(locationListener)
85
86 if (
87 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
88 != ap.kLETTER_BEHAVIOR_VANILLA
89 ):
90 var scout = ap.scout_location(letter.get_ap_id())
91 if scout != null:
92 var item_name = "Unknown"
93 var item_player_game = ap.client._game_by_player[float(scout["player"])]
94 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
95 item_name = ap.client._item_id_to_name[item_player_game][scout["item"]]
96
97 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
98 letter.get_path()
99 )
100 if collectable != null:
101 collectable.setScoutedText.call_deferred(item_name)
102
103 # Set up mastery locations.
104 for mastery in gamedata.objects.get_masteries():
105 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
106 if room.get_map_id() != map_id:
107 continue
108
109 var locationListener = ap.SCRIPT_locationListener.new()
110 locationListener.location_id = mastery.get_ap_id()
111 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
112 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
113
114 get_parent().add_child.call_deferred(locationListener)
115
116 # Set up ending locations.
117 for ending in gamedata.objects.get_endings():
118 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
119 if room.get_map_id() != map_id:
120 continue
121
122 var locationListener = ap.SCRIPT_locationListener.new()
123 locationListener.location_id = ending.get_ap_id()
124 locationListener.name = "locationListener_%d" % ending.get_ap_id()
125 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
126
127 get_parent().add_child.call_deferred(locationListener)
128
129 if kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
130 var victoryListener = ap.SCRIPT_victoryListener.new()
131 victoryListener.name = "victoryListener"
132 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
133
134 get_parent().add_child.call_deferred(victoryListener)
135
136 # Set up keyholder locations, in keyholder sanity.
137 if ap.keyholder_sanity:
138 for keyholder in gamedata.objects.get_keyholders():
139 if not keyholder.has_key():
140 continue
141
142 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
143 if room.get_map_id() != map_id:
144 continue
145
146 var locationListener = ap.SCRIPT_locationListener.new()
147 locationListener.location_id = keyholder.get_ap_id()
148 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
149
150 var khl = khl_script.new()
151 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
152 khl.answer = keyholder.get_key()
153 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
154 get_parent().add_child.call_deferred(khl)
155
156 locationListener.senders.append(NodePath("../" + khl.name))
157
158 get_parent().add_child.call_deferred(locationListener)
159
160 # Block off roof access in Daedalus.
161 if global.map == "daedalus" and not ap.daedalus_roof_access:
162 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
163 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
164 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
165 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
166 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
167 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
168 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
169 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
170 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
171 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
172 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
173 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
174 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
175 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
176 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
177
178 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
179 var warp_exit = warp_exit_prefab.instantiate()
180 warp_exit.name = "roof_access_blocker_warp_exit"
181 warp_exit.position = Vector3(58, 10, 0)
182 warp_exit.rotation_degrees.y = 90
183 get_parent().add_child.call_deferred(warp_exit)
184
185 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
186 var warp_enter = warp_enter_prefab.instantiate()
187 warp_enter.target = warp_exit
188 warp_enter.position = Vector3(76.5, 30, 1)
189 warp_enter.scale = Vector3(4, 1.5, 1)
190 warp_enter.rotation_degrees.y = 90
191 get_parent().add_child.call_deferred(warp_enter)
192
193 if global.map == "the_entry":
194 # Remove door behind X1.
195 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
196 door_node.handleTriggered()
197
198 # Display win condition.
199 var sign_prefab = preload("res://objects/nodes/sign.tscn")
200 var sign1 = sign_prefab.instantiate()
201 sign1.position = Vector3(-7, 5, -15.01)
202 sign1.text = "victory"
203 get_parent().add_child.call_deferred(sign1)
204
205 var sign2 = sign_prefab.instantiate()
206 sign2.position = Vector3(-7, 4, -15.01)
207 sign2.text = "%s ending" % kEndingNameByVictoryValue.get(ap.victory_condition, "?")
208
209 var sign2_color = kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
210 if sign2_color == "white":
211 sign2_color = "silver"
212
213 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
214 get_parent().add_child.call_deferred(sign2)
215
216 super._ready()
217
218 await get_tree().process_frame
219 await get_tree().process_frame
220
221 ap.stop_batching_locations()
222
223
224func _set_up_invis_wall(x, y, z, sx, sy, sz):
225 var prefab = preload("res://objects/nodes/block.tscn")
226 var newwall = prefab.instantiate()
227 newwall.position.x = x
228 newwall.position.y = y
229 newwall.position.z = z
230 newwall.scale.x = sz
231 newwall.scale.y = sy
232 newwall.scale.z = sx
233 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
234 newwall.visibility_range_end = 3
235 newwall.visibility_range_end_margin = 1
236 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
237 newwall.skeleton = ".."
238 get_parent().add_child.call_deferred(newwall)
diff --git a/client/Archipelago/saver.gd b/client/Archipelago/saver.gd new file mode 100644 index 0000000..0fba9e7 --- /dev/null +++ b/client/Archipelago/saver.gd
@@ -0,0 +1,9 @@
1extends "res://scripts/nodes/saver.gd"
2
3
4func levelLoaded():
5 if type == "keyholders":
6 var ap = global.get_node("Archipelago")
7 ap.keyboard.load_keyholders.call_deferred(global.map)
8 else:
9 reload.call_deferred()
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..14975e5 --- /dev/null +++ b/client/Archipelago/settings_screen.gd
@@ -0,0 +1,241 @@
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 ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd")
26 ap_instance.SCRIPT_keyboard = load("user://maps/Archipelago/keyboard.gd")
27 ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd")
28 ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd")
29 ap_instance.SCRIPT_victoryListener = load("user://maps/Archipelago/victoryListener.gd")
30
31 global.add_child(ap_instance)
32
33 # Let's also inject any scripts we need to inject now.
34 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/animationListener.gd"))
35 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/collectable.gd"))
36 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd"))
37 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolder.gd"))
38 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/keyHolderChecker.gd"))
39 installScriptExtension(
40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd")
41 )
42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/panel.gd"))
44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd"))
45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd"))
47 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleport.gd"))
48 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleportListener.gd"))
49 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/visibilityListener.gd"))
50 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldportListener.gd"))
51
52 var proto_script = load("user://maps/Archipelago/generated/proto.gd")
53 var gamedata_script = load("user://maps/Archipelago/gamedata.gd")
54 var gamedata_instance = gamedata_script.new(proto_script)
55 gamedata_instance.load(
56 FileAccess.get_file_as_bytes("user://maps/Archipelago/generated/data.binpb")
57 )
58 gamedata_instance.name = "Gamedata"
59 global.add_child(gamedata_instance)
60
61 var messages_script = load("user://maps/Archipelago/messages.gd")
62 var messages_instance = messages_script.new()
63 messages_instance.name = "Messages"
64 global.add_child(messages_instance)
65
66 var textclient_script = load("user://maps/Archipelago/textclient.gd")
67 var textclient_instance = textclient_script.new()
68 textclient_instance.name = "Textclient"
69 global.add_child(textclient_instance)
70
71 var ap = global.get_node("Archipelago")
72 var gamedata = global.get_node("Gamedata")
73 ap.connect("ap_connected", connectionSuccessful)
74 ap.connect("could_not_connect", connectionUnsuccessful)
75 ap.connect("connect_status", connectionStatus)
76
77 # Populate textboxes with AP settings.
78 $Panel/server_box.text = ap.ap_server
79 $Panel/player_box.text = ap.ap_user
80 $Panel/password_box.text = ap.ap_pass
81
82 var history_box = $Panel/connection_history
83 if ap.connection_history.is_empty():
84 history_box.disabled = true
85 else:
86 history_box.disabled = false
87
88 var i = 0
89 for details in ap.connection_history:
90 history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
91 i += 1
92
93 history_box.get_popup().connect("id_pressed", historySelected)
94
95 # Show client version.
96 $Panel/title.text = "ARCHIPELAGO (%d.%d)" % [gamedata.objects.get_version(), ap.MOD_VERSION]
97
98 # Increase font size in text boxes.
99 $Panel/server_box.add_theme_font_size_override("font_size", 36)
100 $Panel/player_box.add_theme_font_size_override("font_size", 36)
101 $Panel/password_box.add_theme_font_size_override("font_size", 36)
102
103 # Set up version mismatch dialog.
104 $Panel/VersionMismatch.connect("confirmed", startGame)
105 $Panel/VersionMismatch.get_cancel_button().pressed.connect(versionMismatchDeclined)
106
107
108# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
109func installScriptExtension(childScript: Resource):
110 # Force Godot to compile the script now.
111 # We need to do this here to ensure that the inheritance chain is
112 # properly set up, and multiple mods can chain-extend the same
113 # class multiple times.
114 # This is also needed to make Godot instantiate the extended class
115 # when creating singletons.
116 # The actual instance is thrown away.
117 childScript.new()
118
119 var parentScript = childScript.get_base_script()
120 var parentScriptPath = parentScript.resource_path
121 global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
122 childScript.take_over_path(parentScriptPath)
123
124
125func connectionStatus(message):
126 var popup = self.get_node("Panel/AcceptDialog")
127 popup.title = "Connecting to Archipelago"
128 popup.dialog_text = message
129 popup.exclusive = true
130 popup.get_ok_button().visible = false
131 popup.popup_centered()
132
133
134func connectionSuccessful():
135 var ap = global.get_node("Archipelago")
136 var gamedata = global.get_node("Gamedata")
137
138 # Check for major version mismatch.
139 if ap.apworld_version[0] != gamedata.objects.get_version():
140 $Panel/AcceptDialog.exclusive = false
141
142 var popup = self.get_node("Panel/VersionMismatch")
143 popup.title = "Version Mismatch!"
144 popup.dialog_text = (
145 "This slot was generated using v%d.%d of the Lingo 2 apworld,\nwhich has a different major version than this client (v%d.%d).\nIt is highly recommended to play using the correct version of the client.\nYou may experience bugs or logic issues if you continue."
146 % [
147 ap.apworld_version[0],
148 ap.apworld_version[1],
149 gamedata.objects.get_version(),
150 ap.MOD_VERSION
151 ]
152 )
153 popup.exclusive = true
154 popup.popup_centered()
155
156 return
157
158 startGame()
159
160
161func startGame():
162 var ap = global.get_node("Archipelago")
163
164 # Save connection details
165 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
166 if ap.connection_history.has(connection_details):
167 ap.connection_history.erase(connection_details)
168 ap.connection_history.push_front(connection_details)
169 if ap.connection_history.size() > 10:
170 ap.connection_history.resize(10)
171 ap.saveSettings()
172
173 # Switch to the_entry
174 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
175 global.user = ap.getSaveFileName()
176 global.universe = "lingo"
177 global.map = "the_entry"
178
179 unlocks.resetCollectables()
180 unlocks.resetData()
181
182 ap.setup_keys()
183
184 unlocks.loadCollectables()
185 unlocks.loadData()
186 unlocks.unlockKey("capslock", 1)
187
188 clearResourceCache("res://objects/meshes/gridDoor.tscn")
189 clearResourceCache("res://objects/nodes/collectable.tscn")
190 clearResourceCache("res://objects/nodes/door.tscn")
191 clearResourceCache("res://objects/nodes/keyHolder.tscn")
192 clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
193 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
194 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
195 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
196 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
197 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
198 clearResourceCache("res://objects/nodes/panel.tscn")
199 clearResourceCache("res://objects/nodes/player.tscn")
200 clearResourceCache("res://objects/nodes/saver.tscn")
201 clearResourceCache("res://objects/nodes/teleport.tscn")
202 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
203
204 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
205 if paintings_dir:
206 paintings_dir.list_dir_begin()
207 var file_name = paintings_dir.get_next()
208 while file_name != "":
209 if not paintings_dir.current_is_dir() and file_name.ends_with(".tscn"):
210 clearResourceCache("res://objects/meshes/paintings/" + file_name)
211 file_name = paintings_dir.get_next()
212
213 switcher.switch_map.call_deferred("res://objects/scenes/the_entry.tscn")
214
215
216func connectionUnsuccessful(error_message):
217 $Panel/connect_button.disabled = false
218
219 var popup = $Panel/AcceptDialog
220 popup.title = "Could not connect to Archipelago"
221 popup.dialog_text = error_message
222 popup.exclusive = true
223 popup.get_ok_button().visible = true
224 popup.popup_centered()
225
226
227func versionMismatchDeclined():
228 $Panel/AcceptDialog.hide()
229
230
231func historySelected(index):
232 var ap = global.get_node("Archipelago")
233 var details = ap.connection_history[index]
234
235 $Panel/server_box.text = details[0]
236 $Panel/player_box.text = details[1]
237 $Panel/password_box.text = details[2]
238
239
240func clearResourceCache(path):
241 ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
diff --git a/client/Archipelago/teleport.gd b/client/Archipelago/teleport.gd new file mode 100644 index 0000000..428d50b --- /dev/null +++ b/client/Archipelago/teleport.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/teleport.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
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.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/teleportListener.gd b/client/Archipelago/teleportListener.gd new file mode 100644 index 0000000..6f363af --- /dev/null +++ b/client/Archipelago/teleportListener.gd
@@ -0,0 +1,49 @@
1extends "res://scripts/nodes/listeners/teleportListener.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 if (
13 global.map == "daedalus"
14 and (
15 node_path == "Components/Triggers/teleportListenerConnections"
16 or node_path == "Components/Triggers/teleportListenerConnections2"
17 )
18 ):
19 # Effectively disable these.
20 teleport_point = target_path.position
21 return
22
23 var gamedata = global.get_node("Gamedata")
24 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
25 if door_id != null:
26 var ap = global.get_node("Archipelago")
27 var item_lock = ap.get_item_id_for_door(door_id)
28
29 if item_lock != null:
30 item_id = item_lock[0]
31 item_amount = item_lock[1]
32
33 self.senders = []
34 self.senderGroup = []
35 self.nested = false
36 self.complete_at = 0
37 self.max_length = 0
38 self.excludeSenders = []
39
40 call_deferred("_readier")
41
42 super._ready()
43
44
45func _readier():
46 var ap = global.get_node("Archipelago")
47
48 if ap.client.getItemAmount(item_id) >= item_amount:
49 handleTriggered()
diff --git a/client/Archipelago/textclient.gd b/client/Archipelago/textclient.gd new file mode 100644 index 0000000..85cc6d2 --- /dev/null +++ b/client/Archipelago/textclient.gd
@@ -0,0 +1,86 @@
1extends CanvasLayer
2
3var panel
4var label
5var entry
6var is_open = false
7
8
9func _ready():
10 process_mode = ProcessMode.PROCESS_MODE_ALWAYS
11
12 panel = Panel.new()
13 panel.set_name("Panel")
14 panel.offset_left = 100
15 panel.offset_right = 1820
16 panel.offset_top = 100
17 panel.offset_bottom = 980
18 panel.visible = false
19 add_child(panel)
20
21 label = RichTextLabel.new()
22 label.set_name("Label")
23 label.offset_left = 80
24 label.offset_right = 1640
25 label.offset_top = 80
26 label.offset_bottom = 720
27 label.scroll_following = true
28 label.selection_enabled = true
29 panel.add_child(label)
30
31 label.push_font(load("res://assets/fonts/Lingo2.ttf"))
32 label.push_font_size(36)
33
34 var entry_style = StyleBoxFlat.new()
35 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
36
37 entry = LineEdit.new()
38 entry.set_name("Entry")
39 entry.offset_left = 80
40 entry.offset_right = 1640
41 entry.offset_top = 760
42 entry.offset_bottom = 840
43 entry.add_theme_font_override("font", load("res://assets/fonts/Lingo2.ttf"))
44 entry.add_theme_font_size_override("font_size", 36)
45 entry.add_theme_color_override("font_color", Color(0, 0, 0, 1))
46 entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
47 entry.add_theme_stylebox_override("focus", entry_style)
48 panel.add_child(entry)
49 entry.connect("text_submitted", text_entered)
50
51
52func _input(event):
53 if global.loaded and event is InputEventKey and event.pressed:
54 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
55 if !get_tree().paused:
56 is_open = true
57 get_tree().paused = true
58 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
59 panel.visible = true
60 entry.grab_focus()
61 get_viewport().set_input_as_handled()
62 else:
63 dismiss()
64 elif event.keycode == KEY_ESCAPE:
65 if is_open:
66 dismiss()
67 get_viewport().set_input_as_handled()
68
69
70func dismiss():
71 if is_open:
72 get_tree().paused = false
73 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
74 panel.visible = false
75 is_open = false
76
77
78func parse_printjson(text):
79 label.append_text("[p]" + text + "[/p]")
80
81
82func text_entered(text):
83 var ap = global.get_node("Archipelago")
84 var cmd = text.trim_suffix("\n")
85 ap.client.say(cmd)
86 entry.text = ""
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
diff --git a/client/Archipelago/victoryListener.gd b/client/Archipelago/victoryListener.gd new file mode 100644 index 0000000..e9089d7 --- /dev/null +++ b/client/Archipelago/victoryListener.gd
@@ -0,0 +1,20 @@
1extends Receiver
2
3
4func _ready():
5 super._ready()
6
7
8func handleTriggered():
9 triggered += 1
10 if triggered >= total:
11 var ap = global.get_node("Archipelago")
12 ap.client.completedGoal()
13
14 global.get_node("Messages").showMessage("You have completed your goal!")
15
16
17func handleUntriggered():
18 triggered -= 1
19 if triggered < total:
20 pass
diff --git a/client/Archipelago/visibilityListener.gd b/client/Archipelago/visibilityListener.gd new file mode 100644 index 0000000..5ea17a0 --- /dev/null +++ b/client/Archipelago/visibilityListener.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/listeners/visibilityListener.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
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.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/worldportListener.gd b/client/Archipelago/worldportListener.gd new file mode 100644 index 0000000..5c2faff --- /dev/null +++ b/client/Archipelago/worldportListener.gd
@@ -0,0 +1,8 @@
1extends "res://scripts/nodes/listeners/worldportListener.gd"
2
3
4func handleTriggered():
5 if exit == "menus/credits":
6 return
7
8 super.handleTriggered()