From 65ae3619fb59dca4e4942daf2d0ca46bbb5917bd Mon Sep 17 00:00:00 2001 From: eli fessler Date: Sat, 17 Dec 2022 23:20:57 -1000 Subject: [PATCH] v0.3.0 release & tricolor TW support! (fetus-hina/stat.ink#1107) --- README.md | 3 +- s3s.py | 143 +++++++++++++++++++++++++++++++++++------------------- utils.py | 19 ++++++++ 3 files changed, 112 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d0f3bd4..fb3f0fb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Looking to track your _Splatoon 2_ gameplay? See **[splatnet2statink](https://gi - [x] Full automation of SplatNet token generation via user log-in - [x] Ability to parse & upload complete battle/job stats to stat.ink ([example profile](https://stat.ink/@frozenpandaman/spl3)) - [x] Support for Salmon Run Next Wave (including Big Run) + - [x] Support for Splatfest & Tricolor Turf War battles - [x] Monitoring for new results in real-time & checking for missing/unuploaded results - [x] Flag to remove other players' names from results - [x] File exporting function for use with Lean's [gear & Shell-Out Machine seed checker](https://leanny.github.io/splat3seedchecker/) @@ -18,7 +19,6 @@ Looking to track your _Splatoon 2_ gameplay? See **[splatnet2statink](https://gi - [x] Modular design to support [IkaLog3](https://github.com/hasegaw/IkaLog3) and other tools ### What's coming? - - [ ] Support for Tricolor Turf War battles (as soon as stat.ink supports it!) - [ ] Downloadable, pre-packaged program executables --- @@ -80,7 +80,6 @@ You can even enter QR codes on the web version of SplatNet 3 via the list of ava *Splatoon 3* stage rotation information and current SplatNet gear are viewable at [splatoon3.ink](https://splatoon3.ink/). - ## Token generation 🪙 For s3s to work, [tokens](https://en.wikipedia.org/wiki/Access_token) known as `gtoken` and `bulletToken` are required to access SplatNet. These tokens may be obtained automatically, using the script, or manually via the official Nintendo Switch Online app. Please read the following sections carefully to decide whether or not you want to use automatic token generation. diff --git a/s3s.py b/s3s.py index 8cfa23c..e900a3c 100755 --- a/s3s.py +++ b/s3s.py @@ -11,7 +11,7 @@ import msgpack from packaging import version import iksm, utils -A_VERSION = "0.2.7" +A_VERSION = "0.3.0" DEBUG = False @@ -410,12 +410,13 @@ def populate_gear_abilities(player): return h_main, h_subs, c_main, c_subs, s_main, s_subs -def set_scoreboard(battle): - '''Returns two lists of player dictionaries, for our_team_players and their_team_players.''' +def set_scoreboard(battle, tricolor=False): + '''Returns lists of player dictionaries: our_team_players, their_team_players, and optionally third_team_players.''' # https://github.com/fetus-hina/stat.ink/wiki/Spl3-API:-Battle-%EF%BC%8D-Post#player-structure - our_team_players, their_team_players = [], [] + our_team_players, their_team_players, third_team_players = [], [], [] + # not supported yet: species, festDragonCert for i, player in enumerate(battle["myTeam"]["players"]): p_dict = {} p_dict["me"] = "yes" if player["isMyself"] else "no" @@ -434,9 +435,9 @@ def set_scoreboard(battle): p_dict["kill"] = p_dict["kill_or_assist"] - p_dict["assist"] p_dict["death"] = player["result"]["death"] p_dict["special"] = player["result"]["special"] - # noroshiTry + p_dict["signal"] = player["result"]["noroshiTry"] p_dict["disconnected"] = "no" - p_dict["crown"] = "yes" if player["crown"] == True else "no" + p_dict["crown"] = "yes" if player.get("crown") == True else "no" # https://github.com/fetus-hina/stat.ink/wiki/Spl3-API:-Battle-%EF%BC%8D-Post#gears-structure gear_struct = {"headgear": {}, "clothing": {}, "shoes": {}} @@ -449,39 +450,47 @@ def set_scoreboard(battle): p_dict["disconnected"] = "yes" our_team_players.append(p_dict) - for i, player in enumerate(battle["otherTeams"][0]["players"]): # no support for tricolor TW yet - p_dict = {} - p_dict["me"] = "no" - p_dict["name"] = player["name"] - try: - p_dict["number"] = str(player["nameId"]) - except: - pass - p_dict["splashtag_title"] = player["byname"] - p_dict["weapon"] = utils.b64d(player["weapon"]["id"]) - p_dict["inked"] = player["paint"] - p_dict["rank_in_team"] = i+1 - if "result" in player and player["result"] is not None: - p_dict["kill_or_assist"] = player["result"]["kill"] - p_dict["assist"] = player["result"]["assist"] - p_dict["kill"] = p_dict["kill_or_assist"] - p_dict["assist"] - p_dict["death"] = player["result"]["death"] - p_dict["special"] = player["result"]["special"] - # noroshiTry - p_dict["disconnected"] = "no" - p_dict["crown"] = "yes" if player["crown"] == True else "no" + team_nums = [0, 1] if tricolor else [0] + for team_num in team_nums: + for i, player in enumerate(battle["otherTeams"][team_num]["players"]): + p_dict = {} + p_dict["me"] = "no" + p_dict["name"] = player["name"] + try: + p_dict["number"] = str(player["nameId"]) + except: + pass + p_dict["splashtag_title"] = player["byname"] + p_dict["weapon"] = utils.b64d(player["weapon"]["id"]) + p_dict["inked"] = player["paint"] + p_dict["rank_in_team"] = i+1 + if "result" in player and player["result"] is not None: + p_dict["kill_or_assist"] = player["result"]["kill"] + p_dict["assist"] = player["result"]["assist"] + p_dict["kill"] = p_dict["kill_or_assist"] - p_dict["assist"] + p_dict["death"] = player["result"]["death"] + p_dict["special"] = player["result"]["special"] + p_dict["signal"] = player["result"]["noroshiTry"] + p_dict["disconnected"] = "no" + p_dict["crown"] = "yes" if player.get("crown") == True else "no" - gear_struct = {"headgear": {}, "clothing": {}, "shoes": {}} - h_main, h_subs, c_main, c_subs, s_main, s_subs = populate_gear_abilities(player) - gear_struct["headgear"] = {"primary_ability": h_main, "secondary_abilities": h_subs} - gear_struct["clothing"] = {"primary_ability": c_main, "secondary_abilities": c_subs} - gear_struct["shoes"] = {"primary_ability": s_main, "secondary_abilities": s_subs} - p_dict["gears"] = gear_struct - else: - p_dict["disconnected"] = "yes" - their_team_players.append(p_dict) + gear_struct = {"headgear": {}, "clothing": {}, "shoes": {}} + h_main, h_subs, c_main, c_subs, s_main, s_subs = populate_gear_abilities(player) + gear_struct["headgear"] = {"primary_ability": h_main, "secondary_abilities": h_subs} + gear_struct["clothing"] = {"primary_ability": c_main, "secondary_abilities": c_subs} + gear_struct["shoes"] = {"primary_ability": s_main, "secondary_abilities": s_subs} + p_dict["gears"] = gear_struct + else: + p_dict["disconnected"] = "yes" + if team_num == 0: + their_team_players.append(p_dict) + elif team_num == 1: + third_team_players.append(p_dict) - return our_team_players, their_team_players + if tricolor: + return our_team_players, their_team_players, third_team_players + else: + return our_team_players, their_team_players def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): @@ -509,7 +518,7 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): elif mode == "PRIVATE": payload["lobby"] = "private" elif mode == "FEST": - if utils.b64d(battle["vsMode"]["id"]) == 6: + if utils.b64d(battle["vsMode"]["id"]) in (6, 8): # open or tricolor payload["lobby"] = "splatfest_open" elif utils.b64d(battle["vsMode"]["id"]) == 7: payload["lobby"] = "splatfest_challenge" # pro @@ -532,8 +541,7 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): elif rule == "CLAM": payload["rule"] = "asari" elif rule == "TRI_COLOR": - print("Tricolor Turf War Splatfest battles are not yet supported - skipping.") - return {} + payload["rule"] = "tricolor" ## STAGE ## ########### @@ -554,7 +562,7 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): payload["kill"] = payload["kill_or_assist"] - payload["assist"] payload["death"] = player["result"]["death"] payload["special"] = player["result"]["special"] - # ... = player["result"]["noroshiTry"] = ultra signal attempts - splatfest tricolor tw + payload["signal"] = player["result"]["noroshiTry"] # ultra signal attempts in tricolor TW break ## RESULT ## @@ -574,9 +582,16 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): payload["start_at"] = utils.epoch_time(battle["playedTime"]) payload["end_at"] = payload["start_at"] + battle["duration"] - ## SCOREBOARD ## - ################ - payload["our_team_players"], payload["their_team_players"] = set_scoreboard(battle) + ## SCOREBOARD & COLOR ## + ######################## + payload["our_team_color"] = utils.convert_color(battle["myTeam"]["color"]) + payload["their_team_color"] = utils.convert_color(battle["otherTeams"][0]["color"]) + + if rule != "TRI_COLOR": + payload["our_team_players"], payload["their_team_players"] = set_scoreboard(battle) + else: + payload["our_team_players"], payload["their_team_players"], payload["third_team_players"] = set_scoreboard(battle, tricolor=True) + payload["third_team_color"] = utils.convert_color(battle["otherTeams"][1]["color"]) ## SPLATFEST ## ############### @@ -591,12 +606,9 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): payload["clout_change"] = battle["festMatch"]["contribution"] payload["fest_power"] = battle["festMatch"]["myFestPower"] # pro only - # if rule == "TRI_COLOR": - # ... - # no support for splatfest fest_title - - # turf war only - not tricolor + ## TURF WAR ## + ############## if mode in ("REGULAR", "FEST"): try: payload["our_team_percent"] = float(battle["myTeam"]["result"]["paintRatio"]) * 100 @@ -612,7 +624,32 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): payload["our_team_inked"] = our_team_inked payload["their_team_inked"] = their_team_inked - if mode == "PRIVATE": # these don't get sent otherwise + if mode == "FEST": + payload["our_team_theme"] = battle["myTeam"]["festTeamName"] + payload["their_team_theme"] = battle["otherTeams"][0]["festTeamName"] + + ## TRICOLOR TW ## + ################# + if mode == "FEST" and rule == "TRI_COLOR": + try: + payload["third_team_percent"] = float(battle["otherTeams"][1]["result"]["paintRatio"]) * 100 + except TypeError: + pass + + third_team_inked = 0 + for player in battle["otherTeams"][1]["players"]: + third_team_inked += player["paint"] + payload["third_team_inked"] = third_team_inked + + payload["third_team_theme"] = battle["otherTeams"][1]["festTeamName"] + + payload["our_team_role"] = utils.convert_tricolor_role(battle["myTeam"]["tricolorRole"]) + payload["their_team_role"] = utils.convert_tricolor_role(battle["otherTeams"][0]["tricolorRole"]) + payload["third_team_role"] = utils.convert_tricolor_role(battle["otherTeams"][1]["tricolorRole"]) + + ## PRIVATE BATTLES ## + ##################### + if mode == "PRIVATE": # these don't get sent otherwise. no support for private tricolor battles atm # could be a ranked mode try: payload["knockout"] = "no" if battle["knockout"] is None or battle["knockout"] == "NEITHER" else "yes" @@ -630,6 +667,8 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): except: pass + ## ANARCHY BATTLES ## + ##################### if mode == "BANKARA": try: payload["our_team_count"] = battle["myTeam"]["result"]["score"] @@ -719,6 +758,8 @@ def prepare_battle_result(battle, ismonitoring, isblackout, overview_data=None): print(f'* rank_exp_change: 0') break # found the child ID, no need to continue + ## X BATTLES ## + ############### if mode == "X_MATCH": try: payload["our_team_count"] = battle["myTeam"]["result"]["score"] @@ -1180,7 +1221,7 @@ def post_result(data, ismonitoring, isblackout, istestrun, overview_data=None): print(f"{utils.set_noun(which)[:-1].capitalize()} ID: {result_id}") print("Message from server:") print(postbattle.content.decode('utf-8')) - elif time_uploaded <= time_now - 5: # give some leeway + elif time_uploaded <= time_now - 7: # give some leeway print(f"{utils.set_noun(which)[:-1].capitalize()} already uploaded - {headerloc}") else: # 200 OK print(f"{utils.set_noun(which)[:-1].capitalize()} uploaded to {headerloc}") diff --git a/utils.py b/utils.py index 9361574..3736607 100644 --- a/utils.py +++ b/utils.py @@ -83,6 +83,25 @@ def set_noun(which): return "battles" +def convert_color(rgbadict): + '''Given a dict of numbers from 0.0 - 1.0, converts these into a RGBA hex color format (without the leading #).''' + + r = int(255 * rgbadict["r"]) + g = int(255 * rgbadict["g"]) + b = int(255 * rgbadict["b"]) + a = int(255 * rgbadict["a"]) + return f"{r:02x}{g:02x}{b:02x}{a:02x}" + + +def convert_tricolor_role(string): + '''Given a SplatNet 3 Tricolor Turf War team role, convert it to the stat.ink string format.''' + + if string == "DEFENSE": + return "defender" + else: # ATTACK1 or ATTACK2 + return "attacker" + + def b64d(string): '''Base64-decodes a string and cuts off the SplatNet prefix.'''