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