about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-09-27 17:14:40 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-09-27 17:14:40 -0400
commitb0f474bee1c8e1111f7542bf4985136d9aedf340 (patch)
treeef2aa34bad532ffb2a45d90893dbcd4c378a0dfb
parentfeb89a44ddf5f93bc476ca29cd02257aea47dc06 (diff)
downloadlingo2-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__.py2
-rw-r--r--apworld/client/client.gd25
-rw-r--r--apworld/client/gamedata.gd7
-rw-r--r--apworld/client/keyboard.gd36
-rw-r--r--apworld/client/manager.gd1
-rw-r--r--apworld/context.py388
-rw-r--r--apworld/player_logic.py13
-rw-r--r--apworld/static_logic.py1
-rw-r--r--apworld/tracker.py50
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)
33signal hint_received(message) 33signal hint_received(message)
34signal accessible_locations_updated 34signal accessible_locations_updated
35signal checked_locations_updated 35signal checked_locations_updated
36signal keyboard_update_received
36 37
37 38
38func _init(): 39func _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
161func connectToServer(server, un, pw): 169func 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
205func 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
218func say(textdata): 213func 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
225func updateKeyboard(updates):
226 sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}])
227
228
230func sendQuit(): 229func 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
164func 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
164func get_door_receivers(door_id): 171func 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
107func collect_local_letter(key, level): 110func 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
215func 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
123func saveSettings(): 124func 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
15from . import Lingo2World 15from . import Lingo2World
16from .tracker import Tracker 16from .tracker import Tracker
17 17
18PORT = 43182 18ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz"
19MESSAGE_MAX_SIZE = 16*1024*1024 19MESSAGE_MAX_SIZE = 16*1024*1024
20PORT = 43182
21
22KEY_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
29REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()}
30
31
32class 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
22class Lingo2GameContext: 68class 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
115class Lingo2ClientContext(CommonContext): 194class 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({
233async 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
395async 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
252async def process_game_cmd(ctx: Lingo2GameContext, args: dict): 414async 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
274async def run_game(): 440async 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
2from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS 2from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS
3import pkgutil 3import pkgutil
4 4
5
5class Lingo2StaticLogic: 6class 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 @@
1from typing import TYPE_CHECKING
2
1from BaseClasses import MultiWorld, CollectionState, ItemClassification 3from BaseClasses import MultiWorld, CollectionState, ItemClassification
2from NetUtils import NetworkItem 4from NetUtils import NetworkItem
3from . import Lingo2World, Lingo2Item 5from . import Lingo2World, Lingo2Item
4from .regions import connect_ports_from_ut 6from .regions import connect_ports_from_ut
5from .options import Lingo2Options 7from .options import Lingo2Options, ShuffleLetters
8
9if TYPE_CHECKING:
10 from .context import Lingo2Manager
6 11
7PLAYER_NUM = 1 12PLAYER_NUM = 1
8 13
9 14
10class Tracker: 15class 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()