from aiohttp import ClientSession, ClientError, ServerDisconnectedError
import traceback
import asyncio
import pickle
import time
import os
try:
from configuration import config
except ModuleNotFoundError:
from src.configuration import config
[docs]
async def main():
print("Client running. Will periodically check for the savefile and send it over!\n")
has_save = True # whether the server has a save file - we lie at first in case we just restarted and it has an old one
last = 0
last_slots = 0
lasp = [0, 0, 0]
last_sd = 0
last_mt = 0
last_mt2 = 0
runs_last = {}
use_sd = False
use_mt = False
user = None
try:
with open("last_run") as f:
last_run = f.read().strip()
except OSError:
last_run = ""
possible = None
playing = None
timeout = 1
if not config.server.url or not config.server.secret:
print("Config is not complete")
time.sleep(3)
return
try:
user = os.environ["USERPROFILE"]
use_sd = config.slice.enabled
use_mt = config.mt.enabled
except (AttributeError, KeyError):
use_sd = use_mt = False
print(f"User profile folder: {user}\nFetch Slice & Dice Data: {'YES' if use_sd else 'NO'}\nFetch Monster Train Data: {'YES' if use_mt else 'NO'}")
if use_mt:
mt_folder = os.path.join(user, "AppData", "LocalLow", "Shiny Shoe", "MonsterTrain")
mt_file = os.path.join(mt_folder, "saves", "save-singlePlayer.json")
print(f"\nFolder-1: {mt_folder}\nSavefile-1: {mt_file}")
mt2_folder = os.path.join(user, "AppData", "LocalLow", "Shiny Shoe", "MonsterTrain2")
mt2_file = os.path.join(mt2_folder, "saves", "save-singlePlayer.json")
print(f"\nFolder-2: {mt2_folder}\nSavefile-2: {mt2_file}")
async with ClientSession(config.server.url) as session:
# Check if the app is registered, prompt it if not
try:
async with session.post("/twitch/check-token", params={"key": config.server.secret}) as resp:
if resp.ok:
text = await resp.text()
match text:
case "DISABLED":
print("\nTwitch connectivity is disabled.")
case "NOT_EXTENDED":
print("\nExtended OAuth functionality is not enabled. Some features will be unavailable.")
case "NO_CREDENTIALS":
print("\nExtended OAuth is not properly set-up. Contact the server owner.")
case "WORKING":
print("\nExtended OAuth validated.")
case a:
if a.startswith("NEEDS_CONNECTION"):
nc, cl, url = a.partition(":")
import webbrowser
webbrowser.open_new_tab(url)
else:
print(f"\nERROR: Unrecognized return value:\n\n{a}")
except (ClientError, ServerDisconnectedError):
print("\nServer is offline, cannot confirm OAuth mode.\nYou may safely ignore this if you previously authorized the app.")
while True:
time.sleep(timeout)
start = time.time()
timeout = 1
if possible is None:
for file in os.listdir(os.path.join(config.spire.steamdir, "saves")):
if file.endswith(".autosave"):
if possible is None:
possible = file
else:
print("Error: Multiple savefiles detected.")
possible = None
break
if possible is not None:
try:
cur = os.path.getmtime(os.path.join(config.spire.steamdir, "saves", possible))
except OSError:
possible = None
if use_sd:
try:
cur_sd = os.path.getmtime(os.path.join(user, ".prefs", "slice-and-dice-3"))
except OSError:
pass
else:
if cur_sd != last_sd:
with open(os.path.join(user, ".prefs", "slice-and-dice-3")) as f:
sd_data = f.read()
sd_data = sd_data.encode("utf-8", "xmlcharrefreplace")
async with session.post("/sync/slice", data={"data": sd_data}, params={"key": config.server.secret}) as resp:
if resp.ok:
last_sd = cur_sd
curses = await resp.read()
if curses and config.slice.curses:
decoded: list[str] = pickle.loads(curses)
try:
with open(config.slice.curses, "w") as f:
f.write("\n".join(decoded))
except OSError:
pass
to_send = []
files = []
if possible is None and config.client.sync_runs: # don't check run files during a run
for path, folders, _f in os.walk(os.path.join(config.spire.steamdir, "runs")):
for folder in folders:
profile = "0"
if folder[0].isdigit():
profile = folder[0]
for p1, d1, f1 in os.walk(os.path.join(path, folder)):
for file in f1:
if file > last_run:
to_send.append((p1, file, profile))
files.append(file)
try:
if possible is None:
async with session.get("/playing", params={"key": config.server.secret}) as resp:
if resp.ok:
j = await resp.json()
if j and j.get("item"):
track = j['item']['name']
artists = ", ".join(x['name'] for x in j['item']['artists'])
album = j['item']['album']['name']
text = f"{track}\n{artists}\n{album}"
if playing != text:
try:
with open(config.client.playing, "w") as f:
f.write(text)
playing = text
except OSError:
pass
else:
playing = None
try:
with open(config.client.playing, "w") as f:
pass # make it an empty file
except OSError:
pass
all_sent = True
if to_send: # send runs first so savefile can seamlessly transfer its cache
for path, file, profile in to_send:
with open(os.path.join(path, file)) as f:
content = f.read()
content = content.encode("utf-8", "xmlcharrefreplace")
async with session.post("/sync/run", data={"run": content, "name": file, "profile": profile}, params={"key": config.server.secret, "start": start}) as resp:
if not resp.ok:
all_sent = False
if all_sent:
last_run = max(files)
with open("last_run", "w") as f:
f.write(last_run)
if possible is None and has_save: # server has a save, but we don't (anymore)
async with session.post("/sync/save", data={"savefile": b"", "character": b""}, params={"key": config.server.secret, "has_run": str(all_sent).lower(), "start": start}) as resp:
if resp.ok:
has_save = False
if use_mt:
## MT1
try:
cur_mt = os.path.getmtime(mt_file)
except OSError:
traceback.print_exc()
else:
if cur_mt != last_mt:
with open(mt_file, "rb") as f:
mt_data = f.read()
mt_runs = {"save": mt_data}
mt_runs_last = {}
for file in os.listdir(os.path.join(mt_folder, "run-history")):
break # otherwise, it might exceed the data limit. let's not.
if not file.endswith(".db"):
continue
if file == "runHistory.db": # main one
key = "main"
elif file.startswith("runHistoryData"): # something like runHistoryData00.db
key = file[14:16]
else:
key = file # just in case
last = os.path.getmtime(os.path.join(mt_folder, "run-history", file))
if runs_last.get(key) != last:
with open(os.path.join(mt_folder, "run-history", file), "rb") as f:
mt_runs[key] = f.read()
mt_runs_last[key] = last
async with session.post("/sync/monster", data=mt_runs, params={"key": config.server.secret}) as resp:
if resp.ok:
last_mt = cur_mt
runs_last.update(mt_runs_last)
else:
print(f"ERROR: Monster Train data not properly sent:\n{resp.reason}")
## MT2
try:
cur_mt2 = os.path.getmtime(mt2_file)
except OSError:
traceback.print_exc()
else:
if cur_mt2 != last_mt2:
with open(mt2_file, "rb") as f:
mt2_data = f.read()
mt2_runs = {"save": mt2_data}
mt2_runs_last = {}
for file in os.listdir(os.path.join(mt2_folder, "run-history")):
break # otherwise, it might exceed the data limit. let's not.
if not file.endswith(".db"):
continue
if file == "runHistory.db": # main one
key = "main"
elif file.startswith("runHistoryData"): # something like runHistoryData00.db
key = file[14:16]
else:
key = file # just in case
last = os.path.getmtime(os.path.join(mt_folder, "run-history", file))
if runs_last.get(key) != last:
with open(os.path.join(mt_folder, "run-history", file), "rb") as f:
mt_runs[key] = f.read()
mt_runs_last[key] = last
async with session.post("/sync/monster-2", data=mt2_runs, params={"key": config.server.secret}) as resp:
if resp.ok:
last_mt2 = cur_mt2
runs_last.update(mt2_runs_last)
else:
print(f"ERROR: Monster Train 2 data not properly sent:\n{resp.reason}")
# update all profiles
data = {
"slots": b"",
"0": b"",
"1": b"",
"2": b"",
}
# always send the save slots; it's possible it changed, even during a run (e.g. wall card)
cur_slots = os.path.getmtime(os.path.join(config.spire.steamdir, "preferences", "STSSaveSlots"))
if cur_slots != last_slots:
with open(os.path.join(config.spire.steamdir, "preferences", "STSSaveSlots")) as f:
data["slots"] = f.read().encode("utf-8", "xmlcharrefreplace")
tobe_lasp = [0, 0, 0]
for i in range(3):
name = "STSPlayer"
if i:
name = f"{i}_{name}"
try:
fname = os.path.join(config.spire.steamdir, "preferences", name)
tobe_lasp[i] = m = os.path.getmtime(fname)
if m == lasp[i]:
continue # unchanged, don't bother
with open(fname) as f:
data[str(i)] = f.read().encode("utf-8", "xmlcharrefreplace")
except OSError:
continue
if any(data.values()):
async with session.post("/sync/profile", data=data, params={"key": config.server.secret, "start": start}) as resp:
if resp.ok:
lasp = tobe_lasp
last_slots = cur_slots
else:
print("Warning: Profiles were not successfully updated. Desyncs may occur.")
if possible is not None and cur != last:
content = ""
try:
with open(os.path.join(config.spire.steamdir, "saves", possible)) as f:
content = f.read()
except OSError:
possible = None
continue
content = content.encode("utf-8", "xmlcharrefreplace")
char = possible[:-9].encode("utf-8", "xmlcharrefreplace")
async with session.post("/sync/save", data={"savefile": content, "character": char}, params={"key": config.server.secret, "has_run": "false", "start": start}) as resp:
if resp.ok:
last = cur
has_save = True
except (ClientError, ServerDisconnectedError):
timeout = 10 # give it a bit of time
print("Error: Server is offline! Retrying in 10s")
continue
if __name__ == "__main__":
asyncio.run(main())