Files
Grinch-AP/worlds/sc2/client_gui.py
Ziktofel 5f1835c546 SC2: Content update (#5312)
Feature highlights:
- Adds many content to the SC2 game
- Allows custom mission order
- Adds race-swapped missions for build missions (except Epilogue and NCO)
- Allows War Council Nerfs (Protoss units can get pre - War Council State, alternative units get another custom nerf to match the power level of base units)
- Revamps Predator's upgrade tree (never was considered strategically important)
- Adds some units and upgrades
- Locked and excluded items can specify quantity
- Key mode (if opt-in, missions require keys to be unlocked on top of their regular regular requirements
- Victory caches - Victory locations can grant multiple items to the multiworld instead of one 
- The generator is more resilient for generator failures as it validates logic for item excludes
- Fixes the following issues:
  - https://github.com/ArchipelagoMW/Archipelago/issues/3531 
  - https://github.com/ArchipelagoMW/Archipelago/issues/3548
2025-09-02 17:40:58 +02:00

656 lines
29 KiB
Python

from typing import *
import asyncio
import logging
from BaseClasses import ItemClassification
from NetUtils import JSONMessagePart
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser, LogtoUI
from kivy.app import App
from kivy.clock import Clock
from kivy.core.clipboard import Clipboard
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.tooltip import MDTooltip
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty, BooleanProperty, NumericProperty
from .client import SC2Context, calc_unfinished_nodes, is_mission_available, compute_received_items, STARCRAFT2
from .item.item_descriptions import item_descriptions
from .item.item_annotations import ITEM_NAME_ANNOTATIONS
from .mission_order.entry_rules import RuleData, SubRuleRuleData, ItemRuleData
from .mission_tables import lookup_id_to_mission, campaign_race_exceptions, \
SC2Mission, SC2Race
from .locations import LocationType, lookup_location_id_to_type, lookup_location_id_to_flags
from .options import LocationInclusion, MissionOrderScouting
from . import SC2World
class HoverableButton(HoverBehavior, Button):
pass
class MissionButton(HoverableButton, MDTooltip):
tooltip_text = StringProperty("Test")
mission_id = NumericProperty(-1)
is_exit = BooleanProperty(False)
is_goal = BooleanProperty(False)
showing_tooltip = BooleanProperty(False)
def __init__(self, *args, **kwargs):
super(HoverableButton, self).__init__(**kwargs)
self._tooltip = ServerToolTip(text=self.text, markup=True)
self._tooltip.padding = [5, 2, 5, 2]
def on_enter(self):
self._tooltip.text = self.tooltip_text
if self.tooltip_text != "":
self.display_tooltip()
def on_leave(self):
self.remove_tooltip()
def display_tooltip(self, *args):
self.showing_tooltip = True
return super().display_tooltip(*args)
def remove_tooltip(self, *args):
self.showing_tooltip = False
return super().remove_tooltip(*args)
@property
def ctx(self) -> SC2Context:
return App.get_running_app().ctx
class CampaignScroll(ScrollView):
border_on = BooleanProperty(False)
class MultiCampaignLayout(GridLayout):
pass
class DownloadDataWarningMessage(Label):
pass
class CampaignLayout(GridLayout):
pass
class RegionLayout(GridLayout):
pass
class ColumnLayout(GridLayout):
pass
class MissionLayout(GridLayout):
pass
class MissionCategory(GridLayout):
pass
class SC2JSONtoKivyParser(KivyJSONtoTextParser):
def _handle_item_name(self, node: JSONMessagePart):
item_name = node["text"]
if self.ctx.slot_info[node["player"]].game != STARCRAFT2 or item_name not in item_descriptions:
return super()._handle_item_name(node)
flags = node.get("flags", 0)
item_types = []
if flags & ItemClassification.progression:
item_types.append("progression")
if flags & ItemClassification.useful:
item_types.append("useful")
if flags & ItemClassification.trap:
item_types.append("trap")
if not item_types:
item_types.append("normal")
# TODO: Some descriptions are too long and get cut off. Is there a general solution or does someone need to manually check every description?
desc = item_descriptions[item_name].replace(". \n", ".<br>").replace(". ", ".<br>").replace("\n", "<br>")
annotation = ITEM_NAME_ANNOTATIONS.get(item_name)
if annotation is not None:
desc = f"{annotation}<br>{desc}"
ref = "Item Class: " + ", ".join(item_types) + "<br><br>" + desc
node.setdefault("refs", []).append(ref)
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
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):
base_title = "Archipelago Starcraft 2 Client"
campaign_panel: Optional[MultiCampaignLayout] = None
campaign_scroll_panel: Optional[CampaignScroll] = None
last_checked_locations: Set[int] = set()
last_items_received: List[int] = []
last_shown_tooltip: int = -1
last_data_out_of_date = False
mission_buttons: List[MissionButton] = []
launching: Union[bool, int] = False # if int -> mission ID
refresh_from_launching = True
first_check = True
first_mission = ""
button_colors: Dict[SC2Race, Tuple[float, float, float]] = {}
ctx: SC2Context
def __init__(self, ctx: SC2Context) -> None:
super().__init__(ctx)
self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx)
self.minimized = False
def on_start(self) -> None:
from . import gui_config
warnings, window_width, window_height = gui_config.get_window_defaults()
from kivy.core.window import Window
original_size_x, original_size_y = Window.size
Window.size = window_width, window_height
Window.left -= max((window_width - original_size_x) // 2, 0)
Window.top -= max((window_height - original_size_y) // 2, 0)
# Add the logging handler manually here instead of using `logging_pairs` to avoid adding 2 unnecessary tabs
logging.getLogger("Starcraft2").addHandler(LogtoUI(self.log_panels["All"].on_log))
for startup_warning in warnings:
logging.getLogger("Starcraft2").warning(f"Startup WARNING: {startup_warning}")
for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG):
errors, color = gui_config.get_button_color(race.name)
self.button_colors[race] = color
for error in errors:
logging.getLogger("Starcraft2").warning(f"{race.name.title()} button color setting: {error}")
def clear_tooltip(self) -> None:
for button in self.mission_buttons:
button.remove_tooltip()
def shown_tooltip(self) -> int:
for button in self.mission_buttons:
if button.showing_tooltip:
return button.mission_id
return -1
def build(self):
container = super().build()
panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll())
self.campaign_scroll_panel = panel.content
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 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()
return
sorted_items_received = sorted([item.item for item in self.ctx.items_received])
shown_tooltip = self.shown_tooltip()
hovering_tooltip = (
self.last_shown_tooltip != -1
and self.last_shown_tooltip == shown_tooltip
)
data_changed = (
self.last_checked_locations != self.ctx.checked_locations
or self.last_items_received != sorted_items_received
)
needs_redraw = (
data_changed
and not hovering_tooltip
or not self.refresh_from_launching
or self.last_data_out_of_date != self.ctx.data_out_of_date
or self.first_check
)
self.last_shown_tooltip = shown_tooltip
if not needs_redraw:
return
assert self.campaign_panel is not None
self.refresh_from_launching = True
self.clear_tooltip()
self.campaign_panel.clear_widgets()
if self.ctx.data_out_of_date:
self.campaign_panel.add_widget(Label(text="", padding=[0, 5, 0, 5]))
warning_label = DownloadDataWarningMessage(
text="Map/Mod data is out of date. Run /download_data in the client",
padding=[0, 25, 0, 25],
)
self.campaign_scroll_panel.border_on = True
self.campaign_panel.add_widget(warning_label)
else:
self.campaign_scroll_panel.border_on = False
self.last_data_out_of_date = self.ctx.data_out_of_date
if len(self.ctx.custom_mission_order) == 0:
self.campaign_panel.add_widget(Label(text="Connect to a world to see a mission layout here."))
return
self.last_checked_locations = self.ctx.checked_locations.copy()
self.last_items_received = sorted_items_received
self.first_check = False
self.mission_buttons = []
available_missions, available_layouts, available_campaigns, unfinished_missions = calc_unfinished_nodes(self.ctx)
# The MultiCampaignLayout widget needs a default height of 15 (set in the .kv) to display the above Labels correctly
multi_campaign_layout_height = 15
# Fetching IDs of all the locations with hints
self.hints_to_highlight = []
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}")
if hints:
for hint in hints:
if hint['finding_player'] == self.ctx.slot and not hint['found']:
self.hints_to_highlight.append(hint['location'])
MISSION_BUTTON_HEIGHT = 50
MISSION_BUTTON_PADDING = 6
for campaign_idx, campaign in enumerate(self.ctx.custom_mission_order):
longest_column = max(len(col) for layout in campaign.layouts for col in layout.missions)
if longest_column == 1:
campaign_layout_height = 115
else:
campaign_layout_height = (longest_column + 2) * (MISSION_BUTTON_HEIGHT + MISSION_BUTTON_PADDING)
multi_campaign_layout_height += campaign_layout_height
campaign_layout = CampaignLayout(size_hint_y=None, height=campaign_layout_height)
campaign_layout.add_widget(
Label(text=campaign.name, size_hint_y=None, height=25, outline_width=1)
)
mission_layout = MissionLayout(padding=[10,0,10,0])
for layout_idx, layout in enumerate(campaign.layouts):
layout_panel = RegionLayout()
layout_panel.add_widget(
Label(text=layout.name, size_hint_y=None, height=25, outline_width=1))
column_panel = ColumnLayout()
for column in layout.missions:
category_panel = MissionCategory(padding=[3,MISSION_BUTTON_PADDING,3,MISSION_BUTTON_PADDING])
for mission in column:
mission_id = mission.mission_id
# Empty mission slots
if mission_id == -1:
column_spacer = Label(text='', size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
category_panel.add_widget(column_spacer)
continue
mission_obj = lookup_id_to_mission[mission_id]
mission_finished = self.ctx.is_mission_completed(mission_id)
is_layout_exit = mission_id in layout.exits and not mission_finished
is_campaign_exit = mission_id in campaign.exits and not mission_finished
text, tooltip = self.mission_text(
self.ctx, mission_id, mission_obj,
layout_idx, is_layout_exit, layout.name,
campaign_idx, is_campaign_exit, campaign.name,
available_missions, available_layouts, available_campaigns, unfinished_missions
)
mission_button = MissionButton(text=text, size_hint_y=None, height=MISSION_BUTTON_HEIGHT)
mission_button.mission_id = mission_id
if mission_id in self.ctx.final_mission_ids:
mission_button.is_goal = True
if is_layout_exit or is_campaign_exit:
mission_button.is_exit = True
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)
if race in self.button_colors:
mission_button.background_color = self.button_colors[race]
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_buttons.append(mission_button)
category_panel.add_widget(mission_button)
# layout_panel.add_widget(Label(text=""))
column_panel.add_widget(category_panel)
layout_panel.add_widget(column_panel)
mission_layout.add_widget(layout_panel)
campaign_layout.add_widget(mission_layout)
self.campaign_panel.add_widget(campaign_layout)
self.campaign_panel.height = multi_campaign_layout_height
# For some reason the AP HoverBehavior won't send an enter event if a button spawns under the cursor,
# so manually send an enter event if a button is hovered immediately
for button in self.mission_buttons:
if button.hovered:
button.dispatch("on_enter")
break
def mission_text(
self, ctx: SC2Context, mission_id: int, mission_obj: SC2Mission,
layout_id: int, is_layout_exit: bool, layout_name: str, campaign_id: int, is_campaign_exit: bool, campaign_name: str,
available_missions: List[int], available_layouts: Dict[int, List[int]], available_campaigns: List[int],
unfinished_missions: List[int]
) -> Tuple[str, str]:
COLOR_MISSION_IMPORTANT = "6495ED" # blue
COLOR_MISSION_UNIMPORTANT = "A0BEF4" # lighter blue
COLOR_MISSION_CLEARED = "FFFFFF" # white
COLOR_MISSION_LOCKED = "A9A9A9" # gray
COLOR_PARENT_LOCKED = "848484" # darker gray
COLOR_MISSION_FINAL = "FFBC95" # orange
COLOR_MISSION_FINAL_LOCKED = "D0C0BE" # gray + orange
COLOR_FINAL_PARENT_LOCKED = "D0C0BE" # gray + orange
COLOR_FINAL_MISSION_REMINDER = "FF5151" # light red
COLOR_VICTORY_LOCATION = "FFC156" # gold
COLOR_TOOLTIP_DONE = "51FF51" # light green
COLOR_TOOLTIP_NOT_DONE = "FF5151" # light red
text = mission_obj.mission_name
tooltip: str = ""
remaining_locations, plando_locations, remaining_count = self.sort_unfinished_locations(mission_id)
campaign_locked = campaign_id not in available_campaigns
layout_locked = layout_id not in available_layouts[campaign_id]
# Map has uncollected locations
if mission_id in unfinished_missions:
if self.any_valuable_locations(remaining_locations):
text = f"[color={COLOR_MISSION_IMPORTANT}]{text}[/color]"
else:
text = f"[color={COLOR_MISSION_UNIMPORTANT}]{text}[/color]"
elif mission_id in available_missions:
text = f"[color={COLOR_MISSION_CLEARED}]{text}[/color]"
# Map requirements not met
else:
mission_rule, layout_rule, campaign_rule = ctx.mission_id_to_entry_rules[mission_id]
mission_has_rule = mission_rule.amount > 0
layout_has_rule = layout_rule.amount > 0
extra_reqs = False
if campaign_locked:
text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]"
tooltip += "To unlock this campaign, "
shown_rule = campaign_rule
extra_reqs = layout_has_rule or mission_has_rule
elif layout_locked:
text = f"[color={COLOR_PARENT_LOCKED}]{text}[/color]"
tooltip += "To unlock this questline, "
shown_rule = layout_rule
extra_reqs = mission_has_rule
else:
text = f"[color={COLOR_MISSION_LOCKED}]{text}[/color]"
tooltip += "To unlock this mission, "
shown_rule = mission_rule
rule_tooltip = shown_rule.tooltip(0, lookup_id_to_mission, COLOR_TOOLTIP_DONE, COLOR_TOOLTIP_NOT_DONE)
tooltip += rule_tooltip.replace(rule_tooltip[0], rule_tooltip[0].lower(), 1)
extra_word = "are"
if shown_rule.shows_single_rule():
extra_word = "is"
tooltip += "."
if extra_reqs:
tooltip += f"\nThis mission has additional requirements\nthat will be shown once the above {extra_word} met."
# Mark exit missions
exit_for: str = ""
if is_layout_exit:
exit_for += layout_name if layout_name else "this questline"
if is_campaign_exit:
if exit_for:
exit_for += " and "
exit_for += campaign_name if campaign_name else "this campaign"
if exit_for:
if tooltip:
tooltip += "\n\n"
tooltip += f"Required to beat {exit_for}"
# Mark goal missions
if mission_id in self.ctx.final_mission_ids:
if mission_id in available_missions:
text = f"[color={COLOR_MISSION_FINAL}]{mission_obj.mission_name}[/color]"
elif campaign_locked or layout_locked:
text = f"[color={COLOR_FINAL_PARENT_LOCKED}]{mission_obj.mission_name}[/color]"
else:
text = f"[color={COLOR_MISSION_FINAL_LOCKED}]{mission_obj.mission_name}[/color]"
if tooltip and not exit_for:
tooltip += "\n\n"
elif exit_for:
tooltip += "\n"
if any(location_type == LocationType.VICTORY for (location_type, _, _) in remaining_locations):
tooltip += f"[color={COLOR_FINAL_MISSION_REMINDER}]Required to beat the world[/color]"
else:
tooltip += "This goal mission is already beaten.\nBeat the remaining goal missions to beat the world."
# Populate remaining location list
if remaining_count > 0:
if tooltip:
tooltip += "\n\n"
tooltip += f"[b][color={COLOR_MISSION_IMPORTANT}]Uncollected locations[/color][/b]"
last_location_type = LocationType.VICTORY
victory_printed = False
if self.ctx.mission_order_scouting != MissionOrderScouting.option_none:
mission_available = mission_id in available_missions
scoutable = self.is_scoutable(remaining_locations, mission_available, layout_locked, campaign_locked)
else:
scoutable = False
for location_type, location_name, _ in remaining_locations:
if location_type in (LocationType.VICTORY, LocationType.VICTORY_CACHE) and victory_printed:
continue
if location_type != last_location_type:
tooltip += f"\n[color={COLOR_MISSION_IMPORTANT}]{self.get_location_type_title(location_type)}:[/color]"
last_location_type = location_type
if location_type == LocationType.VICTORY:
victory_count = len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)])
victory_loc = location_name.replace(":", f":[color={COLOR_VICTORY_LOCATION}]")
if victory_count > 1:
victory_loc += f' ({victory_count})'
tooltip += f"\n- {victory_loc}[/color]"
victory_printed = True
else:
tooltip += f"\n- {location_name}"
if scoutable:
tooltip += self.handle_scout_display(location_name)
if len(plando_locations) > 0:
tooltip += "\n[b]Plando:[/b]\n- "
tooltip += "\n- ".join(plando_locations)
tooltip = f"[b]{text}[/b]\n" + tooltip
#If the mission has any hints pointing to a check, add asterisks around the mission name
if any(tuple(x in self.hints_to_highlight for x in self.ctx.locations_for_mission_id(mission_id))):
text = "* " + text + " *"
return text, tooltip
def mission_callback(self, button: MissionButton) -> None:
if button.last_touch.button == 'right':
self.open_mission_menu(button)
return
if not self.launching:
mission_id: int = button.mission_id
if self.ctx.play_mission(mission_id):
self.launching = mission_id
Clock.schedule_once(self.finish_launching, 10)
def open_mission_menu(self, button: MissionButton) -> None:
# Will be assigned later, used to close menu in callbacks
menu = None
mission_id = button.mission_id
def copy_mission_name():
Clipboard.copy(lookup_id_to_mission[mission_id].mission_name)
menu.dismiss()
menu_items = [
{
"text": "Copy Mission Name",
"on_release": copy_mission_name,
}
]
width_override = None
hinted_item_ids = Counter()
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}")
if hints:
for hint in hints:
if hint['receiving_player'] == self.ctx.slot and not hint['found']:
hinted_item_ids[hint['item']] += 1
if not self.ctx.is_mission_completed(mission_id) and not is_mission_available(self.ctx, mission_id):
# Uncompleted and inaccessible missions can have items hinted if they're needed
# The inaccessible restriction is to ensure users don't waste hints on missions that they can already access
items_needed = self.resolve_items_needed(mission_id)
received_items = compute_received_items(self.ctx)
for item_id, amount in items_needed.items():
# If we have already received or hinted enough of this item, skip it
if received_items[item_id] + hinted_item_ids[item_id] >= amount:
continue
if width_override is None:
width_override = dp(500)
item_name = self.ctx.item_names.lookup_in_game(item_id)
label_text = f"Hint Required Item: {item_name}"
def hint_and_close():
self.ctx.command_processor(self.ctx)(f"!hint {item_name}")
menu.dismiss()
menu_items.append({
"text": label_text,
"on_release": hint_and_close,
})
menu = MDDropdownMenu(
caller=button,
items=menu_items,
**({"width": width_override} if width_override else {}),
)
menu.open()
def resolve_items_needed(self, mission_id: int) -> Counter[int]:
def resolve_rule_to_items(rule: RuleData) -> Counter[int]:
if isinstance(rule, SubRuleRuleData):
all_items = Counter()
for sub_rule in rule.sub_rules:
# Take max of each item across all sub-rules
all_items |= resolve_rule_to_items(sub_rule)
return all_items
elif isinstance(rule, ItemRuleData):
return Counter(rule.item_ids)
else:
return Counter()
rules = self.ctx.mission_id_to_entry_rules[mission_id]
# Take max value of each item across all rules using '|'
return (resolve_rule_to_items(rules.mission_rule) |
resolve_rule_to_items(rules.layout_rule) |
resolve_rule_to_items(rules.campaign_rule))
def finish_launching(self, dt):
self.launching = False
def sort_unfinished_locations(self, mission_id: int) -> Tuple[List[Tuple[LocationType, str, int]], List[str], int]:
locations: List[Tuple[LocationType, str, int]] = []
location_name_to_index: Dict[str, int] = {}
for loc in self.ctx.locations_for_mission_id(mission_id):
if loc in self.ctx.missing_locations:
location_name = self.ctx.location_names.lookup_in_game(loc)
location_name_to_index[location_name] = len(locations)
locations.append((
lookup_location_id_to_type[loc],
location_name,
loc,
))
count = len(locations)
plando_locations = []
elements_to_remove: Set[Tuple[LocationType, str, int]] = set()
for plando_loc_name in self.ctx.plando_locations:
if plando_loc_name in location_name_to_index:
elements_to_remove.add(locations[location_name_to_index[plando_loc_name]])
plando_locations.append(plando_loc_name)
for element in elements_to_remove:
locations.remove(element)
return sorted(locations), plando_locations, count
def any_valuable_locations(self, locations: List[Tuple[LocationType, str, int]]) -> bool:
for location_type, _, location_id in locations:
if (self.ctx.location_inclusions[location_type] == LocationInclusion.option_enabled
and all(
self.ctx.location_inclusions_by_flag[flag] == LocationInclusion.option_enabled
for flag in lookup_location_id_to_flags[location_id].values()
)
):
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_filler:
title += " (Filler)"
else:
title += ""
return title
def is_scoutable(self, remaining_locations, mission_available: bool, layout_locked: bool, campaign_locked: bool) -> bool:
if self.ctx.mission_order_scouting == MissionOrderScouting.option_all:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_campaign and not campaign_locked:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_layout and not layout_locked:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_available and mission_available:
return True
elif self.ctx.mission_order_scouting == MissionOrderScouting.option_completed and len([loc for loc in remaining_locations if loc[0] in (LocationType.VICTORY, LocationType.VICTORY_CACHE)]) == 0:
# Assuming that when a mission is completed, all victory location are removed
return True
else:
return False
def handle_scout_display(self, location_name: str) -> str:
if self.ctx.mission_item_classification is None:
return ""
# Only one information is provided for the victory locations of a mission
if " Cache (" in location_name:
location_name = location_name.split(" Cache")[0]
item_classification_key = self.ctx.mission_item_classification[location_name]
if ((ItemClassification.progression & item_classification_key)
and (ItemClassification.useful & item_classification_key)
):
# Uncommon, but some games do this to show off that an item is super-important
# This can also happen on a victory display if the cache holds both progression and useful
return " [color=AF99EF](Useful+Progression)[/color]"
if ItemClassification.progression & item_classification_key:
return " [color=AF99EF](Progression)[/color]"
if ItemClassification.useful & item_classification_key:
return " [color=6D8BE8](Useful)[/color]"
if SC2World.settings.show_traps and ItemClassification.trap & item_classification_key:
return " [color=FA8072](Trap)[/color]"
return " [color=00EEEE](Filler)[/color]"
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)