Files
deste-hearthstone-linux-tra…/core/deckstring.py
T
yusufipk c2a5a2d4d2 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
2026-07-27 22:31:13 +07:00

203 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Hearthstone deste kodu (deckstring) çözücü ve kodlayıcı.
Biçim: base64 içinde varint akışı.
0x00, sürüm(1), format, kahraman sayısı + kahramanlar,
tek kopyalı kart sayısı + kartlar,
çift kopyalı kart sayısı + kartlar,
n kopyalı kart sayısı + (kart, adet) çiftleri,
isteğe bağlı sideboard bölümü (ETC gibi kartlar için).
Kartlar dbfId ile tutulur, kart kimliğine çevirmek için data.cards gerekir.
Tamamen stdlib.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass, field
DECKSTRING_VERSION = 1
FORMAT_UNKNOWN = 0
FORMAT_WILD = 1
FORMAT_STANDARD = 2
FORMAT_CLASSIC = 3
FORMAT_TWIST = 4
FORMAT_NAMES = {
FORMAT_UNKNOWN: "UNKNOWN",
FORMAT_WILD: "WILD",
FORMAT_STANDARD: "STANDARD",
FORMAT_CLASSIC: "CLASSIC",
FORMAT_TWIST: "TWIST",
}
class DeckstringError(ValueError):
"""Geçersiz ya da bozuk deste kodu."""
@dataclass(slots=True)
class Deck:
cards: list[tuple[int, int]] = field(default_factory=list) # (dbfId, adet)
heroes: list[int] = field(default_factory=list)
format_id: int = FORMAT_UNKNOWN
# (dbfId, adet, sahip kartın dbfId'si)
sideboards: list[tuple[int, int, int]] = field(default_factory=list)
@property
def format_name(self) -> str:
return FORMAT_NAMES.get(self.format_id, "UNKNOWN")
@property
def card_count(self) -> int:
return sum(count for _, count in self.cards)
def as_counter(self) -> dict[int, int]:
result: dict[int, int] = {}
for dbf_id, count in self.cards:
result[dbf_id] = result.get(dbf_id, 0) + count
return result
class _Reader:
def __init__(self, data: bytes):
self.data = data
self.index = 0
def byte(self) -> int:
if self.index >= len(self.data):
raise DeckstringError("deste kodu beklenenden kısa")
value = self.data[self.index]
self.index += 1
return value
def varint(self) -> int:
value = 0
shift = 0
while True:
byte = self.byte()
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value
shift += 7
if shift > 63:
raise DeckstringError("bozuk varint")
@property
def at_end(self) -> bool:
return self.index >= len(self.data)
def _write_varint(chunks: list[bytes], value: int) -> None:
while True:
piece = value & 0x7F
value >>= 7
if value:
chunks.append(bytes((piece | 0x80,)))
else:
chunks.append(bytes((piece,)))
return
def parse(deckstring: str) -> Deck:
"""Deste kodunu çözer. Geçersizse DeckstringError fırlatır."""
text = deckstring.strip()
if not text:
raise DeckstringError("boş deste kodu")
# Oyundan kopyalanan kodlarda başta yorum satırları olabiliyor.
for line in text.splitlines():
line = line.strip()
if line and not line.startswith("#"):
text = line
break
try:
raw = base64.b64decode(text + "=" * (-len(text) % 4))
except (ValueError, TypeError) as exc:
raise DeckstringError("base64 çözülemedi") from exc
reader = _Reader(raw)
if reader.byte() != 0:
raise DeckstringError("geçersiz deste kodu başlığı")
version = reader.varint()
if version != DECKSTRING_VERSION:
raise DeckstringError(f"desteklenmeyen sürüm: {version}")
deck = Deck(format_id=reader.varint())
for _ in range(reader.varint()):
deck.heroes.append(reader.varint())
deck.heroes.sort()
for _ in range(reader.varint()):
deck.cards.append((reader.varint(), 1))
for _ in range(reader.varint()):
deck.cards.append((reader.varint(), 2))
for _ in range(reader.varint()):
card = reader.varint()
deck.cards.append((card, reader.varint()))
deck.cards.sort()
# Sideboard bölümü isteğe bağlı, eski kodlarda hiç yok.
if not reader.at_end and reader.byte() == 1:
for _ in range(reader.varint()):
deck.sideboards.append((reader.varint(), 1, reader.varint()))
for _ in range(reader.varint()):
deck.sideboards.append((reader.varint(), 2, reader.varint()))
for _ in range(reader.varint()):
card = reader.varint()
count = reader.varint()
deck.sideboards.append((card, count, reader.varint()))
deck.sideboards.sort(key=lambda item: (item[2], item[0]))
return deck
def write(deck: Deck) -> str:
"""Desteyi tekrar deste koduna çevirir."""
chunks: list[bytes] = [b"\0"]
_write_varint(chunks, DECKSTRING_VERSION)
_write_varint(chunks, deck.format_id)
_write_varint(chunks, len(deck.heroes))
for hero in sorted(deck.heroes):
_write_varint(chunks, hero)
singles = sorted(c for c, n in deck.cards if n == 1)
doubles = sorted(c for c, n in deck.cards if n == 2)
others = sorted((c, n) for c, n in deck.cards if n not in (1, 2))
_write_varint(chunks, len(singles))
for card in singles:
_write_varint(chunks, card)
_write_varint(chunks, len(doubles))
for card in doubles:
_write_varint(chunks, card)
_write_varint(chunks, len(others))
for card, count in others:
_write_varint(chunks, card)
_write_varint(chunks, count)
if deck.sideboards:
chunks.append(b"\1")
for wanted in (1, 2):
group = sorted(
(card, owner) for card, count, owner in deck.sideboards if count == wanted
)
_write_varint(chunks, len(group))
for card, owner in group:
_write_varint(chunks, card)
_write_varint(chunks, owner)
group_n = sorted(
(card, count, owner)
for card, count, owner in deck.sideboards
if count not in (1, 2)
)
_write_varint(chunks, len(group_n))
for card, count, owner in group_n:
_write_varint(chunks, card)
_write_varint(chunks, count)
_write_varint(chunks, owner)
return base64.b64encode(b"".join(chunks)).decode("ascii")