async def query_available_abilities( self, units: Union[List[Unit], "Units"], ignore_resource_requirements: bool = False ) -> List[List[AbilityId]]: """ Query abilities of multiple units """ if not isinstance(units, list): """ Deprecated, accepting a single unit may be removed in the future, query a list of units instead """ assert isinstance(units, Unit) units = [units] input_was_a_list = False else: input_was_a_list = True assert len(units) > 0 result = await self._execute(query=query_pb.RequestQuery( abilities=[ query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units ], ignore_resource_requirements=ignore_resource_requirements)) """ Fix for bots that only query a single unit """ if not input_was_a_list: return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities][0] return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities]
def generate_redirect_abilities_dict(data: dict): ability_data = data["Ability"] unit_data = data["Unit"] upgrade_data = data["Upgrade"] # Load pickled game data files path = os.path.dirname(__file__) pickled_files_folder_path = os.path.join(path, "test", "pickle_data") pickled_files = os.listdir(pickled_files_folder_path) random_pickled_file = next(f for f in pickled_files if f.endswith(".xz")) with lzma.open( os.path.join(pickled_files_folder_path, random_pickled_file), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) game_data = GameData(raw_game_data.data) all_redirect_abilities: Dict[AbilityId, AbilityId] = OrderedDict2() entry: dict for entry in ability_data: ability_id_value: int = entry["id"] try: ability_id: AbilityId = AbilityId(ability_id_value) except Exception as e: print(f"Error with ability id value {ability_id_value}") continue generic_redirect_ability_value: int = game_data.abilities[ ability_id_value]._proto.remaps_to_ability_id if generic_redirect_ability_value: # Might be 0 if it has no redirect ability all_redirect_abilities[ability_id] = AbilityId( generic_redirect_ability_value) return all_redirect_abilities
def generate_redirect_abilities_dict(data: dict): ability_data = data["Ability"] unit_data = data["Unit"] upgrade_data = data["Upgrade"] # Load pickled game data files pickled_file_path = Path( __file__).parent / "test" / "pickle_data" / "AcropolisLE.xz" assert pickled_file_path.is_file( ), f"Could not find pickled data file {pickled_file_path}" logger.info(f"Loading pickled game data file {pickled_file_path}") with lzma.open(pickled_file_path.absolute(), "rb") as f: raw_game_data, raw_game_info, raw_observation = pickle.load(f) game_data = GameData(raw_game_data.data) all_redirect_abilities: Dict[AbilityId, AbilityId] = OrderedDict2() entry: dict for entry in ability_data: ability_id_value: int = entry["id"] try: ability_id: AbilityId = AbilityId(ability_id_value) except Exception as e: logger.info(f"Error with ability id value {ability_id_value}") continue generic_redirect_ability_value: int = game_data.abilities[ ability_id_value]._proto.remaps_to_ability_id if generic_redirect_ability_value: # Might be 0 if it has no redirect ability all_redirect_abilities[ability_id] = AbilityId( generic_redirect_ability_value) return all_redirect_abilities
async def query_available_abilities(self, unit): assert isinstance(unit, Unit) result = await self._execute(query=query_pb.RequestQuery( abilities=[query_pb.RequestQueryAvailableAbilities( unit_tag=unit.tag )] )) return [AbilityId(a.ability_id) for a in result.query.abilities[0].abilities]
def ability_specialization_allowed_for(self, a_id, u_id): """Can this unit use this specialization, e.g. do not allow LIFT_COMMANDCENTER for BARRACKS.""" assert isinstance(a_id, int) assert isinstance(u_id, int) a_name = AbilityId(a_id).name.upper() u_name = UnitTypeId(u_id).name if not self.is_specialization(a_name): return True for postfix in ["FLYING", "BURROWED", "MP"]: u_name = remove_postfix(u_name, postfix) return u_name in a_name
async def query_available_abilities(self, units, ignore_resource_requirements=False): if not isinstance(units, list): assert isinstance(units, Unit) units = [units] assert len(units) > 0 result = await self._execute(query=query_pb.RequestQuery( abilities=[ query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units ], ignore_resource_requirements=ignore_resource_requirements)) return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities]
async def query_available_abilities_with_tag( self, units: Union[List[Unit], Units], ignore_resource_requirements: bool = False ) -> Dict[int, Set[AbilityId]]: """ Query abilities of multiple units """ result = await self._execute(query=query_pb.RequestQuery( abilities=(query_pb.RequestQueryAvailableAbilities( unit_tag=unit.tag) for unit in units), ignore_resource_requirements=ignore_resource_requirements, )) return { b.unit_tag: {AbilityId(a.ability_id) for a in b.abilities} for b in result.query.abilities }
async def military_executioner(self, actions): ''' Actions is a vector with the nodes corresponding to: - for each control_group - for each ability - priority - pos[0] - pos[1] They result in the following abilities: - to_actions(*) ''' todo_actions = [] idx = 0 for control_group in self.control_groups: group_units = control_group.select_units(self.units) for ability_id in C.ZERG_MILITARY_ABILITIES_IDS: ability_id = AbilityId(ability_id) priority, pos0, pos1 = actions[idx], actions[idx + 1], actions[idx + 2] target = self.normal_to_mapscale(Point2([pos0, pos1])) self.feed_interest_map(target) if priority > 0.5: for unit in group_units: if ability["target"] == "None" and await self.can_cast( unit, ability_id): todo_actions.append(unit(ability_id)) elif ability["target"] == "Unit": target_unit = self.state.units.closest_to(target) if await self.can_cast(unit, ability_id): todo_actions.append( unit(ability_id, target_unit)) # end group_units self.log_action(ability_id, target, control_group) # end abilities idx += 3 # end control_groups await self.do_actions(todo_actions)
def get_unit_abilities(data: dict): ability_data = data["Ability"] unit_data = data["Unit"] upgrade_data = data["Upgrade"] all_unit_abilities: Dict[UnitTypeId, Set[AbilityId]] = OrderedDict2() entry: dict for entry in unit_data: entry_unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) current_collected_unit_abilities: Set[AbilityId] = OrderedSet2() for ability_info in entry_unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) current_collected_unit_abilities.add(ability_id) # logger.info(unit_type, current_unit_abilities) if current_collected_unit_abilities: all_unit_abilities[unit_type] = current_collected_unit_abilities return all_unit_abilities
def serialize_ability(self, a): """Return None to skip this.""" # TODO: Buffs # TODO: Check interceptors: BUILD_INTERCEPTORS # TODO: Calldowns: SUPPLYDROP_SUPPLYDROP, CALLDOWNMULE_CALLDOWNMULE # TODO: Infestors: INFESTEDTERRANS_INFESTEDTERRANS, INFESTORENSNARE_INFESTORENSNARE # Unmangled id and name, i.e. unit specific variants also real_id = a._proto.ability_id real_name = AbilityId(a._proto.ability_id).name # Check if useless if ( a.id.name.endswith("_DOORDEFAULTCLOSE") or a.id.name.endswith("_BRIDGERETRACT") or a.id.name.endswith("_BRIDGEEXTEND") or "CRITTER" in real_name ): return None is_morph = ( "MORPH" in a.id.name or a.id.name.startswith("LARVATRAIN_") or a.id.name.startswith("UPGRADETO") or a.id.name.startswith("BURROW") or a.id.name in ["LIFT", "LAND", "SIEGEMODE_SIEGEMODE", "UNSIEGE_UNSIEGE"] ) is_building = a._proto.is_building or a.id.name == "BUILDAUTOTURRET_AUTOTURRET" target_name = AbilityData.Target.Name(a._proto.target) if a.id.name in ["BUILD_REACTOR", "BUILD_TECHLAB"]: assert target_name == "PointOrNone", f"{a.id.name}: {target_name}" if "REACTOR" in a.id.name: unit_id = UnitTypeId.REACTOR.value elif "TECHLAB" in a.id.name: unit_id = UnitTypeId.TECHLAB.value else: assert False, "Invalid add-on" build = {"target": {"BuildInstant": {"produces": unit_id}}} elif is_building: if real_id in self.create_abilities: unit_id = self.create_abilities[real_id] else: unit_id = 0 if a._proto.is_instant_placement: assert target_name == "PointOrNone", f"{a.id.name}: {target_name}" build = {"target": {"BuildInstant": {"produces": unit_id}}} elif target_name == "Unit": build = {"target": {"BuildOnUnit": {"produces": unit_id}}} else: assert target_name == "Point", f"{a.id.name}: {target_name}" build = {"target": {"Build": {"produces": unit_id}}} elif "RESEARCH_" in a.id.name: assert target_name == "None", f"{a.id.name}: {target_name}" if real_id not in self.upgrade_abilities: # Not in the game anymore return None build = { "target": {"Research": {"upgrade": self.upgrade_abilities[real_id]}} } elif is_morph: if real_id in self.create_abilities: unit_id = self.create_abilities[real_id] else: unit_id = 0 for prefix in SPEC_PREFIX: if real_name.startswith(prefix): name = remove_prefix(real_name, prefix) if name in ["SWARMHOST", "LURKER"]: name += "MP" cands = [ u for u in self.data_units if name == u["name"].upper() ] assert len(cands) == 1, name unit_id = cands[0]["id"] break for infix in SPEC_INFIX: if infix in real_name: name = real_name[: real_name.find(infix)] cands = [ u for u in self.data_units if name == u["name"].upper() ] assert len(cands) == 1, name unit_id = cands[0]["id"] break if target_name == "Point": build = {"target": {"MorphPlace": {"produces": unit_id}}} else: assert target_name == "None", f"{a.id.name}: {target_name}" build = {"target": {"Morph": {"produces": unit_id}}} elif "WARPGATETRAIN_" in a.id.name or "TRAINWARP_ADEPT" == a.id.name: assert target_name == "Point", f"{a.id.name}: {target_name}" build = {"target": {"TrainPlace": {"produces": 0}}} elif "TRAIN_" in a.id.name or a.id.name == "SPAWNCHANGELING_SPAWNCHANGELING": assert target_name == "None", f"{a.id.name}: {target_name}" build = {"target": {"Train": {"produces": 0}}} elif a.id.name.startswith("EFFECT_"): # TODO: Effects build = {"target": target_name} elif a.id.name.startswith("HALLUCINATION_"): # TODO: Hallucinations? assert target_name == "None", f"{a.id.name}: {target_name}" build = {"target": target_name} else: build = {"target": target_name} return { **{ "id": real_id, "name": real_name, "cast_range": a._proto.cast_range, "energy_cost": 0, "allow_minimap": a._proto.allow_minimap, "allow_autocast": a._proto.allow_autocast, "cost": EMPTY_COST, "effect": [], "buff": [], "cooldown": 0, }, **build, }
def patch(): assert SOURCE_DIR.exists() TARGET_DIR.mkdir(exist_ok=True) with (SOURCE_DIR / "ability.toml").open() as f: c_ability = toml.load(f) with (SOURCE_DIR / "unit.toml").open() as f: c_unit = toml.load(f) with (SOURCE_DIR / "upgrade.toml").open() as f: c_upgrade = toml.load(f) with (Path("generate") / "patches" / "ability_requirement.toml").open() as f: patch_ability_requirement = toml.load(f) with (Path("generate") / "patches" / "ability_produces_replace.toml").open() as f: patch_ability_produces_replace = toml.load(f) with (Path("generate") / "patches" / "ability_produces_missing.toml").open() as f: patch_ability_produces_missing = toml.load(f) with (Path("generate") / "patches" / "unit_ability_missing.toml").open() as f: patch_unit_ability_missing = toml.load(f) with (Path("generate") / "patches" / "unit_ability_disallowed.toml").open() as f: patch_unit_ability_disallowed = toml.load(f) # Patch unit abilities for unit in c_unit["Unit"]: unit_id = unit["id"] current_abilities = [int(a["ability"]) for a in unit["abilities"]] for p in patch_unit_ability_missing.get(str(unit_id), []): assert ( int(p["ability"]) not in current_abilities ), f"Already defined: ({unit['name']}) {unit_id} {p['ability']}" unit["abilities"].append(p) # Patch replacement ability products for abil in c_ability["Ability"]: abil_id = abil["id"] abil_name = abil["name"] if isinstance(abil["target"], dict): keys = abil["target"].keys() assert len(keys) == 1 k = list(keys)[0] if k == "Research": assert abil["target"][k]["upgrade"] != 0 else: if abil["target"][k]["produces"] == 0: p = patch_ability_produces_replace.get(str(abil_id)) assert p, f"Missing ability product: [{abil_id}] # {abil_name}" abil["target"][k]["produces"] = p["produces"] # Patch missing ability products for abil in c_ability["Ability"]: abil_id = abil["id"] patch_target = patch_ability_produces_missing.get(str(abil_id)) if patch_target is None: continue assert set(patch_target.keys()) == {"target"} abil.update(patch_target) # Patch unit requirements for unit in c_unit["Unit"]: unit_id = unit["id"] unit_name = unit["name"] for i, ability in list(enumerate(unit["abilities"]))[::-1]: assert set(ability.keys()) <= { "ability", "requirements", }, f"Keys? {ability.keys()}" ability_id = ability["ability"] # Check if disallowed d0 = patch_unit_ability_disallowed.get(str(unit_id)) if d0: d1 = d0.get(str(ability_id)) if d1 is not None: assert d1["disallow"] del unit["abilities"][i] continue if ability.get("requirements") == "???": # Check if defined ability_name = AbilityId(ability_id).name patch_name = f"[{unit_id}.{ability_id}] # {unit_name} - {ability_name}" p0 = patch_ability_requirement.get(str(unit["id"])) assert p0, f"Missing unit requirement: {patch_name}" p1 = p0.get(str(ability_id)) assert p1, f"Missing unit requirement: {patch_name}" ability["requirements"] = p1["requirements"] else: # Check if defined even if not used ability_name = AbilityId(ability_id).name patch_name = f"[{unit_id}.{ability_id}] # {unit_name} - {ability_name}" p0 = patch_ability_requirement.get(str(unit["id"])) if p0: p1 = p0.get(str(ability_id)) assert not p1, f"Redundant unit requirement: {patch_name}" # assert not p1, f"Redundant unit requirement: {patch_name}" with (T_TOML_DIR / "ability.toml").open("w") as f: toml.dump(c_ability, f) with (T_TOML_DIR / "unit.toml").open("w") as f: toml.dump(c_unit, f) with (T_TOML_DIR / "upgrade.toml").open("w") as f: toml.dump(c_upgrade, f) with (TARGET_DIR / "ability.json").open("w") as f: json.dump(c_ability, f, separators=(",", ":")) with (TARGET_DIR / "unit.json").open("w") as f: json.dump(c_unit, f, separators=(",", ":")) with (TARGET_DIR / "upgrade.json").open("w") as f: json.dump(c_upgrade, f, separators=(",", ":"))
def get_unit_train_build_abilities(data): ability_data = data["Ability"] unit_data = data["Unit"] upgrade_data = data["Upgrade"] # From which abilities can a unit be trained train_abilities: Dict[UnitTypeId, Set[AbilityId]] = OrderedDict2() # If the ability requires a placement position ability_requires_placement: Set[AbilityId] = set() # Map ability to unittypeid ability_to_unittypeid_dict: Dict[AbilityId, UnitTypeId] = OrderedDict2() # From which abilities can a unit be morphed # unit_morph_abilities: Dict[UnitTypeId, Set[AbilityId]] = {} entry: dict for entry in ability_data: """ "target": "PointOrUnit" """ if isinstance(entry.get("target", {}), str): continue ability_id: AbilityId = AbilityId(entry["id"]) created_unit_type_id: UnitTypeId # Check if it is a unit train ability requires_placement = False train_unit_type_id_value: int = entry.get("target", {}).get( "Train", {}).get("produces", 0) train_place_unit_type_id_value: int = entry.get("target", {}).get( "TrainPlace", {}).get("produces", 0) morph_unit_type_id_value: int = entry.get("target", {}).get( "Morph", {}).get("produces", 0) build_unit_type_id_value: int = entry.get("target", {}).get( "Build", {}).get("produces", 0) build_on_unit_unit_type_id_value: int = entry.get("target", {}).get( "BuildOnUnit", {}).get("produces", 0) if not train_unit_type_id_value and train_place_unit_type_id_value: train_unit_type_id_value = train_place_unit_type_id_value requires_placement = True # Collect larva morph abilities, and one way morphs (exclude burrow, hellbat morph, siege tank siege) # Also doesnt include building addons if not train_unit_type_id_value and ( "LARVATRAIN_" in ability_id.name or ability_id in { AbilityId.MORPHTOBROODLORD_BROODLORD, AbilityId.MORPHZERGLINGTOBANELING_BANELING, AbilityId.MORPHTORAVAGER_RAVAGER, AbilityId.MORPH_LURKER, AbilityId.UPGRADETOLAIR_LAIR, AbilityId.UPGRADETOHIVE_HIVE, AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE, AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS, AbilityId.MORPH_OVERLORDTRANSPORT, AbilityId.MORPH_OVERSEER, }): # If all morph units are used, unit_trained_from.py will be "wrong" because it will list that a siege tank can be trained from siegetanksieged and similar: # UnitTypeId.SIEGETANK: {UnitTypeId.SIEGETANKSIEGED, UnitTypeId.FACTORY}, # if not train_unit_type_id_value and morph_unit_type_id_value: train_unit_type_id_value = morph_unit_type_id_value # Add all build abilities, like construct buildings and train queen (exception) if not train_unit_type_id_value and build_unit_type_id_value: train_unit_type_id_value = build_unit_type_id_value if "BUILD_" in entry["name"]: requires_placement = True # Add build gas building (refinery, assimilator, extractor) # TODO: target needs to be a unit, not a position, but i dont want to store an extra line just for this - needs to be an exception in bot_ai.py if not train_unit_type_id_value and build_on_unit_unit_type_id_value: train_unit_type_id_value = build_on_unit_unit_type_id_value if train_unit_type_id_value: created_unit_type_id = UnitTypeId(train_unit_type_id_value) if created_unit_type_id not in train_abilities: train_abilities[created_unit_type_id] = {ability_id} else: train_abilities[created_unit_type_id].add(ability_id) if requires_placement: ability_requires_placement.add(ability_id) ability_to_unittypeid_dict[ability_id] = created_unit_type_id """ unit_train_abilities = { UnitTypeId.GATEWAY: { UnitTypeId.ADEPT: { "ability": AbilityId.TRAIN_ADEPT, "requires_techlab": False, "required_building": UnitTypeId.CYBERNETICSCORE, # Or None "requires_placement_position": False, # True for warp gate "requires_power": True, # If a pylon nearby is required }, UnitTypeId.Zealot: { "ability": AbilityId.GATEWAYTRAIN_ZEALOT, ... } } } """ unit_train_abilities: Dict[UnitTypeId, Dict[str, Union[AbilityId, bool, UnitTypeId]]] = OrderedDict2() for entry in unit_data: unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) current_unit_train_abilities = OrderedDict2() for ability_info in unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) # Ability is not a train ability if ability_id not in ability_to_unittypeid_dict: continue requires_techlab: bool = False required_building: Optional[UnitTypeId] = None requires_placement_position: bool = False requires_power: bool = False """ requirements = [ { "addon": 5 }, { "building": 29 } ] """ requirements: List[Dict[str, int]] = ability_info.get( "requirements", []) if requirements: # Assume train abilities only have one tech building requirement; thors requiring armory and techlab is seperatedly counted assert ( len([ req for req in requirements if req.get("building", 0) ]) <= 1 ), f"Error: Building {unit_type} has more than one tech requirements with train ability {ability_id}" # UnitTypeId 5 == Techlab requires_techlab: bool = any(req for req in requirements if req.get("addon", 0) == 5) requires_tech_builing_id_value: int = next( (req["building"] for req in requirements if req.get("building", 0)), 0) if requires_tech_builing_id_value: required_building = UnitTypeId( requires_tech_builing_id_value) if ability_id in ability_requires_placement: requires_placement_position = True requires_power = entry.get("needs_power", False) resulting_unit = ability_to_unittypeid_dict[ability_id] ability_dict = {"ability": ability_id} # Only add boolean values and tech requirement if they actually exist, to make the resulting dict file smaller if requires_techlab: ability_dict["requires_techlab"] = requires_techlab if required_building: ability_dict["required_building"] = required_building if requires_placement_position: ability_dict[ "requires_placement_position"] = requires_placement_position if requires_power: ability_dict["requires_power"] = requires_power current_unit_train_abilities[resulting_unit] = ability_dict if current_unit_train_abilities: unit_train_abilities[unit_type] = current_unit_train_abilities return unit_train_abilities
def get_upgrade_abilities(data): ability_data = data["Ability"] unit_data = data["Unit"] upgrade_data = data["Upgrade"] ability_to_upgrade_dict: Dict[AbilityId, UpgradeId] = OrderedDict2() """ We want to be able to research an upgrade by doing await self.can_research(UpgradeId, return_idle_structures=True) -> returns list of idle structures that can research it So we need to assign each upgrade id one building type, and its research ability and requirements (e.g. armory for infantry level 2) """ # Collect all upgrades and their corresponding abilities entry: dict for entry in ability_data: if isinstance(entry.get("target", {}), str): continue ability_id: AbilityId = AbilityId(entry["id"]) researched_ability_id: UnitTypeId upgrade_id_value: int = entry.get("target", {}).get("Research", {}).get("upgrade", 0) if upgrade_id_value: upgrade_id: UpgradeId = UpgradeId(upgrade_id_value) ability_to_upgrade_dict[ability_id] = upgrade_id """ unit_research_abilities = { UnitTypeId.ENGINEERINGBAY: { UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, "required_building": None, "requires_power": False, # If a pylon nearby is required }, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: { "ability": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, "required_building": UnitTypeId.ARMORY, "requires_power": False, # If a pylon nearby is required }, } } """ unit_research_abilities = OrderedDict2() for entry in unit_data: unit_abilities = entry.get("abilities", []) unit_type = UnitTypeId(entry["id"]) if unit_type == UnitTypeId.TECHLAB: continue current_unit_research_abilities = OrderedDict2() for ability_info in unit_abilities: ability_id_value: int = ability_info.get("ability", 0) if ability_id_value: ability_id: AbilityId = AbilityId(ability_id_value) # Upgrade is not a known upgrade ability if ability_id not in ability_to_upgrade_dict: continue required_building = None required_upgrade = None requirements = ability_info.get("requirements", []) if requirements: req_building_id_value = next( (req["building"] for req in requirements if req.get("building", 0)), None) if req_building_id_value: req_building_id = UnitTypeId(req_building_id_value) required_building = req_building_id req_upgrade_id_value = next( (req["upgrade"] for req in requirements if req.get("upgrade", 0)), None) if req_upgrade_id_value: req_upgrade_id = UpgradeId(req_upgrade_id_value) required_upgrade = req_upgrade_id requires_power = entry.get("needs_power", False) resulting_upgrade = ability_to_upgrade_dict[ability_id] research_info = {"ability": ability_id} if required_building: research_info["required_building"] = required_building if required_upgrade: research_info["required_upgrade"] = required_upgrade if requires_power: research_info["requires_power"] = requires_power current_unit_research_abilities[ resulting_upgrade] = research_info if current_unit_research_abilities: unit_research_abilities[ unit_type] = current_unit_research_abilities return unit_research_abilities
def exact_id(self) -> AbilityId: """ Returns the exact ID of the ability """ return AbilityId(self._proto.ability_id)
def id(self) -> AbilityId: """ Returns the generic remap ID. See sc2/dicts/generic_redirect_abilities.py """ if self._proto.remaps_to_ability_id: return AbilityId(self._proto.remaps_to_ability_id) return AbilityId(self._proto.ability_id)
def exact_id(self) -> AbilityId: return AbilityId(self.ability_id)