diff options
| -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() |
