mirror of
https://github.com/yusufipk/deste-hearthstone-linux-tracker.git
synced 2026-09-11 10:46:12 +00:00
Reads the game's own Power.log: no screen capture, no image recognition, no memory reading. - deck tracking (remaining cards, draw chance, played cards fade in place) - opponent panel (revealed cards, hand and deck counts) - decks read from the game's offline cache, matched to the match - card art in rows, full card on hover - overlay and window modes, adjustable transparency, tray icon - English and Turkish interface - consistency tests over a real log corpus, cross-checked against Zone.log
162 lines
5.5 KiB
Python
162 lines
5.5 KiB
Python
"""Hearthstone kurulumunu, log dizinini ve log.config durumunu bulur.
|
||
|
||
Oyun Wine/Proton prefix'i içinde çalıştığı için yol sabit değil. Önce
|
||
yapılandırma dosyasına, sonra bilinen prefix konumlarına bakılır.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
LOG_DIR_RE = re.compile(r"^Hearthstone_\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}$")
|
||
|
||
# Gerekli log bileşenleri. Power için Verbose=1 şart, yoksa oyun durumu eksik gelir.
|
||
REQUIRED_COMPONENTS = {
|
||
"Power": {"LogLevel": "1", "FilePrinting": "true", "Verbose": "1"},
|
||
"Zone": {"LogLevel": "1", "FilePrinting": "true"},
|
||
"LoadingScreen": {"LogLevel": "1", "FilePrinting": "true"},
|
||
"Arena": {"LogLevel": "1", "FilePrinting": "true"},
|
||
}
|
||
|
||
# Prefix içindeki oyun dizini, kullanıcıya göre değişen kısımlar glob ile.
|
||
PREFIX_GLOBS = (
|
||
"~/Games/*/drive_c/Program Files (x86)/Hearthstone",
|
||
"~/Games/*/*/drive_c/Program Files (x86)/Hearthstone",
|
||
"~/.wine/drive_c/Program Files (x86)/Hearthstone",
|
||
"~/.local/share/lutris/prefixes/*/drive_c/Program Files (x86)/Hearthstone",
|
||
"~/.steam/steam/steamapps/compatdata/*/pfx/drive_c/Program Files (x86)/Hearthstone",
|
||
)
|
||
|
||
CONFIG_PATH = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser() / "deste" / "config.json"
|
||
|
||
|
||
@dataclass(slots=True)
|
||
class Installation:
|
||
game_dir: Path
|
||
logs_dir: Path
|
||
log_config: Path | None
|
||
client_config: Path
|
||
|
||
|
||
def _expand(pattern: str) -> list[Path]:
|
||
pattern = os.path.expanduser(pattern)
|
||
root = Path("/")
|
||
parts = Path(pattern).parts[1:]
|
||
try:
|
||
return sorted(root.glob(str(Path(*parts))))
|
||
except (OSError, ValueError):
|
||
return []
|
||
|
||
|
||
def find_game_dir(configured: str | None = None) -> Path | None:
|
||
"""Hearthstone kurulum dizinini bulur."""
|
||
if configured:
|
||
path = Path(configured).expanduser()
|
||
if (path / "Hearthstone.exe").exists():
|
||
return path
|
||
for pattern in PREFIX_GLOBS:
|
||
for candidate in _expand(pattern):
|
||
if (candidate / "Hearthstone.exe").exists():
|
||
return candidate
|
||
return None
|
||
|
||
|
||
def find_log_config(game_dir: Path) -> Path | None:
|
||
"""log.config dosyasını prefix içindeki kullanıcı dizininde arar."""
|
||
# .../drive_c/Program Files (x86)/Hearthstone -> .../drive_c
|
||
drive_c = game_dir.parent.parent
|
||
users = drive_c / "users"
|
||
if not users.is_dir():
|
||
return None
|
||
for user_dir in users.iterdir():
|
||
candidate = user_dir / "AppData" / "Local" / "Blizzard" / "Hearthstone" / "log.config"
|
||
if candidate.parent.is_dir():
|
||
return candidate
|
||
return None
|
||
|
||
|
||
def read_config() -> dict:
|
||
if CONFIG_PATH.exists():
|
||
try:
|
||
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
return {}
|
||
return {}
|
||
|
||
|
||
def detect(configured_dir: str | None = None) -> Installation | None:
|
||
if configured_dir is None:
|
||
configured_dir = read_config().get("game_dir")
|
||
game_dir = find_game_dir(configured_dir)
|
||
if game_dir is None:
|
||
return None
|
||
return Installation(
|
||
game_dir=game_dir,
|
||
logs_dir=game_dir / "Logs",
|
||
log_config=find_log_config(game_dir),
|
||
client_config=game_dir / "client.config",
|
||
)
|
||
|
||
|
||
def latest_log_dir(logs_dir: Path) -> Path | None:
|
||
"""En son oturumun log dizini. Oyun her açılışta yenisini oluşturur."""
|
||
if not logs_dir.is_dir():
|
||
return None
|
||
candidates = [d for d in logs_dir.iterdir() if d.is_dir() and LOG_DIR_RE.match(d.name)]
|
||
if not candidates:
|
||
return None
|
||
return max(candidates, key=lambda d: d.stat().st_mtime)
|
||
|
||
|
||
def parse_log_config(path: Path) -> dict[str, dict[str, str]]:
|
||
sections: dict[str, dict[str, str]] = {}
|
||
current: dict[str, str] | None = None
|
||
try:
|
||
text = path.read_text(encoding="utf-8", errors="replace")
|
||
except OSError:
|
||
return sections
|
||
for line in text.splitlines():
|
||
line = line.strip()
|
||
if line.startswith("[") and line.endswith("]"):
|
||
current = {}
|
||
sections[line[1:-1]] = current
|
||
elif "=" in line and current is not None:
|
||
key, _, value = line.partition("=")
|
||
current[key.strip()] = value.strip()
|
||
return sections
|
||
|
||
|
||
def check_log_config(path: Path | None) -> list[str]:
|
||
"""Eksik veya hatalı log.config ayarlarını insan okur biçimde döner."""
|
||
if path is None or not path.exists():
|
||
return ["log.config bulunamadı"]
|
||
sections = parse_log_config(path)
|
||
problems: list[str] = []
|
||
for component, required in REQUIRED_COMPONENTS.items():
|
||
section = sections.get(component)
|
||
if section is None:
|
||
problems.append(f"[{component}] bölümü eksik")
|
||
continue
|
||
for key, value in required.items():
|
||
actual = section.get(key)
|
||
if actual is None:
|
||
problems.append(f"[{component}] {key} eksik")
|
||
elif actual.lower() != value.lower():
|
||
problems.append(f"[{component}] {key}={actual}, beklenen {value}")
|
||
return problems
|
||
|
||
|
||
def check_client_config(path: Path) -> list[str]:
|
||
"""Log boyut limiti açıksa uzun oturumlarda log kesiliyor."""
|
||
if not path.exists():
|
||
return ["client.config yok, log boyut limiti kaldırılmamış"]
|
||
sections = parse_log_config(path)
|
||
limit = sections.get("Log", {}).get("FileSizeLimit.Int")
|
||
if limit != "-1":
|
||
return [f"client.config FileSizeLimit.Int={limit}, beklenen -1"]
|
||
return []
|