diff options
Diffstat (limited to 'apworld')
| -rw-r--r-- | apworld/__init__.py | 11 | ||||
| -rw-r--r-- | apworld/client/client.gd | 26 | ||||
| -rw-r--r-- | apworld/client/gamedata.gd | 4 | ||||
| -rw-r--r-- | apworld/client/main.gd | 2 | ||||
| -rw-r--r-- | apworld/client/manager.gd | 28 | ||||
| -rw-r--r-- | apworld/client/player.gd | 107 | ||||
| -rw-r--r-- | apworld/client/textclient.gd | 102 | ||||
| -rw-r--r-- | apworld/client/unlockReaderListener.gd | 46 | ||||
| -rw-r--r-- | apworld/context.py | 114 | ||||
| -rw-r--r-- | apworld/player_logic.py | 3 | ||||
| -rw-r--r-- | apworld/static_logic.py | 9 | ||||
| -rw-r--r-- | apworld/tracker.py | 5 | 
12 files changed, 389 insertions, 68 deletions
| diff --git a/apworld/__init__.py b/apworld/__init__.py index f5774c6..3d2f075 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py | |||
| @@ -77,7 +77,10 @@ class Lingo2World(World): | |||
| 77 | if self.options.shuffle_worldports: | 77 | if self.options.shuffle_worldports: | 
| 78 | if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: | 78 | if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: | 
| 79 | slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] | 79 | slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] | 
| 80 | self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()} | 80 | self.port_pairings = { | 
| 81 | self.static_logic.port_id_by_ap_id[int(fp)]: self.static_logic.port_id_by_ap_id[int(tp)] | ||
| 82 | for fp, tp in slot_value.items() | ||
| 83 | } | ||
| 81 | 84 | ||
| 82 | connect_ports_from_ut(self.port_pairings, self) | 85 | connect_ports_from_ut(self.port_pairings, self) | 
| 83 | else: | 86 | else: | 
| @@ -152,7 +155,11 @@ class Lingo2World(World): | |||
| 152 | } | 155 | } | 
| 153 | 156 | ||
| 154 | if self.options.shuffle_worldports: | 157 | if self.options.shuffle_worldports: | 
| 155 | slot_data["port_pairings"] = self.port_pairings | 158 | def get_port_ap_id(port_id): | 
| 159 | return self.static_logic.objects.ports[port_id].ap_id | ||
| 160 | |||
| 161 | slot_data["port_pairings"] = {get_port_ap_id(from_id): get_port_ap_id(to_id) | ||
| 162 | for from_id, to_id in self.port_pairings.items()} | ||
| 156 | 163 | ||
| 157 | return slot_data | 164 | return slot_data | 
| 158 | 165 | ||
| diff --git a/apworld/client/client.gd b/apworld/client/client.gd index ce5ac7e..c149482 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd | |||
| @@ -26,6 +26,7 @@ var _accessible_locations = [] | |||
| 26 | var _accessible_worldports = [] | 26 | var _accessible_worldports = [] | 
| 27 | var _goal_accessible = false | 27 | var _goal_accessible = false | 
| 28 | var _latched_doors = [] | 28 | var _latched_doors = [] | 
| 29 | var _hinted_locations = [] | ||
| 29 | 30 | ||
| 30 | signal could_not_connect | 31 | signal could_not_connect | 
| 31 | signal connect_status | 32 | signal connect_status | 
| @@ -38,8 +39,10 @@ signal hint_received(message) | |||
| 38 | signal door_latched(id) | 39 | signal door_latched(id) | 
| 39 | signal accessible_locations_updated | 40 | signal accessible_locations_updated | 
| 40 | signal checked_locations_updated | 41 | signal checked_locations_updated | 
| 42 | signal ignored_locations_updated(locations) | ||
| 41 | signal checked_worldports_updated | 43 | signal checked_worldports_updated | 
| 42 | signal keyboard_update_received | 44 | signal keyboard_update_received | 
| 45 | signal hinted_locations_updated | ||
| 43 | 46 | ||
| 44 | 47 | ||
| 45 | func _init(): | 48 | func _init(): | 
| @@ -199,6 +202,21 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
| 199 | 202 | ||
| 200 | door_latched.emit(iid) | 203 | door_latched.emit(iid) | 
| 201 | 204 | ||
| 205 | elif cmd == "SetIgnoredLocations": | ||
| 206 | var locs = [] | ||
| 207 | for id in message["locations"]: | ||
| 208 | locs.append(int(id)) | ||
| 209 | |||
| 210 | ignored_locations_updated.emit(locs) | ||
| 211 | |||
| 212 | elif cmd == "UpdateHintedLocations": | ||
| 213 | for id in message["locations"]: | ||
| 214 | var iid = int(id) | ||
| 215 | if !_hinted_locations.has(iid): | ||
| 216 | _hinted_locations.append(iid) | ||
| 217 | |||
| 218 | hinted_locations_updated.emit() | ||
| 219 | |||
| 202 | 220 | ||
| 203 | func connectToServer(server, un, pw): | 221 | func connectToServer(server, un, pw): | 
| 204 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) | 222 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) | 
| @@ -280,6 +298,14 @@ func getLogicalPath(object_type, object_id): | |||
| 280 | sendMessage([msg]) | 298 | sendMessage([msg]) | 
| 281 | 299 | ||
| 282 | 300 | ||
| 301 | func addIgnoredLocation(loc_id): | ||
| 302 | sendMessage([{"cmd": "IgnoreLocation", "id": loc_id}]) | ||
| 303 | |||
| 304 | |||
| 305 | func removeIgnoredLocation(loc_id): | ||
| 306 | sendMessage([{"cmd": "UnignoreLocation", "id": loc_id}]) | ||
| 307 | |||
| 308 | |||
| 283 | func sendQuit(): | 309 | func sendQuit(): | 
| 284 | sendMessage([{"cmd": "Quit"}]) | 310 | sendMessage([{"cmd": "Quit"}]) | 
| 285 | 311 | ||
| diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 3a35125..d7e3136 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd | |||
| @@ -15,6 +15,7 @@ var symbol_item_ids = [] | |||
| 15 | var anti_trap_ids = {} | 15 | var anti_trap_ids = {} | 
| 16 | var location_name_by_id = {} | 16 | var location_name_by_id = {} | 
| 17 | var ending_display_name_by_name = {} | 17 | var ending_display_name_by_name = {} | 
| 18 | var port_id_by_ap_id = {} | ||
| 18 | 19 | ||
| 19 | var kSYMBOL_ITEMS | 20 | var kSYMBOL_ITEMS | 
| 20 | 21 | ||
| @@ -99,6 +100,9 @@ func load(data_bytes): | |||
| 99 | var map_data = port_id_by_map_node_path[map.get_name()] | 100 | var map_data = port_id_by_map_node_path[map.get_name()] | 
| 100 | map_data[port.get_path()] = port.get_id() | 101 | map_data[port.get_path()] = port.get_id() | 
| 101 | 102 | ||
| 103 | if port.has_ap_id(): | ||
| 104 | port_id_by_ap_id[port.get_ap_id()] = port.get_id() | ||
| 105 | |||
| 102 | for progressive in objects.get_progressives(): | 106 | for progressive in objects.get_progressives(): | 
| 103 | progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() | 107 | progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() | 
| 104 | 108 | ||
| diff --git a/apworld/client/main.gd b/apworld/client/main.gd index a543678..c90d6e7 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd | |||
| @@ -51,6 +51,7 @@ func _ready(): | |||
| 51 | installScriptExtension(runtime.load_script("saver.gd")) | 51 | installScriptExtension(runtime.load_script("saver.gd")) | 
| 52 | installScriptExtension(runtime.load_script("teleport.gd")) | 52 | installScriptExtension(runtime.load_script("teleport.gd")) | 
| 53 | installScriptExtension(runtime.load_script("teleportListener.gd")) | 53 | installScriptExtension(runtime.load_script("teleportListener.gd")) | 
| 54 | installScriptExtension(runtime.load_script("unlockReaderListener.gd")) | ||
| 54 | installScriptExtension(runtime.load_script("visibilityListener.gd")) | 55 | installScriptExtension(runtime.load_script("visibilityListener.gd")) | 
| 55 | installScriptExtension(runtime.load_script("worldport.gd")) | 56 | installScriptExtension(runtime.load_script("worldport.gd")) | 
| 56 | installScriptExtension(runtime.load_script("worldportListener.gd")) | 57 | installScriptExtension(runtime.load_script("worldportListener.gd")) | 
| @@ -253,6 +254,7 @@ func startGame(): | |||
| 253 | clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn") | 254 | clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn") | 
| 254 | clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") | 255 | clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") | 
| 255 | clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") | 256 | clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") | 
| 257 | clearResourceCache("res://objects/nodes/listeners/unlockReaderListener.tscn") | ||
| 256 | clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn") | 258 | clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn") | 
| 257 | clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") | 259 | clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") | 
| 258 | clearResourceCache("res://objects/nodes/panel.tscn") | 260 | clearResourceCache("res://objects/nodes/panel.tscn") | 
| diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index aa07559..830ebb8 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd | |||
| @@ -29,6 +29,7 @@ var _inverse_item_locks = {} | |||
| 29 | var _held_letters = {} | 29 | var _held_letters = {} | 
| 30 | var _letters_setup = false | 30 | var _letters_setup = false | 
| 31 | var _already_connected = false | 31 | var _already_connected = false | 
| 32 | var _ignored_locations = [] | ||
| 32 | 33 | ||
| 33 | const kSHUFFLE_LETTERS_VANILLA = 0 | 34 | const kSHUFFLE_LETTERS_VANILLA = 0 | 
| 34 | const kSHUFFLE_LETTERS_UNLOCKED = 1 | 35 | const kSHUFFLE_LETTERS_UNLOCKED = 1 | 
| @@ -144,6 +145,8 @@ func _ready(): | |||
| 144 | client.hint_received.connect(_process_hint_received) | 145 | client.hint_received.connect(_process_hint_received) | 
| 145 | client.accessible_locations_updated.connect(_on_accessible_locations_updated) | 146 | client.accessible_locations_updated.connect(_on_accessible_locations_updated) | 
| 146 | client.checked_locations_updated.connect(_on_checked_locations_updated) | 147 | client.checked_locations_updated.connect(_on_checked_locations_updated) | 
| 148 | client.ignored_locations_updated.connect(_on_ignored_locations_updated) | ||
| 149 | client.hinted_locations_updated.connect(_on_hinted_locations_updated) | ||
| 147 | client.checked_worldports_updated.connect(_on_checked_worldports_updated) | 150 | client.checked_worldports_updated.connect(_on_checked_worldports_updated) | 
| 148 | client.door_latched.connect(_on_door_latched) | 151 | client.door_latched.connect(_on_door_latched) | 
| 149 | 152 | ||
| @@ -384,6 +387,20 @@ func _on_checked_worldports_updated(): | |||
| 384 | textclient_node.update_worldports() | 387 | textclient_node.update_worldports() | 
| 385 | 388 | ||
| 386 | 389 | ||
| 390 | func _on_ignored_locations_updated(locations): | ||
| 391 | _ignored_locations = locations | ||
| 392 | |||
| 393 | var textclient_node = global.get_node("Textclient") | ||
| 394 | if textclient_node != null: | ||
| 395 | textclient_node.update_locations() | ||
| 396 | |||
| 397 | |||
| 398 | func _on_hinted_locations_updated(): | ||
| 399 | var textclient_node = global.get_node("Textclient") | ||
| 400 | if textclient_node != null: | ||
| 401 | textclient_node.update_locations() | ||
| 402 | |||
| 403 | |||
| 387 | func _on_door_latched(door_id): | 404 | func _on_door_latched(door_id): | 
| 388 | var gamedata = global.get_node("Gamedata") | 405 | var gamedata = global.get_node("Gamedata") | 
| 389 | if gamedata.get_door_map_name(door_id) != global.map: | 406 | if gamedata.get_door_map_name(door_id) != global.map: | 
| @@ -472,7 +489,9 @@ func _client_connected(slot_data): | |||
| 472 | var raw_pp = slot_data.get("port_pairings") | 489 | var raw_pp = slot_data.get("port_pairings") | 
| 473 | 490 | ||
| 474 | for p1 in raw_pp.keys(): | 491 | for p1 in raw_pp.keys(): | 
| 475 | port_pairings[int(p1)] = int(raw_pp[p1]) | 492 | port_pairings[gamedata.port_id_by_ap_id[int(p1)]] = gamedata.port_id_by_ap_id[int( | 
| 493 | raw_pp[p1] | ||
| 494 | )] | ||
| 476 | 495 | ||
| 477 | # Set up item locks. | 496 | # Set up item locks. | 
| 478 | _item_locks = {} | 497 | _item_locks = {} | 
| @@ -675,3 +694,10 @@ func update_job_well_done_sign(): | |||
| 675 | 694 | ||
| 676 | sign2.get_node("MeshInstance3D").mesh.text = sign2.text | 695 | sign2.get_node("MeshInstance3D").mesh.text = sign2.text | 
| 677 | sign3.get_node("MeshInstance3D").mesh.text = sign3.text | 696 | sign3.get_node("MeshInstance3D").mesh.text = sign3.text | 
| 697 | |||
| 698 | |||
| 699 | func toggle_ignored_location(loc_id): | ||
| 700 | if loc_id in _ignored_locations: | ||
| 701 | client.removeIgnoredLocation(loc_id) | ||
| 702 | else: | ||
| 703 | client.addIgnoredLocation(loc_id) | ||
| diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 0e3fb23..65bf54e 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd | |||
| @@ -98,38 +98,72 @@ func _ready(): | |||
| 98 | old_door.queue_free() | 98 | old_door.queue_free() | 
| 99 | get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door) | 99 | get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door) | 
| 100 | 100 | ||
| 101 | # Block off roof access in Daedalus. | 101 | if global.map == "daedalus": | 
| 102 | if global.map == "daedalus" and not ap.daedalus_roof_access: | 102 | # Teleport the direction panels when the stairs are there. | 
| 103 | _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49) | 103 | var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn") | 
| 104 | _set_up_invis_wall(51.5, 11, -17, 16, 10, 1) | 104 | |
| 105 | _set_up_invis_wall(46, 10, -9.5, 1, 10, 10) | 105 | var dir1 = get_node("/root/scene/Panels/Castle Entrance/castle_direction_1") | 
| 106 | _set_up_invis_wall(67.5, 11, 17, 16, 10, 1) | 106 | var dir1_tpl = tpl_prefab.instantiate() | 
| 107 | _set_up_invis_wall(50.5, 11, 14, 10, 10, 1) | 107 | dir1_tpl.target_path = dir1 | 
| 108 | _set_up_invis_wall(39, 10, 18.5, 1, 10, 22) | 108 | dir1_tpl.teleport_point = Vector3(59.5, 8, -6.5) | 
| 109 | _set_up_invis_wall(20, 15, 18.5, 1, 10, 16) | 109 | dir1_tpl.teleport_rotate = Vector3(-45, 0, 0) | 
| 110 | _set_up_invis_wall(11.5, 15, 3, 32, 10, 1) | 110 | dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south")) | 
| 111 | _set_up_invis_wall(11.5, 16, -20, 14, 20, 1) | 111 | dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north")) | 
| 112 | _set_up_invis_wall(14, 16, -26.5, 1, 20, 4) | 112 | dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west")) | 
| 113 | _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25) | 113 | dir1.add_child.call_deferred(dir1_tpl) | 
| 114 | _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1) | 114 | |
| 115 | _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1) | 115 | var dir2 = get_node("/root/scene/Panels/Castle Entrance/castle_direction_2") | 
| 116 | _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11) | 116 | var dir2_tpl = tpl_prefab.instantiate() | 
| 117 | _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11) | 117 | dir2_tpl.target_path = dir2 | 
| 118 | 118 | dir2_tpl.teleport_point = Vector3(59.5, 8, 6.5) | |
| 119 | var warp_exit_prefab = preload("res://objects/nodes/exit.tscn") | 119 | dir2_tpl.teleport_rotate = Vector3(-45, -180, 0) | 
| 120 | var warp_exit = warp_exit_prefab.instantiate() | 120 | dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south")) | 
| 121 | warp_exit.name = "roof_access_blocker_warp_exit" | 121 | dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north")) | 
| 122 | warp_exit.position = Vector3(58, 10, 0) | 122 | dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west")) | 
| 123 | warp_exit.rotation_degrees.y = 90 | 123 | dir2.add_child.call_deferred(dir2_tpl) | 
| 124 | get_parent().add_child.call_deferred(warp_exit) | 124 | |
| 125 | 125 | var dir3 = get_node("/root/scene/Panels/Castle Entrance/castle_direction_3") | |
| 126 | var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn") | 126 | var dir3_tpl = tpl_prefab.instantiate() | 
| 127 | var warp_enter = warp_enter_prefab.instantiate() | 127 | dir3_tpl.target_path = dir3 | 
| 128 | warp_enter.target = warp_exit | 128 | dir3_tpl.teleport_point = Vector3(54, 8, 0) | 
| 129 | warp_enter.position = Vector3(76.5, 30, 1) | 129 | dir3_tpl.teleport_rotate = Vector3(-45, 90, 0) | 
| 130 | warp_enter.scale = Vector3(4, 1.5, 1) | 130 | dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south")) | 
| 131 | warp_enter.rotation_degrees.y = 90 | 131 | dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north")) | 
| 132 | get_parent().add_child.call_deferred(warp_enter) | 132 | dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west")) | 
| 133 | dir3.add_child.call_deferred(dir3_tpl) | ||
| 134 | |||
| 135 | # Block off roof access in Daedalus. | ||
| 136 | if not ap.daedalus_roof_access: | ||
| 137 | _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49) | ||
| 138 | _set_up_invis_wall(51.5, 11, -17, 16, 10, 1) | ||
| 139 | _set_up_invis_wall(46, 10, -9.5, 1, 10, 10) | ||
| 140 | _set_up_invis_wall(67.5, 11, 17, 16, 10, 1) | ||
| 141 | _set_up_invis_wall(50.5, 11, 14, 10, 10, 1) | ||
| 142 | _set_up_invis_wall(39, 10, 18.5, 1, 10, 22) | ||
| 143 | _set_up_invis_wall(20, 15, 18.5, 1, 10, 16) | ||
| 144 | _set_up_invis_wall(11.5, 15, 3, 32, 10, 1) | ||
| 145 | _set_up_invis_wall(11.5, 16, -20, 14, 20, 1) | ||
| 146 | _set_up_invis_wall(14, 16, -26.5, 1, 20, 4) | ||
| 147 | _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25) | ||
| 148 | _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1) | ||
| 149 | _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1) | ||
| 150 | _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11) | ||
| 151 | _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11) | ||
| 152 | |||
| 153 | var warp_exit_prefab = preload("res://objects/nodes/exit.tscn") | ||
| 154 | var warp_exit = warp_exit_prefab.instantiate() | ||
| 155 | warp_exit.name = "roof_access_blocker_warp_exit" | ||
| 156 | warp_exit.position = Vector3(58, 10, 0) | ||
| 157 | warp_exit.rotation_degrees.y = 90 | ||
| 158 | get_parent().add_child.call_deferred(warp_exit) | ||
| 159 | |||
| 160 | var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn") | ||
| 161 | var warp_enter = warp_enter_prefab.instantiate() | ||
| 162 | warp_enter.target = warp_exit | ||
| 163 | warp_enter.position = Vector3(76.5, 30, 1) | ||
| 164 | warp_enter.scale = Vector3(4, 1.5, 1) | ||
| 165 | warp_enter.rotation_degrees.y = 90 | ||
| 166 | get_parent().add_child.call_deferred(warp_enter) | ||
| 133 | 167 | ||
| 134 | if global.map == "the_entry": | 168 | if global.map == "the_entry": | 
| 135 | # Remove door behind X1. | 169 | # Remove door behind X1. | 
| @@ -604,9 +638,12 @@ func _ready(): | |||
| 604 | continue | 638 | continue | 
| 605 | 639 | ||
| 606 | if ( | 640 | if ( | 
| 607 | door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY | 641 | not (door.has_legacy_location() and door.get_legacy_location()) | 
| 608 | or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING | 642 | and ( | 
| 609 | or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR | 643 | door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY | 
| 644 | or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING | ||
| 645 | or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR | ||
| 646 | ) | ||
| 610 | ): | 647 | ): | 
| 611 | continue | 648 | continue | 
| 612 | 649 | ||
| diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index f785a03..ce28a3a 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd | |||
| @@ -16,6 +16,8 @@ var tracker_loc_tree_item_by_id = {} | |||
| 16 | var tracker_port_tree_item_by_id = {} | 16 | var tracker_port_tree_item_by_id = {} | 
| 17 | var tracker_goal_tree_item = null | 17 | var tracker_goal_tree_item = null | 
| 18 | var tracker_object_by_index = {} | 18 | var tracker_object_by_index = {} | 
| 19 | var tracker_object_by_ignored_index = {} | ||
| 20 | var tracker_ignored_group = null | ||
| 19 | 21 | ||
| 20 | var worldports_tab | 22 | var worldports_tab | 
| 21 | var worldports_tree | 23 | var worldports_tree | 
| @@ -99,7 +101,7 @@ func _ready(): | |||
| 99 | tabs.add_child(tracker_margins) | 101 | tabs.add_child(tracker_margins) | 
| 100 | 102 | ||
| 101 | tracker_tree = Tree.new() | 103 | tracker_tree = Tree.new() | 
| 102 | tracker_tree.columns = 3 | 104 | tracker_tree.columns = 4 | 
| 103 | tracker_tree.hide_root = true | 105 | tracker_tree.hide_root = true | 
| 104 | tracker_tree.add_theme_font_size_override("font_size", 24) | 106 | tracker_tree.add_theme_font_size_override("font_size", 24) | 
| 105 | tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1)) | 107 | tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1)) | 
| @@ -108,7 +110,9 @@ func _ready(): | |||
| 108 | tracker_tree.set_column_expand(0, false) | 110 | tracker_tree.set_column_expand(0, false) | 
| 109 | tracker_tree.set_column_expand(1, true) | 111 | tracker_tree.set_column_expand(1, true) | 
| 110 | tracker_tree.set_column_expand(2, false) | 112 | tracker_tree.set_column_expand(2, false) | 
| 113 | tracker_tree.set_column_expand(3, false) | ||
| 111 | tracker_tree.set_column_custom_minimum_width(2, 200) | 114 | tracker_tree.set_column_custom_minimum_width(2, 200) | 
| 115 | tracker_tree.set_column_custom_minimum_width(3, 200) | ||
| 112 | tracker_margins.add_child(tracker_tree) | 116 | tracker_margins.add_child(tracker_tree) | 
| 113 | 117 | ||
| 114 | worldports_tab = MarginContainer.new() | 118 | worldports_tab = MarginContainer.new() | 
| @@ -208,6 +212,8 @@ func update_locations(reset_locations = true): | |||
| 208 | "name": location_name, | 212 | "name": location_name, | 
| 209 | "type": kLocation, | 213 | "type": kLocation, | 
| 210 | "id": location_id, | 214 | "id": location_id, | 
| 215 | "ignored": ap._ignored_locations.has(location_id), | ||
| 216 | "hint": ap.client._hinted_locations.has(location_id), | ||
| 211 | } | 217 | } | 
| 212 | ) | 218 | ) | 
| 213 | ) | 219 | ) | 
| @@ -222,11 +228,13 @@ func update_locations(reset_locations = true): | |||
| 222 | "name": port_name, | 228 | "name": port_name, | 
| 223 | "type": kWorldport, | 229 | "type": kWorldport, | 
| 224 | "id": port_id, | 230 | "id": port_id, | 
| 231 | "ignored": false, | ||
| 232 | "hint": false, | ||
| 225 | } | 233 | } | 
| 226 | ) | 234 | ) | 
| 227 | ) | 235 | ) | 
| 228 | 236 | ||
| 229 | locations.sort_custom(func(a, b): return a["name"] < b["name"]) | 237 | locations.sort_custom(_cmp_tracker_objects) | 
| 230 | 238 | ||
| 231 | if ap.client._goal_accessible: | 239 | if ap.client._goal_accessible: | 
| 232 | var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[ | 240 | var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[ | 
| @@ -238,14 +246,18 @@ func update_locations(reset_locations = true): | |||
| 238 | { | 246 | { | 
| 239 | "name": location_name, | 247 | "name": location_name, | 
| 240 | "type": kGoal, | 248 | "type": kGoal, | 
| 249 | "ignored": false, | ||
| 250 | "hint": false, | ||
| 241 | } | 251 | } | 
| 242 | ) | 252 | ) | 
| 243 | ) | 253 | ) | 
| 244 | 254 | ||
| 245 | var count = 0 | 255 | var count = 0 | 
| 246 | for location in locations: | 256 | for location in locations: | 
| 247 | if count < 18: | 257 | if count < 18 and not location["ignored"]: | 
| 248 | locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT) | 258 | locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT) | 
| 259 | if location["hint"]: | ||
| 260 | locations_overlay.push_color(Color("#fafad2")) | ||
| 249 | locations_overlay.append_text(location["name"]) | 261 | locations_overlay.append_text(location["name"]) | 
| 250 | locations_overlay.append_text(" ") | 262 | locations_overlay.append_text(" ") | 
| 251 | if location["type"] == kLocation: | 263 | if location["type"] == kLocation: | 
| @@ -254,6 +266,8 @@ func update_locations(reset_locations = true): | |||
| 254 | locations_overlay.add_image(worldport_texture) | 266 | locations_overlay.add_image(worldport_texture) | 
| 255 | elif location["type"] == kGoal: | 267 | elif location["type"] == kGoal: | 
| 256 | locations_overlay.add_image(goal_texture) | 268 | locations_overlay.add_image(goal_texture) | 
| 269 | if location["hint"]: | ||
| 270 | locations_overlay.pop() | ||
| 257 | locations_overlay.pop() | 271 | locations_overlay.pop() | 
| 258 | count += 1 | 272 | count += 1 | 
| 259 | 273 | ||
| @@ -266,17 +280,43 @@ func update_locations(reset_locations = true): | |||
| 266 | var root_ti = tracker_tree.create_item(null) | 280 | var root_ti = tracker_tree.create_item(null) | 
| 267 | 281 | ||
| 268 | for location in locations: | 282 | for location in locations: | 
| 269 | var loc_row = root_ti.create_child() | 283 | var loc_row | 
| 284 | |||
| 285 | if location["ignored"]: | ||
| 286 | if tracker_ignored_group == null: | ||
| 287 | tracker_ignored_group = root_ti.create_child() | ||
| 288 | tracker_ignored_group.set_text(1, "Ignored Locations") | ||
| 289 | tracker_ignored_group.set_selectable(0, false) | ||
| 290 | tracker_ignored_group.set_selectable(1, false) | ||
| 291 | tracker_ignored_group.set_selectable(2, false) | ||
| 292 | tracker_ignored_group.set_selectable(3, false) | ||
| 293 | |||
| 294 | loc_row = tracker_ignored_group.create_child() | ||
| 295 | else: | ||
| 296 | loc_row = root_ti.create_child() | ||
| 297 | |||
| 270 | loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON) | 298 | loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON) | 
| 271 | loc_row.set_selectable(0, false) | 299 | loc_row.set_selectable(0, false) | 
| 272 | loc_row.set_text(1, location["name"]) | 300 | loc_row.set_text(1, location["name"]) | 
| 273 | loc_row.set_selectable(1, false) | 301 | loc_row.set_selectable(1, false) | 
| 302 | if location["hint"]: | ||
| 303 | loc_row.set_custom_color(1, Color("#fafad2")) | ||
| 274 | loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM) | 304 | loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM) | 
| 275 | loc_row.set_text(2, "Show Path") | 305 | loc_row.set_text(2, "Show Path") | 
| 276 | loc_row.set_custom_as_button(2, true) | 306 | loc_row.set_custom_as_button(2, true) | 
| 277 | loc_row.set_editable(2, true) | 307 | loc_row.set_editable(2, true) | 
| 278 | loc_row.set_selectable(2, false) | 308 | loc_row.set_selectable(2, false) | 
| 279 | loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER) | 309 | loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER) | 
| 310 | loc_row.set_selectable(3, false) | ||
| 311 | if location["type"] == kLocation: | ||
| 312 | loc_row.set_cell_mode(3, TreeItem.CELL_MODE_CUSTOM) | ||
| 313 | if location["ignored"]: | ||
| 314 | loc_row.set_text(3, "Unignore") | ||
| 315 | else: | ||
| 316 | loc_row.set_text(3, "Ignore") | ||
| 317 | loc_row.set_custom_as_button(3, true) | ||
| 318 | loc_row.set_editable(3, true) | ||
| 319 | loc_row.set_text_alignment(3, HORIZONTAL_ALIGNMENT_CENTER) | ||
| 280 | 320 | ||
| 281 | if location["type"] == kLocation: | 321 | if location["type"] == kLocation: | 
| 282 | loc_row.set_icon(0, location_texture) | 322 | loc_row.set_icon(0, location_texture) | 
| @@ -288,7 +328,10 @@ func update_locations(reset_locations = true): | |||
| 288 | loc_row.set_icon(0, goal_texture) | 328 | loc_row.set_icon(0, goal_texture) | 
| 289 | tracker_goal_tree_item = loc_row | 329 | tracker_goal_tree_item = loc_row | 
| 290 | 330 | ||
| 291 | tracker_object_by_index[loc_row.get_index()] = location | 331 | if location["ignored"]: | 
| 332 | tracker_object_by_ignored_index[loc_row.get_index()] = location | ||
| 333 | else: | ||
| 334 | tracker_object_by_index[loc_row.get_index()] = location | ||
| 292 | else: | 335 | else: | 
| 293 | for loc_row in tracker_tree.get_root().get_children(): | 336 | for loc_row in tracker_tree.get_root().get_children(): | 
| 294 | loc_row.visible = false | 337 | loc_row.visible = false | 
| @@ -310,6 +353,18 @@ func update_locations(reset_locations = true): | |||
| 310 | if tracker_goal_tree_item != null and ap.client._goal_accessible: | 353 | if tracker_goal_tree_item != null and ap.client._goal_accessible: | 
| 311 | tracker_goal_tree_item.visible = true | 354 | tracker_goal_tree_item.visible = true | 
| 312 | 355 | ||
| 356 | if tracker_ignored_group != null: | ||
| 357 | tracker_ignored_group.visible = true | ||
| 358 | |||
| 359 | |||
| 360 | func _cmp_tracker_objects(a, b) -> bool: | ||
| 361 | if a["ignored"] != b["ignored"]: | ||
| 362 | return !a["ignored"] | ||
| 363 | elif a["hint"] != b["hint"]: | ||
| 364 | return a["hint"] | ||
| 365 | else: | ||
| 366 | return a["name"] < b["name"] | ||
| 367 | |||
| 313 | 368 | ||
| 314 | func update_locations_visibility(): | 369 | func update_locations_visibility(): | 
| 315 | var ap = global.get_node("Archipelago") | 370 | var ap = global.get_node("Archipelago") | 
| @@ -317,20 +372,33 @@ func update_locations_visibility(): | |||
| 317 | 372 | ||
| 318 | 373 | ||
| 319 | func _on_tracker_button_clicked(): | 374 | func _on_tracker_button_clicked(): | 
| 375 | var ap = global.get_node("Archipelago") | ||
| 376 | |||
| 320 | var edited_item = tracker_tree.get_edited() | 377 | var edited_item = tracker_tree.get_edited() | 
| 321 | var edited_index = edited_item.get_index() | 378 | var edited_index = edited_item.get_index() | 
| 322 | 379 | ||
| 323 | if tracker_object_by_index.has(edited_index): | 380 | if edited_item.get_parent() == tracker_tree.get_root(): | 
| 324 | var tracker_object = tracker_object_by_index[edited_index] | 381 | if tracker_object_by_index.has(edited_index): | 
| 325 | var ap = global.get_node("Archipelago") | 382 | var tracker_object = tracker_object_by_index[edited_index] | 
| 326 | var type_str = "" | 383 | if tracker_tree.get_edited_column() == 2: | 
| 327 | if tracker_object["type"] == kLocation: | 384 | var type_str = "" | 
| 328 | type_str = "location" | 385 | if tracker_object["type"] == kLocation: | 
| 329 | elif tracker_object["type"] == kWorldport: | 386 | type_str = "location" | 
| 330 | type_str = "worldport" | 387 | elif tracker_object["type"] == kWorldport: | 
| 331 | elif tracker_object["type"] == kGoal: | 388 | type_str = "worldport" | 
| 332 | type_str = "goal" | 389 | elif tracker_object["type"] == kGoal: | 
| 333 | ap.client.getLogicalPath(type_str, tracker_object.get("id", null)) | 390 | type_str = "goal" | 
| 391 | ap.client.getLogicalPath(type_str, tracker_object.get("id", null)) | ||
| 392 | elif tracker_tree.get_edited_column() == 3: | ||
| 393 | ap.toggle_ignored_location(tracker_object["id"]) | ||
| 394 | elif edited_item.get_parent() == tracker_ignored_group: | ||
| 395 | # This is the ignored locations group. | ||
| 396 | if ( | ||
| 397 | tracker_object_by_ignored_index.has(edited_index) | ||
| 398 | and tracker_tree.get_edited_column() == 3 | ||
| 399 | ): | ||
| 400 | var tracker_object = tracker_object_by_ignored_index[edited_index] | ||
| 401 | ap.toggle_ignored_location(tracker_object["id"]) | ||
| 334 | 402 | ||
| 335 | 403 | ||
| 336 | func display_logical_path(object_type, object_id, paths): | 404 | func display_logical_path(object_type, object_id, paths): | 
| @@ -435,4 +503,6 @@ func reset_tracker_tab(): | |||
| 435 | tracker_port_tree_item_by_id.clear() | 503 | tracker_port_tree_item_by_id.clear() | 
| 436 | tracker_goal_tree_item = null | 504 | tracker_goal_tree_item = null | 
| 437 | tracker_object_by_index.clear() | 505 | tracker_object_by_index.clear() | 
| 506 | tracker_object_by_ignored_index.clear() | ||
| 507 | tracker_ignored_group = null | ||
| 438 | tracker_tree.clear() | 508 | tracker_tree.clear() | 
| diff --git a/apworld/client/unlockReaderListener.gd b/apworld/client/unlockReaderListener.gd new file mode 100644 index 0000000..a5754b9 --- /dev/null +++ b/apworld/client/unlockReaderListener.gd | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | extends "res://scripts/nodes/listeners/unlockReaderListener.gd" | ||
| 2 | |||
| 3 | var item_id = null | ||
| 4 | var item_amount | ||
| 5 | |||
| 6 | |||
| 7 | func _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 | super._ready() | ||
| 30 | |||
| 31 | |||
| 32 | func _readier(): | ||
| 33 | if item_id != null: | ||
| 34 | var ap = global.get_node("Archipelago") | ||
| 35 | |||
| 36 | if ap.client.getItemAmount(item_id) >= item_amount: | ||
| 37 | handleTriggered() | ||
| 38 | else: | ||
| 39 | super._readier() | ||
| 40 | |||
| 41 | |||
| 42 | func handleTriggered(): | ||
| 43 | if item_id != null: | ||
| 44 | emit_signal("trigger") | ||
| 45 | else: | ||
| 46 | super.handleTriggered() | ||
| diff --git a/apworld/context.py b/apworld/context.py index e2d80cd..86392f9 100644 --- a/apworld/context.py +++ b/apworld/context.py | |||
| @@ -30,6 +30,16 @@ KEY_STORAGE_MAPPING = { | |||
| 30 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} | 30 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} | 
| 31 | 31 | ||
| 32 | 32 | ||
| 33 | # There is a distinction between an object's ID and its AP ID. The latter is stable between releases, whereas the former | ||
| 34 | # can change and is also namespaced based on the object type. We should only store AP IDs in multiworld state (such as | ||
| 35 | # slot data and data storage) to increase compatability between releases. The data we currently store is: | ||
| 36 | # - Port pairings for worldport shuffle (slot data) | ||
| 37 | # - Checked worldports for worldport shuffle (data storage) | ||
| 38 | # - Latched doors (data storage) | ||
| 39 | # The client generally deals in the actual object IDs rather than the stable IDs, although it does have to convert the | ||
| 40 | # port pairing IDs when reading them from slot data. The context (this file here) does the work of converting back and | ||
| 41 | # forth between the values. AP IDs are converted to IDs after reading them from data storage, and IDs are converted to | ||
| 42 | # AP IDs before sending them to data storage. | ||
| 33 | class Lingo2Manager: | 43 | class Lingo2Manager: | 
| 34 | game_ctx: "Lingo2GameContext" | 44 | game_ctx: "Lingo2GameContext" | 
| 35 | client_ctx: "Lingo2ClientContext" | 45 | client_ctx: "Lingo2ClientContext" | 
| @@ -39,6 +49,7 @@ class Lingo2Manager: | |||
| 39 | worldports: set[int] | 49 | worldports: set[int] | 
| 40 | goaled: bool | 50 | goaled: bool | 
| 41 | latches: set[int] | 51 | latches: set[int] | 
| 52 | hinted_locations: set[int] | ||
| 42 | 53 | ||
| 43 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | 54 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | 
| 44 | self.game_ctx = game_ctx | 55 | self.game_ctx = game_ctx | 
| @@ -47,8 +58,6 @@ class Lingo2Manager: | |||
| 47 | self.client_ctx.manager = self | 58 | self.client_ctx.manager = self | 
| 48 | self.tracker = Tracker(self) | 59 | self.tracker = Tracker(self) | 
| 49 | self.keyboard = {} | 60 | self.keyboard = {} | 
| 50 | self.worldports = set() | ||
| 51 | self.latches = set() | ||
| 52 | 61 | ||
| 53 | self.reset() | 62 | self.reset() | 
| 54 | 63 | ||
| @@ -59,6 +68,7 @@ class Lingo2Manager: | |||
| 59 | self.worldports = set() | 68 | self.worldports = set() | 
| 60 | self.goaled = False | 69 | self.goaled = False | 
| 61 | self.latches = set() | 70 | self.latches = set() | 
| 71 | self.hinted_locations = set() | ||
| 62 | 72 | ||
| 63 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | 73 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | 
| 64 | ret: dict[str, int] = {} | 74 | ret: dict[str, int] = {} | 
| @@ -74,6 +84,7 @@ class Lingo2Manager: | |||
| 74 | 84 | ||
| 75 | return ret | 85 | return ret | 
| 76 | 86 | ||
| 87 | # Input should be real IDs, not AP IDs | ||
| 77 | def update_worldports(self, new_worldports: set[int]) -> set[int]: | 88 | def update_worldports(self, new_worldports: set[int]) -> set[int]: | 
| 78 | ret = new_worldports.difference(self.worldports) | 89 | ret = new_worldports.difference(self.worldports) | 
| 79 | self.worldports.update(new_worldports) | 90 | self.worldports.update(new_worldports) | 
| @@ -90,6 +101,12 @@ class Lingo2Manager: | |||
| 90 | 101 | ||
| 91 | return ret | 102 | return ret | 
| 92 | 103 | ||
| 104 | def update_hinted_locations(self, new_locs: set[int]) -> set[int]: | ||
| 105 | ret = new_locs.difference(self.hinted_locations) | ||
| 106 | self.hinted_locations.update(new_locs) | ||
| 107 | |||
| 108 | return ret | ||
| 109 | |||
| 93 | 110 | ||
| 94 | class Lingo2GameContext: | 111 | class Lingo2GameContext: | 
| 95 | server: Endpoint | None | 112 | server: Endpoint | None | 
| @@ -227,6 +244,7 @@ class Lingo2GameContext: | |||
| 227 | 244 | ||
| 228 | async_start(self.send_msgs([msg]), name="update keyboard") | 245 | async_start(self.send_msgs([msg]), name="update keyboard") | 
| 229 | 246 | ||
| 247 | # Input should be real IDs, not AP IDs | ||
| 230 | def send_update_worldports(self, worldports): | 248 | def send_update_worldports(self, worldports): | 
| 231 | if self.server is None: | 249 | if self.server is None: | 
| 232 | return | 250 | return | 
| @@ -264,6 +282,28 @@ class Lingo2GameContext: | |||
| 264 | 282 | ||
| 265 | async_start(self.send_msgs([msg]), name="update latches") | 283 | async_start(self.send_msgs([msg]), name="update latches") | 
| 266 | 284 | ||
| 285 | def send_ignored_locations(self, ignored_locations): | ||
| 286 | if self.server is None: | ||
| 287 | return | ||
| 288 | |||
| 289 | msg = { | ||
| 290 | "cmd": "SetIgnoredLocations", | ||
| 291 | "locations": ignored_locations, | ||
| 292 | } | ||
| 293 | |||
| 294 | async_start(self.send_msgs([msg]), name="set ignored locations") | ||
| 295 | |||
| 296 | def send_update_hinted_locations(self, hinted_locations): | ||
| 297 | if self.server is None: | ||
| 298 | return | ||
| 299 | |||
| 300 | msg = { | ||
| 301 | "cmd": "UpdateHintedLocations", | ||
| 302 | "locations": hinted_locations, | ||
| 303 | } | ||
| 304 | |||
| 305 | async_start(self.send_msgs([msg]), name="update hinted locations") | ||
| 306 | |||
| 267 | async def send_msgs(self, msgs: list[Any]) -> None: | 307 | async def send_msgs(self, msgs: list[Any]) -> None: | 
| 268 | """ `msgs` JSON serializable """ | 308 | """ `msgs` JSON serializable """ | 
| 269 | if not self.server or not self.server.socket.open or self.server.socket.closed: | 309 | if not self.server or not self.server.socket.open or self.server.socket.closed: | 
| @@ -278,6 +318,7 @@ class Lingo2ClientContext(CommonContext): | |||
| 278 | items_handling = 0b111 | 318 | items_handling = 0b111 | 
| 279 | 319 | ||
| 280 | slot_data: dict[str, Any] | None | 320 | slot_data: dict[str, Any] | None | 
| 321 | hints_data_storage_key: str | ||
| 281 | victory_data_storage_key: str | 322 | victory_data_storage_key: str | 
| 282 | 323 | ||
| 283 | def __init__(self, server_address: str | None = None, password: str | None = None): | 324 | def __init__(self, server_address: str | None = None, password: str | None = None): | 
| @@ -315,10 +356,12 @@ class Lingo2ClientContext(CommonContext): | |||
| 315 | self.manager.tracker.set_checked_locations(self.checked_locations) | 356 | self.manager.tracker.set_checked_locations(self.checked_locations) | 
| 316 | self.manager.game_ctx.send_accessible_locations() | 357 | self.manager.game_ctx.send_accessible_locations() | 
| 317 | 358 | ||
| 359 | self.hints_data_storage_key = f"_read_hints_{self.team}_{self.slot}" | ||
| 318 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" | 360 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" | 
| 319 | 361 | ||
| 320 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), | 362 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), | 
| 321 | self.victory_data_storage_key, self.get_datastorage_key("latches")) | 363 | self.victory_data_storage_key, self.get_datastorage_key("latches"), | 
| 364 | self.get_datastorage_key("ignored_locations")) | ||
| 322 | msg_batch = [{ | 365 | msg_batch = [{ | 
| 323 | "cmd": "Set", | 366 | "cmd": "Set", | 
| 324 | "key": self.get_datastorage_key("keyboard1"), | 367 | "key": self.get_datastorage_key("keyboard1"), | 
| @@ -337,6 +380,12 @@ class Lingo2ClientContext(CommonContext): | |||
| 337 | "default": [], | 380 | "default": [], | 
| 338 | "want_reply": True, | 381 | "want_reply": True, | 
| 339 | "operations": [{"operation": "default", "value": []}] | 382 | "operations": [{"operation": "default", "value": []}] | 
| 383 | }, { | ||
| 384 | "cmd": "Set", | ||
| 385 | "key": self.get_datastorage_key("ignored_locations"), | ||
| 386 | "default": [], | ||
| 387 | "want_reply": True, | ||
| 388 | "operations": [{"operation": "default", "value": []}] | ||
| 340 | }] | 389 | }] | 
| 341 | 390 | ||
| 342 | if self.slot_data.get("shuffle_worldports", False): | 391 | if self.slot_data.get("shuffle_worldports", False): | 
| @@ -435,21 +484,29 @@ class Lingo2ClientContext(CommonContext): | |||
| 435 | for k, v in args["keys"].items(): | 484 | for k, v in args["keys"].items(): | 
| 436 | if k == self.victory_data_storage_key: | 485 | if k == self.victory_data_storage_key: | 
| 437 | self.handle_status_update(v) | 486 | self.handle_status_update(v) | 
| 487 | elif k == self.hints_data_storage_key: | ||
| 488 | self.update_hints() | ||
| 438 | elif cmd == "SetReply": | 489 | elif cmd == "SetReply": | 
| 439 | if args["key"] == self.get_datastorage_key("keyboard1"): | 490 | if args["key"] == self.get_datastorage_key("keyboard1"): | 
| 440 | self.handle_keyboard_update(1, args) | 491 | self.handle_keyboard_update(1, args) | 
| 441 | elif args["key"] == self.get_datastorage_key("keyboard2"): | 492 | elif args["key"] == self.get_datastorage_key("keyboard2"): | 
| 442 | self.handle_keyboard_update(2, args) | 493 | self.handle_keyboard_update(2, args) | 
| 443 | elif args["key"] == self.get_datastorage_key("worldports"): | 494 | elif args["key"] == self.get_datastorage_key("worldports"): | 
| 444 | updates = self.manager.update_worldports(set(args["value"])) | 495 | port_ids = set(Lingo2World.static_logic.port_id_by_ap_id[ap_id] for ap_id in args["value"]) | 
| 496 | updates = self.manager.update_worldports(port_ids) | ||
| 445 | if len(updates) > 0: | 497 | if len(updates) > 0: | 
| 446 | self.manager.game_ctx.send_update_worldports(updates) | 498 | self.manager.game_ctx.send_update_worldports(updates) | 
| 447 | elif args["key"] == self.victory_data_storage_key: | 499 | elif args["key"] == self.victory_data_storage_key: | 
| 448 | self.handle_status_update(args["value"]) | 500 | self.handle_status_update(args["value"]) | 
| 449 | elif args["key"] == self.get_datastorage_key("latches"): | 501 | elif args["key"] == self.get_datastorage_key("latches"): | 
| 450 | updates = self.manager.update_latches(set(args["value"])) | 502 | door_ids = set(Lingo2World.static_logic.door_id_by_ap_id[ap_id] for ap_id in args["value"]) | 
| 503 | updates = self.manager.update_latches(door_ids) | ||
| 451 | if len(updates) > 0: | 504 | if len(updates) > 0: | 
| 452 | self.manager.game_ctx.send_update_latches(updates) | 505 | self.manager.game_ctx.send_update_latches(updates) | 
| 506 | elif args["key"] == self.get_datastorage_key("ignored_locations"): | ||
| 507 | self.manager.game_ctx.send_ignored_locations(args["value"]) | ||
| 508 | elif args["key"] == self.hints_data_storage_key: | ||
| 509 | self.update_hints() | ||
| 453 | 510 | ||
| 454 | def get_datastorage_key(self, name: str): | 511 | def get_datastorage_key(self, name: str): | 
| 455 | return f"Lingo2_{self.slot}_{name}" | 512 | return f"Lingo2_{self.slot}_{name}" | 
| @@ -515,14 +572,16 @@ class Lingo2ClientContext(CommonContext): | |||
| 515 | if len(updates) > 0: | 572 | if len(updates) > 0: | 
| 516 | self.manager.game_ctx.send_update_keyboard(updates) | 573 | self.manager.game_ctx.send_update_keyboard(updates) | 
| 517 | 574 | ||
| 575 | # Input should be real IDs, not AP IDs | ||
| 518 | async def update_worldports(self, updates: set[int]): | 576 | async def update_worldports(self, updates: set[int]): | 
| 577 | port_ap_ids = [Lingo2World.static_logic.objects.ports[port_id].ap_id for port_id in updates] | ||
| 519 | await self.send_msgs([{ | 578 | await self.send_msgs([{ | 
| 520 | "cmd": "Set", | 579 | "cmd": "Set", | 
| 521 | "key": self.get_datastorage_key("worldports"), | 580 | "key": self.get_datastorage_key("worldports"), | 
| 522 | "want_reply": True, | 581 | "want_reply": True, | 
| 523 | "operations": [{ | 582 | "operations": [{ | 
| 524 | "operation": "update", | 583 | "operation": "update", | 
| 525 | "value": updates | 584 | "value": port_ap_ids | 
| 526 | }] | 585 | }] | 
| 527 | }]) | 586 | }]) | 
| 528 | 587 | ||
| @@ -532,16 +591,47 @@ class Lingo2ClientContext(CommonContext): | |||
| 532 | self.manager.game_ctx.send_accessible_locations() | 591 | self.manager.game_ctx.send_accessible_locations() | 
| 533 | 592 | ||
| 534 | async def update_latches(self, updates: set[int]): | 593 | async def update_latches(self, updates: set[int]): | 
| 594 | door_ap_ids = [Lingo2World.static_logic.objects.doors[door_id].ap_id for door_id in updates] | ||
| 535 | await self.send_msgs([{ | 595 | await self.send_msgs([{ | 
| 536 | "cmd": "Set", | 596 | "cmd": "Set", | 
| 537 | "key": self.get_datastorage_key("latches"), | 597 | "key": self.get_datastorage_key("latches"), | 
| 538 | "want_reply": True, | 598 | "want_reply": True, | 
| 539 | "operations": [{ | 599 | "operations": [{ | 
| 540 | "operation": "update", | 600 | "operation": "update", | 
| 541 | "value": updates | 601 | "value": door_ap_ids | 
| 602 | }] | ||
| 603 | }]) | ||
| 604 | |||
| 605 | async def add_ignored_location(self, loc_id: int): | ||
| 606 | await self.send_msgs([{ | ||
| 607 | "cmd": "Set", | ||
| 608 | "key": self.get_datastorage_key("ignored_locations"), | ||
| 609 | "want_reply": True, | ||
| 610 | "operations": [{ | ||
| 611 | "operation": "update", | ||
| 612 | "value": [loc_id] | ||
| 542 | }] | 613 | }] | 
| 543 | }]) | 614 | }]) | 
| 544 | 615 | ||
| 616 | async def remove_ignored_location(self, loc_id: int): | ||
| 617 | await self.send_msgs([{ | ||
| 618 | "cmd": "Set", | ||
| 619 | "key": self.get_datastorage_key("ignored_locations"), | ||
| 620 | "want_reply": True, | ||
| 621 | "operations": [{ | ||
| 622 | "operation": "remove", | ||
| 623 | "value": loc_id | ||
| 624 | }] | ||
| 625 | }]) | ||
| 626 | |||
| 627 | def update_hints(self): | ||
| 628 | hints = self.stored_data.get(self.hints_data_storage_key, []) | ||
| 629 | |||
| 630 | hinted_locations = set(hint["location"] for hint in hints if hint["finding_player"] == self.slot) | ||
| 631 | updates = self.manager.update_hinted_locations(hinted_locations) | ||
| 632 | if len(updates) > 0: | ||
| 633 | self.manager.game_ctx.send_update_hinted_locations(updates) | ||
| 634 | |||
| 545 | 635 | ||
| 546 | async def pipe_loop(manager: Lingo2Manager): | 636 | async def pipe_loop(manager: Lingo2Manager): | 
| 547 | while not manager.client_ctx.exit_event.is_set(): | 637 | while not manager.client_ctx.exit_event.is_set(): | 
| @@ -590,12 +680,14 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): | |||
| 590 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | 680 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | 
| 591 | elif cmd == "CheckWorldport": | 681 | elif cmd == "CheckWorldport": | 
| 592 | port_id = args["port_id"] | 682 | port_id = args["port_id"] | 
| 683 | port_ap_id = Lingo2World.static_logic.objects.ports[port_id].ap_id | ||
| 593 | worldports = {port_id} | 684 | worldports = {port_id} | 
| 594 | 685 | ||
| 595 | # Also check the reverse port if it's a two-way connection. | 686 | # Also check the reverse port if it's a two-way connection. | 
| 596 | port_pairings = manager.client_ctx.slot_data["port_pairings"] | 687 | port_pairings = manager.client_ctx.slot_data["port_pairings"] | 
| 597 | if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id: | 688 | if str(port_ap_id) in port_pairings and\ | 
| 598 | worldports.add(port_pairings[str(port_id)]) | 689 | port_pairings.get(str(port_pairings[str(port_ap_id)]), None) == port_ap_id: | 
| 690 | worldports.add(Lingo2World.static_logic.port_id_by_ap_id[port_pairings[str(port_ap_id)]]) | ||
| 599 | 691 | ||
| 600 | updates = manager.update_worldports(worldports) | 692 | updates = manager.update_worldports(worldports) | 
| 601 | if len(updates) > 0: | 693 | if len(updates) > 0: | 
| @@ -616,6 +708,10 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): | |||
| 616 | updates = manager.update_latches({args["door"]}) | 708 | updates = manager.update_latches({args["door"]}) | 
| 617 | if len(updates) > 0: | 709 | if len(updates) > 0: | 
| 618 | async_start(manager.client_ctx.update_latches(updates), name="client update latches") | 710 | async_start(manager.client_ctx.update_latches(updates), name="client update latches") | 
| 711 | elif cmd == "IgnoreLocation": | ||
| 712 | async_start(manager.client_ctx.add_ignored_location(args["id"]), name="client ignore loc") | ||
| 713 | elif cmd == "UnignoreLocation": | ||
| 714 | async_start(manager.client_ctx.remove_ignored_location(args["id"]), name="client unignore loc") | ||
| 619 | elif cmd == "Quit": | 715 | elif cmd == "Quit": | 
| 620 | manager.client_ctx.exit_event.set() | 716 | manager.client_ctx.exit_event.set() | 
| 621 | 717 | ||
| diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 1d68e4a..3ee8f38 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py | |||
| @@ -299,8 +299,7 @@ class Lingo2PlayerLogic: | |||
| 299 | if door.map_id not in self.shuffled_maps: | 299 | if door.map_id not in self.shuffled_maps: | 
| 300 | continue | 300 | continue | 
| 301 | 301 | ||
| 302 | if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE, | 302 | if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: | 
| 303 | data_pb2.DoorType.LEGACY_LOCATION]: | ||
| 304 | continue | 303 | continue | 
| 305 | 304 | ||
| 306 | if door.id in self.item_by_door: | 305 | if door.id in self.item_by_door: | 
| diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 702f30b..8a84111 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py | |||
| @@ -15,6 +15,9 @@ class Lingo2StaticLogic: | |||
| 15 | 15 | ||
| 16 | letter_weights: dict[str, int] | 16 | letter_weights: dict[str, int] | 
| 17 | 17 | ||
| 18 | door_id_by_ap_id: dict[int, int] | ||
| 19 | port_id_by_ap_id: dict[int, int] | ||
| 20 | |||
| 18 | def __init__(self): | 21 | def __init__(self): | 
| 19 | self.item_id_to_name = {} | 22 | self.item_id_to_name = {} | 
| 20 | self.location_id_to_name = {} | 23 | self.location_id_to_name = {} | 
| @@ -31,8 +34,7 @@ class Lingo2StaticLogic: | |||
| 31 | location_name = self.get_door_location_name(door) | 34 | location_name = self.get_door_location_name(door) | 
| 32 | self.location_id_to_name[door.ap_id] = location_name | 35 | self.location_id_to_name[door.ap_id] = location_name | 
| 33 | 36 | ||
| 34 | if door.type not in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE, | 37 | if door.type not in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: | 
| 35 | data_pb2.DoorType.LEGACY_LOCATION]: | ||
| 36 | item_name = self.get_door_item_name(door) | 38 | item_name = self.get_door_item_name(door) | 
| 37 | self.item_id_to_name[door.ap_id] = item_name | 39 | self.item_id_to_name[door.ap_id] = item_name | 
| 38 | 40 | ||
| @@ -84,6 +86,9 @@ class Lingo2StaticLogic: | |||
| 84 | for letter in panel.answer.upper(): | 86 | for letter in panel.answer.upper(): | 
| 85 | self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1 | 87 | self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1 | 
| 86 | 88 | ||
| 89 | self.door_id_by_ap_id = {door.ap_id: door.id for door in self.objects.doors if door.HasField("ap_id")} | ||
| 90 | self.port_id_by_ap_id = {port.ap_id: port.id for port in self.objects.ports if port.HasField("ap_id")} | ||
| 91 | |||
| 87 | def get_door_item_name(self, door: data_pb2.Door) -> str: | 92 | def get_door_item_name(self, door: data_pb2.Door) -> str: | 
| 88 | return f"{self.get_map_object_map_name(door)} - {door.name}" | 93 | return f"{self.get_map_object_map_name(door)} - {door.name}" | 
| 89 | 94 | ||
| diff --git a/apworld/tracker.py b/apworld/tracker.py index c65317c..d473af4 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py | |||
| @@ -47,7 +47,10 @@ class Tracker: | |||
| 47 | self.world.create_regions() | 47 | self.world.create_regions() | 
| 48 | 48 | ||
| 49 | if self.world.options.shuffle_worldports: | 49 | if self.world.options.shuffle_worldports: | 
| 50 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} | 50 | port_pairings = { | 
| 51 | self.world.static_logic.port_id_by_ap_id[int(fp)]: self.world.static_logic.port_id_by_ap_id[int(tp)] | ||
| 52 | for fp, tp in slot_data["port_pairings"].items() | ||
| 53 | } | ||
| 51 | connect_ports_from_ut(port_pairings, self.world) | 54 | connect_ports_from_ut(port_pairings, self.world) | 
| 52 | 55 | ||
| 53 | self.refresh_state() | 56 | self.refresh_state() | 
