from typing import * import asyncio from NetUtils import JSONMessagePart from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem from kivy.uix.gridlayout import GridLayout from kivy.lang import Builder from kivy.uix.label import Label from kivy.uix.button import Button from kivy.uix.floatlayout import FloatLayout from kivy.uix.scrollview import ScrollView from kivy.properties import StringProperty from .Client import SC2Context, calc_unfinished_missions, parse_unlock from .MissionTables import (lookup_id_to_mission, lookup_name_to_mission, campaign_race_exceptions, SC2Mission, SC2Race, SC2Campaign) from .Locations import LocationType, lookup_location_id_to_type from .Options import LocationInclusion from . import SC2World, get_first_mission class HoverableButton(HoverBehavior, Button): pass class MissionButton(HoverableButton): tooltip_text = StringProperty("Test") def __init__(self, *args, **kwargs): super(HoverableButton, self).__init__(*args, **kwargs) self.layout = FloatLayout() self.popuplabel = ServerToolTip(text=self.text, markup=True) self.popuplabel.padding = [5, 2, 5, 2] self.layout.add_widget(self.popuplabel) def on_enter(self): self.popuplabel.text = self.tooltip_text if self.ctx.current_tooltip: App.get_running_app().root.remove_widget(self.ctx.current_tooltip) if self.tooltip_text == "": self.ctx.current_tooltip = None else: App.get_running_app().root.add_widget(self.layout) self.ctx.current_tooltip = self.layout def on_leave(self): self.ctx.ui.clear_tooltip() @property def ctx(self) -> SC2Context: return App.get_running_app().ctx class CampaignScroll(ScrollView): pass class MultiCampaignLayout(GridLayout): pass class CampaignLayout(GridLayout): pass class MissionLayout(GridLayout): pass class MissionCategory(GridLayout): pass class SC2JSONtoKivyParser(KivyJSONtoTextParser): def _handle_text(self, node: JSONMessagePart): if node.get("keep_markup", False): for ref in node.get("refs", []): node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" self.ref_count += 1 return super(KivyJSONtoTextParser, self)._handle_text(node) else: return super()._handle_text(node) class SC2Manager(GameManager): logging_pairs = [ ("Client", "Archipelago"), ("Starcraft2", "Starcraft2"), ] base_title = "Archipelago Starcraft 2 Client" campaign_panel: Optional[CampaignLayout] = None last_checked_locations: Set[int] = set() mission_id_to_button: Dict[int, MissionButton] = {} launching: Union[bool, int] = False # if int -> mission ID refresh_from_launching = True first_check = True first_mission = "" ctx: SC2Context def __init__(self, ctx) -> None: super().__init__(ctx) self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) def clear_tooltip(self) -> None: if self.ctx.current_tooltip: App.get_running_app().root.remove_widget(self.ctx.current_tooltip) self.ctx.current_tooltip = None def build(self): container = super().build() panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) self.campaign_panel = MultiCampaignLayout() panel.content.add_widget(self.campaign_panel) Clock.schedule_interval(self.build_mission_table, 0.5) return container def build_mission_table(self, dt) -> None: if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or not self.refresh_from_launching)) or self.first_check: assert self.campaign_panel is not None self.refresh_from_launching = True self.campaign_panel.clear_widgets() if self.ctx.mission_req_table: self.last_checked_locations = self.ctx.checked_locations.copy() self.first_check = False self.first_mission = get_first_mission(self.ctx.mission_req_table) self.mission_id_to_button = {} available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) multi_campaign_layout_height = 0 for campaign, missions in sorted(self.ctx.mission_req_table.items(), key=lambda item: item[0].id): categories: Dict[str, List[str]] = {} # separate missions into categories for mission_index in missions: mission_info = self.ctx.mission_req_table[campaign][mission_index] if mission_info.category not in categories: categories[mission_info.category] = [] categories[mission_info.category].append(mission_index) max_mission_count = max(len(categories[category]) for category in categories) if max_mission_count == 1: campaign_layout_height = 115 else: campaign_layout_height = (max_mission_count + 2) * 50 multi_campaign_layout_height += campaign_layout_height campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height) if campaign != SC2Campaign.GLOBAL: campaign_layout.add_widget( Label(text=campaign.campaign_name, size_hint_y=None, height=25, outline_width=1) ) mission_layout = MissionLayout() for category in categories: category_name_height = 0 category_spacing = 3 if category.startswith('_'): category_display_name = '' else: category_display_name = category category_name_height += 25 category_spacing = 10 category_panel = MissionCategory(padding=[category_spacing,6,category_spacing,6]) category_panel.add_widget( Label(text=category_display_name, size_hint_y=None, height=category_name_height, outline_width=1)) for mission in categories[category]: text: str = mission tooltip: str = "" mission_obj: SC2Mission = lookup_name_to_mission[mission] mission_id: int = mission_obj.id mission_data = self.ctx.mission_req_table[campaign][mission] remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission) # Map has uncollected locations if mission in unfinished_missions: if self.any_valuable_locations(remaining_locations): text = f"[color=6495ED]{text}[/color]" else: text = f"[color=A0BEF4]{text}[/color]" elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met else: text = f"[color=a9a9a9]{text}[/color]" tooltip = f"Requires: " if mission_data.required_world: tooltip += ", ".join(list(self.ctx.mission_req_table[parse_unlock(req_mission).campaign])[parse_unlock(req_mission).connect_to - 1] for req_mission in mission_data.required_world) if mission_data.number: tooltip += " and " if mission_data.number: tooltip += f"{self.ctx.mission_req_table[campaign][mission].number} missions completed" if mission_id == self.ctx.final_mission: if mission in available_missions: text = f"[color=FFBC95]{mission}[/color]" else: text = f"[color=D0C0BE]{mission}[/color]" if tooltip: tooltip += "\n" tooltip += "Final Mission" if remaining_count > 0: if tooltip: tooltip += "\n\n" tooltip += f"-- Uncollected locations --" for loctype in LocationType: if len(remaining_locations[loctype]) > 0: if loctype == LocationType.VICTORY: tooltip += f"\n- {remaining_locations[loctype][0]}" else: tooltip += f"\n{self.get_location_type_title(loctype)}:\n- " tooltip += "\n- ".join(remaining_locations[loctype]) if len(plando_locations) > 0: tooltip += f"\nPlando:\n- " tooltip += "\n- ".join(plando_locations) MISSION_BUTTON_HEIGHT = 50 for pad in range(mission_data.ui_vertical_padding): column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT) category_panel.add_widget(column_spacer) mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT) mission_race = mission_obj.race if mission_race == SC2Race.ANY: mission_race = mission_obj.campaign.race race = campaign_race_exceptions.get(mission_obj, mission_race) racial_colors = { SC2Race.TERRAN: (0.24, 0.84, 0.68), SC2Race.ZERG: (1, 0.65, 0.37), SC2Race.PROTOSS: (0.55, 0.7, 1) } if race in racial_colors: mission_button.background_color = racial_colors[race] mission_button.tooltip_text = tooltip mission_button.bind(on_press=self.mission_callback) self.mission_id_to_button[mission_id] = mission_button category_panel.add_widget(mission_button) category_panel.add_widget(Label(text="")) mission_layout.add_widget(category_panel) campaign_layout.add_widget(mission_layout) self.campaign_panel.add_widget(campaign_layout) self.campaign_panel.height = multi_campaign_layout_height elif self.launching: assert self.campaign_panel is not None self.refresh_from_launching = False self.campaign_panel.clear_widgets() self.campaign_panel.add_widget(Label(text="Launching Mission: " + lookup_id_to_mission[self.launching].mission_name)) if self.ctx.ui: self.ctx.ui.clear_tooltip() def mission_callback(self, button: MissionButton) -> None: if not self.launching: mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button) if self.ctx.play_mission(mission_id): self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) def finish_launching(self, dt): self.launching = False def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationType, List[str]], List[str], int]: locations: Dict[LocationType, List[str]] = {loctype: [] for loctype in LocationType} count = 0 for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_game(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: for loctype in LocationType: if plando_loc in locations[loctype]: locations[loctype].remove(plando_loc) plando_locations.append(plando_loc) return locations, plando_locations, count def any_valuable_locations(self, locations: Dict[LocationType, List[str]]) -> bool: for loctype in LocationType: if len(locations[loctype]) > 0 and self.ctx.location_inclusions[loctype] == LocationInclusion.option_enabled: return True return False def get_location_type_title(self, location_type: LocationType) -> str: title = location_type.name.title().replace("_", " ") if self.ctx.location_inclusions[location_type] == LocationInclusion.option_disabled: title += " (Nothing)" elif self.ctx.location_inclusions[location_type] == LocationInclusion.option_resources: title += " (Resources)" else: title += "" return title def start_gui(context: SC2Context): context.ui = SC2Manager(context) context.ui_task = asyncio.create_task(context.ui.async_run(), name="UI") import pkgutil data = pkgutil.get_data(SC2World.__module__, "Starcraft2.kv").decode() Builder.load_string(data)