greatly enhanced the functionality of the generate_emu_config script + build script + updated .gitignore

This commit is contained in:
a 2023-11-10 16:34:12 +02:00
parent d95ceb0fc9
commit 82adbe4fc7
15 changed files with 1507 additions and 225 deletions

12
.gitignore vendored
View File

@ -14,3 +14,15 @@ base.lib
steamclient.exp
steamclient.lib
out/*
scripts/.py/
scripts/.venv/
scripts/.env/
scripts/.vscode/
scripts/backup/
scripts/bin/
scripts/build_tmp/
scripts/login_temp/
scripts/**/__pycache__/
scripts/generate_emu_config.spec
scripts/my_login.txt

View File

@ -160,13 +160,14 @@ def generate_controller_config(controller_vdf, config_dir):
#print(all_bindings)
if not os.path.exists(config_dir):
os.makedirs(config_dir)
if all_bindings:
if not os.path.exists(config_dir):
os.makedirs(config_dir)
for k in all_bindings:
with open(os.path.join(config_dir, k + '.txt'), 'w') as f:
for b in all_bindings[k]:
f.write(b + "=" + ','.join(all_bindings[k][b]) + "\n")
for k in all_bindings:
with open(os.path.join(config_dir, k + '.txt'), 'w') as f:
for b in all_bindings[k]:
f.write(b + "=" + ','.join(all_bindings[k][b]) + "\n")
if __name__ == '__main__':

View File

@ -0,0 +1,173 @@
import copy
import os
import time
import json
def __ClosestDictKey(targetKey : str, srcDict : dict[str, object] | set[str]) -> str | None:
for k in srcDict:
if k.lower() == f"{targetKey}".lower():
return k
return None
def __generate_ach_watcher_schema(lang: str, app_id: int, achs: list[dict]) -> list[dict]:
out_achs_list = []
for idx in range(len(achs)):
ach = copy.deepcopy(achs[idx])
out_ach_data = {}
# adjust the displayName
displayName = ""
ach_displayName = ach.get("displayName", None)
if ach_displayName:
if type(ach_displayName) == dict: # this is a dictionary
displayName : str = ach_displayName.get(lang, "")
if not displayName and ach_displayName: # has some keys but language not found
#print(f'[?] Missing language "{lang}" in "displayName" of achievement {ach["name"]}')
nearestLang = __ClosestDictKey(lang, ach_displayName)
if nearestLang:
#print(f'[?] Best matching language "{nearestLang}"')
displayName = ach_displayName[nearestLang]
else:
print(f'[?] Missing language "{lang}", using displayName from the first language for achievement {ach["name"]}')
displayName : str = list(ach_displayName.values())[0]
else: # single string (or anything else)
displayName = ach_displayName
del ach["displayName"]
else:
print(f'[?] Missing "displayName" in achievement {ach["name"]}')
out_ach_data["displayName"] = displayName
desc = ""
ach_desc = ach.get("description", None)
if ach_desc:
if type(ach_desc) == dict: # this is a dictionary
desc : str = ach_desc.get(lang, "")
if not desc and ach_desc: # has some keys but language not found
#print(f'[?] Missing language "{lang}" in "description" of achievement {ach["name"]}')
nearestLang = __ClosestDictKey(lang, ach_desc)
if nearestLang:
#print(f'[?] Best matching language "{nearestLang}"')
desc = ach_desc[nearestLang]
else:
print(f'[?] Missing language "{lang}", using description from the first language for achievement {ach["name"]}')
desc : str = list(ach_desc.values())[0]
else: # single string (or anything else)
desc = ach_desc
del ach["description"]
else:
print(f'[?] Missing "description" in achievement {ach["name"]}')
# adjust the description
out_ach_data["description"] = desc
# copy the rest of the data
out_ach_data.update(ach)
# add links to icon, icongray, and icon_gray
base_icon_url = r'https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps'
icon_hash = out_ach_data.get("icon", None)
if icon_hash:
out_ach_data["icon"] = f'{base_icon_url}/{app_id}/{icon_hash}'
else:
out_ach_data["icon"] = ""
icongray_hash = out_ach_data.get("icongray", None)
if icongray_hash:
out_ach_data["icongray"] = f'{base_icon_url}/{app_id}/{icongray_hash}'
else:
out_ach_data["icongray"] = ""
icon_gray_hash = out_ach_data.get("icon_gray", None)
if icon_gray_hash:
del out_ach_data["icon_gray"] # use the old key
out_ach_data["icongray"] = f'{base_icon_url}/{app_id}/{icon_gray_hash}'
if "hidden" in out_ach_data:
try:
out_ach_data["hidden"] = int(out_ach_data["hidden"])
except Exception as e:
pass
else:
out_ach_data["hidden"] = 0
out_achs_list.append(out_ach_data)
return out_achs_list
def generate_all_ach_watcher_schemas(
base_out_dir : str,
appid: int,
app_name : str,
app_exe : str,
achs: list[dict],
small_icon_hash : str) -> None:
ach_watcher_out_dir = os.path.join(base_out_dir, "Achievement Watcher", "steam_cache", "schema")
print(f"generating schemas for Achievement Watcher in: {ach_watcher_out_dir}")
if app_exe:
print(f"detected app exe: '{app_exe}'")
else:
print(f"[X] couldn't detect app exe")
# if not achs:
# print("[X] No achievements were found for Achievement Watcher")
# return
small_icon_url = ''
if small_icon_hash:
small_icon_url = f"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{appid}/{small_icon_hash}.jpg"
images_base_url = r'https://cdn.cloudflare.steamstatic.com/steam/apps'
ach_watcher_base_schema = {
"appid": appid,
"name": app_name,
"binary": app_exe,
"achievement": {
"total": len(achs),
},
"img": {
"header": f"{images_base_url}/{appid}/header.jpg",
"background": f"{images_base_url}/{appid}/page_bg_generated_v6b.jpg",
"portrait": f"{images_base_url}/{appid}/library_600x900.jpg",
"hero": f"{images_base_url}/{appid}/library_hero.jpg",
"icon": small_icon_url,
},
"apiVersion": 1,
}
langs : set[str] = set()
for ach in achs:
displayNameLangs = ach.get("displayName", None)
if displayNameLangs and type(displayNameLangs) == dict:
langs.update(list(displayNameLangs.keys()))
descriptionLangs = ach.get("description", None)
if descriptionLangs and type(descriptionLangs) == dict:
langs.update(list(descriptionLangs.keys()))
if "token" in langs:
langs.remove("token")
tokenKey = __ClosestDictKey("token", langs)
if tokenKey:
langs.remove(tokenKey)
if not langs:
print("[X] Couldn't detect supported languages, assuming English is the only supported language for Achievement Watcher")
langs = ["english"]
for lang in langs:
out_schema_folder = os.path.join(ach_watcher_out_dir, lang)
if not os.path.exists(out_schema_folder):
os.makedirs(out_schema_folder)
time.sleep(0.050)
out_schema = copy.copy(ach_watcher_base_schema)
out_schema["achievement"]["list"] = __generate_ach_watcher_schema(lang, appid, achs)
out_schema_file = os.path.join(out_schema_folder, f'{appid}.db')
with open(out_schema_file, "wt", encoding='utf-8') as f:
json.dump(out_schema, f, ensure_ascii=False, indent=2)

View File

@ -0,0 +1,230 @@
import os
import json
import queue
import threading
import time
import requests
import urllib.parse
from external_components import (
safe_name
)
def __downloader_thread(q : queue.Queue[tuple[str, str]]):
while True:
url, path = q.get()
if not url:
q.task_done()
return
# try 3 times
for download_trial in range(3):
try:
r = requests.get(url)
if r.status_code == requests.codes.ok: # if download was successfull
with open(path, "wb") as f:
f.write(r.content)
break
except Exception:
pass
time.sleep(0.1)
q.task_done()
def __remove_url_query(url : str) -> str:
url_parts = urllib.parse.urlsplit(url)
url_parts_list = list(url_parts)
url_parts_list[3] = '' # remove query
return str(urllib.parse.urlunsplit(url_parts_list))
def __download_screenshots(
base_out_dir : str,
appid : int,
app_details : dict,
download_screenshots : bool,
download_thumbnails : bool):
if not download_screenshots and not download_thumbnails:
return
screenshots : list[dict[str, object]] = app_details.get(f'{appid}', {}).get('data', {}).get('screenshots', [])
if not screenshots:
print(f'[?] no screenshots or thumbnails are available')
return
screenshots_out_dir = os.path.join(base_out_dir, "screenshots")
if download_screenshots:
print(f"downloading screenshots in: {screenshots_out_dir}")
if not os.path.exists(screenshots_out_dir):
os.makedirs(screenshots_out_dir)
time.sleep(0.025)
thumbnails_out_dir = os.path.join(screenshots_out_dir, "thumbnails")
if download_thumbnails:
print(f"downloading screenshots thumbnails in: {thumbnails_out_dir}")
if not os.path.exists(thumbnails_out_dir):
os.makedirs(thumbnails_out_dir)
time.sleep(0.025)
q : queue.Queue[tuple[str, str]] = queue.Queue()
max_threads = 20
for i in range(max_threads):
threading.Thread(target=__downloader_thread, args=(q,), daemon=True).start()
for scrn in screenshots:
if download_screenshots:
full_image_url = scrn.get('path_full', None)
if full_image_url:
full_image_url_sanitized = __remove_url_query(full_image_url)
image_hash_name = f'{full_image_url_sanitized.rsplit("/", 1)[-1]}'.rstrip()
if image_hash_name:
q.put((full_image_url_sanitized, os.path.join(screenshots_out_dir, image_hash_name)))
else:
print(f'[X] cannot download screenshot from url: "{full_image_url}", failed to get image name')
if download_thumbnails:
thumbnail_url = scrn.get('path_thumbnail', None)
if thumbnail_url:
thumbnail_url_sanitized = __remove_url_query(thumbnail_url)
image_hash_name = f'{thumbnail_url_sanitized.rsplit("/", 1)[-1]}'.rstrip()
if image_hash_name:
q.put((thumbnail_url_sanitized, os.path.join(thumbnails_out_dir, image_hash_name)))
else:
print(f'[X] cannot download screenshot thumbnail from url: "{thumbnail_url}", failed to get image name')
q.join()
for i in range(max_threads):
q.put((None, None))
q.join()
print(f"finished downloading app screenshots")
PREFERED_VIDS = [
'trailer', 'gameplay', 'announcement'
]
def __download_videos(base_out_dir : str, appid : int, app_details : dict):
videos : list[dict[str, object]] = app_details.get(f'{appid}', {}).get('data', {}).get('movies', [])
if not videos:
print(f'[?] no videos were found')
return
videos_out_dir = os.path.join(base_out_dir, "videos")
print(f"downloading app videos in: {videos_out_dir}")
first_vid : tuple[str, str] = None
prefered_vid : tuple[str, str] = None
for vid in videos:
vid_name = f"{vid.get('name', '')}"
webm_url = vid.get('webm', {}).get("480", None)
mp4_url = vid.get('mp4', {}).get("480", None)
ext : str = None
prefered_url : str = None
if mp4_url:
prefered_url = mp4_url
ext = 'mp4'
elif webm_url:
prefered_url = webm_url
ext = 'webm'
else: # no url is found
print(f'[X] no url is found for video "{vid_name}"')
continue
vid_url_sanitized = __remove_url_query(prefered_url)
vid_name_in_url = f'{vid_url_sanitized.rsplit("/", 1)[-1]}'.rstrip()
vid_name = safe_name.create_safe_name(vid_name)
if vid_name:
vid_name = f'{vid_name}.{ext}'
else:
vid_name = vid_name_in_url
if vid_name:
if not first_vid:
first_vid = (vid_url_sanitized, vid_name)
if any(vid_name.lower().find(candidate) > -1 for candidate in PREFERED_VIDS):
prefered_vid = (vid_url_sanitized, vid_name)
if prefered_vid:
break
else:
print(f'[X] cannot download video from url: "{prefered_url}", failed to get vido name')
if not first_vid and not prefered_vid:
print(f'[X] no video url could be found')
return
elif not prefered_vid:
prefered_vid = first_vid
if not os.path.exists(videos_out_dir):
os.makedirs(videos_out_dir)
time.sleep(0.05)
q : queue.Queue[tuple[str, str]] = queue.Queue()
max_threads = 1
for i in range(max_threads):
threading.Thread(target=__downloader_thread, args=(q,), daemon=True).start()
# TODO download all videos
print(f'donwloading video: "{prefered_vid[1]}"')
q.put((prefered_vid[0], os.path.join(videos_out_dir, prefered_vid[1])))
q.join()
for i in range(max_threads):
q.put((None, None))
q.join()
print(f"finished downloading app videos")
def download_app_details(
base_out_dir : str,
info_out_dir : str,
appid : int,
download_screenshots : bool,
download_thumbnails : bool,
download_vids : bool):
details_out_file = os.path.join(info_out_dir, "app_details.json")
print(f"downloading app details in: {details_out_file}")
app_details : dict = None
last_exception : Exception | str = None
# try 3 times
for download_trial in range(3):
try:
r = requests.get(f'http://store.steampowered.com/api/appdetails?appids={appid}&format=json')
if r.status_code == requests.codes.ok: # if download was successfull
result : dict = r.json()
json_ok = result.get(f'{appid}', {}).get('success', False)
if json_ok:
app_details = result
break
else:
last_exception = "JSON success was False"
except Exception as e:
last_exception = e
time.sleep(0.1)
if not app_details:
err = "[X] failed to download app details"
if last_exception:
err += f', last error: "{last_exception}"'
print(err)
return
with open(details_out_file, "wt", encoding='utf-8') as f:
json.dump(app_details, f, ensure_ascii=False, indent=2)
__download_screenshots(base_out_dir, appid, app_details, download_screenshots, download_thumbnails)
if download_vids:
__download_videos(base_out_dir, appid, app_details)

View File

@ -0,0 +1,94 @@
import os
import threading
import time
import requests
def download_app_images(
base_out_dir : str,
appid : int,
clienticon : str,
icon : str,
logo : str,
logo_small : str):
icons_out_dir = os.path.join(base_out_dir, "images")
print(f"downloading common app images in: {icons_out_dir}")
def downloader_thread(image_name : str, image_url : str):
# try 3 times
for download_trial in range(3):
try:
r = requests.get(image_url)
if r.status_code == requests.codes.ok: # if download was successfull
with open(os.path.join(icons_out_dir, image_name), "wb") as f:
f.write(r.content)
break
except Exception as ex:
pass
time.sleep(0.1)
app_images_names = [
r'capsule_184x69.jpg',
r'capsule_231x87.jpg',
r'capsule_231x87_alt_assets_0.jpg',
r'capsule_467x181.jpg',
r'capsule_616x353.jpg',
r'capsule_616x353_alt_assets_0.jpg',
r'library_600x900.jpg',
r'library_600x900_2x.jpg',
r'library_hero.jpg',
r'broadcast_left_panel.jpg',
r'broadcast_right_panel.jpg',
r'page.bg.jpg',
r'page_bg_raw.jpg',
r'page_bg_generated.jpg',
r'page_bg_generated_v6b.jpg',
r'header.jpg',
r'header_alt_assets_0.jpg',
r'hero_capsule.jpg',
r'logo.png',
]
if not os.path.exists(icons_out_dir):
os.makedirs(icons_out_dir)
time.sleep(0.050)
threads_list : list[threading.Thread] = []
for image_name in app_images_names:
image_url = f'https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/{image_name}'
t = threading.Thread(target=downloader_thread, args=(image_name, image_url), daemon=True)
threads_list.append(t)
t.start()
community_images_url = f'https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{appid}'
if clienticon:
image_url = f'{community_images_url}/{clienticon}.ico'
t = threading.Thread(target=downloader_thread, args=('clienticon.ico', image_url), daemon=True)
threads_list.append(t)
t.start()
if icon:
image_url = f'{community_images_url}/{icon}.jpg'
t = threading.Thread(target=downloader_thread, args=('icon.jpg', image_url), daemon=True)
threads_list.append(t)
t.start()
if logo:
image_url = f'{community_images_url}/{logo}.jpg'
t = threading.Thread(target=downloader_thread, args=('logo.jpg', image_url), daemon=True)
threads_list.append(t)
t.start()
if logo_small:
image_url = f'{community_images_url}/{logo_small}.jpg'
t = threading.Thread(target=downloader_thread, args=('logo_small.jpg', image_url), daemon=True)
threads_list.append(t)
t.start()
for t in threads_list:
t.join()
print(f"finished downloading common app images")

View File

@ -0,0 +1,157 @@
import os
__cdx_ini = '''
### мллллл м
### Алллл плл лВ ппплллллллм пппппллВллм мВлллп
### Блллп Бллп ппллллА пллл Блллп
### Вллл п ллВ плллБ АллВллл
### Вллл млллллм ллл пллл мллллллм Бллллл
### лллА Аллллп плВ ллл лллВллВ Алл лллВллл
### Бллл ллллА лл ллл Алллллллллллп лллБ Бллл
### Алллм мллпВллм млл Влл лллБлллА млллА Алллм
### плллллп плллВп ллп АлллА плллллллВлп пВллм
### мллллллБ
### пппллВмммммлВлллВпп
###
###
### Game data is stored at %SystemDrive%\\Users\\Public\\Documents\\Steam\\CODEX\\{cdx_id}
###
[Settings]
###
### Game identifier (http://store.steampowered.com/app/{cdx_id})
###
AppId={cdx_id}
###
### Steam Account ID, set it to 0 to get a random Account ID
###
#AccountId=0
###
### Name of the current player
###
UserName=Player2
###
### Language that will be used in the game
###
Language=english
###
### Enable lobby mode
###
LobbyEnabled=1
###
### Lobby port to listen on
###
#LobbyPort=31183
###
### Enable/Disable Steam overlay
###
Overlays=1
###
### Set Steam connection to offline mode
###
Offline=0
###
[Interfaces]
###
### Steam Client API interface versions
###
SteamAppList=STEAMAPPLIST_INTERFACE_VERSION001
SteamApps=STEAMAPPS_INTERFACE_VERSION008
SteamClient=SteamClient017
SteamController=SteamController008
SteamFriends=SteamFriends017
SteamGameServer=SteamGameServer013
SteamGameServerStats=SteamGameServerStats001
SteamHTMLSurface=STEAMHTMLSURFACE_INTERFACE_VERSION_005
SteamHTTP=STEAMHTTP_INTERFACE_VERSION003
SteamInput=SteamInput002
SteamInventory=STEAMINVENTORY_INTERFACE_V003
SteamMatchGameSearch=SteamMatchGameSearch001
SteamMatchMaking=SteamMatchMaking009
SteamMatchMakingServers=SteamMatchMakingServers002
SteamMusic=STEAMMUSIC_INTERFACE_VERSION001
SteamMusicRemote=STEAMMUSICREMOTE_INTERFACE_VERSION001
SteamNetworking=SteamNetworking006
SteamNetworkingSockets=SteamNetworkingSockets008
SteamNetworkingUtils=SteamNetworkingUtils003
SteamParentalSettings=STEAMPARENTALSETTINGS_INTERFACE_VERSION001
SteamParties=SteamParties002
SteamRemotePlay=STEAMREMOTEPLAY_INTERFACE_VERSION001
SteamRemoteStorage=STEAMREMOTESTORAGE_INTERFACE_VERSION014
SteamScreenshots=STEAMSCREENSHOTS_INTERFACE_VERSION003
SteamTV=STEAMTV_INTERFACE_V001
SteamUGC=STEAMUGC_INTERFACE_VERSION015
SteamUser=SteamUser021
SteamUserStats=STEAMUSERSTATS_INTERFACE_VERSION012
SteamUtils=SteamUtils010
SteamVideo=STEAMVIDEO_INTERFACE_V002
###
[DLC]
###
### Automatically unlock all DLCs
###
DLCUnlockall=0
###
### Identifiers for DLCs
###
#ID=Name
{cdx_dlc_list}
###
[AchievementIcons]
###
### Bitmap Icons for Achievements
###
#halloween_8 Achieved=steam_settings\\img\\halloween_8.jpg
#halloween_8 Unachieved=steam_settings\\img\\unachieved\\halloween_8.jpg
{cdx_ach_list}
###
[Crack]
00ec7837693245e3=b7d5bc716512b5d6
'''
def generate_cdx_ini(
base_out_dir : str,
appid: int,
dlc: list[tuple[int, str]],
achs: list[dict]) -> None:
cdx_ini_path = os.path.join(base_out_dir, "steam_emu.ini")
print(f"generating steam_emu.ini for CODEX emulator in: {cdx_ini_path}")
dlc_list = [f"{d[0]}={d[1]}" for d in dlc]
achs_list = []
for ach in achs:
icon = ach.get("icon", None)
if icon:
icon = f"steam_settings\\img\\{icon}"
else:
icon = 'steam_settings\\img\\steam_default_icon_unlocked.jpg'
icon_gray = ach.get("icon_gray", None)
if icon_gray:
icon_gray = f"steam_settings\\img\\{icon_gray}"
else:
icon_gray = 'steam_settings\\img\\steam_default_icon_locked.jpg'
icongray = ach.get("icongray", None)
if icongray:
icon_gray = f"steam_settings\\img\\{icongray}"
achs_list.append(f'{ach["name"]} Achieved={icon}') # unlocked
achs_list.append(f'{ach["name"]} Unachieved={icon_gray}') # locked
formatted_ini = __cdx_ini.format(
cdx_id = appid,
cdx_dlc_list = "\n".join(dlc_list),
cdx_ach_list = "\n".join(achs_list)
)
with open(cdx_ini_path, "wt", encoding='utf-8') as f:
f.writelines(formatted_ini)

View File

@ -0,0 +1,22 @@
import re
ALLOWED_CHARS = set([
'`', '~', '!', '@',
'#', '$', '%', '&',
'(', ')', '-', '_',
'=', '+', '[', '{',
']', '}', ';', '\'',
',', '.', ' ', '\t',
'®', '',
])
def create_safe_name(app_name : str):
safe_name = ''.join(c for c in f'{app_name}' if c.isalnum() or c in ALLOWED_CHARS)\
.rstrip()\
.rstrip('.')\
.replace('\t', ' ')
safe_name = re.sub('\s\s+', ' ', safe_name)
return safe_name

View File

@ -1,11 +1,9 @@
USERNAME = ""
PASSWORD = ""
#steam ids with public profiles that own a lot of games
TOP_OWNER_IDS = [76561198028121353, 76561198001237877, 76561198355625888, 76561198001678750, 76561198237402290, 76561197979911851, 76561198152618007, 76561197969050296, 76561198213148949, 76561198037867621, 76561198108581917]
import pathlib
import time
from stats_schema_achievement_gen import achievements_gen
from external_components import (
ach_watcher_gen, cdx_gen, app_images, app_details, safe_name
)
from controller_config_generator import parse_controller_vdf
from steam.client import SteamClient
from steam.client.cdn import CDNClient
@ -20,59 +18,7 @@ import urllib.request
import urllib.error
import threading
import queue
prompt_for_unavailable = True
if len(sys.argv) < 2:
print("\nUsage: {} appid appid appid etc..\n\nExample: {} 480\n".format(sys.argv[0], sys.argv[0]))
exit(1)
appids = []
for id in sys.argv[1:]:
appids += [int(id)]
client = SteamClient()
if not os.path.exists("login_temp"):
os.makedirs("login_temp")
client.set_credential_location("login_temp")
if (len(USERNAME) == 0 or len(PASSWORD) == 0):
client.cli_login()
else:
result = client.login(USERNAME, password=PASSWORD)
auth_code, two_factor_code = None, None
while result in (EResult.AccountLogonDenied, EResult.InvalidLoginAuthCode,
EResult.AccountLoginDeniedNeedTwoFactor, EResult.TwoFactorCodeMismatch,
EResult.TryAnotherCM, EResult.ServiceUnavailable,
EResult.InvalidPassword,
):
if result == EResult.InvalidPassword:
print("invalid password, the password you set is wrong.")
exit(1)
elif result in (EResult.AccountLogonDenied, EResult.InvalidLoginAuthCode):
prompt = ("Enter email code: " if result == EResult.AccountLogonDenied else
"Incorrect code. Enter email code: ")
auth_code, two_factor_code = input(prompt), None
elif result in (EResult.AccountLoginDeniedNeedTwoFactor, EResult.TwoFactorCodeMismatch):
prompt = ("Enter 2FA code: " if result == EResult.AccountLoginDeniedNeedTwoFactor else
"Incorrect code. Enter 2FA code: ")
auth_code, two_factor_code = None, input(prompt)
elif result in (EResult.TryAnotherCM, EResult.ServiceUnavailable):
if prompt_for_unavailable and result == EResult.ServiceUnavailable:
while True:
answer = input("Steam is down. Keep retrying? [y/n]: ").lower()
if answer in 'yn': break
prompt_for_unavailable = False
if answer == 'n': break
client.reconnect(maxdelay=15)
result = client.login(USERNAME, PASSWORD, None, auth_code, two_factor_code)
import shutil
def get_stats_schema(client, game_id, owner_id):
@ -85,16 +31,18 @@ def get_stats_schema(client, game_id, owner_id):
client.send(message)
return client.wait_msg(EMsg.ClientGetUserStatsResponse, timeout=5)
def download_achievement_images(game_id, image_names, output_folder):
q = queue.Queue()
def download_achievement_images(game_id : int, image_names : set[str], output_folder : str):
print(f"downloading achievements images inside '{output_folder }', images count = {len(image_names)}")
q : queue.Queue[str] = queue.Queue()
def downloader_thread():
while True:
name = q.get()
succeeded = False
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:
@ -110,9 +58,10 @@ def download_achievement_images(game_id, image_names, output_folder):
print("URLError downloading", url, e.code)
if not succeeded:
print("error could not download", name)
q.task_done()
num_threads = 20
num_threads = 50
for i in range(num_threads):
threading.Thread(target=downloader_thread, daemon=True).start()
@ -123,34 +72,319 @@ def download_achievement_images(game_id, image_names, output_folder):
for i in range(num_threads):
q.put(None)
q.join()
print("finished downloading achievements images")
#steam ids with public profiles that own a lot of games
# https://steamladder.com/ladder/games/
# in browser console:
#const links = $x("/html/body/div[3]/table/tbody/tr/td[2]/a[@href]/@href");
#console.clear();
#for (let index = 0; index < links.length; index++) {
# const usr_link = links[index].textContent.split('/').filter(s => s);
# const usr_id = usr_link[usr_link.length - 1]
# console.log(usr_id)
#}
TOP_OWNER_IDS = set([
76561198213148949,
76561198108581917,
76561198028121353,
76561197979911851,
76561198355625888,
76561198237402290,
76561197969050296,
76561198152618007,
76561198001237877,
76561198037867621,
76561198001678750,
76561198217186687,
76561198094227663,
76561197993544755,
76561197963550511,
76561198095049646,
76561197973009892,
76561197969810632,
76561198388522904,
76561198864213876,
# 76561198017975643,
# 76561198044596404,
# 76561197976597747,
# 76561197962473290,
# 76561197976968076,
# 76561198235911884,
# 76561198313790296,
# 76561198407953371,
# 76561198063574735,
# 76561198122859224,
# 76561198154462478,
# 76561197996432822,
# 76561197979667190,
# 76561198139084236,
# 76561198842864763,
# 76561198096081579,
# 76561198019712127,
# 76561198033715344,
# 76561198121398682,
# 76561198027233260,
# 76561198104323854,
# 76561197995070100,
# 76561198001221571,
# 76561198005337430,
# 76561198085065107,
# 76561198027214426,
# 76561198062901118,
# 76561198008181611,
# 76561198124872187,
# 76561198048373585,
# 76561197974742349,
# 76561198040421250,
# 76561198017902347,
# 76561198010615256,
# 76561197970825215,
# 76561198077213101,
# 76561197971011821,
# 76561197992133229,
# 76561197963534359,
# 76561198077248235,
# 76561198152760885,
# 76561198256917957,
# 76561198326510209,
# 76561198019009765,
# 76561198047438206,
# 76561198128158703,
# 76561198037809069,
# 76561198121336040,
# 76561198102767019,
# 76561198063728345,
# 76561198082995144,
# 76561197981111953,
# 76561197995008105,
# 76561198109083829,
# 76561197968410781,
# 76561198808371265,
# 76561198025858988,
# 76561198252374474,
# 76561198382166453,
# 76561198396723427,
# 76561197992548975,
# 76561198134044398,
# 76561198029503957,
# 76561197990233857,
# 76561197971026489,
# 76561197965978376,
# 76561197976796589,
# 76561197994616562,
# 76561197984235967,
# 76561197992967892,
# 76561198097945516,
# 76561198251835488,
# 76561198281128349,
# 76561198044387084,
# 76561198015685843,
# 76561197993312863,
# 76561198020125851,
# 76561198006391846,
# 76561198158932704,
# 76561198039492467,
# 76561198035552258,
# 76561198031837797,
# 76561197982718230,
# 76561198025653291,
# 76561197972951657,
# 76561198269242105,
# 76561198004332929,
# 76561197972378106,
# 76561197962630138,
# 76561198192399786,
# 76561198119667710,
# 76561198120120943,
# 76561198015992850,
# 76561198096632451,
# 76561198008797636,
# 76561198118726910,
# 76561198018254158,
# 76561198061393233,
# 76561198086250077,
# 76561198025391492,
# 76561198050474710,
# 76561197997477460,
# 76561198105279930,
# 76561198026221141,
# 76561198443388781,
# 76561197981228012,
# 76561197986240493,
# 76561198003041763,
# 76561198056971296,
# 76561198072936438,
# 76561198264362271,
# 76561198101049562,
# 76561198831075066,
# 76561197991699268,
# 76561198042965266,
# 76561198019555404,
# 76561198111433283,
# 76561197984010356,
# 76561198427572372,
# 76561198071709714,
# 76561198034213886,
# 76561198846208086,
# 76561197991613008,
# 76561197978640923,
# 76561198009596142,
# 76561199173688191,
# 76561198294806446,
# 76561197992105918,
# 76561198155124847,
# 76561198032614383,
# 76561198051740093,
# 76561198051725954,
# 76561198048151962,
# 76561198172367910,
# 76561198043532513,
# 76561198029532782,
# 76561198106145311,
# 76561198020746864,
# 76561198122276418,
# 76561198844130640,
# 76561198890581618,
# 76561198021180815,
# 76561198046642155,
# 76561197985091630,
# 76561198119915053,
# 76561198318547224,
# 76561198426000196,
# 76561197988052802,
# 76561198008549198,
# 76561198054210948,
# 76561198028011423,
# 76561198026306582,
# 76561198079227501,
# 76561198070220549,
# 76561198034503074,
# 76561198172925593,
# 76561198286209051,
# 76561197998058239,
# 76561198057648189,
# 76561197982273259,
# 76561198093579202,
# 76561198035612474,
# 76561197970307937,
# 76561197996825541,
# 76561197981027062,
# 76561198019841907,
# 76561197970727958,
# 76561197967716198,
# 76561197970545939,
# 76561198315929726,
# 76561198093753361,
# 76561198413266831,
# 76561198045540632,
# 76561198015514779,
# 76561198004532679,
# 76561198080773680,
# 76561198079896896,
# 76561198005299723,
# 76561198337784749,
# 76561198150126284,
# 76561197988445370,
# 76561198258304011,
# 76561198321551799,
# 76561197973701057,
# 76561197973230221,
# 76561198002535276,
# 76561198100306249,
# 76561198116086535,
# 76561197970970678,
# 76561198085238363,
# 76561198007200913,
# 76561198025111129,
# 76561198068747739,
# 76561197970539274,
# 76561198148627568,
# 76561197970360549,
# 76561198098314980,
# 76561197972529138,
# 76561198007403855,
# 76561197977403803,
# 76561198124865933,
# 76561197981323238,
# 76561197960330700,
# 76561198217979953,
# 76561197960366517,
# 76561198044067612,
# 76561197967197052,
# 76561198027066612,
# 76561198072833066,
# 76561198033967307,
# 76561198104561325,
# 76561198272374716,
# 76561197970127197,
# 76561197970257188,
# 76561198026921217,
# 76561198027904347,
# 76561198062469228,
# 76561198026278913,
# 76561197970548935,
# 76561197966617426,
# 76561198356842617,
# 76561198034276722,
# 76561198355953202,
# 76561197986603983,
# 76561197967923946,
# 76561197961542845,
# 76561198121938079,
# 76561197992357639,
# 76561198002536379,
# 76561198017054389,
# 76561198031129658,
# 76561198020728639,
])
def generate_achievement_stats(client, game_id, output_directory, backup_directory):
achievement_images_dir = os.path.join(output_directory, "achievement_images")
images_to_download = []
steam_id_list = TOP_OWNER_IDS + [client.steam_id]
for x in steam_id_list:
out = get_stats_schema(client, game_id, x)
if out is not None:
if len(out.body.schema) > 0:
with open(os.path.join(backup_directory, 'UserGameStatsSchema_{}.bin'.format(appid)), 'wb') as f:
f.write(out.body.schema)
achievements, stats = achievements_gen.generate_stats_achievements(out.body.schema, output_directory)
for ach in achievements:
if "icon" in ach:
images_to_download.append(ach["icon"])
if "icon_gray" in ach:
images_to_download.append(ach["icon_gray"])
break
else:
pass
# print("no schema", out)
def generate_achievement_stats(client, game_id : int, output_directory, backup_directory) -> list[dict]:
steam_id_list = TOP_OWNER_IDS.copy()
steam_id_list.add(client.steam_id)
stats_schema_found = None
print(f"finding achievements stats...")
for id in steam_id_list:
#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 (len(images_to_download) > 0):
if stats_schema_found is None: # nothing found
print(f"[X] app id {game_id} has not achievements")
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)
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("steam_default_icon_unlocked.jpg", achievement_images_dir)
if copy_default_locked_img:
shutil.copy("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],
@ -201,11 +435,11 @@ def get_inventory_info(client, game_id):
})
def generate_inventory(client, game_id):
inventory = get_inventory_info(client, appid)
inventory = get_inventory_info(client, game_id)
if inventory.header.eresult != EResult.OK:
return None
url = "https://api.steampowered.com/IGameInventory/GetItemDefArchive/v0001?appid={}&digest={}".format(game_id, inventory.body.digest)
url = f"https://api.steampowered.com/IGameInventory/GetItemDefArchive/v0001?appid={game_id}&digest={inventory.body.digest}"
try:
with urllib.request.urlopen(url) as response:
return response.read()
@ -217,140 +451,416 @@ def generate_inventory(client, game_id):
def get_dlc(raw_infos):
try:
try:
dlc_list = set(map(lambda a: int(a), raw_infos["extended"]["listofdlc"].split(",")))
except:
dlc_list = set()
dlc_list = set()
depot_app_list = set()
all_depots = set()
try:
dlc_list = set(map(lambda a: int(f"{a}".strip()), raw_infos["extended"]["listofdlc"].split(",")))
except Exception:
dlc_list = set()
if "depots" in raw_infos:
depots = raw_infos["depots"]
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"]))
return (dlc_list, depot_app_list)
except:
if dep.isnumeric():
all_depots.add(int(dep))
return (dlc_list, depot_app_list, all_depots)
except Exception:
print("could not get dlc infos, are there any dlcs ?")
return (set(), set())
return (set(), set(), set())
for appid in appids:
backup_dir = os.path.join("backup","{}".format(appid))
out_dir = os.path.join("{}".format( "{}_output".format(appid)), "steam_settings")
EXTRA_FEATURES: list[tuple[str, str]] = [
("disable_account_avatar.txt", "disable avatar functionality."),
("disable_networking.txt", "disable all networking functionality."),
("disable_overlay.txt", "disable the overlay."),
("disable_overlay_achievement_notification.txt", "disable the achievement notifications."),
("disable_overlay_friend_notification.txt", "disable the friend invite and message notifications."),
("disable_source_query.txt", "Do not send server details for the server browser. Only works for game servers."),
]
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
print("outputting config to", out_dir)
raw = client.get_product_info(apps=[appid])
game_info = raw["apps"][appid]
if "common" in game_info:
game_info_common = game_info["common"]
#if "community_visible_stats" in game_info_common: #NOTE: checking this seems to skip stats on a few games so it's commented out
generate_achievement_stats(client, appid, out_dir, backup_dir)
if "supported_languages" in game_info_common:
with open(os.path.join(out_dir, "supported_languages.txt"), 'w') as f:
languages = game_info_common["supported_languages"]
for l in languages:
if "supported" in languages[l] and languages[l]["supported"] == "true":
f.write("{}\n".format(l))
def disable_all_extra_features(emu_settings_dir : str) -> None:
for item in EXTRA_FEATURES:
with open(os.path.join(emu_settings_dir, item[0]), 'wt', encoding='utf-8') as f:
f.write(item[1])
with open(os.path.join(out_dir, "steam_appid.txt"), 'w') as f:
f.write(str(appid))
def help():
exe_name = os.path.basename(sys.argv[0])
print(f"\nUsage: {exe_name} [-shots] [-thumbs] [-vid] [-imgs] [-name] [-cdx] [-aw] [-clean] appid appid appid ... ")
print(f" Example: {exe_name} 421050 420 480")
print(f" Example: {exe_name} -shots -thumbs -vid -imgs -name -cdx -aw -clean 421050")
print("\nSwitches:")
print(" -shots: download screenshots for each app if they're available")
print(" -thumbs: download screenshots thumbnails for each app if they're available")
print(" -vid: download the first video available for each app: trailer, gameplay, announcement, etc...")
print(" -imgs: download common images for each app: Steam generated background, icon, logo, etc...")
print(" -name: save the output of each app in a folder with the same name as the app, unsafe characters are discarded")
print(" -cdx: generate .ini file for CODEX Steam emu for each app")
print(" -aw: generate schemas of all possible languages for Achievement Watcher")
print(" -clean: delete any folder/file with the same name as the output before generating any data")
print("\nAll switches are optional except app id, at least 1 app id must be provided\n")
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"]
with open(os.path.join(out_dir, "build_id.txt"), 'w') as f:
f.write(str(buildid))
dlc_config_list = []
dlc_list, depot_app_list = get_dlc(game_info)
dlc_infos_backup = ""
if (len(dlc_list) > 0):
dlc_raw = client.get_product_info(apps=dlc_list)["apps"]
for dlc in dlc_raw:
try:
dlc_config_list.append((dlc, dlc_raw[dlc]["common"]["name"]))
except:
dlc_config_list.append((dlc, None))
dlc_infos_backup = json.dumps(dlc_raw, indent=4)
def main():
USERNAME = ""
PASSWORD = ""
with open(os.path.join(out_dir, "DLC.txt"), 'w', encoding="utf-8") as f:
for x in dlc_config_list:
if (x[1] is not None):
f.write("{}={}\n".format(x[0], x[1]))
DOWNLOAD_SCREESHOTS = False
DOWNLOAD_THUMBNAILS = False
DOWNLOAD_VIDEOS = False
DOWNLOAD_COMMON_IMAGES = False
SAVE_APP_NAME = False
GENERATE_CODEX_INI = False
GENERATE_ACHIEVEMENT_WATCHER_SCHEMAS = False
CLEANUP_BEFORE_GENERATING = False
config_generated = False
if "config" in game_info:
if "steamcontrollerconfigdetails" in game_info["config"]:
controller_details = game_info["config"]["steamcontrollerconfigdetails"]
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"]
print(id, controller_type)
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, controller_type + str(id)))
if out_vdf is not None and not config_generated:
if (controller_type in ["controller_xbox360", "controller_xboxone"] and (("default" in enabled_branches) or ("public" in enabled_branches))):
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(out_dir, "controller"))
config_generated = True
if "steamcontrollertouchconfigdetails" in game_info["config"]:
controller_details = game_info["config"]["steamcontrollertouchconfigdetails"]
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"]
print(id, controller_type)
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, controller_type + str(id)))
prompt_for_unavailable = True
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)
with open(os.path.join(backup_dir, "inventory.json"), "w") as f:
f.write(raw_inventory)
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
if len(sys.argv) < 2:
help()
sys.exit(1)
out_json_inventory = json.dumps(out_inventory, indent=2)
with open(os.path.join(out_dir, "items.json"), "w") as f:
f.write(out_json_inventory)
out_json_inventory = json.dumps(default_items, indent=2)
with open(os.path.join(out_dir, "default_items.json"), "w") as f:
f.write(out_json_inventory)
appids : set[int] = set()
for appid in sys.argv[1:]:
if f'{appid}'.isnumeric():
appids.add(int(appid))
elif f'{appid}'.lower() == '-shots':
DOWNLOAD_SCREESHOTS = True
elif f'{appid}'.lower() == '-thumbs':
DOWNLOAD_THUMBNAILS = True
elif f'{appid}'.lower() == '-vid':
DOWNLOAD_VIDEOS = True
elif f'{appid}'.lower() == '-imgs':
DOWNLOAD_COMMON_IMAGES = True
elif f'{appid}'.lower() == '-name':
SAVE_APP_NAME = True
elif f'{appid}'.lower() == '-cdx':
GENERATE_CODEX_INI = True
elif f'{appid}'.lower() == '-aw':
GENERATE_ACHIEVEMENT_WATCHER_SCHEMAS = True
elif f'{appid}'.lower() == '-clean':
CLEANUP_BEFORE_GENERATING = True
else:
print(f'[X] invalid switch: {appid}')
help()
sys.exit(1)
if not appids:
print(f'[X] no app id was provided')
help()
sys.exit(1)
client = SteamClient()
if not os.path.exists("login_temp"):
os.makedirs("login_temp")
client.set_credential_location("login_temp")
if os.path.isfile("my_login.txt"):
filedata = ['']
with open("my_login.txt", "r") as f:
filedata = f.readlines()
filedata = list(map(lambda s: s.strip().replace("\r", "").replace("\n", ""), filedata))
filedata = [l for l in filedata if l]
if len(filedata) == 2:
USERNAME = filedata[0]
PASSWORD = filedata[1]
if (len(USERNAME) == 0 or len(PASSWORD) == 0):
client.cli_login()
else:
result = client.login(USERNAME, password=PASSWORD)
auth_code, two_factor_code = None, None
while result in (EResult.AccountLogonDenied, EResult.InvalidLoginAuthCode,
EResult.AccountLoginDeniedNeedTwoFactor, EResult.TwoFactorCodeMismatch,
EResult.TryAnotherCM, EResult.ServiceUnavailable,
EResult.InvalidPassword,
):
if result == EResult.InvalidPassword:
print("invalid password, the password you set is wrong.")
exit(1)
elif result in (EResult.AccountLogonDenied, EResult.InvalidLoginAuthCode):
prompt = ("Enter email code: " if result == EResult.AccountLogonDenied else
"Incorrect code. Enter email code: ")
auth_code, two_factor_code = input(prompt), None
elif result in (EResult.AccountLoginDeniedNeedTwoFactor, EResult.TwoFactorCodeMismatch):
prompt = ("Enter 2FA code: " if result == EResult.AccountLoginDeniedNeedTwoFactor else
"Incorrect code. Enter 2FA code: ")
auth_code, two_factor_code = None, input(prompt)
elif result in (EResult.TryAnotherCM, EResult.ServiceUnavailable):
if prompt_for_unavailable and result == EResult.ServiceUnavailable:
while True:
answer = input("Steam is down. Keep retrying? [y/n]: ").lower()
if answer in 'yn': break
prompt_for_unavailable = False
if answer == 'n': break
client.reconnect(maxdelay=15)
result = client.login(USERNAME, PASSWORD, None, auth_code, two_factor_code)
for appid in appids:
print(f"********* generating info for app id {appid} *********")
raw = client.get_product_info(apps=[appid])
game_info : dict = raw["apps"][appid]
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"App name on store: '{app_name}'")
if SAVE_APP_NAME:
sanitized_name = safe_name.create_safe_name(app_name)
if sanitized_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] Couldn't find app name on store")
root_backup_dir = "backup"
backup_dir = os.path.join(root_backup_dir, f"{appid}")
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
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, "info")
if CLEANUP_BEFORE_GENERATING:
print("cleaning output folder before generating any data")
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)
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)
print(f"output dir: '{base_out_dir}'")
with open(os.path.join(info_out_dir, "product_info.json"), "wt", encoding='utf-8') as f:
json.dump(game_info, f, ensure_ascii=False, indent=2)
app_details.download_app_details(
base_out_dir, info_out_dir,
appid,
DOWNLOAD_SCREESHOTS,
DOWNLOAD_THUMBNAILS,
DOWNLOAD_VIDEOS)
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']}"
#print(f"generating achievement stats")
#if "community_visible_stats" in game_info_common: #NOTE: checking this seems to skip stats on a few games so it's commented out
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"]
languages = [lang for lang in langs if langs[lang].get("supported", "").lower() == "true"]
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')
with open(os.path.join(emu_settings_dir, "steam_appid.txt"), 'w') as f:
f.write(str(appid))
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"]
with open(os.path.join(emu_settings_dir, "build_id.txt"), 'wt', encoding='utf-8') as f:
f.write(str(buildid))
dlc_config_list : list[tuple[int, str]] = []
dlc_list, depot_app_list, all_depots = get_dlc(game_info)
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 dlc_config_list:
with open(os.path.join(emu_settings_dir, "DLC.txt"), 'wt', encoding="utf-8") as f:
for x in dlc_config_list:
f.write(f"{x[0]}={x[1]}\n")
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")
config_generated = False
if "config" in game_info:
if "steamcontrollerconfigdetails" in game_info["config"]:
controller_details = game_info["config"]["steamcontrollerconfigdetails"]
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"]
print(id, controller_type)
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, controller_type + str(id)))
if out_vdf is not None and not config_generated:
if (controller_type in ["controller_xbox360", "controller_xboxone"] and (("default" in enabled_branches) or ("public" in enabled_branches))):
parse_controller_vdf.generate_controller_config(out_vdf.decode('utf-8'), os.path.join(emu_settings_dir, "controller"))
config_generated = True
if "steamcontrollertouchconfigdetails" in game_info["config"]:
controller_details = game_info["config"]["steamcontrollertouchconfigdetails"]
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"]
print(id, controller_type)
out_vdf = download_published_file(client, int(id), os.path.join(backup_dir, controller_type + str(id)))
if "launch" in game_info["config"]:
launch_configs = game_info["config"]["launch"]
with open(os.path.join(info_out_dir, "launch_config.json"), "wt", encoding='utf-8') as f:
json.dump(launch_configs, f, ensure_ascii=False, indent=2)
first_app_exe : str = None
prefered_app_exe : str = None
unwanted_app_exes = ["launch", "start", "play", "try", "demo", "_vr",]
for cfg in launch_configs.values():
if "executable" in cfg:
app_exe = f'{cfg["executable"]}'
if app_exe.lower().endswith(".exe"):
app_exe = app_exe.replace("\\", "/").split('/')[-1]
if first_app_exe is None:
first_app_exe = app_exe
if all(app_exe.lower().find(unwanted_exe) < 0 for unwanted_exe in unwanted_app_exes):
prefered_app_exe = app_exe
break
if prefered_app_exe:
app_exe = prefered_app_exe
elif first_app_exe:
app_exe = first_app_exe
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,
dlc_config_list,
achievements)
if DOWNLOAD_COMMON_IMAGES:
app_images.download_app_images(
base_out_dir,
appid,
clienticon,
icon,
logo,
logo_small)
disable_all_extra_features(emu_settings_dir)
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)
with open(os.path.join(backup_dir, "inventory.json"), "w") as f:
f.write(raw_inventory)
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)
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)
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)
with open(os.path.join(backup_dir, "dlc_product_info.json"), "wt", encoding='utf-8') as f:
json.dump(dlc_raw, f, ensure_ascii=False, indent=2)
print(f"######### done for app id {appid} #########\n\n")
if __name__ == "__main__":
try:
main()
except Exception as e:
print("Unexpected error:")
print(e)
sys.exit(1)
game_info_backup = json.dumps(game_info, indent=4)
with open(os.path.join(backup_dir, "product_info.json"), "w") as f:
f.write(game_info_backup)
with open(os.path.join(backup_dir, "dlc_product_info.json"), "w") as f:
f.write(dlc_infos_backup)

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

37
scripts/rebuild.bat Normal file
View File

@ -0,0 +1,37 @@
@echo off
setlocal
pushd "%~dp0"
set "venv=.env"
set "out_dir=bin"
set "build_temp_dir=build_tmp"
set "tool_name=generate_emu_config"
set "icon_file=icon\Froyoshark-Enkel-Steam.ico"
set "main_file=generate_emu_config.py"
if exist "%out_dir%" (
rmdir /s /q "%out_dir%"
)
if exist "%build_temp_dir%" (
rmdir /s /q "%build_temp_dir%"
)
del /f /q "*.spec"
call "%venv%\Scripts\activate.bat"
pyinstaller "%main_file%" --distpath "%out_dir%" -y --clean --onedir --name "%tool_name%" --noupx --console -i "%icon_file%" --workpath "%build_temp_dir%" --collect-submodules "steam"
copy /y "steam_default_icon_locked.jpg" "%out_dir%\%tool_name%\"
copy /y "steam_default_icon_unlocked.jpg" "%out_dir%\%tool_name%\"
echo:
echo =============
echo Built inside : "%out_dir%\"
:script_end
popd
endlocal

15
scripts/recreate_venv.bat Normal file
View File

@ -0,0 +1,15 @@
@echo off
cd /d "%~dp0"
set "venv=.env"
set "reqs_file=requirements.txt"
if exist "%venv%" (
rmdir /s /q "%venv%"
)
python -m venv "%venv%"
timeout /t 1 /nobreak
call "%venv%\Scripts\activate.bat"
pip install -r "%reqs_file%"

2
scripts/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
steam[client]
pyinstaller

View File

@ -2,6 +2,7 @@ import vdf
import sys
import os
import json
import copy
STAT_TYPE_INT = '1'
@ -9,11 +10,13 @@ STAT_TYPE_FLOAT = '2'
STAT_TYPE_AVGRATE = '3'
STAT_TYPE_BITS = '4'
def generate_stats_achievements(schema, config_directory):
def generate_stats_achievements(
schema, config_directory
) -> tuple[list[dict], list[dict], bool, bool]:
schema = vdf.binary_loads(schema)
# print(schema)
achievements_out = []
stats_out = []
achievements_out : list[dict] = []
stats_out : list[dict] = []
for appid in schema:
sch = schema[appid]
@ -25,15 +28,19 @@ def generate_stats_achievements(schema, config_directory):
for ach_num in achs:
out = {}
ach = achs[ach_num]
out["hidden"] = '0'
out['hidden'] = 0
for x in ach['display']:
value = ach['display'][x]
if x == 'name':
x = 'displayName'
if x == 'desc':
elif x == 'desc':
x = 'description'
if x == 'Hidden':
elif x == 'Hidden' or f'{x}'.lower() == 'hidden':
x = 'hidden'
try:
value = int(value)
except Exception as e:
pass
out[x] = value
out['name'] = ach['name']
if 'progress' in ach:
@ -57,20 +64,39 @@ def generate_stats_achievements(schema, config_directory):
stats_out += [out]
#print(stat_info[s])
copy_default_unlocked_img = False
copy_default_locked_img = False
output_ach = copy.deepcopy(achievements_out)
for out_ach in output_ach:
icon = out_ach.get("icon", None)
if icon:
out_ach["icon"] = f"img/{icon}"
else:
out_ach["icon"] = r'img/steam_default_icon_unlocked.jpg'
copy_default_unlocked_img = True
icon_gray = out_ach.get("icon_gray", None)
if icon_gray:
out_ach["icon_gray"] = f"img/{icon_gray}"
else:
out_ach["icon_gray"] = r'img/steam_default_icon_locked.jpg'
copy_default_locked_img = True
output_ach = json.dumps(achievements_out, indent=4)
output_stats = ""
icongray = out_ach.get("icongray", None)
if icongray:
out_ach["icongray"] = f"{icongray}"
output_stats : list[str] = []
for s in stats_out:
default_num = 0
if (s['type'] == 'int'):
if f"{s['type']}".lower() == 'int':
try:
default_num = int(s['default'])
except ValueError:
default_num = int(float(s['default']))
else:
default_num = float(s['default'])
output_stats += "{}={}={}\n".format(s['name'], s['type'], default_num)
output_stats.append(f"{s['name']}={s['type']}={default_num}\n")
# print(output_ach)
# print(output_stats)
@ -78,13 +104,16 @@ def generate_stats_achievements(schema, config_directory):
if not os.path.exists(config_directory):
os.makedirs(config_directory)
with open(os.path.join(config_directory, "achievements.json"), 'w') as f:
f.write(output_ach)
if output_ach:
with open(os.path.join(config_directory, "achievements.json"), 'wt', encoding='utf-8') as f:
json.dump(output_ach, f, indent=2)
with open(os.path.join(config_directory, "stats.txt"), 'w', encoding='utf-8') as f:
f.write(output_stats)
if output_stats:
with open(os.path.join(config_directory, "stats.txt"), 'wt', encoding='utf-8') as f:
f.writelines(output_stats)
return (achievements_out, stats_out)
return (achievements_out, stats_out,
copy_default_unlocked_img, copy_default_locked_img)
if __name__ == '__main__':
if len(sys.argv) < 2:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB