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