replace 'client_id' error with simple 'wrong username and / or password' message, asking for correct username and / or password add pcgw_page script to download PCGamingWiki page (source code only), not yet enabled in main script generate steam_emu.ini for CODEX and RUNE versions, update gse_generate_interfaces
1373 lines
59 KiB
Python
1373 lines
59 KiB
Python
import pathlib
|
|
import time
|
|
from stats_schema_achievement_gen import achievements_gen
|
|
from external_components import (
|
|
ach_watcher_gen, cdx_gen, rne_gen, app_images, app_details, safe_name, scx_gen, pcgw_page, top_own
|
|
)
|
|
from controller_config_generator import parse_controller_vdf
|
|
from steam.client import SteamClient
|
|
from steam.webauth import WebAuth
|
|
from steam.enums.common import EResult
|
|
from steam.enums.emsg import EMsg
|
|
from steam.core.msg import MsgProto
|
|
import os
|
|
import re
|
|
import sys
|
|
import platform
|
|
import json
|
|
import requests
|
|
import threading
|
|
import queue
|
|
import shutil
|
|
import traceback
|
|
from configobj import ConfigObj
|
|
|
|
# Steam ids with public profiles that own a lot of games --- https://steamladder.com/ladder/games/
|
|
# How to generate/update top_owners_ids.txt upon running generate_emu_config:
|
|
# - copy and paste the above address in your web browser
|
|
# - right click and save web page, html only with the name top_owners_ids.html
|
|
# - copy and paste top_owners_ids.html next to generate_emu_config exe or py
|
|
TOP_OWNER_IDS = list(dict.fromkeys([
|
|
76561198028121353,
|
|
76561197979911851,
|
|
76561197993544755,
|
|
76561198355953202,
|
|
76561198001237877,
|
|
76561198237402290,
|
|
76561198355625888,
|
|
76561198152618007,
|
|
76561198213148949,
|
|
76561197969050296,
|
|
76561198217186687,
|
|
76561198037867621,
|
|
76561198017975643,
|
|
76561198094227663,
|
|
76561198019712127,
|
|
76561197963550511,
|
|
76561198134044398,
|
|
76561198001678750,
|
|
76561197973009892,
|
|
76561197976597747,
|
|
76561198044596404,
|
|
76561197969810632,
|
|
76561198085065107,
|
|
76561198864213876,
|
|
76561198095049646,
|
|
76561197962473290,
|
|
76561198388522904,
|
|
76561198063574735,
|
|
76561198033715344,
|
|
76561198313790296,
|
|
76561197995070100,
|
|
76561197996432822,
|
|
76561197976968076,
|
|
76561198281128349,
|
|
76561198027233260,
|
|
76561198154462478,
|
|
76561198842864763,
|
|
76561198235911884,
|
|
76561198122859224,
|
|
76561198027214426,
|
|
76561197970825215,
|
|
76561198035900006,
|
|
76561197968410781,
|
|
76561198407953371,
|
|
76561198001221571,
|
|
76561198104323854,
|
|
76561197979667190,
|
|
76561198256917957,
|
|
76561198008181611,
|
|
76561198062901118,
|
|
#76561198121398682,
|
|
#76561198077213101,
|
|
#76561197974742349,
|
|
#76561198096081579,
|
|
#76561198019009765,
|
|
#76561199130977924,
|
|
#76561198139084236,
|
|
#76561197990233857,
|
|
#76561198118726910,
|
|
#76561197971011821,
|
|
#76561198124872187,
|
|
#76561198063728345,
|
|
#76561198119667710,
|
|
#76561198808371265,
|
|
#76561197992133229,
|
|
#76561198077248235,
|
|
#76561198005337430,
|
|
#76561198082995144,
|
|
#76561198045455280,
|
|
#76561198048373585,
|
|
#76561198109083829,
|
|
#76561198326510209,
|
|
#76561198152760885,
|
|
#76561197981111953,
|
|
#76561198037809069,
|
|
#76561198093753361,
|
|
#76561199168919006,
|
|
#76561198396723427,
|
|
#76561198040421250,
|
|
#76561198017902347,
|
|
#76561198006391846,
|
|
#76561198121336040,
|
|
#76561198044387084,
|
|
#76561197994616562,
|
|
#76561199353305847,
|
|
#76561198172367910,
|
|
#76561198251835488,
|
|
#76561198021180815,
|
|
#76561198102767019,
|
|
#76561197976796589,
|
|
#76561197992548975,
|
|
#76561198890581618,
|
|
#76561197972951657,
|
|
#76561198128158703,
|
|
#76561197965978376,
|
|
#76561198047438206,
|
|
#76561197993312863,
|
|
#76561198015685843,
|
|
#76561197971026489,
|
|
#76561198252374474,
|
|
#76561197995008105,
|
|
#76561199173688191,
|
|
#76561197984235967,
|
|
#76561198031837797,
|
|
#76561198417144062,
|
|
#76561198008797636,
|
|
#76561198020125851,
|
|
#76561198039492467,
|
|
#76561198061393233,
|
|
#76561198028011423,
|
|
#76561198192399786,
|
|
#76561198996604130,
|
|
#76561198367471798,
|
|
#76561197969148931,
|
|
#76561198029503957,
|
|
#76561198155124847,
|
|
#76561198168877244,
|
|
#76561198035552258,
|
|
#76561198015992850,
|
|
#76561198026221141,
|
|
#76561198025653291,
|
|
#76561197982718230,
|
|
#76561198219343843,
|
|
#76561198034213886,
|
|
#76561197972378106,
|
|
#76561198318111105,
|
|
#76561198004332929,
|
|
#76561198018254158,
|
|
#76561197970246998,
|
|
#76561197997477460,
|
|
#76561198158932704,
|
|
#76561198269242105,
|
|
#76561198045540632,
|
|
#76561198294806446,
|
|
#76561197986240493,
|
|
#76561198105279930,
|
|
#76561198043532513,
|
|
#76561197973230221,
|
|
#76561198003041763,
|
|
#76561198020746864,
|
|
#76561198054210948,
|
|
#76561198096632451,
|
|
#76561197962630138,
|
|
#76561198029532782,
|
|
#76561198086250077,
|
|
#76561198120120943,
|
|
#76561198111433283,
|
|
#76561198046642155,
|
|
#76561198048151962,
|
|
#76561198072936438,
|
|
#76561198124865933,
|
|
#76561198019555404,
|
|
#76561198075477583,
|
|
#76561198042781427,
|
|
#76561198443388781,
|
|
#76561197984010356,
|
|
#76561198042965266,
|
|
#76561198031164839,
|
|
#76561198025391492,
|
|
#76561198122276418,
|
|
#76561197981228012,
|
|
#76561198019841907,
|
|
#76561198106206019,
|
|
#76561197981027062,
|
|
#76561197992105918,
|
|
#76561198104561325,
|
|
#76561198015856631,
|
|
#76561197991699268,
|
|
#76561198315929726,
|
|
#76561198051725954,
|
|
#76561198050474710,
|
|
#76561197985091630,
|
|
#76561198844130640,
|
|
#76561198264362271,
|
|
#76561198846208086,
|
|
#76561198032614383,
|
|
#76561198079227501,
|
|
#76561198026306582,
|
|
#76561198009596142,
|
|
#76561198056971296,
|
|
#76561197991613008,
|
|
#76561198028428529,
|
|
#76561198427572372,
|
|
#76561198071709714,
|
|
#76561198101049562,
|
|
#76561197969365800,
|
|
#76561198093579202,
|
|
#76561198171791210,
|
|
#76561198413266831,
|
|
#76561198165450871,
|
|
#76561198085238363,
|
|
#76561198106145311,
|
|
#76561197973701057,
|
|
#76561198811114019,
|
|
#76561198034906703,
|
|
#76561198119915053,
|
|
#76561198079896896,
|
|
#76561197988052802,
|
|
#76561198172925593,
|
|
#76561197970545939,
|
|
#76561198004532679,
|
|
#76561198008549198,
|
|
#76561198831075066,
|
|
#76561198002535276,
|
|
#76561197977920776,
|
|
#76561198015514779,
|
|
#76561198072361453,
|
|
#76561198070220549,
|
|
#76561197970307937,
|
|
#76561197982273259,
|
|
#76561197978640923,
|
|
#76561198090111762,
|
|
#76561198007200913,
|
|
#76561197970970678,
|
|
#76561197970360549,
|
|
#76561198051740093,
|
|
#76561197996825541,
|
|
#76561197967716198,
|
|
#76561198027066612,
|
|
#76561197962850521,
|
|
#76561197998058239,
|
|
#76561197966617426,
|
|
#76561198098314980,
|
|
#76561197984605215,
|
|
#76561198035612474,
|
|
#76561198025111129,
|
|
#76561198318547224,
|
|
#76561198034503074,
|
|
#76561198426000196,
|
|
#76561198356842617,
|
|
#76561198150467988,
|
|
#76561198080773680,
|
|
#76561198083977059,
|
|
#76561198286209051,
|
|
#76561198033967307,
|
|
#76561197988445370,
|
|
#76561198217979953,
|
|
#76561198026278913,
|
|
#76561198321551799,
|
|
#76561199080934614,
|
|
#76561197963735863,
|
|
#76561197970127197,
|
|
#76561197994153029,
|
|
#76561197992357639,
|
|
#76561198070585472,
|
|
#76561198026921217,
|
|
#76561197983517848,
|
|
#76561198027904347,
|
|
#76561198002536379,
|
|
#76561198027917594
|
|
]))
|
|
|
|
def get_exe_dir(relative = False):
|
|
# https://pyinstaller.org/en/stable/runtime-information.html
|
|
if relative:
|
|
return os.path.curdir
|
|
|
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
|
return os.path.dirname(sys.executable)
|
|
else:
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
def get_stats_schema(client, game_id, owner_id):
|
|
message = MsgProto(EMsg.ClientGetUserStats)
|
|
message.body.game_id = game_id
|
|
message.body.schema_local_version = -1
|
|
message.body.crc_stats = 0
|
|
message.body.steam_id_for_user = owner_id
|
|
|
|
client.send(message)
|
|
return client.wait_msg(EMsg.ClientGetUserStatsResponse, timeout=5)
|
|
|
|
def download_achievement_images(game_id : int, image_names : set[str], output_folder : str):
|
|
print(f"[ ] Found {len(image_names)} achievements images --- downloading to '.\\steam_settings\\img' folder")
|
|
|
|
q : queue.Queue[str] = queue.Queue()
|
|
|
|
def downloader_thread():
|
|
while True:
|
|
name = q.get()
|
|
if name is None:
|
|
q.task_done()
|
|
return
|
|
|
|
succeeded = False
|
|
for u in ["https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/", "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/"]:
|
|
url = "{}{}/{}".format(u, game_id, name)
|
|
try:
|
|
response = requests.get(url, allow_redirects=True)
|
|
response.raise_for_status()
|
|
image_data = response.content
|
|
with open(os.path.join(output_folder, name), "wb") as f:
|
|
f.write(image_data)
|
|
succeeded = True
|
|
break
|
|
except Exception as e:
|
|
print("____ HTTPError downloading", url, file=sys.stderr)
|
|
traceback.print_exception(e, file=sys.stderr)
|
|
if not succeeded:
|
|
print("____ Error, could not download", name)
|
|
|
|
q.task_done()
|
|
|
|
num_threads = 50
|
|
|
|
for i in range(num_threads):
|
|
threading.Thread(target=downloader_thread, daemon=True).start()
|
|
|
|
for name in image_names:
|
|
q.put(name)
|
|
q.join()
|
|
|
|
for i in range(num_threads):
|
|
q.put(None)
|
|
q.join()
|
|
|
|
def generate_achievement_stats(client, game_id : int, output_directory, backup_directory) -> list[dict]:
|
|
stats_schema_found = None
|
|
#print(f"[ ] Finding achievements stats...")
|
|
for id in TOP_OWNER_IDS:
|
|
#print(f"[ ] Finding achievements stats using account ID {id}...")
|
|
out = get_stats_schema(client, game_id, id)
|
|
if out is not None and len(out.body.schema) > 0:
|
|
stats_schema_found = out
|
|
#print(f"[ ] Found achievement stats using account ID {id}")
|
|
break
|
|
|
|
if stats_schema_found is None: # no achievement found
|
|
print(f"[?] No achievements found - skip creating 'achievements.json'")
|
|
return []
|
|
|
|
achievement_images_dir = os.path.join(output_directory, "img")
|
|
images_to_download : set[str] = set()
|
|
|
|
with open(os.path.join(backup_directory, f'UserGameStatsSchema_{game_id}.bin'), 'wb') as f:
|
|
f.write(stats_schema_found.body.schema)
|
|
|
|
(
|
|
achievements, stats,
|
|
copy_default_unlocked_img, copy_default_locked_img
|
|
) = achievements_gen.generate_stats_achievements(stats_schema_found.body.schema, output_directory)
|
|
|
|
if len(achievements) != 1:
|
|
print(f"[ ] Found {len(achievements)} achievements --- writing to 'achievements.json'")
|
|
else:
|
|
print(f"[ ] Found {len(achievements)} achievement --- writing to 'achievements.json'")
|
|
|
|
#print(f"[ ] Writing 'UserGameStatsSchema_{game_id}.bin'")
|
|
|
|
for ach in achievements:
|
|
icon = f"{ach.get('icon', '')}".strip()
|
|
if icon:
|
|
images_to_download.add(icon)
|
|
icon_gray = f"{ach.get('icon_gray', '')}".strip()
|
|
if icon_gray:
|
|
images_to_download.add(icon_gray)
|
|
|
|
if images_to_download:
|
|
if not os.path.exists(achievement_images_dir):
|
|
os.makedirs(achievement_images_dir)
|
|
if copy_default_unlocked_img:
|
|
shutil.copy(os.path.join(get_exe_dir(), "steam_default_icon_unlocked.jpg"), achievement_images_dir)
|
|
if copy_default_locked_img:
|
|
shutil.copy(os.path.join(get_exe_dir(), "steam_default_icon_locked.jpg"), achievement_images_dir)
|
|
download_achievement_images(game_id, images_to_download, achievement_images_dir)
|
|
|
|
return achievements
|
|
|
|
def get_ugc_info(client, published_file_id):
|
|
return client.send_um_and_wait('PublishedFile.GetDetails#1', {
|
|
'publishedfileids': [published_file_id],
|
|
'includetags': False,
|
|
'includeadditionalpreviews': False,
|
|
'includechildren': False,
|
|
'includekvtags': False,
|
|
'includevotes': False,
|
|
'short_description': True,
|
|
'includeforsaledata': False,
|
|
'includemetadata': False,
|
|
'language': 0
|
|
})
|
|
|
|
def download_published_file(client, published_file_id, backup_directory):
|
|
ugc_info = get_ugc_info(client, published_file_id)
|
|
|
|
if (ugc_info is None):
|
|
print("____ Failed getting published file", published_file_id)
|
|
return None
|
|
|
|
file_details = ugc_info.body.publishedfiledetails[0]
|
|
if (file_details.result != EResult.OK):
|
|
print("____ Failed getting published file", published_file_id, file_details.result)
|
|
return None
|
|
|
|
if not os.path.exists(backup_directory):
|
|
os.makedirs(backup_directory)
|
|
|
|
with open(os.path.join(backup_directory, "info.txt"), "w") as f:
|
|
f.write(str(ugc_info.body))
|
|
|
|
if len(file_details.file_url) > 0:
|
|
try:
|
|
response = requests.get(file_details.file_url, allow_redirects=True)
|
|
response.raise_for_status()
|
|
data = response.content
|
|
with open(os.path.join(backup_directory, file_details.filename.replace("/", "_").replace("\\", "_")), "wb") as f:
|
|
f.write(data)
|
|
return data
|
|
except Exception as e:
|
|
print(f"____ Error downloading from '{file_details.file_url}'", file=sys.stderr)
|
|
traceback.print_exception(e, file=sys.stderr)
|
|
return None
|
|
else:
|
|
print("____ Could not download file", published_file_id, "no url")
|
|
print("____ You can ignore this if the game doesn't need a controller config")
|
|
return None
|
|
|
|
def get_inventory_info(client, game_id):
|
|
return client.send_um_and_wait('Inventory.GetItemDefMeta#1', {
|
|
'appid': game_id
|
|
})
|
|
|
|
def generate_inventory(client, game_id):
|
|
inventory = get_inventory_info(client, game_id)
|
|
if inventory.header.eresult != EResult.OK:
|
|
return None
|
|
|
|
url = f"https://api.steampowered.com/IGameInventory/GetItemDefArchive/v0001?appid={game_id}&digest={inventory.body.digest}"
|
|
try:
|
|
response = requests.get(url, allow_redirects=True)
|
|
response.raise_for_status()
|
|
return response.content
|
|
except Exception as e:
|
|
print(f"Error downloading from '{url}'", file=sys.stderr)
|
|
traceback.print_exception(e, file=sys.stderr)
|
|
return None
|
|
|
|
def parse_branches(branches: dict) -> list[dict]:
|
|
ret = []
|
|
for branch_name in branches:
|
|
branch_data: dict = branches[branch_name]
|
|
branch_info = {
|
|
'name': branch_name,
|
|
'description': f'{branch_data.get("description", "")}',
|
|
'protected': False,
|
|
'build_id': 0, # dummy
|
|
'time_updated': int(time.time()), # dummy
|
|
}
|
|
# password protected
|
|
if 'pwdrequired' in branch_data:
|
|
try:
|
|
protected = f'{branch_data["pwdrequired"]}'.lower()
|
|
branch_info["protected"] = protected == "true" or protected == "1"
|
|
except Exception as e:
|
|
pass
|
|
# build id
|
|
try:
|
|
buildid = int( f'{branch_data.get("buildid", 0)}' )
|
|
branch_info["build_id"] = buildid
|
|
except Exception as e:
|
|
pass
|
|
# time updated
|
|
if 'timeupdated' in branch_data:
|
|
try:
|
|
timeupdated = int( f'{branch_data["timeupdated"]}' )
|
|
branch_info["time_updated"] = timeupdated
|
|
except Exception as e:
|
|
pass
|
|
|
|
ret.append(branch_info)
|
|
|
|
return ret
|
|
|
|
# DLC, Depots, Branches
|
|
def get_depots_infos(raw_infos, appid):
|
|
#print(f"[ ] Finding DLC infos...")
|
|
try:
|
|
dlc_list = set()
|
|
depot_app_list = set()
|
|
all_depots = set()
|
|
all_branches = []
|
|
try:
|
|
dlc_list = set(map(lambda a: int(f"{a}".strip()), raw_infos["extended"]["listofdlc"].split(",")))
|
|
except Exception:
|
|
#print(f"[?] Could not get DLCs info. Is there any depot for {appid}?")
|
|
pass
|
|
|
|
if "depots" in raw_infos:
|
|
depots : dict[str, object] = raw_infos["depots"]
|
|
for dep in depots:
|
|
depot_info = depots[dep]
|
|
if "dlcappid" in depot_info:
|
|
dlc_list.add(int(depot_info["dlcappid"]))
|
|
if "depotfromapp" in depot_info:
|
|
depot_app_list.add(int(depot_info["depotfromapp"]))
|
|
if dep.isnumeric():
|
|
all_depots.add(int(dep))
|
|
elif f'{dep}'.lower() == 'branches':
|
|
all_branches.extend(parse_branches(depot_info))
|
|
#else:
|
|
#print(f"[?] Could not get depots info. Is there any DLC for {appid}?")
|
|
|
|
return (dlc_list, depot_app_list, all_depots, all_branches)
|
|
except Exception:
|
|
return (set(), set(), set())
|
|
|
|
# https://stackoverflow.com/a/48336994
|
|
def GetListOfSubstrings(stringSubject,string1,string2):
|
|
MyList = []
|
|
intstart=0
|
|
strlength=len(stringSubject)
|
|
continueloop = 1
|
|
|
|
while(intstart < strlength and continueloop == 1):
|
|
intindex1=stringSubject.find(string1,intstart)
|
|
if(intindex1 != -1): #The substring was found, lets proceed
|
|
intindex1 = intindex1+len(string1)
|
|
intindex2 = stringSubject.find(string2,intindex1)
|
|
if(intindex2 != -1):
|
|
subsequence=stringSubject[intindex1:intindex2]
|
|
MyList.append(subsequence)
|
|
intstart=intindex2+len(string2)
|
|
else:
|
|
continueloop=0
|
|
else:
|
|
continueloop=0
|
|
return MyList
|
|
|
|
# https://stackoverflow.com/a/13641746 # NOTE using this fix a strange issue where some DLC names had starting and trailing double quotes ( " )
|
|
def ReplaceStringInFile(f_file, search_string, old_string, new_string):
|
|
with open(f_file, 'r') as file:
|
|
lines = file.readlines()
|
|
#matching_lines = [line.strip() for line in lines if ' = "' in line]
|
|
#return matching_lines
|
|
for line in lines:
|
|
if search_string in line:
|
|
# Read contents from file as a single string
|
|
f_handle = open(f_file, 'r')
|
|
f_string = f_handle.read()
|
|
f_handle.close()
|
|
|
|
# Use RE package to allow for replacement, also allowing for multi-line REGEX
|
|
f_string = (re.sub(old_string, new_string, f_string))
|
|
|
|
# Write contents to file - using 'w' truncates the file
|
|
f_handle = open(f_file, 'w')
|
|
f_handle.write(f_string)
|
|
f_handle.close()
|
|
|
|
def help():
|
|
exe_name = os.path.basename(sys.argv[0])
|
|
print(f"\nUsage: {exe_name} [Switches] appid appid appid ... ")
|
|
print(f" Example: {exe_name} 421050 420 480")
|
|
print(f" Example: {exe_name} -img -scr -vids_max -scx -cdx -rne -acw -clr 421050 480")
|
|
print("\nSwitches:")
|
|
print(" -img: download art images for each app: Steam generated background, icon, logo, etc...")
|
|
print(" -scr: download screenshots for each app if they're available")
|
|
print(" -vids_low: download low quality videos for each app if they're available")
|
|
print(" -vids_max: download max quality videos for each app if they're available")
|
|
print(" -scx: download market images for each app: Steam trading cards, badges, backgrounds, etc...")
|
|
print(" -cdx: generate .ini file for CODEX Steam emu for each app")
|
|
print(" -rne: generate .ini file for RUNE Steam emu for each app")
|
|
print(" -acw: generate schemas of all possible languages for Achievement Watcher")
|
|
print(" -skip_ach: skip downloading & generating achievements and their images")
|
|
print(" -skip_con: skip downloading & generating controller configuration files")
|
|
print(" -skip_inv: skip downloading & generating inventory data (items.json & default_items.json)")
|
|
print(" -clr: delete any folder/file with the same name as the output before generating any data")
|
|
print(" -rel: generate temp files/folders, and expect input files, relative to the current working directory")
|
|
print(" -anon: login as an anonymous account, these have very limited access and cannot get all app details")
|
|
print(" -name: save the output of each app in a folder with the same name as the app, unsafe characters are discarded")
|
|
print("\nAll switches are optional except app id, at least 1 app id must be provided")
|
|
print("\nAutomate the login prompt:")
|
|
print(" * You can create a file called 'my_login.txt' beside the script, then add your username on the first line")
|
|
print(" and your password on the second line.")
|
|
print(" * You can set these 2 environment variables (will override 'my_login.txt'):")
|
|
print(" GSE_CFG_USERNAME")
|
|
print(" GSE_CFG_PASSWORD")
|
|
print("")
|
|
|
|
|
|
def main():
|
|
USERNAME = ""
|
|
PASSWORD = ""
|
|
|
|
DOWNLOAD_SCREENSHOTS = False
|
|
DOWNLOAD_VIDEOS = False
|
|
DOWNLOAD_LOW = False
|
|
DOWNLOAD_MAX = False
|
|
DOWNLOAD_COMMON_IMAGES = False
|
|
DOWNLOAD_SCX = False
|
|
SAVE_APP_NAME = False
|
|
GENERATE_CODEX_INI = False
|
|
GENERATE_RUNE_INI = False
|
|
GENERATE_ACHIEVEMENT_WATCHER_SCHEMAS = False
|
|
CLEANUP_BEFORE_GENERATING = False
|
|
ANON_LOGIN = False
|
|
RELATIVE_DIR = False
|
|
SKIP_ACH = False
|
|
SKIP_CONTROLLER = False
|
|
SKIP_INVENTORY = False
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 1
|
|
|
|
|
|
if len(sys.argv) < 2:
|
|
help()
|
|
sys.exit(1)
|
|
|
|
appids : set[int] = set()
|
|
for appid in sys.argv[1:]:
|
|
if f'{appid}'.isnumeric():
|
|
appids.add(int(appid))
|
|
elif f'{appid}'.lower() == '-scr':
|
|
DOWNLOAD_SCREENSHOTS = True
|
|
elif f'{appid}'.lower() == '-vids_low':
|
|
DOWNLOAD_VIDEOS = True
|
|
DOWNLOAD_LOW = True
|
|
elif f'{appid}'.lower() == '-vids_max':
|
|
DOWNLOAD_VIDEOS = True
|
|
DOWNLOAD_MAX = True
|
|
elif f'{appid}'.lower() == '-img':
|
|
DOWNLOAD_COMMON_IMAGES = True
|
|
elif f'{appid}'.lower() == '-name':
|
|
SAVE_APP_NAME = True
|
|
elif f'{appid}'.lower() == '-scx':
|
|
DOWNLOAD_SCX = True
|
|
elif f'{appid}'.lower() == '-cdx':
|
|
GENERATE_CODEX_INI = True
|
|
elif f'{appid}'.lower() == '-rne':
|
|
GENERATE_RUNE_INI = True
|
|
elif f'{appid}'.lower() == '-acw':
|
|
GENERATE_ACHIEVEMENT_WATCHER_SCHEMAS = True
|
|
elif f'{appid}'.lower() == '-clr':
|
|
CLEANUP_BEFORE_GENERATING = True
|
|
elif f'{appid}'.lower() == '-anon':
|
|
ANON_LOGIN = True
|
|
elif f'{appid}'.lower() == '-rel':
|
|
RELATIVE_DIR = True
|
|
elif f'{appid}'.lower() == '-skip_ach':
|
|
SKIP_ACH = True
|
|
elif f'{appid}'.lower() == '-skip_con':
|
|
SKIP_CONTROLLER = True
|
|
elif f'{appid}'.lower() == '-skip_inv':
|
|
SKIP_INVENTORY = True
|
|
elif f'{appid}'.lower() == '-def1':
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 1
|
|
elif f'{appid}'.lower() == '-def2':
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 2
|
|
elif f'{appid}'.lower() == '-def3':
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 3
|
|
elif f'{appid}'.lower() == '-def4':
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 4
|
|
elif f'{appid}'.lower() == '-def5':
|
|
DEFAULT_PRESET = True
|
|
DEFAULT_PRESET_NO = 5
|
|
else:
|
|
print(f'___ Invalid switch: {appid}')
|
|
help()
|
|
sys.exit(1)
|
|
|
|
if not appids:
|
|
print(f'___ No app id was provided')
|
|
help()
|
|
sys.exit(1)
|
|
|
|
client = SteamClient()
|
|
# login_tmp_folder = os.path.join(get_exe_dir(RELATIVE_DIR), "login_temp")
|
|
# if not os.path.exists(login_tmp_folder):
|
|
# os.makedirs(login_tmp_folder)
|
|
# client.set_credential_location(login_tmp_folder)
|
|
|
|
# first read the 'my_login.txt' file
|
|
my_login_file = os.path.join(get_exe_dir(RELATIVE_DIR), "my_login.txt")
|
|
if not ANON_LOGIN and os.path.isfile(my_login_file):
|
|
filedata = ['']
|
|
with open(my_login_file, "r", encoding="utf-8") as f:
|
|
filedata = f.readlines()
|
|
filedata = list(map(lambda s: s.replace("\r", "").replace("\n", ""), filedata))
|
|
filedata = [l for l in filedata if l]
|
|
if len(filedata) == 2:
|
|
USERNAME = filedata[0]
|
|
PASSWORD = filedata[1]
|
|
|
|
# then allow the env vars to override the login details
|
|
env_username = os.environ.get('GSE_CFG_USERNAME', None)
|
|
env_password = os.environ.get('GSE_CFG_PASSWORD', None)
|
|
if env_username:
|
|
USERNAME = env_username
|
|
if env_password:
|
|
PASSWORD = env_password
|
|
|
|
if ANON_LOGIN:
|
|
result = client.anonymous_login()
|
|
trials = 5
|
|
while result != EResult.OK and trials > 0:
|
|
time.sleep(1000)
|
|
result = client.anonymous_login()
|
|
trials -= 1
|
|
else:
|
|
webauth = WebAuth(USERNAME, PASSWORD)
|
|
if (len(USERNAME) > 0 and len(PASSWORD) > 0):
|
|
webauth.cli_login(USERNAME, PASSWORD)
|
|
else:
|
|
webauth_prompt_username = input("Enter Steam username: ")
|
|
webauth.cli_login(webauth_prompt_username)
|
|
client.login(webauth.username, access_token=webauth.refresh_token)
|
|
|
|
# generate 'top_owners_ids.txt' if 'top_owners_ids.html' exists
|
|
top_own.top_owners()
|
|
|
|
# read and prepend top_owners_ids.txt
|
|
top_owners_file = os.path.join(get_exe_dir(RELATIVE_DIR), "top_owners_ids.txt")
|
|
if os.path.isfile(top_owners_file):
|
|
filedata = ['']
|
|
with open(top_owners_file, "r", encoding="utf-8") as f:
|
|
filedata = f.readlines()
|
|
filedata = list(map(lambda s: s.replace("\r", "").replace("\n", "").strip(), filedata))
|
|
filedata = [l for l in filedata if len(l) > 1 and l.isdecimal()]
|
|
all_ids = list(map(lambda s: int(s), filedata))
|
|
TOP_OWNER_IDS[:0] = all_ids
|
|
|
|
# prepend user account ID as a top owner
|
|
if not ANON_LOGIN:
|
|
TOP_OWNER_IDS.insert(0, client.steam_id.as_64)
|
|
|
|
for appid in appids:
|
|
|
|
print(" ")
|
|
print(f"*** STARTED config for app id {appid} ***")
|
|
print(" ")
|
|
|
|
raw = client.get_product_info(apps=[appid])
|
|
if raw["apps"]:
|
|
print(f"[ ] Found app id on Steam store")
|
|
else:
|
|
print(f"[X] Cannot find app id on Steam store")
|
|
print(" ")
|
|
print(f"*** ABORTED config for app id {appid} ***")
|
|
print(" ")
|
|
break
|
|
game_info : dict = raw["apps"][appid]
|
|
|
|
print(f"[ ] Found product info --- writing to 'app_product_info.json'")
|
|
|
|
game_info_common : dict = game_info.get("common", {})
|
|
app_name = game_info_common.get("name", "")
|
|
app_name_on_disk = f"{appid}"
|
|
if app_name:
|
|
print(f"[ ] Found app name on Steam store")
|
|
print(f"[ ] __ orig name: '{app_name}'")
|
|
sanitized_name = safe_name.create_safe_name(app_name)
|
|
if sanitized_name:
|
|
print(f"[ ] __ safe name: '{sanitized_name}'")
|
|
if SAVE_APP_NAME:
|
|
app_name_on_disk = f'{sanitized_name} _ {appid}'
|
|
else:
|
|
app_name = f"Unknown_Steam_app_{appid}" # we need this for later use in the Achievement Watcher
|
|
print(f"[X] Cannot find app name on Steam store")
|
|
|
|
#root_backup_dir = os.path.join(get_exe_dir(RELATIVE_DIR), "BACKUP")
|
|
#backup_dir = os.path.join(root_backup_dir, f"{appid}")
|
|
#if not os.path.exists(backup_dir):
|
|
# os.makedirs(backup_dir)
|
|
|
|
root_def_dir = "_DEFAULT"
|
|
root_out_dir = "_OUTPUT"
|
|
base_out_dir = os.path.join(root_out_dir, app_name_on_disk)
|
|
emu_settings_dir = os.path.join(base_out_dir, "steam_settings")
|
|
info_out_dir = os.path.join(base_out_dir, "steam_misc\\app_info")
|
|
|
|
if CLEANUP_BEFORE_GENERATING:
|
|
print(f"[ ] Cleaning '{base_out_dir}' folder...")
|
|
base_dir_path = pathlib.Path(base_out_dir)
|
|
if base_dir_path.is_file():
|
|
base_dir_path.unlink()
|
|
time.sleep(0.05)
|
|
elif base_dir_path.is_dir():
|
|
shutil.rmtree(base_dir_path)
|
|
time.sleep(0.05)
|
|
|
|
while base_dir_path.exists():
|
|
time.sleep(0.05)
|
|
|
|
root_backup_dir = os.path.join(base_out_dir, "steam_misc\\app_backup")
|
|
#backup_dir = os.path.join(root_backup_dir, f"{appid}")
|
|
backup_dir = root_backup_dir #use different structure for 'backup' dir
|
|
if not os.path.exists(backup_dir):
|
|
os.makedirs(backup_dir)
|
|
|
|
if not os.path.exists(emu_settings_dir):
|
|
os.makedirs(emu_settings_dir)
|
|
|
|
if not os.path.exists(info_out_dir):
|
|
os.makedirs(info_out_dir)
|
|
|
|
#with open(os.path.join(info_out_dir, "app_widget.url"), mode='w', newline='\r\n') as f:
|
|
#f.write(f"[InternetShortcut]\nURL=https://store.steampowered.com/widget/{appid}/")
|
|
|
|
if DEFAULT_PRESET == True:
|
|
print(f"[ ] Copying preset emu configs to '{base_out_dir}' folder")
|
|
shutil.copytree(os.path.join(root_def_dir, str(0)), base_out_dir, dirs_exist_ok=True) # copy from default emu dir
|
|
print(f"[ ] __ default emu config from '{os.path.join(root_def_dir, str(0))}' folder")
|
|
shutil.copytree(os.path.join(root_def_dir, str(DEFAULT_PRESET_NO)), base_out_dir, dirs_exist_ok=True) # copy from preset emu dir
|
|
print(f"[ ] __ preset emu config from '{os.path.join(root_def_dir, str(DEFAULT_PRESET_NO))}' folder")
|
|
if os.path.exists(os.path.join(root_def_dir, str(appid))):
|
|
shutil.copytree(os.path.join(root_def_dir, str(appid)), base_out_dir, dirs_exist_ok=True) # copy from preset app dir
|
|
print(f"[ ] __ app emu config from '{os.path.join(root_def_dir, str(appid))}' folder")
|
|
|
|
with open(os.path.join(emu_settings_dir, "steam_appid.txt"), 'w') as f:
|
|
f.write(str(appid))
|
|
#print(f"[ ] Writing 'steam_appid.txt'")
|
|
|
|
with open(os.path.join(info_out_dir, "app_product_info.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(game_info, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'app_product_info.json'")
|
|
|
|
with open(os.path.join(backup_dir, "product_info.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(game_info, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'app_product_info.json'")
|
|
|
|
app_details.download_app_details(
|
|
base_out_dir, info_out_dir,
|
|
appid,
|
|
DOWNLOAD_SCREENSHOTS,
|
|
DOWNLOAD_VIDEOS,
|
|
DOWNLOAD_LOW,
|
|
DOWNLOAD_MAX)
|
|
|
|
clienticon : str = None
|
|
icon : str = None
|
|
logo : str = None
|
|
logo_small : str = None
|
|
achievements : list[dict] = []
|
|
languages : list[str] = []
|
|
app_exe = ''
|
|
if game_info_common:
|
|
if "clienticon" in game_info_common:
|
|
clienticon = f"{game_info_common['clienticon']}"
|
|
|
|
if "icon" in game_info_common:
|
|
icon = f"{game_info_common['icon']}"
|
|
|
|
if "logo" in game_info_common:
|
|
logo = f"{game_info_common['logo']}"
|
|
|
|
if "logo_small" in game_info_common:
|
|
logo_small = f"{game_info_common['logo_small']}"
|
|
|
|
#if "community_visible_stats" in game_info_common: #NOTE: checking this seems to skip stats on a few games so it's commented out
|
|
if not SKIP_ACH:
|
|
achievements = generate_achievement_stats(client, appid, emu_settings_dir, backup_dir)
|
|
|
|
if "supported_languages" in game_info_common:
|
|
langs: dict[str, dict] = game_info_common["supported_languages"]
|
|
for lang in langs:
|
|
support: str = langs[lang].get("supported", "").lower()
|
|
if support == "true" or support == "1":
|
|
languages.append(f'{lang}'.lower())
|
|
|
|
if languages:
|
|
with open(os.path.join(emu_settings_dir, "supported_languages.txt"), 'wt', encoding='utf-8') as f:
|
|
for lang in languages:
|
|
f.write(f'{lang}\n')
|
|
#print(f"[ ] Writing 'supported_languages.txt'")
|
|
if len(languages) == 1:
|
|
print(f"[ ] Found {len(languages)} supported language --- writing to 'supported_languages.txt'")
|
|
else:
|
|
print(f"[ ] Found {len(languages)} supported languages --- writing to 'supported_languages.txt'")
|
|
else:
|
|
print(f"[?] No supported languages found - skip creating 'supported_languages.txt'")
|
|
|
|
ReplaceStringInFile(os.path.join(emu_settings_dir, "configs.app.ini"), 'This is another example DLC name', '# 56789=', '56789=') # make sure we write DLCs after '# 56789=This is another example DLC name'
|
|
|
|
# use ConfigObj to correctly update existing 'configs.app.ini' copied from ./DEFAULT configuration --- START, read ini
|
|
configs_app = ConfigObj(os.path.join(emu_settings_dir, "configs.app.ini"), encoding='utf-8')
|
|
|
|
''' # NOTE no need to write build_id to ini anymore - it will be read from 'branches.json'
|
|
if "depots" in game_info:
|
|
if "branches" in game_info["depots"]:
|
|
if "public" in game_info["depots"]["branches"]:
|
|
if "buildid" in game_info["depots"]["branches"]["public"]:
|
|
buildid = game_info["depots"]["branches"]["public"]["buildid"]
|
|
configs_app['app::general']['build_id'] = str(buildid) #updated ini through ConfigObj # NOTE deprecated, build id is read from 'branches.json'
|
|
'''
|
|
|
|
dlc_config_list : list[tuple[int, str]] = []
|
|
dlc_list, depot_app_list, all_depots, all_branches = get_depots_infos(game_info, appid)
|
|
dlc_raw = {}
|
|
if dlc_list:
|
|
dlc_raw = client.get_product_info(apps=dlc_list)["apps"]
|
|
for dlc in dlc_raw:
|
|
dlc_name = ''
|
|
try:
|
|
dlc_name = f'{dlc_raw[dlc]["common"]["name"]}'
|
|
except Exception:
|
|
pass
|
|
|
|
if not dlc_name:
|
|
dlc_name = f"Unknown Steam app {dlc}"
|
|
|
|
dlc_config_list.append((dlc, dlc_name))
|
|
|
|
if len(dlc_list) == 1:
|
|
print(f"[ ] Found {len(dlc_config_list)} DLC --- writing to 'configs.app.ini'")
|
|
else:
|
|
print(f"[ ] Found {len(dlc_config_list)} DLCs --- writing to 'configs.app.ini'")
|
|
else:
|
|
print(f"[?] No DLCs found - skip writing to 'configs.app.ini'")
|
|
|
|
if not dlc_raw == {}:
|
|
with open(os.path.join(info_out_dir, "dlc_product_info.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(dlc_raw, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'dlc_product_info.json'")
|
|
|
|
# we set unlock_all=0 nonetheless, to make the emu lock DLCs, otherwise everything is allowed
|
|
# some games use that as a detection mechanism
|
|
#configs["app::dlcs"]["unlock_all"] = str(0) #updated ini through ConfigObj - disabled, keep the existing value from default 'configs.app.ini'
|
|
|
|
for x in dlc_config_list:
|
|
configs_app["app::dlcs"][str(x[0])] = str(x[1]) #updated ini through ConfigObj
|
|
# used x[1].encode('utf-8') instead of str(x[1]) to properly deal with DLC names containing special characters like (TM) sign, (C) sign, etc
|
|
|
|
# use ConfigObj to correctly update existing 'configs.app.ini' copied from ./DEFAULT configuration --- END, write ini
|
|
configs_app.write()
|
|
#print(f"[ ] Writing 'configs.app.ini'")
|
|
|
|
ReplaceStringInFile(os.path.join(emu_settings_dir, "configs.app.ini"), ' = "', '"', '')
|
|
|
|
# ConfigObj overrides the default ini format, adding spaces before and after '=' and '""' for empty keys, so we'll use this to undo the changes
|
|
with open(os.path.join(emu_settings_dir, "configs.app.ini"), 'r') as file:
|
|
filedata = file.read()
|
|
filedata = filedata.replace(' = ""', '=')
|
|
filedata = filedata.replace(' = ', '=')
|
|
with open(os.path.join(emu_settings_dir, "configs.app.ini"), 'w') as file:
|
|
file.write(filedata)
|
|
|
|
ReplaceStringInFile(os.path.join(emu_settings_dir, "configs.app.ini"), 'This is another example DLC name', '56789=', '# 56789=') # make sure we write DLCs after '# 56789=This is another example DLC name'
|
|
|
|
# use ConfigObj to correctly update existing 'configs.main.ini' copied from ./DEFAULT configuration --- START, read ini
|
|
configs_main = ConfigObj(os.path.join(emu_settings_dir, "configs.main.ini"), encoding='utf-8')
|
|
|
|
# use CongigObj to correctly update existing 'configs.main.ini' copied from ./DEFAULT configuration --- END, write ini
|
|
configs_main.write()
|
|
#print(f"[ ] Writing 'configs.main.ini'")
|
|
|
|
# ConfigObj overrides the default ini format, adding spaces before and after '=' and '""' for empty keys, so we'll use this to undo the changes
|
|
with open(os.path.join(emu_settings_dir, "configs.main.ini"), 'r') as file:
|
|
filedata = file.read()
|
|
filedata = filedata.replace(' = ""', '=')
|
|
filedata = filedata.replace(' = ', '=')
|
|
with open(os.path.join(emu_settings_dir, "configs.main.ini"), 'w') as file:
|
|
file.write(filedata)
|
|
|
|
# use ConfigObj to correctly update existing 'configs.overlay.ini' copied from ./DEFAULT configuration --- START, read ini
|
|
configs_overlay = ConfigObj(os.path.join(emu_settings_dir, "configs.overlay.ini"), encoding='utf-8')
|
|
|
|
# use CongigObj to correctly update existing 'configs.overlay.ini' copied from ./DEFAULT configuration --- END, write ini
|
|
configs_overlay.write()
|
|
#print(f"[ ] Writing 'configs.overlay.ini'")
|
|
|
|
# ConfigObj overrides the default ini format, adding spaces before and after '=' and '""' for empty keys, so we'll use this to undo the changes
|
|
with open(os.path.join(emu_settings_dir, "configs.overlay.ini"), 'r') as file:
|
|
filedata = file.read()
|
|
filedata = filedata.replace(' = ""', '=')
|
|
filedata = filedata.replace(' = ', '=')
|
|
with open(os.path.join(emu_settings_dir, "configs.overlay.ini"), 'w') as file:
|
|
file.write(filedata)
|
|
|
|
# use ConfigObj to correctly update existing 'configs.user.ini' copied from ./DEFAULT configuration --- START, read ini
|
|
configs_user = ConfigObj(os.path.join(emu_settings_dir, "configs.user.ini"), encoding='utf-8')
|
|
|
|
# use ConfigObj to correctly update existing 'configs.user.ini' copied from ./DEFAULT configuration --- END, write ini
|
|
configs_user.write()
|
|
#print(f"[ ] Writing 'configs.user.ini'")
|
|
|
|
# ConfigObj overrides the default ini format, adding spaces before and after '=' and '""' for empty keys, so we'll use this to undo the changes
|
|
with open(os.path.join(emu_settings_dir, "configs.user.ini"), 'r') as file:
|
|
filedata = file.read()
|
|
filedata = filedata.replace(' = ""', '=')
|
|
filedata = filedata.replace(' = ', '=')
|
|
with open(os.path.join(emu_settings_dir, "configs.user.ini"), 'w') as file:
|
|
file.write(filedata)
|
|
|
|
if all_depots:
|
|
with open(os.path.join(emu_settings_dir, "depots.txt"), 'wt', encoding="utf-8") as f:
|
|
for game_depot in all_depots:
|
|
f.write(f"{game_depot}\n")
|
|
#print(f"[ ] Writing 'depots.txt'")
|
|
if len(all_depots) == 1:
|
|
print(f"[ ] Found {len(all_depots)} depot --- writing to 'depots.txt'")
|
|
else:
|
|
print(f"[ ] Found {len(all_depots)} depots --- writing to 'depots.txt'")
|
|
else:
|
|
print(f"[?] No depots found - skip creating 'depots.txt'")
|
|
|
|
if len(all_branches) >= 1:
|
|
with open(os.path.join(emu_settings_dir, "branches.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(all_branches, f, ensure_ascii=False, indent=2)
|
|
if len(all_branches) == 1:
|
|
print(f"[ ] Found {len(all_branches)} branch --- writing to 'branches.json'")
|
|
else:
|
|
print(f"[ ] Found {len(all_branches)} branches --- writing to 'branches.json'")
|
|
if "public" in game_info["depots"]["branches"]:
|
|
if "buildid" in game_info["depots"]["branches"]["public"]:
|
|
buildid = game_info["depots"]["branches"]["public"]["buildid"]
|
|
print(f"[ ] __ default branch name: public, latest build id: {buildid}")
|
|
else:
|
|
print(f"[?] No branches found - skip creating 'branches.json'")
|
|
|
|
# read some keys from 'configs.user.ini'
|
|
cfg_user = ConfigObj(os.path.join(emu_settings_dir, "configs.user.ini"), encoding='utf-8')
|
|
cfg_user_account_name = cfg_user["user::general"]["account_name"]
|
|
cfg_user_account_steamid = cfg_user["user::general"]["account_steamid"]
|
|
cfg_user_language = cfg_user["user::general"]["language"]
|
|
|
|
config_found = 0
|
|
config_generated = 0 # used to avoid overwriting supported config by unsupported one
|
|
config_generated_not_sup = 0 # used to avoid overwriting prefered unsupported config if no supported config present
|
|
downloading_ctrl_vdf = 0 # needed to remove possible duplicate 'Found controller configs...'
|
|
if "config" in game_info:
|
|
if not SKIP_CONTROLLER and "steamcontrollerconfigdetails" in game_info["config"]:
|
|
controller_details = game_info["config"]["steamcontrollerconfigdetails"]
|
|
print(f"[ ] Found controller configs --- generating action sets...")
|
|
downloading_ctrl_vdf=1
|
|
for id in controller_details:
|
|
details = controller_details[id]
|
|
controller_type = ""
|
|
enabled_branches = ""
|
|
if "controller_type" in details:
|
|
controller_type = details["controller_type"]
|
|
if "enabled_branches" in details:
|
|
enabled_branches = details["enabled_branches"]
|
|
|
|
if (("default" in enabled_branches) or ("public" in enabled_branches)): # download only 'default' and 'public' branches to avoid multiple configs for same controller type
|
|
print(f'[ ] __ downloading config, file id = {id}, controller type = {controller_type}') # first noticed for Elden Ring, two 'controller_ps4' vdf configs are downloaded, but only one of them is converted to action sets
|
|
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'))
|
|
|
|
if out_vdf is not None:
|
|
if (controller_type in ["controller_xbox360", "controller_xboxone"] and (("default" in enabled_branches) or ("public" in enabled_branches))):
|
|
config_found = 1
|
|
#print(f"[ ] __ controller type '{controller_type}' is supported ... converting .vdf to action sets")
|
|
if config_generated == 0:
|
|
print(f"[ ] __ parsing '{controller_type}' vdf - supported, can be used with emu")
|
|
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"))
|
|
|
|
# delete txt files in .\steam_settings\controller folder
|
|
for txt_file in os.listdir(os.path.join(emu_settings_dir, "controller")):
|
|
if not txt_file.endswith(".txt"):
|
|
continue
|
|
os.remove(os.path.join(os.path.join(emu_settings_dir, "controller"), txt_file))
|
|
shutil.copytree(os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"), os.path.join(emu_settings_dir, "controller"), dirs_exist_ok=True)
|
|
config_generated = 1
|
|
else:
|
|
print(f"[ ] __ parsing '{controller_type}' vdf - supported, can be used with emu")
|
|
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"))
|
|
|
|
if controller_type in ["controller_xboxone"]: # always use xboxone config if present
|
|
# delete txt files in .\steam_settings\controller folder
|
|
for txt_file in os.listdir(os.path.join(emu_settings_dir, "controller")):
|
|
if not txt_file.endswith(".txt"):
|
|
continue
|
|
os.remove(os.path.join(os.path.join(emu_settings_dir, "controller"), txt_file))
|
|
shutil.copytree(os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"), os.path.join(emu_settings_dir, "controller"), dirs_exist_ok=True)
|
|
#config_generated = 1
|
|
|
|
elif (controller_type in ["controller_ps4", "controller_ps5", "controller_steamcontroller_gordon", "controller_neptune", "controller_switch_pro"] and (("default" in enabled_branches) or ("public" in enabled_branches))):
|
|
config_found=1
|
|
#print(f"[X] __ controller type '{controller_type}' is not supported ... converting .vdf to action sets")
|
|
print(f"[X] __ parsing '{controller_type}' vdf - not supported, backup purposes only")
|
|
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"))
|
|
|
|
if not SKIP_CONTROLLER and "steamcontrollertouchconfigdetails" in game_info["config"]:
|
|
controller_details = game_info["config"]["steamcontrollertouchconfigdetails"]
|
|
if downloading_ctrl_vdf == 0:
|
|
print(f"[ ] Found controller configs --- generating action sets...")
|
|
for id in controller_details:
|
|
details = controller_details[id]
|
|
controller_type = ""
|
|
enabled_branches = ""
|
|
if "controller_type" in details:
|
|
controller_type = details["controller_type"]
|
|
if "enabled_branches" in details:
|
|
enabled_branches = details["enabled_branches"]
|
|
|
|
if (("default" in enabled_branches) or ("public" in enabled_branches)): # download only 'default' and 'public' branches to avoid multiple configs for same controller type
|
|
print(f'[ ] __ downloading config, file id = {id}, controller type = {controller_type}') # first noticed for Elden Ring, two 'controller_ps4' vdf configs are downloaded, but only one of them is converted to action sets
|
|
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'))
|
|
|
|
if out_vdf is not None:
|
|
if (controller_type in ["controller_mobile_touch"] and (("default" in enabled_branches) or ("public" in enabled_branches))):
|
|
config_found = 1
|
|
#print(f"[X] __ controller type '{controller_type}' is not supported ... converting .vdf to action sets")
|
|
print(f"[X] __ parsing '{controller_type}' vdf - not supported, backup purposes only")
|
|
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(os.path.join(backup_dir, 'controller\\' + f'{controller_type}' + '_' + f'{id}'), "action_set"))
|
|
|
|
''' # NOTE zip the parent 'app_backup' folder instead of only the child 'controller' folder
|
|
if config_found:
|
|
shutil.make_archive(os.path.join(backup_dir, 'controller'), 'zip', os.path.join(backup_dir, 'controller')) # first argument is the name of the zip file
|
|
shutil.rmtree(os.path.join(backup_dir, 'controller'))
|
|
os.makedirs(os.path.join(backup_dir, 'controller'))
|
|
shutil.move(os.path.join(backup_dir, 'controller.zip'), os.path.join(backup_dir, 'controller\\controller.zip'))
|
|
'''
|
|
|
|
if config_found == 0:
|
|
print(f"[?] No controller configs found - skip generating action sets")
|
|
|
|
if "supported_languages" in game_info["common"]:
|
|
languages_config = game_info["common"]["supported_languages"]
|
|
with open(os.path.join(info_out_dir, "common_supported_languages.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(languages_config, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'common_supported_languages.json'")
|
|
|
|
if "launch" in game_info["config"]:
|
|
launch_configs = game_info["config"]["launch"]
|
|
with open(os.path.join(info_out_dir, "config_launch.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(launch_configs, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'config_launch.json'")
|
|
|
|
app_type : str = ""
|
|
app_mode_tmp : str = ""
|
|
app_mode_new : str = ""
|
|
first_app_exe : str = None
|
|
default_app_exe : str = None
|
|
prefered_app_exe : str = None
|
|
unwanted_app_exe_launcher = ["launch", "start", "play", "settings"]
|
|
unwanted_app_exe_demo = ["demo"]
|
|
unwanted_app_exe_vr = ["_vr", "-vr", "vr_", "vr-"]
|
|
unwanted_app_exe_benchmark = ["benchmark"]
|
|
for cfg in launch_configs.values():
|
|
if "executable" in cfg:
|
|
app_exe = f'{cfg["executable"]}'
|
|
app_exe = app_exe.replace("\\", "/").split('/')[-1]
|
|
|
|
first_app_exe = app_exe
|
|
if "type" in cfg:
|
|
app_type = f'{cfg["type"]}'
|
|
if app_type == "default":
|
|
default_app_exe = app_exe
|
|
else:
|
|
prefered_app_exe = app_exe
|
|
else:
|
|
prefered_app_exe = app_exe
|
|
|
|
if all(app_exe.lower().find(unwanted_exe_l) < 0 for unwanted_exe_l in unwanted_app_exe_launcher):
|
|
app_mode_tmp = app_mode_tmp + " _no_launcher_ "
|
|
if all(app_exe.lower().find(unwanted_exe_d) < 0 for unwanted_exe_d in unwanted_app_exe_demo):
|
|
app_mode_tmp = app_mode_tmp + " _no_demo_ "
|
|
if all(app_exe.lower().find(unwanted_exe_v) < 0 for unwanted_exe_v in unwanted_app_exe_vr):
|
|
app_mode_tmp = app_mode_tmp + " _no_vr_ "
|
|
if all(app_exe.lower().find(unwanted_exe_b) < 0 for unwanted_exe_b in unwanted_app_exe_benchmark):
|
|
app_mode_tmp = app_mode_tmp + " _no_benchmark_ "
|
|
|
|
if platform.system() == "Windows": # Windows
|
|
if app_exe.lower().endswith(".exe"):
|
|
break
|
|
elif platform.system() == "Linux": # Linux
|
|
if app_exe.lower().endswith(".sh"):
|
|
break
|
|
elif platform.system() == "Darwin": # OSX
|
|
if app_exe.lower().endswith(".app"):
|
|
break
|
|
|
|
if default_app_exe:
|
|
app_exe = default_app_exe
|
|
elif prefered_app_exe:
|
|
app_exe = prefered_app_exe
|
|
elif first_app_exe:
|
|
app_exe = first_app_exe
|
|
|
|
if not "_no_launcher_" in app_mode_tmp:
|
|
app_mode_new = app_mode_new + " launcher "
|
|
if not "_no_demo_" in app_mode_tmp:
|
|
app_mode_new = app_mode_new + " demo "
|
|
if not "_no_vr_" in app_mode_tmp:
|
|
app_mode_new = app_mode_new + " vr "
|
|
if not "_no_benchmark_" in app_mode_tmp:
|
|
app_mode_new = app_mode_new + " benchmark "
|
|
|
|
app_mode_new = app_mode_new.lstrip(" ")
|
|
app_mode_new = app_mode_new.replace(" ", ", ")
|
|
app_mode_new = app_mode_new.rstrip(" ")
|
|
|
|
if "ufs" in game_info:
|
|
savegame_configs = game_info["ufs"]
|
|
with open(os.path.join(info_out_dir, "config_ufs.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(savegame_configs, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] Writing 'config_ufs.json'")
|
|
|
|
inventory_data = None
|
|
if not SKIP_INVENTORY:
|
|
inventory_data = generate_inventory(client, appid)
|
|
if inventory_data is not None:
|
|
out_inventory = {}
|
|
default_items = {}
|
|
inventory = json.loads(inventory_data.rstrip(b"\x00"))
|
|
raw_inventory = json.dumps(inventory, indent=4)
|
|
|
|
if len(inventory) != 1:
|
|
print(f"[ ] Found {len(inventory)} inventory items --- writing to 'items.json' and 'default_items.json'")
|
|
else:
|
|
print(f"[ ] Found {len(inventory)} inventory item --- writing to 'items.json' and 'default_items.json'")
|
|
|
|
with open(os.path.join(backup_dir, f"InventoryItems_{appid}.json"), "w") as f:
|
|
f.write(raw_inventory)
|
|
#print(f"[ ] Writing 'inventory.json'")
|
|
|
|
for i in inventory:
|
|
index = str(i["itemdefid"])
|
|
x = {}
|
|
for t in i:
|
|
if i[t] is True:
|
|
x[t] = "true"
|
|
elif i[t] is False:
|
|
x[t] = "false"
|
|
else:
|
|
x[t] = str(i[t])
|
|
out_inventory[index] = x
|
|
default_items[index] = 1
|
|
|
|
with open(os.path.join(emu_settings_dir, "items.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(out_inventory, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] __ writing 'items.json'")
|
|
|
|
with open(os.path.join(emu_settings_dir, "default_items.json"), "wt", encoding='utf-8') as f:
|
|
json.dump(default_items, f, ensure_ascii=False, indent=2)
|
|
#print(f"[ ] __ writing 'default_items.json'")
|
|
|
|
else:
|
|
print(f"[?] No inventory items found - skip creating 'items.json' and 'default_items.json'")
|
|
|
|
if app_exe:
|
|
if app_mode_new != "":
|
|
#print(f"[ ] Detected app exe: '{app_exe}', tags: {app_mode_new}") # use it to get some idea of what the exe might be
|
|
print(f"[ ] Detected app exe: '{app_exe}'")
|
|
else:
|
|
#print(f"[ ] Detected app exe: '{app_exe}', tags: app") # use it to get some idea of what the exe might be
|
|
print(f"[ ] Detected app exe: '{app_exe}'")
|
|
else:
|
|
print(f"[X] Cannot detect app exe")
|
|
|
|
''' # NOTE proof of concept code to get a string between two substrings in a file, e.g. get metacritic link for app from app_details.json
|
|
app_metacritic = []
|
|
if os.path.isfile(os.path.join(base_out_dir, 'steam_misc\\app_info\\app_details.json')):
|
|
with open(os.path.join(base_out_dir, 'steam_misc\\app_info\\app_details.json'), 'r', encoding='utf-8') as app_det:
|
|
app_det_line = app_det.readlines()
|
|
for line in app_det_line:
|
|
if '"url": "https://www.metacritic' in line:
|
|
app_metacritic = GetListOfSubstrings(line,'"url": "https://www.metacritic', '"')
|
|
break
|
|
'''
|
|
|
|
if os.path.isdir(os.path.join(base_out_dir, 'steam_misc\\app_backup')):
|
|
if os.listdir(os.path.join(base_out_dir, 'steam_misc\\app_backup')): # zip 'app_backup' folder only if not empty
|
|
shutil.make_archive(os.path.join(base_out_dir, 'steam_misc\\app_backup'), 'zip', os.path.join(base_out_dir, 'steam_misc\\app_backup')) # first argument is the name of the zip file
|
|
shutil.rmtree(os.path.join(base_out_dir, 'steam_misc\\app_backup'))
|
|
os.makedirs(os.path.join(base_out_dir, 'steam_misc\\app_backup'))
|
|
shutil.move(os.path.join(base_out_dir, 'steam_misc\\app_backup.zip'), os.path.join(base_out_dir, 'steam_misc\\app_backup\\app_backup.zip'))
|
|
|
|
if DOWNLOAD_COMMON_IMAGES:
|
|
app_images.download_app_images(
|
|
base_out_dir,
|
|
appid,
|
|
clienticon,
|
|
icon,
|
|
logo,
|
|
logo_small)
|
|
|
|
if GENERATE_ACHIEVEMENT_WATCHER_SCHEMAS:
|
|
ach_watcher_gen.generate_all_ach_watcher_schemas(
|
|
base_out_dir,
|
|
appid,
|
|
app_name,
|
|
app_exe,
|
|
achievements,
|
|
icon)
|
|
|
|
if GENERATE_CODEX_INI:
|
|
cdx_gen.generate_cdx_ini(
|
|
base_out_dir,
|
|
appid,
|
|
cfg_user_account_steamid,
|
|
cfg_user_account_name,
|
|
cfg_user_language,
|
|
dlc_config_list,
|
|
achievements)
|
|
|
|
if GENERATE_RUNE_INI:
|
|
rne_gen.generate_rne_ini(
|
|
base_out_dir,
|
|
appid,
|
|
cfg_user_account_steamid,
|
|
cfg_user_account_name,
|
|
cfg_user_language,
|
|
dlc_config_list,
|
|
achievements)
|
|
|
|
if DOWNLOAD_SCX:
|
|
scx_gen.download_scx(base_out_dir, appid)
|
|
|
|
print(" ")
|
|
print(f"*** FINISHED config for app id {appid} ***")
|
|
print(" ")
|
|
|
|
def _tracebackPrint(_errorValue):
|
|
print("Unexpected error:")
|
|
print(_errorValue)
|
|
print("-----------------------")
|
|
for line in traceback.format_exception(_errorValue):
|
|
print(line)
|
|
print("-----------------------")
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
if 'client_id' in e.args:
|
|
print("Wrong Steam username and / or password. Please try again!")
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
if 'client_id' in e.args:
|
|
print("Wrong Steam username and / or password. Please try again!")
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
if 'client_id' in e.args:
|
|
print("Wrong Steam username and / or password. Please try again!")
|
|
sys.exit(1)
|
|
else:
|
|
_tracebackPrint(e)
|
|
sys.exit(1)
|
|
else:
|
|
_tracebackPrint(e)
|
|
sys.exit(1)
|
|
else:
|
|
_tracebackPrint(e)
|
|
sys.exit(1)
|
|
|