about summary refs log tree commit diff stats
path: root/client/Archipelago/manager.gd
diff options
context:
space:
mode:
Diffstat (limited to 'client/Archipelago/manager.gd')
-rw-r--r--client/Archipelago/manager.gd544
1 files changed, 544 insertions, 0 deletions
diff --git a/client/Archipelago/manager.gd b/client/Archipelago/manager.gd new file mode 100644 index 0000000..218870c --- /dev/null +++ b/client/Archipelago/manager.gd
@@ -0,0 +1,544 @@
1extends Node
2
3const MOD_VERSION = 7
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 = []
15var show_compass = false
16
17var client
18var keyboard
19
20var _localdata_file = ""
21var _last_new_item = -1
22var _batch_locations = false
23var _held_locations = []
24var _held_location_scouts = []
25var _location_scouts = {}
26var _item_locks = {}
27var _inverse_item_locks = {}
28var _held_letters = {}
29var _letters_setup = false
30
31const kSHUFFLE_LETTERS_VANILLA = 0
32const kSHUFFLE_LETTERS_UNLOCKED = 1
33const kSHUFFLE_LETTERS_PROGRESSIVE = 2
34const kSHUFFLE_LETTERS_VANILLA_CYAN = 3
35const kSHUFFLE_LETTERS_ITEM_CYAN = 4
36
37const kLETTER_BEHAVIOR_VANILLA = 0
38const kLETTER_BEHAVIOR_ITEM = 1
39const kLETTER_BEHAVIOR_UNLOCKED = 2
40
41const kCYAN_DOOR_BEHAVIOR_H2 = 0
42const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
43const kCYAN_DOOR_BEHAVIOR_ITEM = 2
44
45var apworld_version = [0, 0]
46var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
47var daedalus_roof_access = false
48var keyholder_sanity = false
49var shuffle_control_center_colors = false
50var shuffle_doors = false
51var shuffle_gallery_paintings = false
52var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
53var shuffle_symbols = false
54var strict_cyan_ending = false
55var strict_purple_ending = false
56var victory_condition = -1
57
58signal could_not_connect
59signal connect_status
60signal ap_connected
61
62
63func _init():
64 # Read AP settings from file, if there are any
65 if FileAccess.file_exists("user://ap_settings"):
66 var file = FileAccess.open("user://ap_settings", FileAccess.READ)
67 var data = file.get_var(true)
68 file.close()
69
70 if typeof(data) != TYPE_ARRAY:
71 global._print("AP settings file is corrupted")
72 data = []
73
74 if data.size() > 0:
75 ap_server = data[0]
76
77 if data.size() > 1:
78 ap_user = data[1]
79
80 if data.size() > 2:
81 ap_pass = data[2]
82
83 if data.size() > 3:
84 connection_history = data[3]
85
86 if data.size() > 4:
87 show_compass = data[4]
88
89
90func _ready():
91 client = SCRIPT_client.new()
92 client.SCRIPT_uuid = SCRIPT_uuid
93
94 client.connect("item_received", _process_item)
95 client.connect("message_received", _process_message)
96 client.connect("location_scout_received", _process_location_scout)
97 client.connect("could_not_connect", _client_could_not_connect)
98 client.connect("connect_status", _client_connect_status)
99 client.connect("client_connected", _client_connected)
100
101 add_child(client)
102
103 keyboard = SCRIPT_keyboard.new()
104 add_child(keyboard)
105
106
107func saveSettings():
108 # Save the AP settings to disk.
109 var path = "user://ap_settings"
110 var file = FileAccess.open(path, FileAccess.WRITE)
111
112 var data = [
113 ap_server,
114 ap_user,
115 ap_pass,
116 connection_history,
117 show_compass,
118 ]
119 file.store_var(data, true)
120 file.close()
121
122
123func saveLocaldata():
124 # Save the MW/slot specific settings to disk.
125 var dir = DirAccess.open("user://")
126 var folder = "archipelago_data"
127 if not dir.dir_exists(folder):
128 dir.make_dir(folder)
129
130 var file = FileAccess.open(_localdata_file, FileAccess.WRITE)
131
132 var data = [
133 _last_new_item,
134 ]
135 file.store_var(data, true)
136 file.close()
137
138
139func connectToServer():
140 _last_new_item = -1
141 _batch_locations = false
142 _held_locations = []
143 _held_location_scouts = []
144 _location_scouts = {}
145 _letters_setup = false
146 _held_letters = {}
147
148 client.connectToServer(ap_server, ap_user, ap_pass)
149
150
151func getSaveFileName():
152 return "zzAP_%s_%d" % [client._seed, client._slot]
153
154
155func disconnect_from_ap():
156 client.disconnect_from_ap()
157
158
159func get_item_id_for_door(door_id):
160 return _item_locks.get(door_id, null)
161
162
163func _process_item(item, index, from, flags, amount):
164 var item_name = "Unknown"
165 if client._item_id_to_name["Lingo 2"].has(item):
166 item_name = client._item_id_to_name["Lingo 2"][item]
167
168 var gamedata = global.get_node("Gamedata")
169
170 var prog_id = null
171 if _inverse_item_locks.has(item):
172 for lock in _inverse_item_locks.get(item):
173 if lock[1] != amount:
174 continue
175
176 if gamedata.progressive_id_by_ap_id.has(item):
177 prog_id = lock[0]
178
179 if gamedata.get_door_map_name(lock[0]) != global.map:
180 continue
181
182 var receivers = gamedata.get_door_receivers(lock[0])
183 var scene = get_tree().get_root().get_node_or_null("scene")
184 if scene != null:
185 for receiver in receivers:
186 var rnode = scene.get_node_or_null(receiver)
187 if rnode != null:
188 rnode.handleTriggered()
189
190 var letter_id = gamedata.letter_id_by_ap_id.get(item, null)
191 if letter_id != null:
192 var letter = gamedata.objects.get_letters()[letter_id]
193 if not letter.has_level2() or not letter.get_level2():
194 _process_key_item(letter.get_key(), amount)
195
196 if gamedata.symbol_item_ids.has(item):
197 var player = get_tree().get_root().get_node_or_null("scene/player")
198 if player != null:
199 player.emit_signal("evaluate_solvability")
200
201 # Show a message about the item if it's new.
202 if index != null and index > _last_new_item:
203 _last_new_item = index
204 saveLocaldata()
205
206 var player_name = "Unknown"
207 if client._player_name_by_slot.has(float(from)):
208 player_name = client._player_name_by_slot[float(from)]
209
210 var item_color = colorForItemType(flags)
211
212 var full_item_name = item_name
213 if prog_id != null:
214 var door = gamedata.objects.get_doors()[prog_id]
215 full_item_name = "%s (%s)" % [item_name, door.get_name()]
216
217 var message
218 if from == client._slot:
219 message = "Found [color=%s]%s[/color]" % [item_color, full_item_name]
220 else:
221 message = (
222 "Received [color=%s]%s[/color] from %s" % [item_color, full_item_name, player_name]
223 )
224
225 if gamedata.anti_trap_ids.has(item):
226 keyboard.block_letter(gamedata.anti_trap_ids[item])
227
228 global._print(message)
229
230 global.get_node("Messages").showMessage(message)
231
232
233func _process_message(message):
234 parse_printjson_for_textclient(message)
235
236 if (
237 !message.has("receiving")
238 or !message.has("item")
239 or message["item"]["player"] != client._slot
240 ):
241 return
242
243 var item_name = "Unknown"
244 var item_player_game = client._game_by_player[message["receiving"]]
245 if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])):
246 item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])]
247
248 var location_name = "Unknown"
249 var location_player_game = client._game_by_player[message["item"]["player"]]
250 if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])):
251 location_name = (client._location_id_to_name[location_player_game][int(
252 message["item"]["location"]
253 )])
254
255 var player_name = "Unknown"
256 if client._player_name_by_slot.has(message["receiving"]):
257 player_name = client._player_name_by_slot[message["receiving"]]
258
259 var item_color = colorForItemType(message["item"]["flags"])
260
261 if message["type"] == "Hint":
262 var is_for = ""
263 if message["receiving"] != client._slot:
264 is_for = " for %s" % player_name
265 if !message.has("found") || !message["found"]:
266 global.get_node("Messages").showMessage(
267 (
268 "Hint: [color=%s]%s[/color]%s is on %s"
269 % [item_color, item_name, is_for, location_name]
270 )
271 )
272 else:
273 if message["receiving"] != client._slot:
274 var sentMsg = "Sent [color=%s]%s[/color] to %s" % [item_color, item_name, player_name]
275 #if _hinted_locations.has(message["item"]["location"]):
276 # sentMsg += " ([color=#fafad2]Hinted![/color])"
277 global.get_node("Messages").showMessage(sentMsg)
278
279
280func parse_printjson_for_textclient(message):
281 var parts = []
282 for message_part in message["data"]:
283 if !message_part.has("type") and message_part.has("text"):
284 parts.append(message_part["text"])
285 elif message_part["type"] == "player_id":
286 if int(message_part["text"]) == client._slot:
287 parts.append(
288 "[color=#ee00ee]%s[/color]" % client._player_name_by_slot[client._slot]
289 )
290 else:
291 var from = float(message_part["text"])
292 parts.append("[color=#fafad2]%s[/color]" % client._player_name_by_slot[from])
293 elif message_part["type"] == "item_id":
294 var item_name = "Unknown"
295 var item_player_game = client._game_by_player[message_part["player"]]
296 if client._item_id_to_name[item_player_game].has(int(message_part["text"])):
297 item_name = client._item_id_to_name[item_player_game][int(message_part["text"])]
298
299 parts.append(
300 "[color=%s]%s[/color]" % [colorForItemType(message_part["flags"]), item_name]
301 )
302 elif message_part["type"] == "location_id":
303 var location_name = "Unknown"
304 var location_player_game = client._game_by_player[message_part["player"]]
305 if client._location_id_to_name[location_player_game].has(int(message_part["text"])):
306 location_name = client._location_id_to_name[location_player_game][int(
307 message_part["text"]
308 )]
309
310 parts.append("[color=#00ff7f]%s[/color]" % location_name)
311 elif message_part.has("text"):
312 parts.append(message_part["text"])
313
314 var textclient_node = global.get_node("Textclient")
315 if textclient_node != null:
316 textclient_node.parse_printjson("".join(parts))
317
318
319func _process_location_scout(item_id, location_id, player, flags):
320 _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags}
321
322 if player == client._slot and flags & 4 != 0:
323 # This is a trap for us, so let's not display it.
324 return
325
326 var gamedata = global.get_node("Gamedata")
327 var map_id = gamedata.map_id_by_name.get(global.map)
328
329 var item_name = "Unknown"
330 var item_player_game = client._game_by_player[float(player)]
331 if client._item_id_to_name[item_player_game].has(item_id):
332 item_name = client._item_id_to_name[item_player_game][item_id]
333
334 var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null)
335 if letter_id != null:
336 var letter = gamedata.objects.get_letters()[letter_id]
337 var room = gamedata.objects.get_rooms()[letter.get_room_id()]
338 if room.get_map_id() == map_id:
339 var collectable = get_tree().get_root().get_node("scene").get_node_or_null(
340 letter.get_path()
341 )
342 if collectable != null:
343 collectable.setScoutedText(item_name)
344
345
346func _client_could_not_connect(message):
347 emit_signal("could_not_connect", message)
348
349
350func _client_connect_status(message):
351 emit_signal("connect_status", message)
352
353
354func _client_connected(slot_data):
355 var gamedata = global.get_node("Gamedata")
356
357 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
358 _last_new_item = -1
359
360 if FileAccess.file_exists(_localdata_file):
361 var ap_file = FileAccess.open(_localdata_file, FileAccess.READ)
362 var localdata = []
363 if ap_file != null:
364 localdata = ap_file.get_var(true)
365 ap_file.close()
366
367 if typeof(localdata) != TYPE_ARRAY:
368 print("AP localdata file is corrupted")
369 localdata = []
370
371 if localdata.size() > 0:
372 _last_new_item = localdata[0]
373
374 # Read slot data.
375 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
376 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
377 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
378 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
379 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
380 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
381 shuffle_letters = int(slot_data.get("shuffle_letters", 0))
382 shuffle_symbols = bool(slot_data.get("shuffle_symbols", false))
383 strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false))
384 strict_purple_ending = bool(slot_data.get("strict_purple_ending", false))
385 victory_condition = int(slot_data.get("victory_condition", 0))
386
387 if slot_data.has("version"):
388 apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])]
389
390 # Set up item locks.
391 _item_locks = {}
392
393 if shuffle_doors:
394 for door in gamedata.objects.get_doors():
395 if (
396 door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD
397 or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
398 ):
399 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
400
401 for progressive in gamedata.objects.get_progressives():
402 for i in range(0, progressive.get_doors().size()):
403 var door = gamedata.objects.get_doors()[progressive.get_doors()[i]]
404 _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1]
405
406 for door_group in gamedata.objects.get_door_groups():
407 if (
408 door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR
409 or door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP
410 ):
411 for door in door_group.get_doors():
412 _item_locks[door] = [door_group.get_ap_id(), 1]
413
414 if shuffle_control_center_colors:
415 for door in gamedata.objects.get_doors():
416 if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR:
417 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
418
419 for door_group in gamedata.objects.get_door_groups():
420 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR:
421 for door in door_group.get_doors():
422 _item_locks[door] = [door_group.get_ap_id(), 1]
423
424 if shuffle_gallery_paintings:
425 for door in gamedata.objects.get_doors():
426 if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING:
427 _item_locks[door.get_id()] = [door.get_ap_id(), 1]
428
429 if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM:
430 for door_group in gamedata.objects.get_door_groups():
431 if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS:
432 for door in door_group.get_doors():
433 if not _item_locks.has(door):
434 _item_locks[door] = [door_group.get_ap_id(), 1]
435
436 # Create a reverse item locks map for processing items.
437 _inverse_item_locks = {}
438
439 for door_id in _item_locks.keys():
440 var lock = _item_locks.get(door_id)
441
442 if not _inverse_item_locks.has(lock[0]):
443 _inverse_item_locks[lock[0]] = []
444
445 _inverse_item_locks[lock[0]].append([door_id, lock[1]])
446
447 emit_signal("ap_connected")
448
449
450func start_batching_locations():
451 _batch_locations = true
452
453
454func send_location(loc_id):
455 if _batch_locations:
456 _held_locations.append(loc_id)
457 else:
458 client.sendLocation(loc_id)
459
460
461func scout_location(loc_id):
462 if _location_scouts.has(loc_id):
463 return _location_scouts.get(loc_id)
464
465 if _batch_locations:
466 _held_location_scouts.append(loc_id)
467 else:
468 client.scoutLocation(loc_id)
469
470 return null
471
472
473func stop_batching_locations():
474 _batch_locations = false
475
476 if not _held_locations.is_empty():
477 client.sendLocations(_held_locations)
478 _held_locations.clear()
479
480 if not _held_location_scouts.is_empty():
481 client.scoutLocations(_held_location_scouts)
482 _held_location_scouts.clear()
483
484
485func colorForItemType(flags):
486 var int_flags = int(flags)
487 if int_flags & 1: # progression
488 if int_flags & 2: # proguseful
489 return "#f0d200"
490 else:
491 return "#bc51e0"
492 elif int_flags & 2: # useful
493 return "#2b67ff"
494 elif int_flags & 4: # trap
495 return "#d63a22"
496 else: # filler
497 return "#14de9e"
498
499
500func get_letter_behavior(key, level2):
501 if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED:
502 return kLETTER_BEHAVIOR_UNLOCKED
503
504 if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters):
505 if level2:
506 if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN:
507 return kLETTER_BEHAVIOR_VANILLA
508 else:
509 return kLETTER_BEHAVIOR_ITEM
510 else:
511 return kLETTER_BEHAVIOR_UNLOCKED
512
513 if not level2 and ["h", "i", "n", "t"].has(key):
514 # This differs from the equivalent function in the apworld. Logically it is
515 # the same as UNLOCKED since they are in the starting room, but VANILLA
516 # means the player still has to actually pick up the letters.
517 return kLETTER_BEHAVIOR_VANILLA
518
519 if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE:
520 return kLETTER_BEHAVIOR_ITEM
521
522 return kLETTER_BEHAVIOR_VANILLA
523
524
525func setup_keys():
526 keyboard.load_seed()
527
528 _letters_setup = true
529
530 for k in _held_letters.keys():
531 _process_key_item(k, _held_letters[k])
532
533 _held_letters.clear()
534
535
536func _process_key_item(key, level):
537 if not _letters_setup:
538 _held_letters[key] = max(_held_letters.get(key, 0), level)
539 return
540
541 if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN:
542 level += 1
543
544 keyboard.collect_remote_letter(key, level)