async def champions_stats(self, ctx: commands.Context, user_id=None, date_start=None): """Returns your games total and winrate for all champions. """ try: player = await self.get_player_with_team_check(ctx, user_id) except PermissionError: return date_start = dateparser.parse(date_start) if date_start else date_start stats = player.get_champions_stats(date_start) table = [] for champion_id in stats: table.append([ lit.get_name(champion_id) if champion_id else "Unknown", f"{stats[champion_id].role.capitalize()}", stats[champion_id].games, f"{stats[champion_id].wins / stats[champion_id].games * 100:.1f}%", ]) # Sorting the table by games total table = sorted(table, key=lambda x: -x[2]) # Adding the header last to not screw with the sorting table.insert(0, ["Champion", "Role", "Games", "Winrate"]) await ctx.send(f'```{tabulate(table, headers="firstrow")}```')
async def history(self, ctx: commands.Context, user_id=None, display_games=20): """ Returns your match history in a table. If user_id is supplied, shows the user’s match history. Requires being on the same team or admin. display_games specifies how many games to show and is 20 by default. """ try: player = await self.get_player_with_team_check(ctx, user_id) except PermissionError: return games_list = player.get_latest_games(display_games) table = [["Game ID", "Date", "Role", "Champion", "Result"]] for game, participant in games_list: table.append([ game.id, game.date.date(), participant.role, lit.get_name(participant.champion_id) if participant.champion_id else "Unknown", "Win" if game.winner == participant.team else "Loss", ]) await ctx.send(f'```{tabulate(table, headers="firstrow")}```')
def test_parse_summoners(): data = parsing.parse_summoners(latest_version) # Not that many summoners assert len(data) > 5 for id_, name in data.items(): # We test using the *old* parser! assert name == lol_id_tools.get_name(id_, object_type="summoner_spell")
def test_parse_items(): data = parsing.parse_items(latest_version) # More items than Pokemons assert len(data) > 200 for id_, name in data.items(): # We test using the *old* parser! assert name == lol_id_tools.get_name(id_, object_type="item")
def test_parse_runes(): data = parsing.parse_runes(latest_version) # Not that many runes really assert len(data) > 5 * 9 for id_, name in data.items(): # We test using the *old* parser! assert name == lol_id_tools.get_name(id_, object_type="rune")
def test_get_name_with_patch(tuple): id_, name, type_, patch = tuple response = lol_id_tools.get_name( id_, object_type=type_, patch=patch, ) assert response == name
def test_botrk_id(): # Base case assert lit.get_id("Blade of the Ruined King") == 3153 # Case sensitivity test assert lit.get_id("Blade of the ruined king") == 3153 # Typo test assert lit.get_id("Blade of the kuined ring") == 3153 # Korean test assert lit.get_id("몰락한 왕의 검") == 3153 # Nickname test assert lit.get_id("botrk") == 3153 # Name test assert lit.get_name(3153, object_type="item") == "Blade of the Ruined King"
def test_grasp_id(): # Base case assert lit.get_id("Grasp of the Undying") == 8437 # Case sensitivity test assert lit.get_id("grasp of the undying") == 8437 # Shorthand test assert lit.get_id("undying") == 8437 # Typo test assert lit.get_id("graps of the undying") == 8437 # Korean test assert lit.get_id("착취의 손아귀") == 8437 # Name test assert lit.get_name(8437, object_type="rune") == "Grasp of the Undying"
def get_champion_emoji(champion_id: Optional[int], bot) -> str: if champion_id is None: return "❔" champion_name = lol_id_tools.get_name(champion_id, object_type="champion") emoji_name = no_symbols_regex.sub("", champion_name).replace(" ", "") for emoji in bot.emojis: emoji: Emoji if emoji.name == emoji_name: return str(emoji) return champion_name
def test_botrk_id(): # Base case assert lit.get_id("Blade of the Ruined King") == 3153 # Case sensitivity test assert lit.get_id("Blade of the ruined king") == 3153 # Typo test assert lit.get_id("Blade of the kuined ring") == 3153 # OBSOLETE - We don't support nicknames or different locales anymore # Korean test # assert lit.get_id("몰락한 왕의 검") == 3153 # Nickname test # assert lit.get_id("botrk") == 3153 # Name test assert lit.get_name(3153, object_type="item") == "Blade of The Ruined King"
def test_mf_id(): # Base case assert lit.get_id("Miss Fortune", object_type="champion") == 21 # Case sensitivity test assert lit.get_id("missfortune", object_type="champion") == 21 # Typo test assert lit.get_id("misforune", object_type="champion") == 21 # Different languages test assert lit.get_id("미스 포츈", input_locale="ko_KR") == 21 assert lit.get_id("미스 포츈", input_locale="korean") == 21 assert lit.get_id("missfortune", input_locale="french") == 21 # Nickname test assert lit.get_id("MF") == 21 # Get name from ID test assert lit.get_name(21, object_type="champion") == "Miss Fortune"
def get_champion_emoji(emoji_input: Optional[Union[int, str]], bot) -> str: """ Accepts champion IDs, "loading", and None """ emoji_name = None fallback = None if emoji_input is None: return "❔" elif emoji_input == "loading": emoji_name = emoji_input fallback = "❔" elif type(emoji_input) == int: fallback = lol_id_tools.get_name(emoji_input, object_type="champion") emoji_name = no_symbols_regex.sub("", fallback).replace(" ", "") for emoji in bot.emojis: emoji: Emoji if emoji.name == emoji_name: return str(emoji) # Fallback that should only be reached when we don’t find the rights emoji return fallback
def add_runes(player: lol_dto.classes.game.LolGamePlayer, runes_info, patch, add_names=True): player_runes = next(p for p in runes_info if p["hero_id_"] == player["championId"]) player["runes"] = [] for rune_index, rune in enumerate(player_runes["runes_info_"]["runes_list_"]): # slot is 0 for keystones, 1 for first rune of primary tree, ... # rank is 1 for most keystones, except stat perks that can be 2 rune = lol_dto.classes.game.LolGamePlayerRune(id=rune["runes_id_"], slot=rune_index, rank=rune["runes_num_"],) if add_names: rune["name"] = lit.get_name(rune["id"], object_type="rune") player["runes"].append(rune) # We need patch information to properly load rune tree names if patch: player["primaryRuneTreeId"], player["primaryRuneTreeName"] = rune_tree_handler.get_primary_tree( player["runes"], patch ) (player["secondaryRuneTreeId"], player["secondaryRuneTreeName"],) = rune_tree_handler.get_secondary_tree( player["runes"], patch ) return player
def match_to_game(match_dto: dict, add_names: bool = False) -> game_dto.LolGame: """Returns a LolGame from a MatchDto. Args: match_dto: A MatchDto from Riot’s API add_names: whether or not to add names for human readability in the DTO. False by default. Returns: The LolGame representation of the game. """ riot_source = { "riotLolApi": RiotGameIdentifier( gameId=match_dto["gameId"], platformId=match_dto["platformId"] ) } log_prefix = ( f"gameId {match_dto['gameId']}|" f"platformId {match_dto['platformId']}:\t" ) info_log = set() date_time = datetime.utcfromtimestamp(match_dto["gameCreation"] / 1000) date_time = date_time.replace(tzinfo=timezone.utc) iso_date = date_time.isoformat(timespec="seconds") patch = ".".join(match_dto["gameVersion"].split(".")[:2]) winner = ( "BLUE" if (match_dto["teams"][0]["teamId"] == 100) == (match_dto["teams"][0]["win"] == "Win") else "RED" ) # TODO Change optional fields to .get() instead of [], do it in timeline too game = game_dto.LolGame( sources=riot_source, duration=match_dto["gameDuration"], start=iso_date, patch=patch, gameVersion=match_dto["gameVersion"], winner=winner, teams={}, ) for team in match_dto["teams"]: side = "BLUE" if team["teamId"] == 100 else "RED" # TODO Handle old games with elemental drakes before they were part of the API team_dto = game_dto.LolGameTeam( endOfGameStats=game_dto.LolGameTeamEndOfGameStats( riftHeraldKills=team.get("riftHeraldKills"), dragonKills=team.get("dragonKills"), baronKills=team.get("baronKills"), towerKills=team.get("towerKills"), inhibitorKills=team.get("inhibitorKills"), firstTower=team.get("firstTower"), firstInhibitor=team.get("firstInhibitor"), firstRiftHerald=team.get("firstRiftHerald"), firstDragon=team.get("firstDragon"), firstBaron=team.get("firstBaron"), ) ) team_dto["bans"] = [b["championId"] for b in team["bans"]] team_dto["players"] = [] for participant in match_dto["participants"]: if participant["teamId"] != team["teamId"]: continue try: participant_identity = next( identity["player"] for identity in match_dto["participantIdentities"] if identity["participantId"] == participant["participantId"] ) # Esports matches do not have an accountId field if "accountId" in participant_identity: unique_identifier = { "riotLolApi": { "accountId": participant_identity["accountId"], "platformId": participant_identity["platformId"], } } else: unique_identifier = {} # Custom games don’t have identity info except KeyError: participant_identity = None unique_identifier = {} # TODO Make that backwards-compatible with pre-runes reforged games runes = [ game_dto.LolGamePlayerRune( id=participant["stats"].get(f"perk{i}"), slot=i, stats=[ participant["stats"].get(f"perk{i}Var{j}") for j in range(1, 4) ], ) for i in range(0, 6) ] # Adding stats perks runes.extend( [ game_dto.LolGamePlayerRune( id=participant["stats"].get(f"statPerk{i}"), slot=i + 6, ) for i in range(0, 3) ] ) items = [ game_dto.LolGamePlayerItem( id=participant["stats"].get(f"item{i}"), slot=i ) for i in range(0, 7) ] summoner_spells = [ game_dto.LolGamePlayerSummonerSpell( id=participant.get(f"spell{i}Id"), slot=i - 1 ) for i in range(1, 3) ] end_of_game_stats = game_dto.LolGamePlayerEndOfGameStats( items=items, firstBlood=participant["stats"].get("firstBloodKill"), firstBloodAssist=participant["stats"].get( "firstBloodAssist" ), # This field is wrong by default kills=participant["stats"].get("kills"), deaths=participant["stats"].get("deaths"), assists=participant["stats"].get("assists"), gold=participant["stats"].get("goldEarned"), # TODO Test with older games cs=int(participant["stats"].get("totalMinionsKilled") or 0) + int(participant["stats"].get("neutralMinionsKilled") or 0), level=participant["stats"].get("champLevel"), wardsPlaced=participant["stats"].get("wardsPlaced"), wardsKilled=participant["stats"].get("wardsKilled"), visionWardsBought=participant["stats"].get("visionWardsBoughtInGame"), visionScore=participant["stats"].get("visionScore"), killingSprees=participant["stats"].get("killingSprees"), largestKillingSpree=participant["stats"].get("largestKillingSpree"), doubleKills=participant["stats"].get("doubleKills"), tripleKills=participant["stats"].get("tripleKills"), quadraKills=participant["stats"].get("quadraKills"), pentaKills=participant["stats"].get("pentaKills"), monsterKills=participant["stats"].get("neutralMinionsKilled"), monsterKillsInAlliedJungle=participant["stats"].get( "neutralMinionsKilledTeamJungle" ), monsterKillsInEnemyJungle=participant["stats"].get( "neutralMinionsKilledEnemyJungle" ), totalDamageDealt=participant["stats"].get("totalDamageDealt"), physicalDamageDealt=participant["stats"].get("physicalDamageDealt"), magicDamageDealt=participant["stats"].get("magicDamageDealt"), totalDamageDealtToChampions=participant["stats"].get( "totalDamageDealtToChampions" ), physicalDamageDealtToChampions=participant["stats"].get( "physicalDamageDealtToChampions" ), magicDamageDealtToChampions=participant["stats"].get( "magicDamageDealtToChampions" ), damageDealtToObjectives=participant["stats"].get( "damageDealtToObjectives" ), damageDealtToTurrets=participant["stats"].get("damageDealtToTurrets"), totalDamageTaken=participant["stats"].get("totalDamageTaken"), physicalDamageTaken=participant["stats"].get("physicalDamageTaken"), magicDamageTaken=participant["stats"].get("magicalDamageTaken"), longestTimeSpentLiving=participant["stats"].get( "longestTimeSpentLiving" ), largestCriticalStrike=participant["stats"].get("largestCriticalStrike"), goldSpent=participant["stats"].get("goldSpent"), totalHeal=participant["stats"].get("totalHeal"), totalUnitsHealed=participant["stats"].get("totalUnitsHealed"), damageSelfMitigated=participant["stats"].get("damageSelfMitigated"), totalTimeCCDealt=participant["stats"].get("totalTimeCrowdControlDealt"), timeCCingOthers=participant["stats"].get("timeCCingOthers"), ) # The following fields have proved to be missing or buggy in multiple games if "firstTowerKill" in participant["stats"]: end_of_game_stats["firstTower"] = participant["stats"]["firstTowerKill"] end_of_game_stats["firstTowerAssist"] = participant["stats"].get( "firstTowerAssist" ) else: info_log.add(f"{log_prefix}Missing ['player']['firstTower']") if "firstInhibitorKill" in participant["stats"]: end_of_game_stats["firstInhibitor"] = participant["stats"][ "firstInhibitorKill" ] end_of_game_stats["firstInhibitorAssist"] = participant["stats"].get( "firstInhibitorAssist" ) else: info_log.add(f"{log_prefix}Missing ['player']['firstInhibitor']") player = game_dto.LolGamePlayer( id=participant["participantId"], championId=participant["championId"], uniqueIdentifiers=unique_identifier, primaryRuneTreeId=participant["stats"].get("perkPrimaryStyle"), secondaryRuneTreeId=participant["stats"].get("perkSubStyle"), runes=runes, summonerSpells=summoner_spells, endOfGameStats=end_of_game_stats, ) if participant_identity: player["inGameName"] = participant_identity["summonerName"] player["profileIconId"] = participant_identity["profileIcon"] # roleml compatibility if "role" in participant: # TODO Remove that after roleml refactor if participant["role"] not in {"TOP", "JGL", "MID", "BOT", "SUP"}: participant["role"] = { "top": "TOP", "jungle": "JGL", "mid": "MID", "bot": "BOT", "supp": "SUP", }[participant["role"]] player["role"] = participant["role"] # Then, we add convenience name fields for human readability if asked if add_names: player["championName"] = lit.get_name( player["championId"], object_type="champion" ) player["primaryRuneTreeName"] = lit.get_name( player["primaryRuneTreeId"], object_type="rune" ) player["secondaryRuneTreeName"] = lit.get_name( player["secondaryRuneTreeId"], object_type="rune" ) for item in player["endOfGameStats"]["items"]: item["name"] = lit.get_name(item["id"], object_type="item") for rune in player["runes"]: rune["name"] = lit.get_name(rune["id"], object_type="rune") for summoner_spell in player["summonerSpells"]: summoner_spell["name"] = lit.get_name( summoner_spell["id"], object_type="summoner_spell" ) team_dto["players"].append(player) # We want to make extra sure players are always ordered by Riot’s given id team_dto["players"] = sorted(team_dto["players"], key=lambda x: x["id"]) if add_names: team_dto["bansNames"] = [] for ban in team_dto["bans"]: team_dto["bansNames"].append(lit.get_name(ban, object_type="champion")) game["teams"][side] = team_dto return game
def test_summoner_spell(): assert lit.get_name(21, object_type="summoner_spell") == "Barrier"
def test_perks(): assert lit.get_name(5002) == "Armor"
def test_keystone(): assert lit.get_name(8000) == "Precision"
def test_jungle_item(): assert lit.get_name(1400) == "Stalker's Blade - Warrior"
def parse_player_battle_data( player: lol_dto.classes.game.LolGamePlayer, player_battle_data: dict, add_names: bool ) -> List[str]: missing_fields = [] try: # We start by verifying champion ID is coherent assert player["championId"] == int(player_battle_data["hero"]) except AssertionError: # In the games with buggy championId, almost all fields were empty, we raise raise ValueError end_of_game_stats = lol_dto.classes.game.LolGamePlayerEndOfGameStats( kills=int(player_battle_data["kill"]), deaths=int(player_battle_data["death"]), assists=int(player_battle_data["assist"]), gold=int(player_battle_data["gold"]), cs=int(player_battle_data["lasthit"]), level=int(player_battle_data["level"]), items=[], # We cast boolean statistics as proper booleans firstBlood=bool(player_battle_data["firstBlood"]), firstTower=bool(player_battle_data["firstTower"]), # Then we add other numerical statistics killingSprees=int(player_battle_data["killingSprees"]), doubleKills=int(player_battle_data["dKills"]), tripleKills=int(player_battle_data["tKills"]), quadraKills=int(player_battle_data["qKills"]), pentaKills=int(player_battle_data["pKills"]), towerKills=int(player_battle_data["towerKills"]), monsterKills=int(player_battle_data["neutralKilled"]), monsterKillsInAlliedJungle=int(player_battle_data["neutralKilledTJungle"]), monsterKillsInEnemyJungle=int(player_battle_data["neutralKilledEJungle"]), wardsPlaced=int(player_battle_data["wardsPlaced"]), wardsKilled=int(player_battle_data["wardsKilled"]), visionWardsBought=int(player_battle_data["visionWardsBought"]), # Damage totals, using a nomenclature close to match-v4 totalDamageDealt=int(player_battle_data["totalDamage"]), totalDamageDealtToChampions=int(player_battle_data["totalDamageToChamp"]), physicalDamageDealtToChampions=int(player_battle_data["pDamageToChamp"]), magicDamageDealtToChampions=int(player_battle_data["mDamageDealtToChamp"]), totalDamageTaken=int(player_battle_data["totalDamageTaken"]), physicalDamageTaken=int(player_battle_data["pDamageTaken"]), magicDamageTaken=int(player_battle_data["mDamageTaken"]), ) for item_key in player_battle_data["equip"]: item = lol_dto.classes.game.LolGamePlayerItem( id=int(player_battle_data["equip"][item_key]), slot=int(item_key[-1]), ) if add_names: item["name"] = lit.get_name(item["id"], object_type="item") end_of_game_stats["items"].append(item) # We validate the fields from sMatchMember assert end_of_game_stats["kills"] == player["endOfGameStats"]["kills"] assert end_of_game_stats["deaths"] == player["endOfGameStats"]["deaths"] assert end_of_game_stats["assists"] == player["endOfGameStats"]["assists"] assert end_of_game_stats["gold"] == player["endOfGameStats"]["gold"] # We check fields that are regularly missing for field_name in ["largestCriticalStrike", "largestKillingSpree", "inhibitorKills", "totalHeal"]: if field_name in player_battle_data: end_of_game_stats[field_name] = int(player_battle_data[field_name]) else: missing_fields.append(field_name) if "pDamageDealt" in player_battle_data: end_of_game_stats["physicalDamageDealt"] = int(player_battle_data["pDamageDealt"]) else: missing_fields.append("pDamageDealt") if "mDamageDealt" in player_battle_data: end_of_game_stats["magicDamageDealt"] = int(player_battle_data["mDamageDealt"]) else: missing_fields.append("mDamageDealt") # Finally, we write them to the player object player["endOfGameStats"] = end_of_game_stats player["summonerSpells"] = [] for skill_key in "skill-1", "skill-2": summoner_spell = lol_dto.classes.game.LolGamePlayerSummonerSpell( id=int(player_battle_data[skill_key]), slot=int(skill_key[-1]), ) if add_names: summoner_spell["name"] = lit.get_name(summoner_spell["id"], object_type="summoner_spell") player["summonerSpells"].append(summoner_spell) return missing_fields
def parse_qq_game(qq_game_id: int, patch: str = None, add_names: bool = True) -> lol_dto.classes.game.LolGame: """Parses a QQ game and returns a LolGameDto. Params: qq_game_̤d: the qq game id, acquired from qq’s match list endpoint. patch: optional patch to include in the object and query rune trees. add_names: whether or not to add champions/items/runes names next to their objects through lol_id_tools. Returns: A LolGameDto. """ game_info, team_info, runes_info, qq_server_id, qq_battle_id = get_all_qq_game_info(qq_game_id) log_prefix = ( f"match {game_info['sMatchInfo']['bMatchId']}|" f"game {game_info['sMatchInfo']['MatchNum']}|" f"id {qq_game_id}:\t" ) logging.debug(f"{log_prefix}Starting parsing") # We start by building the root of the game object lol_game_dto = lol_dto.classes.game.LolGame( sources={"qq": SourceQQ(id=int(qq_game_id), serverId=qq_server_id, battleId=qq_battle_id)}, gameInSeries=int(game_info["sMatchInfo"]["MatchNum"]), teams={}, ) warnings = set() info = set() try: # The 'BattleTime' field sometimes has sub-second digits that we cut date_time = dateparser.parse( f"{game_info['battleInfo']['BattleDate']}T{game_info['battleInfo']['BattleTime'][:8]}" ) date_time = date_time.replace(tzinfo=datetime.timezone(datetime.timedelta(hours=8))) lol_game_dto["start"] = date_time.isoformat(timespec="seconds") except KeyError: info.add(f"{log_prefix}Missing ['game']['start']") if patch: lol_game_dto["patch"] = patch # Get blue and red team IDs through the BlueTeam field in the first JSON blue_team_id = int(game_info["sMatchInfo"]["BlueTeam"]) red_team_id = int( game_info["sMatchInfo"]["TeamA"] if game_info["sMatchInfo"]["BlueTeam"] == game_info["sMatchInfo"]["TeamB"] else game_info["sMatchInfo"]["TeamB"] ) # TODO Write that in a more beautiful way # The MatchWin field refers to TeamA/TeamB, which are not blue/red if game_info["sMatchInfo"]["MatchWin"] == "1": if int(game_info["sMatchInfo"]["TeamA"]) == blue_team_id: lol_game_dto["winner"] = "BLUE" else: lol_game_dto["winner"] = "RED" elif game_info["sMatchInfo"]["MatchWin"] == "2": if int(game_info["sMatchInfo"]["TeamB"]) == blue_team_id: lol_game_dto["winner"] = "BLUE" else: lol_game_dto["winner"] = "RED" # Handle battle_data parsing and game-related information try: # This is a json inside the json of game_info battle_data = json.loads(game_info["battleInfo"]["BattleData"]) lol_game_dto["duration"] = int(battle_data["game-period"]) except JSONDecodeError: # Usually means the field was empty battle_data = {} # Match name usually look like TES vs EDG but some casing typos can be caught here possible_team_names = game_info["sMatchInfo"]["bMatchName"].lower().split("vs") # Used if team_info is not there possible_team_names = [n.replace(" ", "").upper() for n in possible_team_names] # We iterate of the two team IDs from sMatchInfo for team_id in blue_team_id, red_team_id: team_color = "BLUE" if team_id == blue_team_id else "RED" team = lol_dto.classes.game.LolGameTeam(uniqueIdentifiers={"qq": {"id": team_id}}, players=[]) # We start by getting as much information as possible from the sMatchMember fields for match_member in game_info["sMatchMember"]: # We match players on the team id if int(match_member["TeamId"]) != team_id: continue player = lol_dto.classes.game.LolGamePlayer( inGameName=match_member["GameName"], role=roles[match_member["Place"]], championId=int(match_member["ChampionId"]), endOfGameStats=lol_dto.classes.game.LolGamePlayerEndOfGameStats( kills=int(match_member["Game_K"]), deaths=int(match_member["Game_D"]), assists=int(match_member["Game_A"]), gold=int(match_member["Game_M"]), ), uniqueIdentifiers={ "qq": {"accountId": int(match_member["AccountId"]), "memberId": int(match_member["MemberId"])} }, ) if add_names: player["championName"] = lit.get_name(player["championId"], object_type="champion") team["players"].append(player) # We get the tentative team name # We cast team names and player names as lowercase because they made the mistake in some old games tentative_team_name = next(t for t in possible_team_names if t.lower() in match_member["GameName"].lower()) # If we have info from the second endpoint we use it to validate team id and game winner if team_info: try: assert team_id == team_info[f"{team_color.lower()}_clan_id_"] team["name"] = team_info[f"{team_color.lower()}_clan_name_"] if team_id == team_info["win_clan_id_"]: assert lol_game_dto["winner"] == team_color except AssertionError: warnings.add(f"{log_prefix}⚠ Inconsistent team sides between endpoints ⚠") info.add(f"{log_prefix}Team sides might be wrong") # We use the tentative team name from the first object instead team["name"] = tentative_team_name try: # In the games with this issue, the second endpoint was the one with the right side information team_color = next( color for color in ("blue", "red") if team_info[f"{color}_clan_id_"] == team_id ).upper() except StopIteration: # When we cannot find a team with the same ID as the one in the first object, we drop the process continue if team_info["win_clan_id_"] == team_id: lol_game_dto["winner"] = team_color # If it is missing, we log what we are missing and try to guess team names from players else: info.add(f"{log_prefix}Missing ['team']['name'], guessing them from game name") info.add(f"{log_prefix}Missing ['player']['runes']") team["name"] = tentative_team_name # Without "battle data", we stop there # We also check both teams have the "players" field as we found games without it, and battleData was faulty if not battle_data or any(len(battle_data[team_side]["players"]) != 5 for team_side in ("left", "right")): warnings.add(f"{log_prefix}⚠ Missing 'BattleData', meaning almost all end of game stats ⚠") lol_game_dto["teams"][team_color] = team continue # Finding left/right team side through player names # TODO Make that a bit more palatable for tentative_team_side in "left", "right": # We just look at the first player player = battle_data[tentative_team_side]["players"][0] for match_member in game_info["sMatchMember"]: if player["name"] == match_member["GameName"] and int(match_member["TeamId"]) == team_id: team_side = tentative_team_side # Sometimes the firstTower field isn’t in battleData but it can be calculated from the players if "firstTower" in battle_data[team_side]: first_tower = bool(battle_data[team_side]["firstTower"]) else: first_tower = True in (bool(player["firstTower"]) for player in battle_data[team_side]["players"]) # We fill more team related information team["bans"] = [ int(battle_data[team_side][f"ban-hero-{ban_number}"]) for ban_number in range(1, 6) if f"ban-hero-{ban_number}" in battle_data[team_side] ] team["endOfGameStats"] = lol_dto.classes.game.LolGameTeamEndOfGameStats( towerKills=int(battle_data[team_side]["tower"]), baronKills=int(battle_data[team_side]["b-dragon"]), dragonKills=int(battle_data[team_side]["s-dragon"]), firstTower=first_tower, ) # We add ban names from the bans field if add_names: team["bansNames"] = [lit.get_name(i, object_type="champion") for i in team["bans"]] # Bans are sometimes incomplete if team["bans"].__len__() < 5: info.add(f"{log_prefix}Missing ['team']['bans']") # Finally, we look at per-player BattleData for player_battle_data in battle_data[team_side]["players"]: player = next(p for p in team["players"] if player_battle_data["name"] == p["inGameName"]) # Updating missing fields for logging try: info.update( [ f"{log_prefix}Missing ['player'][{field}]" for field in parse_player_battle_data(player, player_battle_data, add_names) ] ) except ValueError: warnings.add(f"{log_prefix}⚠ Missing most end of game stats ⚠") warnings.add(f"{log_prefix}⚠ Missing runes ⚠") warnings.add(f"{log_prefix}⚠ Bans are likely wrong ⚠") continue try: add_runes(player, runes_info, patch, add_names) except StopIteration: info.add(f"{log_prefix}Missing ['player']['runes']") # Finally, we insert the team lol_game_dto["teams"][team_color] = team for warning in warnings: logging.warning(warning) for log in info: logging.info(log) return lol_game_dto
def match_timeline_to_game( match_timeline_dto: dict, game_id: int, platform_id: str, add_names: bool = False, ) -> game_dto.LolGame: """Returns a LolGame from a MatchTimelineDto. Args: match_timeline_dto: A MatchTimelineDto from Riot’s API. game_id: The gameId of the game, required as it is not present in the MatchTimelineDto. platform_id: The platformId of the game, required as it is not present in the MatchTimelineDto. add_names: whether or not to add names for human readability in the DTO. False by default. Returns: The LolGame representation of the game. """ riot_source = { "riotLolApi": RiotGameIdentifier(gameId=game_id, platformId=platform_id) } # Creating the game_dto skeleton game = game_dto.LolGame( sources=riot_source, teams={ "BLUE": game_dto.LolGameTeam( players=[ game_dto.LolGamePlayer(id=i, snapshots=[], itemsEvents=[], wardsEvents=[], skillsLevelUpEvents=[]) for i in range(1, 6) ], monstersKills=[], buildingsKills=[], ), "RED": game_dto.LolGameTeam( players=[ game_dto.LolGamePlayer(id=i, snapshots=[], itemsEvents=[], wardsEvents=[], skillsLevelUpEvents=[]) for i in range(6, 11) ], monstersKills=[], buildingsKills=[], ), }, kills=[], ) for frame in match_timeline_dto["frames"]: # We start by adding player information at the given snapshot timestamps for participant_frame in frame["participantFrames"].values(): team_side = "BLUE" if participant_frame[ "participantId"] < 6 else "RED" # Finding the player with the same id in our game object player = next(p for p in game["teams"][team_side]["players"] if p["id"] == participant_frame["participantId"]) try: position = game_dto.Position( x=participant_frame["position"]["x"], y=participant_frame["position"]["y"]) except KeyError: position = None snapshot = game_dto.LolGamePlayerSnapshot( timestamp=frame["timestamp"] / 1000, currentGold=participant_frame["currentGold"], totalGold=participant_frame["totalGold"], xp=participant_frame["xp"], level=participant_frame["level"], cs=participant_frame["minionsKilled"] + participant_frame["jungleMinionsKilled"], monstersKilled=participant_frame["jungleMinionsKilled"], position=position, # Next fields gotten with .get() so they are None if they haven’t been created by roleml totalGoldDiff=participant_frame.get("totalGoldDiff"), xpDiff=participant_frame.get("xpDiff"), csDiff=participant_frame.get("minionsKilledDiff"), monstersKilledDiff=participant_frame.get( "jungleMinionsKilledDiff"), ) player["snapshots"].append(snapshot) for event in frame["events"]: timestamp = event["timestamp"] / 1000 # Epic monsters kills if event["type"] == "ELITE_MONSTER_KILL": if event["killerId"] < 1: # This is Rift Herald killing itself, we just pass riot_transmute_logger.debug( f"Epic monster kill with killer id 0 found, likely Rift Herald killing itself." ) continue team = game["teams"][ "BLUE" if event["killerId"] < 6 else "RED"] monster_type = monster_type_dict[event["monsterType"]] event_dto = game_dto.LolGameTeamMonsterKill( timestamp=timestamp, type=monster_type, killerId=event["killerId"]) if monster_type == "DRAGON": try: event_dto["subType"] = monster_subtype_dict[ event["monsterSubType"]] # If we don’t know how to translate the monster subtype, we simply leave it as-is except KeyError: event_dto["subType"] = event["monsterSubType"] team["monstersKills"].append(event_dto) # Buildings kills elif event["type"] == "BUILDING_KILL": # The teamId here refers to the SIDE of the tower that was killed, so the opponents killed it team = game["teams"]["RED" if event["teamId"] == 100 else "BLUE"] # Get the prebuilt "building" event DTO event_dto = building_dict[event["position"]["x"], event["position"]["y"]] # Fill its timestamp event_dto["timestamp"] = timestamp if event.get("killerId"): event_dto["killerId"] = event.get("killerId") team["buildingsKills"].append(event_dto) # Champions kills elif event["type"] == "CHAMPION_KILL": position = game_dto.Position(x=event["position"]["x"], y=event["position"]["y"]) game["kills"].append( game_dto.LolGameKill( timestamp=timestamp, position=position, killerId=event["killerId"], victimId=event["victimId"], assistsIds=event["assistingParticipantIds"], )) # Skill points use elif event["type"] == "SKILL_LEVEL_UP": player = get_player(game, event["participantId"]) player["skillsLevelUpEvents"].append( game_dto.LolGamePlayerSkillLevelUpEvent( timestamp=timestamp, slot=event["skillSlot"], type=event["levelUpType"])) # Item buying, selling, and undoing elif "ITEM" in event["type"]: if not event.get("participantId"): riot_transmute_logger.debug( f"Dropping item event because it does not have a participantId:\n{event}" ) # Some weird ITEM_DESTROYED events without a participantId can appear in older games (tower items) continue player = get_player(game, event["participantId"]) event_type = event["type"].lstrip("ITEM_") if event_type == "UNDO": item_event = game_dto.LolGamePlayerItemEvent( timestamp=timestamp, type=event_type, id=event["afterId"], undoId=event["beforeId"]) else: item_event = game_dto.LolGamePlayerItemEvent( timestamp=timestamp, type=event_type, id=event["itemId"]) if add_names: item_event["name"] = lit.get_name(item_event["id"], object_type="item") player["itemsEvents"].append(item_event) # Wards placing and killing elif "WARD" in event["type"]: if event["type"] == "WARD_KILL": if not event.get("killerId"): riot_transmute_logger.debug( f"Ward kill event without killerId dropped:\n{event}" ) continue player = get_player(game, event["killerId"]) event_type = "KILLED" else: try: player = get_player(game, event["creatorId"]) except StopIteration: # TODO Understand events with ward_type=UNDEFINED + creatorId=0, they are dropped atm continue event_type = "PLACED" player["wardsEvents"].append( game_dto.LolGamePlayerWardEvent( timestamp=timestamp, type=event_type, wardType=event["wardType"])) return game