Some minor changes to support arch distros.

This commit is contained in:
2026-08-28 20:14:35 +02:00
parent e515080171
commit d2c66b6316
5 changed files with 23 additions and 176 deletions

View File

@@ -121,6 +121,9 @@ ptlinux_resolve_dependency_packages()
arch:openssl) arch:openssl)
candidates=(openssl) candidates=(openssl)
;; ;;
arch:umu)
candidates=(umu-launcher)
;;
suse:python) suse:python)
candidates=(python3) candidates=(python3)
;; ;;
@@ -210,6 +213,7 @@ ptlinux_has_wine_stack()
ptlinux_detect_missing_dependencies() ptlinux_detect_missing_dependencies()
{ {
local python_bin="${PYTHON_BIN:-python3}" local python_bin="${PYTHON_BIN:-python3}"
local family="${1:-}"
PTLINUX_MISSING_DEPENDENCIES=() PTLINUX_MISSING_DEPENDENCIES=()
if ! command -v "$python_bin" >/dev/null 2>&1 if ! command -v "$python_bin" >/dev/null 2>&1
@@ -227,10 +231,6 @@ ptlinux_detect_missing_dependencies()
then then
PTLINUX_MISSING_DEPENDENCIES+=(winetricks) PTLINUX_MISSING_DEPENDENCIES+=(winetricks)
fi fi
if ! command -v gamescope >/dev/null 2>&1
then
PTLINUX_MISSING_DEPENDENCIES+=(gamescope)
fi
if ! command -v cabextract >/dev/null 2>&1 if ! command -v cabextract >/dev/null 2>&1
then then
PTLINUX_MISSING_DEPENDENCIES+=(cabextract) PTLINUX_MISSING_DEPENDENCIES+=(cabextract)
@@ -239,6 +239,10 @@ ptlinux_detect_missing_dependencies()
then then
PTLINUX_MISSING_DEPENDENCIES+=(openssl) PTLINUX_MISSING_DEPENDENCIES+=(openssl)
fi fi
if [[ "$family" == "arch" ]] && ! command -v umu-run >/dev/null 2>&1
then
PTLINUX_MISSING_DEPENDENCIES+=(umu)
fi
} }
ptlinux_select_privilege_command() ptlinux_select_privilege_command()
@@ -344,15 +348,14 @@ ptlinux_install_host_dependencies()
local package_line local package_line
local -a packages=() local -a packages=()
ptlinux_detect_missing_dependencies ptlinux_read_distribution
family="$(ptlinux_dependency_family "$PTLINUX_DISTRIBUTION_ID" "$PTLINUX_DISTRIBUTION_LIKE")"
ptlinux_detect_missing_dependencies "$family"
if (( ${#PTLINUX_MISSING_DEPENDENCIES[@]} == 0 )) if (( ${#PTLINUX_MISSING_DEPENDENCIES[@]} == 0 ))
then then
printf '%s\n' "All required host dependencies are already available." printf '%s\n' "All required host dependencies are already available."
return 0 return 0
fi fi
ptlinux_read_distribution
family="$(ptlinux_dependency_family "$PTLINUX_DISTRIBUTION_ID" "$PTLINUX_DISTRIBUTION_LIKE")"
if [[ "$family" == "unknown" ]] if [[ "$family" == "unknown" ]]
then then
printf 'Unsupported distribution: %s. Re-run with --skip-deps after installing: %s\n' \ printf 'Unsupported distribution: %s. Re-run with --skip-deps after installing: %s\n' \
@@ -374,7 +377,7 @@ ptlinux_install_host_dependencies()
printf '%s\n' "Installing missing host dependencies (authentication may be requested)." printf '%s\n' "Installing missing host dependencies (authentication may be requested)."
ptlinux_install_resolved_packages "$family" "${packages[@]}" ptlinux_install_resolved_packages "$family" "${packages[@]}"
hash -r hash -r
ptlinux_detect_missing_dependencies ptlinux_detect_missing_dependencies "$family"
if (( ${#PTLINUX_MISSING_DEPENDENCIES[@]} != 0 )) if (( ${#PTLINUX_MISSING_DEPENDENCIES[@]} != 0 ))
then then
printf 'Dependencies remain unavailable after package installation: %s\n' \ printf 'Dependencies remain unavailable after package installation: %s\n' \

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"] __all__ = ["AppPaths", "__version__"]
__version__ = "0.5.3" __version__ = "0.5.8"

View File

@@ -70,8 +70,8 @@ def inspect_host(environment: Mapping[str, str] | None = None) -> DoctorReport:
ToolStatus( ToolStatus(
"gamescope", "gamescope",
shutil.which("gamescope"), shutil.which("gamescope"),
True, False,
"borderless X11/Wayland game container", "optional manual game-container diagnostics",
), ),
ToolStatus( ToolStatus(
"cabextract", "cabextract",
@@ -147,22 +147,22 @@ def _python_tk_path() -> str | None:
def _package_hint(distribution: str) -> str | None: def _package_hint(distribution: str) -> str | None:
if distribution in {"ubuntu", "debian", "linuxmint", "pop"}: if distribution in {"ubuntu", "debian", "linuxmint", "pop"}:
return ( 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." "Cabextract, OpenSSL, and both 64- and 32-bit Vulkan drivers with APT."
) )
if distribution in {"fedora", "nobara"}: if distribution in {"fedora", "nobara"}:
return ( 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." "and the matching 32-bit Vulkan driver with DNF."
) )
if distribution in {"arch", "cachyos", "endeavouros", "manjaro"}: if distribution in {"arch", "cachyos", "endeavouros", "manjaro"}:
return ( return (
"Install python, tk, wine, winetricks, gamescope, cabextract, openssl, and " "Install python, tk, wine, winetricks, umu-launcher, cabextract, "
"the matching lib32 Vulkan driver with Pacman." "openssl, and the matching lib32 Vulkan driver with Pacman."
) )
if distribution in {"opensuse-tumbleweed", "opensuse-leap", "suse"}: if distribution in {"opensuse-tumbleweed", "opensuse-leap", "suse"}:
return ( return (
"Install Python/Tk, Wine, Winetricks, Gamescope, Cabextract, OpenSSL, and " "Install Python/Tk, Wine, Winetricks, Cabextract, OpenSSL, and "
"32-bit Vulkan support with Zypper." "32-bit Vulkan support with Zypper."
) )
return None return None

View File

@@ -92,10 +92,10 @@ def _validate(settings: Settings) -> None:
if settings.refresh_rate < 24 or settings.refresh_rate > 1000: if settings.refresh_rate < 24 or settings.refresh_rate > 1000:
raise ValueError("refresh_rate must be between 24 and 1000") raise ValueError("refresh_rate must be between 24 and 1000")
if settings.scaler not in {"auto", "integer", "fit", "fill", "stretch"}: 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"}: if settings.filter not in {"linear", "nearest", "fsr", "nis", "pixel"}:
raise ValueError(f"unsupported Gamescope filter: {settings.filter}") raise ValueError(f"unsupported legacy filter setting: {settings.filter}")
if settings.runtime not in {"auto", "proton7", "umu"}: if settings.runtime not in {"auto", "proton7", "proton7-container", "umu"}:
raise ValueError(f"unsupported runtime selection: {settings.runtime}") raise ValueError(f"unsupported runtime selection: {settings.runtime}")
if settings.proton_sync not in {"auto", "fsync", "esync"}: if settings.proton_sync not in {"auto", "fsync", "esync"}:
raise ValueError(f"unsupported Proton synchronization mode: {settings.proton_sync}") raise ValueError(f"unsupported Proton synchronization mode: {settings.proton_sync}")

View File

@@ -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()