diff --git a/README.md b/README.md index 24f72b2..828e301 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ 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] Monitoring for new results in real-time & checking for missing/unuploaded results - - [x] Flag to black out other players' names from 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/) - [x] Support for all available game languages - [x] Modular design to support [IkaLog3](https://github.com/hasegaw/IkaLog3) and other tools @@ -23,7 +24,7 @@ Looking to track your _Splatoon 2_ gameplay? See **[splatnet2statink](https://gi ## Usage 🐙 ``` -$ python s3s.py [-M [N]] [-r] [-nsr | -osr] [--blackout] +$ python s3s.py [-M [N]] [-r] [-nsr | -osr] [--blackout] [--getseed] ``` The `-M` flag runs the script in monitoring mode, uploading new battles/jobs as you play, checking for new results every `N` seconds; if no `N` is provided, it defaults to 300 (5 minutes). @@ -34,7 +35,9 @@ The `-nsr` flag makes Salmon Run jobs **not** be monitored/uploaded. Use this if The `-osr` flag, conversely, makes **only** Salmon Run jobs be monitored/uploaded. Use this if you're playing at Grizzco only. -The `--blackout` flag blacks out other players' names in uploaded scoreboard data. +The `--blackout` flag removes other players' names from uploaded scoreboard data. + +The `--getseed` flag exports a file which can be uploaded to Lean's [gear & Shell-Out Machine seed checker](https://leanny.github.io/splat3seedchecker/). Arguments for advanced usage (e.g. locally exporting data to JSON files) can be viewed using `--help`. diff --git a/requirements.txt b/requirements.txt index 1c07cbb..28afbaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ beautifulsoup4 +mmh3 msgpack_python packaging requests diff --git a/s3s.py b/s3s.py index 6d8063a..fab4dd7 100755 --- a/s3s.py +++ b/s3s.py @@ -5,11 +5,11 @@ # License: GPLv3 import argparse, base64, datetime, json, os, shutil, re, requests, sys, time, uuid -import msgpack +import mmh3, msgpack from packaging import version import iksm, utils -A_VERSION = "0.1.16" +A_VERSION = "0.1.17" DEBUG = False @@ -1225,6 +1225,67 @@ class SquidProgress: sys.stdout.flush() +def export_seed_json(skipprefetch=False): + '''Export a JSON file for use with Lean's seed checker at https://leanny.github.io/splat3seedchecker/.''' + + if not skipprefetch: + prefetch_checks(printout=True) + + sha = utils.translate_rid["MyOutfitCommonDataEquipmentsQuery"] + outfit_post = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), + headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) + + sha = utils.translate_rid["LatestBattleHistoriesQuery"] + history_post = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), + headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) + + if outfit_post.status_code != 200 or history_post.status_code != 200: + print("Could not reach SplatNet 3. Exiting.") + sys.exit(1) + try: + outfit = json.loads(outfit_post.text) + history = json.loads(history_post.text) + except: + print("Ill-formatted JSON file received. Exiting.") + sys.exit(1) + + try: + pid = history["data"]["latestBattleHistories"]["historyGroupsOnlyFirst"]["nodes"][0]["historyDetails"]["nodes"][0]["player"]["id"] + # VsPlayer-u-<20 char long player id>:RECENT:T_:u- + s = utils.b64d(pid) + r = s.split(":")[-1] + except KeyError: # no recent battles (mr. grizz is pleased) + try: + sha = utils.translate_rid["CoopHistoryQuery"] + history_post = requests.post(utils.GRAPHQL_URL, data=utils.gen_graphql_body(sha), + headers=headbutt(), cookies=dict(_gtoken=GTOKEN)) + + if history_post.status_code != 200: + print("Could not reach SplatNet 3. Exiting.") + sys.exit(1) + try: + history = json.loads(history_post.text) + except: + print("Ill-formatted JSON file received. Exiting.") + sys.exit(1) + + pid = history["data"]["coopResult"]["historyGroupsOnlyFirst"]["nodes"][0]["historyDetails"]["nodes"][0]["id"] + # CoopHistoryDetail-u-<20 char long player id>:T_ + s = utils.b64d(pid) + r = s.split(":")[0].replace("CoopHistoryDetail-", "") + except KeyError: + r = "" + + h = mmh3.hash(r)&0xFFFFFFFF # make positive + key = base64.b64encode(bytes([k^(h&0xFF) for k in bytes(r, "utf-8")])) + t = int(time.time()) + + with open(os.path.join(os.getcwd(), f"gear_{t}.json"), "x") as fout: + json.dump({"key": key.decode("utf-8"), "h": h, "timestamp": t, "gear": outfit}, fout) + + print(f"gear_{t}.json has been exported.") + + def parse_arguments(): '''Setup for command-line options.''' @@ -1235,17 +1296,19 @@ def parse_arguments(): parser.add_argument("-r", required=False, action="store_true", help="retroactively post unuploaded battles/jobs") srgroup.add_argument("-nsr", required=False, action="store_true", - help="do not check for Salmon Run jobs") + help="do not check for Salmon Run jobs") srgroup.add_argument("-osr", required=False, action="store_true", - help="only check for Salmon Run jobs") + help="only check for Salmon Run jobs") parser.add_argument("--blackout", required=False, action="store_true", - help="black out names in uploaded scoreboard data") + help="remove player names from uploaded scoreboard data") parser.add_argument("-o", required=False, action="store_true", help="export all possible results to local files") parser.add_argument("-i", dest="file", nargs=2, required=False, help="upload local results; use `-i results.json overview.json`") parser.add_argument("-t", required=False, action="store_true", help="dry run for testing (won't post to stat.ink)") + parser.add_argument("--getseed", required=False, action="store_true", + help="export JSON for gear & Shell-Out Machine seed checker") parser.add_argument("--skipprefetch", required=False, action="store_true", help=argparse.SUPPRESS) return parser.parse_args() @@ -1271,6 +1334,7 @@ def main(): only_ink = parser_result.nsr # ink battles ONLY only_salmon = parser_result.osr # salmon run ONLY blackout = parser_result.blackout + getseed = parser_result.getseed # testing/dev stuff test_run = parser_result.t # send to stat.ink as dry run @@ -1280,7 +1344,15 @@ def main(): # i/o checks ############ - if only_ink and only_salmon: + if getseed and len(sys.argv) > 2 and "--skipprefetch" not in sys.argv: + print("Cannot use --getseed with other arguments. Exiting.") + sys.exit(0) + + elif getseed: + export_seed_json(skipprefetch) + sys.exit(0) + + elif only_ink and only_salmon: print("That doesn't make any sense! :) Exiting.") sys.exit(0) diff --git a/utils.py b/utils.py index ea3eaae..538c208 100644 --- a/utils.py +++ b/utils.py @@ -20,14 +20,15 @@ SUPPORTED_KEYS = [ # SHA256 hash database for SplatNet 3 GraphQL queries # full list: https://github.com/samuelthomas2774/nxapi/discussions/11#discussioncomment-3614603 translate_rid = { - 'HomeQuery': 'dba47124d5ec3090c97ba17db5d2f4b3', # blank vars - 'LatestBattleHistoriesQuery': '7d8b560e31617e981cf7c8aa1ca13a00', # INK / blank vars - query1 - 'RegularBattleHistoriesQuery': 'f6e7e0277e03ff14edfef3b41f70cd33', # INK / blank vars - query1 - 'BankaraBattleHistoriesQuery': 'c1553ac75de0a3ea497cdbafaa93e95b', # INK / blank vars - query1 - 'PrivateBattleHistoriesQuery': '38e0529de8bc77189504d26c7a14e0b8', # INK / blank vars - query1 - 'VsHistoryDetailQuery': '2b085984f729cd51938fc069ceef784a', # INK / req "vsResultId" - query2 - 'CoopHistoryQuery': '817618ce39bcf5570f52a97d73301b30', # SR / blank vars - query1 - 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e' # SR / req "coopHistoryDetailId" - query2 + 'HomeQuery': 'dba47124d5ec3090c97ba17db5d2f4b3', # blank vars + 'LatestBattleHistoriesQuery': '7d8b560e31617e981cf7c8aa1ca13a00', # INK / blank vars - query1 + 'RegularBattleHistoriesQuery': 'f6e7e0277e03ff14edfef3b41f70cd33', # INK / blank vars - query1 + 'BankaraBattleHistoriesQuery': 'c1553ac75de0a3ea497cdbafaa93e95b', # INK / blank vars - query1 + 'PrivateBattleHistoriesQuery': '38e0529de8bc77189504d26c7a14e0b8', # INK / blank vars - query1 + 'VsHistoryDetailQuery': '2b085984f729cd51938fc069ceef784a', # INK / req "vsResultId" - query2 + 'CoopHistoryQuery': '817618ce39bcf5570f52a97d73301b30', # SR / blank vars - query1 + 'CoopHistoryDetailQuery': 'f3799a033f0a7ad4b1b396f9a3bafb1e', # SR / req "coopHistoryDetailId" - query2 + 'MyOutfitCommonDataEquipmentsQuery': 'd29cd0c2b5e6bac90dd5b817914832f8' # for Lean's seed checker } @@ -94,7 +95,7 @@ def b64d(string): if len(thing_id) == 5 and thing_id[:1] == "2" and thing_id[-3:] == "900": # grizzco weapon ID from a hacker return "" - if thing_id[:15] == "VsHistoryDetail" or thing_id[:17] == "CoopHistoryDetail": + if thing_id[:15] == "VsHistoryDetail" or thing_id[:17] == "CoopHistoryDetail" or thing_id[:8] == "VsPlayer": return thing_id # string else: return int(thing_id) # integer