Reworked some umu / proton 7 maximize window and keyboard issues.
This commit is contained in:
@@ -1095,6 +1095,7 @@ def _run_and_watch(
|
||||
detected_at_ns: int | None = None
|
||||
timed_out = False
|
||||
stop_reason = "process-exited"
|
||||
failure_reason: str | None = None
|
||||
with runner_log_path.open("a", encoding="utf-8") as runner_log:
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
@@ -1151,16 +1152,24 @@ def _run_and_watch(
|
||||
current_state = state
|
||||
current_analysis = analysis.to_summary()
|
||||
current_text = read_current_run_log_text(state, baseline)
|
||||
if analysis.load_texture_to_material_failed:
|
||||
if (
|
||||
analysis.load_texture_to_material_failed
|
||||
or analysis.asset_pack_health_failed
|
||||
):
|
||||
failure_state = state
|
||||
failure_trigger_mtime_ns = state.mtime_ns
|
||||
detected_at_ns = now_ns
|
||||
post_failure_deadline_ns = now_ns + int(
|
||||
testcase.stop_after_failure_seconds * 1_000_000_000
|
||||
)
|
||||
failure_reason = (
|
||||
"asset-pack-health"
|
||||
if analysis.asset_pack_health_failed
|
||||
else "load-texture-to-material"
|
||||
)
|
||||
break
|
||||
if post_failure_deadline_ns is not None and now_ns >= post_failure_deadline_ns:
|
||||
stop_reason = "load-texture-to-material"
|
||||
stop_reason = failure_reason or "load-texture-to-material"
|
||||
_stop_process_group(process)
|
||||
break
|
||||
if now_ns >= deadline_ns:
|
||||
@@ -1216,6 +1225,9 @@ def _run_and_watch(
|
||||
"load_texture_to_material_failed": analysis.get(
|
||||
"load_texture_to_material_failed", False
|
||||
),
|
||||
"asset_pack_health_failed": analysis.get(
|
||||
"asset_pack_health_failed", False
|
||||
),
|
||||
"import_scene_threw": analysis.get("import_scene_threw", False),
|
||||
}
|
||||
)
|
||||
@@ -1244,6 +1256,7 @@ def _run_and_watch(
|
||||
whispering_vale
|
||||
and not timed_out
|
||||
and not current_analysis.get("load_texture_to_material_failed", False)
|
||||
and not current_analysis.get("asset_pack_health_failed", False)
|
||||
and not current_analysis.get("import_scene_threw", False)
|
||||
and not current_analysis.get("failed_import_scenes", [])
|
||||
) if isinstance(current_analysis, Mapping) else False
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Iterable, Mapping
|
||||
|
||||
|
||||
DEFAULT_NATIVE_LOG_PATTERNS = ("log_*.txt", "*.log")
|
||||
_ASSET_MISSING_FILE_THRESHOLD = 200
|
||||
|
||||
_SCENE_RE = re.compile(
|
||||
r"\[S1]\s+after import\s+(?P<scene>[^:]+):\s*"
|
||||
@@ -72,6 +73,12 @@ class NativeLogAnalysis:
|
||||
scenes: dict[str, list[SceneImport]] = field(default_factory=dict)
|
||||
events: list[LogEvent] = field(default_factory=list)
|
||||
failed_files: list[LogEvent] = field(default_factory=list)
|
||||
failed_file_find_count: int = 0
|
||||
failed_file_load_count: int = 0
|
||||
failed_file_read_count: int = 0
|
||||
failed_asset_file_count: int = 0
|
||||
failed_texture_file_count: int = 0
|
||||
failed_effect_file_count: int = 0
|
||||
failed_import_scenes: list[str] = field(default_factory=list)
|
||||
load_texture_to_material_failed: bool = False
|
||||
import_scene_threw: bool = False
|
||||
@@ -83,6 +90,14 @@ class NativeLogAnalysis:
|
||||
module_code_sync_delta: int | None = None
|
||||
banned_code_1: bool = False
|
||||
|
||||
@property
|
||||
def asset_pack_health_failed(self) -> bool:
|
||||
return (
|
||||
self.failed_asset_file_count >= _ASSET_MISSING_FILE_THRESHOLD
|
||||
or self.failed_texture_file_count >= _ASSET_MISSING_FILE_THRESHOLD // 3
|
||||
or self.failed_effect_file_count >= max(25, _ASSET_MISSING_FILE_THRESHOLD // 8)
|
||||
)
|
||||
|
||||
def latest_scene(self, name: str) -> SceneImport | None:
|
||||
wanted = _scene_key(name)
|
||||
for scene_name, imports in self.scenes.items():
|
||||
@@ -98,6 +113,13 @@ class NativeLogAnalysis:
|
||||
},
|
||||
"load_texture_to_material_failed": self.load_texture_to_material_failed,
|
||||
"import_scene_threw": self.import_scene_threw,
|
||||
"failed_file_find_count": self.failed_file_find_count,
|
||||
"failed_file_load_count": self.failed_file_load_count,
|
||||
"failed_file_read_count": self.failed_file_read_count,
|
||||
"failed_asset_file_count": self.failed_asset_file_count,
|
||||
"failed_texture_file_count": self.failed_texture_file_count,
|
||||
"failed_effect_file_count": self.failed_effect_file_count,
|
||||
"asset_pack_health_failed": self.asset_pack_health_failed,
|
||||
"failed_import_scenes": self.failed_import_scenes,
|
||||
"failed_files": [event.to_dict() for event in self.failed_files],
|
||||
"stage_without_new_stage": self.stage_without_new_stage,
|
||||
@@ -291,6 +313,18 @@ def parse_native_log_lines(lines: Iterable[str]) -> NativeLogAnalysis:
|
||||
event = LogEvent(f"failed_{operation}_file", line_number, raw, timestamp, value)
|
||||
analysis.failed_files.append(event)
|
||||
analysis.events.append(event)
|
||||
if operation == "find":
|
||||
analysis.failed_file_find_count += 1
|
||||
elif operation == "load":
|
||||
analysis.failed_file_load_count += 1
|
||||
else:
|
||||
analysis.failed_file_read_count += 1
|
||||
if _is_game_asset_path(value):
|
||||
analysis.failed_asset_file_count += 1
|
||||
if _is_texture_path(value):
|
||||
analysis.failed_texture_file_count += 1
|
||||
if _is_effect_path(value):
|
||||
analysis.failed_effect_file_count += 1
|
||||
|
||||
if "Stage without new stage" in raw:
|
||||
analysis.stage_without_new_stage = True
|
||||
@@ -319,3 +353,61 @@ def _scene_key(value: str) -> str:
|
||||
def _timestamp(line: str) -> str | None:
|
||||
match = _TIMESTAMP_RE.match(line)
|
||||
return match.group("timestamp") if match is not None else None
|
||||
|
||||
|
||||
def _is_game_asset_path(path: str) -> bool:
|
||||
lower = path.casefold()
|
||||
if not lower:
|
||||
return False
|
||||
if lower.startswith("save/") or lower.startswith("save\\"):
|
||||
return False
|
||||
if lower.startswith(("game\\", "game/")):
|
||||
return True
|
||||
if lower.startswith(("field\\", "field/")):
|
||||
return True
|
||||
if lower.startswith(("image\\", "image/")):
|
||||
return True
|
||||
if lower.startswith(("effect\\", "effect/")):
|
||||
return True
|
||||
if lower.startswith(("startimage\\", "startimage/")):
|
||||
return True
|
||||
return (
|
||||
"\\effect\\" in lower
|
||||
or "\\image\\" in lower
|
||||
or "\\field\\" in lower
|
||||
or "\\weapons\\" in lower
|
||||
or "\\char\\" in lower
|
||||
or "\\sinimage\\" in lower
|
||||
or "\\skill\\" in lower
|
||||
or lower.endswith(".bmp")
|
||||
or lower.endswith(".tga")
|
||||
or lower.endswith(".png")
|
||||
or lower.endswith(".jpg")
|
||||
or lower.endswith(".jpeg")
|
||||
or lower.endswith(".gif")
|
||||
or lower.endswith(".ase")
|
||||
or lower.endswith(".part")
|
||||
)
|
||||
|
||||
|
||||
def _is_texture_path(path: str) -> bool:
|
||||
lower = path.casefold()
|
||||
return (
|
||||
lower.endswith(".bmp")
|
||||
or lower.endswith(".tga")
|
||||
or lower.endswith(".png")
|
||||
or lower.endswith(".jpg")
|
||||
or lower.endswith(".jpeg")
|
||||
or lower.endswith(".gif")
|
||||
or lower.endswith(".dds")
|
||||
)
|
||||
|
||||
|
||||
def _is_effect_path(path: str) -> bool:
|
||||
lower = path.casefold()
|
||||
return (
|
||||
"\\effect\\" in lower
|
||||
or lower.startswith("effect\\")
|
||||
or lower.startswith("effect/")
|
||||
or lower.endswith(".part")
|
||||
)
|
||||
|
||||
@@ -1046,6 +1046,7 @@ def _current_game_log(
|
||||
"size": state.size,
|
||||
"load_texture_to_material_failed": analysis.load_texture_to_material_failed,
|
||||
"import_scene_threw": analysis.import_scene_threw,
|
||||
"asset_pack_health_failed": analysis.asset_pack_health_failed,
|
||||
"failed_import_scenes": analysis.failed_import_scenes,
|
||||
"stage_without_new_stage": analysis.stage_without_new_stage,
|
||||
"module_code_sync_error": analysis.module_code_sync_error,
|
||||
@@ -1058,6 +1059,12 @@ def _current_game_log(
|
||||
"native-game-log-only; server-provided login-screen text is not "
|
||||
"written to this log"
|
||||
),
|
||||
"failed_file_find_count": analysis.failed_file_find_count,
|
||||
"failed_file_load_count": analysis.failed_file_load_count,
|
||||
"failed_file_read_count": analysis.failed_file_read_count,
|
||||
"failed_asset_file_count": analysis.failed_asset_file_count,
|
||||
"failed_texture_file_count": analysis.failed_texture_file_count,
|
||||
"failed_effect_file_count": analysis.failed_effect_file_count,
|
||||
"stage_without_new_stage_events": [
|
||||
event.to_dict()
|
||||
for event in analysis.events
|
||||
@@ -1162,6 +1169,17 @@ def _print_interrupted_game_log(report: dict[str, object]) -> None:
|
||||
"with publisher-pristine code in this launcher version.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["asset_pack_health_failed"]:
|
||||
print(
|
||||
"Detected a large number of missing game-asset files. This indicates a "
|
||||
"corrupted or incomplete installation that needs repair.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Run `pristontale-eu update --repair` and restart. If the issue "
|
||||
"reappears, re-download the game files with a clean install path.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif report["load_texture_to_material_failed"]:
|
||||
print(
|
||||
"Detected a texture/material failure while loading the game world.",
|
||||
|
||||
@@ -46,6 +46,7 @@ _X_SUBSTRUCTURE_REDIRECT_MASK = 1 << 20
|
||||
_X_WM_STATE_ICONIC = 3
|
||||
_NET_WM_STATE_REMOVE = 0
|
||||
_NET_WM_STATE_SOURCE_APPLICATION = 1
|
||||
WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS = 1.0
|
||||
_P_MIN_SIZE = 1 << 4
|
||||
_P_MAX_SIZE = 1 << 5
|
||||
_MOTIF_HINTS_FUNCTIONS = 1 << 0
|
||||
@@ -466,7 +467,20 @@ class _X11:
|
||||
and updated_normal_hints != normal_hints
|
||||
)
|
||||
motif_changed = motif_hints is not None and updated_motif_hints != motif_hints
|
||||
if not normal_changed and not motif_changed:
|
||||
allowed_actions = self._long_property(window, b"_NET_WM_ALLOWED_ACTIONS", 64)
|
||||
allowed_actions_with_scaling = self._allowed_actions_with_scaling(
|
||||
allowed_actions,
|
||||
)
|
||||
allowed_actions_changed = (
|
||||
allowed_actions is not None
|
||||
and allowed_actions_with_scaling is not None
|
||||
and allowed_actions_with_scaling != allowed_actions
|
||||
)
|
||||
if (
|
||||
not normal_changed
|
||||
and not motif_changed
|
||||
and not allowed_actions_changed
|
||||
):
|
||||
return not confirm or self.window_allows_scaling(window)
|
||||
|
||||
confirmed = False
|
||||
@@ -485,6 +499,13 @@ class _X11:
|
||||
b"_MOTIF_WM_HINTS",
|
||||
updated_motif_hints,
|
||||
)
|
||||
if allowed_actions_changed and allowed_actions_with_scaling is not None:
|
||||
self._replace_long_property(
|
||||
window,
|
||||
b"_NET_WM_ALLOWED_ACTIONS",
|
||||
b"_ATOM",
|
||||
allowed_actions_with_scaling,
|
||||
)
|
||||
if not confirm:
|
||||
self.library.XFlush(self.display)
|
||||
confirmed = True
|
||||
@@ -515,6 +536,16 @@ class _X11:
|
||||
)
|
||||
except Exception as exc:
|
||||
restore_errors.append(exc)
|
||||
if allowed_actions_changed and allowed_actions is not None:
|
||||
try:
|
||||
self._replace_long_property(
|
||||
window,
|
||||
b"_NET_WM_ALLOWED_ACTIONS",
|
||||
b"_ATOM",
|
||||
allowed_actions,
|
||||
)
|
||||
except Exception as exc:
|
||||
restore_errors.append(exc)
|
||||
try:
|
||||
self.library.XSync(self.display, 0)
|
||||
except Exception as exc:
|
||||
@@ -546,6 +577,36 @@ class _X11:
|
||||
vertical,
|
||||
}.issubset(actions)
|
||||
|
||||
def _allowed_actions_with_scaling(
|
||||
self,
|
||||
actions: tuple[int, ...] | None,
|
||||
) -> tuple[int, ...] | None:
|
||||
if actions is None:
|
||||
return None
|
||||
|
||||
resize = self._intern_atom(
|
||||
b"_NET_WM_ACTION_RESIZE",
|
||||
only_if_exists=True,
|
||||
)
|
||||
horizontal = self._intern_atom(
|
||||
b"_NET_WM_ACTION_MAXIMIZE_HORZ",
|
||||
only_if_exists=True,
|
||||
)
|
||||
vertical = self._intern_atom(
|
||||
b"_NET_WM_ACTION_MAXIMIZE_VERT",
|
||||
only_if_exists=True,
|
||||
)
|
||||
if resize == 0 or horizontal == 0 or vertical == 0:
|
||||
return actions
|
||||
|
||||
updated_actions = list(actions)
|
||||
known_actions = set(actions)
|
||||
for atom in (resize, horizontal, vertical):
|
||||
if atom not in known_actions:
|
||||
updated_actions.append(atom)
|
||||
known_actions.add(atom)
|
||||
return tuple(updated_actions)
|
||||
|
||||
def _wait_for_scaling_actions(self, window: int) -> bool:
|
||||
deadline = time.monotonic() + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS
|
||||
while not self.window_allows_scaling(window):
|
||||
@@ -595,6 +656,7 @@ class _X11:
|
||||
watched_atoms = {
|
||||
self._intern_atom(b"WM_NORMAL_HINTS", only_if_exists=False),
|
||||
self._intern_atom(b"_MOTIF_WM_HINTS", only_if_exists=False),
|
||||
self._intern_atom(b"_NET_WM_ALLOWED_ACTIONS", only_if_exists=False),
|
||||
}
|
||||
changed = False
|
||||
events_read = 0
|
||||
@@ -831,6 +893,17 @@ class _X11:
|
||||
)
|
||||
self.library.XSync(self.display, 0)
|
||||
|
||||
def window_is_focused(self, window: int) -> bool:
|
||||
focused = ctypes.c_ulong()
|
||||
revert_to = ctypes.c_int()
|
||||
if self.library.XGetInputFocus(
|
||||
self.display,
|
||||
ctypes.byref(focused),
|
||||
ctypes.byref(revert_to),
|
||||
) != 1:
|
||||
return False
|
||||
return int(focused.value) == window
|
||||
|
||||
def activate_window(self, window: int) -> bool:
|
||||
"""Ask the window manager to restore and activate one client."""
|
||||
|
||||
@@ -1111,6 +1184,12 @@ class _X11:
|
||||
ctypes.c_ulong,
|
||||
]
|
||||
self.library.XSetInputFocus.restype = ctypes.c_int
|
||||
self.library.XGetInputFocus.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(ctypes.c_ulong),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
]
|
||||
self.library.XGetInputFocus.restype = ctypes.c_int
|
||||
self.library.XSendEvent.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_ulong,
|
||||
@@ -1250,6 +1329,27 @@ def _focus_game_window(mapper: _X11, window: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _repair_game_window_focus(mapper: _X11, window: int) -> bool:
|
||||
try:
|
||||
if mapper.window_is_focused(window):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
mapper.activate_window(window)
|
||||
time.sleep(WINDOW_ACTIVATION_DELAY_SECONDS)
|
||||
mapper.focus_window(window)
|
||||
return mapper.window_is_focused(window)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"Warning: could not repair game-window focus ({exc}); continuing "
|
||||
"normally",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _enable_game_window_scaling(mapper: _X11, window: int) -> bool:
|
||||
"""Enable confirmed resize/maximize actions without risking game startup."""
|
||||
|
||||
@@ -1301,7 +1401,8 @@ def _maintain_game_window_scaling(
|
||||
return int(process.wait())
|
||||
|
||||
repair_failures = 0
|
||||
next_repair_at = 0.0
|
||||
next_scaling_repair_at = 0.0
|
||||
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
while process.poll() is None:
|
||||
try:
|
||||
hints_changed = mapper.wait_for_scaling_hint_change(window)
|
||||
@@ -1313,13 +1414,17 @@ def _maintain_game_window_scaling(
|
||||
flush=True,
|
||||
)
|
||||
break
|
||||
if not hints_changed:
|
||||
continue
|
||||
now = time.monotonic()
|
||||
if now < next_repair_at:
|
||||
time.sleep(next_repair_at - now)
|
||||
if now >= next_focus_repair_at:
|
||||
_repair_game_window_focus(mapper, window)
|
||||
next_focus_repair_at = time.monotonic() + WINDOW_FOCUS_REPAIR_INTERVAL_SECONDS
|
||||
if not hints_changed and now < next_scaling_repair_at:
|
||||
continue
|
||||
try:
|
||||
mapper.enable_window_scaling(window, confirm=False)
|
||||
mapper.enable_window_scaling(
|
||||
window,
|
||||
confirm=False,
|
||||
)
|
||||
except Exception as exc:
|
||||
repair_failures += 1
|
||||
print(
|
||||
@@ -1337,7 +1442,7 @@ def _maintain_game_window_scaling(
|
||||
break
|
||||
else:
|
||||
repair_failures = 0
|
||||
next_repair_at = time.monotonic() + WINDOW_SCALING_REPAIR_INTERVAL_SECONDS
|
||||
next_scaling_repair_at = time.monotonic() + WINDOW_SCALING_REPAIR_INTERVAL_SECONDS
|
||||
return int(process.wait())
|
||||
|
||||
|
||||
@@ -1517,7 +1622,7 @@ def run_game_window(
|
||||
game_window,
|
||||
borderless=borderless,
|
||||
)
|
||||
if startup_stable and not borderless:
|
||||
if not borderless:
|
||||
return _maintain_game_window_scaling(
|
||||
mapper,
|
||||
process,
|
||||
|
||||
Reference in New Issue
Block a user