about summary refs log tree commit diff stats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/Archipelago/animationListener.gd38
-rw-r--r--client/Archipelago/client.gd417
-rw-r--r--client/Archipelago/collectable.gd16
-rw-r--r--client/Archipelago/door.gd38
-rw-r--r--client/Archipelago/gamedata.gd140
-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.gd199
-rw-r--r--client/Archipelago/locationListener.gd20
-rw-r--r--client/Archipelago/manager.gd528
-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.gd244
-rw-r--r--client/Archipelago/saver.gd9
-rw-r--r--client/Archipelago/settings_buttons.gd24
-rw-r--r--client/Archipelago/settings_screen.gd244
-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
-rw-r--r--client/CHANGELOG.md21
-rw-r--r--client/README.md90
-rw-r--r--client/archipelago.tscn284
30 files changed, 135 insertions, 2914 deletions
diff --git a/client/Archipelago/animationListener.gd b/client/Archipelago/animationListener.gd deleted file mode 100644 index c3b26db..0000000 --- a/client/Archipelago/animationListener.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index 843647d..0000000 --- a/client/Archipelago/client.gd +++ /dev/null
@@ -1,417 +0,0 @@
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 _ws.inbound_buffer_size = 8388608
51
52 global._print("Instantiated APClient")
53
54 # Read AP datapackages from file, if there are any
55 if FileAccess.file_exists("user://ap_datapackages"):
56 var file = FileAccess.open("user://ap_datapackages", FileAccess.READ)
57 var data = file.get_var(true)
58 file.close()
59
60 if typeof(data) != TYPE_DICTIONARY:
61 global._print("AP datapackages file is corrupted")
62 data = {}
63
64 _datapackages = data
65
66 processDatapackages()
67
68
69func _ready():
70 pass
71 #_ws.connect("connection_closed", _closed)
72 #_ws.connect("connection_failed", _closed)
73 #_ws.connect("server_disconnected", _closed)
74 #_ws.connect("connection_error", _errored)
75 #_ws.connect("connection_established", _connected)
76
77
78func _reset_state():
79 _should_process = false
80 _authenticated = false
81 _try_wss = false
82 _has_connected = false
83 _received_items = {}
84 _received_indexes = []
85
86
87func _errored():
88 if _try_wss:
89 global._print("Could not connect to AP with ws://, now trying wss://")
90 connectToServer(ap_server, ap_user, ap_pass)
91 else:
92 global._print("AP connection failed")
93 _reset_state()
94
95 emit_signal(
96 "could_not_connect",
97 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information."
98 )
99
100
101func _closed(_was_clean = true):
102 global._print("Connection closed")
103 _reset_state()
104
105 if not _initiated_disconnect:
106 emit_signal("could_not_connect", "Disconnected from Archipelago")
107
108 _initiated_disconnect = false
109
110
111func _connected(_proto = ""):
112 global._print("Connected!")
113 _try_wss = false
114
115
116func disconnect_from_ap():
117 _initiated_disconnect = true
118 _ws.close()
119
120
121func _process(_delta):
122 if _should_process:
123 _ws.poll()
124
125 var state = _ws.get_ready_state()
126 if state == WebSocketPeer.STATE_OPEN:
127 if not _has_connected:
128 _has_connected = true
129
130 _connected()
131
132 while _ws.get_available_packet_count():
133 var packet = _ws.get_packet()
134 global._print("Got data from server: " + packet.get_string_from_utf8())
135 var json = JSON.new()
136 var jserror = json.parse(packet.get_string_from_utf8())
137 if jserror != OK:
138 global._print("Error parsing packet from AP: " + jserror.error_string)
139 return
140
141 for message in json.data:
142 var cmd = message["cmd"]
143 global._print("Received command: " + cmd)
144
145 if cmd == "RoomInfo":
146 _seed = message["seed_name"]
147 _remote_version = message["version"]
148 _gen_version = message["generator_version"]
149
150 var needed_games = []
151 for game in message["datapackage_checksums"].keys():
152 if (
153 !_datapackages.has(game)
154 or (
155 _datapackages[game]["checksum"]
156 != message["datapackage_checksums"][game]
157 )
158 ):
159 needed_games.append(game)
160
161 if !needed_games.is_empty():
162 _pending_packages = needed_games
163 var cur_needed = _pending_packages.pop_front()
164 requestDatapackages([cur_needed])
165 else:
166 connectToRoom()
167
168 elif cmd == "DataPackage":
169 for game in message["data"]["games"].keys():
170 _datapackages[game] = message["data"]["games"][game]
171 saveDatapackages()
172
173 if !_pending_packages.is_empty():
174 var cur_needed = _pending_packages.pop_front()
175 requestDatapackages([cur_needed])
176 else:
177 processDatapackages()
178 connectToRoom()
179
180 elif cmd == "Connected":
181 _authenticated = true
182 _team = message["team"]
183 _slot = message["slot"]
184 _players = message["players"]
185 _checked_locations = message["checked_locations"]
186 _slot_data = message["slot_data"]
187
188 for player in _players:
189 _player_name_by_slot[player["slot"]] = player["alias"]
190 _game_by_player[player["slot"]] = message["slot_info"][str(
191 player["slot"]
192 )]["game"]
193
194 emit_signal("client_connected", _slot_data)
195
196 elif cmd == "ConnectionRefused":
197 var error_message = ""
198 for error in message["errors"]:
199 var submsg = ""
200 if error == "InvalidSlot":
201 submsg = "Invalid player name."
202 elif error == "InvalidGame":
203 submsg = "The specified player is not playing Lingo."
204 elif error == "IncompatibleVersion":
205 submsg = (
206 "The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d."
207 % [
208 ap_version["major"],
209 ap_version["minor"],
210 ap_version["build"],
211 _remote_version["major"],
212 _remote_version["minor"],
213 _remote_version["build"]
214 ]
215 )
216 elif error == "InvalidPassword":
217 submsg = "Incorrect password."
218 elif error == "InvalidItemsHandling":
219 submsg = "Invalid item handling flag. This is a bug with the client."
220
221 if submsg != "":
222 if error_message != "":
223 error_message += " "
224 error_message += submsg
225
226 if error_message == "":
227 error_message = "Unknown error."
228
229 _initiated_disconnect = true
230 _ws.close()
231
232 emit_signal("could_not_connect", error_message)
233 global._print("Connection to AP refused")
234 global._print(message)
235
236 elif cmd == "ReceivedItems":
237 var i = 0
238 for item in message["items"]:
239 var index = int(message["index"] + i)
240 i += 1
241
242 if _received_indexes.has(index):
243 # Do not re-process items.
244 continue
245
246 _received_indexes.append(index)
247
248 var item_id = int(item["item"])
249 _received_items[item_id] = _received_items.get(item_id, 0) + 1
250
251 emit_signal(
252 "item_received",
253 item_id,
254 index,
255 int(item["player"]),
256 int(item["flags"]),
257 _received_items[item_id]
258 )
259
260 elif cmd == "PrintJSON":
261 emit_signal("message_received", message)
262
263 elif cmd == "LocationInfo":
264 for loc in message["locations"]:
265 emit_signal(
266 "location_scout_received",
267 int(loc["item"]),
268 int(loc["location"]),
269 int(loc["player"]),
270 int(loc["flags"])
271 )
272
273 elif state == WebSocketPeer.STATE_CLOSED:
274 if _has_connected:
275 _closed()
276 else:
277 _errored()
278
279
280func saveDatapackages():
281 # Save the AP datapackages to disk.
282 var file = FileAccess.open("user://ap_datapackages", FileAccess.WRITE)
283 file.store_var(_datapackages, true)
284 file.close()
285
286
287func connectToServer(server, un, pw):
288 ap_server = server
289 ap_user = un
290 ap_pass = pw
291
292 _initiated_disconnect = false
293
294 var url = ""
295 if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"):
296 url = ap_server
297 _try_wss = false
298 elif _try_wss:
299 url = "wss://" + ap_server
300 _try_wss = false
301 else:
302 url = "ws://" + ap_server
303 _try_wss = true
304
305 var err = _ws.connect_to_url(url)
306 if err != OK:
307 emit_signal(
308 "could_not_connect",
309 (
310 "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d."
311 % err
312 )
313 )
314 global._print("Could not connect to AP: %d" % err)
315 return
316 _should_process = true
317
318 emit_signal("connect_status", "Connecting...")
319
320
321func sendMessage(msg):
322 var payload = JSON.stringify(msg)
323 _ws.send_text(payload)
324
325
326func requestDatapackages(games):
327 emit_signal("connect_status", "Downloading %s data package..." % games[0])
328
329 sendMessage([{"cmd": "GetDataPackage", "games": games}])
330
331
332func processDatapackages():
333 _item_id_to_name = {}
334 _location_id_to_name = {}
335 for game in _datapackages.keys():
336 var package = _datapackages[game]
337
338 _item_id_to_name[game] = {}
339 for item_name in package["item_name_to_id"].keys():
340 _item_id_to_name[game][int(package["item_name_to_id"][item_name])] = item_name
341
342 _location_id_to_name[game] = {}
343 for location_name in package["location_name_to_id"].keys():
344 _location_id_to_name[game][int(package["location_name_to_id"][location_name])] = location_name
345
346 if _datapackages.has("Lingo 2"):
347 _item_name_to_id = _datapackages["Lingo 2"]["item_name_to_id"]
348 _location_name_to_id = _datapackages["Lingo 2"]["location_name_to_id"]
349
350
351func connectToRoom():
352 emit_signal("connect_status", "Authenticating...")
353
354 sendMessage(
355 [
356 {
357 "cmd": "Connect",
358 "password": ap_pass,
359 "game": "Lingo 2",
360 "name": ap_user,
361 "uuid": SCRIPT_uuid.v4(),
362 "version": ap_version,
363 "items_handling": 0b111, # always receive our items
364 "tags": [],
365 "slot_data": true
366 }
367 ]
368 )
369
370
371func sendConnectUpdate(tags):
372 sendMessage([{"cmd": "ConnectUpdate", "tags": tags}])
373
374
375func requestSync():
376 sendMessage([{"cmd": "Sync"}])
377
378
379func sendLocation(loc_id):
380 sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
381
382
383func sendLocations(loc_ids):
384 sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}])
385
386
387func setValue(key, value, operation = "replace"):
388 sendMessage(
389 [
390 {
391 "cmd": "Set",
392 "key": "Lingo2_%d_%s" % [_slot, key],
393 "want_reply": false,
394 "operations": [{"operation": operation, "value": value}]
395 }
396 ]
397 )
398
399
400func say(textdata):
401 sendMessage([{"cmd": "Say", "text": textdata}])
402
403
404func completedGoal():
405 sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL
406
407
408func scoutLocations(loc_ids):
409 sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}])
410
411
412func hasItem(item_id):
413 return _received_items.has(item_id)
414
415
416func getItemAmount(item_id):
417 return _received_items.get(item_id, 0)
diff --git a/client/Archipelago/collectable.gd b/client/Archipelago/collectable.gd deleted file mode 100644 index 4a17a2a..0000000 --- a/client/Archipelago/collectable.gd +++ /dev/null
@@ -1,16 +0,0 @@
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 deleted file mode 100644 index fead818..0000000 --- a/client/Archipelago/door.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index 41d966a..0000000 --- a/client/Archipelago/gamedata.gd +++ /dev/null
@@ -1,140 +0,0 @@
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 = []
14var anti_trap_ids = {}
15
16var kSYMBOL_ITEMS
17
18
19func _init(proto_script):
20 SCRIPT_proto = proto_script
21
22 kSYMBOL_ITEMS = {
23 SCRIPT_proto.PuzzleSymbol.SUN: "Sun Symbol",
24 SCRIPT_proto.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
25 SCRIPT_proto.PuzzleSymbol.ZERO: "Zero Symbol",
26 SCRIPT_proto.PuzzleSymbol.EXAMPLE: "Example Symbol",
27 SCRIPT_proto.PuzzleSymbol.BOXES: "Boxes Symbol",
28 SCRIPT_proto.PuzzleSymbol.PLANET: "Planet Symbol",
29 SCRIPT_proto.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
30 SCRIPT_proto.PuzzleSymbol.CROSS: "Cross Symbol",
31 SCRIPT_proto.PuzzleSymbol.SWEET: "Sweet Symbol",
32 SCRIPT_proto.PuzzleSymbol.GENDER: "Gender Symbol",
33 SCRIPT_proto.PuzzleSymbol.AGE: "Age Symbol",
34 SCRIPT_proto.PuzzleSymbol.SOUND: "Sound Symbol",
35 SCRIPT_proto.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
36 SCRIPT_proto.PuzzleSymbol.JOB: "Job Symbol",
37 SCRIPT_proto.PuzzleSymbol.STARS: "Stars Symbol",
38 SCRIPT_proto.PuzzleSymbol.NULL: "Null Symbol",
39 SCRIPT_proto.PuzzleSymbol.EVAL: "Eval Symbol",
40 SCRIPT_proto.PuzzleSymbol.LINGO: "Lingo Symbol",
41 SCRIPT_proto.PuzzleSymbol.QUESTION: "Question Symbol",
42 }
43
44
45func load(data_bytes):
46 objects = SCRIPT_proto.AllObjects.new()
47
48 var result_code = objects.from_bytes(data_bytes)
49 if result_code != SCRIPT_proto.PB_ERR.NO_ERRORS:
50 print("Could not load generated data: %d" % result_code)
51 return
52
53 for map in objects.get_maps():
54 map_id_by_name[map.get_name()] = map.get_id()
55
56 for door in objects.get_doors():
57 var map = objects.get_maps()[door.get_map_id()]
58
59 if not map.get_name() in door_id_by_map_node_path:
60 door_id_by_map_node_path[map.get_name()] = {}
61
62 var map_data = door_id_by_map_node_path[map.get_name()]
63 for receiver in door.get_receivers():
64 map_data[receiver] = door.get_id()
65
66 for painting_id in door.get_move_paintings():
67 var painting = objects.get_paintings()[painting_id]
68 map_data[painting.get_path()] = door.get_id()
69
70 if door.has_ap_id():
71 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
72
73 for painting in objects.get_paintings():
74 var room = objects.get_rooms()[painting.get_room_id()]
75 var map = objects.get_maps()[room.get_map_id()]
76
77 if not map.get_name() in painting_id_by_map_node_path:
78 painting_id_by_map_node_path[map.get_name()] = {}
79
80 var _map_data = painting_id_by_map_node_path[map.get_name()]
81
82 for progressive in objects.get_progressives():
83 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
84
85 for letter in objects.get_letters():
86 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
87
88 for panel in objects.get_panels():
89 var room = objects.get_rooms()[panel.get_room_id()]
90 var map = objects.get_maps()[room.get_map_id()]
91
92 if not map.get_name() in panel_id_by_map_node_path:
93 panel_id_by_map_node_path[map.get_name()] = {}
94
95 var map_data = panel_id_by_map_node_path[map.get_name()]
96 map_data[panel.get_path()] = panel.get_id()
97
98 for symbol_name in kSYMBOL_ITEMS.values():
99 symbol_item_ids.append(objects.get_special_ids()[symbol_name])
100
101 for special_name in objects.get_special_ids().keys():
102 if special_name.begins_with("Anti "):
103 anti_trap_ids[objects.get_special_ids()[special_name]] = (
104 special_name.substr(5).to_lower()
105 )
106
107
108func get_door_for_map_node_path(map_name, node_path):
109 if not door_id_by_map_node_path.has(map_name):
110 return null
111
112 var map_data = door_id_by_map_node_path[map_name]
113 return map_data.get(node_path, null)
114
115
116func get_panel_for_map_node_path(map_name, node_path):
117 if not panel_id_by_map_node_path.has(map_name):
118 return null
119
120 var map_data = panel_id_by_map_node_path[map_name]
121 return map_data.get(node_path, null)
122
123
124func get_door_ap_id(door_id):
125 var door = objects.get_doors()[door_id]
126 if door.has_ap_id():
127 return door.get_ap_id()
128 else:
129 return null
130
131
132func get_door_receivers(door_id):
133 var door = objects.get_doors()[door_id]
134 return door.get_receivers()
135
136
137func get_door_map_name(door_id):
138 var door = objects.get_doors()[door_id]
139 var map = objects.get_maps()[door.get_map_id()]
140 return map.get_name()
diff --git a/client/Archipelago/keyHolder.gd b/client/Archipelago/keyHolder.gd deleted file mode 100644 index 3c037ff..0000000 --- a/client/Archipelago/keyHolder.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index a75a9e4..0000000 --- a/client/Archipelago/keyHolderChecker.gd +++ /dev/null
@@ -1,24 +0,0 @@
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 deleted file mode 100644 index d5300f3..0000000 --- a/client/Archipelago/keyHolderResetterListener.gd +++ /dev/null
@@ -1,8 +0,0 @@
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 deleted file mode 100644 index 450566d..0000000 --- a/client/Archipelago/keyboard.gd +++ /dev/null
@@ -1,199 +0,0 @@
1extends Node
2
3const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz"
4
5var letters_saved = {}
6var letters_in_keyholders = []
7var letters_blocked = []
8var letters_dynamic = {}
9var keyholder_state = {}
10
11var filename = ""
12
13
14func _init():
15 reset()
16
17
18func reset():
19 letters_saved.clear()
20 letters_in_keyholders.clear()
21 letters_blocked.clear()
22 letters_dynamic.clear()
23 keyholder_state.clear()
24
25
26func load_seed():
27 var ap = global.get_node("Archipelago")
28
29 reset()
30
31 filename = "user://archipelago_keys/%s_%d" % [ap.client._seed, ap.client._slot]
32
33 if FileAccess.file_exists(filename):
34 var ap_file = FileAccess.open(filename, FileAccess.READ)
35 var localdata = []
36 if ap_file != null:
37 localdata = ap_file.get_var(true)
38 ap_file.close()
39
40 if typeof(localdata) != TYPE_ARRAY:
41 print("AP keyboard file is corrupted")
42 localdata = []
43
44 if localdata.size() > 0:
45 letters_saved = localdata[0]
46 if localdata.size() > 1:
47 letters_in_keyholders = localdata[1]
48 if localdata.size() > 2:
49 keyholder_state = localdata[2]
50
51 for k in kALL_LETTERS:
52 var level = 0
53
54 if ap.get_letter_behavior(k, false) == ap.kLETTER_BEHAVIOR_UNLOCKED:
55 level += 1
56 if ap.get_letter_behavior(k, true) == ap.kLETTER_BEHAVIOR_UNLOCKED:
57 level += 1
58
59 letters_dynamic[k] = level
60
61 update_unlocks()
62
63
64func save():
65 var dir = DirAccess.open("user://")
66 var folder = "archipelago_keys"
67 if not dir.dir_exists(folder):
68 dir.make_dir(folder)
69
70 var file = FileAccess.open(filename, FileAccess.WRITE)
71
72 var data = [
73 letters_saved,
74 letters_in_keyholders,
75 keyholder_state,
76 ]
77 file.store_var(data, true)
78 file.close()
79
80
81func update_unlocks():
82 unlocks.resetKeys()
83
84 var has_doubles = false
85
86 for k in kALL_LETTERS:
87 var level = 0
88
89 if not letters_in_keyholders.has(k):
90 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
91
92 if level >= 2:
93 level = 2
94 has_doubles = true
95
96 if letters_blocked.has(k):
97 level = 0
98
99 unlocks.unlockKey(k, level)
100
101 if has_doubles and unlocks.data["double_letters"] != "unlocked":
102 var ap = global.get_node("Archipelago")
103 if ap.cyan_door_behavior == ap.kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER:
104 unlocks.setData("double_letters", "unlocked")
105
106
107func collect_local_letter(key, level):
108 if level < 0 or level > 2 or level < letters_saved.get(key, 0):
109 return
110
111 letters_saved[key] = level
112
113 if letters_blocked.has(key):
114 letters_blocked.erase(key)
115
116 update_unlocks()
117 save()
118
119
120func collect_remote_letter(key, level):
121 if level < 0 or level > 2 or level < letters_dynamic.get(key, 0):
122 return
123
124 letters_dynamic[key] = level
125
126 if letters_blocked.has(key):
127 letters_blocked.erase(key)
128
129 update_unlocks()
130 save()
131
132
133func put_in_keyholder(key, map, kh_path):
134 if not keyholder_state.has(map):
135 keyholder_state[map] = {}
136
137 keyholder_state[map][kh_path] = key
138 letters_in_keyholders.append(key)
139
140 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(
141 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
142 )
143
144 update_unlocks()
145 save()
146
147
148func remove_from_keyholder(key, map, kh_path):
149 if not keyholder_state.has(map):
150 # This... shouldn't happen.
151 keyholder_state[map] = {}
152
153 keyholder_state[map].erase(kh_path)
154 letters_in_keyholders.erase(key)
155
156 get_tree().get_root().get_node("scene").get_node(kh_path).setFromAp(key, 0)
157
158 update_unlocks()
159 save()
160
161
162func block_letter(key):
163 if not letters_blocked.has(key):
164 letters_blocked.append(key)
165
166 update_unlocks()
167
168
169func load_keyholders(map):
170 if keyholder_state.has(map):
171 var khs = keyholder_state[map]
172
173 for path in khs.keys():
174 var key = khs[path]
175 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
176 key, min(letters_saved.get(key, 0) + letters_dynamic.get(key, 0), 2)
177 )
178
179
180func reset_keyholders():
181 if letters_in_keyholders.is_empty() and letters_blocked.is_empty():
182 return false
183
184 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
185
186 if keyholder_state.has(global.map):
187 for path in keyholder_state[global.map]:
188 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
189 keyholder_state[global.map][path], 0
190 )
191
192 keyholder_state.clear()
193 letters_in_keyholders.clear()
194 letters_blocked.clear()
195
196 update_unlocks()
197 save()
198
199 return cleared_anything
diff --git a/client/Archipelago/locationListener.gd b/client/Archipelago/locationListener.gd deleted file mode 100644 index 71792ed..0000000 --- a/client/Archipelago/locationListener.gd +++ /dev/null
@@ -1,20 +0,0 @@
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 deleted file mode 100644 index 5a36336..0000000 --- a/client/Archipelago/manager.gd +++ /dev/null
@@ -1,528 +0,0 @@
1extends Node
2
3const MOD_VERSION = 3
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 if gamedata.anti_trap_ids.has(item):
219 keyboard.block_letter(gamedata.anti_trap_ids[item])
220
221 global._print(message)
222
223 global.get_node("Messages").showMessage(message)
224
225
226func _process_message(message):
227 parse_printjson_for_textclient(message)
228
229 if (
230 !message.has("receiving")
231 or !message.has("item")
232 or message["item"]["player"] != client._slot
233 ):
234 return
235
236 var item_name = "Unknown"
237 var item_player_game = client._game_by_player[message["receiving"]]
238 if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])):
239 item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])]
240
241 var location_name = "Unknown"
242 var location_player_game = client._game_by_player[message["item"]["player"]]
243 if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])):
244 location_name = (client._location_id_to_name[location_player_game][int(
245 message["item"]["location"]
246 )])
247
248 var player_name = "Unknown"
249 if client._player_name_by_slot.has(message["receiving"]):
250 player_name = client._player_name_by_slot[message["receiving"]]
251
252 var item_color = colorForItemType(message["item"]["flags"])
253
254 if message["type"] == "Hint":
255 var is_for = ""
256 if message["receiving"] != client._slot:
257 is_for = " for %s" % player_name
258 if !message.has("found") || !message["found"]:
259 global.get_node("Messages").showMessage(
260 (
261 "Hint: [color=%s]%s[/color]%s is on %s"
262 % [item_color, item_name, is_for, location_name]
263 )
264 )
265 else:
266 if message["receiving"] != client._slot:
267 var sentMsg = "Sent [color=%s]%s[/color] to %s" % [item_color, item_name, player_name]
268 #if _hinted_locations.has(message["item"]["location"]):
269 # sentMsg += " ([color=#fafad2]Hinted![/color])"
270 global.get_node("Messages").showMessage(sentMsg)
271
272
273func parse_printjson_for_textclient(message):
274 var parts = []
275 for message_part in message["data"]:
276 if !message_part.has("type") and message_part.has("text"):
277 parts.append(message_part["text"])
278 elif message_part["type"] == "player_id":
279 if int(message_part["text"]) == client._slot:
280 parts.append(
281 "[color=#ee00ee]%s[/color]" % client._player_name_by_slot[client._slot]
282 )
283 else:
284 var from = float(message_part["text"])
285 parts.append("[color=#fafad2]%s[/color]" % client._player_name_by_slot[from])
286 elif message_part["type"] == "item_id":
287 var item_name = "Unknown"
288 var item_player_game = client._game_by_player[message_part["player"]]
289 if client._item_id_to_name[item_player_game].has(int(message_part["text"])):
290 item_name = client._item_id_to_name[item_player_game][int(message_part["text"])]
291
292 parts.append(
293 "[color=%s]%s[/color]" % [colorForItemType(message_part["flags"]), item_name]
294 )
295 elif message_part["type"] == "location_id":
296 var location_name = "Unknown"
297 var location_player_game = client._game_by_player[message_part["player"]]
298 if client._location_id_to_name[location_player_game].has(int(message_part["text"])):
299 location_name = client._location_id_to_name[location_player_game][int(
300 message_part["text"]
301 )]
302
303 parts.append("[color=#00ff7f]%s[/color]" % location_name)
304 elif message_part.has("text"):
305 parts.append(message_part["text"])
306
307 var textclient_node = global.get_node("Textclient")
308 if textclient_node != null:
309 textclient_node.parse_printjson("".join(parts))
310
311
312func _process_location_scout(item_id, location_id, player, flags):
313 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
314
315 var gamedata = global.get_node("Gamedata")
316 var map_id = gamedata.map_id_by_name.get(global.map)
317
318 var item_name = "Unknown"
319 var item_player_game = client._game_by_player[float(player)]
320 if client._item_id_to_name[item_player_game].has(item_id):
321 item_name = client._item_id_to_name[item_player_game][item_id]
322
323 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
324 if letter_id != null:
325 var letter = gamedata.objects.get_letters()[letter_id]
326 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
327 if room.get_map_id() == map_id:
328 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
329 letter.get_path()
330 )
331 if collectable != null:
332 collectable.setScoutedText(item_name)
333
334
335func _client_could_not_connect(message):
336 emit_signal("could_not_connect", message)
337
338
339func _client_connect_status(message):
340 emit_signal("connect_status", message)
341
342
343func _client_connected(slot_data):
344 var gamedata = global.get_node("Gamedata")
345
346 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
347 _last_new_item = -1
348
349 if FileAccess.file_exists(_localdata_file):
350 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
351 var localdata = []
352 if ap_file != null:
353 localdata = ap_file.get_var(true)
354 ap_file.close()
355
356 if typeof(localdata) != TYPE_ARRAY:
357 print("AP localdata file is corrupted")
358 localdata = []
359
360 if localdata.size() > 0:
361 _last_new_item = localdata[0]
362
363 # Read slot data.
364 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
365 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
366 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
367 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
368 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
369 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
370 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
371 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
372 victory_condition = int(slot_data.get("victory_condition", 0))
373
374 if slot_data.has("version"):
375 apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])]
376
377 # Set up item locks.
378 _item_locks = {}
379
380 if shuffle_doors:
381 for door in gamedata.objects.get_doors():
382 if (
383 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
384 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
385 ):
386 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
387
388 for progressive in gamedata.objects.get_progressives():
389 for i in range(0, progressive.get_doors().size()):
390 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
391 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
392
393 for door_group in gamedata.objects.get_door_groups():
394 if (
395 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
396 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
397 ):
398 for door in door_group.get_doors():
399 _item_locks[door] = [door_group.get_ap_id(), 1]
400
401 if shuffle_control_center_colors:
402 for door in gamedata.objects.get_doors():
403 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
404 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
405
406 for door_group in gamedata.objects.get_door_groups():
407 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
408 for door in door_group.get_doors():
409 _item_locks[door] = [door_group.get_ap_id(), 1]
410
411 if shuffle_gallery_paintings:
412 for door in gamedata.objects.get_doors():
413 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
414 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
415
416 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
417 for door_group in gamedata.objects.get_door_groups():
418 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
419 for door in door_group.get_doors():
420 if not _item_locks.has(door):
421 _item_locks[door] = [door_group.get_ap_id(), 1]
422
423 # Create a reverse item locks map for processing items.
424 _inverse_item_locks = {}
425
426 for door_id in _item_locks.keys():
427 var lock = _item_locks.get(door_id)
428
429 if not _inverse_item_locks.has(lock[0]):
430 _inverse_item_locks[lock[0]] = []
431
432 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
433
434 emit_signal("ap_connected")
435
436
437func start_batching_locations():
438 _batch_locations = true
439
440
441func send_location(loc_id):
442 if _batch_locations:
443 _held_locations.append(loc_id)
444 else:
445 client.sendLocation(loc_id)
446
447
448func scout_location(loc_id):
449 if _location_scouts.has(loc_id):
450 return _location_scouts.get(loc_id)
451
452 if _batch_locations:
453 _held_location_scouts.append(loc_id)
454 else:
455 client.scoutLocation(loc_id)
456
457 return null
458
459
460func stop_batching_locations():
461 _batch_locations = false
462
463 if not _held_locations.is_empty():
464 client.sendLocations(_held_locations)
465 _held_locations.clear()
466
467 if not _held_location_scouts.is_empty():
468 client.scoutLocations(_held_location_scouts)
469 _held_location_scouts.clear()
470
471
472func colorForItemType(flags):
473 var int_flags = int(flags)
474 if int_flags & 1: # progression
475 if int_flags & 2: # proguseful
476 return "#f0d200"
477 else:
478 return "#bc51e0"
479 elif int_flags & 2: # useful
480 return "#2b67ff"
481 elif int_flags & 4: # trap
482 return "#d63a22"
483 else: # filler
484 return "#14de9e"
485
486
487func get_letter_behavior(key, level2):
488 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
489 return kLETTER_BEHAVIOR_UNLOCKED
490
491 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
492 if level2:
493 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
494 return kLETTER_BEHAVIOR_VANILLA
495 else:
496 return kLETTER_BEHAVIOR_ITEM
497 else:
498 return kLETTER_BEHAVIOR_UNLOCKED
499
500 if not level2 and ["h", "i", "n", "t"].has(key):
501 # This differs from the equivalent function in the apworld. Logically it is
502 # the same as UNLOCKED since they are in the starting room, but VANILLA
503 # means the player still has to actually pick up the letters.
504 return kLETTER_BEHAVIOR_VANILLA
505
506 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
507 return kLETTER_BEHAVIOR_ITEM
508
509 return kLETTER_BEHAVIOR_VANILLA
510
511
512func setup_keys():
513 keyboard.load_seed()
514
515 _letters_setup = true
516
517 for k in _held_letters.keys():
518 _process_key_item(k, _held_letters[k])
519
520 _held_letters.clear()
521
522
523func _process_key_item(key, level):
524 if not _letters_setup:
525 _held_letters[key] = max(_held_letters.get(key, 0), level)
526 return
527
528 keyboard.collect_remote_letter(key, level)
diff --git a/client/Archipelago/messages.gd b/client/Archipelago/messages.gd deleted file mode 100644 index 52f38b9..0000000 --- a/client/Archipelago/messages.gd +++ /dev/null
@@ -1,61 +0,0 @@
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 deleted file mode 100644 index 276d4eb..0000000 --- a/client/Archipelago/painting.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index fdaaf0e..0000000 --- a/client/Archipelago/panel.gd +++ /dev/null
@@ -1,101 +0,0 @@
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 deleted file mode 100644 index 5da114a..0000000 --- a/client/Archipelago/pauseMenu.gd +++ /dev/null
@@ -1,12 +0,0 @@
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 deleted file mode 100644 index b4f7cc7..0000000 --- a/client/Archipelago/player.gd +++ /dev/null
@@ -1,244 +0,0 @@
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 for sender in door.get_senders():
72 locationListener.senders.append(NodePath("/root/scene/" + sender))
73
74 if door.has_complete_at():
75 locationListener.complete_at = door.get_complete_at()
76
77 get_parent().add_child.call_deferred(locationListener)
78
79 # Set up letter locations.
80 for letter in gamedata.objects.get_letters():
81 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
82 if room.get_map_id() != map_id:
83 continue
84
85 var locationListener = ap.SCRIPT_locationListener.new()
86 locationListener.location_id = letter.get_ap_id()
87 locationListener.name = "locationListener_%d" % letter.get_ap_id()
88 locationListener.senders.append(NodePath("/root/scene/" + letter.get_path()))
89
90 get_parent().add_child.call_deferred(locationListener)
91
92 if (
93 ap.get_letter_behavior(letter.get_key(), letter.has_level2() and letter.get_level2())
94 != ap.kLETTER_BEHAVIOR_VANILLA
95 ):
96 var scout = ap.scout_location(letter.get_ap_id())
97 if scout != null:
98 var item_name = "Unknown"
99 var item_player_game = ap.client._game_by_player[float(scout["player"])]
100 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
101 item_name = ap.client._item_id_to_name[item_player_game][scout["item"]]
102
103 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
104 letter.get_path()
105 )
106 if collectable != null:
107 collectable.setScoutedText.call_deferred(item_name)
108
109 # Set up mastery locations.
110 for mastery in gamedata.objects.get_masteries():
111 var room = gamedata.objects.get_rooms()[mastery.get_room_id()]
112 if room.get_map_id() != map_id:
113 continue
114
115 var locationListener = ap.SCRIPT_locationListener.new()
116 locationListener.location_id = mastery.get_ap_id()
117 locationListener.name = "locationListener_%d" % mastery.get_ap_id()
118 locationListener.senders.append(NodePath("/root/scene/" + mastery.get_path()))
119
120 get_parent().add_child.call_deferred(locationListener)
121
122 # Set up ending locations.
123 for ending in gamedata.objects.get_endings():
124 var room = gamedata.objects.get_rooms()[ending.get_room_id()]
125 if room.get_map_id() != map_id:
126 continue
127
128 var locationListener = ap.SCRIPT_locationListener.new()
129 locationListener.location_id = ending.get_ap_id()
130 locationListener.name = "locationListener_%d" % ending.get_ap_id()
131 locationListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
132
133 get_parent().add_child.call_deferred(locationListener)
134
135 if kEndingNameByVictoryValue.get(ap.victory_condition, null) == ending.get_name():
136 var victoryListener = ap.SCRIPT_victoryListener.new()
137 victoryListener.name = "victoryListener"
138 victoryListener.senders.append(NodePath("/root/scene/" + ending.get_path()))
139
140 get_parent().add_child.call_deferred(victoryListener)
141
142 # Set up keyholder locations, in keyholder sanity.
143 if ap.keyholder_sanity:
144 for keyholder in gamedata.objects.get_keyholders():
145 if not keyholder.has_key():
146 continue
147
148 var room = gamedata.objects.get_rooms()[keyholder.get_room_id()]
149 if room.get_map_id() != map_id:
150 continue
151
152 var locationListener = ap.SCRIPT_locationListener.new()
153 locationListener.location_id = keyholder.get_ap_id()
154 locationListener.name = "locationListener_%d" % keyholder.get_ap_id()
155
156 var khl = khl_script.new()
157 khl.name = "location_%d_keyholder" % keyholder.get_ap_id()
158 khl.answer = keyholder.get_key()
159 khl.senders.append(NodePath("/root/scene/" + keyholder.get_path()))
160 get_parent().add_child.call_deferred(khl)
161
162 locationListener.senders.append(NodePath("../" + khl.name))
163
164 get_parent().add_child.call_deferred(locationListener)
165
166 # Block off roof access in Daedalus.
167 if global.map == "daedalus" and not ap.daedalus_roof_access:
168 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
169 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
170 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
171 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
172 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
173 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
174 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
175 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
176 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
177 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
178 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
179 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
180 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
181 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
182 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
183
184 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
185 var warp_exit = warp_exit_prefab.instantiate()
186 warp_exit.name = "roof_access_blocker_warp_exit"
187 warp_exit.position = Vector3(58, 10, 0)
188 warp_exit.rotation_degrees.y = 90
189 get_parent().add_child.call_deferred(warp_exit)
190
191 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
192 var warp_enter = warp_enter_prefab.instantiate()
193 warp_enter.target = warp_exit
194 warp_enter.position = Vector3(76.5, 30, 1)
195 warp_enter.scale = Vector3(4, 1.5, 1)
196 warp_enter.rotation_degrees.y = 90
197 get_parent().add_child.call_deferred(warp_enter)
198
199 if global.map == "the_entry":
200 # Remove door behind X1.
201 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
202 door_node.handleTriggered()
203
204 # Display win condition.
205 var sign_prefab = preload("res://objects/nodes/sign.tscn")
206 var sign1 = sign_prefab.instantiate()
207 sign1.position = Vector3(-7, 5, -15.01)
208 sign1.text = "victory"
209 get_parent().add_child.call_deferred(sign1)
210
211 var sign2 = sign_prefab.instantiate()
212 sign2.position = Vector3(-7, 4, -15.01)
213 sign2.text = "%s ending" % kEndingNameByVictoryValue.get(ap.victory_condition, "?")
214
215 var sign2_color = kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
216 if sign2_color == "white":
217 sign2_color = "silver"
218
219 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
220 get_parent().add_child.call_deferred(sign2)
221
222 super._ready()
223
224 await get_tree().process_frame
225 await get_tree().process_frame
226
227 ap.stop_batching_locations()
228
229
230func _set_up_invis_wall(x, y, z, sx, sy, sz):
231 var prefab = preload("res://objects/nodes/block.tscn")
232 var newwall = prefab.instantiate()
233 newwall.position.x = x
234 newwall.position.y = y
235 newwall.position.z = z
236 newwall.scale.x = sz
237 newwall.scale.y = sy
238 newwall.scale.z = sx
239 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
240 newwall.visibility_range_end = 3
241 newwall.visibility_range_end_margin = 1
242 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
243 newwall.skeleton = ".."
244 get_parent().add_child.call_deferred(newwall)
diff --git a/client/Archipelago/saver.gd b/client/Archipelago/saver.gd deleted file mode 100644 index 0fba9e7..0000000 --- a/client/Archipelago/saver.gd +++ /dev/null
@@ -1,9 +0,0 @@
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 deleted file mode 100644 index 9e61cb0..0000000 --- a/client/Archipelago/settings_buttons.gd +++ /dev/null
@@ -1,24 +0,0 @@
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 deleted file mode 100644 index 2236672..0000000 --- a/client/Archipelago/settings_screen.gd +++ /dev/null
@@ -1,244 +0,0 @@
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 $Panel/connect_button.disabled = false
227
228
229func versionMismatchDeclined():
230 $Panel/AcceptDialog.hide()
231 $Panel/connect_button.disabled = false
232
233
234func historySelected(index):
235 var ap = global.get_node("Archipelago")
236 var details = ap.connection_history[index]
237
238 $Panel/server_box.text = details[0]
239 $Panel/player_box.text = details[1]
240 $Panel/password_box.text = details[2]
241
242
243func clearResourceCache(path):
244 ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
diff --git a/client/Archipelago/teleport.gd b/client/Archipelago/teleport.gd deleted file mode 100644 index 428d50b..0000000 --- a/client/Archipelago/teleport.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index 6f363af..0000000 --- a/client/Archipelago/teleportListener.gd +++ /dev/null
@@ -1,49 +0,0 @@
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 deleted file mode 100644 index 85cc6d2..0000000 --- a/client/Archipelago/textclient.gd +++ /dev/null
@@ -1,86 +0,0 @@
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 deleted file mode 100644 index 115ba15..0000000 --- a/client/Archipelago/vendor/LICENSE +++ /dev/null
@@ -1,21 +0,0 @@
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 deleted file mode 100644 index b63fa04..0000000 --- a/client/Archipelago/vendor/uuid.gd +++ /dev/null
@@ -1,195 +0,0 @@
1# Note: The code might not be as pretty it could be, since it's written
2# in a way that maximizes performance. Methods are inlined and loops are avoided.
3extends Node
4
5const BYTE_MASK: int = 0b11111111
6
7
8static func uuidbin():
9 randomize()
10 # 16 random bytes with the bytes on index 6 and 8 modified
11 return [
12 randi() & BYTE_MASK,
13 randi() & BYTE_MASK,
14 randi() & BYTE_MASK,
15 randi() & BYTE_MASK,
16 randi() & BYTE_MASK,
17 randi() & BYTE_MASK,
18 ((randi() & BYTE_MASK) & 0x0f) | 0x40,
19 randi() & BYTE_MASK,
20 ((randi() & BYTE_MASK) & 0x3f) | 0x80,
21 randi() & BYTE_MASK,
22 randi() & BYTE_MASK,
23 randi() & BYTE_MASK,
24 randi() & BYTE_MASK,
25 randi() & BYTE_MASK,
26 randi() & BYTE_MASK,
27 randi() & BYTE_MASK,
28 ]
29
30
31static func uuidbinrng(rng: RandomNumberGenerator):
32 rng.randomize()
33 return [
34 rng.randi() & BYTE_MASK,
35 rng.randi() & BYTE_MASK,
36 rng.randi() & BYTE_MASK,
37 rng.randi() & BYTE_MASK,
38 rng.randi() & BYTE_MASK,
39 rng.randi() & BYTE_MASK,
40 ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40,
41 rng.randi() & BYTE_MASK,
42 ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80,
43 rng.randi() & BYTE_MASK,
44 rng.randi() & BYTE_MASK,
45 rng.randi() & BYTE_MASK,
46 rng.randi() & BYTE_MASK,
47 rng.randi() & BYTE_MASK,
48 rng.randi() & BYTE_MASK,
49 rng.randi() & BYTE_MASK,
50 ]
51
52
53static func v4():
54 # 16 random bytes with the bytes on index 6 and 8 modified
55 var b = uuidbin()
56
57 return (
58 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
59 % [
60 # low
61 b[0],
62 b[1],
63 b[2],
64 b[3],
65 # mid
66 b[4],
67 b[5],
68 # hi
69 b[6],
70 b[7],
71 # clock
72 b[8],
73 b[9],
74 # clock
75 b[10],
76 b[11],
77 b[12],
78 b[13],
79 b[14],
80 b[15]
81 ]
82 )
83
84
85static func v4_rng(rng: RandomNumberGenerator):
86 # 16 random bytes with the bytes on index 6 and 8 modified
87 var b = uuidbinrng(rng)
88
89 return (
90 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
91 % [
92 # low
93 b[0],
94 b[1],
95 b[2],
96 b[3],
97 # mid
98 b[4],
99 b[5],
100 # hi
101 b[6],
102 b[7],
103 # clock
104 b[8],
105 b[9],
106 # clock
107 b[10],
108 b[11],
109 b[12],
110 b[13],
111 b[14],
112 b[15]
113 ]
114 )
115
116
117var _uuid: Array
118
119
120func _init(rng := RandomNumberGenerator.new()) -> void:
121 _uuid = uuidbinrng(rng)
122
123
124func as_array() -> Array:
125 return _uuid.duplicate()
126
127
128func as_dict(big_endian := true) -> Dictionary:
129 if big_endian:
130 return {
131 "low": (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8) + _uuid[3],
132 "mid": (_uuid[4] << 8) + _uuid[5],
133 "hi": (_uuid[6] << 8) + _uuid[7],
134 "clock": (_uuid[8] << 8) + _uuid[9],
135 "node":
136 (
137 (_uuid[10] << 40)
138 + (_uuid[11] << 32)
139 + (_uuid[12] << 24)
140 + (_uuid[13] << 16)
141 + (_uuid[14] << 8)
142 + _uuid[15]
143 )
144 }
145 else:
146 return {
147 "low": _uuid[0] + (_uuid[1] << 8) + (_uuid[2] << 16) + (_uuid[3] << 24),
148 "mid": _uuid[4] + (_uuid[5] << 8),
149 "hi": _uuid[6] + (_uuid[7] << 8),
150 "clock": _uuid[8] + (_uuid[9] << 8),
151 "node":
152 (
153 _uuid[10]
154 + (_uuid[11] << 8)
155 + (_uuid[12] << 16)
156 + (_uuid[13] << 24)
157 + (_uuid[14] << 32)
158 + (_uuid[15] << 40)
159 )
160 }
161
162
163func as_string() -> String:
164 return (
165 "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
166 % [
167 # low
168 _uuid[0],
169 _uuid[1],
170 _uuid[2],
171 _uuid[3],
172 # mid
173 _uuid[4],
174 _uuid[5],
175 # hi
176 _uuid[6],
177 _uuid[7],
178 # clock
179 _uuid[8],
180 _uuid[9],
181 # node
182 _uuid[10],
183 _uuid[11],
184 _uuid[12],
185 _uuid[13],
186 _uuid[14],
187 _uuid[15]
188 ]
189 )
190
191
192func is_equal(other) -> bool:
193 # Godot Engine compares Array recursively
194 # There's no need for custom comparison here.
195 return _uuid == other._uuid
diff --git a/client/Archipelago/victoryListener.gd b/client/Archipelago/victoryListener.gd deleted file mode 100644 index e9089d7..0000000 --- a/client/Archipelago/victoryListener.gd +++ /dev/null
@@ -1,20 +0,0 @@
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 deleted file mode 100644 index 5ea17a0..0000000 --- a/client/Archipelago/visibilityListener.gd +++ /dev/null
@@ -1,38 +0,0 @@
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 deleted file mode 100644 index 5c2faff..0000000 --- a/client/Archipelago/worldportListener.gd +++ /dev/null
@@ -1,8 +0,0 @@
1extends "res://scripts/nodes/listeners/worldportListener.gd"
2
3
4func handleTriggered():
5 if exit == "menus/credits":
6 return
7
8 super.handleTriggered()
diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md deleted file mode 100644 index 89d9873..0000000 --- a/client/CHANGELOG.md +++ /dev/null
@@ -1,21 +0,0 @@
1# lingo2-archipelago Client Releases
2
3## v3.3 - 2025-09-12
4
5- Fixed issue downloading large datapackages (such as TUNIC's).
6- Connection failures now show error messages.
7
8Download:
9[lingo2-archipelago-client-v3.3.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.3.zip)<br/>
10Source:
11[v3.3](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.3)
12
13## v3.2 - 2025-09-12
14
15- Initial release for testing. Features include door shuffle, letter shuffle,
16 and symbol shuffle.
17
18Download:
19[lingo2-archipelago-client-v3.2.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.2.zip)<br/>
20Source:
21[v3.2](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.2)
diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 99589c5..0000000 --- a/client/README.md +++ /dev/null
@@ -1,90 +0,0 @@
1# Lingo 2 Archipelago Client
2
3The Lingo 2 Archipelago Client is a mod for Lingo 2 that allows you to connect
4to an Archipelago Multiworld and randomize your game.
5
6## Installation
7
81. Download the Lingo 2 Archipelago Randomizer from
9 [the releases page](https://code.fourisland.com/lingo2-archipelago/about/client/CHANGELOG.md).
102. Open up Lingo 2, go to settings, and click View Game Data. This should open
11 up a folder in Windows Explorer.
123. Unzip the randomizer into the "maps" folder. Ensure that archipelago.tscn and
13 the Archipelago folder are both within the maps folder.
14
15**NOTE**: It is important that the major version number of your client matches
16the major version number of the apworld you generated with.
17
18## Joining a Multiworld game
19
201. Launch Lingo 2.
212. Click on Level Selection, and choose Archipelago from the list.
223. The selected player is generally ignored by the mod, and you don't even need
23 to ensure you use the same player between connections. However, if your
24 player name has a gift map associated with it, Lingo 2 will prioritize the
25 gift map over loading the mod, so in that case you should choose another
26 player.
274. Press Play.
285. Enter the Archipelago address, slot name, and password into the fields.
296. Press Connect.
307. Enjoy!
31
32To continue an earlier game, you can perform the exact same steps as above. You
33will probably have to re-select Archipelago from the Level Selection screen, as
34the game does not remember which level you were playing.
35
36**Note**: Running the randomizer modifies the game's memory. If you want to play
37the base game after playing the randomizer, you need to restart Lingo 2 first.
38
39## Running from source
40
41The mod is mostly written in GDScript, which is parsed and executed by Lingo 2
42itself, and thus does not need to be compiled. However, there are two files that
43need to be generated before the client can be run.
44
45The first file is `data.binpb`, the datafile containing the randomizer logic.
46You can read about how to generate it on
47[its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md).
48Once you have it, put it in a subfolder of `client` called `generated`.
49
50The second generated file is `proto.gd`. This file allows Lingo 2 to read the
51datafile. We use a Godot script to generate it, which means
52[the Godot Editor](https://godotengine.org/download/) is required. From the root
53of the repository:
54
55```shell
56cd vendor\godobuf
57godot --headless -s addons\protobuf\protobuf_cmdln.gd --input=..\..\proto\data.proto ^
58 --output=..\..\client\Archipelago\generated\proto.gd
59```
60
61If you are not on Windows, replace the forward slashes with backslashes as
62appropriate (and the caret with a forward slash). You will also probably need to
63replace "godot" at the start of the second line with a path to a Godot Editor
64executable.
65
66After generating those two files, the contents of the `client` folder (minus
67this README) can be pasted into the Lingo 2 maps directory as described above.
68
69## Frequently Asked Questions
70
71### Is my progress saved locally?
72
73Lingo 2 autosaves your progress every time you solve a puzzle, get a
74collectable, or interact with a keyholder. The randomizer generates a savefile
75name based on your Multiworld seed and slot number, so you should be able to
76seamlessly switch between multiworlds and even slots within a multiworld.
77
78The exception to this is different rooms created from the same multiworld seed.
79The client is unable to tell rooms in a seed apart (this is a limitation of the
80Archipelago API), so the client will use the same save file for the same slot in
81different rooms on the same seed. You can work around this by manually moving or
82removing the save file from the level1 save file directory.
83
84If you play the base game again, you will see one or more save files with a long
85name that begins with "zzAP\_". These are the saves for your multiworlds. They
86can be safely deleted after you have completed the associated multiworld. It is
87not recommended to load these save files outside of the randomizer.
88
89A connection to Archipelago is required to resume playing a multiworld. This is
90because the set of items you have received is not stored locally.
diff --git a/client/archipelago.tscn b/client/archipelago.tscn index da83b23..1c156a3 100644 --- a/client/archipelago.tscn +++ b/client/archipelago.tscn
@@ -1,167 +1,153 @@
1[gd_scene load_steps=11 format=2] 1[gd_scene load_steps=3 format=3 uid="uid://b5mj3cq2bcesd"]
2 2
3[ext_resource path="user://maps/Archipelago/settings_buttons.gd" type="Script" id=4] 3[ext_resource type="Theme" uid="uid://7w454egydi41" path="res://assets/themes/baseUI.tres" id="1_mw3f1"]
4[ext_resource path="user://maps/Archipelago/settings_screen.gd" type="Script" id=5] 4
5[ext_resource path="res://images/unchecked.png" type="Texture" id=7] 5[sub_resource id=2 type="GDScript"]
6[ext_resource path="res://images/checked.png" type="Texture" id=8] 6script/source = "extends Node2D
7[ext_resource type="Theme" uid="uid://7w454egydi41" path="res://assets/themes/baseUI.tres" id="2_g4bvn"] 7
8 8const CACHE_PATH = \"user://apworld_path.txt\"
9[sub_resource type="StyleBoxFlat" id=1] 9
10bg_color = Color( 0, 0, 0, 0 ) 10
11 11func _ready():
12[sub_resource type="StyleBoxFlat" id=2] 12 if FileAccess.file_exists(CACHE_PATH):
13bg_color = Color( 1, 1, 1, 1 ) 13 var file = FileAccess.open(CACHE_PATH, FileAccess.READ)
14border_width_left = 1 14 $Panel/HBoxContainer/LineEdit.text = file.get_as_text()
15border_width_top = 1 15 file.close()
16border_width_right = 1 16
17border_width_bottom = 1 17
18border_color = Color( 1, 1, 0, 1 ) 18func _browse_pressed():
19border_blend = true 19 $FileDialog.popup_centered()
20corner_radius_top_left = 3 20
21corner_radius_top_right = 3 21
22corner_radius_bottom_right = 3 22func _file_selected(path):
23corner_radius_bottom_left = 3 23 $Panel/HBoxContainer/LineEdit.text = path
24expand_offset_left = 5.0 24
25expand_offset_right = 5.0 25
26expand_offset_top = 5.0 26func _start_pressed():
27expand_offset_bottom = 5.0 27 var apworld_path = $Panel/HBoxContainer/LineEdit.text
28 28
29[node name="settings_screen" type="Node2D"] 29 if not FileAccess.file_exists(apworld_path):
30script = ExtResource( 5 ) 30 $AcceptDialog.popup_centered()
31 return
32
33 var zip_reader = ZIPReader.new()
34 zip_reader.open(apworld_path)
35
36 var inner_path = \"lingo2/client/apworld_runtime.gd\"
37 if not zip_reader.file_exists(inner_path):
38 zip_reader.close()
39 $AcceptDialog.popup_centered()
40 return
41
42 var cache_file = FileAccess.open(CACHE_PATH, FileAccess.WRITE)
43 cache_file.store_string(apworld_path)
44 cache_file.close()
45
46 var runtime_script = GDScript.new()
47 runtime_script.source_code = zip_reader.read_file(inner_path).get_string_from_utf8()
48 runtime_script.reload()
49
50 zip_reader.close()
51
52 var runtime = runtime_script.new(apworld_path)
53 runtime.name = \"Runtime\"
54
55 global.add_child(runtime)
56
57 runtime.load_script_as_scene.call_deferred(\"settings_screen.gd\", \"settings_screen\")
58
59
60func _quit_pressed():
61 get_tree().change_scene_to_file(\"res://objects/scenes/menus/main_menu.tscn\")
62
63"
64
65[node name="Node2D" type="Node2D"]
66script = SubResource( 2 )
31 67
32[node name="Panel" type="Panel" parent="."] 68[node name="Panel" type="Panel" parent="."]
69anchors_preset = -1
33offset_right = 1920.0 70offset_right = 1920.0
34offset_bottom = 1080.0 71offset_bottom = 1080.0
35 72
36[node name="title" parent="Panel" type="Label"] 73[node name="Label" type="Label" parent="Panel"]
37offset_left = 0.0 74layout_mode = 1
75anchors_preset = -1
38offset_top = 75.0 76offset_top = 75.0
39offset_right = 1920.0 77offset_right = 1920.0
40offset_bottom = 225.0 78offset_bottom = 225.0
41text = "ARCHIPELAGO" 79theme = ExtResource("1_mw3f1")
42valign = 1 80text = "archipelago"
81horizontal_alignment = 1
82vertical_alignment = 1
83
84[node name="Label2" type="Label" parent="Panel"]
85layout_mode = 1
86anchors_preset = -1
87anchor_right = 1.0
88offset_left = 80.0
89offset_top = 300.0
90offset_right = -80.0
91offset_bottom = 388.0
92theme = ExtResource("1_mw3f1")
93theme_override_font_sizes/font_size = 56
94text = "Put the path to your lingo2.apworld in the below field and click start. Then, open the archipelago launcher and click \"Lingo 2 Client\"."
43horizontal_alignment = 1 95horizontal_alignment = 1
44theme = ExtResource("2_g4bvn") 96autowrap_mode = 3
45 97
46[node name="credit" parent="Panel" type="Label"] 98[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
47visible = false 99layout_mode = 1
48offset_left = 1278.0 100anchors_preset = -1
49offset_top = 974.0 101anchor_right = 1.0
50offset_right = 1868.0 102offset_left = 80.0
51offset_bottom = 1034.0 103offset_top = 595.0
52text = "Brenton Wildes" 104offset_right = -80.0
53theme = ExtResource("2_g4bvn") 105offset_bottom = 755.0
54 106theme_override_constants/separation = 32
55[node name="connect_button" parent="Panel" type="Button"] 107
108[node name="LineEdit" type="LineEdit" parent="Panel/HBoxContainer"]
109layout_mode = 2
110size_flags_horizontal = 3
111
112[node name="Button" type="Button" parent="Panel/HBoxContainer"]
113layout_mode = 2
114theme = ExtResource("1_mw3f1")
115text = "browse"
116
117[node name="StartButton" type="Button" parent="Panel"]
118layout_mode = 1
119anchors_preset = -1
56offset_left = 255.0 120offset_left = 255.0
57offset_top = 875.0 121offset_top = 875.0
58offset_right = 891.0 122offset_right = 891.0
59offset_bottom = 1025.0 123offset_bottom = 1025.0
60custom_colors/font_color_hover = Color( 1, 0.501961, 0, 1 ) 124theme = ExtResource("1_mw3f1")
61text = "CONNECT" 125text = "start"
62theme = ExtResource("2_g4bvn")
63script = ExtResource( 4 )
64 126
65[node name="quit_button" parent="Panel" type="Button"] 127[node name="QuitButton" type="Button" parent="Panel"]
128layout_mode = 1
129anchors_preset = -1
66offset_left = 1102.0 130offset_left = 1102.0
67offset_top = 875.0 131offset_top = 875.0
68offset_right = 1738.0 132offset_right = 1738.0
69offset_bottom = 1025.0 133offset_bottom = 1025.0
70custom_colors/font_color_hover = Color( 1, 0, 0, 1 ) 134theme = ExtResource("1_mw3f1")
71text = "BACK" 135text = "back"
72theme = ExtResource("2_g4bvn") 136
73script = ExtResource( 4 ) 137[node name="FileDialog" type="FileDialog" parent="."]
74 138title = "Open a File"
75[node name="credit2" parent="Panel" type="Label"] 139size = Vector2i(512, 512)
76offset_left = -105.0 140ok_button_text = "Open"
77offset_top = 346.0 141file_mode = 0
78offset_right = 485.0 142access = 2
79offset_bottom = 410.0 143filters = PackedStringArray("*.apworld;Archipelago Worlds")
80custom_styles/normal = SubResource( 1 ) 144show_hidden_files = true
81text = "SERVER" 145use_native_dialog = true
82align = 2 146
83theme = ExtResource("2_g4bvn") 147[node name="AcceptDialog" type="AcceptDialog" parent="."]
84 148dialog_text = "Could not open Lingo 2 apworld. Please check that the path is correct."
85[node name="credit5" parent="Panel" type="Label"] 149
86offset_left = 1239.0 150[connection signal="pressed" from="Panel/HBoxContainer/Button" to="." method="_browse_pressed"]
87offset_top = 422.0 151[connection signal="pressed" from="Panel/StartButton" to="." method="_start_pressed"]
88offset_right = 1829.0 152[connection signal="pressed" from="Panel/QuitButton" to="." method="_quit_pressed"]
89offset_bottom = 486.0 153[connection signal="file_selected" from="FileDialog" to="." method="_file_selected"]
90custom_styles/normal = SubResource( 1 )
91text = "OPTIONS"
92theme = ExtResource("2_g4bvn")
93
94[node name="credit3" parent="Panel" type="Label"]
95offset_left = -105.0
96offset_top = 519.0
97offset_right = 485.0
98offset_bottom = 583.0
99custom_styles/normal = SubResource( 1 )
100text = "PLAYER"
101align = 2
102theme = ExtResource("2_g4bvn")
103
104[node name="credit4" parent="Panel" type="Label"]
105offset_left = -105.0
106offset_top = 704.0
107offset_right = 485.0
108offset_bottom = 768.0
109custom_styles/normal = SubResource( 1 )
110text = "PASSWORD"
111align = 2
112theme = ExtResource("2_g4bvn")
113
114[node name="server_box" type="LineEdit" parent="Panel"]
115offset_left = 502.0
116offset_top = 295.0
117offset_right = 1144.0
118offset_bottom = 445.0
119custom_colors/selection_color = Color( 0.482353, 0, 0, 1 )
120custom_colors/cursor_color = Color( 0, 0, 0, 1 )
121custom_colors/font_color = Color( 0, 0, 0, 1 )
122custom_styles/focus = SubResource( 2 )
123align = 1
124caret_blink = true
125
126[node name="player_box" type="LineEdit" parent="Panel"]
127offset_left = 502.0
128offset_top = 477.0
129offset_right = 1144.0
130offset_bottom = 627.0
131custom_colors/selection_color = Color( 0.482353, 0, 0, 1 )
132custom_colors/cursor_color = Color( 0, 0, 0, 1 )
133custom_colors/font_color = Color( 0, 0, 0, 1 )
134custom_styles/focus = SubResource( 2 )
135align = 1
136caret_blink = true
137
138[node name="password_box" type="LineEdit" parent="Panel"]
139offset_left = 502.0
140offset_top = 659.0
141offset_right = 1144.0
142offset_bottom = 809.0
143custom_colors/selection_color = Color( 0.482353, 0, 0, 1 )
144custom_colors/cursor_color = Color( 0, 0, 0, 1 )
145custom_colors/font_color = Color( 0, 0, 0, 1 )
146custom_styles/focus = SubResource( 2 )
147align = 1
148caret_blink = true
149
150[node name="AcceptDialog" type="AcceptDialog" parent="Panel"]
151offset_right = 83.0
152offset_bottom = 58.0
153
154[node name="VersionMismatch" type="ConfirmationDialog" parent="Panel"]
155offset_right = 83.0
156offset_bottom = 58.0
157
158[node name="connection_history" type="MenuButton" parent="Panel"]
159offset_left = 1239.0
160offset_top = 276.0
161offset_right = 1829.0
162offset_bottom = 372.0
163text = "connection history"
164flat = false
165
166[connection signal="pressed" from="Panel/connect_button" to="Panel/connect_button" method="_connect_pressed"]
167[connection signal="pressed" from="Panel/quit_button" to="Panel/quit_button" method="_back_pressed"]