From 3afb16864e693b19abe43cce8c5a7492a792cd24 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:04:14 -0500 Subject: [PATCH] update hint tab with status of hinted locations (#4) --- worlds/tracker/TrackerClient.py | 184 +++++++++++++++++++------------- 1 file changed, 111 insertions(+), 73 deletions(-) diff --git a/worlds/tracker/TrackerClient.py b/worlds/tracker/TrackerClient.py index af1a0444496..7329f1e6a15 100644 --- a/worlds/tracker/TrackerClient.py +++ b/worlds/tracker/TrackerClient.py @@ -3,15 +3,15 @@ import traceback import typing from collections.abc import Callable -from CommonClient import CommonContext, gui_enabled, get_base_parser, server_loop,ClientCommandProcessor +from CommonClient import CommonContext, gui_enabled, get_base_parser, server_loop, ClientCommandProcessor import os import time import sys from typing import Dict, Optional, List -from BaseClasses import Region,Location,ItemClassification +from BaseClasses import Region, Location, ItemClassification -from BaseClasses import CollectionState,MultiWorld,LocationProgressType -from worlds.generic.Rules import exclusion_rules,locality_rules +from BaseClasses import CollectionState, MultiWorld, LocationProgressType +from worlds.generic.Rules import exclusion_rules, locality_rules from Options import StartInventoryPool from settings import get_settings from Utils import __version__, output_path @@ -21,7 +21,7 @@ from Generate import main as GMain, mystery_argparse -#webserver imports +# webserver imports import urllib.parse logger = logging.getLogger("Client") @@ -30,37 +30,35 @@ ITEMS_HANDLING = 0b111 - - class TrackerCommandProcessor(ClientCommandProcessor): def _cmd_inventory(self): """Print the list of current items in the inventory""" logger.info("Current Inventory:") all_items, prog_items, events = updateTracker(self.ctx) - for item,count in sorted(all_items.items()): - logger.info( str(count) + "x: " + item) + for item, count in sorted(all_items.items()): + logger.info(str(count) + "x: " + item) def _cmd_prog_inventory(self): """Print the list of current items in the inventory""" logger.info("Current Inventory:") all_items, prog_items, events = updateTracker(self.ctx) - for item,count in sorted(prog_items.items()): - logger.info( str(count) + "x: " + item) + for item, count in sorted(prog_items.items()): + logger.info(str(count) + "x: " + item) def _cmd_event_inventory(self): """Print the list of current items in the inventory""" logger.info("Current Inventory:") all_items, prog_items, events = updateTracker(self.ctx) for event in sorted(events): - logger.info( event) + logger.info(event) class TrackerGameContext(CommonContext): from kvui import GameManager game = "" httpServer_task: typing.Optional["asyncio.Task[None]"] = None - tags = CommonContext.tags|{"Tracker"} + tags = CommonContext.tags | {"Tracker"} command_processor = TrackerCommandProcessor tracker_page = None watcher_task = None @@ -75,30 +73,30 @@ def __init__(self, server_address, password): super().__init__(server_address, password) self.items_handling = ITEMS_HANDLING self.locations_checked = [] + self.locations_available = [] self.datapackage = [] - self.multiworld:MultiWorld = None + self.multiworld: MultiWorld = None self.player_id = None def clear_page(self): if self.tracker_page is not None: self.tracker_page.resetData() - def log_to_tab(self,line: str, sort:bool = False): + def log_to_tab(self, line: str, sort: bool = False): if self.tracker_page is not None: - self.tracker_page.addLine(line,sort) + self.tracker_page.addLine(line, sort) - def set_callback(self,func:Optional[Callable[[list[str]],bool]]=None): + def set_callback(self, func: Optional[Callable[[list[str]], bool]] = None): self.update_callback = func - - def set_region_callback(self,func:Optional[Callable[[list[str]],bool]] = None): + + def set_region_callback(self, func: Optional[Callable[[list[str]], bool]] = None): self.region_callback = func - def build_gui(self,manager : GameManager): + def build_gui(self, manager: GameManager): from kivy.uix.boxlayout import BoxLayout from kivy.uix.tabbedpanel import TabbedPanelItem from kivy.uix.recycleview import RecycleView - class TrackerLayout(BoxLayout): pass @@ -106,18 +104,18 @@ class TrackerView(RecycleView): def __init__(self, **kwargs): super().__init__(**kwargs) self.data = [] - self.data.append({"text":"Tracker v0.1.4 Initializing"}) - + self.data.append({"text": "Tracker v0.1.4 Initializing"}) + def resetData(self): self.data.clear() - def addLine(self, line:str,sort:bool = False): - self.data.append({"text":line}) + def addLine(self, line: str, sort: bool = False): + self.data.append({"text": line}) if sort: self.data.sort(key=lambda e: e["text"]) tracker_page = TabbedPanelItem(text="Tracker Page") - + try: tracker = TrackerLayout(orientation="horizontal") tracker_view = TrackerView() @@ -126,32 +124,62 @@ def addLine(self, line:str,sort:bool = False): tracker_page.content = tracker if self.gen_error is not None: for line in self.gen_error.split("\n"): - self.log_to_tab(line,False) + self.log_to_tab(line, False) except Exception as e: tb = traceback.format_exc() print(tb) manager.tabs.add_widget(tracker_page) - + + from kvui import HintLog + # hook hint tab + + def update_available_hints(log: HintLog, hints: typing.Set[typing.Dict[str, typing.Any]]): + data = [] + for hint in hints: + in_logic = int(hint["location"]) in self.locations_available \ + if int(hint["finding_player"]) == self.player_id else False + data.append({ + "receiving": { + "text": log.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, + "item": {"text": log.parser.handle_node( + {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "finding": {"text": log.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, + "location": {"text": log.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "entrance": {"text": log.parser.handle_node({"type": "color" if hint["entrance"] else "text", + "color": "blue", "text": hint["entrance"] + if hint["entrance"] else "Vanilla"})}, + "found": { + "text": log.parser.handle_node({"type": "color", "color": "green" if hint["found"] else + "yellow" if in_logic else "red", + "text": "Found" if hint["found"] else "In Logic" if in_logic + else "Not Found"})}, + }) + + data.sort(key=log.hint_sorter, reverse=log.reversed) + for i in range(0, len(data), 2): + data[i]["striped"] = True + data.insert(0, log.header) + log.data = data + + HintLog.refresh_hints = update_available_hints def run_gui(self): from kvui import GameManager class TrackerManager(GameManager): logging_pairs = [ - ("Client","Archipelago") + ("Client", "Archipelago") ] base_title = "Archipelago Tracker Client" - def build(self): container = super().build() self.tabs.do_default_tab = True self.tabs.current_tab.height = 40 self.tabs.tab_height = 40 self.ctx.build_gui(self) - - return container + return container self.ui = TrackerManager(self) self.load_kv() @@ -175,34 +203,33 @@ async def server_auth(self, password_requested: bool = False): def on_package(self, cmd: str, args: dict): if cmd == 'Connected': if self.multiworld is None: - self.log_to_tab("Internal world was not able to be generated, check your yamls and relaunch",False) + self.log_to_tab("Internal world was not able to be generated, check your yamls and relaunch", False) return - player_ids = [i for i,n in self.multiworld.player_name.items() if n==self.username] + player_ids = [i for i, n in self.multiworld.player_name.items() if n == self.username] if len(player_ids) < 1: - self.log_to_tab("Player's Yaml not in tracker's list",False) + self.log_to_tab("Player's Yaml not in tracker's list", False) return - self.player_id = player_ids[0] #should only really ever be one match + self.player_id = player_ids[0] # should only really ever be one match self.game = args["slot_info"][str(args["slot"])][1] - if callable(getattr(self.multiworld.worlds[self.player_id],"interpret_slot_data",None)): + if callable(getattr(self.multiworld.worlds[self.player_id], "interpret_slot_data", None)): temp = self.multiworld.worlds[self.player_id].interpret_slot_data(args["slot_data"]) if temp is not None: - self.re_gen_passthrough = {self.game : temp} + self.re_gen_passthrough = {self.game: temp} self.run_generator() updateTracker(self) self.watcher_task = asyncio.create_task(game_watcher(self), name="GameWatcher") elif cmd == 'RoomUpdate': updateTracker(self) - - + async def disconnect(self, allow_autoreconnect: bool = False): if "Tracker" in self.tags: self.game = "" self.re_gen_passthrough = None await super().disconnect(allow_autoreconnect) - - def _set_host_settings(self,host): + + def _set_host_settings(self, host): if 'universal_tracker' not in host: host['universal_tracker'] = {} if 'player_files_path' not in host['universal_tracker']: @@ -214,7 +241,7 @@ def _set_host_settings(self,host): if 'hide_excluded_locations' not in host['universal_tracker']: host['universal_tracker']['hide_excluded_locations'] = False report_type = "Both" - if host['universal_tracker']['include_location_name']: + if host['universal_tracker']['include_location_name']: if host['universal_tracker']['include_region_name']: report_type = "Both" else: @@ -222,13 +249,14 @@ def _set_host_settings(self,host): else: report_type = "Region" host.save() - return host['universal_tracker']['player_files_path'],report_type, host['universal_tracker']['hide_excluded_locations'] + return host['universal_tracker']['player_files_path'], report_type, host['universal_tracker'][ + 'hide_excluded_locations'] def run_generator(self): try: host = get_settings() - yaml_path ,self.output_format, self.hide_excluded = self._set_host_settings(host) - #strip command line args, they won't be useful from the client anyway + yaml_path, self.output_format, self.hide_excluded = self._set_host_settings(host) + # strip command line args, they won't be useful from the client anyway sys.argv = sys.argv[:1] args, _settings = mystery_argparse() if yaml_path: @@ -246,7 +274,7 @@ def run_generator(self): logger.error(tb) def TMain(self, args, seed=None, baked_server_options: Optional[Dict[str, object]] = None): - + if not baked_server_options: baked_server_options = get_settings().server_options.as_dict() assert isinstance(baked_server_options, dict) @@ -319,7 +347,8 @@ def TMain(self, args, seed=None, baked_server_options: Optional[Dict[str, object item_digits = len(str(max_item)) location_digits = len(str(max_location)) item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) - location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) + location_count = len( + str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) del max_item, max_location for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): @@ -346,14 +375,15 @@ def TMain(self, args, seed=None, baked_server_options: Optional[Dict[str, object for _ in range(count): world.push_precollected(world.create_item(item_name, player)) - for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): + for item_name, count in world.start_inventory_from_pool.setdefault(player, + StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) # remove from_pool items also from early items handling, as starting is plenty early. early = world.early_items[player].get(item_name, 0) if early: - world.early_items[player][item_name] = max(0, early-count) - remaining_count = count-early + world.early_items[player][item_name] = max(0, early - count) + remaining_count = count - early if remaining_count > 0: local_early = world.early_local_items[player].get(item_name, 0) if local_early: @@ -378,13 +408,15 @@ def TMain(self, args, seed=None, baked_server_options: Optional[Dict[str, object for player in world.player_ids: exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value) - world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value + world.worlds[player].options.priority_locations.value -= world.worlds[ + player].options.exclude_locations.value for location_name in world.worlds[player].options.priority_locations.value: try: location = world.get_location(location_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location if location_name not in world.worlds[player].location_name_to_id: - raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e + raise Exception( + f"Unable to prioritize location {location_name} in player {player}'s world.") from e else: location.progress_type = LocationProgressType.PRIORITY @@ -397,18 +429,19 @@ def TMain(self, args, seed=None, baked_server_options: Optional[Dict[str, object AutoWorld.call_all(world, "generate_basic") - self.multiworld = world return + def updateTracker(ctx: TrackerGameContext): if ctx.player_id is None or ctx.multiworld is None: logger.error("Player YAML not installed or Generator failed") - ctx.log_to_tab("Check Player YAMLs for error",False) + ctx.log_to_tab("Check Player YAMLs for error", False) return state = CollectionState(ctx.multiworld) - state.sweep_for_events(locations=(location for location in ctx.multiworld.get_locations() if ( not location.address))) + state.sweep_for_events( + locations=(location for location in ctx.multiworld.get_locations() if (not location.address))) prog_items = Counter() all_items = Counter() @@ -416,51 +449,59 @@ def updateTracker(ctx: TrackerGameContext): for item in ctx.items_received: try: - world_item = ctx.multiworld.create_item(ctx.multiworld.worlds[ctx.player_id].item_id_to_name[item[0]],ctx.player_id) - state.collect(world_item,True) + world_item = ctx.multiworld.create_item(ctx.multiworld.worlds[ctx.player_id].item_id_to_name[item[0]], + ctx.player_id) + state.collect(world_item, True) if world_item.classification == ItemClassification.progression or world_item.classification == ItemClassification.progression_skip_balancing: prog_items[world_item.name] += 1 if world_item.code is not None: all_items[world_item.name] += 1 except: - ctx.log_to_tab("Item id " + str(item[0]) + " not able to be created",False) - state.sweep_for_events(locations=(location for location in ctx.multiworld.get_locations() if ( not location.address))) - + ctx.log_to_tab("Item id " + str(item[0]) + " not able to be created", False) + state.sweep_for_events( + locations=(location for location in ctx.multiworld.get_locations() if (not location.address))) + ctx.clear_page() regions = [] - for temp_loc in ctx.multiworld.get_reachable_locations(state,ctx.player_id): - if temp_loc.address == None or isinstance(temp_loc.address,list): + locations = [] + for temp_loc in ctx.multiworld.get_reachable_locations(state, ctx.player_id): + if temp_loc.address == None or isinstance(temp_loc.address, list): continue elif ctx.hide_excluded and temp_loc.progress_type == LocationProgressType.EXCLUDED: continue try: if (temp_loc.address in ctx.missing_locations): - #logger.info("YES rechable (" + temp_loc.name + ")") + # logger.info("YES rechable (" + temp_loc.name + ")") region = "" if temp_loc.parent_region is None: region = "" else: region = temp_loc.parent_region.name if ctx.output_format == "Both": - ctx.log_to_tab(region + " | " + temp_loc.name,True ) + ctx.log_to_tab(region + " | " + temp_loc.name, True) elif ctx.output_format == "Location": - ctx.log_to_tab(temp_loc.name,True ) + ctx.log_to_tab(temp_loc.name, True) if region not in regions: regions.append(region) if ctx.output_format == "Region": - ctx.log_to_tab(region,True) + ctx.log_to_tab(region, True) callback_list.append(temp_loc.name) + locations.append(temp_loc.address) except: ctx.log_to_tab("ERROR: location " + temp_loc.name + " broke something, report this to discord") pass ctx.tracker_page.refresh_from_data() + ctx.locations_available = locations + if f"_read_hints_{ctx.team}_{ctx.slot}" in ctx.stored_data: + ctx.ui.update_hints() if ctx.update_callback is not None: ctx.update_callback(callback_list) if ctx.region_callback is not None: ctx.region_callback(regions) if len(callback_list) == 0: - ctx.log_to_tab("All " + str(len(ctx.checked_locations)) + " accessible locations have been checked! Congrats!") - return (all_items,prog_items,[item.name for item in state.events if item.player == ctx.player_id]) + ctx.log_to_tab("All " + str(len(ctx.checked_locations)) + " accessible locations have been checked! Congrats!") + return (all_items, prog_items, [item.name for item in state.events if item.player == ctx.player_id]) + async def game_watcher(ctx: TrackerGameContext) -> None: while not ctx.exit_event.is_set(): @@ -477,7 +518,6 @@ async def game_watcher(ctx: TrackerGameContext) -> None: async def main(args): - ctx = TrackerGameContext(args.connect, args.password) ctx.auth = args.name ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") @@ -492,7 +532,6 @@ async def main(args): def launch(): - parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") @@ -506,5 +545,4 @@ def launch(): if url.password: args.password = urllib.parse.unquote(url.password) - - asyncio.run(main(args)) \ No newline at end of file + asyncio.run(main(args))