Some minor changes to support arch distros.
This commit is contained in:
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
||||
|
||||
__all__ = ["AppPaths", "__version__"]
|
||||
|
||||
__version__ = "0.5.3"
|
||||
__version__ = "0.5.8"
|
||||
|
||||
@@ -70,8 +70,8 @@ def inspect_host(environment: Mapping[str, str] | None = None) -> DoctorReport:
|
||||
ToolStatus(
|
||||
"gamescope",
|
||||
shutil.which("gamescope"),
|
||||
True,
|
||||
"borderless X11/Wayland game container",
|
||||
False,
|
||||
"optional manual game-container diagnostics",
|
||||
),
|
||||
ToolStatus(
|
||||
"cabextract",
|
||||
@@ -147,22 +147,22 @@ def _python_tk_path() -> str | None:
|
||||
def _package_hint(distribution: str) -> str | None:
|
||||
if distribution in {"ubuntu", "debian", "linuxmint", "pop"}:
|
||||
return (
|
||||
"Install Python/Tk, Wine (64- and 32-bit), Winetricks, Gamescope, "
|
||||
"Install Python/Tk, Wine (64- and 32-bit), Winetricks, "
|
||||
"Cabextract, OpenSSL, and both 64- and 32-bit Vulkan drivers with APT."
|
||||
)
|
||||
if distribution in {"fedora", "nobara"}:
|
||||
return (
|
||||
"Install python3-tkinter, wine, winetricks, gamescope, cabextract, openssl, "
|
||||
"Install python3-tkinter, wine, winetricks, cabextract, openssl, "
|
||||
"and the matching 32-bit Vulkan driver with DNF."
|
||||
)
|
||||
if distribution in {"arch", "cachyos", "endeavouros", "manjaro"}:
|
||||
return (
|
||||
"Install python, tk, wine, winetricks, gamescope, cabextract, openssl, and "
|
||||
"the matching lib32 Vulkan driver with Pacman."
|
||||
"Install python, tk, wine, winetricks, umu-launcher, cabextract, "
|
||||
"openssl, and the matching lib32 Vulkan driver with Pacman."
|
||||
)
|
||||
if distribution in {"opensuse-tumbleweed", "opensuse-leap", "suse"}:
|
||||
return (
|
||||
"Install Python/Tk, Wine, Winetricks, Gamescope, Cabextract, OpenSSL, and "
|
||||
"Install Python/Tk, Wine, Winetricks, Cabextract, OpenSSL, and "
|
||||
"32-bit Vulkan support with Zypper."
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -92,10 +92,10 @@ def _validate(settings: Settings) -> None:
|
||||
if settings.refresh_rate < 24 or settings.refresh_rate > 1000:
|
||||
raise ValueError("refresh_rate must be between 24 and 1000")
|
||||
if settings.scaler not in {"auto", "integer", "fit", "fill", "stretch"}:
|
||||
raise ValueError(f"unsupported Gamescope scaler: {settings.scaler}")
|
||||
raise ValueError(f"unsupported legacy scaler setting: {settings.scaler}")
|
||||
if settings.filter not in {"linear", "nearest", "fsr", "nis", "pixel"}:
|
||||
raise ValueError(f"unsupported Gamescope filter: {settings.filter}")
|
||||
if settings.runtime not in {"auto", "proton7", "umu"}:
|
||||
raise ValueError(f"unsupported legacy filter setting: {settings.filter}")
|
||||
if settings.runtime not in {"auto", "proton7", "proton7-container", "umu"}:
|
||||
raise ValueError(f"unsupported runtime selection: {settings.runtime}")
|
||||
if settings.proton_sync not in {"auto", "fsync", "esync"}:
|
||||
raise ValueError(f"unsupported Proton synchronization mode: {settings.proton_sync}")
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
"""Build the verified V21 texture-reclaim runtime DLL from a local game DLL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
|
||||
|
||||
SOURCE_SHA256 = "4b4e5c28e2bfdaa8d36c8341fad27becfd03faf63aa8ea874f5d43c5b1961868"
|
||||
PATCHED_SHA256 = "0be11b06ccf9be747e32b195c8d5980062f446d7d3a0a662ebfe0e602a01c538"
|
||||
TEXT_VA = 0x10001000
|
||||
TEXT_FILE_OFFSET = 0x400
|
||||
ENTRY_VA = 0x107C0B40
|
||||
ENTRY_FILE_OFFSET = TEXT_FILE_OFFSET + (ENTRY_VA - TEXT_VA)
|
||||
ENTRY_ORIGINAL = bytes.fromhex("55 8b ec 6a ff")
|
||||
CAVE_VA = 0x11195CE0
|
||||
CAVE_FILE_OFFSET = TEXT_FILE_OFFSET + (CAVE_VA - TEXT_VA)
|
||||
TEXT_RAW_SIZE = 0x1194E00
|
||||
MAX_HOOK_SIZE = TEXT_FILE_OFFSET + TEXT_RAW_SIZE - CAVE_FILE_OFFSET
|
||||
HOOK = bytes.fromhex(
|
||||
"9c60e8490136ff85c0744a89c38d70088dbe505d010039fe743b80be58030000"
|
||||
"00742a8b068b4c242885c974083b81d41f000074188b4c242c85c974083b81d4"
|
||||
"1f000074085089d9e8a32036ff81c650040000ebc1619d5589e56affe904ae62"
|
||||
"ff"
|
||||
)
|
||||
|
||||
|
||||
class TextureReclaimPatchError(RuntimeError):
|
||||
"""Raised when the exact V21 binary profile cannot be patched safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TextureReclaimPatchResult:
|
||||
source: str
|
||||
destination: str
|
||||
source_sha256: str
|
||||
destination_sha256: str
|
||||
entry_va: str
|
||||
cave_va: str
|
||||
hook_size: int
|
||||
old_text_virtual_size: int
|
||||
new_text_virtual_size: int
|
||||
commands: tuple[tuple[str, ...], ...]
|
||||
created_at: str
|
||||
|
||||
|
||||
def build_texture_reclaim_patch(
|
||||
source: Path,
|
||||
destination: Path,
|
||||
) -> TextureReclaimPatchResult:
|
||||
"""Create the V21 patch in a separate runtime cache, never in the game tree."""
|
||||
source = source.expanduser().resolve(strict=True)
|
||||
if _sha256(source) != SOURCE_SHA256:
|
||||
raise TextureReclaimPatchError(
|
||||
"source game.dll does not match the verified V21 profile"
|
||||
)
|
||||
|
||||
hook = HOOK
|
||||
commands: list[tuple[str, ...]] = []
|
||||
if not hook or len(hook) > MAX_HOOK_SIZE:
|
||||
raise TextureReclaimPatchError(
|
||||
f"compiled hook size {len(hook)} exceeds the {MAX_HOOK_SIZE}-byte code cave"
|
||||
)
|
||||
|
||||
image = bytearray(source.read_bytes())
|
||||
if bytes(image[ENTRY_FILE_OFFSET : ENTRY_FILE_OFFSET + len(ENTRY_ORIGINAL)]) != ENTRY_ORIGINAL:
|
||||
raise TextureReclaimPatchError(
|
||||
"V21 map-loader entry bytes do not match the verified profile"
|
||||
)
|
||||
if any(image[CAVE_FILE_OFFSET : CAVE_FILE_OFFSET + len(hook)]):
|
||||
raise TextureReclaimPatchError("V21 code cave is not empty")
|
||||
|
||||
text_header = _find_pe_section(image, b".text")
|
||||
old_virtual_size, text_rva, raw_size, raw_offset = struct.unpack_from(
|
||||
"<IIII", image, text_header + 8
|
||||
)
|
||||
if (text_rva, raw_size, raw_offset) != (
|
||||
TEXT_VA - 0x10000000,
|
||||
TEXT_RAW_SIZE,
|
||||
TEXT_FILE_OFFSET,
|
||||
):
|
||||
raise TextureReclaimPatchError(
|
||||
"V21 executable section layout does not match the verified profile"
|
||||
)
|
||||
new_virtual_size = CAVE_VA - TEXT_VA + len(hook)
|
||||
if new_virtual_size > TEXT_RAW_SIZE:
|
||||
raise TextureReclaimPatchError("compiled hook extends beyond the executable section")
|
||||
if old_virtual_size > new_virtual_size:
|
||||
raise TextureReclaimPatchError(
|
||||
"verified executable section is already larger than the patch target"
|
||||
)
|
||||
|
||||
entry_displacement = CAVE_VA - (ENTRY_VA + 5)
|
||||
image[ENTRY_FILE_OFFSET : ENTRY_FILE_OFFSET + 5] = b"\xe9" + struct.pack(
|
||||
"<i", entry_displacement
|
||||
)
|
||||
image[CAVE_FILE_OFFSET : CAVE_FILE_OFFSET + len(hook)] = hook
|
||||
struct.pack_into("<I", image, text_header + 8, new_virtual_size)
|
||||
|
||||
destination = destination.expanduser().absolute()
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_name(f".{destination.name}.tmp")
|
||||
try:
|
||||
temporary.write_bytes(image)
|
||||
if _sha256(temporary) != PATCHED_SHA256:
|
||||
raise TextureReclaimPatchError(
|
||||
"built runtime patch does not match the verified V21 profile"
|
||||
)
|
||||
temporary.replace(destination)
|
||||
shutil.copystat(source, destination)
|
||||
except BaseException:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
return TextureReclaimPatchResult(
|
||||
source=str(source),
|
||||
destination=str(destination),
|
||||
source_sha256=SOURCE_SHA256,
|
||||
destination_sha256=PATCHED_SHA256,
|
||||
entry_va=f"0x{ENTRY_VA:08x}",
|
||||
cave_va=f"0x{CAVE_VA:08x}",
|
||||
hook_size=len(hook),
|
||||
old_text_virtual_size=old_virtual_size,
|
||||
new_text_virtual_size=new_virtual_size,
|
||||
commands=tuple(commands),
|
||||
created_at=datetime.now().astimezone().isoformat(),
|
||||
)
|
||||
|
||||
|
||||
def _find_pe_section(image: bytes | bytearray, section_name: bytes) -> int:
|
||||
if len(image) < 0x40:
|
||||
raise TextureReclaimPatchError("source is too small to be a PE image")
|
||||
pe_offset = struct.unpack_from("<I", image, 0x3C)[0]
|
||||
if image[pe_offset : pe_offset + 4] != b"PE\0\0":
|
||||
raise TextureReclaimPatchError("source does not have a valid PE signature")
|
||||
number_of_sections = struct.unpack_from("<H", image, pe_offset + 6)[0]
|
||||
optional_header_size = struct.unpack_from("<H", image, pe_offset + 20)[0]
|
||||
section_table = pe_offset + 24 + optional_header_size
|
||||
for index in range(number_of_sections):
|
||||
header = section_table + index * 40
|
||||
name = bytes(image[header : header + 8]).rstrip(b"\0")
|
||||
if name == section_name:
|
||||
return header
|
||||
raise TextureReclaimPatchError(f"PE section is missing: {section_name!r}")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
Reference in New Issue
Block a user