diff options
Diffstat (limited to 'apworld')
-rw-r--r-- | apworld/__init__.py | 2 | ||||
-rw-r--r-- | apworld/client/client.gd | 48 | ||||
-rw-r--r-- | apworld/client/gamedata.gd | 12 | ||||
-rw-r--r-- | apworld/client/keyboard.gd | 36 | ||||
-rw-r--r-- | apworld/client/manager.gd | 14 | ||||
-rw-r--r-- | apworld/client/pauseMenu.gd | 20 | ||||
-rw-r--r-- | apworld/client/player.gd | 1 | ||||
-rw-r--r-- | apworld/client/textclient.gd | 5 | ||||
-rw-r--r-- | apworld/client/worldport.gd | 10 | ||||
-rw-r--r-- | apworld/context.py | 450 | ||||
-rw-r--r-- | apworld/locations.py | 2 | ||||
-rw-r--r-- | apworld/player_logic.py | 13 | ||||
-rw-r--r-- | apworld/regions.py | 29 | ||||
-rw-r--r-- | apworld/static_logic.py | 1 | ||||
-rw-r--r-- | apworld/tracker.py | 65 |
15 files changed, 557 insertions, 151 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index f99f5f5..8da6d1f 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py | |||
@@ -59,6 +59,8 @@ class Lingo2World(World): | |||
59 | item_name_groups = static_logic.item_name_groups | 59 | item_name_groups = static_logic.item_name_groups |
60 | location_name_groups = static_logic.location_name_groups | 60 | location_name_groups = static_logic.location_name_groups |
61 | 61 | ||
62 | for_tracker: ClassVar[bool] = False | ||
63 | |||
62 | player_logic: Lingo2PlayerLogic | 64 | player_logic: Lingo2PlayerLogic |
63 | 65 | ||
64 | port_pairings: dict[int, int] | 66 | port_pairings: dict[int, int] |
diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 286ad4b..a23e85a 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd | |||
@@ -18,10 +18,12 @@ var _seed = "" | |||
18 | var _team = 0 | 18 | var _team = 0 |
19 | var _slot = 0 | 19 | var _slot = 0 |
20 | var _checked_locations = [] | 20 | var _checked_locations = [] |
21 | var _checked_worldports = [] | ||
21 | var _received_indexes = [] | 22 | var _received_indexes = [] |
22 | var _received_items = {} | 23 | var _received_items = {} |
23 | var _slot_data = {} | 24 | var _slot_data = {} |
24 | var _accessible_locations = [] | 25 | var _accessible_locations = [] |
26 | var _accessible_worldports = [] | ||
25 | 27 | ||
26 | signal could_not_connect | 28 | signal could_not_connect |
27 | signal connect_status | 29 | signal connect_status |
@@ -33,6 +35,8 @@ signal item_sent_notification(message) | |||
33 | signal hint_received(message) | 35 | signal hint_received(message) |
34 | signal accessible_locations_updated | 36 | signal accessible_locations_updated |
35 | signal checked_locations_updated | 37 | signal checked_locations_updated |
38 | signal checked_worldports_updated | ||
39 | signal keyboard_update_received | ||
36 | 40 | ||
37 | 41 | ||
38 | func _init(): | 42 | func _init(): |
@@ -54,7 +58,9 @@ func _reset_state(): | |||
54 | _should_process = false | 58 | _should_process = false |
55 | _received_items = {} | 59 | _received_items = {} |
56 | _received_indexes = [] | 60 | _received_indexes = [] |
61 | _checked_worldports = [] | ||
57 | _accessible_locations = [] | 62 | _accessible_locations = [] |
63 | _accessible_worldports = [] | ||
58 | 64 | ||
59 | 65 | ||
60 | func disconnect_from_ap(): | 66 | func disconnect_from_ap(): |
@@ -116,6 +122,14 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
116 | 122 | ||
117 | checked_locations_updated.emit() | 123 | checked_locations_updated.emit() |
118 | 124 | ||
125 | elif cmd == "UpdateWorldports": | ||
126 | for port_id in message["worldports"]: | ||
127 | var lint = int(port_id) | ||
128 | if not _checked_worldports.has(lint): | ||
129 | _checked_worldports.append(lint) | ||
130 | |||
131 | checked_worldports_updated.emit() | ||
132 | |||
119 | elif cmd == "ItemReceived": | 133 | elif cmd == "ItemReceived": |
120 | for item in message["items"]: | 134 | for item in message["items"]: |
121 | var index = int(item["index"]) | 135 | var index = int(item["index"]) |
@@ -151,12 +165,24 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
151 | 165 | ||
152 | elif cmd == "AccessibleLocations": | 166 | elif cmd == "AccessibleLocations": |
153 | _accessible_locations.clear() | 167 | _accessible_locations.clear() |
168 | _accessible_worldports.clear() | ||
154 | 169 | ||
155 | for loc in message["locations"]: | 170 | for loc in message["locations"]: |
156 | _accessible_locations.append(int(loc)) | 171 | _accessible_locations.append(int(loc)) |
157 | 172 | ||
173 | if "worldports" in message: | ||
174 | for port_id in message["worldports"]: | ||
175 | _accessible_worldports.append(int(port_id)) | ||
176 | |||
158 | accessible_locations_updated.emit() | 177 | accessible_locations_updated.emit() |
159 | 178 | ||
179 | elif cmd == "UpdateKeyboard": | ||
180 | var updates = {} | ||
181 | for k in message["updates"]: | ||
182 | updates[k] = int(message["updates"][k]) | ||
183 | |||
184 | keyboard_update_received.emit(updates) | ||
185 | |||
160 | 186 | ||
161 | func connectToServer(server, un, pw): | 187 | func connectToServer(server, un, pw): |
162 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) | 188 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) |
@@ -202,19 +228,6 @@ func sendLocations(loc_ids): | |||
202 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) | 228 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) |
203 | 229 | ||
204 | 230 | ||
205 | func setValue(key, value, operation = "replace"): | ||
206 | sendMessage( | ||
207 | [ | ||
208 | { | ||
209 | "cmd": "Set", | ||
210 | "key": "Lingo2_%d_%s" % [_slot, key], | ||
211 | "want_reply": false, | ||
212 | "operations": [{"operation": operation, "value": value}] | ||
213 | } | ||
214 | ] | ||
215 | ) | ||
216 | |||
217 | |||
218 | func say(textdata): | 231 | func say(textdata): |
219 | sendMessage([{"cmd": "Say", "text": textdata}]) | 232 | sendMessage([{"cmd": "Say", "text": textdata}]) |
220 | 233 | ||
@@ -227,6 +240,15 @@ func scoutLocations(loc_ids): | |||
227 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) | 240 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) |
228 | 241 | ||
229 | 242 | ||
243 | func updateKeyboard(updates): | ||
244 | sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}]) | ||
245 | |||
246 | |||
247 | func checkWorldport(port_id): | ||
248 | if not _checked_worldports.has(port_id): | ||
249 | sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) | ||
250 | |||
251 | |||
230 | func sendQuit(): | 252 | func sendQuit(): |
231 | sendMessage([{"cmd": "Quit"}]) | 253 | sendMessage([{"cmd": "Quit"}]) |
232 | 254 | ||
diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 39e0583..e44fa17 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd | |||
@@ -161,11 +161,23 @@ func get_door_ap_id(door_id): | |||
161 | return null | 161 | return null |
162 | 162 | ||
163 | 163 | ||
164 | func get_door_map_name(door_id): | ||
165 | var door = objects.get_doors()[door_id] | ||
166 | var room = objects.get_rooms()[door.get_room_id()] | ||
167 | var map = objects.get_maps()[room.get_map_id()] | ||
168 | return map.get_name() | ||
169 | |||
170 | |||
164 | func get_door_receivers(door_id): | 171 | func get_door_receivers(door_id): |
165 | var door = objects.get_doors()[door_id] | 172 | var door = objects.get_doors()[door_id] |
166 | return door.get_receivers() | 173 | return door.get_receivers() |
167 | 174 | ||
168 | 175 | ||
176 | func get_worldport_display_name(port_id): | ||
177 | var port = objects.get_ports()[port_id] | ||
178 | return "%s - %s (Worldport)" % [_get_room_object_map_name(port), port.get_name()] | ||
179 | |||
180 | |||
169 | func _get_map_object_map_name(obj): | 181 | func _get_map_object_map_name(obj): |
170 | return objects.get_maps()[obj.get_map_id()].get_display_name() | 182 | return objects.get_maps()[obj.get_map_id()].get_display_name() |
171 | 183 | ||
diff --git a/apworld/client/keyboard.gd b/apworld/client/keyboard.gd index 450566d..a59c4d0 100644 --- a/apworld/client/keyboard.gd +++ b/apworld/client/keyboard.gd | |||
@@ -48,6 +48,9 @@ func load_seed(): | |||
48 | if localdata.size() > 2: | 48 | if localdata.size() > 2: |
49 | keyholder_state = localdata[2] | 49 | keyholder_state = localdata[2] |
50 | 50 | ||
51 | if not letters_saved.is_empty(): | ||
52 | ap.client.updateKeyboard(letters_saved) | ||
53 | |||
51 | for k in kALL_LETTERS: | 54 | for k in kALL_LETTERS: |
52 | var level = 0 | 55 | var level = 0 |
53 | 56 | ||
@@ -105,10 +108,20 @@ func update_unlocks(): | |||
105 | 108 | ||
106 | 109 | ||
107 | func collect_local_letter(key, level): | 110 | func collect_local_letter(key, level): |
108 | if level < 0 or level > 2 or level < letters_saved.get(key, 0): | 111 | var ap = global.get_node("Archipelago") |
112 | var true_level = 0 | ||
113 | |||
114 | if ap.get_letter_behavior(key, false) == ap.kLETTER_BEHAVIOR_VANILLA: | ||
115 | true_level += 1 | ||
116 | if level == 2 and ap.get_letter_behavior(key, true) == ap.kLETTER_BEHAVIOR_VANILLA: | ||
117 | true_level += 1 | ||
118 | |||
119 | if true_level < letters_saved.get(key, 0): | ||
109 | return | 120 | return |
110 | 121 | ||
111 | letters_saved[key] = level | 122 | letters_saved[key] = true_level |
123 | |||
124 | ap.client.updateKeyboard({key: true_level}) | ||
112 | 125 | ||
113 | if letters_blocked.has(key): | 126 | if letters_blocked.has(key): |
114 | letters_blocked.erase(key) | 127 | letters_blocked.erase(key) |
@@ -197,3 +210,22 @@ func reset_keyholders(): | |||
197 | save() | 210 | save() |
198 | 211 | ||
199 | return cleared_anything | 212 | return cleared_anything |
213 | |||
214 | |||
215 | func remote_keyboard_updated(updates): | ||
216 | var reverse = {} | ||
217 | var should_update = false | ||
218 | |||
219 | for k in updates: | ||
220 | if not letters_saved.has(k) or updates[k] > letters_saved[k]: | ||
221 | letters_saved[k] = updates[k] | ||
222 | should_update = true | ||
223 | elif updates[k] < letters_saved[k]: | ||
224 | reverse[k] = letters_saved[k] | ||
225 | |||
226 | if should_update: | ||
227 | update_unlocks() | ||
228 | |||
229 | if not reverse.is_empty(): | ||
230 | var ap = global.get_node("Archipelago") | ||
231 | ap.client.updateKeyboard(reverse) | ||
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 2c25269..3facfba 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd | |||
@@ -15,6 +15,7 @@ var ap_pass = "" | |||
15 | var connection_history = [] | 15 | var connection_history = [] |
16 | var show_compass = false | 16 | var show_compass = false |
17 | var show_locations = false | 17 | var show_locations = false |
18 | var show_minimap = false | ||
18 | 19 | ||
19 | var client | 20 | var client |
20 | var keyboard | 21 | var keyboard |
@@ -93,6 +94,9 @@ func _init(): | |||
93 | if data.size() > 5: | 94 | if data.size() > 5: |
94 | show_locations = data[5] | 95 | show_locations = data[5] |
95 | 96 | ||
97 | if data.size() > 6: | ||
98 | show_minimap = data[6] | ||
99 | |||
96 | 100 | ||
97 | func _ready(): | 101 | func _ready(): |
98 | client = SCRIPT_client.new() | 102 | client = SCRIPT_client.new() |
@@ -105,6 +109,7 @@ func _ready(): | |||
105 | client.hint_received.connect(_process_hint_received) | 109 | client.hint_received.connect(_process_hint_received) |
106 | client.accessible_locations_updated.connect(_on_accessible_locations_updated) | 110 | client.accessible_locations_updated.connect(_on_accessible_locations_updated) |
107 | client.checked_locations_updated.connect(_on_checked_locations_updated) | 111 | client.checked_locations_updated.connect(_on_checked_locations_updated) |
112 | client.checked_worldports_updated.connect(_on_checked_worldports_updated) | ||
108 | 113 | ||
109 | client.could_not_connect.connect(_client_could_not_connect) | 114 | client.could_not_connect.connect(_client_could_not_connect) |
110 | client.connect_status.connect(_client_connect_status) | 115 | client.connect_status.connect(_client_connect_status) |
@@ -114,6 +119,7 @@ func _ready(): | |||
114 | 119 | ||
115 | keyboard = SCRIPT_keyboard.new() | 120 | keyboard = SCRIPT_keyboard.new() |
116 | add_child(keyboard) | 121 | add_child(keyboard) |
122 | client.keyboard_update_received.connect(keyboard.remote_keyboard_updated) | ||
117 | 123 | ||
118 | 124 | ||
119 | func saveSettings(): | 125 | func saveSettings(): |
@@ -128,6 +134,7 @@ func saveSettings(): | |||
128 | connection_history, | 134 | connection_history, |
129 | show_compass, | 135 | show_compass, |
130 | show_locations, | 136 | show_locations, |
137 | show_minimap, | ||
131 | ] | 138 | ] |
132 | file.store_var(data, true) | 139 | file.store_var(data, true) |
133 | file.close() | 140 | file.close() |
@@ -189,6 +196,7 @@ func _process_item(item, amount): | |||
189 | if gamedata.get_door_map_name(lock[0]) != global.map: | 196 | if gamedata.get_door_map_name(lock[0]) != global.map: |
190 | continue | 197 | continue |
191 | 198 | ||
199 | # TODO: fix doors opening from door groups | ||
192 | var receivers = gamedata.get_door_receivers(lock[0]) | 200 | var receivers = gamedata.get_door_receivers(lock[0]) |
193 | var scene = get_tree().get_root().get_node_or_null("scene") | 201 | var scene = get_tree().get_root().get_node_or_null("scene") |
194 | if scene != null: | 202 | if scene != null: |
@@ -321,6 +329,12 @@ func _on_checked_locations_updated(): | |||
321 | textclient_node.update_locations() | 329 | textclient_node.update_locations() |
322 | 330 | ||
323 | 331 | ||
332 | func _on_checked_worldports_updated(): | ||
333 | var textclient_node = global.get_node("Textclient") | ||
334 | if textclient_node != null: | ||
335 | textclient_node.update_locations() | ||
336 | |||
337 | |||
324 | func _client_could_not_connect(message): | 338 | func _client_could_not_connect(message): |
325 | could_not_connect.emit(message) | 339 | could_not_connect.emit(message) |
326 | 340 | ||
diff --git a/apworld/client/pauseMenu.gd b/apworld/client/pauseMenu.gd index d1b4bb3..72b45e8 100644 --- a/apworld/client/pauseMenu.gd +++ b/apworld/client/pauseMenu.gd | |||
@@ -2,6 +2,7 @@ extends "res://scripts/ui/pauseMenu.gd" | |||
2 | 2 | ||
3 | var compass_button | 3 | var compass_button |
4 | var locations_button | 4 | var locations_button |
5 | var minimap_button | ||
5 | 6 | ||
6 | 7 | ||
7 | func _ready(): | 8 | func _ready(): |
@@ -29,6 +30,15 @@ func _ready(): | |||
29 | locations_button.pressed.connect(_toggle_locations) | 30 | locations_button.pressed.connect(_toggle_locations) |
30 | ap_panel.add_child(locations_button) | 31 | ap_panel.add_child(locations_button) |
31 | 32 | ||
33 | minimap_button = CheckBox.new() | ||
34 | minimap_button.text = "show minimap" | ||
35 | minimap_button.button_pressed = ap.show_minimap | ||
36 | minimap_button.position = Vector2(65, 300) | ||
37 | minimap_button.theme = preload("res://assets/themes/baseUI.tres") | ||
38 | minimap_button.add_theme_font_size_override("font_size", 60) | ||
39 | minimap_button.pressed.connect(_toggle_minimap) | ||
40 | ap_panel.add_child(minimap_button) | ||
41 | |||
32 | super._ready() | 42 | super._ready() |
33 | 43 | ||
34 | 44 | ||
@@ -69,3 +79,13 @@ func _toggle_locations(): | |||
69 | 79 | ||
70 | var textclient = global.get_node("Textclient") | 80 | var textclient = global.get_node("Textclient") |
71 | textclient.update_locations_visibility() | 81 | textclient.update_locations_visibility() |
82 | |||
83 | |||
84 | func _toggle_minimap(): | ||
85 | var ap = global.get_node("Archipelago") | ||
86 | ap.show_minimap = minimap_button.button_pressed | ||
87 | ap.saveSettings() | ||
88 | |||
89 | var minimap = get_tree().get_root().get_node("scene/Minimap") | ||
90 | if minimap != null: | ||
91 | minimap.visible = ap.show_minimap | ||
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index fb88880..366c3b0 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd | |||
@@ -330,6 +330,7 @@ func _ready(): | |||
330 | 330 | ||
331 | var minimap = ap.SCRIPT_minimap.new() | 331 | var minimap = ap.SCRIPT_minimap.new() |
332 | minimap.name = "Minimap" | 332 | minimap.name = "Minimap" |
333 | minimap.visible = ap.show_minimap | ||
333 | get_parent().add_child.call_deferred(minimap) | 334 | get_parent().add_child.call_deferred(minimap) |
334 | 335 | ||
335 | super._ready() | 336 | super._ready() |
diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 1b36c29..af155fb 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd | |||
@@ -150,6 +150,11 @@ func update_locations(): | |||
150 | var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") | 150 | var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") |
151 | location_names.append(location_name) | 151 | location_names.append(location_name) |
152 | 152 | ||
153 | for port_id in ap.client._accessible_worldports: | ||
154 | if not ap.client._checked_worldports.has(port_id): | ||
155 | var port_name = gamedata.get_worldport_display_name(port_id) | ||
156 | location_names.append(port_name) | ||
157 | |||
153 | location_names.sort() | 158 | location_names.sort() |
154 | 159 | ||
155 | var count = 0 | 160 | var count = 0 |
diff --git a/apworld/client/worldport.gd b/apworld/client/worldport.gd index cdca248..ed9891e 100644 --- a/apworld/client/worldport.gd +++ b/apworld/client/worldport.gd | |||
@@ -3,6 +3,8 @@ extends "res://scripts/nodes/worldport.gd" | |||
3 | var absolute_rotation = false | 3 | var absolute_rotation = false |
4 | var target_rotation = 0 | 4 | var target_rotation = 0 |
5 | 5 | ||
6 | var port_id = null | ||
7 | |||
6 | 8 | ||
7 | func _ready(): | 9 | func _ready(): |
8 | var node_path = String( | 10 | var node_path = String( |
@@ -13,7 +15,7 @@ func _ready(): | |||
13 | 15 | ||
14 | if ap.shuffle_worldports: | 16 | if ap.shuffle_worldports: |
15 | var gamedata = global.get_node("Gamedata") | 17 | var gamedata = global.get_node("Gamedata") |
16 | var port_id = gamedata.get_port_for_map_node_path(global.map, node_path) | 18 | port_id = gamedata.get_port_for_map_node_path(global.map, node_path) |
17 | if port_id != null: | 19 | if port_id != null: |
18 | if port_id in ap.port_pairings: | 20 | if port_id in ap.port_pairings: |
19 | var target_port = gamedata.objects.get_ports()[ap.port_pairings[port_id]] | 21 | var target_port = gamedata.objects.get_ports()[ap.port_pairings[port_id]] |
@@ -29,6 +31,8 @@ func _ready(): | |||
29 | sets_entry_point = true | 31 | sets_entry_point = true |
30 | invisible = false | 32 | invisible = false |
31 | fades = true | 33 | fades = true |
34 | else: | ||
35 | port_id = null | ||
32 | 36 | ||
33 | if global.map == "icarus" and exit == "daedalus": | 37 | if global.map == "icarus" and exit == "daedalus": |
34 | if not ap.daedalus_roof_access: | 38 | if not ap.daedalus_roof_access: |
@@ -39,6 +43,10 @@ func _ready(): | |||
39 | 43 | ||
40 | func bodyEntered(body): | 44 | func bodyEntered(body): |
41 | if body.is_in_group("player"): | 45 | if body.is_in_group("player"): |
46 | if port_id != null: | ||
47 | var ap = global.get_node("Archipelago") | ||
48 | ap.client.checkWorldport(port_id) | ||
49 | |||
42 | if absolute_rotation: | 50 | if absolute_rotation: |
43 | entry_rotate.y = target_rotation - body.rotation_degrees.y | 51 | entry_rotate.y = target_rotation - body.rotation_degrees.y |
44 | 52 | ||
diff --git a/apworld/context.py b/apworld/context.py index 0a058e5..4a85868 100644 --- a/apworld/context.py +++ b/apworld/context.py | |||
@@ -15,35 +15,99 @@ from Utils import async_start | |||
15 | from . import Lingo2World | 15 | from . import Lingo2World |
16 | from .tracker import Tracker | 16 | from .tracker import Tracker |
17 | 17 | ||
18 | PORT = 43182 | 18 | ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz" |
19 | MESSAGE_MAX_SIZE = 16*1024*1024 | 19 | MESSAGE_MAX_SIZE = 16*1024*1024 |
20 | PORT = 43182 | ||
21 | |||
22 | KEY_STORAGE_MAPPING = { | ||
23 | "a": (1, 0), "b": (1, 1), "c": (1, 2), "d": (1, 3), "e": (1, 4), "f": (1, 5), "g": (1, 6), "h": (1, 7), "i": (1, 8), | ||
24 | "j": (1, 9), "k": (1, 10), "l": (1, 11), "m": (1, 12), "n": (2, 0), "o": (2, 1), "p": (2, 2), "q": (2, 3), | ||
25 | "r": (2, 4), "s": (2, 5), "t": (2, 6), "u": (2, 7), "v": (2, 8), "w": (2, 9), "x": (2, 10), "y": (2, 11), | ||
26 | "z": (2, 12), | ||
27 | } | ||
28 | |||
29 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} | ||
30 | |||
31 | |||
32 | class Lingo2Manager: | ||
33 | game_ctx: "Lingo2GameContext" | ||
34 | client_ctx: "Lingo2ClientContext" | ||
35 | tracker: Tracker | ||
36 | |||
37 | keyboard: dict[str, int] | ||
38 | worldports: set[int] | ||
39 | |||
40 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | ||
41 | self.game_ctx = game_ctx | ||
42 | self.game_ctx.manager = self | ||
43 | self.client_ctx = client_ctx | ||
44 | self.client_ctx.manager = self | ||
45 | self.tracker = Tracker(self) | ||
46 | self.keyboard = {} | ||
47 | self.worldports = set() | ||
48 | |||
49 | self.reset() | ||
50 | |||
51 | def reset(self): | ||
52 | for k in ALL_LETTERS: | ||
53 | self.keyboard[k] = 0 | ||
54 | |||
55 | self.worldports = set() | ||
56 | |||
57 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | ||
58 | ret: dict[str, int] = {} | ||
59 | |||
60 | for k, v in new_keyboard.items(): | ||
61 | if v > self.keyboard.get(k, 0): | ||
62 | self.keyboard[k] = v | ||
63 | ret[k] = v | ||
64 | |||
65 | if len(ret) > 0: | ||
66 | self.tracker.refresh_state() | ||
67 | self.game_ctx.send_accessible_locations() | ||
68 | |||
69 | return ret | ||
70 | |||
71 | def update_worldports(self, new_worldports: set[int]) -> set[int]: | ||
72 | ret = new_worldports.difference(self.worldports) | ||
73 | self.worldports.update(new_worldports) | ||
74 | |||
75 | if len(ret) > 0: | ||
76 | self.tracker.refresh_state() | ||
77 | self.game_ctx.send_accessible_locations() | ||
78 | |||
79 | return ret | ||
20 | 80 | ||
21 | 81 | ||
22 | class Lingo2GameContext: | 82 | class Lingo2GameContext: |
23 | server: Endpoint | None | 83 | server: Endpoint | None |
24 | client: "Lingo2ClientContext" | 84 | manager: Lingo2Manager |
25 | tracker: Tracker | ||
26 | 85 | ||
27 | def __init__(self): | 86 | def __init__(self): |
28 | self.server = None | 87 | self.server = None |
29 | self.tracker = Tracker() | ||
30 | 88 | ||
31 | def send_connected(self): | 89 | def send_connected(self): |
90 | if self.server is None: | ||
91 | return | ||
92 | |||
32 | msg = { | 93 | msg = { |
33 | "cmd": "Connected", | 94 | "cmd": "Connected", |
34 | "user": self.client.username, | 95 | "user": self.manager.client_ctx.username, |
35 | "seed_name": self.client.seed_name, | 96 | "seed_name": self.manager.client_ctx.seed_name, |
36 | "version": self.client.server_version, | 97 | "version": self.manager.client_ctx.server_version, |
37 | "generator_version": self.client.generator_version, | 98 | "generator_version": self.manager.client_ctx.generator_version, |
38 | "team": self.client.team, | 99 | "team": self.manager.client_ctx.team, |
39 | "slot": self.client.slot, | 100 | "slot": self.manager.client_ctx.slot, |
40 | "checked_locations": self.client.checked_locations, | 101 | "checked_locations": self.manager.client_ctx.checked_locations, |
41 | "slot_data": self.client.slot_data, | 102 | "slot_data": self.manager.client_ctx.slot_data, |
42 | } | 103 | } |
43 | 104 | ||
44 | async_start(self.send_msgs([msg]), name="game Connected") | 105 | async_start(self.send_msgs([msg]), name="game Connected") |
45 | 106 | ||
46 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): | 107 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): |
108 | if self.server is None: | ||
109 | return | ||
110 | |||
47 | msg = { | 111 | msg = { |
48 | "cmd": "ItemSentNotif", | 112 | "cmd": "ItemSentNotif", |
49 | "item_name": item_name, | 113 | "item_name": item_name, |
@@ -54,6 +118,9 @@ class Lingo2GameContext: | |||
54 | async_start(self.send_msgs([msg]), name="item sent notif") | 118 | async_start(self.send_msgs([msg]), name="item sent notif") |
55 | 119 | ||
56 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): | 120 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): |
121 | if self.server is None: | ||
122 | return | ||
123 | |||
57 | msg = { | 124 | msg = { |
58 | "cmd": "HintReceived", | 125 | "cmd": "HintReceived", |
59 | "item_name": item_name, | 126 | "item_name": item_name, |
@@ -66,6 +133,9 @@ class Lingo2GameContext: | |||
66 | async_start(self.send_msgs([msg]), name="hint received notif") | 133 | async_start(self.send_msgs([msg]), name="hint received notif") |
67 | 134 | ||
68 | def send_item_received(self, items): | 135 | def send_item_received(self, items): |
136 | if self.server is None: | ||
137 | return | ||
138 | |||
69 | msg = { | 139 | msg = { |
70 | "cmd": "ItemReceived", | 140 | "cmd": "ItemReceived", |
71 | "items": items, | 141 | "items": items, |
@@ -74,6 +144,9 @@ class Lingo2GameContext: | |||
74 | async_start(self.send_msgs([msg]), name="item received") | 144 | async_start(self.send_msgs([msg]), name="item received") |
75 | 145 | ||
76 | def send_location_info(self, locations): | 146 | def send_location_info(self, locations): |
147 | if self.server is None: | ||
148 | return | ||
149 | |||
77 | msg = { | 150 | msg = { |
78 | "cmd": "LocationInfo", | 151 | "cmd": "LocationInfo", |
79 | "locations": locations, | 152 | "locations": locations, |
@@ -82,6 +155,9 @@ class Lingo2GameContext: | |||
82 | async_start(self.send_msgs([msg]), name="location info") | 155 | async_start(self.send_msgs([msg]), name="location info") |
83 | 156 | ||
84 | def send_text_message(self, parts): | 157 | def send_text_message(self, parts): |
158 | if self.server is None: | ||
159 | return | ||
160 | |||
85 | msg = { | 161 | msg = { |
86 | "cmd": "TextMessage", | 162 | "cmd": "TextMessage", |
87 | "data": parts, | 163 | "data": parts, |
@@ -90,14 +166,23 @@ class Lingo2GameContext: | |||
90 | async_start(self.send_msgs([msg]), name="notif") | 166 | async_start(self.send_msgs([msg]), name="notif") |
91 | 167 | ||
92 | def send_accessible_locations(self): | 168 | def send_accessible_locations(self): |
169 | if self.server is None: | ||
170 | return | ||
171 | |||
93 | msg = { | 172 | msg = { |
94 | "cmd": "AccessibleLocations", | 173 | "cmd": "AccessibleLocations", |
95 | "locations": list(self.tracker.accessible_locations), | 174 | "locations": list(self.manager.tracker.accessible_locations), |
96 | } | 175 | } |
97 | 176 | ||
177 | if len(self.manager.tracker.accessible_worldports) > 0: | ||
178 | msg["worldports"] = list(self.manager.tracker.accessible_worldports) | ||
179 | |||
98 | async_start(self.send_msgs([msg]), name="accessible locations") | 180 | async_start(self.send_msgs([msg]), name="accessible locations") |
99 | 181 | ||
100 | def send_update_locations(self, locations): | 182 | def send_update_locations(self, locations): |
183 | if self.server is None: | ||
184 | return | ||
185 | |||
101 | msg = { | 186 | msg = { |
102 | "cmd": "UpdateLocations", | 187 | "cmd": "UpdateLocations", |
103 | "locations": locations, | 188 | "locations": locations, |
@@ -105,6 +190,28 @@ class Lingo2GameContext: | |||
105 | 190 | ||
106 | async_start(self.send_msgs([msg]), name="update locations") | 191 | async_start(self.send_msgs([msg]), name="update locations") |
107 | 192 | ||
193 | def send_update_keyboard(self, updates): | ||
194 | if self.server is None: | ||
195 | return | ||
196 | |||
197 | msg = { | ||
198 | "cmd": "UpdateKeyboard", | ||
199 | "updates": updates, | ||
200 | } | ||
201 | |||
202 | async_start(self.send_msgs([msg]), name="update keyboard") | ||
203 | |||
204 | def send_update_worldports(self, worldports): | ||
205 | if self.server is None: | ||
206 | return | ||
207 | |||
208 | msg = { | ||
209 | "cmd": "UpdateWorldports", | ||
210 | "worldports": worldports, | ||
211 | } | ||
212 | |||
213 | async_start(self.send_msgs([msg]), name="update worldports") | ||
214 | |||
108 | async def send_msgs(self, msgs: list[Any]) -> None: | 215 | async def send_msgs(self, msgs: list[Any]) -> None: |
109 | """ `msgs` JSON serializable """ | 216 | """ `msgs` JSON serializable """ |
110 | if not self.server or not self.server.socket.open or self.server.socket.closed: | 217 | if not self.server or not self.server.socket.open or self.server.socket.closed: |
@@ -113,7 +220,7 @@ class Lingo2GameContext: | |||
113 | 220 | ||
114 | 221 | ||
115 | class Lingo2ClientContext(CommonContext): | 222 | class Lingo2ClientContext(CommonContext): |
116 | game_ctx: Lingo2GameContext | 223 | manager: Lingo2Manager |
117 | 224 | ||
118 | game = "Lingo 2" | 225 | game = "Lingo 2" |
119 | items_handling = 0b111 | 226 | items_handling = 0b111 |
@@ -138,118 +245,226 @@ class Lingo2ClientContext(CommonContext): | |||
138 | elif cmd == "Connected": | 245 | elif cmd == "Connected": |
139 | self.slot_data = args.get("slot_data", None) | 246 | self.slot_data = args.get("slot_data", None) |
140 | 247 | ||
141 | if self.game_ctx.server is not None: | 248 | self.manager.reset() |
142 | self.game_ctx.send_connected() | 249 | |
143 | 250 | self.manager.game_ctx.send_connected() | |
144 | self.game_ctx.tracker.setup_slot(self.slot_data) | 251 | |
252 | self.manager.tracker.setup_slot(self.slot_data) | ||
253 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
254 | self.manager.game_ctx.send_accessible_locations() | ||
255 | |||
256 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2")) | ||
257 | msg_batch = [{ | ||
258 | "cmd": "Set", | ||
259 | "key": self.get_datastorage_key("keyboard1"), | ||
260 | "default": 0, | ||
261 | "want_reply": True, | ||
262 | "operations": [{"operation": "default", "value": 0}] | ||
263 | }, { | ||
264 | "cmd": "Set", | ||
265 | "key": self.get_datastorage_key("keyboard2"), | ||
266 | "default": 0, | ||
267 | "want_reply": True, | ||
268 | "operations": [{"operation": "default", "value": 0}] | ||
269 | }] | ||
270 | |||
271 | if self.slot_data["shuffle_worldports"]: | ||
272 | self.set_notify(self.get_datastorage_key("worldports")) | ||
273 | msg_batch.append({ | ||
274 | "cmd": "Set", | ||
275 | "key": self.get_datastorage_key("worldports"), | ||
276 | "default": [], | ||
277 | "want_reply": True, | ||
278 | "operations": [{"operation": "default", "value": []}] | ||
279 | }) | ||
280 | |||
281 | async_start(self.send_msgs(msg_batch), name="default keys") | ||
145 | elif cmd == "RoomUpdate": | 282 | elif cmd == "RoomUpdate": |
146 | if self.game_ctx.server is not None: | 283 | self.manager.tracker.set_checked_locations(self.checked_locations) |
147 | self.game_ctx.send_update_locations(args["checked_locations"]) | 284 | self.manager.game_ctx.send_update_locations(args["checked_locations"]) |
148 | elif cmd == "ReceivedItems": | 285 | elif cmd == "ReceivedItems": |
149 | self.game_ctx.tracker.set_collected_items(self.items_received) | 286 | self.manager.tracker.set_collected_items(self.items_received) |
150 | 287 | ||
151 | if self.game_ctx.server is not None: | 288 | cur_index = 0 |
152 | cur_index = 0 | 289 | items = [] |
153 | items = [] | ||
154 | 290 | ||
155 | for item in args["items"]: | 291 | for item in args["items"]: |
156 | index = cur_index + args["index"] | 292 | index = cur_index + args["index"] |
157 | cur_index += 1 | 293 | cur_index += 1 |
158 | 294 | ||
159 | item_msg = { | 295 | item_msg = { |
160 | "id": item.item, | 296 | "id": item.item, |
161 | "index": index, | 297 | "index": index, |
162 | "flags": item.flags, | 298 | "flags": item.flags, |
163 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | 299 | "text": self.item_names.lookup_in_slot(item.item, self.slot), |
164 | } | 300 | } |
165 | 301 | ||
166 | if item.player != self.slot: | 302 | if item.player != self.slot: |
167 | item_msg["sender"] = self.player_names.get(item.player) | 303 | item_msg["sender"] = self.player_names.get(item.player) |
168 | 304 | ||
169 | items.append(item_msg) | 305 | items.append(item_msg) |
170 | 306 | ||
171 | self.game_ctx.send_item_received(items) | 307 | self.manager.game_ctx.send_item_received(items) |
172 | 308 | ||
173 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): | 309 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): |
174 | self.game_ctx.send_accessible_locations() | 310 | self.manager.game_ctx.send_accessible_locations() |
175 | elif cmd == "PrintJSON": | 311 | elif cmd == "PrintJSON": |
176 | if self.game_ctx.server is not None: | 312 | if "receiving" in args and "item" in args and args["item"].player == self.slot: |
177 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | 313 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) |
178 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | 314 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) |
179 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | 315 | receiver_name = self.player_names.get(args["receiving"]) |
180 | receiver_name = self.player_names.get(args["receiving"]) | 316 | |
181 | 317 | if args["type"] == "Hint" and not args.get("found", False): | |
182 | if args["type"] == "Hint" and not args.get("found", False): | 318 | self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, |
183 | self.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | 319 | int(args["receiving"]) == self.slot) |
184 | int(args["receiving"]) == self.slot) | 320 | elif args["receiving"] != self.slot: |
185 | elif args["receiving"] != self.slot: | 321 | self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) |
186 | self.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | 322 | |
187 | 323 | parts = [] | |
188 | parts = [] | 324 | for message_part in args["data"]: |
189 | for message_part in args["data"]: | 325 | if "type" not in message_part and "text" in message_part: |
190 | if "type" not in message_part and "text" in message_part: | 326 | parts.append({"type": "text", "text": message_part["text"]}) |
191 | parts.append({"type": "text", "text": message_part["text"]}) | 327 | elif message_part["type"] == "player_id": |
192 | elif message_part["type"] == "player_id": | 328 | parts.append({ |
193 | parts.append({ | 329 | "type": "player", |
194 | "type": "player", | 330 | "text": self.player_names.get(int(message_part["text"])), |
195 | "text": self.player_names.get(int(message_part["text"])), | 331 | "self": int(int(message_part["text"]) == self.slot), |
196 | "self": int(int(message_part["text"]) == self.slot), | ||
197 | }) | ||
198 | elif message_part["type"] == "item_id": | ||
199 | parts.append({ | ||
200 | "type": "item", | ||
201 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
202 | "flags": message_part["flags"], | ||
203 | }) | ||
204 | elif message_part["type"] == "location_id": | ||
205 | parts.append({ | ||
206 | "type": "location", | ||
207 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
208 | message_part["player"]) | ||
209 | }) | ||
210 | elif "text" in message_part: | ||
211 | parts.append({"type": "text", "text": message_part["text"]}) | ||
212 | |||
213 | self.game_ctx.send_text_message(parts) | ||
214 | elif cmd == "LocationInfo": | ||
215 | if self.game_ctx.server is not None: | ||
216 | locations = [] | ||
217 | |||
218 | for location in args["locations"]: | ||
219 | locations.append({ | ||
220 | "id": location.location, | ||
221 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
222 | "player": self.player_names.get(location.player), | ||
223 | "flags": location.flags, | ||
224 | "self": int(location.player) == self.slot, | ||
225 | }) | 332 | }) |
333 | elif message_part["type"] == "item_id": | ||
334 | parts.append({ | ||
335 | "type": "item", | ||
336 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
337 | "flags": message_part["flags"], | ||
338 | }) | ||
339 | elif message_part["type"] == "location_id": | ||
340 | parts.append({ | ||
341 | "type": "location", | ||
342 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
343 | message_part["player"]) | ||
344 | }) | ||
345 | elif "text" in message_part: | ||
346 | parts.append({"type": "text", "text": message_part["text"]}) | ||
226 | 347 | ||
227 | self.game_ctx.send_location_info(locations) | 348 | self.manager.game_ctx.send_text_message(parts) |
228 | 349 | elif cmd == "LocationInfo": | |
229 | if cmd in ["Connected", "RoomUpdate"]: | 350 | locations = [] |
230 | self.game_ctx.tracker.set_checked_locations(self.checked_locations) | 351 | |
231 | 352 | for location in args["locations"]: | |
232 | 353 | locations.append({ | |
233 | async def pipe_loop(ctx: Lingo2GameContext): | 354 | "id": location.location, |
234 | while not ctx.client.exit_event.is_set(): | 355 | "item": self.item_names.lookup_in_slot(location.item, location.player), |
356 | "player": self.player_names.get(location.player), | ||
357 | "flags": location.flags, | ||
358 | "self": int(location.player) == self.slot, | ||
359 | }) | ||
360 | |||
361 | self.manager.game_ctx.send_location_info(locations) | ||
362 | elif cmd == "SetReply": | ||
363 | if args["key"] == self.get_datastorage_key("keyboard1"): | ||
364 | self.handle_keyboard_update(1, args) | ||
365 | elif args["key"] == self.get_datastorage_key("keyboard2"): | ||
366 | self.handle_keyboard_update(2, args) | ||
367 | elif args["key"] == self.get_datastorage_key("worldports"): | ||
368 | updates = self.manager.update_worldports(set(args["value"])) | ||
369 | if len(updates) > 0: | ||
370 | self.manager.game_ctx.send_update_worldports(updates) | ||
371 | |||
372 | def get_datastorage_key(self, name: str): | ||
373 | return f"Lingo2_{self.slot}_{name}" | ||
374 | |||
375 | async def update_keyboard(self, updates: dict[str, int]): | ||
376 | kb1 = 0 | ||
377 | kb2 = 0 | ||
378 | |||
379 | for k, v in updates.items(): | ||
380 | if v == 0: | ||
381 | continue | ||
382 | |||
383 | effect = 0 | ||
384 | if v >= 1: | ||
385 | effect |= 1 | ||
386 | if v == 2: | ||
387 | effect |= 2 | ||
388 | |||
389 | pos = KEY_STORAGE_MAPPING[k] | ||
390 | if pos[0] == 1: | ||
391 | kb1 |= (effect << pos[1] * 2) | ||
392 | else: | ||
393 | kb2 |= (effect << pos[1] * 2) | ||
394 | |||
395 | msgs = [] | ||
396 | |||
397 | if kb1 != 0: | ||
398 | msgs.append({ | ||
399 | "cmd": "Set", | ||
400 | "key": self.get_datastorage_key("keyboard1"), | ||
401 | "want_reply": True, | ||
402 | "operations": [{ | ||
403 | "operation": "or", | ||
404 | "value": kb1 | ||
405 | }] | ||
406 | }) | ||
407 | |||
408 | if kb2 != 0: | ||
409 | msgs.append({ | ||
410 | "cmd": "Set", | ||
411 | "key": self.get_datastorage_key("keyboard2"), | ||
412 | "want_reply": True, | ||
413 | "operations": [{ | ||
414 | "operation": "or", | ||
415 | "value": kb2 | ||
416 | }] | ||
417 | }) | ||
418 | |||
419 | if len(msgs) > 0: | ||
420 | await self.send_msgs(msgs) | ||
421 | |||
422 | def handle_keyboard_update(self, field: int, args: dict[str, Any]): | ||
423 | keys = {} | ||
424 | value = args["value"] | ||
425 | |||
426 | for i in range(0, 13): | ||
427 | if (value & (1 << (i * 2))) != 0: | ||
428 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1 | ||
429 | if (value & (1 << (i * 2 + 1))) != 0: | ||
430 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2 | ||
431 | |||
432 | updates = self.manager.update_keyboard(keys) | ||
433 | if len(updates) > 0: | ||
434 | self.manager.game_ctx.send_update_keyboard(updates) | ||
435 | |||
436 | async def update_worldports(self, updates: set[int]): | ||
437 | await self.send_msgs([{ | ||
438 | "cmd": "Set", | ||
439 | "key": self.get_datastorage_key("worldports"), | ||
440 | "want_reply": True, | ||
441 | "operations": [{ | ||
442 | "operation": "update", | ||
443 | "value": updates | ||
444 | }] | ||
445 | }]) | ||
446 | |||
447 | |||
448 | async def pipe_loop(manager: Lingo2Manager): | ||
449 | while not manager.client_ctx.exit_event.is_set(): | ||
235 | try: | 450 | try: |
236 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, | 451 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, |
237 | max_size=MESSAGE_MAX_SIZE) | 452 | max_size=MESSAGE_MAX_SIZE) |
238 | ctx.server = Endpoint(socket) | 453 | manager.game_ctx.server = Endpoint(socket) |
239 | logger.info("Connected to Lingo 2!") | 454 | logger.info("Connected to Lingo 2!") |
240 | if ctx.client.auth is not None: | 455 | if manager.client_ctx.auth is not None: |
241 | ctx.send_connected() | 456 | manager.game_ctx.send_connected() |
242 | ctx.send_accessible_locations() | 457 | manager.game_ctx.send_accessible_locations() |
243 | async for data in ctx.server.socket: | 458 | async for data in manager.game_ctx.server.socket: |
244 | for msg in decode(data): | 459 | for msg in decode(data): |
245 | await process_game_cmd(ctx, msg) | 460 | await process_game_cmd(manager, msg) |
246 | except ConnectionRefusedError: | 461 | except ConnectionRefusedError: |
247 | logger.info("Could not connect to Lingo 2.") | 462 | logger.info("Could not connect to Lingo 2.") |
248 | finally: | 463 | finally: |
249 | ctx.server = None | 464 | manager.game_ctx.server = None |
250 | 465 | ||
251 | 466 | ||
252 | async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | 467 | async def process_game_cmd(manager: Lingo2Manager, args: dict): |
253 | cmd = args["cmd"] | 468 | cmd = args["cmd"] |
254 | 469 | ||
255 | if cmd == "Connect": | 470 | if cmd == "Connect": |
@@ -262,13 +477,26 @@ async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | |||
262 | else: | 477 | else: |
263 | server_address = f"{player}:None@{server}" | 478 | server_address = f"{player}:None@{server}" |
264 | 479 | ||
265 | async_start(ctx.client.connect(server_address), name="client connect") | 480 | async_start(manager.client_ctx.connect(server_address), name="client connect") |
266 | elif cmd == "Disconnect": | 481 | elif cmd == "Disconnect": |
267 | async_start(ctx.client.disconnect(), name="client disconnect") | 482 | async_start(manager.client_ctx.disconnect(), name="client disconnect") |
268 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | 483 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: |
269 | async_start(ctx.client.send_msgs([args]), name="client forward") | 484 | async_start(manager.client_ctx.send_msgs([args]), name="client forward") |
485 | elif cmd == "UpdateKeyboard": | ||
486 | updates = manager.update_keyboard(args["keyboard"]) | ||
487 | if len(updates) > 0: | ||
488 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | ||
489 | elif cmd == "CheckWorldport": | ||
490 | port_id = args["port_id"] | ||
491 | worldports = {port_id} | ||
492 | if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: | ||
493 | worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) | ||
494 | |||
495 | updates = manager.update_worldports(worldports) | ||
496 | if len(updates) > 0: | ||
497 | async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") | ||
270 | elif cmd == "Quit": | 498 | elif cmd == "Quit": |
271 | ctx.client.exit_event.set() | 499 | manager.client_ctx.exit_event.set() |
272 | 500 | ||
273 | 501 | ||
274 | async def run_game(): | 502 | async def run_game(): |
@@ -318,9 +546,7 @@ def client_main(*launch_args: str) -> None: | |||
318 | 546 | ||
319 | client_ctx = Lingo2ClientContext(args.connect, args.password) | 547 | client_ctx = Lingo2ClientContext(args.connect, args.password) |
320 | game_ctx = Lingo2GameContext() | 548 | game_ctx = Lingo2GameContext() |
321 | 549 | manager = Lingo2Manager(game_ctx, client_ctx) | |
322 | client_ctx.game_ctx = game_ctx | ||
323 | game_ctx.client = client_ctx | ||
324 | 550 | ||
325 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") | 551 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") |
326 | 552 | ||
@@ -328,7 +554,7 @@ def client_main(*launch_args: str) -> None: | |||
328 | client_ctx.run_gui() | 554 | client_ctx.run_gui() |
329 | client_ctx.run_cli() | 555 | client_ctx.run_cli() |
330 | 556 | ||
331 | pipe_task = asyncio.create_task(pipe_loop(game_ctx), name="GameWatcher") | 557 | pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher") |
332 | 558 | ||
333 | try: | 559 | try: |
334 | await pipe_task | 560 | await pipe_task |
diff --git a/apworld/locations.py b/apworld/locations.py index 108decb..a502931 100644 --- a/apworld/locations.py +++ b/apworld/locations.py | |||
@@ -3,3 +3,5 @@ from BaseClasses import Location | |||
3 | 3 | ||
4 | class Lingo2Location(Location): | 4 | class Lingo2Location(Location): |
5 | game: str = "Lingo 2" | 5 | game: str = "Lingo 2" |
6 | |||
7 | port_id: int | ||
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 966f712..8f2bd59 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py | |||
@@ -297,13 +297,14 @@ class Lingo2PlayerLogic: | |||
297 | AccessRequirements())) | 297 | AccessRequirements())) |
298 | behavior = self.get_letter_behavior(letter.key, letter.level2) | 298 | behavior = self.get_letter_behavior(letter.key, letter.level2) |
299 | if behavior == LetterBehavior.VANILLA: | 299 | if behavior == LetterBehavior.VANILLA: |
300 | letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" | 300 | if not world.for_tracker: |
301 | event_name = f"{letter_name} (Collected)" | 301 | letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" |
302 | self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() | 302 | event_name = f"{letter_name} (Collected)" |
303 | |||
304 | if letter.level2: | ||
305 | event_name = f"{letter_name} (Double Collected)" | ||
306 | self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() | 303 | self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() |
304 | |||
305 | if letter.level2: | ||
306 | event_name = f"{letter_name} (Double Collected)" | ||
307 | self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() | ||
307 | elif behavior == LetterBehavior.ITEM: | 308 | elif behavior == LetterBehavior.ITEM: |
308 | self.real_items.append(letter.key.upper()) | 309 | self.real_items.append(letter.key.upper()) |
309 | 310 | ||
diff --git a/apworld/regions.py b/apworld/regions.py index a7d9a1c..9f44682 100644 --- a/apworld/regions.py +++ b/apworld/regions.py | |||
@@ -32,6 +32,22 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di | |||
32 | new_location.place_locked_item(event_item) | 32 | new_location.place_locked_item(event_item) |
33 | new_region.locations.append(new_location) | 33 | new_region.locations.append(new_location) |
34 | 34 | ||
35 | if world.for_tracker and world.options.shuffle_worldports: | ||
36 | for port_id in room.ports: | ||
37 | port = world.static_logic.objects.ports[port_id] | ||
38 | if port.no_shuffle: | ||
39 | continue | ||
40 | |||
41 | new_location = Lingo2Location(world.player, f"Worldport {port.id} Entered", None, new_region) | ||
42 | new_location.port_id = port.id | ||
43 | |||
44 | if port.HasField("required_door"): | ||
45 | new_location.access_rule = \ | ||
46 | make_location_lambda(world.player_logic.get_door_open_reqs(port.required_door), world, regions) | ||
47 | |||
48 | new_region.locations.append(new_location) | ||
49 | |||
50 | |||
35 | def create_regions(world: "Lingo2World"): | 51 | def create_regions(world: "Lingo2World"): |
36 | regions = { | 52 | regions = { |
37 | "Menu": Region("Menu", world.player, world.multiworld) | 53 | "Menu": Region("Menu", world.player, world.multiworld) |
@@ -52,7 +68,6 @@ def create_regions(world: "Lingo2World"): | |||
52 | 68 | ||
53 | regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") | 69 | regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") |
54 | 70 | ||
55 | # TODO: The requirements of the opposite trigger also matter. | ||
56 | for connection in world.static_logic.objects.connections: | 71 | for connection in world.static_logic.objects.connections: |
57 | if connection.roof_access and not world.options.daedalus_roof_access: | 72 | if connection.roof_access and not world.options.daedalus_roof_access: |
58 | continue | 73 | continue |
@@ -176,11 +191,17 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): | |||
176 | 191 | ||
177 | connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region) | 192 | connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region) |
178 | 193 | ||
194 | reqs = AccessRequirements() | ||
179 | if from_port.HasField("required_door"): | 195 | if from_port.HasField("required_door"): |
180 | door_reqs = world.player_logic.get_door_open_reqs(from_port.required_door) | 196 | reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy() |
181 | connection.access_rule = make_location_lambda(door_reqs, world, None) | ||
182 | 197 | ||
183 | for region in door_reqs.get_referenced_rooms(): | 198 | if world.for_tracker: |
199 | reqs.items.add(f"Worldport {fpid} Entered") | ||
200 | |||
201 | if not reqs.is_empty(): | ||
202 | connection.access_rule = make_location_lambda(reqs, world, None) | ||
203 | |||
204 | for region in reqs.get_referenced_rooms(): | ||
184 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), | 205 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), |
185 | connection) | 206 | connection) |
186 | 207 | ||
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index e4d7d49..ef70b58 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py | |||
@@ -2,6 +2,7 @@ from .generated import data_pb2 as data_pb2 | |||
2 | from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS | 2 | from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS |
3 | import pkgutil | 3 | import pkgutil |
4 | 4 | ||
5 | |||
5 | class Lingo2StaticLogic: | 6 | class Lingo2StaticLogic: |
6 | item_id_to_name: dict[int, str] | 7 | item_id_to_name: dict[int, str] |
7 | location_id_to_name: dict[int, str] | 8 | location_id_to_name: dict[int, str] |
diff --git a/apworld/tracker.py b/apworld/tracker.py index 721e9b3..cf2dbe1 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py | |||
@@ -1,41 +1,54 @@ | |||
1 | from typing import TYPE_CHECKING | ||
2 | |||
1 | from BaseClasses import MultiWorld, CollectionState, ItemClassification | 3 | from BaseClasses import MultiWorld, CollectionState, ItemClassification |
2 | from NetUtils import NetworkItem | 4 | from NetUtils import NetworkItem |
3 | from . import Lingo2World, Lingo2Item | 5 | from . import Lingo2World, Lingo2Item |
4 | from .regions import connect_ports_from_ut | 6 | from .regions import connect_ports_from_ut |
5 | from .options import Lingo2Options | 7 | from .options import Lingo2Options, ShuffleLetters |
8 | |||
9 | if TYPE_CHECKING: | ||
10 | from .context import Lingo2Manager | ||
6 | 11 | ||
7 | PLAYER_NUM = 1 | 12 | PLAYER_NUM = 1 |
8 | 13 | ||
9 | 14 | ||
10 | class Tracker: | 15 | class Tracker: |
16 | manager: "Lingo2Manager" | ||
17 | |||
11 | multiworld: MultiWorld | 18 | multiworld: MultiWorld |
19 | world: Lingo2World | ||
12 | 20 | ||
13 | collected_items: dict[int, int] | 21 | collected_items: dict[int, int] |
14 | checked_locations: set[int] | 22 | checked_locations: set[int] |
15 | accessible_locations: set[int] | 23 | accessible_locations: set[int] |
24 | accessible_worldports: set[int] | ||
16 | 25 | ||
17 | state: CollectionState | 26 | state: CollectionState |
18 | 27 | ||
19 | def __init__(self): | 28 | def __init__(self, manager: "Lingo2Manager"): |
29 | self.manager = manager | ||
20 | self.collected_items = {} | 30 | self.collected_items = {} |
21 | self.checked_locations = set() | 31 | self.checked_locations = set() |
22 | self.accessible_locations = set() | 32 | self.accessible_locations = set() |
33 | self.accessible_worldports = set() | ||
23 | 34 | ||
24 | def setup_slot(self, slot_data): | 35 | def setup_slot(self, slot_data): |
36 | Lingo2World.for_tracker = True | ||
37 | |||
25 | self.multiworld = MultiWorld(players=PLAYER_NUM) | 38 | self.multiworld = MultiWorld(players=PLAYER_NUM) |
26 | world = Lingo2World(self.multiworld, PLAYER_NUM) | 39 | self.world = Lingo2World(self.multiworld, PLAYER_NUM) |
27 | self.multiworld.worlds[1] = world | 40 | self.multiworld.worlds[1] = self.world |
28 | world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) | 41 | self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) |
29 | for k, t in Lingo2Options.type_hints.items()}) | 42 | for k, t in Lingo2Options.type_hints.items()}) |
30 | 43 | ||
31 | world.generate_early() | 44 | self.world.generate_early() |
32 | world.create_regions() | 45 | self.world.create_regions() |
33 | 46 | ||
34 | if world.options.shuffle_worldports: | 47 | if self.world.options.shuffle_worldports: |
35 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} | 48 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} |
36 | connect_ports_from_ut(port_pairings, world) | 49 | connect_ports_from_ut(port_pairings, self.world) |
37 | 50 | ||
38 | self.state = CollectionState(self.multiworld) | 51 | self.refresh_state() |
39 | 52 | ||
40 | def set_checked_locations(self, checked_locations: set[int]): | 53 | def set_checked_locations(self, checked_locations: set[int]): |
41 | self.checked_locations = checked_locations.copy() | 54 | self.checked_locations = checked_locations.copy() |
@@ -56,12 +69,38 @@ class Tracker: | |||
56 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), | 69 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), |
57 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) | 70 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) |
58 | 71 | ||
72 | for k, v in self.manager.keyboard.items(): | ||
73 | # Unless all level 1 letters are pre-unlocked, H1 I1 N1 and T1 act differently between the generator and | ||
74 | # game. The generator considers them to be unlocked, which means they are not included in logic | ||
75 | # requirements, and only one item/event is needed to unlock their level 2 forms. The game considers them to | ||
76 | # be vanilla, which means you still have to pick them up in the Starting Room in order for them to appear on | ||
77 | # your keyboard. This also means that whether or not you have the level 1 forms should be synced to the | ||
78 | # multiworld. The tracker specifically should collect one fewer item for these letters in this scenario. | ||
79 | tv = v | ||
80 | if k in "hint" and self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla, | ||
81 | ShuffleLetters.option_progressive]: | ||
82 | tv = max(0, v - 1) | ||
83 | |||
84 | if tv > 0: | ||
85 | for i in range(tv): | ||
86 | self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM), | ||
87 | prevent_sweep=True) | ||
88 | |||
89 | for port_id in self.manager.worldports: | ||
90 | self.state.collect(Lingo2Item(f"Worldport {port_id} Entered", ItemClassification.progression, None, | ||
91 | PLAYER_NUM), prevent_sweep=True) | ||
92 | |||
59 | self.state.sweep_for_advancements() | 93 | self.state.sweep_for_advancements() |
60 | 94 | ||
61 | self.accessible_locations = set() | 95 | self.accessible_locations = set() |
96 | self.accessible_worldports = set() | ||
62 | 97 | ||
63 | for region in self.state.reachable_regions[PLAYER_NUM]: | 98 | for region in self.state.reachable_regions[PLAYER_NUM]: |
64 | for location in region.locations: | 99 | for location in region.locations: |
65 | if location.address not in self.checked_locations and location.access_rule(self.state): | 100 | if location.access_rule(self.state): |
66 | if location.address is not None: | 101 | if location.address is not None: |
67 | self.accessible_locations.add(location.address) | 102 | if location.address not in self.checked_locations: |
103 | self.accessible_locations.add(location.address) | ||
104 | elif hasattr(location, "port_id"): | ||
105 | if location.port_id not in self.manager.worldports: | ||
106 | self.accessible_worldports.add(location.port_id) | ||