Fixed issues with encodings and wine / proton compatibilities.

This commit is contained in:
2026-08-30 00:08:41 +02:00
parent d1587b785a
commit bef8d04d72
13 changed files with 772 additions and 132 deletions

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
from dataclasses import asdict, dataclass, replace
from datetime import datetime
import hashlib
import json
@@ -14,6 +14,13 @@ import time
from typing import Callable, Protocol
from . import __version__
from .command import CommandRunner
from .identity_probe import (
IDENTITY_PROBE_PRIVACY,
IdentityProbeResult,
probe_client_identity,
)
from .login_trace import LOGIN_TRACE_DISABLED_REASON, LOGIN_TRACE_ENABLED
from .overlay import (
COMPATIBILITY_BINARY_HASHES,
CURRENT_V21_BINARY_HASHES,
@@ -43,6 +50,36 @@ DEFAULT_LOGIN_TIMEOUT_SECONDS = 5.0
VERIFIED_GAME_PROFILES = frozenset(
{"v21.75.83", "verified-compatibility"}
)
SERVER_LOGIN_RESPONSE_OWNERSHIP = (
"Banned Code 1 and Banned Code 3 are server-provided login response text; "
"neither string exists in the verified V21.75.83 Game.exe/game.dll pair"
)
CLIENT_IDENTITY_INPUTS = (
"Win32_Processor",
"Win32_BIOS",
"Win32_BaseBoard",
"Win32_VideoController",
"network-adapter identity",
"C: volume information",
"computer name",
)
PUBLISHER_COMPARISON_COMMAND = "pristontale-eu wine-update"
RUNTIME_COMPONENT_PRIVACY = (
"Only SHA-256 hashes of public runtime executables and DLLs plus expected DLL-"
"override flags are reported. Registry contents and machine identifiers are not "
"hashed or returned."
)
PREFIX_COMPONENT_PATHS = (
"drive_c/windows/syswow64/d3d9.dll",
"drive_c/windows/syswow64/d3dx9_43.dll",
"drive_c/windows/syswow64/d3dcompiler_43.dll",
"drive_c/windows/syswow64/iphlpapi.dll",
"drive_c/windows/syswow64/kernel32.dll",
"drive_c/windows/syswow64/ole32.dll",
"drive_c/windows/syswow64/oleaut32.dll",
"drive_c/windows/syswow64/wbemprox.dll",
"drive_c/windows/syswow64/wbem/wbemprox.dll",
)
class _Closable(Protocol):
@@ -65,6 +102,20 @@ class LoginServerProbe:
error: str | None
@dataclass(frozen=True)
class RuntimeComponentFingerprint:
privacy: str
runtime_wrapper_sha256: str | None
proton_script_sha256: str | None
proton_version_sha256: str | None
proton_toolmanifest_sha256: str | None
proton_wine_sha256: str | None
proton_wineserver_sha256: str | None
prefix_dll_sha256: dict[str, str | None]
registry_overrides: dict[str, bool]
runtime_marker_matches_selection: bool | None
@dataclass(frozen=True)
class LoginDiagnosticReport:
preflight_ok: bool
@@ -76,6 +127,16 @@ class LoginDiagnosticReport:
kernel_release: str
login_server: LoginServerProbe
authentication_tested: bool
login_response_ownership: str
client_identity_inputs: tuple[str, ...]
client_identity_values_collected: bool
client_identity_shape_collected: bool
client_identity_ready: bool | None
client_identity_privacy: str
client_identity_probe: IdentityProbeResult | None
login_trace_enabled: bool
login_trace_disabled_reason: str | None
publisher_comparison_command: str
install_directory: str
game_exe: str
game_exe_sha256: str | None
@@ -93,9 +154,13 @@ class LoginDiagnosticReport:
world_fix_helper_ready: bool
world_fix_enabled: bool
world_fix_disabled_reason: str | None
runtime_configured: str
runtime_requested: str
runtime_override_applied: bool
runtime_kind: str | None
runtime_path: str | None
runtime_error: str | None
runtime_components: RuntimeComponentFingerprint | None
proton_sync_configured: str
proton_sync_effective: str | None
prefix: str | None
@@ -148,6 +213,8 @@ def inspect_login_environment(
*,
timeout_seconds: float = DEFAULT_LOGIN_TIMEOUT_SECONDS,
connector: LoginConnector = _create_login_connection,
runner: CommandRunner | None = None,
runtime_override: str | None = None,
) -> LoginDiagnosticReport:
"""Inspect the installed client and its network route without authenticating."""
@@ -155,7 +222,9 @@ def inspect_login_environment(
timeout_seconds=timeout_seconds,
connector=connector,
)
selection, runtime_error = _runtime_selection(settings)
runtime_requested = runtime_override or settings.runtime
diagnostic_settings = replace(settings, runtime=runtime_requested)
selection, runtime_error = _runtime_selection(diagnostic_settings)
distribution_id = detect_distribution_id() or "unknown"
kernel_release = platform.release()
proton_sync_effective = (
@@ -192,6 +261,16 @@ def inspect_login_environment(
prefix = _runtime_prefix(paths, selection)
prefix_ready = _prefix_ready(prefix)
directx_ready = native_d3d_ready(prefix) if prefix is not None else False
runtime_components = (
inspect_runtime_components(paths, selection, prefix)
if selection is not None and prefix is not None
else None
)
identity_probe = (
probe_client_identity(paths, selection, runner)
if selection is not None and prefix_ready and runner is not None
else None
)
latest_log = latest_native_game_log(paths, settings)
warnings = _diagnostic_warnings(
probe,
@@ -201,6 +280,12 @@ def inspect_login_environment(
directx_ready,
latest_log,
runtime_matches_managed_source,
identity_probe,
)
identity_preflight_ok = (
True
if runner is None
else identity_probe is not None and identity_probe.ready is True
)
return LoginDiagnosticReport(
preflight_ok=(
@@ -210,6 +295,7 @@ def inspect_login_environment(
and selection is not None
and prefix_ready
and directx_ready
and identity_preflight_ok
),
checked_at=datetime.now().astimezone().isoformat(),
launcher_version=__version__,
@@ -219,6 +305,22 @@ def inspect_login_environment(
kernel_release=kernel_release,
login_server=probe,
authentication_tested=False,
login_response_ownership=SERVER_LOGIN_RESPONSE_OWNERSHIP,
client_identity_inputs=CLIENT_IDENTITY_INPUTS,
client_identity_values_collected=False,
client_identity_shape_collected=(
identity_probe is not None and identity_probe.report is not None
),
client_identity_ready=(
identity_probe.ready if identity_probe is not None else None
),
client_identity_privacy=IDENTITY_PROBE_PRIVACY,
client_identity_probe=identity_probe,
login_trace_enabled=LOGIN_TRACE_ENABLED,
login_trace_disabled_reason=(
None if LOGIN_TRACE_ENABLED else LOGIN_TRACE_DISABLED_REASON
),
publisher_comparison_command=PUBLISHER_COMPARISON_COMMAND,
install_directory=str(paths.data),
game_exe=str(game_exe),
game_exe_sha256=game_exe_sha256,
@@ -238,9 +340,13 @@ def inspect_login_environment(
world_fix_disabled_reason=(
None if WORLD_FIX_ENABLED else WORLD_FIX_DISABLED_REASON
),
runtime_configured=settings.runtime,
runtime_requested=runtime_requested,
runtime_override_applied=runtime_override is not None,
runtime_kind=selection.kind if selection is not None else None,
runtime_path=_runtime_path(selection),
runtime_error=runtime_error,
runtime_components=runtime_components,
proton_sync_configured=settings.proton_sync,
proton_sync_effective=proton_sync_effective,
prefix=str(prefix) if prefix is not None else None,
@@ -252,6 +358,85 @@ def inspect_login_environment(
)
def inspect_runtime_components(
paths: AppPaths,
selection: RuntimeSelection,
prefix: Path,
) -> RuntimeComponentFingerprint:
"""Fingerprint non-private runtime components for side-by-side reports."""
proton_script_sha256: str | None = None
proton_version_sha256: str | None = None
proton_toolmanifest_sha256: str | None = None
proton_wine_sha256: str | None = None
proton_wineserver_sha256: str | None = None
if selection.proton_path:
proton = Path(selection.proton_path)
proton_script_sha256 = _sha256_if_file(proton)
proton_version_sha256 = _sha256_if_file(proton.parent / "version")
proton_toolmanifest_sha256 = _sha256_if_file(
proton.parent / "toolmanifest.vdf"
)
for directory in (proton.parent / "files/bin", proton.parent / "dist/bin"):
wine = directory / "wine"
wineserver = directory / "wineserver"
if wine.is_file() and wineserver.is_file():
proton_wine_sha256 = _sha256_if_file(wine)
proton_wineserver_sha256 = _sha256_if_file(wineserver)
break
prefix_dll_sha256 = {
relative: _sha256_if_file(prefix / relative)
for relative in PREFIX_COMPONENT_PATHS
}
runtime_marker = _read_json_object(paths.game_runtime_marker)
marker_matches = (
runtime_marker.get("runtime") == asdict(selection)
if runtime_marker
else None
)
return RuntimeComponentFingerprint(
privacy=RUNTIME_COMPONENT_PRIVACY,
runtime_wrapper_sha256=_sha256_if_file(Path(selection.executable)),
proton_script_sha256=proton_script_sha256,
proton_version_sha256=proton_version_sha256,
proton_toolmanifest_sha256=proton_toolmanifest_sha256,
proton_wine_sha256=proton_wine_sha256,
proton_wineserver_sha256=proton_wineserver_sha256,
prefix_dll_sha256=prefix_dll_sha256,
registry_overrides=_runtime_registry_override_flags(prefix),
runtime_marker_matches_selection=marker_matches,
)
def _runtime_registry_override_flags(prefix: Path) -> dict[str, bool]:
try:
registry = (prefix / "user.reg").read_text(
encoding="utf-8",
errors="replace",
).casefold()
except OSError:
registry = ""
return {
"d3d9_native_builtin": '"d3d9"="native,builtin"' in registry,
"d3dx9_43_native": _native_override_present(registry, "d3dx9_43"),
"d3dcompiler_43_native": _native_override_present(
registry,
"d3dcompiler_43",
),
}
def _native_override_present(registry: str, name: str) -> bool:
return any(
entry in registry
for entry in (
f'"{name}"="native,builtin"',
f'"{name}"="native"',
f'"*{name}"="native"',
)
)
def classify_game_profile(
game_exe_sha256: str | None,
game_dll_sha256: str | None,
@@ -354,6 +539,7 @@ def _diagnostic_warnings(
directx_ready: bool,
latest_log: Path | None,
runtime_matches_managed_source: bool,
identity_probe: IdentityProbeResult | None,
) -> tuple[str, ...]:
warnings: list[str] = []
if not probe.reachable:
@@ -392,6 +578,8 @@ def _diagnostic_warnings(
warnings.append("The native DirectX helper DLLs are not ready; run play to repair them.")
if latest_log is None:
warnings.append("No native game log was found yet.")
if identity_probe is not None:
warnings.extend(identity_probe.warnings)
return tuple(warnings)