diff options
-rw-r--r-- | apworld/__init__.py | 2 | ||||
-rw-r--r-- | apworld/client/client.gd | 54 | ||||
-rw-r--r-- | apworld/client/gamedata.gd | 127 | ||||
-rw-r--r-- | apworld/client/keyboard.gd | 36 | ||||
-rw-r--r-- | apworld/client/main.gd | 1 | ||||
-rw-r--r-- | apworld/client/manager.gd | 25 | ||||
-rw-r--r-- | apworld/client/pauseMenu.gd | 40 | ||||
-rw-r--r-- | apworld/client/player.gd | 1 | ||||
-rw-r--r-- | apworld/client/textclient.gd | 123 | ||||
-rw-r--r-- | apworld/context.py | 407 | ||||
-rw-r--r-- | apworld/player_logic.py | 13 | ||||
-rw-r--r-- | apworld/static_logic.py | 1 | ||||
-rw-r--r-- | apworld/tracker.py | 95 |
13 files changed, 775 insertions, 150 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 67edf29..3d4096f 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd | |||
@@ -21,6 +21,7 @@ var _checked_locations = [] | |||
21 | var _received_indexes = [] | 21 | var _received_indexes = [] |
22 | var _received_items = {} | 22 | var _received_items = {} |
23 | var _slot_data = {} | 23 | var _slot_data = {} |
24 | var _accessible_locations = [] | ||
24 | 25 | ||
25 | signal could_not_connect | 26 | signal could_not_connect |
26 | signal connect_status | 27 | signal connect_status |
@@ -30,6 +31,9 @@ signal location_scout_received(location_id, item_name, player_name, flags, for_s | |||
30 | signal text_message_received(message) | 31 | signal text_message_received(message) |
31 | signal item_sent_notification(message) | 32 | signal item_sent_notification(message) |
32 | signal hint_received(message) | 33 | signal hint_received(message) |
34 | signal accessible_locations_updated | ||
35 | signal checked_locations_updated | ||
36 | signal keyboard_update_received | ||
33 | 37 | ||
34 | 38 | ||
35 | func _init(): | 39 | func _init(): |
@@ -51,6 +55,7 @@ func _reset_state(): | |||
51 | _should_process = false | 55 | _should_process = false |
52 | _received_items = {} | 56 | _received_items = {} |
53 | _received_indexes = [] | 57 | _received_indexes = [] |
58 | _accessible_locations = [] | ||
54 | 59 | ||
55 | 60 | ||
56 | func disconnect_from_ap(): | 61 | func disconnect_from_ap(): |
@@ -92,15 +97,26 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
92 | _gen_version = message["generator_version"] | 97 | _gen_version = message["generator_version"] |
93 | _team = message["team"] | 98 | _team = message["team"] |
94 | _slot = message["slot"] | 99 | _slot = message["slot"] |
95 | _checked_locations = message["checked_locations"] | ||
96 | _slot_data = message["slot_data"] | 100 | _slot_data = message["slot_data"] |
97 | 101 | ||
102 | _checked_locations = [] | ||
103 | for location in message["checked_locations"]: | ||
104 | _checked_locations.append(int(message["checked_locations"])) | ||
105 | |||
98 | client_connected.emit(_slot_data) | 106 | client_connected.emit(_slot_data) |
99 | 107 | ||
100 | elif cmd == "ConnectionRefused": | 108 | elif cmd == "ConnectionRefused": |
101 | could_not_connect.emit(message["text"]) | 109 | could_not_connect.emit(message["text"]) |
102 | global._print("Connection to AP refused") | 110 | global._print("Connection to AP refused") |
103 | 111 | ||
112 | elif cmd == "UpdateLocations": | ||
113 | for location in message["locations"]: | ||
114 | var lint = int(location) | ||
115 | if not _checked_locations.has(lint): | ||
116 | _checked_locations.append(lint) | ||
117 | |||
118 | checked_locations_updated.emit() | ||
119 | |||
104 | elif cmd == "ItemReceived": | 120 | elif cmd == "ItemReceived": |
105 | for item in message["items"]: | 121 | for item in message["items"]: |
106 | var index = int(item["index"]) | 122 | var index = int(item["index"]) |
@@ -134,6 +150,21 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
134 | int(loc["for_self"]) | 150 | int(loc["for_self"]) |
135 | ) | 151 | ) |
136 | 152 | ||
153 | elif cmd == "AccessibleLocations": | ||
154 | _accessible_locations.clear() | ||
155 | |||
156 | for loc in message["locations"]: | ||
157 | _accessible_locations.append(int(loc)) | ||
158 | |||
159 | accessible_locations_updated.emit() | ||
160 | |||
161 | elif cmd == "UpdateKeyboard": | ||
162 | var updates = {} | ||
163 | for k in message["updates"]: | ||
164 | updates[k] = int(message["updates"][k]) | ||
165 | |||
166 | keyboard_update_received.emit(updates) | ||
167 | |||
137 | 168 | ||
138 | func connectToServer(server, un, pw): | 169 | func connectToServer(server, un, pw): |
139 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) | 170 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) |
@@ -179,19 +210,6 @@ func sendLocations(loc_ids): | |||
179 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) | 210 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) |
180 | 211 | ||
181 | 212 | ||
182 | func setValue(key, value, operation = "replace"): | ||
183 | sendMessage( | ||
184 | [ | ||
185 | { | ||
186 | "cmd": "Set", | ||
187 | "key": "Lingo2_%d_%s" % [_slot, key], | ||
188 | "want_reply": false, | ||
189 | "operations": [{"operation": operation, "value": value}] | ||
190 | } | ||
191 | ] | ||
192 | ) | ||
193 | |||
194 | |||
195 | func say(textdata): | 213 | func say(textdata): |
196 | sendMessage([{"cmd": "Say", "text": textdata}]) | 214 | sendMessage([{"cmd": "Say", "text": textdata}]) |
197 | 215 | ||
@@ -204,6 +222,14 @@ func scoutLocations(loc_ids): | |||
204 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) | 222 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) |
205 | 223 | ||
206 | 224 | ||
225 | func updateKeyboard(updates): | ||
226 | sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}]) | ||
227 | |||
228 | |||
229 | func sendQuit(): | ||
230 | sendMessage([{"cmd": "Quit"}]) | ||
231 | |||
232 | |||
207 | func hasItem(item_id): | 233 | func hasItem(item_id): |
208 | return _received_items.has(item_id) | 234 | return _received_items.has(item_id) |
209 | 235 | ||
diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 9eeec3b..13ec568 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd | |||
@@ -13,6 +13,7 @@ var progressive_id_by_ap_id = {} | |||
13 | var letter_id_by_ap_id = {} | 13 | var letter_id_by_ap_id = {} |
14 | var symbol_item_ids = [] | 14 | var symbol_item_ids = [] |
15 | var anti_trap_ids = {} | 15 | var anti_trap_ids = {} |
16 | var location_name_by_id = {} | ||
16 | 17 | ||
17 | var kSYMBOL_ITEMS | 18 | var kSYMBOL_ITEMS |
18 | 19 | ||
@@ -70,6 +71,7 @@ func load(data_bytes): | |||
70 | 71 | ||
71 | if door.has_ap_id(): | 72 | if door.has_ap_id(): |
72 | door_id_by_ap_id[door.get_ap_id()] = door.get_id() | 73 | door_id_by_ap_id[door.get_ap_id()] = door.get_id() |
74 | location_name_by_id[door.get_ap_id()] = _get_door_location_name(door) | ||
73 | 75 | ||
74 | for painting in objects.get_paintings(): | 76 | for painting in objects.get_paintings(): |
75 | var room = objects.get_rooms()[painting.get_room_id()] | 77 | var room = objects.get_rooms()[painting.get_room_id()] |
@@ -95,6 +97,17 @@ func load(data_bytes): | |||
95 | 97 | ||
96 | for letter in objects.get_letters(): | 98 | for letter in objects.get_letters(): |
97 | letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id() | 99 | letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id() |
100 | location_name_by_id[letter.get_ap_id()] = _get_letter_location_name(letter) | ||
101 | |||
102 | for mastery in objects.get_masteries(): | ||
103 | location_name_by_id[mastery.get_ap_id()] = _get_mastery_location_name(mastery) | ||
104 | |||
105 | for ending in objects.get_endings(): | ||
106 | location_name_by_id[ending.get_ap_id()] = _get_ending_location_name(ending) | ||
107 | |||
108 | for keyholder in objects.get_keyholders(): | ||
109 | if keyholder.has_key(): | ||
110 | location_name_by_id[keyholder.get_ap_id()] = _get_keyholder_location_name(keyholder) | ||
98 | 111 | ||
99 | for panel in objects.get_panels(): | 112 | for panel in objects.get_panels(): |
100 | var room = objects.get_rooms()[panel.get_room_id()] | 113 | var room = objects.get_rooms()[panel.get_room_id()] |
@@ -148,12 +161,118 @@ func get_door_ap_id(door_id): | |||
148 | return null | 161 | return null |
149 | 162 | ||
150 | 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 | |||
151 | func get_door_receivers(door_id): | 171 | func get_door_receivers(door_id): |
152 | var door = objects.get_doors()[door_id] | 172 | var door = objects.get_doors()[door_id] |
153 | return door.get_receivers() | 173 | return door.get_receivers() |
154 | 174 | ||
155 | 175 | ||
156 | func get_door_map_name(door_id): | 176 | func _get_map_object_map_name(obj): |
157 | var door = objects.get_doors()[door_id] | 177 | return objects.get_maps()[obj.get_map_id()].get_display_name() |
158 | var map = objects.get_maps()[door.get_map_id()] | 178 | |
159 | return map.get_name() | 179 | |
180 | func _get_room_object_map_name(obj): | ||
181 | return _get_map_object_map_name(objects.get_rooms()[obj.get_room_id()]) | ||
182 | |||
183 | |||
184 | func _get_room_object_location_prefix(obj): | ||
185 | var room = objects.get_rooms()[obj.get_room_id()] | ||
186 | var game_map = objects.get_maps()[room.get_map_id()] | ||
187 | |||
188 | if room.has_panel_display_name(): | ||
189 | return "%s (%s)" % [game_map.get_display_name(), room.get_panel_display_name()] | ||
190 | else: | ||
191 | return game_map.get_display_name() | ||
192 | |||
193 | |||
194 | func _get_door_location_name(door): | ||
195 | var map_part = _get_room_object_location_prefix(door) | ||
196 | |||
197 | if door.has_location_name(): | ||
198 | return "%s - %s" % [map_part, door.get_location_name()] | ||
199 | |||
200 | var generated_location_name = _get_generated_door_location_name(door) | ||
201 | if generated_location_name != null: | ||
202 | return generated_location_name | ||
203 | |||
204 | return "%s - %s" % [map_part, door.get_name()] | ||
205 | |||
206 | |||
207 | func _get_generated_door_location_name(door): | ||
208 | if door.get_type() != SCRIPT_proto.DoorType.STANDARD: | ||
209 | return null | ||
210 | |||
211 | if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at(): | ||
212 | return null | ||
213 | |||
214 | if door.get_panels().size() > 4: | ||
215 | return null | ||
216 | |||
217 | var map_areas = [] | ||
218 | for panel_id in door.get_panels(): | ||
219 | var panel = objects.get_panels()[panel_id.get_panel()] | ||
220 | var panel_room = objects.get_rooms()[panel.get_room_id()] | ||
221 | # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas. | ||
222 | if not map_areas.has(panel_room.get_panel_display_name()): | ||
223 | map_areas.append(panel_room.get_panel_display_name()) | ||
224 | |||
225 | if map_areas.size() > 1: | ||
226 | return null | ||
227 | |||
228 | var game_map = objects.get_maps()[door.get_map_id()] | ||
229 | var map_area = map_areas[0] | ||
230 | var map_part | ||
231 | if map_area == "": | ||
232 | map_part = game_map.get_display_name() | ||
233 | else: | ||
234 | map_part = "%s (%s)" % [game_map.get_display_name(), map_area] | ||
235 | |||
236 | var panel_names = [] | ||
237 | for panel_id in door.get_panels(): | ||
238 | var panel_data = objects.get_panels()[panel_id.get_panel()] | ||
239 | var panel_name | ||
240 | if panel_data.has_display_name(): | ||
241 | panel_name = panel_data.get_display_name() | ||
242 | else: | ||
243 | panel_name = panel_data.get_name() | ||
244 | |||
245 | var location_part | ||
246 | if panel_id.has_answer(): | ||
247 | location_part = "%s/%s" % [panel_name, panel_id.get_answer().to_upper()] | ||
248 | else: | ||
249 | location_part = panel_name | ||
250 | |||
251 | panel_names.append(location_part) | ||
252 | |||
253 | panel_names.sort() | ||
254 | |||
255 | return map_part + " - " + ", ".join(panel_names) | ||
256 | |||
257 | |||
258 | func _get_letter_location_name(letter): | ||
259 | var letter_level = 2 if letter.get_level2() else 1 | ||
260 | var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level] | ||
261 | return "%s - %s" % [_get_room_object_map_name(letter), letter_name] | ||
262 | |||
263 | |||
264 | func _get_mastery_location_name(mastery): | ||
265 | return "%s - Mastery" % _get_room_object_map_name(mastery) | ||
266 | |||
267 | |||
268 | func _get_ending_location_name(ending): | ||
269 | return ( | ||
270 | "%s - %s Ending" % [_get_room_object_map_name(ending), ending.get_name().to_pascal_case()] | ||
271 | ) | ||
272 | |||
273 | |||
274 | func _get_keyholder_location_name(keyholder): | ||
275 | return ( | ||
276 | "%s - %s Keyholder" | ||
277 | % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()] | ||
278 | ) | ||
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/main.gd b/apworld/client/main.gd index 9d66358..8425d8c 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd | |||
@@ -137,6 +137,7 @@ func _connect_pressed(): | |||
137 | func _back_pressed(): | 137 | func _back_pressed(): |
138 | var ap = global.get_node("Archipelago") | 138 | var ap = global.get_node("Archipelago") |
139 | ap.disconnect_from_ap() | 139 | ap.disconnect_from_ap() |
140 | ap.client.sendQuit() | ||
140 | 141 | ||
141 | get_tree().quit() | 142 | get_tree().quit() |
142 | 143 | ||
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 955d470..afa3ebe 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd | |||
@@ -14,6 +14,8 @@ var ap_user = "" | |||
14 | var ap_pass = "" | 14 | 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 | ||
18 | var show_minimap = false | ||
17 | 19 | ||
18 | var client | 20 | var client |
19 | var keyboard | 21 | var keyboard |
@@ -89,6 +91,12 @@ func _init(): | |||
89 | if data.size() > 4: | 91 | if data.size() > 4: |
90 | show_compass = data[4] | 92 | show_compass = data[4] |
91 | 93 | ||
94 | if data.size() > 5: | ||
95 | show_locations = data[5] | ||
96 | |||
97 | if data.size() > 6: | ||
98 | show_minimap = data[6] | ||
99 | |||
92 | 100 | ||
93 | func _ready(): | 101 | func _ready(): |
94 | client = SCRIPT_client.new() | 102 | client = SCRIPT_client.new() |
@@ -99,6 +107,8 @@ func _ready(): | |||
99 | client.text_message_received.connect(_process_text_message) | 107 | client.text_message_received.connect(_process_text_message) |
100 | client.item_sent_notification.connect(_process_item_sent_notification) | 108 | client.item_sent_notification.connect(_process_item_sent_notification) |
101 | client.hint_received.connect(_process_hint_received) | 109 | client.hint_received.connect(_process_hint_received) |
110 | client.accessible_locations_updated.connect(_on_accessible_locations_updated) | ||
111 | client.checked_locations_updated.connect(_on_checked_locations_updated) | ||
102 | 112 | ||
103 | client.could_not_connect.connect(_client_could_not_connect) | 113 | client.could_not_connect.connect(_client_could_not_connect) |
104 | client.connect_status.connect(_client_connect_status) | 114 | client.connect_status.connect(_client_connect_status) |
@@ -108,6 +118,7 @@ func _ready(): | |||
108 | 118 | ||
109 | keyboard = SCRIPT_keyboard.new() | 119 | keyboard = SCRIPT_keyboard.new() |
110 | add_child(keyboard) | 120 | add_child(keyboard) |
121 | client.keyboard_update_received.connect(keyboard.remote_keyboard_updated) | ||
111 | 122 | ||
112 | 123 | ||
113 | func saveSettings(): | 124 | func saveSettings(): |
@@ -121,6 +132,8 @@ func saveSettings(): | |||
121 | ap_pass, | 132 | ap_pass, |
122 | connection_history, | 133 | connection_history, |
123 | show_compass, | 134 | show_compass, |
135 | show_locations, | ||
136 | show_minimap, | ||
124 | ] | 137 | ] |
125 | file.store_var(data, true) | 138 | file.store_var(data, true) |
126 | file.close() | 139 | file.close() |
@@ -302,6 +315,18 @@ func _process_location_scout(location_id, item_name, player_name, flags, for_sel | |||
302 | collectable.setScoutedText(item_name) | 315 | collectable.setScoutedText(item_name) |
303 | 316 | ||
304 | 317 | ||
318 | func _on_accessible_locations_updated(): | ||
319 | var textclient_node = global.get_node("Textclient") | ||
320 | if textclient_node != null: | ||
321 | textclient_node.update_locations() | ||
322 | |||
323 | |||
324 | func _on_checked_locations_updated(): | ||
325 | var textclient_node = global.get_node("Textclient") | ||
326 | if textclient_node != null: | ||
327 | textclient_node.update_locations() | ||
328 | |||
329 | |||
305 | func _client_could_not_connect(message): | 330 | func _client_could_not_connect(message): |
306 | could_not_connect.emit(message) | 331 | could_not_connect.emit(message) |
307 | 332 | ||
diff --git a/apworld/client/pauseMenu.gd b/apworld/client/pauseMenu.gd index 8bc12c6..72b45e8 100644 --- a/apworld/client/pauseMenu.gd +++ b/apworld/client/pauseMenu.gd | |||
@@ -1,6 +1,8 @@ | |||
1 | extends "res://scripts/ui/pauseMenu.gd" | 1 | extends "res://scripts/ui/pauseMenu.gd" |
2 | 2 | ||
3 | var compass_button | 3 | var compass_button |
4 | var locations_button | ||
5 | var minimap_button | ||
4 | 6 | ||
5 | 7 | ||
6 | func _ready(): | 8 | func _ready(): |
@@ -19,6 +21,24 @@ func _ready(): | |||
19 | compass_button.pressed.connect(_toggle_compass) | 21 | compass_button.pressed.connect(_toggle_compass) |
20 | ap_panel.add_child(compass_button) | 22 | ap_panel.add_child(compass_button) |
21 | 23 | ||
24 | locations_button = CheckBox.new() | ||
25 | locations_button.text = "show locations overlay" | ||
26 | locations_button.button_pressed = ap.show_locations | ||
27 | locations_button.position = Vector2(65, 200) | ||
28 | locations_button.theme = preload("res://assets/themes/baseUI.tres") | ||
29 | locations_button.add_theme_font_size_override("font_size", 60) | ||
30 | locations_button.pressed.connect(_toggle_locations) | ||
31 | ap_panel.add_child(locations_button) | ||
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 | |||
22 | super._ready() | 42 | super._ready() |
23 | 43 | ||
24 | 44 | ||
@@ -32,6 +52,7 @@ func _main_menu(): | |||
32 | global.get_node("Archipelago").disconnect_from_ap() | 52 | global.get_node("Archipelago").disconnect_from_ap() |
33 | global.get_node("Messages").clear() | 53 | global.get_node("Messages").clear() |
34 | global.get_node("Compass").visible = false | 54 | global.get_node("Compass").visible = false |
55 | global.get_node("Textclient").reset() | ||
35 | 56 | ||
36 | autosplitter.reset() | 57 | autosplitter.reset() |
37 | _unpause_game() | 58 | _unpause_game() |
@@ -49,3 +70,22 @@ func _toggle_compass(): | |||
49 | 70 | ||
50 | var compass = global.get_node("Compass") | 71 | var compass = global.get_node("Compass") |
51 | compass.visible = compass_button.button_pressed | 72 | compass.visible = compass_button.button_pressed |
73 | |||
74 | |||
75 | func _toggle_locations(): | ||
76 | var ap = global.get_node("Archipelago") | ||
77 | ap.show_locations = locations_button.button_pressed | ||
78 | ap.saveSettings() | ||
79 | |||
80 | var textclient = global.get_node("Textclient") | ||
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 9841063..1b36c29 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd | |||
@@ -1,35 +1,57 @@ | |||
1 | extends CanvasLayer | 1 | extends CanvasLayer |
2 | 2 | ||
3 | var tabs | ||
3 | var panel | 4 | var panel |
4 | var label | 5 | var label |
5 | var entry | 6 | var entry |
7 | var tracker_label | ||
6 | var is_open = false | 8 | var is_open = false |
7 | 9 | ||
10 | var locations_overlay | ||
11 | |||
8 | 12 | ||
9 | func _ready(): | 13 | func _ready(): |
10 | process_mode = ProcessMode.PROCESS_MODE_ALWAYS | 14 | process_mode = ProcessMode.PROCESS_MODE_ALWAYS |
11 | layer = 2 | 15 | layer = 2 |
12 | 16 | ||
13 | panel = Panel.new() | 17 | locations_overlay = RichTextLabel.new() |
14 | panel.set_name("Panel") | 18 | locations_overlay.name = "LocationsOverlay" |
15 | panel.offset_left = 100 | 19 | locations_overlay.offset_top = 220 |
16 | panel.offset_right = 1820 | 20 | locations_overlay.offset_bottom = 720 |
17 | panel.offset_top = 100 | 21 | locations_overlay.offset_left = 20 |
18 | panel.offset_bottom = 980 | 22 | locations_overlay.anchor_right = 1.0 |
19 | panel.visible = false | 23 | locations_overlay.offset_right = -20 |
20 | add_child(panel) | 24 | locations_overlay.scroll_active = false |
25 | locations_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE | ||
26 | add_child(locations_overlay) | ||
27 | update_locations_visibility() | ||
28 | |||
29 | tabs = TabContainer.new() | ||
30 | tabs.name = "Tabs" | ||
31 | tabs.offset_left = 100 | ||
32 | tabs.offset_right = 1820 | ||
33 | tabs.offset_top = 100 | ||
34 | tabs.offset_bottom = 980 | ||
35 | tabs.visible = false | ||
36 | tabs.theme = preload("res://assets/themes/baseUI.tres") | ||
37 | tabs.add_theme_font_size_override("font_size", 36) | ||
38 | add_child(tabs) | ||
39 | |||
40 | panel = MarginContainer.new() | ||
41 | panel.name = "Text Client" | ||
42 | panel.add_theme_constant_override("margin_top", 60) | ||
43 | panel.add_theme_constant_override("margin_left", 60) | ||
44 | panel.add_theme_constant_override("margin_right", 60) | ||
45 | panel.add_theme_constant_override("margin_bottom", 60) | ||
46 | tabs.add_child(panel) | ||
21 | 47 | ||
22 | label = RichTextLabel.new() | 48 | label = RichTextLabel.new() |
23 | label.set_name("Label") | 49 | label.set_name("Label") |
24 | label.offset_left = 80 | ||
25 | label.offset_right = 1640 | ||
26 | label.offset_top = 80 | ||
27 | label.offset_bottom = 720 | ||
28 | label.scroll_following = true | 50 | label.scroll_following = true |
29 | label.selection_enabled = true | 51 | label.selection_enabled = true |
30 | panel.add_child(label) | 52 | label.size_flags_horizontal = Control.SIZE_EXPAND_FILL |
31 | 53 | label.size_flags_vertical = Control.SIZE_EXPAND_FILL | |
32 | label.push_font(load("res://assets/fonts/Lingo2.ttf")) | 54 | label.push_font(preload("res://assets/fonts/Lingo2.ttf")) |
33 | label.push_font_size(36) | 55 | label.push_font_size(36) |
34 | 56 | ||
35 | var entry_style = StyleBoxFlat.new() | 57 | var entry_style = StyleBoxFlat.new() |
@@ -37,18 +59,30 @@ func _ready(): | |||
37 | 59 | ||
38 | entry = LineEdit.new() | 60 | entry = LineEdit.new() |
39 | entry.set_name("Entry") | 61 | entry.set_name("Entry") |
40 | entry.offset_left = 80 | 62 | entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf")) |
41 | entry.offset_right = 1640 | ||
42 | entry.offset_top = 760 | ||
43 | entry.offset_bottom = 840 | ||
44 | entry.add_theme_font_override("font", load("res://assets/fonts/Lingo2.ttf")) | ||
45 | entry.add_theme_font_size_override("font_size", 36) | 63 | entry.add_theme_font_size_override("font_size", 36) |
46 | entry.add_theme_color_override("font_color", Color(0, 0, 0, 1)) | 64 | entry.add_theme_color_override("font_color", Color(0, 0, 0, 1)) |
47 | entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1)) | 65 | entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1)) |
48 | entry.add_theme_stylebox_override("focus", entry_style) | 66 | entry.add_theme_stylebox_override("focus", entry_style) |
49 | panel.add_child(entry) | ||
50 | entry.text_submitted.connect(text_entered) | 67 | entry.text_submitted.connect(text_entered) |
51 | 68 | ||
69 | var tc_arranger = VBoxContainer.new() | ||
70 | tc_arranger.add_child(label) | ||
71 | tc_arranger.add_child(entry) | ||
72 | tc_arranger.add_theme_constant_override("separation", 40) | ||
73 | panel.add_child(tc_arranger) | ||
74 | |||
75 | var tracker_margins = MarginContainer.new() | ||
76 | tracker_margins.name = "Locations" | ||
77 | tracker_margins.add_theme_constant_override("margin_top", 60) | ||
78 | tracker_margins.add_theme_constant_override("margin_left", 60) | ||
79 | tracker_margins.add_theme_constant_override("margin_right", 60) | ||
80 | tracker_margins.add_theme_constant_override("margin_bottom", 60) | ||
81 | tabs.add_child(tracker_margins) | ||
82 | |||
83 | tracker_label = RichTextLabel.new() | ||
84 | tracker_margins.add_child(tracker_label) | ||
85 | |||
52 | 86 | ||
53 | func _input(event): | 87 | func _input(event): |
54 | if global.loaded and event is InputEventKey and event.pressed: | 88 | if global.loaded and event is InputEventKey and event.pressed: |
@@ -57,7 +91,7 @@ func _input(event): | |||
57 | is_open = true | 91 | is_open = true |
58 | get_tree().paused = true | 92 | get_tree().paused = true |
59 | Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) | 93 | Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) |
60 | panel.visible = true | 94 | tabs.visible = true |
61 | entry.grab_focus() | 95 | entry.grab_focus() |
62 | get_viewport().set_input_as_handled() | 96 | get_viewport().set_input_as_handled() |
63 | else: | 97 | else: |
@@ -72,7 +106,7 @@ func dismiss(): | |||
72 | if is_open: | 106 | if is_open: |
73 | get_tree().paused = false | 107 | get_tree().paused = false |
74 | Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) | 108 | Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) |
75 | panel.visible = false | 109 | tabs.visible = false |
76 | is_open = false | 110 | is_open = false |
77 | 111 | ||
78 | 112 | ||
@@ -93,3 +127,46 @@ func text_entered(text): | |||
93 | return | 127 | return |
94 | 128 | ||
95 | ap.client.say(cmd) | 129 | ap.client.say(cmd) |
130 | |||
131 | |||
132 | func update_locations(): | ||
133 | var ap = global.get_node("Archipelago") | ||
134 | var gamedata = global.get_node("Gamedata") | ||
135 | |||
136 | tracker_label.clear() | ||
137 | tracker_label.push_font(preload("res://assets/fonts/Lingo2.ttf")) | ||
138 | tracker_label.push_font_size(24) | ||
139 | |||
140 | locations_overlay.clear() | ||
141 | locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf")) | ||
142 | locations_overlay.push_font_size(24) | ||
143 | locations_overlay.push_color(Color(0.9, 0.9, 0.9, 1)) | ||
144 | locations_overlay.push_outline_color(Color(0, 0, 0, 1)) | ||
145 | locations_overlay.push_outline_size(2) | ||
146 | |||
147 | var location_names = [] | ||
148 | for location_id in ap.client._accessible_locations: | ||
149 | if not ap.client._checked_locations.has(location_id): | ||
150 | var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") | ||
151 | location_names.append(location_name) | ||
152 | |||
153 | location_names.sort() | ||
154 | |||
155 | var count = 0 | ||
156 | for location_name in location_names: | ||
157 | tracker_label.append_text("[p]%s[/p]" % location_name) | ||
158 | if count < 18: | ||
159 | locations_overlay.append_text("[p align=right]%s[/p]" % location_name) | ||
160 | count += 1 | ||
161 | |||
162 | if count > 18: | ||
163 | locations_overlay.append_text("[p align=right][lb]...[rb][/p]") | ||
164 | |||
165 | |||
166 | func update_locations_visibility(): | ||
167 | var ap = global.get_node("Archipelago") | ||
168 | locations_overlay.visible = ap.show_locations | ||
169 | |||
170 | |||
171 | func reset(): | ||
172 | locations_overlay.clear() | ||
diff --git a/apworld/context.py b/apworld/context.py index 05f75a3..bc3b1bf 100644 --- a/apworld/context.py +++ b/apworld/context.py | |||
@@ -8,37 +8,92 @@ import websockets | |||
8 | 8 | ||
9 | import Utils | 9 | import Utils |
10 | import settings | 10 | import settings |
11 | from BaseClasses import ItemClassification | ||
11 | from CommonClient import CommonContext, server_loop, gui_enabled, logger, get_base_parser, handle_url_arg | 12 | from CommonClient import CommonContext, server_loop, gui_enabled, logger, get_base_parser, handle_url_arg |
12 | from NetUtils import Endpoint, decode, encode | 13 | from NetUtils import Endpoint, decode, encode |
13 | from Utils import async_start | 14 | from Utils import async_start |
15 | from . import Lingo2World | ||
16 | from .tracker import Tracker | ||
14 | 17 | ||
15 | PORT = 43182 | 18 | ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz" |
16 | 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 | |||
39 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | ||
40 | self.game_ctx = game_ctx | ||
41 | self.game_ctx.manager = self | ||
42 | self.client_ctx = client_ctx | ||
43 | self.client_ctx.manager = self | ||
44 | self.tracker = Tracker(self) | ||
45 | self.keyboard = {} | ||
46 | |||
47 | self.reset() | ||
48 | |||
49 | def reset(self): | ||
50 | for k in ALL_LETTERS: | ||
51 | self.keyboard[k] = 0 | ||
52 | |||
53 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | ||
54 | ret: dict[str, int] = {} | ||
55 | |||
56 | for k, v in new_keyboard.items(): | ||
57 | if v > self.keyboard.get(k, 0): | ||
58 | self.keyboard[k] = v | ||
59 | ret[k] = v | ||
60 | |||
61 | if len(ret) > 0: | ||
62 | self.tracker.refresh_state() | ||
63 | self.game_ctx.send_accessible_locations() | ||
64 | |||
65 | return ret | ||
17 | 66 | ||
18 | 67 | ||
19 | class Lingo2GameContext: | 68 | class Lingo2GameContext: |
20 | server: Endpoint | None | 69 | server: Endpoint | None |
21 | client: "Lingo2ClientContext" | 70 | manager: Lingo2Manager |
22 | 71 | ||
23 | def __init__(self): | 72 | def __init__(self): |
24 | self.server = None | 73 | self.server = None |
25 | 74 | ||
26 | def send_connected(self): | 75 | def send_connected(self): |
76 | if self.server is None: | ||
77 | return | ||
78 | |||
27 | msg = { | 79 | msg = { |
28 | "cmd": "Connected", | 80 | "cmd": "Connected", |
29 | "user": self.client.username, | 81 | "user": self.manager.client_ctx.username, |
30 | "seed_name": self.client.seed_name, | 82 | "seed_name": self.manager.client_ctx.seed_name, |
31 | "version": self.client.server_version, | 83 | "version": self.manager.client_ctx.server_version, |
32 | "generator_version": self.client.generator_version, | 84 | "generator_version": self.manager.client_ctx.generator_version, |
33 | "team": self.client.team, | 85 | "team": self.manager.client_ctx.team, |
34 | "slot": self.client.slot, | 86 | "slot": self.manager.client_ctx.slot, |
35 | "checked_locations": self.client.checked_locations, | 87 | "checked_locations": self.manager.client_ctx.checked_locations, |
36 | "slot_data": self.client.slot_data, | 88 | "slot_data": self.manager.client_ctx.slot_data, |
37 | } | 89 | } |
38 | 90 | ||
39 | async_start(self.send_msgs([msg]), name="game Connected") | 91 | async_start(self.send_msgs([msg]), name="game Connected") |
40 | 92 | ||
41 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): | 93 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): |
94 | if self.server is None: | ||
95 | return | ||
96 | |||
42 | msg = { | 97 | msg = { |
43 | "cmd": "ItemSentNotif", | 98 | "cmd": "ItemSentNotif", |
44 | "item_name": item_name, | 99 | "item_name": item_name, |
@@ -49,6 +104,9 @@ class Lingo2GameContext: | |||
49 | async_start(self.send_msgs([msg]), name="item sent notif") | 104 | async_start(self.send_msgs([msg]), name="item sent notif") |
50 | 105 | ||
51 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): | 106 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): |
107 | if self.server is None: | ||
108 | return | ||
109 | |||
52 | msg = { | 110 | msg = { |
53 | "cmd": "HintReceived", | 111 | "cmd": "HintReceived", |
54 | "item_name": item_name, | 112 | "item_name": item_name, |
@@ -61,6 +119,9 @@ class Lingo2GameContext: | |||
61 | async_start(self.send_msgs([msg]), name="hint received notif") | 119 | async_start(self.send_msgs([msg]), name="hint received notif") |
62 | 120 | ||
63 | def send_item_received(self, items): | 121 | def send_item_received(self, items): |
122 | if self.server is None: | ||
123 | return | ||
124 | |||
64 | msg = { | 125 | msg = { |
65 | "cmd": "ItemReceived", | 126 | "cmd": "ItemReceived", |
66 | "items": items, | 127 | "items": items, |
@@ -69,6 +130,9 @@ class Lingo2GameContext: | |||
69 | async_start(self.send_msgs([msg]), name="item received") | 130 | async_start(self.send_msgs([msg]), name="item received") |
70 | 131 | ||
71 | def send_location_info(self, locations): | 132 | def send_location_info(self, locations): |
133 | if self.server is None: | ||
134 | return | ||
135 | |||
72 | msg = { | 136 | msg = { |
73 | "cmd": "LocationInfo", | 137 | "cmd": "LocationInfo", |
74 | "locations": locations, | 138 | "locations": locations, |
@@ -77,6 +141,9 @@ class Lingo2GameContext: | |||
77 | async_start(self.send_msgs([msg]), name="location info") | 141 | async_start(self.send_msgs([msg]), name="location info") |
78 | 142 | ||
79 | def send_text_message(self, parts): | 143 | def send_text_message(self, parts): |
144 | if self.server is None: | ||
145 | return | ||
146 | |||
80 | msg = { | 147 | msg = { |
81 | "cmd": "TextMessage", | 148 | "cmd": "TextMessage", |
82 | "data": parts, | 149 | "data": parts, |
@@ -84,6 +151,39 @@ class Lingo2GameContext: | |||
84 | 151 | ||
85 | async_start(self.send_msgs([msg]), name="notif") | 152 | async_start(self.send_msgs([msg]), name="notif") |
86 | 153 | ||
154 | def send_accessible_locations(self): | ||
155 | if self.server is None: | ||
156 | return | ||
157 | |||
158 | msg = { | ||
159 | "cmd": "AccessibleLocations", | ||
160 | "locations": list(self.manager.tracker.accessible_locations), | ||
161 | } | ||
162 | |||
163 | async_start(self.send_msgs([msg]), name="accessible locations") | ||
164 | |||
165 | def send_update_locations(self, locations): | ||
166 | if self.server is None: | ||
167 | return | ||
168 | |||
169 | msg = { | ||
170 | "cmd": "UpdateLocations", | ||
171 | "locations": locations, | ||
172 | } | ||
173 | |||
174 | async_start(self.send_msgs([msg]), name="update locations") | ||
175 | |||
176 | def send_update_keyboard(self, updates): | ||
177 | if self.server is None: | ||
178 | return | ||
179 | |||
180 | msg = { | ||
181 | "cmd": "UpdateKeyboard", | ||
182 | "updates": updates, | ||
183 | } | ||
184 | |||
185 | async_start(self.send_msgs([msg]), name="update keyboard") | ||
186 | |||
87 | async def send_msgs(self, msgs: list[Any]) -> None: | 187 | async def send_msgs(self, msgs: list[Any]) -> None: |
88 | """ `msgs` JSON serializable """ | 188 | """ `msgs` JSON serializable """ |
89 | if not self.server or not self.server.socket.open or self.server.socket.closed: | 189 | if not self.server or not self.server.socket.open or self.server.socket.closed: |
@@ -92,7 +192,7 @@ class Lingo2GameContext: | |||
92 | 192 | ||
93 | 193 | ||
94 | class Lingo2ClientContext(CommonContext): | 194 | class Lingo2ClientContext(CommonContext): |
95 | game_ctx: Lingo2GameContext | 195 | manager: Lingo2Manager |
96 | 196 | ||
97 | game = "Lingo 2" | 197 | game = "Lingo 2" |
98 | items_handling = 0b111 | 198 | items_handling = 0b111 |
@@ -117,104 +217,201 @@ class Lingo2ClientContext(CommonContext): | |||
117 | elif cmd == "Connected": | 217 | elif cmd == "Connected": |
118 | self.slot_data = args.get("slot_data", None) | 218 | self.slot_data = args.get("slot_data", None) |
119 | 219 | ||
120 | if self.game_ctx.server is not None: | 220 | self.manager.reset() |
121 | self.game_ctx.send_connected() | 221 | |
222 | self.manager.game_ctx.send_connected() | ||
223 | |||
224 | self.manager.tracker.setup_slot(self.slot_data) | ||
225 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
226 | self.manager.game_ctx.send_accessible_locations() | ||
227 | |||
228 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2")) | ||
229 | async_start(self.send_msgs([{ | ||
230 | "cmd": "Set", | ||
231 | "key": self.get_datastorage_key("keyboard1"), | ||
232 | "default": 0, | ||
233 | "want_reply": True, | ||
234 | "operations": [{"operation": "default", "value": 0}] | ||
235 | }, { | ||
236 | "cmd": "Set", | ||
237 | "key": self.get_datastorage_key("keyboard2"), | ||
238 | "default": 0, | ||
239 | "want_reply": True, | ||
240 | "operations": [{"operation": "default", "value": 0}] | ||
241 | }]), name="default keys") | ||
242 | elif cmd == "RoomUpdate": | ||
243 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
244 | self.manager.game_ctx.send_update_locations(args["checked_locations"]) | ||
122 | elif cmd == "ReceivedItems": | 245 | elif cmd == "ReceivedItems": |
123 | if self.game_ctx.server is not None: | 246 | self.manager.tracker.set_collected_items(self.items_received) |
124 | cur_index = 0 | ||
125 | items = [] | ||
126 | 247 | ||
127 | for item in args["items"]: | 248 | cur_index = 0 |
128 | index = cur_index + args["index"] | 249 | items = [] |
129 | cur_index += 1 | ||
130 | 250 | ||
131 | item_msg = { | 251 | for item in args["items"]: |
132 | "id": item.item, | 252 | index = cur_index + args["index"] |
133 | "index": index, | 253 | cur_index += 1 |
134 | "flags": item.flags, | ||
135 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | ||
136 | } | ||
137 | 254 | ||
138 | if item.player != self.slot: | 255 | item_msg = { |
139 | item_msg["sender"] = self.player_names.get(item.player) | 256 | "id": item.item, |
257 | "index": index, | ||
258 | "flags": item.flags, | ||
259 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | ||
260 | } | ||
140 | 261 | ||
141 | items.append(item_msg) | 262 | if item.player != self.slot: |
263 | item_msg["sender"] = self.player_names.get(item.player) | ||
142 | 264 | ||
143 | self.game_ctx.send_item_received(items) | 265 | items.append(item_msg) |
144 | elif cmd == "PrintJSON": | ||
145 | if self.game_ctx.server is not None: | ||
146 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | ||
147 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | ||
148 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | ||
149 | receiver_name = self.player_names.get(args["receiving"]) | ||
150 | |||
151 | if args["type"] == "Hint" and not args.get("found", False): | ||
152 | self.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | ||
153 | int(args["receiving"]) == self.slot) | ||
154 | elif args["receiving"] != self.slot: | ||
155 | self.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | ||
156 | |||
157 | parts = [] | ||
158 | for message_part in args["data"]: | ||
159 | if "type" not in message_part and "text" in message_part: | ||
160 | parts.append({"type": "text", "text": message_part["text"]}) | ||
161 | elif message_part["type"] == "player_id": | ||
162 | parts.append({ | ||
163 | "type": "player", | ||
164 | "text": self.player_names.get(int(message_part["text"])), | ||
165 | "self": int(int(message_part["text"]) == self.slot), | ||
166 | }) | ||
167 | elif message_part["type"] == "item_id": | ||
168 | parts.append({ | ||
169 | "type": "item", | ||
170 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
171 | "flags": message_part["flags"], | ||
172 | }) | ||
173 | elif message_part["type"] == "location_id": | ||
174 | parts.append({ | ||
175 | "type": "location", | ||
176 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
177 | message_part["player"]) | ||
178 | }) | ||
179 | elif "text" in message_part: | ||
180 | parts.append({"type": "text", "text": message_part["text"]}) | ||
181 | |||
182 | self.game_ctx.send_text_message(parts) | ||
183 | elif cmd == "LocationInfo": | ||
184 | if self.game_ctx.server is not None: | ||
185 | locations = [] | ||
186 | |||
187 | for location in args["locations"]: | ||
188 | locations.append({ | ||
189 | "id": location.location, | ||
190 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
191 | "player": self.player_names.get(location.player), | ||
192 | "flags": location.flags, | ||
193 | "self": int(location.player) == self.slot, | ||
194 | }) | ||
195 | 266 | ||
196 | self.game_ctx.send_location_info(locations) | 267 | self.manager.game_ctx.send_item_received(items) |
197 | 268 | ||
269 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): | ||
270 | self.manager.game_ctx.send_accessible_locations() | ||
271 | elif cmd == "PrintJSON": | ||
272 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | ||
273 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | ||
274 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | ||
275 | receiver_name = self.player_names.get(args["receiving"]) | ||
276 | |||
277 | if args["type"] == "Hint" and not args.get("found", False): | ||
278 | self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | ||
279 | int(args["receiving"]) == self.slot) | ||
280 | elif args["receiving"] != self.slot: | ||
281 | self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | ||
282 | |||
283 | parts = [] | ||
284 | for message_part in args["data"]: | ||
285 | if "type" not in message_part and "text" in message_part: | ||
286 | parts.append({"type": "text", "text": message_part["text"]}) | ||
287 | elif message_part["type"] == "player_id": | ||
288 | parts.append({ | ||
289 | "type": "player", | ||
290 | "text": self.player_names.get(int(message_part["text"])), | ||
291 | "self": int(int(message_part["text"]) == self.slot), | ||
292 | }) | ||
293 | elif message_part["type"] == "item_id": | ||
294 | parts.append({ | ||
295 | "type": "item", | ||
296 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
297 | "flags": message_part["flags"], | ||
298 | }) | ||
299 | elif message_part["type"] == "location_id": | ||
300 | parts.append({ | ||
301 | "type": "location", | ||
302 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
303 | message_part["player"]) | ||
304 | }) | ||
305 | elif "text" in message_part: | ||
306 | parts.append({"type": "text", "text": message_part["text"]}) | ||
198 | 307 | ||
199 | async def pipe_loop(ctx: Lingo2GameContext): | 308 | self.manager.game_ctx.send_text_message(parts) |
200 | while not ctx.client.exit_event.is_set(): | 309 | elif cmd == "LocationInfo": |
310 | locations = [] | ||
311 | |||
312 | for location in args["locations"]: | ||
313 | locations.append({ | ||
314 | "id": location.location, | ||
315 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
316 | "player": self.player_names.get(location.player), | ||
317 | "flags": location.flags, | ||
318 | "self": int(location.player) == self.slot, | ||
319 | }) | ||
320 | |||
321 | self.manager.game_ctx.send_location_info(locations) | ||
322 | elif cmd == "SetReply": | ||
323 | if args["key"] == self.get_datastorage_key("keyboard1"): | ||
324 | self.handle_keyboard_update(1, args) | ||
325 | elif args["key"] == self.get_datastorage_key("keyboard2"): | ||
326 | self.handle_keyboard_update(2, args) | ||
327 | |||
328 | def get_datastorage_key(self, name: str): | ||
329 | return f"Lingo2_{self.slot}_{name}" | ||
330 | |||
331 | async def update_keyboard(self, updates: dict[str, int]): | ||
332 | kb1 = 0 | ||
333 | kb2 = 0 | ||
334 | |||
335 | for k, v in updates.items(): | ||
336 | if v == 0: | ||
337 | continue | ||
338 | |||
339 | effect = 0 | ||
340 | if v >= 1: | ||
341 | effect |= 1 | ||
342 | if v == 2: | ||
343 | effect |= 2 | ||
344 | |||
345 | pos = KEY_STORAGE_MAPPING[k] | ||
346 | if pos[0] == 1: | ||
347 | kb1 |= (effect << pos[1] * 2) | ||
348 | else: | ||
349 | kb2 |= (effect << pos[1] * 2) | ||
350 | |||
351 | msgs = [] | ||
352 | |||
353 | if kb1 != 0: | ||
354 | msgs.append({ | ||
355 | "cmd": "Set", | ||
356 | "key": self.get_datastorage_key("keyboard1"), | ||
357 | "want_reply": True, | ||
358 | "operations": [{ | ||
359 | "operation": "or", | ||
360 | "value": kb1 | ||
361 | }] | ||
362 | }) | ||
363 | |||
364 | if kb2 != 0: | ||
365 | msgs.append({ | ||
366 | "cmd": "Set", | ||
367 | "key": self.get_datastorage_key("keyboard2"), | ||
368 | "want_reply": True, | ||
369 | "operations": [{ | ||
370 | "operation": "or", | ||
371 | "value": kb2 | ||
372 | }] | ||
373 | }) | ||
374 | |||
375 | if len(msgs) > 0: | ||
376 | print(updates) | ||
377 | print(msgs) | ||
378 | await self.send_msgs(msgs) | ||
379 | |||
380 | def handle_keyboard_update(self, field: int, args: dict[str, Any]): | ||
381 | keys = {} | ||
382 | value = args["value"] | ||
383 | |||
384 | for i in range(0, 13): | ||
385 | if (value & (1 << (i * 2))) != 0: | ||
386 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1 | ||
387 | if (value & (1 << (i * 2 + 1))) != 0: | ||
388 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2 | ||
389 | |||
390 | updates = self.manager.update_keyboard(keys) | ||
391 | if len(updates) > 0: | ||
392 | self.manager.game_ctx.send_update_keyboard(updates) | ||
393 | |||
394 | |||
395 | async def pipe_loop(manager: Lingo2Manager): | ||
396 | while not manager.client_ctx.exit_event.is_set(): | ||
201 | try: | 397 | try: |
202 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, | 398 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, |
203 | max_size=MESSAGE_MAX_SIZE) | 399 | max_size=MESSAGE_MAX_SIZE) |
204 | ctx.server = Endpoint(socket) | 400 | manager.game_ctx.server = Endpoint(socket) |
205 | logger.info("Connected to Lingo 2!") | 401 | logger.info("Connected to Lingo 2!") |
206 | if ctx.client.auth is not None: | 402 | if manager.client_ctx.auth is not None: |
207 | ctx.send_connected() | 403 | manager.game_ctx.send_connected() |
208 | async for data in ctx.server.socket: | 404 | manager.game_ctx.send_accessible_locations() |
405 | async for data in manager.game_ctx.server.socket: | ||
209 | for msg in decode(data): | 406 | for msg in decode(data): |
210 | await process_game_cmd(ctx, msg) | 407 | await process_game_cmd(manager, msg) |
211 | except ConnectionRefusedError: | 408 | except ConnectionRefusedError: |
212 | logger.info("Could not connect to Lingo 2.") | 409 | logger.info("Could not connect to Lingo 2.") |
213 | finally: | 410 | finally: |
214 | ctx.server = None | 411 | manager.game_ctx.server = None |
215 | 412 | ||
216 | 413 | ||
217 | async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | 414 | async def process_game_cmd(manager: Lingo2Manager, args: dict): |
218 | cmd = args["cmd"] | 415 | cmd = args["cmd"] |
219 | 416 | ||
220 | if cmd == "Connect": | 417 | if cmd == "Connect": |
@@ -227,20 +424,29 @@ async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | |||
227 | else: | 424 | else: |
228 | server_address = f"{player}:None@{server}" | 425 | server_address = f"{player}:None@{server}" |
229 | 426 | ||
230 | async_start(ctx.client.connect(server_address), name="client connect") | 427 | async_start(manager.client_ctx.connect(server_address), name="client connect") |
231 | elif cmd == "Disconnect": | 428 | elif cmd == "Disconnect": |
232 | async_start(ctx.client.disconnect(), name="client disconnect") | 429 | async_start(manager.client_ctx.disconnect(), name="client disconnect") |
233 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | 430 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: |
234 | async_start(ctx.client.send_msgs([args]), name="client forward") | 431 | async_start(manager.client_ctx.send_msgs([args]), name="client forward") |
432 | elif cmd == "UpdateKeyboard": | ||
433 | updates = manager.update_keyboard(args["keyboard"]) | ||
434 | if len(updates) > 0: | ||
435 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | ||
436 | elif cmd == "Quit": | ||
437 | manager.client_ctx.exit_event.set() | ||
235 | 438 | ||
236 | 439 | ||
237 | async def run_game(): | 440 | async def run_game(): |
238 | exe_file = settings.get_settings().lingo2_options.exe_file | 441 | exe_file = settings.get_settings().lingo2_options.exe_file |
239 | 442 | ||
240 | from worlds import AutoWorldRegister | 443 | # This ensures we can use Steam features without having to open the game |
241 | world = AutoWorldRegister.world_types["Lingo 2"] | 444 | # through steam. |
445 | steam_appid_path = os.path.join(os.path.dirname(exe_file), "steam_appid.txt") | ||
446 | with open(steam_appid_path, "w") as said_handle: | ||
447 | said_handle.write("2523310") | ||
242 | 448 | ||
243 | if world.zip_path is not None: | 449 | if Lingo2World.zip_path is not None: |
244 | # This is a packaged apworld. | 450 | # This is a packaged apworld. |
245 | init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") | 451 | init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") |
246 | init_path = Utils.local_path("data", "lingo2_init.tscn") | 452 | init_path = Utils.local_path("data", "lingo2_init.tscn") |
@@ -254,7 +460,7 @@ async def run_game(): | |||
254 | "--scene", | 460 | "--scene", |
255 | init_path, | 461 | init_path, |
256 | "--", | 462 | "--", |
257 | str(world.zip_path.absolute()), | 463 | str(Lingo2World.zip_path.absolute()), |
258 | ], | 464 | ], |
259 | cwd=os.path.dirname(exe_file), | 465 | cwd=os.path.dirname(exe_file), |
260 | ) | 466 | ) |
@@ -278,9 +484,7 @@ def client_main(*launch_args: str) -> None: | |||
278 | 484 | ||
279 | client_ctx = Lingo2ClientContext(args.connect, args.password) | 485 | client_ctx = Lingo2ClientContext(args.connect, args.password) |
280 | game_ctx = Lingo2GameContext() | 486 | game_ctx = Lingo2GameContext() |
281 | 487 | manager = Lingo2Manager(game_ctx, client_ctx) | |
282 | client_ctx.game_ctx = game_ctx | ||
283 | game_ctx.client = client_ctx | ||
284 | 488 | ||
285 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") | 489 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") |
286 | 490 | ||
@@ -288,7 +492,7 @@ def client_main(*launch_args: str) -> None: | |||
288 | client_ctx.run_gui() | 492 | client_ctx.run_gui() |
289 | client_ctx.run_cli() | 493 | client_ctx.run_cli() |
290 | 494 | ||
291 | pipe_task = asyncio.create_task(pipe_loop(game_ctx), name="GameWatcher") | 495 | pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher") |
292 | 496 | ||
293 | try: | 497 | try: |
294 | await pipe_task | 498 | await pipe_task |
@@ -296,6 +500,7 @@ def client_main(*launch_args: str) -> None: | |||
296 | logger.exception(e) | 500 | logger.exception(e) |
297 | 501 | ||
298 | await client_ctx.exit_event.wait() | 502 | await client_ctx.exit_event.wait() |
503 | client_ctx.ui.stop() | ||
299 | await client_ctx.shutdown() | 504 | await client_ctx.shutdown() |
300 | 505 | ||
301 | Utils.init_logging("Lingo2Client", exception_logger="Client") | 506 | Utils.init_logging("Lingo2Client", exception_logger="Client") |
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/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 new file mode 100644 index 0000000..2c3d0f3 --- /dev/null +++ b/apworld/tracker.py | |||
@@ -0,0 +1,95 @@ | |||
1 | from typing import TYPE_CHECKING | ||
2 | |||
3 | from BaseClasses import MultiWorld, CollectionState, ItemClassification | ||
4 | from NetUtils import NetworkItem | ||
5 | from . import Lingo2World, Lingo2Item | ||
6 | from .regions import connect_ports_from_ut | ||
7 | from .options import Lingo2Options, ShuffleLetters | ||
8 | |||
9 | if TYPE_CHECKING: | ||
10 | from .context import Lingo2Manager | ||
11 | |||
12 | PLAYER_NUM = 1 | ||
13 | |||
14 | |||
15 | class Tracker: | ||
16 | manager: "Lingo2Manager" | ||
17 | |||
18 | multiworld: MultiWorld | ||
19 | world: Lingo2World | ||
20 | |||
21 | collected_items: dict[int, int] | ||
22 | checked_locations: set[int] | ||
23 | accessible_locations: set[int] | ||
24 | |||
25 | state: CollectionState | ||
26 | |||
27 | def __init__(self, manager: "Lingo2Manager"): | ||
28 | self.manager = manager | ||
29 | self.collected_items = {} | ||
30 | self.checked_locations = set() | ||
31 | self.accessible_locations = set() | ||
32 | |||
33 | def setup_slot(self, slot_data): | ||
34 | Lingo2World.for_tracker = True | ||
35 | |||
36 | self.multiworld = MultiWorld(players=PLAYER_NUM) | ||
37 | self.world = Lingo2World(self.multiworld, PLAYER_NUM) | ||
38 | self.multiworld.worlds[1] = self.world | ||
39 | self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) | ||
40 | for k, t in Lingo2Options.type_hints.items()}) | ||
41 | |||
42 | self.world.generate_early() | ||
43 | self.world.create_regions() | ||
44 | |||
45 | if self.world.options.shuffle_worldports: | ||
46 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} | ||
47 | connect_ports_from_ut(port_pairings, self.world) | ||
48 | |||
49 | self.refresh_state() | ||
50 | |||
51 | def set_checked_locations(self, checked_locations: set[int]): | ||
52 | self.checked_locations = checked_locations.copy() | ||
53 | |||
54 | def set_collected_items(self, network_items: list[NetworkItem]): | ||
55 | self.collected_items = {} | ||
56 | |||
57 | for item in network_items: | ||
58 | self.collected_items[item.item] = self.collected_items.get(item.item, 0) + 1 | ||
59 | |||
60 | self.refresh_state() | ||
61 | |||
62 | def refresh_state(self): | ||
63 | self.state = CollectionState(self.multiworld) | ||
64 | |||
65 | for item_id, item_amount in self.collected_items.items(): | ||
66 | for i in range(item_amount): | ||
67 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), | ||
68 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) | ||
69 | |||
70 | for k, v in self.manager.keyboard.items(): | ||
71 | # Unless all level 1 letters are pre-unlocked, H1 I1 N1 and T1 act differently between the generator and | ||
72 | # game. The generator considers them to be unlocked, which means they are not included in logic | ||
73 | # requirements, and only one item/event is needed to unlock their level 2 forms. The game considers them to | ||
74 | # be vanilla, which means you still have to pick them up in the Starting Room in order for them to appear on | ||
75 | # your keyboard. This also means that whether or not you have the level 1 forms should be synced to the | ||
76 | # multiworld. The tracker specifically should collect one fewer item for these letters in this scenario. | ||
77 | tv = v | ||
78 | if k in "hint" and self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla, | ||
79 | ShuffleLetters.option_progressive]: | ||
80 | tv = max(0, v - 1) | ||
81 | |||
82 | if tv > 0: | ||
83 | for i in range(tv): | ||
84 | self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM), | ||
85 | prevent_sweep=True) | ||
86 | |||
87 | self.state.sweep_for_advancements() | ||
88 | |||
89 | self.accessible_locations = set() | ||
90 | |||
91 | for region in self.state.reachable_regions[PLAYER_NUM]: | ||
92 | for location in region.locations: | ||
93 | if location.address not in self.checked_locations and location.access_rule(self.state): | ||
94 | if location.address is not None: | ||
95 | self.accessible_locations.add(location.address) | ||