mirror of
https://github.com/yusufipk/deste-hearthstone-linux-tracker.git
synced 2026-09-11 10:46:12 +00:00
Hearthstone deck tracker for Linux, first public version
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
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
"""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 []
|
||||
Reference in New Issue
Block a user