From 611501fa4caa09523e842402b32f432745c0f08d Mon Sep 17 00:00:00 2001 From: Valentin Courtois Date: Wed, 18 Feb 2026 15:23:56 +0100 Subject: [PATCH 1/6] Add a scout button to locations with scoutable: true, consider false by default --- .gitignore | 4 ++- src/ManualClient.py | 63 +++++++++++++++++++++++++++++++++++------ src/data/locations.json | 3 +- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f8c52754..cdd7a6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .vscode/ .vs/ /*.code-workspace -.idea \ No newline at end of file +.idea +build.py +*.apworld \ No newline at end of file diff --git a/src/ManualClient.py b/src/ManualClient.py index 257676aa..44174846 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -675,10 +675,14 @@ def build_tracker_and_locations_table(self): if not self.ctx.location_table and not hasattr(AutoWorldRegister.world_types[self.ctx.game], 'location_name_to_location'): raise Exception("The apworld for %s is too outdated for this client. Please update it." % (self.ctx.game)) + scoutable_locations = [] + for location_id in self.ctx.missing_locations: # holy nesting, wow location_name = self.ctx.location_names.lookup_in_game(location_id) location = self.ctx.get_location_by_name(location_name) + if location.get("scoutable", False): + scoutable_locations.append(location_id) if not location: continue @@ -772,15 +776,28 @@ def alphanum_key(i): ) category_scroll = locations_panel.add_node(TreeViewScrollView(size_hint=(1, None), size=(Window.width / 2, 250)), category_tree) - category_layout = GridLayout(cols=1, size_hint_y=None) + category_layout = GridLayout(cols=2, size_hint_y=None) category_layout.bind(minimum_height = category_layout.setter('height')) category_scroll.add_widget(category_layout) for location_id in self.listed_locations[location_category]: - location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(None, None), height=30, width=400) - location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) - location_button.id = location_id - category_layout.add_widget(location_button) + if location_id in scoutable_locations: + location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(None, None), height=30, width=350) + location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) + location_button.id = location_id + category_layout.add_widget(location_button) + + location_scout = TreeViewButton(text="Scout", size_hint=(None, None), height=30, width=50, font_size=10) + location_scout.bind(on_release=lambda *args, loc_id=location_id: self.location_scout_callback(loc_id, *args)) + location_scout.id = location_id + category_layout.add_widget(location_scout) + else: + location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(None, None), height=30, width=400) + location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) + location_button.id = location_id + category_layout.add_widget(location_button) + # Add invisible spacer to maintain 2-column grid structure + category_layout.add_widget(Label(text="", size_hint=(None, None), height=0, width=0, opacity=0)) # if this is the category that Victory is in, display the Victory button # if ("category" in victory_location_data and location_category in victory_location_data["category"]) or \ @@ -829,8 +846,8 @@ def update_tracker_and_locations_table(self, update_highlights=False): for _, child in enumerate(self.tracker_and_locations_panel.children): # # Structure of items: - # TrackerLayoutScrollable -> TreeView -> TreeViewLabel, TreeViewScrollView -> GridLayout -> Label - # item tracker -> category -> category label, category scroll -> label col -> item + # TrackerLayoutScrollable -> TreeView -> TreeViewLabel, TreeViewScrollView -> GridLayout -> BoxLayout -> Label + # item tracker -> category -> category label, category scroll -> label col -> loc&scout -> item # if type(child) is TrackerLayoutScrollable: treeview = child.children[0] # TreeView @@ -1017,6 +1034,9 @@ def hide_button_during_search(btn: TreeViewButton): # Label (for existing item listings) for location_button in category_grid.children: if type(location_button) is TreeViewButton: + # Skip scout buttons + if location_button.text == "Scout": + continue # should only be true for the victory location button, which has different text if location_button.text not in (self.ctx.location_table or AutoWorldRegister.world_types[self.ctx.game].location_name_to_location): # if the player is searching for text and the location name doesn't contain it, hide and disable it @@ -1108,11 +1128,35 @@ def location_button_callback(self, location_id, button): else: self.ctx.locations_checked.append(location_id) self.ctx.syncing = True - button.parent.remove_widget(button) + # Remove both the location button and its adjacent scout button/spacer from 2-column grid + parent = button.parent + button_index = parent.children.index(button) + # Grid cols=2: pairs are at adjacent indices (even with odd, odd with even) + pair_index = button_index - 1 if button_index % 2 == 1 else button_index + 1 + if 0 <= pair_index < len(parent.children): + parent.remove_widget(parent.children[pair_index]) + parent.remove_widget(button) # message = [{"cmd": 'LocationChecks', "locations": [location_id]}] # self.ctx.send_msgs(message) + def location_scout_callback(self, location_id, button): + # if the mouse is currently hovering over any of the controls/tabs at the top of the client, ignore clicks for location buttons underneath + if self.are_top_controls_at_mouse_pos(): + # if there's an obj in the top controls/tab at the current mouse position, click it instead + if hovered_obj := self.get_top_obj_at_mouse_pos(): + if hasattr(hovered_obj, 'trigger_action'): # buttons, tabs, etc. + hovered_obj.trigger_action(duration=0) + elif hasattr(hovered_obj, 'focus'): # text inputs + hovered_obj.focus = True + + return + + if location_id: + self.ctx.locations_scouted.append(location_id) + self.ctx.syncing = True + # TODO Replace the scout button with the placeholder spacer to maintain 2-column grid structure (did not manage to make it work) + def victory_button_callback(self, button): # if the mouse is currently hovering over any of the controls/tabs at the top of the client, ignore clicks for location buttons underneath if self.are_top_controls_at_mouse_pos(): @@ -1139,6 +1183,8 @@ async def game_watcher_manual(ctx: ManualContext): sync_msg = [{'cmd': 'Sync'}] if ctx.locations_checked: sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) + if ctx.locations_scouted: + sync_msg.append({"cmd": "LocationScouts", "locations": list(ctx.locations_scouted), "create_as_hint": 2}) await ctx.send_msgs(sync_msg) ctx.syncing = False @@ -1152,6 +1198,7 @@ async def game_watcher_manual(ctx: ManualContext): victory = ("__Victory__" in ctx.items_received) ctx.locations_checked = [] + ctx.locations_scouted = [] if not ctx.finished_game and victory: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) ctx.finished_game = True diff --git a/src/data/locations.json b/src/data/locations.json index 0d8c939d..6ecf96db 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -8,7 +8,8 @@ { "name": "Beat the Game - Ryu", "category": ["Starter Team", "Left Side"], - "region": "ExampleRegion" + "region": "ExampleRegion", + "scoutable": true }, { "name": "Beat the Game - Chun-Li", From 62ba7a4630a1a21b602179e788263c5e86b49b90 Mon Sep 17 00:00:00 2001 From: Valentin Courtois Date: Wed, 18 Feb 2026 15:26:45 +0100 Subject: [PATCH 2/6] Fix relic comment of a previous version --- src/ManualClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ManualClient.py b/src/ManualClient.py index 44174846..ec2f37bf 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -846,8 +846,8 @@ def update_tracker_and_locations_table(self, update_highlights=False): for _, child in enumerate(self.tracker_and_locations_panel.children): # # Structure of items: - # TrackerLayoutScrollable -> TreeView -> TreeViewLabel, TreeViewScrollView -> GridLayout -> BoxLayout -> Label - # item tracker -> category -> category label, category scroll -> label col -> loc&scout -> item + # TrackerLayoutScrollable -> TreeView -> TreeViewLabel, TreeViewScrollView -> GridLayout -> Label + # item tracker -> category -> category label, category scroll -> label col -> item # if type(child) is TrackerLayoutScrollable: treeview = child.children[0] # TreeView From c25565ce25869b5b69b1cb00cce1a0579bac52a8 Mon Sep 17 00:00:00 2001 From: Katelyn Gigante Date: Sat, 28 Feb 2026 15:17:29 +1100 Subject: [PATCH 3/6] Bump client version --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 0813346f..6d570897 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -564,7 +564,7 @@ def __init__(self, display_name: str, script_name: Optional[str] = None, func: O self.version = version def add_client_to_launcher() -> None: - version = 2026_01_02 # YYYYMMDD + version = 2026_02_23 # YYYYMMDD found = False if "manual" not in icon_paths: From fb9c295b7fecb2ccce7505c8468efcf3cde0c745 Mon Sep 17 00:00:00 2001 From: Katelyn Gigante Date: Sun, 15 Mar 2026 20:17:46 +1100 Subject: [PATCH 4/6] Still not good, but better --- src/ManualClient.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/ManualClient.py b/src/ManualClient.py index ec2f37bf..b01e137e 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -568,6 +568,16 @@ def update_hints(self): if "(Hinted)" not in location["category"]: location["category"].append("(Hinted)") rebuild = True + json_str = [ + {"type": "player_id", "text": hint["receiving_player"]}, + {"type": "text", "text": "'s "}, + {"type": "item_id", + "text": hint["item"], + "flags": hint["item_flags"], + "player": hint["receiving_player"], + }, + ] + location['hint_text'] = self.json_to_kivy_parser(json_str) if rebuild: self.build_tracker_and_locations_table() @@ -675,18 +685,22 @@ def build_tracker_and_locations_table(self): if not self.ctx.location_table and not hasattr(AutoWorldRegister.world_types[self.ctx.game], 'location_name_to_location'): raise Exception("The apworld for %s is too outdated for this client. Please update it." % (self.ctx.game)) - scoutable_locations = [] + scoutable_locations = set() + hinted_locations = {} for location_id in self.ctx.missing_locations: # holy nesting, wow location_name = self.ctx.location_names.lookup_in_game(location_id) location = self.ctx.get_location_by_name(location_name) - if location.get("scoutable", False): - scoutable_locations.append(location_id) if not location: continue + if location.get("scoutable", False): + scoutable_locations.add(location_id) + if location.get("hint_text", False): + hinted_locations[location_id] = location.get("hint_text", "") + if "category" in location and len(location["category"]) > 0: for category in location["category"]: category_settings = self.ctx.category_table.get(category) or getattr(AutoWorldRegister.world_types[self.ctx.game], "category_table", {}).get(category, {}) @@ -781,21 +795,21 @@ def alphanum_key(i): category_scroll.add_widget(category_layout) for location_id in self.listed_locations[location_category]: - if location_id in scoutable_locations: - location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(None, None), height=30, width=350) - location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) - location_button.id = location_id - category_layout.add_widget(location_button) - - location_scout = TreeViewButton(text="Scout", size_hint=(None, None), height=30, width=50, font_size=10) + has_hint = location_id in hinted_locations or location_id in scoutable_locations + + location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(.75 if has_hint else 1, None), height=30) + location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) + location_button.id = location_id + category_layout.add_widget(location_button) + + if location_id in hinted_locations: + category_layout.add_widget(Label(text=hinted_locations[location_id], size_hint=(.25, None), height=30, font_size=10, markup=True)) + elif location_id in scoutable_locations: + location_scout = TreeViewButton(text="Scout", size_hint=(.25, None), height=30, font_size=10) location_scout.bind(on_release=lambda *args, loc_id=location_id: self.location_scout_callback(loc_id, *args)) location_scout.id = location_id category_layout.add_widget(location_scout) else: - location_button = TreeViewButton(text=self.ctx.location_names.lookup_in_game(location_id), size_hint=(None, None), height=30, width=400) - location_button.bind(on_release=lambda *args, loc_id=location_id: self.location_button_callback(loc_id, *args)) - location_button.id = location_id - category_layout.add_widget(location_button) # Add invisible spacer to maintain 2-column grid structure category_layout.add_widget(Label(text="", size_hint=(None, None), height=0, width=0, opacity=0)) From b20fc936bd78f8a42887087983b9fd24a5e5784c Mon Sep 17 00:00:00 2001 From: Katelyn Gigante Date: Wed, 1 Apr 2026 20:07:08 +1100 Subject: [PATCH 5/6] Remove the hint label from associated buttons in other categories --- src/ManualClient.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ManualClient.py b/src/ManualClient.py index 7e0a5c03..5e228c6d 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -1093,7 +1093,13 @@ def hide_button_during_search(btn: TreeViewButton): category_count += 1 for location_button in buttons_to_remove: - location_button.parent.remove_widget(location_button) + parent = location_button.parent + button_index = parent.children.index(location_button) + # Grid cols=2: pairs are at adjacent indices (even with odd, odd with even) + pair_index = button_index - 1 if button_index % 2 == 1 else button_index + 1 + if 0 <= pair_index < len(parent.children): + parent.remove_widget(parent.children[pair_index]) + parent.remove_widget(location_button) scrollview_height = 30 * category_count @@ -1121,7 +1127,7 @@ def hide_button_during_search(btn: TreeViewButton): category_scrollview.size=(Window.width / 2, scrollview_height) - def location_button_callback(self, location_id, button): + def location_button_callback(self, location_id: int, button: TreeViewButton): if button.text not in self.ctx.location_names_to_id: raise Exception("Locations were not loaded correctly. Please reconnect your client.") @@ -1154,7 +1160,7 @@ def location_button_callback(self, location_id, button): # message = [{"cmd": 'LocationChecks', "locations": [location_id]}] # self.ctx.send_msgs(message) - def location_scout_callback(self, location_id, button): + def location_scout_callback(self, location_id: int, button: TreeViewButton) -> None: # if the mouse is currently hovering over any of the controls/tabs at the top of the client, ignore clicks for location buttons underneath if self.are_top_controls_at_mouse_pos(): # if there's an obj in the top controls/tab at the current mouse position, click it instead From a00484db68b647c79dab7b33e0ccaf6441496a38 Mon Sep 17 00:00:00 2001 From: Valentin Courtois Date: Tue, 14 Apr 2026 13:48:42 +0200 Subject: [PATCH 6/6] removed unneeded TODO --- src/ManualClient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ManualClient.py b/src/ManualClient.py index 4d46381c..3ac9d9bd 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -1175,7 +1175,6 @@ def location_scout_callback(self, location_id: int, button: TreeViewButton) -> N if location_id: self.ctx.locations_scouted.append(location_id) self.ctx.syncing = True - # TODO Replace the scout button with the placeholder spacer to maintain 2-column grid structure (did not manage to make it work) def victory_button_callback(self, button): # if the mouse is currently hovering over any of the controls/tabs at the top of the client, ignore clicks for location buttons underneath