| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | from typing import * | 
					
						
							|  |  |  | import asyncio | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-17 23:18:43 +02:00
										 |  |  | from NetUtils import JSONMessagePart | 
					
						
							|  |  |  | from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-27 08:51:27 +02:00
										 |  |  | 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 | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-17 23:18:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | 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) | 
					
						
							| 
									
										
										
										
											2024-09-17 23:18:43 +02:00
										 |  |  |         self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-22 09:24:14 -05:00
										 |  |  |         panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2024-06-16 05:37:05 -05:00
										 |  |  |                 locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_game(loc)) | 
					
						
							| 
									
										
										
										
											2024-03-15 17:33:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         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) |