Compare commits

..

No commits in common. "main" and "v1.03.2-2" have entirely different histories.

2 changed files with 75 additions and 168 deletions

View file

@ -5,66 +5,46 @@ A tool aimed at enhancing the experience when playing the game on linux through
## Warning ## Warning
**This tool is based on patching the game executable through hex-edits. However it is done in a safe and non-destructive way, that ensures the patched executable is never run with EAC enabled (unless explicity told to do so). Use at your own risk!** **This tool is based on patching the game executable through hex-edits. However it is done in a safe and non-destructive way, that ensures the patched executable is never run with EAC enabled. Use at your own risk!**
## Dependencies ## Dependencies
- Python >= 3.8 - Python >= 3.8.xx (lowest tested)
## Usage ## Usage
1. Copy the file `er-patcher` to the game directory. 1. Copy the file `er-patcher` to the game directory.
2. In steam, set the game launch options to `python er-patcher ARGS -- %command%` See [Features](#features) for available options. 2. In steam, set the game launch options to `./er-patcher ARGS -- %command%` See [Features](#features) for available options.
- Example: - Example: `./er-patcher --all --rate 30 --fix-camera -- %command%`
- Example using [MangoHud](https://github.com/flightlessmango/MangoHud) and wine fullscreen FSR: `./er-patcher --rate 144 -uvca -- env WINE_FULLSCREEN_FSR=1 MANGOHUD=1 MANGOHUD_CONFIG=histogram %command%`
`python er-patcher --all --rate 30 --disable-rune-loss -- %command%`
- Example using the [Seamless Co-op](https://www.nexusmods.com/eldenring/mods/510) mod:
`python er-patcher --all --executable ersc_launcher.exe -- %command%`
- Example using [MangoHud](https://github.com/flightlessmango/MangoHud) and wine fullscreen FSR:
`python er-patcher --rate 144 -uvca -- env WINE_FULLSCREEN_FSR=1 MANGOHUD=1 MANGOHUD_CONFIG=histogram %command%`
- Example for enabling HDR using gamescope on Linux (reported to work on Plasma 6.1):
`ENABLE_GAMESCOPE_WSI=1 DXVK_HDR=1 gamescope -W 3440 -H 1440 -f -r 165 --hdr-enabled -- python er-patcher --all --rate 165 -- %command%`
3. Launch the game through steam. `er-patcher` automatically launches a patched version of `eldenring.exe` with EAC disabled. 3. Launch the game through steam. `er-patcher` automatically launches a patched version of `eldenring.exe` with EAC disabled.
Note: There might be some distros (e.g. older Ubuntu releases) that launch python 2 instead of 3 when running `python`. In that case you'll need to replace `python` with `python3` in the launch option line.
## Features ## Features
| Argument | Description | | Argument | Description |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------- | | --------------------------------------- | ---------------------------------------------------------------------------- |
| `-r RATE` or `--rate RATE` | Set a custom framerate limit (default: 60). | | `-r RATE` or `--rate RATE` | Set a custom framerate limit (default: 60). |
| `-x EXE` or `--executable EXE` | The executable to launch, relative to the games folder.<br>Mutually exclusive with `--with-eac`. | | `--fix-camera` | Disable camera auto-rotation. |
| `--with-eac` | Run game with EAC (Use it at your own risk).<br>Mutually exclusive with `--executable`. | | `--all` | Enable all options except `--rate` and<br>gameplay changes like `--fix-camera`. |
| `--disable-rune-loss` | Disable losing runes upon death. | | `-u` or `--ultrawide` | Remove black bars. |
| `--all` | Enable all options except `--rate`, `--executable`, and<br>gameplay changes like `--disable-rune-loss`. | | `-v` or `--disable-vigniette` | Remove the vigniette overlay . |
| `-u` or `--ultrawide` | Remove black bars. | | `-c` or `--disable-ca` | Disable chromatic abberation. |
| `-v` or `--disable-vignette` | Remove the vignette overlay. | | `-a` or `--increase-animation-distance` | Fix low frame rate animations at screen<br>edges or for distant entities. |
| `-c` or `--disable-ca` | Disable chromatic abberation. | | `-s` or `--skip-intro` | Skip intro logos at game start. |
| `-a` or `--increase-animation-distance` | Fix low frame rate animations at screen<br>edges or for distant entities. | | `-f` or `--remove-60hz-fullscreen` | Remove the 60Hz limit in fullscreen<br>mode (not needed with proton). |
| `-s` or `--skip-intro` | Skip intro logos at game start. |
| `-f` or `--remove-60hz-fullscreen` | Remove the 60Hz limit in fullscreen<br>mode (not needed with proton). |
## Windows Support ## Windows Support
The patcher works just as well on windows. The following launch option line works in case you e.g. installed Python from Microsoft Store: The patcher works just as well on windows. The only difference is that you need to explicitly call python to run the script `er-patcher`. The following launch option line works in case you installed Python from Microsoft Store:
> `python er-patcher --rate 165 --all -- %command%` > `python er-patcher --rate 165 --all -- %command%`
Note: This spawns a python console which will close by itself after the game has finished running. If you find this annoying you can try using `pythonw` instead. In any case `python` needs to be in PATH for windows to find it. Note: This spawns a python console which will close by itself after the game has finished running. If you find this annoying you can try using `pythonw` instead. In any case `python` needs to be in PATH for windows to find it.
Note 2: Ensure Vertical Sync is turned off for Elden Ring in Nvidia Control Panel / AMD Radeon Software / Intel Graphics Command Center, otherwise the custom framerate limit feature won't work.
## How it works ## How it works
When the game is launched through steam, the tool creates a patched version of `eldenring.exe` in a temporary subdirectory while leaving the original intact. As long the flag `--with-eac` is not set, the tool modifies the steam launch command to launch the patched executable instead of `start_protected_game.exe`, thefore ensuring that the patched exe is never run with EAC enabled. After the game is closed, the patched executable is removed. When the game is launched through steam, the tool creates a patched version of `eldenring.exe` in a temporary subdirectory while leaving the original intact. The tool then modifies the steam launch command to launch the patched executable instead of `start_protected_game.exe`. This ensures that the patched exe is never run with EAC enabled. After the game is closed, the patched executable is removed.
## Credits ## Credits
@ -72,8 +52,7 @@ When the game is launched through steam, the tool creates a patched version of `
- frame time limit adjustment - frame time limit adjustment
- black bar removal - black bar removal
- [Flawless Widescreen](https://www.flawlesswidescreen.org) - [Flawless Widescreen](https://www.flawlesswidescreen.org)
- vignette and ca removal - vigniette and ca removal
- animation distance increase - animation distance increase
- [DarkSouls3RemoveIntroScreens](https://github.com/bladecoding/DarkSouls3RemoveIntroScreens): intro logo skip - [DarkSouls3RemoveIntroScreens](https://github.com/bladecoding/DarkSouls3RemoveIntroScreens): intro logo skip
- [EldenRingMods](https://github.com/techiew/EldenRingMods) + [EldenRingFpsUnlockAndMore](https://github.com/uberhalit/EldenRingFpsUnlockAndMore) - [EldenRingMods](https://github.com/techiew/EldenRingMods) + [EldenRingFpsUnlockAndMore](https://github.com/uberhalit/EldenRingFpsUnlockAndMore): camera fix
- disable rune loss

View file

@ -1,29 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import sys import sys
import subprocess import subprocess
import argparse import argparse
from pathlib import Path from pathlib import Path
import struct import struct
import re import re
from shutil import rmtree
import os
import time
def cleanup(game_dir_patched):
if game_dir_patched.exists():
eldenring_path = game_dir_patched / "eldenring.exe"
while eldenring_path.exists():
try:
os.remove(eldenring_path)
break
except PermissionError:
# eldenring.exe is still running, retry in 3 s
time.sleep(3)
except Exception as e:
print(f"er-patcher: could not delete {eldenring_path}: {e}")
break
rmtree(game_dir_patched)
if __name__ == "__main__": if __name__ == "__main__":
@ -32,141 +16,85 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Patch Elden Ring executable and launch it without EAC.") parser = argparse.ArgumentParser(description="Patch Elden Ring executable and launch it without EAC.")
parser.add_argument("-r", "--rate", type=int, default=60, help="Modify the frame rate limit (e.g. 30, 120, 165 or whatever).") parser.add_argument("-r", "--rate", type=int, default=60, help="Modify the frame rate limit (e.g. 30, 120, 165 or whatever).")
parser.add_argument("-x", "--executable", action='store', type=str, default="eldenring.exe", help="The executable to launch, relative to the games folder.") parser.add_argument("--fix-camera", action='store_true', help="Disable camera auto-rotation.")
parser.add_argument("--with-eac", action='store_true', help="Run game with EAC (Use at own your risk)") parser.add_argument("--all", action='store_true', help="Enable all options except rate adjustment and gamplay changes like `--fix-camera`.")
parser.add_argument("--disable-rune-loss", action='store_true', help="Disable losing runes upon death.")
parser.add_argument("--all", action='store_true', help="Enable all options except rate adjustment and gamplay changes like `--disable-rune-loss`.")
parser.add_argument("-u", "--ultrawide", action='store_true', help="Removes black bars when using a resolution with an aspect ratio other than 16:9.") parser.add_argument("-u", "--ultrawide", action='store_true', help="Removes black bars when using a resolution with an aspect ratio other than 16:9.")
parser.add_argument("-v", "--disable-vignette", action='store_true', help="Disables the vignette overlay.") parser.add_argument("-v", "--disable-vigniette", action='store_true', help="Disables the vigniette overlay.")
parser.add_argument("-c", "--disable-ca", action='store_true', help="Disables chromatic abberation.") parser.add_argument("-c", "--disable-ca", action='store_true', help="Disables chromatic abberation.")
parser.add_argument("-a", "--increase-animation-distance", action='store_true', help="Increase animation distance.") parser.add_argument("-a", "--increase-animation-distance", action='store_true', help="Increase animation distance.")
parser.add_argument("-s", "--skip-intro", action='store_true', help="Skip intro logos.") parser.add_argument("-s", "--skip-intro", action='store_true', help="Skip intro logos.")
parser.add_argument("-f", "--remove-60hz-fullscreen", action='store_true', help="Remove 60hz lock in fullscreen.") parser.add_argument("-f", "--remove-60hz-fullscreen", action='store_true', help="Remove 60hz lock in fullscreen.")
patch = parser.parse_args(patcher_args) patch = parser.parse_args(patcher_args)
if patch.with_eac and patch.executable != "eldenring.exe": exe_name = Path("eldenring.exe")
print("er-patcher: --with-eac is mutually exclusive with --executable") with open(exe_name, "rb") as f:
sys.exit(1)
game_dir = Path(".")
with open(game_dir / "eldenring.exe", "rb") as f:
exe_hex = f.read().hex() exe_hex = f.read().hex()
if patch.rate != 60 and patch.rate > 0: if patch.rate != 60 and patch.rate > 0:
r_pattern = "c7 43 1c 89 88 88 3c eb 6d 89 73 18 eb c7 89 73 18".replace(" ", "") exe_hex = exe_hex.replace(
if (res := re.search(r_pattern, exe_hex)) is not None: "c743208988883ceb43897318ebca897318",
r_addr = res.span()[0] + 6 "c743208988883ceb43897318ebca897318".replace(
r_patch = struct.pack('<f', 1 / patch.rate).hex() "8988883c", struct.pack('<f', 1 / patch.rate).hex()
exe_hex = exe_hex[:r_addr] + r_patch + exe_hex[r_addr + len(r_patch):] )
else: )
print("er-patcher: rate pattern scan failed")
if patch.disable_rune_loss: if patch.fix_camera:
rl_pattern = "41 .. 01 48 .. .. e8 .. .. .. .. 48 .. .. .. .. 32 c0".replace(" ", "") cf_pattern = '0f 29 a6 .. .. .. .. 41 0f 28 cf'.replace(" ", "")
if (res := re.search(rl_pattern, exe_hex)) is not None: cf_addr = re.search(cf_pattern, exe_hex).span()[0]
rl_addr = res.span()[0] + 12 cf_offset = 0
rl_patch = "90 90 90 90 90".replace(" ", "") # NOP cf_patch = "90 90 90 90 90 90 90".replace(" ", "")
exe_hex = exe_hex[:rl_addr] + rl_patch + exe_hex[rl_addr + len(rl_patch):] exe_hex = exe_hex[:cf_addr + cf_offset] + cf_patch + exe_hex[cf_addr + cf_offset + len(cf_patch):]
else:
print("er-patcher: disable rune loss pattern scan failed")
if patch.ultrawide or patch.all: if patch.ultrawide or patch.all:
uw_pattern = "74 4f 45 8b 94 cc".replace(" ", "") exe_hex = exe_hex.replace(
if (res := re.search(uw_pattern, exe_hex)) is not None: "8b0185c07442448b5904",
uw_addr = res.span()[0] "8b0185c0eb42448b5904"
uw_patch = "eb" )
exe_hex = exe_hex[:uw_addr] + uw_patch + exe_hex[uw_addr + len(uw_patch):]
else:
print("er-patcher: ultrawide pattern scan failed")
if patch.disable_vignette or patch.all: if patch.disable_vigniette or patch.all:
v_pattern = 'f3 0f 10 .. .. f3 0f 59 .. .. .. .. .. e8 .. .. .. .. f3 41 0f .. .. f3 45 0f .. .. 4c 8d .. .. .. .. .. .. 48'.replace(" ", "") v_pattern = 'f3 0f 10 .. .. f3 0f 59 .. .. .. .. .. e8 .. .. .. .. f3 41 0f .. .. f3 45 0f .. .. 4c 8d .. .. .. .. .. .. 48'.replace(" ", "")
if (res := re.search(v_pattern, exe_hex)) is not None: v_addr = re.search(v_pattern, exe_hex).span()[0]
v_addr = res.span()[0] + 46 v_offset = 46
v_patch = "f3 0f 5c c0 90".replace(" ", "") # SUBSS XMM0,XMM0; NOP; all NOP does work too v_patch = "f3 0f 5c c0 90".replace(" ", "") # SUBSS XMM0,XMM0; NOP; all NOP does work too
exe_hex = exe_hex[:v_addr] + v_patch + exe_hex[v_addr + len(v_patch):] exe_hex = exe_hex[:v_addr + v_offset] + v_patch + exe_hex[v_addr + v_offset + len(v_patch):]
else:
print("er-patcher: disable_vignette pattern scan failed")
if patch.disable_ca or patch.all: if patch.disable_ca or patch.all:
ca_pattern = "0f 11 43 60 48 8d 8b 80 00 00 00 0f 10 87 a0 00 00 00 0f 11 41 f0 48 8d 87 b0 00 00 00 0f 10 08 0f 11 09".replace(" ", "") ca_addr = 94 + exe_hex.index("0f114360488d8b800000000f1087a00000000f1141f0488d87b00000000f10080f1109")
if (res := re.search(ca_pattern, exe_hex)) is not None: if exe_hex[ca_addr:ca_addr + 8] == "0f114920":
ca_addr = res.span()[0] + 94 exe_hex = exe_hex[:ca_addr] + "660fefc9" + exe_hex[ca_addr + 8:] # PXOR XMM1,XMM1
ca_orig = "0f 11 49 20".replace(" ", "")
ca_patch = "66 0f ef c9".replace(" ", "") # PXOR XMM1,XMM1
if exe_hex[ca_addr:ca_addr + len(ca_patch)] == ca_orig:
exe_hex = exe_hex[:ca_addr] + ca_patch + exe_hex[ca_addr + len(ca_patch):]
else:
print("er-patcher: disable_ca pattern scan failed")
if patch.increase_animation_distance or patch.all: if patch.increase_animation_distance or patch.all:
iad_pattern = "e8 .. .. .. .. 0f 28 .. 0f 28 .. e8 .. .. .. .. f3 0f .. .. 0f 28 .. f3 41 0f 5e 4c 24 54".replace(" ", "") # DIVSS XMM1,dword ptr [R12 + 0x54] -> XORPS XMM1,XMM1; PXOR XMM1,XMM1
if (res := re.search(iad_pattern, exe_hex)) is not None: exe_hex = exe_hex.replace(
iad_addr = res.span()[0] + 46 "e82b309c010f28f80f28c6e820359c01f30f5ef80f28cff3410f5e4c2454",
iad_patch = "0f 57 c9 66 0f ef c9".replace(" ", "") # DIVSS XMM1,dword ptr [R12 + 0x54] -> XORPS XMM1,XMM1; PXOR XMM1,XMM1 "e82b309c010f28f80f28c6e820359c01f30f5ef80f28cf0f57c9660fefc9"
exe_hex = exe_hex[:iad_addr] + iad_patch + exe_hex[iad_addr + len(iad_patch):] )
else:
print("er-patcher: increase_animation_distance pattern scan failed")
if patch.skip_intro or patch.all: if patch.skip_intro or patch.all:
si_pattern = "80 bf b8 00 00 00 00 74 53 48".replace(" ", "") exe_hex = exe_hex.replace(
if (res := re.search(si_pattern, exe_hex)) is not None: "80 bf b8 00 00 00 00 74 53 48".replace(" ", ""),
si_addr = res.span()[0] + 14 "80 bf b8 00 00 00 00 90 90 48".replace(" ", "")
si_patch = "90 90".replace(" ", "") )
exe_hex = exe_hex[:si_addr] + si_patch + exe_hex[si_addr + len(si_patch):]
else:
print("er-patcher: skip_intro pattern scan failed")
if patch.remove_60hz_fullscreen or patch.all: if patch.remove_60hz_fullscreen or patch.all:
fs_pattern = "eb .. c7 .. .. 3c 00 00 00 c7 .. .. 01 00 00 00".replace(" ", "") exe_hex = exe_hex.replace(
if (res := re.search(fs_pattern, exe_hex)) is not None: "c745ef3c000000",
fs_addr = res.span()[0] + 10 "c745ef00000000"
fs_patch = "00" )
exe_hex = exe_hex[:fs_addr] + fs_patch + exe_hex[fs_addr + len(fs_patch):]
fs_addr_2 = res.span()[0] + 24 patched_exe_dir = Path("./er-patcher-tmp")
fs_patch_2 = "00" if not patched_exe_dir.is_dir():
exe_hex = exe_hex[:fs_addr_2] + fs_patch_2 + exe_hex[fs_addr_2 + len(fs_patch_2):] patched_exe_dir.mkdir()
else:
print("er-patcher: remove_60hz_fullscreen pattern scan failed")
game_dir_patched = Path("er-patcher-tmp") with open(patched_exe_dir / exe_name, "wb") as f:
# make sure a fresh directory is used
cleanup(game_dir_patched)
if not game_dir_patched.is_dir():
game_dir_patched.mkdir()
with open(game_dir_patched / "eldenring.exe", "wb") as f:
f.write(bytes.fromhex(exe_hex)) f.write(bytes.fromhex(exe_hex))
del exe_hex del exe_hex
# recreate game directory tree in game_dir_patched
game_dirs = [d for d in game_dir.rglob("*") if d.is_dir()]
for d in game_dirs:
if d == game_dir_patched:
continue
if not (game_dir_patched / d).is_dir():
(game_dir_patched / d).mkdir(parents=True)
# hard link game files to game_dir_patched; symbolic links would be easier
# to handle but by default windows 10 doesn't allow them
game_files = [f for f in game_dir.rglob("*") if f.is_file()]
for f in game_files:
if f.name in ["eldenring.exe", "er-patcher"]:
continue
if not (game_dir_patched / f).is_file():
if sys.version_info.minor >= 10:
(game_dir_patched / f).hardlink_to(f)
else:
f.link_to(game_dir_patched / f)
# start patched exe directly to avoid EAC # start patched exe directly to avoid EAC
steam_cmd = sys.argv[1 + sys.argv.index("--"):] steam_cmd = sys.argv[1 + sys.argv.index("--"):]
steam_cmd[-1] = Path(steam_cmd[-1]).parent.absolute() / game_dir_patched / ("start_protected_game.exe" if patch.with_eac else patch.executable) steam_cmd[-1] = Path(steam_cmd[-1]).parent.absolute() / patched_exe_dir / exe_name
subprocess.run(steam_cmd, cwd=steam_cmd[-1].parent.absolute()) subprocess.run(steam_cmd)
# try to remove game_dir_patched os.remove(patched_exe_dir / exe_name)
cleanup(game_dir_patched) os.rmdir(patched_exe_dir)