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/client.gd6
-rw-r--r--client/Archipelago/gamedata.gd54
-rw-r--r--client/Archipelago/keyboard.gd35
-rw-r--r--client/Archipelago/manager.gd121
-rw-r--r--client/Archipelago/messages.gd18
-rw-r--r--client/Archipelago/panel.gd101
-rw-r--r--client/Archipelago/pauseMenu.gd7
-rw-r--r--client/Archipelago/player.gd41
-rw-r--r--client/Archipelago/saver.gd14
-rw-r--r--client/Archipelago/settings_screen.gd49
-rw-r--r--client/Archipelago/teleport.gd38
-rw-r--r--client/Archipelago/teleportListener.gd11
-rw-r--r--client/Archipelago/textclient.gd2
-rw-r--r--client/Archipelago/visibilityListener.gd38
-rw-r--r--client/Archipelago/worldport.gd10
-rw-r--r--client/Archipelago/worldportListener.gd4
-rw-r--r--client/CHANGELOG.md47
-rw-r--r--client/README.md90
-rw-r--r--client/archipelago.tscn5
19 files changed, 655 insertions, 36 deletions
diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd index 2e080fd..843647d 100644 --- a/client/Archipelago/client.gd +++ b/client/Archipelago/client.gd
@@ -47,6 +47,8 @@ signal location_scout_received(item_id, location_id, player, flags)
47func _init(): 47func _init():
48 set_process_mode(Node.PROCESS_MODE_ALWAYS) 48 set_process_mode(Node.PROCESS_MODE_ALWAYS)
49 49
50 _ws.inbound_buffer_size = 8388608
51
50 global._print("Instantiated APClient") 52 global._print("Instantiated APClient")
51 53
52 # Read AP datapackages from file, if there are any 54 # Read AP datapackages from file, if there are any
@@ -225,7 +227,7 @@ func _process(_delta):
225 error_message = "Unknown error." 227 error_message = "Unknown error."
226 228
227 _initiated_disconnect = true 229 _initiated_disconnect = true
228 _ws.disconnect_from_host() 230 _ws.close()
229 231
230 emit_signal("could_not_connect", error_message) 232 emit_signal("could_not_connect", error_message)
231 global._print("Connection to AP refused") 233 global._print("Connection to AP refused")
@@ -309,7 +311,7 @@ func connectToServer(server, un, pw):
309 % err 311 % err
310 ) 312 )
311 ) 313 )
312 global._print("Could not connect to AP: " + err) 314 global._print("Could not connect to AP: %d" % err)
313 return 315 return
314 _should_process = true 316 _should_process = true
315 317
diff --git a/client/Archipelago/gamedata.gd b/client/Archipelago/gamedata.gd index f7a5d90..41d966a 100644 --- a/client/Archipelago/gamedata.gd +++ b/client/Archipelago/gamedata.gd
@@ -5,15 +5,42 @@ var SCRIPT_proto
5var objects 5var objects
6var door_id_by_map_node_path = {} 6var door_id_by_map_node_path = {}
7var painting_id_by_map_node_path = {} 7var painting_id_by_map_node_path = {}
8var panel_id_by_map_node_path = {}
8var door_id_by_ap_id = {} 9var door_id_by_ap_id = {}
9var map_id_by_name = {} 10var map_id_by_name = {}
10var progressive_id_by_ap_id = {} 11var progressive_id_by_ap_id = {}
11var letter_id_by_ap_id = {} 12var letter_id_by_ap_id = {}
13var symbol_item_ids = []
14var anti_trap_ids = {}
15
16var kSYMBOL_ITEMS
12 17
13 18
14func _init(proto_script): 19func _init(proto_script):
15 SCRIPT_proto = proto_script 20 SCRIPT_proto = proto_script
16 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
17 44
18func load(data_bytes): 45func load(data_bytes):
19 objects = SCRIPT_proto.AllObjects.new() 46 objects = SCRIPT_proto.AllObjects.new()
@@ -58,6 +85,25 @@ func load(data_bytes):
58 for letter in objects.get_letters(): 85 for letter in objects.get_letters():
59 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id() 86 letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id()
60 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
61 107
62func get_door_for_map_node_path(map_name, node_path): 108func get_door_for_map_node_path(map_name, node_path):
63 if not door_id_by_map_node_path.has(map_name): 109 if not door_id_by_map_node_path.has(map_name):
@@ -67,6 +113,14 @@ func get_door_for_map_node_path(map_name, node_path):
67 return map_data.get(node_path, null) 113 return map_data.get(node_path, null)
68 114
69 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
70func get_door_ap_id(door_id): 124func get_door_ap_id(door_id):
71 var door = objects.get_doors()[door_id] 125 var door = objects.get_doors()[door_id]
72 if door.has_ap_id(): 126 if door.has_ap_id():
diff --git a/client/Archipelago/keyboard.gd b/client/Archipelago/keyboard.gd index e43ec9f..450566d 100644 --- a/client/Archipelago/keyboard.gd +++ b/client/Archipelago/keyboard.gd
@@ -4,6 +4,7 @@ const kALL_LETTERS = "abcdefghjiklmnopqrstuvwxyz"
4 4
5var letters_saved = {} 5var letters_saved = {}
6var letters_in_keyholders = [] 6var letters_in_keyholders = []
7var letters_blocked = []
7var letters_dynamic = {} 8var letters_dynamic = {}
8var keyholder_state = {} 9var keyholder_state = {}
9 10
@@ -17,6 +18,7 @@ func _init():
17func reset(): 18func reset():
18 letters_saved.clear() 19 letters_saved.clear()
19 letters_in_keyholders.clear() 20 letters_in_keyholders.clear()
21 letters_blocked.clear()
20 letters_dynamic.clear() 22 letters_dynamic.clear()
21 keyholder_state.clear() 23 keyholder_state.clear()
22 24
@@ -79,17 +81,28 @@ func save():
79func update_unlocks(): 81func update_unlocks():
80 unlocks.resetKeys() 82 unlocks.resetKeys()
81 83
84 var has_doubles = false
85
82 for k in kALL_LETTERS: 86 for k in kALL_LETTERS:
83 var level = 0 87 var level = 0
84 88
85 if not letters_in_keyholders.has(k): 89 if not letters_in_keyholders.has(k):
86 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0) 90 level = letters_saved.get(k, 0) + letters_dynamic.get(k, 0)
87 91
88 if level > 2: 92 if level >= 2:
89 level = 2 93 level = 2
94 has_doubles = true
95
96 if letters_blocked.has(k):
97 level = 0
90 98
91 unlocks.unlockKey(k, level) 99 unlocks.unlockKey(k, level)
92 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
93 106
94func collect_local_letter(key, level): 107func collect_local_letter(key, level):
95 if level < 0 or level > 2 or level < letters_saved.get(key, 0): 108 if level < 0 or level > 2 or level < letters_saved.get(key, 0):
@@ -97,6 +110,9 @@ func collect_local_letter(key, level):
97 110
98 letters_saved[key] = level 111 letters_saved[key] = level
99 112
113 if letters_blocked.has(key):
114 letters_blocked.erase(key)
115
100 update_unlocks() 116 update_unlocks()
101 save() 117 save()
102 118
@@ -107,6 +123,9 @@ func collect_remote_letter(key, level):
107 123
108 letters_dynamic[key] = level 124 letters_dynamic[key] = level
109 125
126 if letters_blocked.has(key):
127 letters_blocked.erase(key)
128
110 update_unlocks() 129 update_unlocks()
111 save() 130 save()
112 131
@@ -140,6 +159,13 @@ func remove_from_keyholder(key, map, kh_path):
140 save() 159 save()
141 160
142 161
162func block_letter(key):
163 if not letters_blocked.has(key):
164 letters_blocked.append(key)
165
166 update_unlocks()
167
168
143func load_keyholders(map): 169func load_keyholders(map):
144 if keyholder_state.has(map): 170 if keyholder_state.has(map):
145 var khs = keyholder_state[map] 171 var khs = keyholder_state[map]
@@ -152,9 +178,11 @@ func load_keyholders(map):
152 178
153 179
154func reset_keyholders(): 180func reset_keyholders():
155 if letters_in_keyholders.is_empty(): 181 if letters_in_keyholders.is_empty() and letters_blocked.is_empty():
156 return false 182 return false
157 183
184 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
185
158 if keyholder_state.has(global.map): 186 if keyholder_state.has(global.map):
159 for path in keyholder_state[global.map]: 187 for path in keyholder_state[global.map]:
160 get_tree().get_root().get_node("scene").get_node(path).setFromAp( 188 get_tree().get_root().get_node("scene").get_node(path).setFromAp(
@@ -163,8 +191,9 @@ func reset_keyholders():
163 191
164 keyholder_state.clear() 192 keyholder_state.clear()
165 letters_in_keyholders.clear() 193 letters_in_keyholders.clear()
194 letters_blocked.clear()
166 195
167 update_unlocks() 196 update_unlocks()
168 save() 197 save()
169 198
170 return true 199 return cleared_anything
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd index bcb21e7..06ae7d9 100644 --- a/client/Archipelago/manager.gd +++ b/client/Archipelago/manager.gd
@@ -1,6 +1,6 @@
1extends Node 1extends Node
2 2
3const my_version = "0.1.0" 3const MOD_VERSION = 6
4 4
5var SCRIPT_client 5var SCRIPT_client
6var SCRIPT_keyboard 6var SCRIPT_keyboard
@@ -23,6 +23,7 @@ var _held_locations = []
23var _held_location_scouts = [] 23var _held_location_scouts = []
24var _location_scouts = {} 24var _location_scouts = {}
25var _item_locks = {} 25var _item_locks = {}
26var _inverse_item_locks = {}
26var _held_letters = {} 27var _held_letters = {}
27var _letters_setup = false 28var _letters_setup = false
28 29
@@ -36,10 +37,19 @@ const kLETTER_BEHAVIOR_VANILLA = 0
36const kLETTER_BEHAVIOR_ITEM = 1 37const kLETTER_BEHAVIOR_ITEM = 1
37const kLETTER_BEHAVIOR_UNLOCKED = 2 38const kLETTER_BEHAVIOR_UNLOCKED = 2
38 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
39var daedalus_roof_access = false 46var daedalus_roof_access = false
40var keyholder_sanity = false 47var keyholder_sanity = false
48var shuffle_control_center_colors = false
41var shuffle_doors = false 49var shuffle_doors = false
50var shuffle_gallery_paintings = false
42var shuffle_letters = kSHUFFLE_LETTERS_VANILLA 51var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
52var shuffle_symbols = false
43var victory_condition = -1 53var victory_condition = -1
44 54
45signal could_not_connect 55signal could_not_connect
@@ -121,6 +131,10 @@ func saveLocaldata():
121 131
122func connectToServer(): 132func connectToServer():
123 _last_new_item = -1 133 _last_new_item = -1
134 _batch_locations = false
135 _held_locations = []
136 _held_location_scouts = []
137 _location_scouts = {}
124 _letters_setup = false 138 _letters_setup = false
125 _held_letters = {} 139 _held_letters = {}
126 140
@@ -145,24 +159,26 @@ func _process_item(item, index, from, flags, amount):
145 item_name = client._item_id_to_name["Lingo 2"][item] 159 item_name = client._item_id_to_name["Lingo 2"][item]
146 160
147 var gamedata = global.get_node("Gamedata") 161 var gamedata = global.get_node("Gamedata")
148 var door_id = gamedata.door_id_by_ap_id.get(item, null) 162
149 var prog_id = null 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
150 168
151 if door_id == null: 169 if gamedata.progressive_id_by_ap_id.has(item):
152 prog_id = gamedata.progressive_id_by_ap_id.get(item, null) 170 prog_id = lock[0]
153 if prog_id != null: 171
154 var progressive = gamedata.objects.get_progressives()[prog_id] 172 if gamedata.get_door_map_name(lock[0]) != global.map:
155 if progressive.get_doors().size() >= amount: 173 continue
156 door_id = progressive.get_doors()[amount - 1] 174
157 175 var receivers = gamedata.get_door_receivers(lock[0])
158 if door_id != null and gamedata.get_door_map_name(door_id) == global.map: 176 var scene = get_tree().get_root().get_node_or_null("scene")
159 var receivers = gamedata.get_door_receivers(door_id) 177 if scene != null:
160 var scene = get_tree().get_root().get_node_or_null("scene") 178 for receiver in receivers:
161 if scene != null: 179 var rnode = scene.get_node_or_null(receiver)
162 for receiver in receivers: 180 if rnode != null:
163 var rnode = scene.get_node_or_null(receiver) 181 rnode.handleTriggered()
164 if rnode != null:
165 rnode.handleTriggered()
166 182
167 var letter_id = gamedata.letter_id_by_ap_id.get(item, null) 183 var letter_id = gamedata.letter_id_by_ap_id.get(item, null)
168 if letter_id != null: 184 if letter_id != null:
@@ -170,6 +186,11 @@ func _process_item(item, index, from, flags, amount):
170 if not letter.has_level2() or not letter.get_level2(): 186 if not letter.has_level2() or not letter.get_level2():
171 _process_key_item(letter.get_key(), amount) 187 _process_key_item(letter.get_key(), amount)
172 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
173 # Show a message about the item if it's new. 194 # Show a message about the item if it's new.
174 if index != null and index > _last_new_item: 195 if index != null and index > _last_new_item:
175 _last_new_item = index 196 _last_new_item = index
@@ -182,8 +203,8 @@ func _process_item(item, index, from, flags, amount):
182 var item_color = colorForItemType(flags) 203 var item_color = colorForItemType(flags)
183 204
184 var full_item_name = item_name 205 var full_item_name = item_name
185 if prog_id != null and door_id != null: 206 if prog_id != null:
186 var door = gamedata.objects.get_doors()[door_id] 207 var door = gamedata.objects.get_doors()[prog_id]
187 full_item_name = "%s (%s)" % [item_name, door.get_name()] 208 full_item_name = "%s (%s)" % [item_name, door.get_name()]
188 209
189 var message 210 var message
@@ -194,6 +215,9 @@ func _process_item(item, index, from, flags, amount):
194 "Received [color=%s]%s[/color] from %s" % [item_color, full_item_name, player_name] 215 "Received [color=%s]%s[/color] from %s" % [item_color, full_item_name, player_name]
195 ) 216 )
196 217
218 if gamedata.anti_trap_ids.has(item):
219 keyboard.block_letter(gamedata.anti_trap_ids[item])
220
197 global._print(message) 221 global._print(message)
198 222
199 global.get_node("Messages").showMessage(message) 223 global.get_node("Messages").showMessage(message)
@@ -288,6 +312,10 @@ func parse_printjson_for_textclient(message):
288func _process_location_scout(item_id, location_id, player, flags): 312func _process_location_scout(item_id, location_id, player, flags):
289 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags} 313 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
290 314
315 if player == client._slot and flags & 4 != 0:
316 # This is a trap for us, so let's not display it.
317 return
318
291 var gamedata = global.get_node("Gamedata") 319 var gamedata = global.get_node("Gamedata")
292 var map_id = gamedata.map_id_by_name.get(global.map) 320 var map_id = gamedata.map_id_by_name.get(global.map)
293 321
@@ -308,8 +336,8 @@ func _process_location_scout(item_id, location_id, player, flags):
308 collectable.setScoutedText(item_name) 336 collectable.setScoutedText(item_name)
309 337
310 338
311func _client_could_not_connect(): 339func _client_could_not_connect(message):
312 emit_signal("could_not_connect") 340 emit_signal("could_not_connect", message)
313 341
314 342
315func _client_connect_status(message): 343func _client_connect_status(message):
@@ -337,12 +365,19 @@ func _client_connected(slot_data):
337 _last_new_item = localdata[0] 365 _last_new_item = localdata[0]
338 366
339 # Read slot data. 367 # Read slot data.
368 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
340 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) 369 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
341 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) 370 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
371 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
342 shuffle_doors = bool(slot_data.get("shuffle_doors", false)) 372 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
373 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
343 shuffle_letters = int(slot_data.get("shuffle_letters", 0)) 374 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
375 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
344 victory_condition = int(slot_data.get("victory_condition", 0)) 376 victory_condition = int(slot_data.get("victory_condition", 0))
345 377
378 if slot_data.has("version"):
379 apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])]
380
346 # Set up item locks. 381 # Set up item locks.
347 _item_locks = {} 382 _item_locks = {}
348 383
@@ -359,6 +394,47 @@ func _client_connected(slot_data):
359 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]] 394 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
360 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1] 395 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
361 396
397 for door_group in gamedata.objects.get_door_groups():
398 if (
399 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
400 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
401 ):
402 for door in door_group.get_doors():
403 _item_locks[door] = [door_group.get_ap_id(), 1]
404
405 if shuffle_control_center_colors:
406 for door in gamedata.objects.get_doors():
407 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
408 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
409
410 for door_group in gamedata.objects.get_door_groups():
411 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
412 for door in door_group.get_doors():
413 _item_locks[door] = [door_group.get_ap_id(), 1]
414
415 if shuffle_gallery_paintings:
416 for door in gamedata.objects.get_doors():
417 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
418 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
419
420 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
421 for door_group in gamedata.objects.get_door_groups():
422 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
423 for door in door_group.get_doors():
424 if not _item_locks.has(door):
425 _item_locks[door] = [door_group.get_ap_id(), 1]
426
427 # Create a reverse item locks map for processing items.
428 _inverse_item_locks = {}
429
430 for door_id in _item_locks.keys():
431 var lock = _item_locks.get(door_id)
432
433 if not _inverse_item_locks.has(lock[0]):
434 _inverse_item_locks[lock[0]] = []
435
436 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
437
362 emit_signal("ap_connected") 438 emit_signal("ap_connected")
363 439
364 440
@@ -453,4 +529,7 @@ func _process_key_item(key, level):
453 _held_letters[key] = max(_held_letters.get(key, 0), level) 529 _held_letters[key] = max(_held_letters.get(key, 0), level)
454 return 530 return
455 531
532 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
533 level += 1
534
456 keyboard.collect_remote_letter(key, level) 535 keyboard.collect_remote_letter(key, level)
diff --git a/client/Archipelago/messages.gd b/client/Archipelago/messages.gd index 52f38b9..82fdbc4 100644 --- a/client/Archipelago/messages.gd +++ b/client/Archipelago/messages.gd
@@ -48,10 +48,11 @@ func showMessage(text):
48 while !_ordered_labels.is_empty(): 48 while !_ordered_labels.is_empty():
49 await get_tree().create_timer(timeout).timeout 49 await get_tree().create_timer(timeout).timeout
50 50
51 var to_remove = _ordered_labels.pop_front() 51 if !_ordered_labels.is_empty():
52 var to_tween = get_tree().create_tween().bind_node(to_remove) 52 var to_remove = _ordered_labels.pop_front()
53 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5) 53 var to_tween = get_tree().create_tween().bind_node(to_remove)
54 to_tween.tween_callback(to_remove.queue_free) 54 to_tween.tween_property(to_remove, "modulate:a", 0.0, 0.5)
55 to_tween.tween_callback(to_remove.queue_free)
55 56
56 if !_message_queue.is_empty(): 57 if !_message_queue.is_empty():
57 var next_msg = _message_queue.pop_front() 58 var next_msg = _message_queue.pop_front()
@@ -59,3 +60,12 @@ func showMessage(text):
59 60
60 if timeout > 4: 61 if timeout > 4:
61 timeout -= 3 62 timeout -= 3
63
64
65func clear():
66 _message_queue.clear()
67
68 for message_label in _ordered_labels:
69 message_label.queue_free()
70
71 _ordered_labels.clear()
diff --git a/client/Archipelago/panel.gd b/client/Archipelago/panel.gd new file mode 100644 index 0000000..fdaaf0e --- /dev/null +++ b/client/Archipelago/panel.gd
@@ -0,0 +1,101 @@
1extends "res://scripts/nodes/panel.gd"
2
3var panel_logic = null
4var symbol_solvable = true
5
6var black = load("res://assets/materials/black.material")
7
8
9func _ready():
10 super._ready()
11
12 var node_path = String(
13 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
14 )
15
16 var gamedata = global.get_node("Gamedata")
17 var panel_id = gamedata.get_panel_for_map_node_path(global.map, node_path)
18 if panel_id != null:
19 var ap = global.get_node("Archipelago")
20 if ap.shuffle_symbols:
21 if global.map == "the_entry" and node_path == "Panels/Entry/front_1":
22 clue = "i"
23 symbol = ""
24
25 setField("clue", clue)
26 setField("symbol", symbol)
27
28 panel_logic = gamedata.objects.get_panels()[panel_id]
29 checkSymbolSolvable()
30
31 if not symbol_solvable:
32 get_tree().get_root().get_node("scene/player").connect(
33 "evaluate_solvability", evaluateSolvability
34 )
35
36
37func checkSymbolSolvable():
38 var old_solvable = symbol_solvable
39 symbol_solvable = true
40
41 if panel_logic == null:
42 # There's no logic for this panel.
43 return
44
45 var ap = global.get_node("Archipelago")
46 if not ap.shuffle_symbols:
47 # Symbols aren't item-locked.
48 return
49
50 var gamedata = global.get_node("Gamedata")
51 for symbol in panel_logic.get_symbols():
52 var item_name = gamedata.kSYMBOL_ITEMS.get(symbol)
53 var item_id = gamedata.objects.get_special_ids()[item_name]
54 if ap.client.getItemAmount(item_id) < 1:
55 symbol_solvable = false
56 break
57
58 if symbol_solvable != old_solvable:
59 if symbol_solvable:
60 setField("clue", clue)
61 setField("symbol", symbol)
62 setField("answer", answer)
63 else:
64 quad_mesh.surface_set_material(0, black)
65 get_node("Hinge/clue").text = "missing"
66 get_node("Hinge/answer").text = "symbols"
67
68
69func checkSolvable(key):
70 checkSymbolSolvable()
71 if not symbol_solvable:
72 return false
73
74 return super.checkSolvable(key)
75
76
77func evaluateSolvability():
78 checkSolvable("")
79
80
81func passedInput(key, skip_focus_check = false):
82 if not symbol_solvable:
83 return
84
85 super.passedInput(key, skip_focus_check)
86
87
88func focus():
89 if not symbol_solvable:
90 has_focus = false
91 return
92
93 super.focus()
94
95
96func unfocus():
97 if not symbol_solvable:
98 has_focus = false
99 return
100
101 super.unfocus()
diff --git a/client/Archipelago/pauseMenu.gd b/client/Archipelago/pauseMenu.gd index 6c013a5..df4bfd1 100644 --- a/client/Archipelago/pauseMenu.gd +++ b/client/Archipelago/pauseMenu.gd
@@ -4,3 +4,10 @@ extends "res://scripts/ui/pauseMenu.gd"
4func _pause_game(): 4func _pause_game():
5 global.get_node("Textclient").dismiss() 5 global.get_node("Textclient").dismiss()
6 super._pause_game() 6 super._pause_game()
7
8
9func _main_menu():
10 global.loaded = false
11 global.get_node("Archipelago").disconnect_from_ap()
12 global.get_node("Messages").clear()
13 super._main_menu()
diff --git a/client/Archipelago/player.gd b/client/Archipelago/player.gd index dd6aa2b..f0b214f 100644 --- a/client/Archipelago/player.gd +++ b/client/Archipelago/player.gd
@@ -16,6 +16,8 @@ const kEndingNameByVictoryValue = {
16 12: "WHITE", 16 12: "WHITE",
17} 17}
18 18
19signal evaluate_solvability
20
19 21
20func _ready(): 22func _ready():
21 var khl_script = load("res://scripts/nodes/keyHolderListener.gd") 23 var khl_script = load("res://scripts/nodes/keyHolderListener.gd")
@@ -34,7 +36,10 @@ func _ready():
34 if not door.has_ap_id(): 36 if not door.has_ap_id():
35 continue 37 continue
36 38
37 if door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY: 39 if (
40 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
41 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
42 ):
38 continue 43 continue
39 44
40 var locationListener = ap.SCRIPT_locationListener.new() 45 var locationListener = ap.SCRIPT_locationListener.new()
@@ -66,6 +71,12 @@ func _ready():
66 71
67 locationListener.senders.append(NodePath("../" + khl.name)) 72 locationListener.senders.append(NodePath("../" + khl.name))
68 73
74 for sender in door.get_senders():
75 locationListener.senders.append(NodePath("/root/scene/" + sender))
76
77 if door.has_complete_at():
78 locationListener.complete_at = door.get_complete_at()
79
69 get_parent().add_child.call_deferred(locationListener) 80 get_parent().add_child.call_deferred(locationListener)
70 81
71 # Set up letter locations. 82 # Set up letter locations.
@@ -86,7 +97,10 @@ func _ready():
86 != ap.kLETTER_BEHAVIOR_VANILLA 97 != ap.kLETTER_BEHAVIOR_VANILLA
87 ): 98 ):
88 var scout = ap.scout_location(letter.get_ap_id()) 99 var scout = ap.scout_location(letter.get_ap_id())
89 if scout != null: 100 if (
101 scout != null
102 and not (scout["player"] == ap.client._slot and scout["flags"] & 4 != 0)
103 ):
90 var item_name = "Unknown" 104 var item_name = "Unknown"
91 var item_player_game = ap.client._game_by_player[float(scout["player"])] 105 var item_player_game = ap.client._game_by_player[float(scout["player"])]
92 if ap.client._item_id_to_name[item_player_game].has(scout["item"]): 106 if ap.client._item_id_to_name[item_player_game].has(scout["item"]):
@@ -188,6 +202,29 @@ func _ready():
188 warp_enter.rotation_degrees.y = 90 202 warp_enter.rotation_degrees.y = 90
189 get_parent().add_child.call_deferred(warp_enter) 203 get_parent().add_child.call_deferred(warp_enter)
190 204
205 if global.map == "the_entry":
206 # Remove door behind X1.
207 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
208 door_node.handleTriggered()
209
210 # Display win condition.
211 var sign_prefab = preload("res://objects/nodes/sign.tscn")
212 var sign1 = sign_prefab.instantiate()
213 sign1.position = Vector3(-7, 5, -15.01)
214 sign1.text = "victory"
215 get_parent().add_child.call_deferred(sign1)
216
217 var sign2 = sign_prefab.instantiate()
218 sign2.position = Vector3(-7, 4, -15.01)
219 sign2.text = "%s ending" % kEndingNameByVictoryValue.get(ap.victory_condition, "?")
220
221 var sign2_color = kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
222 if sign2_color == "white":
223 sign2_color = "silver"
224
225 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
226 get_parent().add_child.call_deferred(sign2)
227
191 super._ready() 228 super._ready()
192 229
193 await get_tree().process_frame 230 await get_tree().process_frame
diff --git a/client/Archipelago/saver.gd b/client/Archipelago/saver.gd index 0fba9e7..44bc179 100644 --- a/client/Archipelago/saver.gd +++ b/client/Archipelago/saver.gd
@@ -7,3 +7,17 @@ func levelLoaded():
7 ap.keyboard.load_keyholders.call_deferred(global.map) 7 ap.keyboard.load_keyholders.call_deferred(global.map)
8 else: 8 else:
9 reload.call_deferred() 9 reload.call_deferred()
10
11
12func reload():
13 # Just rewriting this whole thing so I can remove Chris's safeguard.
14 var file = FileAccess.open(path + type + ".save", FileAccess.READ)
15 if file:
16 var data = file.get_var(true)
17 file.close()
18 for datum in data:
19 var saveable = get_node_or_null(datum[0])
20 if saveable != null:
21 saveable.is_complete = datum[1]
22 if saveable.is_complete:
23 saveable.loadData(saveable.is_complete)
diff --git a/client/Archipelago/settings_screen.gd b/client/Archipelago/settings_screen.gd index aaaf72a..140b4f4 100644 --- a/client/Archipelago/settings_screen.gd +++ b/client/Archipelago/settings_screen.gd
@@ -40,10 +40,14 @@ func _ready():
40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd") 40 ResourceLoader.load("user://maps/Archipelago/keyHolderResetterListener.gd")
41 ) 41 )
42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd")) 42 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/panel.gd"))
43 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd")) 44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/pauseMenu.gd"))
44 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd")) 45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
45 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd")) 46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd"))
47 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleport.gd"))
46 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleportListener.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/worldport.gd"))
47 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldportListener.gd")) 51 installScriptExtension(ResourceLoader.load("user://maps/Archipelago/worldportListener.gd"))
48 52
49 var proto_script = load("user://maps/Archipelago/generated/proto.gd") 53 var proto_script = load("user://maps/Archipelago/generated/proto.gd")
@@ -66,6 +70,7 @@ func _ready():
66 global.add_child(textclient_instance) 70 global.add_child(textclient_instance)
67 71
68 var ap = global.get_node("Archipelago") 72 var ap = global.get_node("Archipelago")
73 var gamedata = global.get_node("Gamedata")
69 ap.connect("ap_connected", connectionSuccessful) 74 ap.connect("ap_connected", connectionSuccessful)
70 ap.connect("could_not_connect", connectionUnsuccessful) 75 ap.connect("could_not_connect", connectionUnsuccessful)
71 ap.connect("connect_status", connectionStatus) 76 ap.connect("connect_status", connectionStatus)
@@ -89,13 +94,17 @@ func _ready():
89 history_box.get_popup().connect("id_pressed", historySelected) 94 history_box.get_popup().connect("id_pressed", historySelected)
90 95
91 # Show client version. 96 # Show client version.
92 $Panel/title.text = "ARCHIPELAGO (%s)" % ap.my_version 97 $Panel/title.text = "ARCHIPELAGO (%d.%d)" % [gamedata.objects.get_version(), ap.MOD_VERSION]
93 98
94 # Increase font size in text boxes. 99 # Increase font size in text boxes.
95 $Panel/server_box.add_theme_font_size_override("font_size", 36) 100 $Panel/server_box.add_theme_font_size_override("font_size", 36)
96 $Panel/player_box.add_theme_font_size_override("font_size", 36) 101 $Panel/player_box.add_theme_font_size_override("font_size", 36)
97 $Panel/password_box.add_theme_font_size_override("font_size", 36) 102 $Panel/password_box.add_theme_font_size_override("font_size", 36)
98 103
104 # Set up version mismatch dialog.
105 $Panel/VersionMismatch.connect("confirmed", startGame)
106 $Panel/VersionMismatch.get_cancel_button().pressed.connect(versionMismatchDeclined)
107
99 108
100# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd 109# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
101func installScriptExtension(childScript: Resource): 110func installScriptExtension(childScript: Resource):
@@ -125,6 +134,33 @@ func connectionStatus(message):
125 134
126func connectionSuccessful(): 135func connectionSuccessful():
127 var ap = global.get_node("Archipelago") 136 var ap = global.get_node("Archipelago")
137 var gamedata = global.get_node("Gamedata")
138
139 # Check for major version mismatch.
140 if ap.apworld_version[0] != gamedata.objects.get_version():
141 $Panel/AcceptDialog.exclusive = false
142
143 var popup = self.get_node("Panel/VersionMismatch")
144 popup.title = "Version Mismatch!"
145 popup.dialog_text = (
146 "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."
147 % [
148 ap.apworld_version[0],
149 ap.apworld_version[1],
150 gamedata.objects.get_version(),
151 ap.MOD_VERSION
152 ]
153 )
154 popup.exclusive = true
155 popup.popup_centered()
156
157 return
158
159 startGame()
160
161
162func startGame():
163 var ap = global.get_node("Archipelago")
128 164
129 # Save connection details 165 # Save connection details
130 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass] 166 var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
@@ -158,9 +194,13 @@ func connectionSuccessful():
158 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn") 194 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
159 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") 195 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
160 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") 196 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
197 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
161 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") 198 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
199 clearResourceCache("res://objects/nodes/panel.tscn")
162 clearResourceCache("res://objects/nodes/player.tscn") 200 clearResourceCache("res://objects/nodes/player.tscn")
163 clearResourceCache("res://objects/nodes/saver.tscn") 201 clearResourceCache("res://objects/nodes/saver.tscn")
202 clearResourceCache("res://objects/nodes/teleport.tscn")
203 clearResourceCache("res://objects/nodes/worldport.tscn")
164 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn") 204 clearResourceCache("res://objects/scenes/menus/pause_menu.tscn")
165 205
166 var paintings_dir = DirAccess.open("res://objects/meshes/paintings") 206 var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
@@ -185,6 +225,13 @@ func connectionUnsuccessful(error_message):
185 popup.get_ok_button().visible = true 225 popup.get_ok_button().visible = true
186 popup.popup_centered() 226 popup.popup_centered()
187 227
228 $Panel/connect_button.disabled = false
229
230
231func versionMismatchDeclined():
232 $Panel/AcceptDialog.hide()
233 $Panel/connect_button.disabled = false
234
188 235
189func historySelected(index): 236func historySelected(index):
190 var ap = global.get_node("Archipelago") 237 var ap = global.get_node("Archipelago")
diff --git a/client/Archipelago/teleport.gd b/client/Archipelago/teleport.gd new file mode 100644 index 0000000..428d50b --- /dev/null +++ b/client/Archipelago/teleport.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/teleport.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/teleportListener.gd b/client/Archipelago/teleportListener.gd index 4a7deec..6f363af 100644 --- a/client/Archipelago/teleportListener.gd +++ b/client/Archipelago/teleportListener.gd
@@ -9,6 +9,17 @@ func _ready():
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names() 9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 ) 10 )
11 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
12 var gamedata = global.get_node("Gamedata") 23 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 24 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null: 25 if door_id != null:
diff --git a/client/Archipelago/textclient.gd b/client/Archipelago/textclient.gd index 4b03151..85cc6d2 100644 --- a/client/Archipelago/textclient.gd +++ b/client/Archipelago/textclient.gd
@@ -50,7 +50,7 @@ func _ready():
50 50
51 51
52func _input(event): 52func _input(event):
53 if event is InputEventKey and event.pressed: 53 if global.loaded and event is InputEventKey and event.pressed:
54 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT): 54 if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
55 if !get_tree().paused: 55 if !get_tree().paused:
56 is_open = true 56 is_open = true
diff --git a/client/Archipelago/visibilityListener.gd b/client/Archipelago/visibilityListener.gd new file mode 100644 index 0000000..5ea17a0 --- /dev/null +++ b/client/Archipelago/visibilityListener.gd
@@ -0,0 +1,38 @@
1extends "res://scripts/nodes/listeners/visibilityListener.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33
34func _readier():
35 var ap = global.get_node("Archipelago")
36
37 if ap.client.getItemAmount(item_id) >= item_amount:
38 handleTriggered()
diff --git a/client/Archipelago/worldport.gd b/client/Archipelago/worldport.gd new file mode 100644 index 0000000..d0fb6c9 --- /dev/null +++ b/client/Archipelago/worldport.gd
@@ -0,0 +1,10 @@
1extends "res://scripts/nodes/worldport.gd"
2
3
4func _ready():
5 if global.map == "icarus" and exit == "daedalus":
6 var ap = global.get_node("Archipelago")
7 if not ap.daedalus_roof_access:
8 entry_point = Vector3(58, 10, 0)
9
10 super._ready()
diff --git a/client/Archipelago/worldportListener.gd b/client/Archipelago/worldportListener.gd index c31c825..5c2faff 100644 --- a/client/Archipelago/worldportListener.gd +++ b/client/Archipelago/worldportListener.gd
@@ -1,8 +1,8 @@
1extends "res://scripts/nodes/listeners/worldportListener.gd" 1extends "res://scripts/nodes/listeners/worldportListener.gd"
2 2
3 3
4func changeScene(): 4func handleTriggered():
5 if exit == "menus/credits": 5 if exit == "menus/credits":
6 return 6 return
7 7
8 super.changeScene() 8 super.handleTriggered()
diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md new file mode 100644 index 0000000..3174853 --- /dev/null +++ b/client/CHANGELOG.md
@@ -0,0 +1,47 @@
1# lingo2-archipelago Client Releases
2
3## v5.5 - 2025-09-16
4
5- Compatability update for v5.5 of the apworld.
6
7Download:
8[lingo2-archipelago-client-v5.5.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v5.5.zip)<br/>
9Source:
10[v5.5](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v5.5)
11
12## v4.4 - 2025-09-13
13
14- Added support for anti-collectable trap items.
15- Fixed entrance to The Jubilant not opening properly when using control center
16 color shuffle.
17- Fixed the location "The Entry (Colored Doors Area) - OPEN" not sending.
18- Fixed level 2 letters not activating properly when letter shuffle is set to
19 Item Cyan.
20- Messages are now cleared out when returning to the main menu.
21- The player is prevented from accidentally breaking roof access logic when
22 returning to Daedalus from Icarus.
23
24Download:
25[lingo2-archipelago-client-v4.4.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v4.4.zip)<br/>
26Source:
27[v4.4](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v4.4)
28
29## v3.3 - 2025-09-12
30
31- Fixed issue downloading large datapackages (such as TUNIC's).
32- Connection failures now show error messages.
33
34Download:
35[lingo2-archipelago-client-v3.3.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.3.zip)<br/>
36Source:
37[v3.3](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.3)
38
39## v3.2 - 2025-09-12
40
41- Initial release for testing. Features include door shuffle, letter shuffle,
42 and symbol shuffle.
43
44Download:
45[lingo2-archipelago-client-v3.2.zip](https://files.fourisland.com/releases/lingo2-archipelago/client/lingo2-archipelago-client-v3.2.zip)<br/>
46Source:
47[v3.2](https://code.fourisland.com/lingo2-archipelago/tag/?h=client-v3.2)
diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..99589c5 --- /dev/null +++ b/client/README.md
@@ -0,0 +1,90 @@
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 40dd46f..da83b23 100644 --- a/client/archipelago.tscn +++ b/client/archipelago.tscn
@@ -40,6 +40,7 @@ offset_right = 1920.0
40offset_bottom = 225.0 40offset_bottom = 225.0
41text = "ARCHIPELAGO" 41text = "ARCHIPELAGO"
42valign = 1 42valign = 1
43horizontal_alignment = 1
43theme = ExtResource("2_g4bvn") 44theme = ExtResource("2_g4bvn")
44 45
45[node name="credit" parent="Panel" type="Label"] 46[node name="credit" parent="Panel" type="Label"]
@@ -150,6 +151,10 @@ caret_blink = true
150offset_right = 83.0 151offset_right = 83.0
151offset_bottom = 58.0 152offset_bottom = 58.0
152 153
154[node name="VersionMismatch" type="ConfirmationDialog" parent="Panel"]
155offset_right = 83.0
156offset_bottom = 58.0
157
153[node name="connection_history" type="MenuButton" parent="Panel"] 158[node name="connection_history" type="MenuButton" parent="Panel"]
154offset_left = 1239.0 159offset_left = 1239.0
155offset_top = 276.0 160offset_top = 276.0