diff options
author | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-27 17:14:40 -0400 |
---|---|---|
committer | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-27 17:14:40 -0400 |
commit | b0f474bee1c8e1111f7542bf4985136d9aedf340 (patch) | |
tree | ef2aa34bad532ffb2a45d90893dbcd4c378a0dfb | |
parent | feb89a44ddf5f93bc476ca29cd02257aea47dc06 (diff) | |
download | lingo2-archipelago-b0f474bee1c8e1111f7542bf4985136d9aedf340.tar.gz lingo2-archipelago-b0f474bee1c8e1111f7542bf4985136d9aedf340.tar.bz2 lingo2-archipelago-b0f474bee1c8e1111f7542bf4985136d9aedf340.zip |
Treat local letters as items for tracker
Local letters are now synced with datastorage, so they transfer to other computers like regular items would, and the tracker also now waits until you collect local letters before showing what they give you in logic.
-rw-r--r-- | apworld/__init__.py | 2 | ||||
-rw-r--r-- | apworld/client/client.gd | 25 | ||||
-rw-r--r-- | apworld/client/gamedata.gd | 7 | ||||
-rw-r--r-- | apworld/client/keyboard.gd | 36 | ||||
-rw-r--r-- | apworld/client/manager.gd | 1 | ||||
-rw-r--r-- | apworld/context.py | 388 | ||||
-rw-r--r-- | apworld/player_logic.py | 13 | ||||
-rw-r--r-- | apworld/static_logic.py | 1 | ||||
-rw-r--r-- | apworld/tracker.py | 50 |
9 files changed, 379 insertions, 144 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..3d4096f 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd | |||
@@ -33,6 +33,7 @@ signal item_sent_notification(message) | |||
33 | signal hint_received(message) | 33 | signal hint_received(message) |
34 | signal accessible_locations_updated | 34 | signal accessible_locations_updated |
35 | signal checked_locations_updated | 35 | signal checked_locations_updated |
36 | signal keyboard_update_received | ||
36 | 37 | ||
37 | 38 | ||
38 | func _init(): | 39 | func _init(): |
@@ -157,6 +158,13 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo | |||
157 | 158 | ||
158 | accessible_locations_updated.emit() | 159 | accessible_locations_updated.emit() |
159 | 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 | |||
160 | 168 | ||
161 | func connectToServer(server, un, pw): | 169 | func connectToServer(server, un, pw): |
162 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) | 170 | sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) |
@@ -202,19 +210,6 @@ func sendLocations(loc_ids): | |||
202 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) | 210 | sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) |
203 | 211 | ||
204 | 212 | ||
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): | 213 | func say(textdata): |
219 | sendMessage([{"cmd": "Say", "text": textdata}]) | 214 | sendMessage([{"cmd": "Say", "text": textdata}]) |
220 | 215 | ||
@@ -227,6 +222,10 @@ func scoutLocations(loc_ids): | |||
227 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) | 222 | sendMessage([{"cmd": "LocationScouts", "locations": loc_ids}]) |
228 | 223 | ||
229 | 224 | ||
225 | func updateKeyboard(updates): | ||
226 | sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}]) | ||
227 | |||
228 | |||
230 | func sendQuit(): | 229 | func sendQuit(): |
231 | sendMessage([{"cmd": "Quit"}]) | 230 | sendMessage([{"cmd": "Quit"}]) |
232 | 231 | ||
diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 39e0583..13ec568 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd | |||
@@ -161,6 +161,13 @@ 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() |
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 e7765dd..afa3ebe 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd | |||
@@ -118,6 +118,7 @@ func _ready(): | |||
118 | 118 | ||
119 | keyboard = SCRIPT_keyboard.new() | 119 | keyboard = SCRIPT_keyboard.new() |
120 | add_child(keyboard) | 120 | add_child(keyboard) |
121 | client.keyboard_update_received.connect(keyboard.remote_keyboard_updated) | ||
121 | 122 | ||
122 | 123 | ||
123 | func saveSettings(): | 124 | func saveSettings(): |
diff --git a/apworld/context.py b/apworld/context.py index 0a058e5..bc3b1bf 100644 --- a/apworld/context.py +++ b/apworld/context.py | |||
@@ -15,35 +15,85 @@ 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 | |||
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 | ||
20 | 66 | ||
21 | 67 | ||
22 | class Lingo2GameContext: | 68 | class Lingo2GameContext: |
23 | server: Endpoint | None | 69 | server: Endpoint | None |
24 | client: "Lingo2ClientContext" | 70 | manager: Lingo2Manager |
25 | tracker: Tracker | ||
26 | 71 | ||
27 | def __init__(self): | 72 | def __init__(self): |
28 | self.server = None | 73 | self.server = None |
29 | self.tracker = Tracker() | ||
30 | 74 | ||
31 | def send_connected(self): | 75 | def send_connected(self): |
76 | if self.server is None: | ||
77 | return | ||
78 | |||
32 | msg = { | 79 | msg = { |
33 | "cmd": "Connected", | 80 | "cmd": "Connected", |
34 | "user": self.client.username, | 81 | "user": self.manager.client_ctx.username, |
35 | "seed_name": self.client.seed_name, | 82 | "seed_name": self.manager.client_ctx.seed_name, |
36 | "version": self.client.server_version, | 83 | "version": self.manager.client_ctx.server_version, |
37 | "generator_version": self.client.generator_version, | 84 | "generator_version": self.manager.client_ctx.generator_version, |
38 | "team": self.client.team, | 85 | "team": self.manager.client_ctx.team, |
39 | "slot": self.client.slot, | 86 | "slot": self.manager.client_ctx.slot, |
40 | "checked_locations": self.client.checked_locations, | 87 | "checked_locations": self.manager.client_ctx.checked_locations, |
41 | "slot_data": self.client.slot_data, | 88 | "slot_data": self.manager.client_ctx.slot_data, |
42 | } | 89 | } |
43 | 90 | ||
44 | async_start(self.send_msgs([msg]), name="game Connected") | 91 | async_start(self.send_msgs([msg]), name="game Connected") |
45 | 92 | ||
46 | 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 | |||
47 | msg = { | 97 | msg = { |
48 | "cmd": "ItemSentNotif", | 98 | "cmd": "ItemSentNotif", |
49 | "item_name": item_name, | 99 | "item_name": item_name, |
@@ -54,6 +104,9 @@ class Lingo2GameContext: | |||
54 | async_start(self.send_msgs([msg]), name="item sent notif") | 104 | async_start(self.send_msgs([msg]), name="item sent notif") |
55 | 105 | ||
56 | 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 | |||
57 | msg = { | 110 | msg = { |
58 | "cmd": "HintReceived", | 111 | "cmd": "HintReceived", |
59 | "item_name": item_name, | 112 | "item_name": item_name, |
@@ -66,6 +119,9 @@ class Lingo2GameContext: | |||
66 | async_start(self.send_msgs([msg]), name="hint received notif") | 119 | async_start(self.send_msgs([msg]), name="hint received notif") |
67 | 120 | ||
68 | def send_item_received(self, items): | 121 | def send_item_received(self, items): |
122 | if self.server is None: | ||
123 | return | ||
124 | |||
69 | msg = { | 125 | msg = { |
70 | "cmd": "ItemReceived", | 126 | "cmd": "ItemReceived", |
71 | "items": items, | 127 | "items": items, |
@@ -74,6 +130,9 @@ class Lingo2GameContext: | |||
74 | async_start(self.send_msgs([msg]), name="item received") | 130 | async_start(self.send_msgs([msg]), name="item received") |
75 | 131 | ||
76 | def send_location_info(self, locations): | 132 | def send_location_info(self, locations): |
133 | if self.server is None: | ||
134 | return | ||
135 | |||
77 | msg = { | 136 | msg = { |
78 | "cmd": "LocationInfo", | 137 | "cmd": "LocationInfo", |
79 | "locations": locations, | 138 | "locations": locations, |
@@ -82,6 +141,9 @@ class Lingo2GameContext: | |||
82 | async_start(self.send_msgs([msg]), name="location info") | 141 | async_start(self.send_msgs([msg]), name="location info") |
83 | 142 | ||
84 | def send_text_message(self, parts): | 143 | def send_text_message(self, parts): |
144 | if self.server is None: | ||
145 | return | ||
146 | |||
85 | msg = { | 147 | msg = { |
86 | "cmd": "TextMessage", | 148 | "cmd": "TextMessage", |
87 | "data": parts, | 149 | "data": parts, |
@@ -90,14 +152,20 @@ class Lingo2GameContext: | |||
90 | async_start(self.send_msgs([msg]), name="notif") | 152 | async_start(self.send_msgs([msg]), name="notif") |
91 | 153 | ||
92 | def send_accessible_locations(self): | 154 | def send_accessible_locations(self): |
155 | if self.server is None: | ||
156 | return | ||
157 | |||
93 | msg = { | 158 | msg = { |
94 | "cmd": "AccessibleLocations", | 159 | "cmd": "AccessibleLocations", |
95 | "locations": list(self.tracker.accessible_locations), | 160 | "locations": list(self.manager.tracker.accessible_locations), |
96 | } | 161 | } |
97 | 162 | ||
98 | async_start(self.send_msgs([msg]), name="accessible locations") | 163 | async_start(self.send_msgs([msg]), name="accessible locations") |
99 | 164 | ||
100 | def send_update_locations(self, locations): | 165 | def send_update_locations(self, locations): |
166 | if self.server is None: | ||
167 | return | ||
168 | |||
101 | msg = { | 169 | msg = { |
102 | "cmd": "UpdateLocations", | 170 | "cmd": "UpdateLocations", |
103 | "locations": locations, | 171 | "locations": locations, |
@@ -105,6 +173,17 @@ class Lingo2GameContext: | |||
105 | 173 | ||
106 | async_start(self.send_msgs([msg]), name="update locations") | 174 | async_start(self.send_msgs([msg]), name="update locations") |
107 | 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 | |||
108 | async def send_msgs(self, msgs: list[Any]) -> None: | 187 | async def send_msgs(self, msgs: list[Any]) -> None: |
109 | """ `msgs` JSON serializable """ | 188 | """ `msgs` JSON serializable """ |
110 | 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: |
@@ -113,7 +192,7 @@ class Lingo2GameContext: | |||
113 | 192 | ||
114 | 193 | ||
115 | class Lingo2ClientContext(CommonContext): | 194 | class Lingo2ClientContext(CommonContext): |
116 | game_ctx: Lingo2GameContext | 195 | manager: Lingo2Manager |
117 | 196 | ||
118 | game = "Lingo 2" | 197 | game = "Lingo 2" |
119 | items_handling = 0b111 | 198 | items_handling = 0b111 |
@@ -138,118 +217,201 @@ class Lingo2ClientContext(CommonContext): | |||
138 | elif cmd == "Connected": | 217 | elif cmd == "Connected": |
139 | self.slot_data = args.get("slot_data", None) | 218 | self.slot_data = args.get("slot_data", None) |
140 | 219 | ||
141 | if self.game_ctx.server is not None: | 220 | self.manager.reset() |
142 | self.game_ctx.send_connected() | 221 | |
143 | 222 | self.manager.game_ctx.send_connected() | |
144 | self.game_ctx.tracker.setup_slot(self.slot_data) | 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") | ||
145 | elif cmd == "RoomUpdate": | 242 | elif cmd == "RoomUpdate": |
146 | if self.game_ctx.server is not None: | 243 | self.manager.tracker.set_checked_locations(self.checked_locations) |
147 | self.game_ctx.send_update_locations(args["checked_locations"]) | 244 | self.manager.game_ctx.send_update_locations(args["checked_locations"]) |
148 | elif cmd == "ReceivedItems": | 245 | elif cmd == "ReceivedItems": |
149 | self.game_ctx.tracker.set_collected_items(self.items_received) | 246 | self.manager.tracker.set_collected_items(self.items_received) |
150 | 247 | ||
151 | if self.game_ctx.server is not None: | 248 | cur_index = 0 |
152 | cur_index = 0 | 249 | items = [] |
153 | items = [] | ||
154 | 250 | ||
155 | for item in args["items"]: | 251 | for item in args["items"]: |
156 | index = cur_index + args["index"] | 252 | index = cur_index + args["index"] |
157 | cur_index += 1 | 253 | cur_index += 1 |
158 | 254 | ||
159 | item_msg = { | 255 | item_msg = { |
160 | "id": item.item, | 256 | "id": item.item, |
161 | "index": index, | 257 | "index": index, |
162 | "flags": item.flags, | 258 | "flags": item.flags, |
163 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | 259 | "text": self.item_names.lookup_in_slot(item.item, self.slot), |
164 | } | 260 | } |
165 | 261 | ||
166 | if item.player != self.slot: | 262 | if item.player != self.slot: |
167 | item_msg["sender"] = self.player_names.get(item.player) | 263 | item_msg["sender"] = self.player_names.get(item.player) |
168 | 264 | ||
169 | items.append(item_msg) | 265 | items.append(item_msg) |
170 | 266 | ||
171 | self.game_ctx.send_item_received(items) | 267 | self.manager.game_ctx.send_item_received(items) |
172 | 268 | ||
173 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): | 269 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): |
174 | self.game_ctx.send_accessible_locations() | 270 | self.manager.game_ctx.send_accessible_locations() |
175 | elif cmd == "PrintJSON": | 271 | elif cmd == "PrintJSON": |
176 | if self.game_ctx.server is not None: | 272 | 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: | 273 | 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"]) | 274 | 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) | 275 | receiver_name = self.player_names.get(args["receiving"]) |
180 | receiver_name = self.player_names.get(args["receiving"]) | 276 | |
181 | 277 | if args["type"] == "Hint" and not args.get("found", False): | |
182 | 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, |
183 | self.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | 279 | int(args["receiving"]) == self.slot) |
184 | int(args["receiving"]) == self.slot) | 280 | elif args["receiving"] != self.slot: |
185 | elif args["receiving"] != self.slot: | 281 | 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) | 282 | |
187 | 283 | parts = [] | |
188 | parts = [] | 284 | for message_part in args["data"]: |
189 | for message_part in args["data"]: | 285 | if "type" not in message_part and "text" in message_part: |
190 | if "type" not in message_part and "text" in message_part: | 286 | parts.append({"type": "text", "text": message_part["text"]}) |
191 | parts.append({"type": "text", "text": message_part["text"]}) | 287 | elif message_part["type"] == "player_id": |
192 | elif message_part["type"] == "player_id": | 288 | parts.append({ |
193 | parts.append({ | 289 | "type": "player", |
194 | "type": "player", | 290 | "text": self.player_names.get(int(message_part["text"])), |
195 | "text": self.player_names.get(int(message_part["text"])), | 291 | "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 | }) | 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"]}) | ||
226 | 307 | ||
227 | self.game_ctx.send_location_info(locations) | 308 | self.manager.game_ctx.send_text_message(parts) |
228 | 309 | elif cmd == "LocationInfo": | |
229 | if cmd in ["Connected", "RoomUpdate"]: | 310 | locations = [] |
230 | self.game_ctx.tracker.set_checked_locations(self.checked_locations) | 311 | |
231 | 312 | for location in args["locations"]: | |
232 | 313 | locations.append({ | |
233 | async def pipe_loop(ctx: Lingo2GameContext): | 314 | "id": location.location, |
234 | while not ctx.client.exit_event.is_set(): | 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(): | ||
235 | try: | 397 | try: |
236 | 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, |
237 | max_size=MESSAGE_MAX_SIZE) | 399 | max_size=MESSAGE_MAX_SIZE) |
238 | ctx.server = Endpoint(socket) | 400 | manager.game_ctx.server = Endpoint(socket) |
239 | logger.info("Connected to Lingo 2!") | 401 | logger.info("Connected to Lingo 2!") |
240 | if ctx.client.auth is not None: | 402 | if manager.client_ctx.auth is not None: |
241 | ctx.send_connected() | 403 | manager.game_ctx.send_connected() |
242 | ctx.send_accessible_locations() | 404 | manager.game_ctx.send_accessible_locations() |
243 | async for data in ctx.server.socket: | 405 | async for data in manager.game_ctx.server.socket: |
244 | for msg in decode(data): | 406 | for msg in decode(data): |
245 | await process_game_cmd(ctx, msg) | 407 | await process_game_cmd(manager, msg) |
246 | except ConnectionRefusedError: | 408 | except ConnectionRefusedError: |
247 | logger.info("Could not connect to Lingo 2.") | 409 | logger.info("Could not connect to Lingo 2.") |
248 | finally: | 410 | finally: |
249 | ctx.server = None | 411 | manager.game_ctx.server = None |
250 | 412 | ||
251 | 413 | ||
252 | async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | 414 | async def process_game_cmd(manager: Lingo2Manager, args: dict): |
253 | cmd = args["cmd"] | 415 | cmd = args["cmd"] |
254 | 416 | ||
255 | if cmd == "Connect": | 417 | if cmd == "Connect": |
@@ -262,13 +424,17 @@ async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | |||
262 | else: | 424 | else: |
263 | server_address = f"{player}:None@{server}" | 425 | server_address = f"{player}:None@{server}" |
264 | 426 | ||
265 | async_start(ctx.client.connect(server_address), name="client connect") | 427 | async_start(manager.client_ctx.connect(server_address), name="client connect") |
266 | elif cmd == "Disconnect": | 428 | elif cmd == "Disconnect": |
267 | async_start(ctx.client.disconnect(), name="client disconnect") | 429 | async_start(manager.client_ctx.disconnect(), name="client disconnect") |
268 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | 430 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: |
269 | 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") | ||
270 | elif cmd == "Quit": | 436 | elif cmd == "Quit": |
271 | ctx.client.exit_event.set() | 437 | manager.client_ctx.exit_event.set() |
272 | 438 | ||
273 | 439 | ||
274 | async def run_game(): | 440 | async def run_game(): |
@@ -318,9 +484,7 @@ def client_main(*launch_args: str) -> None: | |||
318 | 484 | ||
319 | client_ctx = Lingo2ClientContext(args.connect, args.password) | 485 | client_ctx = Lingo2ClientContext(args.connect, args.password) |
320 | game_ctx = Lingo2GameContext() | 486 | game_ctx = Lingo2GameContext() |
321 | 487 | manager = Lingo2Manager(game_ctx, client_ctx) | |
322 | client_ctx.game_ctx = game_ctx | ||
323 | game_ctx.client = client_ctx | ||
324 | 488 | ||
325 | 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") |
326 | 490 | ||
@@ -328,7 +492,7 @@ def client_main(*launch_args: str) -> None: | |||
328 | client_ctx.run_gui() | 492 | client_ctx.run_gui() |
329 | client_ctx.run_cli() | 493 | client_ctx.run_cli() |
330 | 494 | ||
331 | pipe_task = asyncio.create_task(pipe_loop(game_ctx), name="GameWatcher") | 495 | pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher") |
332 | 496 | ||
333 | try: | 497 | try: |
334 | await pipe_task | 498 | await pipe_task |
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 index 721e9b3..2c3d0f3 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py | |||
@@ -1,14 +1,22 @@ | |||
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] |
@@ -16,26 +24,29 @@ class Tracker: | |||
16 | 24 | ||
17 | state: CollectionState | 25 | state: CollectionState |
18 | 26 | ||
19 | def __init__(self): | 27 | def __init__(self, manager: "Lingo2Manager"): |
28 | self.manager = manager | ||
20 | self.collected_items = {} | 29 | self.collected_items = {} |
21 | self.checked_locations = set() | 30 | self.checked_locations = set() |
22 | self.accessible_locations = set() | 31 | self.accessible_locations = set() |
23 | 32 | ||
24 | def setup_slot(self, slot_data): | 33 | def setup_slot(self, slot_data): |
34 | Lingo2World.for_tracker = True | ||
35 | |||
25 | self.multiworld = MultiWorld(players=PLAYER_NUM) | 36 | self.multiworld = MultiWorld(players=PLAYER_NUM) |
26 | world = Lingo2World(self.multiworld, PLAYER_NUM) | 37 | self.world = Lingo2World(self.multiworld, PLAYER_NUM) |
27 | self.multiworld.worlds[1] = world | 38 | self.multiworld.worlds[1] = self.world |
28 | world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) | 39 | self.world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) |
29 | for k, t in Lingo2Options.type_hints.items()}) | 40 | for k, t in Lingo2Options.type_hints.items()}) |
30 | 41 | ||
31 | world.generate_early() | 42 | self.world.generate_early() |
32 | world.create_regions() | 43 | self.world.create_regions() |
33 | 44 | ||
34 | if world.options.shuffle_worldports: | 45 | if self.world.options.shuffle_worldports: |
35 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} | 46 | port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} |
36 | connect_ports_from_ut(port_pairings, world) | 47 | connect_ports_from_ut(port_pairings, self.world) |
37 | 48 | ||
38 | self.state = CollectionState(self.multiworld) | 49 | self.refresh_state() |
39 | 50 | ||
40 | def set_checked_locations(self, checked_locations: set[int]): | 51 | def set_checked_locations(self, checked_locations: set[int]): |
41 | self.checked_locations = checked_locations.copy() | 52 | self.checked_locations = checked_locations.copy() |
@@ -56,6 +67,23 @@ class Tracker: | |||
56 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), | 67 | self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), |
57 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) | 68 | ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) |
58 | 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 | |||
59 | self.state.sweep_for_advancements() | 87 | self.state.sweep_for_advancements() |
60 | 88 | ||
61 | self.accessible_locations = set() | 89 | self.accessible_locations = set() |