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