changes for clarity - no major changes in function.

remove PyInstaller stuff

use NOPs to fill in the end of `new_and_improved_bytes`

prevent corrupting the exe if the script is interrupted

I verified this works on my Steam copy of Trials: Rising on Linux
This commit is contained in:
Kyle Mohr committed 2026-09-26 11:29:53 -05:00
1 parent 75a2077b54
commit 3e615cd1a0
5 files changed
+69 -107

No files matched your search

+64 -21
View File
@@ -1,5 +1,7 @@
import sys
import getpass
import os
import tempfile
# Check if user specified a path
if len(sys.argv) > 1:
@@ -15,7 +17,9 @@ else:
# so we would end up patching many places that don't need to be patched,
# causing unintended functionality (likely) or else just causing the program to crash (extremely likely)
# instead, we specify *just* enough bytes such that the sequence only occurs in one place:
original_bytes = b'\x48\x89\x54\x24\x10\x55\x56\x57\x48\x81\xEC\xF0\x00\x00\x00\x48\xC7\x44\x24\x78'
original_bytes = (
b'\x48\x89\x54\x24\x10\x55\x56\x57\x48\x81\xEC\xF0\x00\x00\x00\x48\xC7\x44\x24\x78'
)
# translated (from Ghidra (used to disassemble this program)): 15a0b00dc(*)
# 14178b6c0 48 89 54 MOV qword ptr [RSP + 0x10],RDX
# 24 10
@@ -27,17 +31,28 @@ original_bytes = b'\x48\x89\x54\x24\x10\x55\x56\x57\x48\x81\xEC\xF0\x00\x00\x00\
# 14178b6cf 48 c7 44 MOV qword ptr [RSP + 0x78],-0x2
# 24 78 fe
# ff ff ff
new_and_improved_bytes = b'\xB0\x01\xC3\x24\x10\x55\x56\x57\x48\x81\xEC\xF0\x00\x00\x00\x48\xC7\x44\x24\x78'
new_and_improved_bytes = (
b'\xB0\x01\xC3\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90'
)
# Translated:
# mov al, 1
# ret
# (then the rest of these bytes don't matter because they will never be reached)
# or in pseudocode, `return True;`
# (The rest of the bytes won't be reached, but they are there to ensure that the length of the new_and_improved_bytes is the same as the original_bytes, so that we don't accidentally shift any other bytes in the executable.)
# Older releases of this patcher wrote the original tail bytes back after `B0 01 C3` instead of
# NOP padding. Detect that form too, so people who already patched with an older version don't
# get a misleading "couldn't detect the patch" error.
old_patched_bytes = b'\xB0\x01\xC3\x24\x10\x55\x56\x57\x48\x81\xEC\xF0\x00\x00\x00\x48\xC7\x44\x24\x78'
# Sanity check: the replacement must be exactly the same length as the original so we never shift
# any other bytes in the executable. This is a hard requirement, so use sys.exit rather than assert
# (assert statements are stripped when Python runs with -O, which would silently remove this guard).
if len(original_bytes) != len(new_and_improved_bytes):
sys.exit("Internal error: original_bytes and new_and_improved_bytes differ in length. Refusing to patch.")
try:
with open(path, 'rb') as file:
# read the .exe file
data = file.read()
except FileNotFoundError:
print(f"Error: The file '{path}' was not found.")
@@ -45,21 +60,49 @@ except FileNotFoundError:
print(f"python {sys.argv[0]} /path/to/trialsrising.exe")
sys.exit(1)
# Perform the replacement, limited to 1 occurrence
patched_data = data.replace(original_bytes, new_and_improved_bytes, 1)
# Ensure we actually changed *something*
if patched_data != data:
# Write the modified data back to the file
with open(path, 'wb') as file:
file.write(patched_data)
print(f"Success: Byte sequence found and replaced in {path}.")
else:
if data.find(new_and_improved_bytes) >= 1:
# The signature is chosen to be unique to this one function. Enforce that at runtime: patching the
# wrong occurrence would corrupt the executable, so fail loudly instead of silently patching one.
matches = data.count(original_bytes)
if matches == 0:
# Nothing to patch. Either it's already patched, or we don't recognize this build.
if data.count(new_and_improved_bytes) or data.count(old_patched_bytes):
print("The patch had already been applied to your .exe, no further changes were needed.")
else:
print("The original bytes were not found in the binary, and we couldn't detect the patch either. \n\n \
Please try running the patch again with the full path to your .exe, like this:")
print("python patch.py /path/to/trialsrising.exe")
print("")
print("If that still doesn't work, open an issue to let me know: https://github.com/kylefmohr/Trials-Rising-Steam-Deck-Patch/issues")
sys.exit(0)
print("The original bytes were not found in the binary, and we couldn't detect the patch either. \n\n \
Please try running the patch again with the full path to your .exe, like this:")
print(f"{sys.argv[0]} /path/to/trialsrising.exe")
print("")
print("If that still doesn't work, open an issue to let me know: https://github.com/kylefmohr/Trials-Rising-Steam-Deck-Patch/issues")
sys.exit(1)
elif matches > 1:
sys.exit(
f"Error: expected exactly 1 match for the original bytes, but found {matches}. "
"Refusing to patch to avoid corrupting the executable. Please open an issue."
)
# Keep a one-time backup of the original so the patch can be reverted (for example, after a game update).
backup_path = path + '.bak'
if not os.path.exists(backup_path):
with open(backup_path, 'wb') as backup:
backup.write(data)
# Write atomically: `open(path, 'wb')` truncates the file immediately, so an interrupted write would
# leave an unusable large executable. Write to a temp file in the same directory, flush it to disk,
# then atomically replace the original.
patched_data = data.replace(original_bytes, new_and_improved_bytes, 1)
temp_path = None
try:
with tempfile.NamedTemporaryFile('wb', dir=os.path.dirname(os.path.abspath(path)), delete=False) as temp:
temp_path = temp.name
temp.write(patched_data)
temp.flush()
os.fsync(temp.fileno())
# NamedTemporaryFile creates the file as 0600, so copy the original file's permissions over.
os.chmod(temp_path, os.stat(path).st_mode)
os.replace(temp_path, path)
finally:
if temp_path is not None and os.path.exists(temp_path):
os.remove(temp_path)
print(f"Success: Byte sequence found and replaced in {path}.")
print(f"A backup of the original file was saved to {backup_path}.")