1631 lines
		
	
	
		
			75 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1631 lines
		
	
	
		
			75 KiB
		
	
	
	
		
			Python
		
	
	
	
from __future__ import annotations
 | 
						|
 | 
						|
import asyncio
 | 
						|
import copy
 | 
						|
import ctypes
 | 
						|
import enum
 | 
						|
import inspect
 | 
						|
import logging
 | 
						|
import multiprocessing
 | 
						|
import os.path
 | 
						|
import re
 | 
						|
import sys
 | 
						|
import tempfile
 | 
						|
import typing
 | 
						|
import queue
 | 
						|
import zipfile
 | 
						|
import io
 | 
						|
import random
 | 
						|
import concurrent.futures
 | 
						|
from pathlib import Path
 | 
						|
 | 
						|
# CommonClient import first to trigger ModuleUpdater
 | 
						|
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
 | 
						|
from Utils import init_logging, is_windows, async_start
 | 
						|
from . import ItemNames, Options
 | 
						|
from .ItemGroups import item_name_groups
 | 
						|
from .Options import (
 | 
						|
    MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence,
 | 
						|
    GameSpeed, GenericUpgradeItems, GenericUpgradeResearch, ColorChoice, GenericUpgradeMissions,
 | 
						|
    LocationInclusion, ExtraLocations, MasteryLocations, ChallengeLocations, VanillaLocations,
 | 
						|
    DisableForcedCamera, SkipCutscenes, GrantStoryTech, GrantStoryLevels, TakeOverAIAllies, RequiredTactics,
 | 
						|
    SpearOfAdunPresence, SpearOfAdunPresentInNoBuild, SpearOfAdunAutonomouslyCastAbilityPresence,
 | 
						|
    SpearOfAdunAutonomouslyCastPresentInNoBuild
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    init_logging("SC2Client", exception_logger="Client")
 | 
						|
 | 
						|
logger = logging.getLogger("Client")
 | 
						|
sc2_logger = logging.getLogger("Starcraft2")
 | 
						|
 | 
						|
import nest_asyncio
 | 
						|
from worlds._sc2common import bot
 | 
						|
from worlds._sc2common.bot.data import Race
 | 
						|
from worlds._sc2common.bot.main import run_game
 | 
						|
from worlds._sc2common.bot.player import Bot
 | 
						|
from .Items import (lookup_id_to_name, get_full_item_list, ItemData, type_flaggroups, upgrade_numbers,
 | 
						|
                    upgrade_numbers_all)
 | 
						|
from .Locations import SC2WOL_LOC_ID_OFFSET, LocationType, SC2HOTS_LOC_ID_OFFSET
 | 
						|
from .MissionTables import (lookup_id_to_mission, SC2Campaign, lookup_name_to_mission,
 | 
						|
                            lookup_id_to_campaign, MissionConnection, SC2Mission, campaign_mission_table, SC2Race)
 | 
						|
from .Regions import MissionInfo
 | 
						|
 | 
						|
import colorama
 | 
						|
from Options import Option
 | 
						|
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
 | 
						|
from MultiServer import mark_raw
 | 
						|
 | 
						|
pool = concurrent.futures.ThreadPoolExecutor(1)
 | 
						|
loop = asyncio.get_event_loop_policy().new_event_loop()
 | 
						|
nest_asyncio.apply(loop)
 | 
						|
MAX_BONUS: int = 28
 | 
						|
VICTORY_MODULO: int = 100
 | 
						|
 | 
						|
# GitHub repo where the Map/mod data is hosted for /download_data command
 | 
						|
DATA_REPO_OWNER = "Ziktofel"
 | 
						|
DATA_REPO_NAME = "Archipelago-SC2-data"
 | 
						|
DATA_API_VERSION = "API3"
 | 
						|
 | 
						|
# Bot controller
 | 
						|
CONTROLLER_HEALTH: int = 38281
 | 
						|
CONTROLLER2_HEALTH: int = 38282
 | 
						|
 | 
						|
# Games
 | 
						|
STARCRAFT2 = "Starcraft 2"
 | 
						|
STARCRAFT2_WOL = "Starcraft 2 Wings of Liberty"
 | 
						|
 | 
						|
 | 
						|
# Data version file path.
 | 
						|
# This file is used to tell if the downloaded data are outdated
 | 
						|
# Associated with /download_data command
 | 
						|
def get_metadata_file() -> str:
 | 
						|
    return os.environ["SC2PATH"] + os.sep + "ArchipelagoSC2Metadata.txt"
 | 
						|
 | 
						|
 | 
						|
class ConfigurableOptionType(enum.Enum):
 | 
						|
    INTEGER = enum.auto()
 | 
						|
    ENUM = enum.auto()
 | 
						|
 | 
						|
class ConfigurableOptionInfo(typing.NamedTuple):
 | 
						|
    name: str
 | 
						|
    variable_name: str
 | 
						|
    option_class: typing.Type[Option]
 | 
						|
    option_type: ConfigurableOptionType = ConfigurableOptionType.ENUM
 | 
						|
    can_break_logic: bool = False
 | 
						|
 | 
						|
 | 
						|
class ColouredMessage:
 | 
						|
    def __init__(self, text: str = '') -> None:
 | 
						|
        self.parts: typing.List[dict] = []
 | 
						|
        if text:
 | 
						|
            self(text)
 | 
						|
    def __call__(self, text: str) -> 'ColouredMessage':
 | 
						|
        add_json_text(self.parts, text)
 | 
						|
        return self
 | 
						|
    def coloured(self, text: str, colour: str) -> 'ColouredMessage':
 | 
						|
        add_json_text(self.parts, text, type="color", color=colour)
 | 
						|
        return self
 | 
						|
    def location(self, location_id: int, player_id: int) -> 'ColouredMessage':
 | 
						|
        add_json_location(self.parts, location_id, player_id)
 | 
						|
        return self
 | 
						|
    def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage':
 | 
						|
        add_json_item(self.parts, item_id, player_id, flags)
 | 
						|
        return self
 | 
						|
    def player(self, player_id: int) -> 'ColouredMessage':
 | 
						|
        add_json_text(self.parts, str(player_id), type=JSONTypes.player_id)
 | 
						|
        return self
 | 
						|
    def send(self, ctx: SC2Context) -> None:
 | 
						|
        ctx.on_print_json({"data": self.parts, "cmd": "PrintJSON"})
 | 
						|
 | 
						|
 | 
						|
class StarcraftClientProcessor(ClientCommandProcessor):
 | 
						|
    ctx: SC2Context
 | 
						|
 | 
						|
    def formatted_print(self, text: str) -> None:
 | 
						|
        """Prints with kivy formatting to the GUI, and also prints to command-line and to all logs"""
 | 
						|
        # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output.
 | 
						|
        # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs,
 | 
						|
        # without having to branch code from CommonClient
 | 
						|
        self.ctx.on_print_json({"data": [{"text": text}]})
 | 
						|
 | 
						|
    def _cmd_difficulty(self, difficulty: str = "") -> bool:
 | 
						|
        """Overrides the current difficulty set for the world.  Takes the argument casual, normal, hard, or brutal"""
 | 
						|
        options = difficulty.split()
 | 
						|
        num_options = len(options)
 | 
						|
 | 
						|
        if num_options > 0:
 | 
						|
            difficulty_choice = options[0].lower()
 | 
						|
            if difficulty_choice == "casual":
 | 
						|
                self.ctx.difficulty_override = 0
 | 
						|
            elif difficulty_choice == "normal":
 | 
						|
                self.ctx.difficulty_override = 1
 | 
						|
            elif difficulty_choice == "hard":
 | 
						|
                self.ctx.difficulty_override = 2
 | 
						|
            elif difficulty_choice == "brutal":
 | 
						|
                self.ctx.difficulty_override = 3
 | 
						|
            else:
 | 
						|
                self.output("Unable to parse difficulty '" + options[0] + "'")
 | 
						|
                return False
 | 
						|
 | 
						|
            self.output("Difficulty set to " + options[0])
 | 
						|
            return True
 | 
						|
 | 
						|
        else:
 | 
						|
            if self.ctx.difficulty == -1:
 | 
						|
                self.output("Please connect to a seed before checking difficulty.")
 | 
						|
            else:
 | 
						|
                current_difficulty = self.ctx.difficulty
 | 
						|
                if self.ctx.difficulty_override >= 0:
 | 
						|
                    current_difficulty = self.ctx.difficulty_override
 | 
						|
                self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][current_difficulty])
 | 
						|
            self.output("To change the difficulty, add the name of the difficulty after the command.")
 | 
						|
            return False
 | 
						|
 | 
						|
 | 
						|
    def _cmd_game_speed(self, game_speed: str = "") -> bool:
 | 
						|
        """Overrides the current game speed for the world.
 | 
						|
         Takes the arguments default, slower, slow, normal, fast, faster"""
 | 
						|
        options = game_speed.split()
 | 
						|
        num_options = len(options)
 | 
						|
 | 
						|
        if num_options > 0:
 | 
						|
            speed_choice = options[0].lower()
 | 
						|
            if speed_choice == "default":
 | 
						|
                self.ctx.game_speed_override = 0
 | 
						|
            elif speed_choice == "slower":
 | 
						|
                self.ctx.game_speed_override = 1
 | 
						|
            elif speed_choice == "slow":
 | 
						|
                self.ctx.game_speed_override = 2
 | 
						|
            elif speed_choice == "normal":
 | 
						|
                self.ctx.game_speed_override = 3
 | 
						|
            elif speed_choice == "fast":
 | 
						|
                self.ctx.game_speed_override = 4
 | 
						|
            elif speed_choice == "faster":
 | 
						|
                self.ctx.game_speed_override = 5
 | 
						|
            else:
 | 
						|
                self.output("Unable to parse game speed '" + options[0] + "'")
 | 
						|
                return False
 | 
						|
 | 
						|
            self.output("Game speed set to " + options[0])
 | 
						|
            return True
 | 
						|
 | 
						|
        else:
 | 
						|
            if self.ctx.game_speed == -1:
 | 
						|
                self.output("Please connect to a seed before checking game speed.")
 | 
						|
            else:
 | 
						|
                current_speed = self.ctx.game_speed
 | 
						|
                if self.ctx.game_speed_override >= 0:
 | 
						|
                    current_speed = self.ctx.game_speed_override
 | 
						|
                self.output("Current game speed: "
 | 
						|
                            + ["Default", "Slower", "Slow", "Normal", "Fast", "Faster"][current_speed])
 | 
						|
            self.output("To change the game speed, add the name of the speed after the command,"
 | 
						|
                        " or Default to select based on difficulty.")
 | 
						|
            return False
 | 
						|
    
 | 
						|
    @mark_raw
 | 
						|
    def _cmd_received(self, filter_search: str = "") -> bool:
 | 
						|
        """List received items.
 | 
						|
        Pass in a parameter to filter the search by partial item name or exact item group."""
 | 
						|
        # Groups must be matched case-sensitively, so we properly capitalize the search term
 | 
						|
        # eg. "Spear of Adun" over "Spear Of Adun" or "spear of adun"
 | 
						|
        # This fails a lot of item name matches, but those should be found by partial name match
 | 
						|
        formatted_filter_search = " ".join([(part.lower() if len(part) <= 3 else part.lower().capitalize()) for part in filter_search.split()])
 | 
						|
 | 
						|
        def item_matches_filter(item_name: str) -> bool:
 | 
						|
            # The filter can be an exact group name or a partial item name
 | 
						|
            # Partial item name can be matched case-insensitively
 | 
						|
            if filter_search.lower() in item_name.lower():
 | 
						|
                return True
 | 
						|
            # The search term should already be formatted as a group name
 | 
						|
            if formatted_filter_search in item_name_groups and item_name in item_name_groups[formatted_filter_search]:
 | 
						|
                return True
 | 
						|
            return False
 | 
						|
 | 
						|
        items = get_full_item_list()
 | 
						|
        categorized_items: typing.Dict[SC2Race, typing.List[int]] = {}
 | 
						|
        parent_to_child: typing.Dict[int, typing.List[int]] = {}
 | 
						|
        items_received: typing.Dict[int, typing.List[NetworkItem]] = {}
 | 
						|
        filter_match_count = 0
 | 
						|
        for item in self.ctx.items_received:
 | 
						|
            items_received.setdefault(item.item, []).append(item)
 | 
						|
        items_received_set = set(items_received)
 | 
						|
        for item_data in items.values():
 | 
						|
            if item_data.parent_item:
 | 
						|
                parent_to_child.setdefault(items[item_data.parent_item].code, []).append(item_data.code)
 | 
						|
            else:
 | 
						|
                categorized_items.setdefault(item_data.race, []).append(item_data.code)
 | 
						|
        for faction in SC2Race:
 | 
						|
            has_printed_faction_title = False
 | 
						|
            def print_faction_title():
 | 
						|
                if not has_printed_faction_title:
 | 
						|
                    self.formatted_print(f" [u]{faction.name}[/u] ")
 | 
						|
            
 | 
						|
            for item_id in categorized_items[faction]:
 | 
						|
                item_name = self.ctx.item_names.lookup_in_game(item_id)
 | 
						|
                received_child_items = items_received_set.intersection(parent_to_child.get(item_id, []))
 | 
						|
                matching_children = [child for child in received_child_items
 | 
						|
                                     if item_matches_filter(self.ctx.item_names.lookup_in_game(child))]
 | 
						|
                received_items_of_this_type = items_received.get(item_id, [])
 | 
						|
                item_is_match = item_matches_filter(item_name)
 | 
						|
                if item_is_match or len(matching_children) > 0:
 | 
						|
                    # Print found item if it or its children match the filter
 | 
						|
                    if item_is_match:
 | 
						|
                        filter_match_count += len(received_items_of_this_type)
 | 
						|
                    for item in received_items_of_this_type:
 | 
						|
                        print_faction_title()
 | 
						|
                        has_printed_faction_title = True
 | 
						|
                        (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags)
 | 
						|
                            (" from ").location(item.location, self.ctx.slot)
 | 
						|
                            (" by ").player(item.player)
 | 
						|
                        ).send(self.ctx)
 | 
						|
                
 | 
						|
                if received_child_items:
 | 
						|
                    # We have this item's children
 | 
						|
                    if len(matching_children) == 0:
 | 
						|
                        # ...but none of them match the filter
 | 
						|
                        continue
 | 
						|
 | 
						|
                    if not received_items_of_this_type:
 | 
						|
                        # We didn't receive the item itself
 | 
						|
                        print_faction_title()
 | 
						|
                        has_printed_faction_title = True
 | 
						|
                        ColouredMessage("- ").coloured(item_name, "black")(" - not obtained").send(self.ctx)
 | 
						|
                    
 | 
						|
                    for child_item in matching_children:
 | 
						|
                        received_items_of_this_type = items_received.get(child_item, [])
 | 
						|
                        for item in received_items_of_this_type:
 | 
						|
                            filter_match_count += len(received_items_of_this_type)
 | 
						|
                            (ColouredMessage('  * ').item(item.item, self.ctx.slot, flags=item.flags)
 | 
						|
                                (" from ").location(item.location, self.ctx.slot)
 | 
						|
                                (" by ").player(item.player)
 | 
						|
                            ).send(self.ctx)
 | 
						|
                    
 | 
						|
                    non_matching_children = len(received_child_items) - len(matching_children)
 | 
						|
                    if non_matching_children > 0:
 | 
						|
                        self.formatted_print(f"  + {non_matching_children} child items that don't match the filter")
 | 
						|
        if filter_search == "":
 | 
						|
            self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items[/b]")
 | 
						|
        else:
 | 
						|
            self.formatted_print(f"[b]Filter \"{filter_search}\" found {filter_match_count} out of {len(self.ctx.items_received)} obtained items[/b]")
 | 
						|
        return True
 | 
						|
    
 | 
						|
    def _cmd_option(self, option_name: str = "", option_value: str = "") -> None:
 | 
						|
        """Sets a Starcraft game option that can be changed after generation. Use "/option list" to see all options."""
 | 
						|
 | 
						|
        LOGIC_WARNING = f"  *Note changing this may result in logically unbeatable games*\n"
 | 
						|
 | 
						|
        options = (
 | 
						|
            ConfigurableOptionInfo('kerrigan_presence', 'kerrigan_presence', Options.KerriganPresence, can_break_logic=True),
 | 
						|
            ConfigurableOptionInfo('soa_presence', 'spear_of_adun_presence', Options.SpearOfAdunPresence, can_break_logic=True),
 | 
						|
            ConfigurableOptionInfo('soa_in_nobuilds', 'spear_of_adun_present_in_no_build', Options.SpearOfAdunPresentInNoBuild, can_break_logic=True),
 | 
						|
            ConfigurableOptionInfo('control_ally', 'take_over_ai_allies', Options.TakeOverAIAllies, can_break_logic=True),
 | 
						|
            ConfigurableOptionInfo('minerals_per_item', 'minerals_per_item', Options.MineralsPerItem, ConfigurableOptionType.INTEGER),
 | 
						|
            ConfigurableOptionInfo('gas_per_item', 'vespene_per_item', Options.VespenePerItem, ConfigurableOptionType.INTEGER),
 | 
						|
            ConfigurableOptionInfo('supply_per_item', 'starting_supply_per_item', Options.StartingSupplyPerItem, ConfigurableOptionType.INTEGER),
 | 
						|
            ConfigurableOptionInfo('no_forced_camera', 'disable_forced_camera', Options.DisableForcedCamera),
 | 
						|
            ConfigurableOptionInfo('skip_cutscenes', 'skip_cutscenes', Options.SkipCutscenes),
 | 
						|
        )
 | 
						|
 | 
						|
        WARNING_COLOUR = "salmon"
 | 
						|
        CMD_COLOUR = "slateblue"
 | 
						|
        boolean_option_map = {
 | 
						|
            'y': 'true', 'yes': 'true', 'n': 'false', 'no': 'false',
 | 
						|
        }
 | 
						|
 | 
						|
        help_message = ColouredMessage(inspect.cleandoc("""
 | 
						|
            Options
 | 
						|
        --------------------
 | 
						|
        """))('\n')
 | 
						|
        for option in options:
 | 
						|
            option_help_text = inspect.cleandoc(option.option_class.__doc__ or "No description provided.").split('\n', 1)[0]
 | 
						|
            help_message.coloured(option.name, CMD_COLOUR)(": " + " | ".join(option.option_class.options)
 | 
						|
                + f" -- {option_help_text}\n")
 | 
						|
            if option.can_break_logic:
 | 
						|
                help_message.coloured(LOGIC_WARNING, WARNING_COLOUR)
 | 
						|
        help_message("--------------------\nEnter an option without arguments to see its current value.\n")
 | 
						|
 | 
						|
        if not option_name or option_name == 'list' or option_name == 'help':
 | 
						|
            help_message.send(self.ctx)
 | 
						|
            return
 | 
						|
        for option in options:
 | 
						|
            if option_name == option.name:
 | 
						|
                option_value = boolean_option_map.get(option_value, option_value)
 | 
						|
                if not option_value:
 | 
						|
                    pass
 | 
						|
                elif option.option_type == ConfigurableOptionType.ENUM and option_value in option.option_class.options:
 | 
						|
                    self.ctx.__dict__[option.variable_name] = option.option_class.options[option_value]
 | 
						|
                elif option.option_type == ConfigurableOptionType.INTEGER:
 | 
						|
                    try:
 | 
						|
                        self.ctx.__dict__[option.variable_name] = int(option_value, base=0)
 | 
						|
                    except:
 | 
						|
                        self.output(f"{option_value} is not a valid integer")
 | 
						|
                else:
 | 
						|
                    self.output(f"Unknown option value '{option_value}'")
 | 
						|
                ColouredMessage(f"{option.name} is '{option.option_class.get_option_name(self.ctx.__dict__[option.variable_name])}'").send(self.ctx)
 | 
						|
                break
 | 
						|
        else:
 | 
						|
            self.output(f"Unknown option '{option_name}'")
 | 
						|
            help_message.send(self.ctx)
 | 
						|
 | 
						|
    def _cmd_color(self, faction: str = "", color: str = "") -> None:
 | 
						|
        """Changes the player color for a given faction."""
 | 
						|
        player_colors = [
 | 
						|
            "White", "Red", "Blue", "Teal",
 | 
						|
            "Purple", "Yellow", "Orange", "Green",
 | 
						|
            "LightPink", "Violet", "LightGrey", "DarkGreen",
 | 
						|
            "Brown", "LightGreen", "DarkGrey", "Pink",
 | 
						|
            "Rainbow", "Random", "Default"
 | 
						|
        ]
 | 
						|
        var_names = {
 | 
						|
            'raynor': 'player_color_raynor',
 | 
						|
            'kerrigan': 'player_color_zerg',
 | 
						|
            'primal': 'player_color_zerg_primal',
 | 
						|
            'protoss': 'player_color_protoss',
 | 
						|
            'nova': 'player_color_nova',
 | 
						|
        }
 | 
						|
        faction = faction.lower()
 | 
						|
        if not faction:
 | 
						|
            for faction_name, key in var_names.items():
 | 
						|
                self.output(f"Current player color for {faction_name}: {player_colors[self.ctx.__dict__[key]]}")
 | 
						|
            self.output("To change your color, add the faction name and color after the command.")
 | 
						|
            self.output("Available factions: " + ', '.join(var_names))
 | 
						|
            self.output("Available colors: " + ', '.join(player_colors))
 | 
						|
            return
 | 
						|
        elif faction not in var_names:
 | 
						|
            self.output(f"Unknown faction '{faction}'.")
 | 
						|
            self.output("Available factions: " + ', '.join(var_names))
 | 
						|
            return
 | 
						|
        match_colors = [player_color.lower() for player_color in player_colors]
 | 
						|
        if not color:
 | 
						|
            self.output(f"Current player color for {faction}: {player_colors[self.ctx.__dict__[var_names[faction]]]}")
 | 
						|
            self.output("To change this faction's colors, add the name of the color after the command.")
 | 
						|
            self.output("Available colors: " + ', '.join(player_colors))
 | 
						|
        else:
 | 
						|
            if color.lower() not in match_colors:
 | 
						|
                self.output(color + " is not a valid color.  Available colors: " + ', '.join(player_colors))
 | 
						|
                return
 | 
						|
            if color.lower() == "random":
 | 
						|
                color = random.choice(player_colors[:16])
 | 
						|
            self.ctx.__dict__[var_names[faction]] = match_colors.index(color.lower())
 | 
						|
            self.ctx.pending_color_update = True
 | 
						|
            self.output(f"Color for {faction} set to " + player_colors[self.ctx.__dict__[var_names[faction]]])
 | 
						|
 | 
						|
    def _cmd_disable_mission_check(self) -> bool:
 | 
						|
        """Disables the check to see if a mission is available to play.  Meant for co-op runs where one player can play
 | 
						|
        the next mission in a chain the other player is doing."""
 | 
						|
        self.ctx.missions_unlocked = True
 | 
						|
        sc2_logger.info("Mission check has been disabled")
 | 
						|
        return True
 | 
						|
 | 
						|
    def _cmd_play(self, mission_id: str = "") -> bool:
 | 
						|
        """Start a Starcraft 2 mission"""
 | 
						|
 | 
						|
        options = mission_id.split()
 | 
						|
        num_options = len(options)
 | 
						|
 | 
						|
        if num_options > 0:
 | 
						|
            mission_number = int(options[0])
 | 
						|
 | 
						|
            self.ctx.play_mission(mission_number)
 | 
						|
 | 
						|
        else:
 | 
						|
            sc2_logger.info(
 | 
						|
                "Mission ID needs to be specified.  Use /unfinished or /available to view ids for available missions.")
 | 
						|
            return False
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def _cmd_available(self) -> bool:
 | 
						|
        """Get what missions are currently available to play"""
 | 
						|
 | 
						|
        request_available_missions(self.ctx)
 | 
						|
        return True
 | 
						|
 | 
						|
    def _cmd_unfinished(self) -> bool:
 | 
						|
        """Get what missions are currently available to play and have not had all locations checked"""
 | 
						|
 | 
						|
        request_unfinished_missions(self.ctx)
 | 
						|
        return True
 | 
						|
 | 
						|
    @mark_raw
 | 
						|
    def _cmd_set_path(self, path: str = '') -> bool:
 | 
						|
        """Manually set the SC2 install directory (if the automatic detection fails)."""
 | 
						|
        if path:
 | 
						|
            os.environ["SC2PATH"] = path
 | 
						|
            is_mod_installed_correctly()
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
 | 
						|
        return False
 | 
						|
 | 
						|
    def _cmd_download_data(self) -> bool:
 | 
						|
        """Download the most recent release of the necessary files for playing SC2 with
 | 
						|
        Archipelago. Will overwrite existing files."""
 | 
						|
        pool.submit(self._download_data)
 | 
						|
        return True
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _download_data() -> bool:
 | 
						|
        if "SC2PATH" not in os.environ:
 | 
						|
            check_game_install_path()
 | 
						|
 | 
						|
        if os.path.exists(get_metadata_file()):
 | 
						|
            with open(get_metadata_file(), "r") as f:
 | 
						|
                metadata = f.read()
 | 
						|
        else:
 | 
						|
            metadata = None
 | 
						|
 | 
						|
        tempzip, metadata = download_latest_release_zip(
 | 
						|
            DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, metadata=metadata, force_download=True)
 | 
						|
 | 
						|
        if tempzip:
 | 
						|
            try:
 | 
						|
                zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
 | 
						|
                sc2_logger.info(f"Download complete. Package installed.")
 | 
						|
                if metadata is not None:
 | 
						|
                    with open(get_metadata_file(), "w") as f:
 | 
						|
                        f.write(metadata)
 | 
						|
            finally:
 | 
						|
                os.remove(tempzip)
 | 
						|
        else:
 | 
						|
            sc2_logger.warning("Download aborted/failed. Read the log for more information.")
 | 
						|
            return False
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
class SC2JSONtoTextParser(JSONtoTextParser):
 | 
						|
    def __init__(self, ctx) -> None:
 | 
						|
        self.handlers = {
 | 
						|
            "ItemSend": self._handle_color,
 | 
						|
            "ItemCheat": self._handle_color,
 | 
						|
            "Hint": self._handle_color,
 | 
						|
        }
 | 
						|
        super().__init__(ctx)
 | 
						|
 | 
						|
    def _handle_color(self, node: JSONMessagePart) -> str:
 | 
						|
        codes = node["color"].split(";")
 | 
						|
        buffer = "".join(self.color_code(code) for code in codes if code in self.color_codes)
 | 
						|
        return buffer + self._handle_text(node) + '</c>'
 | 
						|
 | 
						|
    def color_code(self, code: str) -> str:
 | 
						|
        return '<c val="' + self.color_codes[code] + '">'
 | 
						|
 | 
						|
 | 
						|
class SC2Context(CommonContext):
 | 
						|
    command_processor = StarcraftClientProcessor
 | 
						|
    game = STARCRAFT2
 | 
						|
    items_handling = 0b111
 | 
						|
 | 
						|
    def __init__(self, *args, **kwargs) -> None:
 | 
						|
        super(SC2Context, self).__init__(*args, **kwargs)
 | 
						|
        self.raw_text_parser = SC2JSONtoTextParser(self)
 | 
						|
 | 
						|
        self.difficulty = -1
 | 
						|
        self.game_speed = -1
 | 
						|
        self.disable_forced_camera = 0
 | 
						|
        self.skip_cutscenes = 0
 | 
						|
        self.all_in_choice = 0
 | 
						|
        self.mission_order = 0
 | 
						|
        self.player_color_raynor = ColorChoice.option_blue
 | 
						|
        self.player_color_zerg = ColorChoice.option_orange
 | 
						|
        self.player_color_zerg_primal = ColorChoice.option_purple
 | 
						|
        self.player_color_protoss = ColorChoice.option_blue
 | 
						|
        self.player_color_nova = ColorChoice.option_dark_grey
 | 
						|
        self.pending_color_update = False
 | 
						|
        self.kerrigan_presence = 0
 | 
						|
        self.kerrigan_primal_status = 0
 | 
						|
        self.levels_per_check = 0
 | 
						|
        self.checks_per_level = 1
 | 
						|
        self.mission_req_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]] = {}
 | 
						|
        self.final_mission: int = 29
 | 
						|
        self.announcements: queue.Queue = queue.Queue()
 | 
						|
        self.sc2_run_task: typing.Optional[asyncio.Task] = None
 | 
						|
        self.missions_unlocked: bool = False  # allow launching missions ignoring requirements
 | 
						|
        self.generic_upgrade_missions = 0
 | 
						|
        self.generic_upgrade_research = 0
 | 
						|
        self.generic_upgrade_items = 0
 | 
						|
        self.location_inclusions: typing.Dict[LocationType, int] = {}
 | 
						|
        self.plando_locations: typing.List[str] = []
 | 
						|
        self.current_tooltip = None
 | 
						|
        self.last_loc_list = None
 | 
						|
        self.difficulty_override = -1
 | 
						|
        self.game_speed_override = -1
 | 
						|
        self.mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
 | 
						|
        self.last_bot: typing.Optional[ArchipelagoBot] = None
 | 
						|
        self.slot_data_version = 2
 | 
						|
        self.grant_story_tech = 0
 | 
						|
        self.required_tactics = RequiredTactics.option_standard
 | 
						|
        self.take_over_ai_allies = TakeOverAIAllies.option_false
 | 
						|
        self.spear_of_adun_presence = SpearOfAdunPresence.option_not_present
 | 
						|
        self.spear_of_adun_present_in_no_build = SpearOfAdunPresentInNoBuild.option_false
 | 
						|
        self.spear_of_adun_autonomously_cast_ability_presence = SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present
 | 
						|
        self.spear_of_adun_autonomously_cast_present_in_no_build = SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false
 | 
						|
        self.minerals_per_item = 15
 | 
						|
        self.vespene_per_item = 15
 | 
						|
        self.starting_supply_per_item = 2
 | 
						|
        self.nova_covert_ops_only = False
 | 
						|
        self.kerrigan_levels_per_mission_completed = 0
 | 
						|
 | 
						|
    async def server_auth(self, password_requested: bool = False) -> None:
 | 
						|
        self.game = STARCRAFT2
 | 
						|
        if password_requested and not self.password:
 | 
						|
            await super(SC2Context, self).server_auth(password_requested)
 | 
						|
        await self.get_username()
 | 
						|
        await self.send_connect()
 | 
						|
        if self.ui:
 | 
						|
            self.ui.first_check = True
 | 
						|
 | 
						|
    def is_legacy_game(self):
 | 
						|
        return self.game == STARCRAFT2_WOL
 | 
						|
 | 
						|
    def event_invalid_game(self):
 | 
						|
        if self.is_legacy_game():
 | 
						|
            self.game = STARCRAFT2
 | 
						|
            super().event_invalid_game()
 | 
						|
        else:
 | 
						|
            self.game = STARCRAFT2_WOL
 | 
						|
            async_start(self.send_connect())
 | 
						|
 | 
						|
    def on_package(self, cmd: str, args: dict) -> None:
 | 
						|
        if cmd == "Connected":
 | 
						|
            self.difficulty = args["slot_data"]["game_difficulty"]
 | 
						|
            self.game_speed = args["slot_data"].get("game_speed", GameSpeed.option_default)
 | 
						|
            self.disable_forced_camera = args["slot_data"].get("disable_forced_camera", DisableForcedCamera.default)
 | 
						|
            self.skip_cutscenes = args["slot_data"].get("skip_cutscenes", SkipCutscenes.default)
 | 
						|
            self.all_in_choice = args["slot_data"]["all_in_map"]
 | 
						|
            self.slot_data_version = args["slot_data"].get("version", 2)
 | 
						|
            slot_req_table: dict = args["slot_data"]["mission_req"]
 | 
						|
 | 
						|
            first_item = list(slot_req_table.keys())[0]
 | 
						|
            # Maintaining backwards compatibility with older slot data
 | 
						|
            if first_item in [str(campaign.id) for campaign in SC2Campaign]:
 | 
						|
                # Multi-campaign
 | 
						|
                self.mission_req_table = {}
 | 
						|
                for campaign_id in slot_req_table:
 | 
						|
                    campaign = lookup_id_to_campaign[int(campaign_id)]
 | 
						|
                    self.mission_req_table[campaign] = {
 | 
						|
                        mission: self.parse_mission_info(mission_info)
 | 
						|
                        for mission, mission_info in slot_req_table[campaign_id].items()
 | 
						|
                    }
 | 
						|
            else:
 | 
						|
                # Old format
 | 
						|
                self.mission_req_table = {SC2Campaign.GLOBAL: {
 | 
						|
                        mission: self.parse_mission_info(mission_info)
 | 
						|
                        for mission, mission_info in slot_req_table.items()
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
            self.mission_order = args["slot_data"].get("mission_order", MissionOrder.option_vanilla)
 | 
						|
            self.final_mission = args["slot_data"].get("final_mission", SC2Mission.ALL_IN.id)
 | 
						|
            self.player_color_raynor = args["slot_data"].get("player_color_terran_raynor", ColorChoice.option_blue)
 | 
						|
            self.player_color_zerg = args["slot_data"].get("player_color_zerg", ColorChoice.option_orange)
 | 
						|
            self.player_color_zerg_primal = args["slot_data"].get("player_color_zerg_primal", ColorChoice.option_purple)
 | 
						|
            self.player_color_protoss = args["slot_data"].get("player_color_protoss", ColorChoice.option_blue)
 | 
						|
            self.player_color_nova = args["slot_data"].get("player_color_nova", ColorChoice.option_dark_grey)
 | 
						|
            self.generic_upgrade_missions = args["slot_data"].get("generic_upgrade_missions", GenericUpgradeMissions.default)
 | 
						|
            self.generic_upgrade_items = args["slot_data"].get("generic_upgrade_items", GenericUpgradeItems.option_individual_items)
 | 
						|
            self.generic_upgrade_research = args["slot_data"].get("generic_upgrade_research", GenericUpgradeResearch.option_vanilla)
 | 
						|
            self.kerrigan_presence = args["slot_data"].get("kerrigan_presence", KerriganPresence.option_vanilla)
 | 
						|
            self.kerrigan_primal_status = args["slot_data"].get("kerrigan_primal_status", KerriganPrimalStatus.option_vanilla)
 | 
						|
            self.kerrigan_levels_per_mission_completed = args["slot_data"].get("kerrigan_levels_per_mission_completed", 0)
 | 
						|
            self.kerrigan_levels_per_mission_completed_cap = args["slot_data"].get("kerrigan_levels_per_mission_completed_cap", -1)
 | 
						|
            self.kerrigan_total_level_cap = args["slot_data"].get("kerrigan_total_level_cap", -1)
 | 
						|
            self.grant_story_tech = args["slot_data"].get("grant_story_tech", GrantStoryTech.option_false)
 | 
						|
            self.grant_story_levels = args["slot_data"].get("grant_story_levels", GrantStoryLevels.option_additive)
 | 
						|
            self.required_tactics = args["slot_data"].get("required_tactics", RequiredTactics.option_standard)
 | 
						|
            self.take_over_ai_allies = args["slot_data"].get("take_over_ai_allies", TakeOverAIAllies.option_false)
 | 
						|
            self.spear_of_adun_presence = args["slot_data"].get("spear_of_adun_presence", SpearOfAdunPresence.option_not_present)
 | 
						|
            self.spear_of_adun_present_in_no_build = args["slot_data"].get("spear_of_adun_present_in_no_build", SpearOfAdunPresentInNoBuild.option_false)
 | 
						|
            self.spear_of_adun_autonomously_cast_ability_presence = args["slot_data"].get("spear_of_adun_autonomously_cast_ability_presence", SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present)
 | 
						|
            self.spear_of_adun_autonomously_cast_present_in_no_build = args["slot_data"].get("spear_of_adun_autonomously_cast_present_in_no_build", SpearOfAdunAutonomouslyCastPresentInNoBuild.option_false)
 | 
						|
            self.minerals_per_item = args["slot_data"].get("minerals_per_item", 15)
 | 
						|
            self.vespene_per_item = args["slot_data"].get("vespene_per_item", 15)
 | 
						|
            self.starting_supply_per_item = args["slot_data"].get("starting_supply_per_item", 2)
 | 
						|
            self.nova_covert_ops_only = args["slot_data"].get("nova_covert_ops_only", False)
 | 
						|
 | 
						|
            if self.required_tactics == RequiredTactics.option_no_logic:
 | 
						|
                # Locking Grant Story Tech/Levels if no logic
 | 
						|
                self.grant_story_tech = GrantStoryTech.option_true
 | 
						|
                self.grant_story_levels = GrantStoryLevels.option_minimum
 | 
						|
 | 
						|
            self.location_inclusions = {
 | 
						|
                LocationType.VICTORY: LocationInclusion.option_enabled, # Victory checks are always enabled
 | 
						|
                LocationType.VANILLA: args["slot_data"].get("vanilla_locations", VanillaLocations.default),
 | 
						|
                LocationType.EXTRA: args["slot_data"].get("extra_locations", ExtraLocations.default),
 | 
						|
                LocationType.CHALLENGE: args["slot_data"].get("challenge_locations", ChallengeLocations.default),
 | 
						|
                LocationType.MASTERY: args["slot_data"].get("mastery_locations", MasteryLocations.default),
 | 
						|
            }
 | 
						|
            self.plando_locations = args["slot_data"].get("plando_locations", [])
 | 
						|
 | 
						|
            self.build_location_to_mission_mapping()
 | 
						|
 | 
						|
            # Looks for the required maps and mods for SC2. Runs check_game_install_path.
 | 
						|
            maps_present = is_mod_installed_correctly()
 | 
						|
            if os.path.exists(get_metadata_file()):
 | 
						|
                with open(get_metadata_file(), "r") as f:
 | 
						|
                    current_ver = f.read()
 | 
						|
                    sc2_logger.debug(f"Current version: {current_ver}")
 | 
						|
                if is_mod_update_available(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, current_ver):
 | 
						|
                    sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
 | 
						|
            elif maps_present:
 | 
						|
                sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
 | 
						|
                                   "Run /download_data to update them.")
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def parse_mission_info(mission_info: dict[str, typing.Any]) -> MissionInfo:
 | 
						|
        if mission_info.get("id") is not None:
 | 
						|
            mission_info["mission"] = lookup_id_to_mission[mission_info["id"]]
 | 
						|
        elif isinstance(mission_info["mission"], int):
 | 
						|
            mission_info["mission"] = lookup_id_to_mission[mission_info["mission"]]
 | 
						|
 | 
						|
        return MissionInfo(
 | 
						|
            **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
 | 
						|
        )
 | 
						|
 | 
						|
    def find_campaign(self, mission_name: str) -> SC2Campaign:
 | 
						|
        data = self.mission_req_table
 | 
						|
        for campaign in data.keys():
 | 
						|
            if mission_name in data[campaign].keys():
 | 
						|
                return campaign
 | 
						|
        sc2_logger.info(f"Attempted to find campaign of unknown mission '{mission_name}'; defaulting to GLOBAL")
 | 
						|
        return SC2Campaign.GLOBAL
 | 
						|
 | 
						|
 | 
						|
 | 
						|
    def on_print_json(self, args: dict) -> None:
 | 
						|
        # goes to this world
 | 
						|
        if "receiving" in args and self.slot_concerns_self(args["receiving"]):
 | 
						|
            relevant = True
 | 
						|
        # found in this world
 | 
						|
        elif "item" in args and self.slot_concerns_self(args["item"].player):
 | 
						|
            relevant = True
 | 
						|
        # not related
 | 
						|
        else:
 | 
						|
            relevant = False
 | 
						|
 | 
						|
        if relevant:
 | 
						|
            self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
 | 
						|
 | 
						|
        super(SC2Context, self).on_print_json(args)
 | 
						|
 | 
						|
    def run_gui(self) -> None:
 | 
						|
        from .ClientGui import start_gui
 | 
						|
        start_gui(self)
 | 
						|
 | 
						|
 | 
						|
    async def shutdown(self) -> None:
 | 
						|
        await super(SC2Context, self).shutdown()
 | 
						|
        if self.last_bot:
 | 
						|
            self.last_bot.want_close = True
 | 
						|
        if self.sc2_run_task:
 | 
						|
            self.sc2_run_task.cancel()
 | 
						|
 | 
						|
    def play_mission(self, mission_id: int) -> bool:
 | 
						|
        if self.missions_unlocked or is_mission_available(self, mission_id):
 | 
						|
            if self.sc2_run_task:
 | 
						|
                if not self.sc2_run_task.done():
 | 
						|
                    sc2_logger.warning("Starcraft 2 Client is still running!")
 | 
						|
                self.sc2_run_task.cancel()  # doesn't actually close the game, just stops the python task
 | 
						|
            if self.slot is None:
 | 
						|
                sc2_logger.warning("Launching Mission without Archipelago authentication, "
 | 
						|
                                   "checks will not be registered to server.")
 | 
						|
            self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
 | 
						|
                                                    name="Starcraft 2 Launch")
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            sc2_logger.info(
 | 
						|
                f"{lookup_id_to_mission[mission_id].mission_name} is not currently unlocked.  "
 | 
						|
                f"Use /unfinished or /available to see what is available.")
 | 
						|
            return False
 | 
						|
 | 
						|
    def build_location_to_mission_mapping(self) -> None:
 | 
						|
        mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
 | 
						|
            mission_info.mission.id: set() for campaign_mission in self.mission_req_table.values() for mission_info in campaign_mission.values()
 | 
						|
        }
 | 
						|
 | 
						|
        for loc in self.server_locations:
 | 
						|
            offset = SC2WOL_LOC_ID_OFFSET if loc < SC2HOTS_LOC_ID_OFFSET \
 | 
						|
                else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO)
 | 
						|
            mission_id, objective = divmod(loc - offset, VICTORY_MODULO)
 | 
						|
            mission_id_to_location_ids[mission_id].add(objective)
 | 
						|
        self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
 | 
						|
                                           mission_id_to_location_ids.items()}
 | 
						|
 | 
						|
    def locations_for_mission(self, mission_name: str):
 | 
						|
        mission = lookup_name_to_mission[mission_name]
 | 
						|
        mission_id: int = mission.id
 | 
						|
        objectives = self.mission_id_to_location_ids[mission_id]
 | 
						|
        for objective in objectives:
 | 
						|
            yield get_location_offset(mission_id) + mission_id * VICTORY_MODULO + objective
 | 
						|
 | 
						|
 | 
						|
class CompatItemHolder(typing.NamedTuple):
 | 
						|
    name: str
 | 
						|
    quantity: int = 1
 | 
						|
 | 
						|
 | 
						|
async def main():
 | 
						|
    multiprocessing.freeze_support()
 | 
						|
    parser = get_base_parser()
 | 
						|
    parser.add_argument('--name', default=None, help="Slot Name to connect as.")
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    ctx = SC2Context(args.connect, args.password)
 | 
						|
    ctx.auth = args.name
 | 
						|
    if ctx.server_task is None:
 | 
						|
        ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
 | 
						|
 | 
						|
    if gui_enabled:
 | 
						|
        ctx.run_gui()
 | 
						|
    ctx.run_cli()
 | 
						|
 | 
						|
    await ctx.exit_event.wait()
 | 
						|
 | 
						|
    await ctx.shutdown()
 | 
						|
 | 
						|
# These items must be given to the player if the game is generated on version 2
 | 
						|
API2_TO_API3_COMPAT_ITEMS: typing.Set[CompatItemHolder] = {
 | 
						|
    CompatItemHolder(ItemNames.PHOTON_CANNON),
 | 
						|
    CompatItemHolder(ItemNames.OBSERVER),
 | 
						|
    CompatItemHolder(ItemNames.WARP_HARMONIZATION),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_WEAPON, 3),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_GROUND_ARMOR, 3),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_SHIELDS, 3),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_WEAPON, 3),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_AIR_ARMOR, 3),
 | 
						|
    CompatItemHolder(ItemNames.PROGRESSIVE_PROTOSS_WEAPON_ARMOR_UPGRADE, 3)
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
def compat_item_to_network_items(compat_item: CompatItemHolder) -> typing.List[NetworkItem]:
 | 
						|
    item_id = get_full_item_list()[compat_item.name].code
 | 
						|
    network_item = NetworkItem(item_id, 0, 0, 0)
 | 
						|
    return compat_item.quantity * [network_item]
 | 
						|
 | 
						|
 | 
						|
def calculate_items(ctx: SC2Context) -> typing.Dict[SC2Race, typing.List[int]]:
 | 
						|
    items = ctx.items_received.copy()
 | 
						|
    # Items unlocked in API2 by default (Prophecy default items)
 | 
						|
    if ctx.slot_data_version < 3:
 | 
						|
        for compat_item in API2_TO_API3_COMPAT_ITEMS:
 | 
						|
            items.extend(compat_item_to_network_items(compat_item))
 | 
						|
 | 
						|
    network_item: NetworkItem
 | 
						|
    accumulators: typing.Dict[SC2Race, typing.List[int]] = {race: [0 for _ in type_flaggroups[race]] for race in SC2Race}
 | 
						|
 | 
						|
    # Protoss Shield grouped item specific logic
 | 
						|
    shields_from_ground_upgrade: int = 0
 | 
						|
    shields_from_air_upgrade: int = 0
 | 
						|
 | 
						|
    item_list = get_full_item_list()
 | 
						|
    for network_item in items:
 | 
						|
        name: str = lookup_id_to_name[network_item.item]
 | 
						|
        item_data: ItemData = item_list[name]
 | 
						|
 | 
						|
        # exists exactly once
 | 
						|
        if item_data.quantity == 1:
 | 
						|
            accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] |= 1 << item_data.number
 | 
						|
 | 
						|
        # exists multiple times
 | 
						|
        elif item_data.type in ["Upgrade", "Progressive Upgrade","Progressive Upgrade 2"]:
 | 
						|
            flaggroup = type_flaggroups[item_data.race][item_data.type]
 | 
						|
 | 
						|
            # Generic upgrades apply only to Weapon / Armor upgrades
 | 
						|
            if item_data.type != "Upgrade" or ctx.generic_upgrade_items == 0:
 | 
						|
                accumulators[item_data.race][flaggroup] += 1 << item_data.number
 | 
						|
            else:
 | 
						|
                if name == ItemNames.PROGRESSIVE_PROTOSS_GROUND_UPGRADE:
 | 
						|
                    shields_from_ground_upgrade += 1
 | 
						|
                if name == ItemNames.PROGRESSIVE_PROTOSS_AIR_UPGRADE:
 | 
						|
                    shields_from_air_upgrade += 1
 | 
						|
                for bundled_number in upgrade_numbers[item_data.number]:
 | 
						|
                    accumulators[item_data.race][flaggroup] += 1 << bundled_number
 | 
						|
 | 
						|
            # Regen bio-steel nerf with API3 - undo for older games
 | 
						|
            if ctx.slot_data_version < 3 and name == ItemNames.PROGRESSIVE_REGENERATIVE_BIO_STEEL:
 | 
						|
                current_level = (accumulators[item_data.race][flaggroup] >> item_data.number) % 4
 | 
						|
                if current_level == 2:
 | 
						|
                    # Switch from level 2 to level 3 for compatibility
 | 
						|
                    accumulators[item_data.race][flaggroup] += 1 << item_data.number
 | 
						|
        # sum
 | 
						|
        else:
 | 
						|
            if name == ItemNames.STARTING_MINERALS:
 | 
						|
                accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.minerals_per_item
 | 
						|
            elif name == ItemNames.STARTING_VESPENE:
 | 
						|
                accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.vespene_per_item
 | 
						|
            elif name == ItemNames.STARTING_SUPPLY:
 | 
						|
                accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += ctx.starting_supply_per_item
 | 
						|
            else:
 | 
						|
                accumulators[item_data.race][type_flaggroups[item_data.race][item_data.type]] += item_data.number
 | 
						|
 | 
						|
    # Fix Shields from generic upgrades by unit class (Maximum of ground/air upgrades)
 | 
						|
    if shields_from_ground_upgrade > 0 or shields_from_air_upgrade > 0:
 | 
						|
        shield_upgrade_level = max(shields_from_ground_upgrade, shields_from_air_upgrade)
 | 
						|
        shield_upgrade_item = item_list[ItemNames.PROGRESSIVE_PROTOSS_SHIELDS]
 | 
						|
        for _ in range(0, shield_upgrade_level):
 | 
						|
            accumulators[shield_upgrade_item.race][type_flaggroups[shield_upgrade_item.race][shield_upgrade_item.type]] += 1 << shield_upgrade_item.number
 | 
						|
 | 
						|
    # Kerrigan levels per check
 | 
						|
    accumulators[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] += (len(ctx.checked_locations) // ctx.checks_per_level) * ctx.levels_per_check
 | 
						|
 | 
						|
    # Upgrades from completed missions
 | 
						|
    if ctx.generic_upgrade_missions > 0:
 | 
						|
        total_missions = sum(len(ctx.mission_req_table[campaign]) for campaign in ctx.mission_req_table)
 | 
						|
        for race in SC2Race:
 | 
						|
            if "Upgrade" not in type_flaggroups[race]:
 | 
						|
                continue
 | 
						|
            upgrade_flaggroup = type_flaggroups[race]["Upgrade"]
 | 
						|
            num_missions = ctx.generic_upgrade_missions * total_missions
 | 
						|
            amounts = [
 | 
						|
                num_missions // 100,
 | 
						|
                2 * num_missions // 100,
 | 
						|
                3 * num_missions // 100
 | 
						|
            ]
 | 
						|
            upgrade_count = 0
 | 
						|
            completed = len([id for id in ctx.mission_id_to_location_ids if get_location_offset(id) + VICTORY_MODULO * id in ctx.checked_locations])
 | 
						|
            for amount in amounts:
 | 
						|
                if completed >= amount:
 | 
						|
                    upgrade_count += 1
 | 
						|
            # Equivalent to "Progressive Weapon/Armor Upgrade" item
 | 
						|
            for bundled_number in upgrade_numbers[upgrade_numbers_all[race]]:
 | 
						|
                accumulators[race][upgrade_flaggroup] += upgrade_count << bundled_number
 | 
						|
 | 
						|
    return accumulators
 | 
						|
 | 
						|
 | 
						|
def calc_difficulty(difficulty: int):
 | 
						|
    if difficulty == 0:
 | 
						|
        return 'C'
 | 
						|
    elif difficulty == 1:
 | 
						|
        return 'N'
 | 
						|
    elif difficulty == 2:
 | 
						|
        return 'H'
 | 
						|
    elif difficulty == 3:
 | 
						|
        return 'B'
 | 
						|
 | 
						|
    return 'X'
 | 
						|
 | 
						|
 | 
						|
def get_kerrigan_level(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]], missions_beaten: int) -> int:
 | 
						|
    item_value = items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]]
 | 
						|
    mission_value = missions_beaten * ctx.kerrigan_levels_per_mission_completed
 | 
						|
    if ctx.kerrigan_levels_per_mission_completed_cap != -1:
 | 
						|
        mission_value = min(mission_value, ctx.kerrigan_levels_per_mission_completed_cap)
 | 
						|
    total_value = item_value + mission_value
 | 
						|
    if ctx.kerrigan_total_level_cap != -1:
 | 
						|
        total_value = min(total_value, ctx.kerrigan_total_level_cap)
 | 
						|
    return total_value
 | 
						|
 | 
						|
 | 
						|
def calculate_kerrigan_options(ctx: SC2Context) -> int:
 | 
						|
    options = 0
 | 
						|
 | 
						|
    # Bits 0, 1
 | 
						|
    # Kerrigan unit available
 | 
						|
    if ctx.kerrigan_presence in kerrigan_unit_available:
 | 
						|
        options |= 1 << 0
 | 
						|
 | 
						|
    # Bit 2
 | 
						|
    # Kerrigan primal status by map
 | 
						|
    if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_vanilla:
 | 
						|
        options |= 1 << 2
 | 
						|
 | 
						|
    return options
 | 
						|
 | 
						|
 | 
						|
def caclulate_soa_options(ctx: SC2Context) -> int:
 | 
						|
    options = 0
 | 
						|
 | 
						|
    # Bits 0, 1
 | 
						|
    # SoA Calldowns available
 | 
						|
    soa_presence_value = 0
 | 
						|
    if ctx.spear_of_adun_presence == SpearOfAdunPresence.option_not_present:
 | 
						|
        soa_presence_value = 0
 | 
						|
    elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_lotv_protoss:
 | 
						|
        soa_presence_value = 1
 | 
						|
    elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_protoss:
 | 
						|
        soa_presence_value = 2
 | 
						|
    elif ctx.spear_of_adun_presence == SpearOfAdunPresence.option_everywhere:
 | 
						|
        soa_presence_value = 3
 | 
						|
    options |= soa_presence_value << 0
 | 
						|
 | 
						|
    # Bit 2
 | 
						|
    # SoA Calldowns for no-builds
 | 
						|
    if ctx.spear_of_adun_present_in_no_build == SpearOfAdunPresentInNoBuild.option_true:
 | 
						|
        options |= 1 << 2
 | 
						|
 | 
						|
    # Bits 3,4
 | 
						|
    # Autocasts
 | 
						|
    soa_autocasts_presence_value = 0
 | 
						|
    if ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_not_present:
 | 
						|
        soa_autocasts_presence_value = 0
 | 
						|
    elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_lotv_protoss:
 | 
						|
        soa_autocasts_presence_value = 1
 | 
						|
    elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_protoss:
 | 
						|
        soa_autocasts_presence_value = 2
 | 
						|
    elif ctx.spear_of_adun_autonomously_cast_ability_presence == SpearOfAdunAutonomouslyCastAbilityPresence.option_everywhere:
 | 
						|
        soa_autocasts_presence_value = 3
 | 
						|
    options |= soa_autocasts_presence_value << 3
 | 
						|
 | 
						|
    # Bit 5
 | 
						|
    # Autocasts in no-builds
 | 
						|
    if ctx.spear_of_adun_autonomously_cast_present_in_no_build == SpearOfAdunAutonomouslyCastPresentInNoBuild.option_true:
 | 
						|
        options |= 1 << 5
 | 
						|
 | 
						|
    return options
 | 
						|
 | 
						|
def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool:
 | 
						|
    if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg:
 | 
						|
        return True
 | 
						|
    elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human:
 | 
						|
        return False
 | 
						|
    elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35:
 | 
						|
        return kerrigan_level >= 35
 | 
						|
    elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion:
 | 
						|
        total_missions = len(ctx.mission_id_to_location_ids)
 | 
						|
        completed = sum((mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations
 | 
						|
                         for mission_id in ctx.mission_id_to_location_ids)
 | 
						|
        return completed >= (total_missions / 2)
 | 
						|
    elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item:
 | 
						|
        codes = [item.item for item in ctx.items_received]
 | 
						|
        return get_full_item_list()[ItemNames.KERRIGAN_PRIMAL_FORM].code in codes
 | 
						|
    return False
 | 
						|
 | 
						|
async def starcraft_launch(ctx: SC2Context, mission_id: int):
 | 
						|
    sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id].mission_name}. If game does not launch check log file for errors.")
 | 
						|
 | 
						|
    with DllDirectory(None):
 | 
						|
        run_game(bot.maps.get(lookup_id_to_mission[mission_id].map_file), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
 | 
						|
                                                                name="Archipelago", fullscreen=True)], realtime=True)
 | 
						|
 | 
						|
 | 
						|
class ArchipelagoBot(bot.bot_ai.BotAI):
 | 
						|
    __slots__ = [
 | 
						|
        'game_running',
 | 
						|
        'mission_completed',
 | 
						|
        'boni',
 | 
						|
        'setup_done',
 | 
						|
        'ctx',
 | 
						|
        'mission_id',
 | 
						|
        'want_close',
 | 
						|
        'can_read_game',
 | 
						|
        'last_received_update',
 | 
						|
    ]
 | 
						|
 | 
						|
    def __init__(self, ctx: SC2Context, mission_id: int):
 | 
						|
        self.game_running = False
 | 
						|
        self.mission_completed = False
 | 
						|
        self.want_close = False
 | 
						|
        self.can_read_game = False
 | 
						|
        self.last_received_update: int = 0
 | 
						|
        self.setup_done = False
 | 
						|
        self.ctx = ctx
 | 
						|
        self.ctx.last_bot = self
 | 
						|
        self.mission_id = mission_id
 | 
						|
        self.boni = [False for _ in range(MAX_BONUS)]
 | 
						|
 | 
						|
        super(ArchipelagoBot, self).__init__()
 | 
						|
 | 
						|
    async def on_step(self, iteration: int):
 | 
						|
        if self.want_close:
 | 
						|
            self.want_close = False
 | 
						|
            await self._client.leave()
 | 
						|
            return
 | 
						|
        game_state = 0
 | 
						|
        if not self.setup_done:
 | 
						|
            self.setup_done = True
 | 
						|
            start_items = calculate_items(self.ctx)
 | 
						|
            missions_beaten = self.missions_beaten_count()
 | 
						|
            kerrigan_level = get_kerrigan_level(self.ctx, start_items, missions_beaten)
 | 
						|
            kerrigan_options = calculate_kerrigan_options(self.ctx)
 | 
						|
            soa_options = caclulate_soa_options(self.ctx)
 | 
						|
            if self.ctx.difficulty_override >= 0:
 | 
						|
                difficulty = calc_difficulty(self.ctx.difficulty_override)
 | 
						|
            else:
 | 
						|
                difficulty = calc_difficulty(self.ctx.difficulty)
 | 
						|
            if self.ctx.game_speed_override >= 0:
 | 
						|
                game_speed = self.ctx.game_speed_override
 | 
						|
            else:
 | 
						|
                game_speed = self.ctx.game_speed
 | 
						|
            await self.chat_send("?SetOptions {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
 | 
						|
                difficulty,
 | 
						|
                self.ctx.generic_upgrade_research,
 | 
						|
                self.ctx.all_in_choice,
 | 
						|
                game_speed,
 | 
						|
                self.ctx.disable_forced_camera,
 | 
						|
                self.ctx.skip_cutscenes,
 | 
						|
                kerrigan_options,
 | 
						|
                self.ctx.grant_story_tech,
 | 
						|
                self.ctx.take_over_ai_allies,
 | 
						|
                soa_options,
 | 
						|
                self.ctx.mission_order,
 | 
						|
                1 if self.ctx.nova_covert_ops_only else 0,
 | 
						|
                self.ctx.grant_story_levels
 | 
						|
            ))
 | 
						|
            await self.chat_send("?GiveResources {} {} {}".format(
 | 
						|
                start_items[SC2Race.ANY][0],
 | 
						|
                start_items[SC2Race.ANY][1],
 | 
						|
                start_items[SC2Race.ANY][2]
 | 
						|
            ))
 | 
						|
            await self.updateTerranTech(start_items)
 | 
						|
            await self.updateZergTech(start_items, kerrigan_level)
 | 
						|
            await self.updateProtossTech(start_items)
 | 
						|
            await self.updateColors()
 | 
						|
            await self.chat_send("?LoadFinished")
 | 
						|
            self.last_received_update = len(self.ctx.items_received)
 | 
						|
 | 
						|
        else:
 | 
						|
            if self.ctx.pending_color_update:
 | 
						|
                await self.updateColors()
 | 
						|
 | 
						|
            if not self.ctx.announcements.empty():
 | 
						|
                message = self.ctx.announcements.get(timeout=1)
 | 
						|
                await self.chat_send("?SendMessage " + message)
 | 
						|
                self.ctx.announcements.task_done()
 | 
						|
 | 
						|
            # Archipelago reads the health
 | 
						|
            controller1_state = 0
 | 
						|
            controller2_state = 0
 | 
						|
            for unit in self.all_own_units():
 | 
						|
                if unit.health_max == CONTROLLER_HEALTH:
 | 
						|
                    controller1_state = int(CONTROLLER_HEALTH - unit.health)
 | 
						|
                    self.can_read_game = True
 | 
						|
                elif unit.health_max == CONTROLLER2_HEALTH:
 | 
						|
                    controller2_state = int(CONTROLLER2_HEALTH - unit.health)
 | 
						|
                    self.can_read_game = True
 | 
						|
            game_state = controller1_state + (controller2_state << 15)
 | 
						|
 | 
						|
            if iteration == 160 and not game_state & 1:
 | 
						|
                await self.chat_send("?SendMessage Warning: Archipelago unable to connect or has lost connection to " +
 | 
						|
                                     "Starcraft 2 (This is likely a map issue)")
 | 
						|
 | 
						|
            if self.last_received_update < len(self.ctx.items_received):
 | 
						|
                current_items = calculate_items(self.ctx)
 | 
						|
                missions_beaten = self.missions_beaten_count()
 | 
						|
                kerrigan_level = get_kerrigan_level(self.ctx, current_items, missions_beaten)
 | 
						|
                await self.updateTerranTech(current_items)
 | 
						|
                await self.updateZergTech(current_items, kerrigan_level)
 | 
						|
                await self.updateProtossTech(current_items)
 | 
						|
                self.last_received_update = len(self.ctx.items_received)
 | 
						|
 | 
						|
            if game_state & 1:
 | 
						|
                if not self.game_running:
 | 
						|
                    print("Archipelago Connected")
 | 
						|
                    self.game_running = True
 | 
						|
 | 
						|
                if self.can_read_game:
 | 
						|
                    if game_state & (1 << 1) and not self.mission_completed:
 | 
						|
                        if self.mission_id != self.ctx.final_mission:
 | 
						|
                            print("Mission Completed")
 | 
						|
                            await self.ctx.send_msgs(
 | 
						|
                                [{"cmd": 'LocationChecks',
 | 
						|
                                  "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id]}])
 | 
						|
                            self.mission_completed = True
 | 
						|
                        else:
 | 
						|
                            print("Game Complete")
 | 
						|
                            await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
 | 
						|
                            self.mission_completed = True
 | 
						|
 | 
						|
                    for x, completed in enumerate(self.boni):
 | 
						|
                        if not completed and game_state & (1 << (x + 2)):
 | 
						|
                            await self.ctx.send_msgs(
 | 
						|
                                [{"cmd": 'LocationChecks',
 | 
						|
                                  "locations": [get_location_offset(self.mission_id) + VICTORY_MODULO * self.mission_id + x + 1]}])
 | 
						|
                            self.boni[x] = True
 | 
						|
                else:
 | 
						|
                    await self.chat_send("?SendMessage LostConnection - Lost connection to game.")
 | 
						|
 | 
						|
    def missions_beaten_count(self):
 | 
						|
        return len([location for location in self.ctx.checked_locations if location % VICTORY_MODULO == 0])
 | 
						|
 | 
						|
    async def updateColors(self):
 | 
						|
        await self.chat_send("?SetColor rr " + str(self.ctx.player_color_raynor))
 | 
						|
        await self.chat_send("?SetColor ks " + str(self.ctx.player_color_zerg))
 | 
						|
        await self.chat_send("?SetColor pz " + str(self.ctx.player_color_zerg_primal))
 | 
						|
        await self.chat_send("?SetColor da " + str(self.ctx.player_color_protoss))
 | 
						|
        await self.chat_send("?SetColor nova " + str(self.ctx.player_color_nova))
 | 
						|
        self.ctx.pending_color_update = False
 | 
						|
 | 
						|
    async def updateTerranTech(self, current_items):
 | 
						|
        terran_items = current_items[SC2Race.TERRAN]
 | 
						|
        await self.chat_send("?GiveTerranTech {} {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
 | 
						|
            terran_items[0], terran_items[1], terran_items[2], terran_items[3], terran_items[4],
 | 
						|
            terran_items[5], terran_items[6], terran_items[7], terran_items[8], terran_items[9], terran_items[10],
 | 
						|
            terran_items[11], terran_items[12], terran_items[13]))
 | 
						|
 | 
						|
    async def updateZergTech(self, current_items, kerrigan_level):
 | 
						|
        zerg_items = current_items[SC2Race.ZERG]
 | 
						|
        kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level)
 | 
						|
        kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0
 | 
						|
        await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format(
 | 
						|
            kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2],
 | 
						|
            zerg_items[3], zerg_items[4], zerg_items[5], zerg_items[6], zerg_items[9], zerg_items[10], zerg_items[11]
 | 
						|
        ))
 | 
						|
 | 
						|
    async def updateProtossTech(self, current_items):
 | 
						|
        protoss_items = current_items[SC2Race.PROTOSS]
 | 
						|
        await self.chat_send("?GiveProtossTech {} {} {} {} {} {} {} {} {} {}".format(
 | 
						|
            protoss_items[0], protoss_items[1], protoss_items[2], protoss_items[3], protoss_items[4],
 | 
						|
            protoss_items[5], protoss_items[6], protoss_items[7], protoss_items[8], protoss_items[9]
 | 
						|
        ))
 | 
						|
 | 
						|
 | 
						|
def request_unfinished_missions(ctx: SC2Context) -> None:
 | 
						|
    if ctx.mission_req_table:
 | 
						|
        message = "Unfinished Missions: "
 | 
						|
        unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
 | 
						|
        unfinished_locations: typing.Dict[SC2Mission, typing.List[str]] = {}
 | 
						|
 | 
						|
        _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
 | 
						|
 | 
						|
        for mission in unfinished_missions:
 | 
						|
            objectives = set(ctx.locations_for_mission(mission))
 | 
						|
            if objectives:
 | 
						|
                remaining_objectives = objectives.difference(ctx.checked_locations)
 | 
						|
                unfinished_locations[mission] = [ctx.location_names.lookup_in_game(location_id) for location_id in remaining_objectives]
 | 
						|
            else:
 | 
						|
                unfinished_locations[mission] = []
 | 
						|
 | 
						|
        # Removing All-In from location pool
 | 
						|
        final_mission = lookup_id_to_mission[ctx.final_mission]
 | 
						|
        if final_mission in unfinished_missions.keys():
 | 
						|
            message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
 | 
						|
            if unfinished_missions[final_mission] == -1:
 | 
						|
                unfinished_missions.pop(final_mission)
 | 
						|
 | 
						|
        message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}] " +
 | 
						|
                             mark_up_objectives(
 | 
						|
                                 f"[{len(unfinished_missions[mission])}/"
 | 
						|
                                 f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
 | 
						|
                                 ctx, unfinished_locations, mission)
 | 
						|
                             for mission in unfinished_missions)
 | 
						|
 | 
						|
        if ctx.ui:
 | 
						|
            ctx.ui.log_panels['All'].on_message_markup(message)
 | 
						|
            ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
 | 
						|
        else:
 | 
						|
            sc2_logger.info(message)
 | 
						|
    else:
 | 
						|
        sc2_logger.warning("No mission table found, you are likely not connected to a server.")
 | 
						|
 | 
						|
 | 
						|
def calc_unfinished_missions(ctx: SC2Context, unlocks: typing.Optional[typing.Dict] = None):
 | 
						|
    unfinished_missions: typing.List[str] = []
 | 
						|
    locations_completed: typing.List[typing.Union[typing.Set[int], typing.Literal[-1]]] = []
 | 
						|
 | 
						|
    if not unlocks:
 | 
						|
        unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
 | 
						|
 | 
						|
    available_missions = calc_available_missions(ctx, unlocks)
 | 
						|
 | 
						|
    for name in available_missions:
 | 
						|
        objectives = set(ctx.locations_for_mission(name))
 | 
						|
        if objectives:
 | 
						|
            objectives_completed = ctx.checked_locations & objectives
 | 
						|
            if len(objectives_completed) < len(objectives):
 | 
						|
                unfinished_missions.append(name)
 | 
						|
                locations_completed.append(objectives_completed)
 | 
						|
 | 
						|
        else:  # infer that this is the final mission as it has no objectives
 | 
						|
            unfinished_missions.append(name)
 | 
						|
            locations_completed.append(-1)
 | 
						|
 | 
						|
    return available_missions, dict(zip(unfinished_missions, locations_completed))
 | 
						|
 | 
						|
 | 
						|
def is_mission_available(ctx: SC2Context, mission_id_to_check: int) -> bool:
 | 
						|
    unfinished_missions = calc_available_missions(ctx)
 | 
						|
 | 
						|
    return any(mission_id_to_check == ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id for mission in unfinished_missions)
 | 
						|
 | 
						|
 | 
						|
def mark_up_mission_name(ctx: SC2Context, mission_name: str, unlock_table: typing.Dict) -> str:
 | 
						|
    """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
 | 
						|
 | 
						|
    campaign = ctx.find_campaign(mission_name)
 | 
						|
    mission_info = ctx.mission_req_table[campaign][mission_name]
 | 
						|
    if mission_info.completion_critical:
 | 
						|
        if ctx.ui:
 | 
						|
            message = "[color=AF99EF]" + mission_name + "[/color]"
 | 
						|
        else:
 | 
						|
            message = "*" + mission_name + "*"
 | 
						|
    else:
 | 
						|
        message = mission_name
 | 
						|
 | 
						|
    if ctx.ui:
 | 
						|
        campaign_missions = list(ctx.mission_req_table[campaign].keys())
 | 
						|
        unlocks: typing.List[str]
 | 
						|
        index = campaign_missions.index(mission_name)
 | 
						|
        if index in unlock_table[campaign]:
 | 
						|
            unlocks = unlock_table[campaign][index]
 | 
						|
        else:
 | 
						|
            unlocks = []
 | 
						|
 | 
						|
        if len(unlocks) > 0:
 | 
						|
            pre_message = f"[ref={mission_info.mission.id}|Unlocks: "
 | 
						|
            pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[ctx.find_campaign(unlock)][unlock].mission.id})" for unlock in unlocks)
 | 
						|
            pre_message += f"]"
 | 
						|
            message = pre_message + message + "[/ref]"
 | 
						|
 | 
						|
    return message
 | 
						|
 | 
						|
 | 
						|
def mark_up_objectives(message, ctx, unfinished_locations, mission):
 | 
						|
    formatted_message = message
 | 
						|
 | 
						|
    if ctx.ui:
 | 
						|
        locations = unfinished_locations[mission]
 | 
						|
        campaign = ctx.find_campaign(mission)
 | 
						|
 | 
						|
        pre_message = f"[ref={list(ctx.mission_req_table[campaign]).index(mission) + 30}|"
 | 
						|
        pre_message += "<br>".join(location for location in locations)
 | 
						|
        pre_message += f"]"
 | 
						|
        formatted_message = pre_message + message + "[/ref]"
 | 
						|
 | 
						|
    return formatted_message
 | 
						|
 | 
						|
 | 
						|
def request_available_missions(ctx: SC2Context):
 | 
						|
    if ctx.mission_req_table:
 | 
						|
        message = "Available Missions: "
 | 
						|
 | 
						|
        # Initialize mission unlock table
 | 
						|
        unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
 | 
						|
 | 
						|
        missions = calc_available_missions(ctx, unlocks)
 | 
						|
        message += \
 | 
						|
            ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
 | 
						|
                      f"[{ctx.mission_req_table[ctx.find_campaign(mission)][mission].mission.id}]"
 | 
						|
                      for mission in missions)
 | 
						|
 | 
						|
        if ctx.ui:
 | 
						|
            ctx.ui.log_panels['All'].on_message_markup(message)
 | 
						|
            ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
 | 
						|
        else:
 | 
						|
            sc2_logger.info(message)
 | 
						|
    else:
 | 
						|
        sc2_logger.warning("No mission table found, you are likely not connected to a server.")
 | 
						|
 | 
						|
 | 
						|
def calc_available_missions(ctx: SC2Context, unlocks: typing.Optional[dict] = None) -> typing.List[str]:
 | 
						|
    available_missions: typing.List[str] = []
 | 
						|
    missions_complete = 0
 | 
						|
 | 
						|
    # Get number of missions completed
 | 
						|
    for loc in ctx.checked_locations:
 | 
						|
        if loc % VICTORY_MODULO == 0:
 | 
						|
            missions_complete += 1
 | 
						|
 | 
						|
    for campaign in ctx.mission_req_table:
 | 
						|
        # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
 | 
						|
        for mission_name in ctx.mission_req_table[campaign]:
 | 
						|
            if unlocks:
 | 
						|
                for unlock in ctx.mission_req_table[campaign][mission_name].required_world:
 | 
						|
                    parsed_unlock = parse_unlock(unlock)
 | 
						|
                    # TODO prophecy-only wants to connect to WoL here
 | 
						|
                    index = parsed_unlock.connect_to - 1
 | 
						|
                    unlock_mission = list(ctx.mission_req_table[parsed_unlock.campaign])[index]
 | 
						|
                    unlock_campaign = ctx.find_campaign(unlock_mission)
 | 
						|
                    if unlock_campaign in unlocks:
 | 
						|
                        if index not in unlocks[unlock_campaign]:
 | 
						|
                            unlocks[unlock_campaign][index] = list()
 | 
						|
                        unlocks[unlock_campaign][index].append(mission_name)
 | 
						|
 | 
						|
            if mission_reqs_completed(ctx, mission_name, missions_complete):
 | 
						|
                available_missions.append(mission_name)
 | 
						|
 | 
						|
    return available_missions
 | 
						|
 | 
						|
 | 
						|
def parse_unlock(unlock: typing.Union[typing.Dict[typing.Literal["connect_to", "campaign"], int], MissionConnection, int]) -> MissionConnection:
 | 
						|
    if isinstance(unlock, int):
 | 
						|
        # Legacy
 | 
						|
        return MissionConnection(unlock)
 | 
						|
    elif isinstance(unlock, MissionConnection):
 | 
						|
        return unlock
 | 
						|
    else:
 | 
						|
        # Multi-campaign
 | 
						|
        return MissionConnection(unlock["connect_to"], lookup_id_to_campaign[unlock["campaign"]])
 | 
						|
 | 
						|
 | 
						|
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int) -> bool:
 | 
						|
    """Returns a bool signifying if the mission has all requirements complete and can be done
 | 
						|
 | 
						|
    Arguments:
 | 
						|
    ctx -- instance of SC2Context
 | 
						|
    locations_to_check -- the mission string name to check
 | 
						|
    missions_complete -- an int of how many missions have been completed
 | 
						|
    mission_path -- a list of missions that have already been checked
 | 
						|
    """
 | 
						|
    campaign = ctx.find_campaign(mission_name)
 | 
						|
 | 
						|
    if len(ctx.mission_req_table[campaign][mission_name].required_world) >= 1:
 | 
						|
        # A check for when the requirements are being or'd
 | 
						|
        or_success = False
 | 
						|
 | 
						|
        # Loop through required missions
 | 
						|
        for req_mission in ctx.mission_req_table[campaign][mission_name].required_world:
 | 
						|
            req_success = True
 | 
						|
            parsed_req_mission = parse_unlock(req_mission)
 | 
						|
 | 
						|
            # Check if required mission has been completed
 | 
						|
            mission_id = ctx.mission_req_table[parsed_req_mission.campaign][
 | 
						|
                list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1]].mission.id
 | 
						|
            if not (mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations:
 | 
						|
                if not ctx.mission_req_table[campaign][mission_name].or_requirements:
 | 
						|
                    return False
 | 
						|
                else:
 | 
						|
                    req_success = False
 | 
						|
 | 
						|
            # Grid-specific logic (to avoid long path checks and infinite recursion)
 | 
						|
            if ctx.mission_order in (MissionOrder.option_grid, MissionOrder.option_mini_grid, MissionOrder.option_medium_grid):
 | 
						|
                if req_success:
 | 
						|
                    return True
 | 
						|
                else:
 | 
						|
                    if parsed_req_mission == ctx.mission_req_table[campaign][mission_name].required_world[-1]:
 | 
						|
                        return False
 | 
						|
                    else:
 | 
						|
                        continue
 | 
						|
 | 
						|
            # Recursively check required mission to see if it's requirements are met, in case !collect has been done
 | 
						|
            # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
 | 
						|
            if not mission_reqs_completed(ctx, list(ctx.mission_req_table[parsed_req_mission.campaign])[parsed_req_mission.connect_to - 1], missions_complete):
 | 
						|
                if not ctx.mission_req_table[campaign][mission_name].or_requirements:
 | 
						|
                    return False
 | 
						|
                else:
 | 
						|
                    req_success = False
 | 
						|
 | 
						|
            # If requirement check succeeded mark or as satisfied
 | 
						|
            if ctx.mission_req_table[campaign][mission_name].or_requirements and req_success:
 | 
						|
                or_success = True
 | 
						|
 | 
						|
        if ctx.mission_req_table[campaign][mission_name].or_requirements:
 | 
						|
            # Return false if or requirements not met
 | 
						|
            if not or_success:
 | 
						|
                return False
 | 
						|
 | 
						|
        # Check number of missions
 | 
						|
        if missions_complete >= ctx.mission_req_table[campaign][mission_name].number:
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            return False
 | 
						|
    else:
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
def initialize_blank_mission_dict(location_table: typing.Dict[SC2Campaign, typing.Dict[str, MissionInfo]]):
 | 
						|
    unlocks: typing.Dict[SC2Campaign, typing.Dict] = {}
 | 
						|
 | 
						|
    for mission in list(location_table):
 | 
						|
        unlocks[mission] = {}
 | 
						|
 | 
						|
    return unlocks
 | 
						|
 | 
						|
 | 
						|
def check_game_install_path() -> bool:
 | 
						|
    # First thing: go to the default location for ExecuteInfo.
 | 
						|
    # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
 | 
						|
    if is_windows:
 | 
						|
        # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
 | 
						|
        # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
 | 
						|
        import ctypes.wintypes
 | 
						|
        CSIDL_PERSONAL = 5  # My Documents
 | 
						|
        SHGFP_TYPE_CURRENT = 0  # Get current, not default value
 | 
						|
 | 
						|
        buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
 | 
						|
        ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
 | 
						|
        documentspath: str = buf.value
 | 
						|
        einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
 | 
						|
    else:
 | 
						|
        einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
 | 
						|
 | 
						|
    # Check if the file exists.
 | 
						|
    if os.path.isfile(einfo):
 | 
						|
 | 
						|
        # Open the file and read it, picking out the latest executable's path.
 | 
						|
        with open(einfo) as f:
 | 
						|
            content = f.read()
 | 
						|
        if content:
 | 
						|
            search_result = re.search(r" = (.*)Versions", content)
 | 
						|
            if not search_result:
 | 
						|
                sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, "
 | 
						|
                                    "then try again.")
 | 
						|
                return False
 | 
						|
            base = search_result.group(1)
 | 
						|
 | 
						|
            if os.path.exists(base):
 | 
						|
                executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
 | 
						|
 | 
						|
                # Finally, check the path for an actual executable.
 | 
						|
                # If we find one, great. Set up the SC2PATH.
 | 
						|
                if os.path.isfile(executable):
 | 
						|
                    sc2_logger.info(f"Found an SC2 install at {base}!")
 | 
						|
                    sc2_logger.debug(f"Latest executable at {executable}.")
 | 
						|
                    os.environ["SC2PATH"] = base
 | 
						|
                    sc2_logger.debug(f"SC2PATH set to {base}.")
 | 
						|
                    return True
 | 
						|
                else:
 | 
						|
                    sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
 | 
						|
            else:
 | 
						|
                sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
 | 
						|
    else:
 | 
						|
        sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
 | 
						|
                           f"If that fails, please run /set_path with your SC2 install directory.")
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def is_mod_installed_correctly() -> bool:
 | 
						|
    """Searches for all required files."""
 | 
						|
    if "SC2PATH" not in os.environ:
 | 
						|
        check_game_install_path()
 | 
						|
    sc2_path: str = os.environ["SC2PATH"]
 | 
						|
    mapdir = sc2_path / Path('Maps/ArchipelagoCampaign')
 | 
						|
    mods = ["ArchipelagoCore", "ArchipelagoPlayer", "ArchipelagoPlayerSuper", "ArchipelagoPatches",
 | 
						|
            "ArchipelagoTriggers", "ArchipelagoPlayerWoL", "ArchipelagoPlayerHotS",
 | 
						|
            "ArchipelagoPlayerLotV", "ArchipelagoPlayerLotVPrologue", "ArchipelagoPlayerNCO"]
 | 
						|
    modfiles = [sc2_path / Path("Mods/" + mod + ".SC2Mod") for mod in mods]
 | 
						|
    wol_required_maps: typing.List[str] = ["WoL" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission
 | 
						|
                         if mission.campaign in (SC2Campaign.WOL, SC2Campaign.PROPHECY)]
 | 
						|
    hots_required_maps: typing.List[str] = ["HotS" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.HOTS]]
 | 
						|
    lotv_required_maps: typing.List[str] = ["LotV" + os.sep + mission.map_file + ".SC2Map" for mission in SC2Mission
 | 
						|
                                            if mission.campaign in (SC2Campaign.LOTV, SC2Campaign.PROLOGUE, SC2Campaign.EPILOGUE)]
 | 
						|
    nco_required_maps: typing.List[str] = ["NCO" + os.sep + mission.map_file + ".SC2Map" for mission in campaign_mission_table[SC2Campaign.NCO]]
 | 
						|
    required_maps = wol_required_maps + hots_required_maps + lotv_required_maps + nco_required_maps
 | 
						|
    needs_files = False
 | 
						|
 | 
						|
    # Check for maps.
 | 
						|
    missing_maps: typing.List[str] = []
 | 
						|
    for mapfile in required_maps:
 | 
						|
        if not os.path.isfile(mapdir / mapfile):
 | 
						|
            missing_maps.append(mapfile)
 | 
						|
    if len(missing_maps) >= 19:
 | 
						|
        sc2_logger.warning(f"All map files missing from {mapdir}.")
 | 
						|
        needs_files = True
 | 
						|
    elif len(missing_maps) > 0:
 | 
						|
        for map in missing_maps:
 | 
						|
            sc2_logger.debug(f"Missing {map} from {mapdir}.")
 | 
						|
        sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
 | 
						|
        needs_files = True
 | 
						|
    else:  # Must be no maps missing
 | 
						|
        sc2_logger.info(f"All maps found in {mapdir}.")
 | 
						|
 | 
						|
    # Check for mods.
 | 
						|
    for modfile in modfiles:
 | 
						|
        if os.path.isfile(modfile) or os.path.isdir(modfile):
 | 
						|
            sc2_logger.info(f"Archipelago mod found at {modfile}.")
 | 
						|
        else:
 | 
						|
            sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
 | 
						|
            needs_files = True
 | 
						|
 | 
						|
    # Final verdict.
 | 
						|
    if needs_files:
 | 
						|
        sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
 | 
						|
        return False
 | 
						|
    else:
 | 
						|
        sc2_logger.debug(f"All map/mod files are properly installed.")
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
class DllDirectory:
 | 
						|
    # Credit to Black Sliver for this code.
 | 
						|
    # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
 | 
						|
    _old: typing.Optional[str] = None
 | 
						|
    _new: typing.Optional[str] = None
 | 
						|
 | 
						|
    def __init__(self, new: typing.Optional[str]):
 | 
						|
        self._new = new
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        old = self.get()
 | 
						|
        if self.set(self._new):
 | 
						|
            self._old = old
 | 
						|
 | 
						|
    def __exit__(self, *args):
 | 
						|
        if self._old is not None:
 | 
						|
            self.set(self._old)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get() -> typing.Optional[str]:
 | 
						|
        if sys.platform == "win32":
 | 
						|
            n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
 | 
						|
            buf = ctypes.create_unicode_buffer(n)
 | 
						|
            ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
 | 
						|
            return buf.value
 | 
						|
        # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
 | 
						|
        return None
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def set(s: typing.Optional[str]) -> bool:
 | 
						|
        if sys.platform == "win32":
 | 
						|
            return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
 | 
						|
        # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def download_latest_release_zip(
 | 
						|
    owner: str,
 | 
						|
    repo: str,
 | 
						|
    api_version: str,
 | 
						|
    metadata: typing.Optional[str] = None,
 | 
						|
    force_download=False
 | 
						|
) -> typing.Tuple[str, typing.Optional[str]]:
 | 
						|
    """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
 | 
						|
    import requests
 | 
						|
 | 
						|
    headers = {"Accept": 'application/vnd.github.v3+json'}
 | 
						|
    url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
 | 
						|
 | 
						|
    r1 = requests.get(url, headers=headers)
 | 
						|
    if r1.status_code == 200:
 | 
						|
        latest_metadata = r1.json()
 | 
						|
        cleanup_downloaded_metadata(latest_metadata)
 | 
						|
        latest_metadata = str(latest_metadata)
 | 
						|
        # sc2_logger.info(f"Latest version: {latest_metadata}.")
 | 
						|
    else:
 | 
						|
        sc2_logger.warning(f"Status code: {r1.status_code}")
 | 
						|
        sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
 | 
						|
        sc2_logger.warning(f"text: {r1.text}")
 | 
						|
        return "", metadata
 | 
						|
 | 
						|
    if (force_download is False) and (metadata == latest_metadata):
 | 
						|
        sc2_logger.info("Latest version already installed.")
 | 
						|
        return "", metadata
 | 
						|
 | 
						|
    sc2_logger.info(f"Attempting to download latest version of API version {api_version} of {repo}.")
 | 
						|
    download_url = r1.json()["assets"][0]["browser_download_url"]
 | 
						|
 | 
						|
    r2 = requests.get(download_url, headers=headers)
 | 
						|
    if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
 | 
						|
        tempdir = tempfile.gettempdir()
 | 
						|
        file = tempdir + os.sep + f"{repo}.zip"
 | 
						|
        with open(file, "wb") as fh:
 | 
						|
            fh.write(r2.content)
 | 
						|
        sc2_logger.info(f"Successfully downloaded {repo}.zip.")
 | 
						|
        return file, latest_metadata
 | 
						|
    else:
 | 
						|
        sc2_logger.warning(f"Status code: {r2.status_code}")
 | 
						|
        sc2_logger.warning("Download failed.")
 | 
						|
        sc2_logger.warning(f"text: {r2.text}")
 | 
						|
        return "", metadata
 | 
						|
 | 
						|
 | 
						|
def cleanup_downloaded_metadata(medatada_json: dict) -> None:
 | 
						|
    for asset in medatada_json['assets']:
 | 
						|
        del asset['download_count']
 | 
						|
 | 
						|
 | 
						|
def is_mod_update_available(owner: str, repo: str, api_version: str, metadata: str) -> bool:
 | 
						|
    import requests
 | 
						|
 | 
						|
    headers = {"Accept": 'application/vnd.github.v3+json'}
 | 
						|
    url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{api_version}"
 | 
						|
 | 
						|
    r1 = requests.get(url, headers=headers)
 | 
						|
    if r1.status_code == 200:
 | 
						|
        latest_metadata = r1.json()
 | 
						|
        cleanup_downloaded_metadata(latest_metadata)
 | 
						|
        latest_metadata = str(latest_metadata)
 | 
						|
        if metadata != latest_metadata:
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            return False
 | 
						|
 | 
						|
    else:
 | 
						|
        sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
 | 
						|
        sc2_logger.warning(f"Status code: {r1.status_code}")
 | 
						|
        sc2_logger.warning(f"text: {r1.text}")
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def get_location_offset(mission_id):
 | 
						|
    return SC2WOL_LOC_ID_OFFSET if mission_id <= SC2Mission.ALL_IN.id \
 | 
						|
        else (SC2HOTS_LOC_ID_OFFSET - SC2Mission.ALL_IN.id * VICTORY_MODULO)
 | 
						|
 | 
						|
 | 
						|
def launch():
 | 
						|
    colorama.init()
 | 
						|
    asyncio.run(main())
 | 
						|
    colorama.deinit()
 |