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