Improvements to try resolve the issues with login checksums.

This commit is contained in:
2026-08-28 22:54:46 +02:00
parent 06672589ad
commit 1e1f76a000
10 changed files with 740 additions and 33 deletions

View File

@@ -25,6 +25,13 @@ from .runtime import (
)
from .settings import Settings
from ptdiag.logs import (
LogFileState,
newest_current_native_log,
parse_current_run_log,
snapshot_native_logs,
)
RELAY_REGISTRY_KEY = r"HKCU\Software\Wine\Debug"
RELAY_REGISTRY_VALUE = "RelayInclude"
@@ -33,7 +40,7 @@ GAME_DLL_LOGIN_COMPARE_RETURN_OFFSET = 0x79F157
DEFAULT_TRACE_TIMEOUT_SECONDS = 10 * 60.0
_LOAD_GAME_DLL = re.compile(
r'Loaded L"[^"\r\n]*(?:\\|/)game\.dll" at (?P<base>[0-9a-f]+):',
r'Loaded L"[^"\r\n]*[\\/]game\.dll" at (?P<base>[0-9a-f]+):',
re.IGNORECASE,
)
_COMPARE_RETURN = re.compile(
@@ -78,6 +85,21 @@ class LoginTraceReport:
native_log_before_modified_at: str | None
native_log_after: str | None
native_log_after_modified_at: str | None
current_game_log: str | None
current_game_log_modified_at: str | None
failed_file_count: int
failed_import_scenes: tuple[str, ...]
load_texture_to_material_failed: bool
import_scene_threw: bool
stage_without_new_stage: bool
module_code_sync_error: bool
module_code_sync_left: int | None
module_code_sync_right: int | None
module_code_sync_delta: int | None
banned_code_1: bool
runtime_futex_error: bool
gstreamer_wrong_elf_class: bool
gstreamer_stream_error: bool
game_returncode: int | None
game_timed_out: bool
game_duration_seconds: float
@@ -121,22 +143,41 @@ def run_login_trace(
raise RuntimeError("the selected Proton 7 runtime path is incomplete")
started = datetime.now().astimezone()
started_ns = int(started.timestamp() * 1_000_000_000)
result_directory = _create_result_directory(paths, started)
game_exe = paths.runtime_view / "Game.exe"
game_dll = paths.runtime_view / "game.dll"
log_before, log_before_modified = _native_log_snapshot(paths, settings)
native_log_directory = paths.game_save / "logs"
native_log_baseline = snapshot_native_logs(
native_log_directory,
patterns=("log_*.txt",),
)
registry_before = _query_relay_filter(paths, selection, runner)
registry_restored = False
trace = RelayTraceResult(None, None, (), 0)
outcome: CommandOutcome | None = None
raw_directory_path: Path | None = None
current_log_path = None
current_log_modified_at = None
current_log_failure_count = 0
current_failed_import_scenes: list[str] = []
current_load_texture_to_material_failed = False
current_import_scene_threw = False
current_stage_without_new_stage = False
current_module_code_sync_error = False
current_module_code_sync_left = None
current_module_code_sync_right = None
current_module_code_sync_delta = None
current_banned_code_1 = False
paths.cache.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
prefix="ptlinux-login-relay-",
dir=paths.cache,
) as raw_directory:
raw_directory_path = Path(raw_directory)
raw_path = Path(raw_directory)
raw_directory_path = raw_path
try:
_set_relay_filter(paths, selection, runner, RELAY_INCLUDE_FILTER)
outcome = run_game(
@@ -150,14 +191,14 @@ def run_login_trace(
check=False,
extra_environment={
"PROTON_LOG": "1",
"PROTON_LOG_DIR": str(raw_directory_path),
"PROTON_LOG_DIR": str(raw_path),
"SteamAppId": "0",
"SteamGameId": "0",
"WINEDEBUG": "+relay,+loaddll",
},
output_line_filter=redact_relay_output_line,
)
trace = parse_relay_logs(raw_directory_path.rglob("*.log"))
trace = parse_relay_logs(raw_path.rglob("*.log"))
finally:
registry_restored = _restore_relay_filter(
paths,
@@ -168,6 +209,52 @@ def run_login_trace(
if outcome is None:
raise RuntimeError("the instrumented game process did not start")
current_game_log = _current_game_log(
native_log_directory,
native_log_baseline,
started_ns,
)
if current_game_log is not None:
current_log_path = _string_or_none(current_game_log.get("path"))
current_log_modified_at = _string_or_none(
current_game_log.get("modified_at")
)
current_log_failure_count = _integer_or_zero(
current_game_log.get("failed_file_count")
)
failed_scenes = current_game_log.get("failed_import_scenes")
if isinstance(failed_scenes, (list, tuple)):
current_failed_import_scenes = [
scene for scene in failed_scenes if isinstance(scene, str)
]
current_load_texture_to_material_failed = bool(
current_game_log.get("load_texture_to_material_failed")
)
current_import_scene_threw = bool(current_game_log.get("import_scene_threw"))
current_stage_without_new_stage = bool(
current_game_log.get("stage_without_new_stage")
)
current_module_code_sync_error = bool(
current_game_log.get("module_code_sync_error")
)
current_module_code_sync_left = _integer_or_none(
current_game_log.get("module_code_sync_left")
)
current_module_code_sync_right = _integer_or_none(
current_game_log.get("module_code_sync_right")
)
current_module_code_sync_delta = _integer_or_none(
current_game_log.get("module_code_sync_delta")
)
current_banned_code_1 = bool(current_game_log.get("banned_code_1"))
runtime_futex_error = "futex facility returned an unexpected error code" in (
outcome.stderr.casefold()
)
gstreamer_wrong_elf_class = (
"gstreamer-warning" in outcome.stderr.casefold()
and "wrong elf class" in outcome.stderr.casefold()
)
gstreamer_stream_error = "winegstreamer error" in outcome.stderr.casefold()
raw_relay_deleted = raw_directory_path is not None and not raw_directory_path.exists()
log_after, log_after_modified = _native_log_snapshot(paths, settings)
comparison_observed = bool(trace.comparison_results)
@@ -176,6 +263,9 @@ def run_login_trace(
)
conclusion, warnings = _conclusion(
trace,
module_code_sync_error=current_module_code_sync_error,
banned_code_1=current_banned_code_1,
runtime_futex_error=runtime_futex_error,
registry_restored=registry_restored,
raw_relay_deleted=raw_relay_deleted,
timed_out=outcome.timed_out,
@@ -200,6 +290,21 @@ def run_login_trace(
native_log_before_modified_at=log_before_modified,
native_log_after=log_after,
native_log_after_modified_at=log_after_modified,
current_game_log=current_log_path,
current_game_log_modified_at=current_log_modified_at,
failed_file_count=current_log_failure_count,
failed_import_scenes=tuple(current_failed_import_scenes),
load_texture_to_material_failed=current_load_texture_to_material_failed,
import_scene_threw=current_import_scene_threw,
stage_without_new_stage=current_stage_without_new_stage,
module_code_sync_error=current_module_code_sync_error,
module_code_sync_left=current_module_code_sync_left,
module_code_sync_right=current_module_code_sync_right,
module_code_sync_delta=current_module_code_sync_delta,
banned_code_1=current_banned_code_1,
runtime_futex_error=runtime_futex_error,
gstreamer_wrong_elf_class=gstreamer_wrong_elf_class,
gstreamer_stream_error=gstreamer_stream_error,
game_returncode=outcome.returncode,
game_timed_out=outcome.timed_out,
game_duration_seconds=round(outcome.duration_seconds, 3),
@@ -393,15 +498,67 @@ def _native_log_snapshot(
return str(path), modified
def _current_game_log(
directory: Path,
baseline: dict[str, LogFileState],
started_ns: int,
) -> dict[str, object] | None:
state = newest_current_native_log(
directory,
baseline,
started_ns,
patterns=("log_*.txt",),
)
if state is None:
return None
analysis = parse_current_run_log(
state,
baseline,
)
return {
"path": str(state.path),
"modified_at": datetime.fromtimestamp(
state.mtime_ns / 1_000_000_000
).astimezone().isoformat(),
"failed_file_count": len(analysis.failed_files),
"failed_import_scenes": tuple(analysis.failed_import_scenes),
"load_texture_to_material_failed": analysis.load_texture_to_material_failed,
"import_scene_threw": analysis.import_scene_threw,
"stage_without_new_stage": analysis.stage_without_new_stage,
"module_code_sync_error": analysis.module_code_sync_error,
"module_code_sync_left": analysis.module_code_sync_left,
"module_code_sync_right": analysis.module_code_sync_right,
"module_code_sync_delta": analysis.module_code_sync_delta,
"banned_code_1": analysis.banned_code_1,
}
def _conclusion(
trace: RelayTraceResult,
*,
module_code_sync_error: bool,
banned_code_1: bool,
runtime_futex_error: bool,
registry_restored: bool,
raw_relay_deleted: bool,
timed_out: bool,
) -> tuple[str, tuple[str, ...]]:
warnings: list[str] = []
if trace.comparison_results:
if module_code_sync_error:
conclusion = "module-code-sync-error"
warnings.append(
"The current-run native game log recorded a module-code integrity "
"mismatch. Do not retry this account with an instrumented or modified "
"game module."
)
elif banned_code_1:
conclusion = "banned-code-1"
warnings.append(
"The current-run native game log recorded Banned Code 1. Confirm the "
"same account with the unmodified publisher launcher before changing "
"the local runtime again."
)
elif trace.comparison_results:
if trace.comparison_results[-1]:
conclusion = "account-id-match"
else:
@@ -422,6 +579,12 @@ def _conclusion(
warnings.append("The relay log did not record the loaded game.dll base address.")
if timed_out:
warnings.append("The diagnostic reached its timeout and terminated the launcher process.")
if runtime_futex_error:
warnings.append(
"A host-side process emitted glibc's fatal futex message. This is not a "
"game module-code result; compare the same prefix with direct Proton 7 "
"outside UMU before attributing the login failure to authentication."
)
if not registry_restored:
warnings.append("The temporary Wine relay registry value could not be restored.")
if not raw_relay_deleted:
@@ -445,6 +608,13 @@ def _write_report(report: LoginTraceReport) -> None:
"# PristonTale EU login trace\n\n"
f"- Started: {report.started_at}\n"
f"- Runtime: {report.runtime_kind}\n"
f"- Game.exe SHA-256: `{report.game_exe_sha256 or 'missing'}`\n"
f"- game.dll SHA-256: `{report.game_dll_sha256 or 'missing'}`\n"
f"- Current native log: {report.current_game_log or 'not found'}\n"
f"- Module-code mismatch: {report.module_code_sync_error}\n"
f"- Banned Code 1 in native log: {report.banned_code_1}\n"
f"- Host futex fatal message: {report.runtime_futex_error}\n"
f"- GStreamer wrong-ELF warnings: {report.gstreamer_wrong_elf_class}\n"
f"- Account-ID comparison observed: {report.comparison_observed}\n"
f"- Exact account-ID match: {match}\n"
f"- Conclusion: `{report.conclusion}`\n"
@@ -455,6 +625,21 @@ def _write_report(report: LoginTraceReport) -> None:
)
def _string_or_none(value: object) -> str | None:
return value if isinstance(value, str) else None
def _integer_or_none(value: object) -> int | None:
if isinstance(value, bool) or not isinstance(value, int):
return None
return value
def _integer_or_zero(value: object) -> int:
result = _integer_or_none(value)
return result if result is not None else 0
def _sha256_if_file(path: Path) -> str | None:
try:
if not path.is_file():