Skip to content

Commit

Permalink
Some refactoring
Browse files Browse the repository at this point in the history
util.py:
- Added create_dos_device()
- Added patch_conf_value()
- Added patch_voodoo_conf()
- Added get_path_syswow64()
- Moved class ReplaceType from Bethesda mod support (class Redirect)

Other:
- Refactored gamefixes to use pathlib
- Simplified / reimplemented some fixes
- Some docstrings
- Linked Gobliiins 5 demo to main game
- Unified Gothic 3 and Gothic 3 Forsaken Gods Enhanced Edition
- Fixes after rebase
  • Loading branch information
Root-Core committed Dec 17, 2024
1 parent 813c064 commit 9b7aa49
Show file tree
Hide file tree
Showing 28 changed files with 429 additions and 390 deletions.
34 changes: 24 additions & 10 deletions .github/scripts/steam_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,60 @@
from steam.utils.proto import proto_to_dict
from steam.core.connection import WebsocketConnection

class Steam: # noqa: D101
def __init__(self) -> None: # noqa: D107
class Steam:
"""Minimal implementation of the SteamClient package that allows app id validation"""

def __init__(self) -> None:
"""Setup SteamClient and it's events
Raises:
ValueError: When the SteamClient fires it's "error" event
"""
self.logged_on_once = False

self.steam = client = SteamClient()
client.connection = WebsocketConnection()

@client.on('error')
# FIXME: pyright outputs 'error: Object of type "None" cannot be called (reportOptionalCall)'
@client.on(SteamClient.EVENT_ERROR) # pyright: ignore (reportOptionalCall)
def handle_error(result: EResult) -> None:
raise ValueError(f'Steam error: {repr(result)}')

@client.on('connected')
@client.on(SteamClient.EVENT_CONNECTED) # pyright: ignore (reportOptionalCall)
def handle_connected() -> None:
print(f'Connected to {client.current_server_addr}', file=sys.stderr)

@client.on('channel_secured')
@client.on(SteamClient.EVENT_CHANNEL_SECURED) # pyright: ignore (reportOptionalCall)
def send_login() -> None:
if self.logged_on_once and self.steam.relogin_available:
self.steam.relogin()

@client.on('disconnected')
@client.on(SteamClient.EVENT_DISCONNECTED) # pyright: ignore (reportOptionalCall)
def handle_disconnect() -> None:
print('Steam disconnected', file=sys.stderr)
if self.logged_on_once:
print('Reconnecting...', file=sys.stderr)
client.reconnect(maxdelay=30)

@client.on('logged_on')
@client.on(SteamClient.EVENT_LOGGED_ON) # pyright: ignore (reportOptionalCall)
def handle_after_logon() -> None:
self.logged_on_once = True

client.anonymous_login()


def get_valid_appids(self, appids: set[int]) -> set[int]:
"""Queries Steam for the specified appids.
If an appid doesn't exist, it won't be in the response.
Args:
appids (set[int]): The app ids that should be validated
Raises:
ValueError: When the response is empty / unexpected
Raises a ValueError if Steam returns unexpected data
"""
Returns:
set[int]: Only valid app ids will be returned
"""
# https://github.com/SteamRE/SteamKit/blob/master/SteamKit2/SteamKit2/Base/Generated/SteamMsgClientServerAppInfo.cs#L331
resp = self.steam.send_job_and_wait(
message = MsgProto(EMsg.ClientPICSProductInfoRequest),
Expand Down
13 changes: 5 additions & 8 deletions gamefixes-gog/umu-1209310984.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
directory and renamed to xaudio2_8.dll.
"""

import os
from gzip import open as gzip_open
from hashlib import sha256
from tempfile import mkdtemp
Expand All @@ -18,15 +17,14 @@
def main() -> None:
arc = 'https://github.com/user-attachments/files/16788423/xaudio2_8.dll.gz'
hashsum_file = '173cac0a7931989d66338e0d7779e451f2f01b2377903df7954d86c07c1bc8fb'

tmp = f'{mkdtemp()}/xaudio2_8.dll.gz'
hashsum = sha256()
path_dll = f'{util.get_game_install_path()}/xaudio2_8.dll'
path_dll = util.get_game_install_path() / 'xaudio2_8.dll'

# Full Metal Daemon from gog will not have the xaudio2_8.dll
if os.path.exists(path_dll):
log.info(
f"xaudio2_8.dll exists in '{util.get_game_install_path()}', skipping..."
)
if path_dll.is_file():
log.info(f"xaudio2_8.dll exists in '{path_dll.parent}', skipping...")
return

# Download the archive
Expand Down Expand Up @@ -54,5 +52,4 @@ def main() -> None:
# NOTE: The file is actually xaudio2_9.dll from winetricks but renamed
log.info("Applying fix for 'Full Metal Daemon Muramasa'...")
with gzip_open(tmp, 'rb') as reader:
with open(path_dll, 'wb') as writer:
writer.write(reader.read())
path_dll.write_bytes(reader.read())
29 changes: 12 additions & 17 deletions gamefixes-gog/umu-1564851593.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"""

import os

from hashlib import sha256
from tempfile import mkdtemp
from urllib.request import urlopen
from zipfile import ZipFile, is_zipfile
from pathlib import Path

from .. import util
from ..logger import log
Expand All @@ -23,44 +25,36 @@


def main() -> None:
tmp = f'{mkdtemp()}/d3d9-2206220222.zip'
tmp = Path(mkdtemp()) / 'd3d9-2206220222.zip'
install_dir = util.get_game_install_path()
path_config = f'{install_dir}/config.json'
path_dll = f'{install_dir}/d3d9.dll'
path_config = install_dir / 'config.json'
path_dll = install_dir / 'd3d9.dll'
hashsum = sha256()

# Ensure that the text injection files do not already exist before opening
if not os.path.isfile(path_config) or not os.path.isfile(path_dll):
if not path_config.is_file() or not path_dll.is_dir():
log.warn(
f"File 'config.json' or 'd3d9.dll' not found in '{install_dir}', skipping..."
)
return

config = open(path_config, mode='rb')
dll = open(path_dll, mode='rb')

# Check if the text injection framework files have already been replaced
if (
sha256(config.read()).hexdigest() == hashsum_config
and sha256(dll.read()).hexdigest() == hashsum_d3d9
sha256(path_config.read_bytes()).hexdigest() == hashsum_config
and sha256(path_dll.read_bytes()).hexdigest() == hashsum_d3d9
):
log.info(
"Fix for 'Flowers - Le Volume Sur Hiver' has already been applied, skipping..."
)
config.close()
dll.close()
return

config.close()
dll.close()

# Download the archive
with urlopen(arc, timeout=30) as resp:
if resp.status != 200:
log.warn(f'github returned the status code: {resp.status}')
return

with open(tmp, mode='wb', buffering=0) as file:
with tmp.open('wb', 0) as file:
chunk_size = 64 * 1024 # 64 KB
buffer = bytearray(chunk_size)
view = memoryview(buffer)
Expand All @@ -81,9 +75,10 @@ def main() -> None:
# Rename the old files and apply the fix
randstr = os.urandom(16).hex()
log.info(f"Renaming 'config.json' -> '.{randstr}.config.json.bak'")
path_config.rename(install_dir / f'.{randstr}.config.json.bak')

log.info(f"Renaming 'd3d9.dll' -> '.{randstr}.d3d9.dll.bak'")
os.rename(path_config, f'{install_dir}/.{randstr}.config.json.bak')
os.rename(path_dll, f'{install_dir}/.{randstr}.d3d9.dll.bak')
path_dll.rename(install_dir / f'.{randstr}.d3d9.dll.bak')

with ZipFile(tmp, mode='r') as zipfile:
log.info("Fixing in-game font for 'Flowers - Le Volume Sur Hiver'...")
Expand Down
19 changes: 9 additions & 10 deletions gamefixes-steam/105400.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Game fix for Fable III"""

import os
import shutil

from .. import util
Expand All @@ -12,15 +11,15 @@ def main() -> None:
util.protontricks('xliveless')

# Remove Windows Live folder
dirpath = os.path.join(
util.protonprefix(),
'drive_c',
'Program Files',
'Common Files',
'Microsoft Shared',
'Windows Live',
dirpath = (
util.protonprefix() /
'drive_c'
'Program Files'
'Common Files'
'Microsoft Shared'
'Windows Live'
)
if os.path.exists(dirpath):
if dirpath.is_dir():
shutil.rmtree(dirpath)
else:
log(f"Path '{dirpath}' could not be found")
log.info(f"Path '{dirpath}' could not be found")
11 changes: 4 additions & 7 deletions gamefixes-steam/1237970.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
"""Game fix for Titanfall 2"""

import os
import subprocess
import glob
from .. import util


def main() -> None:
"""Allow -northstar option to work"""
# Define game directory
install_dir = glob.escape(util.get_game_install_path())
# Path of backup file
backup_file = util.get_game_install_path() / 'Titanfall2.exe.bak'

# Restore original titanfall2.exe if NorthstarLauncher.exe was previously symlinked
if os.path.isfile(install_dir + '/Titanfall2.exe.bak'):
subprocess.run(['mv', 'Titanfall2.exe.bak', 'Titanfall2.exe'], check=False)
if backup_file.is_file():
backup_file.rename(backup_file.with_suffix(''))
3 changes: 1 addition & 2 deletions gamefixes-steam/1245620.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Game fix for Elden Ring: Create the `DLC.bdt` and `DLC.bhd` files to work around the "Inappropriate activity detected" error for players that don't own the DLC"""

from pathlib import Path
from .. import util


def main() -> None:
game_dir = Path(util.get_game_install_path()) / 'Game'
game_dir = util.get_game_install_path() / 'Game'
# Create the DLC.bdt file if it doesn't already exist, which is known to fix Easy AntiCheat not working for players that don't own the DLC
# A blank file is enough to get multiplayer working
(game_dir / 'DLC.bdt').touch(exist_ok=True)
Expand Down
11 changes: 7 additions & 4 deletions gamefixes-steam/1449280.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"""Game fix for Ghostbusters: The Video Game Remastered (2019)"""

from pathlib import Path
from .. import util
from ..logger import log


def main() -> None:
# This directory is required to make the game settings persistent
# [source: https://www.pcgamingwiki.com/wiki/Ghostbusters:_The_Video_Game_Remastered#Game_settings_do_not_save]
save_dir = f'{util.protonprefix()}/drive_c/users/steamuser/Local Settings/Application Data/GHOSTBUSTERS'
save_dir = (
util.protonprefix()
/ 'drive_c/users/steamuser/Local Settings/'
'Application Data/GHOSTBUSTERS'
)

try:
Path(save_dir).mkdir(parents=True, exist_ok=True)
save_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
log(f"Not able to make the settings directory at '{save_dir}': {e}")
log.warn(f"Not able to make the settings directory at '{save_dir}': {e}")
61 changes: 37 additions & 24 deletions gamefixes-steam/1873170.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
"""Cease to Breathe
Replace included nwjs(0.71) wich doesn't work with 0.86
Replace included nwjs (0.71) - which doesn't work - with 0.86
Fix cursor hitbox (set frame=false in package.json)
Updated from 0.85 that didn't display custom cursors.
"""

import os
import glob
import shutil
import urllib.request
import zipfile
import subprocess
import hashlib

from .. import util
from ..logger import log
from pathlib import Path


def main() -> None:
util.replace_command('CTB.exe', 'nw.exe')
install_dir = glob.escape(util.get_game_install_path())
if not os.path.isfile(os.path.join(install_dir, 'nw.exe')):
url = 'https://dl.nwjs.io/v0.86.0/nwjs-v0.86.0-win-x64.zip'
hashsum = 'ed2681847162e0fa457dd55e293b6f331ccd58acedd934a98e7fe1406c26dd4f'
nwjs = os.path.basename(url)
urllib.request.urlretrieve(url, nwjs)
with open(nwjs, 'rb') as f:
nwjs_sum = hashlib.sha256(f.read()).hexdigest()
if hashsum == nwjs_sum:
with zipfile.ZipFile(nwjs, 'r') as zip_ref:
zip_ref.extractall(install_dir)
nwjs = os.path.join(install_dir, nwjs.rsplit('.', 1)[0])
shutil.copytree(nwjs, install_dir, dirs_exist_ok=True)
shutil.rmtree(nwjs)
else:
log(f"{nwjs} checksum doesn't match, fix not applied.")
subprocess.call(
[f'sed -i \'s/"frame": true/"frame": false/\' "{install_dir}/package.json"'],
shell=True,
)
install_dir = util.get_game_install_path()

install_nwjs(install_dir)
patch_package_json(install_dir)


def install_nwjs(install_dir: Path) -> None:
if (install_dir / 'nw.exe').is_file():
return

url = 'https://dl.nwjs.io/v0.86.0/nwjs-v0.86.0-win-x64.zip'
hashsum = 'ed2681847162e0fa457dd55e293b6f331ccd58acedd934a98e7fe1406c26dd4f'
nwjs = Path(Path(url).name)
urllib.request.urlretrieve(url, nwjs)

# Check digest
nwjs_sum = hashlib.sha256(nwjs.read_bytes()).hexdigest()
if hashsum != nwjs_sum:
log.warn(f"{nwjs} checksum doesn't match, fix not applied.")
return

# Install
with zipfile.ZipFile(nwjs, 'r') as zip_ref:
zip_ref.extractall(install_dir)
nwjs_dir = install_dir / nwjs.with_suffix('')
shutil.copytree(nwjs_dir, install_dir, dirs_exist_ok=True)
shutil.rmtree(nwjs_dir)


def patch_package_json(install_dir: Path) -> None:
json_file = install_dir / 'package.json'
json = json_file.read_text()
json = json.replace('"frame": true', '"frame": false', 1)
json_file.write_text(json)
17 changes: 4 additions & 13 deletions gamefixes-steam/22330.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import os

from dataclasses import dataclass
from .. import util

def main_with_id(game_id: str) -> None:
Expand All @@ -22,19 +21,11 @@ def main_with_id(game_id: str) -> None:

# Run script extender if it exists.
mapping = get_redirect_name(game_id)
if os.path.isfile(mapping.to_name):
util.replace_command(mapping.from_name, mapping.to_name)
if os.path.isfile(mapping.to_value):
util.replace_command(mapping.from_value, mapping.to_value)


@dataclass
class Redirect:
"""Used for replacements"""

from_name: str
to_name: str


def get_redirect_name(game_id: str) -> Redirect:
def get_redirect_name(game_id: str) -> util.ReplaceType:
"""Mapping for SteamID -> script extender replacements"""
mapping = {
'22380': ('FalloutNV.exe', 'nvse_loader.exe'), # Fallout New Vegas
Expand All @@ -45,4 +36,4 @@ def get_redirect_name(game_id: str) -> Redirect:
'489830': ('SkyrimSELauncher.exe', 'skse64_loader.exe'), # Skyrim SE
'1716740': ('Starfield.exe', 'sfse_loader.exe') # Starfield
}.get(game_id, ('', ''))
return Redirect(*mapping)
return util.ReplaceType(*mapping)
Loading

0 comments on commit 9b7aa49

Please sign in to comment.