Skip to content

Commit e8bd1fe

Browse files
committed
improved docstrings and removed parent dependency for LiveEntities
1 parent 1d49e1b commit e8bd1fe

7 files changed

Lines changed: 244 additions & 92 deletions

File tree

example_live_parser.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,26 @@
44
from hslog.live.parser import LiveLogParser
55

66

7-
"""
8-
----------------------------------------------------------------------
9-
LiveLogParser assumes that you"ve configured Power.log to be a symlink
10-
11-
in "SOME_PATH/Hearthstone/Logs" folder:
12-
ln -s Power.log /tmp/hearthstone-redirected.log
13-
14-
this will redirect all data coming into Power.log
15-
so we can access it from a RAM disk
16-
----------------------------------------------------------------------
17-
For better performance make /tmp of type tmpfs (or another location)
7+
def main():
8+
"""
9+
----------------------------------------------------------------------
10+
LiveLogParser assumes that you"ve configured Power.log to be a symlink.
1811
19-
in /etc/fstab add line:
20-
tmpfs /tmp tmpfs nodev,nosuid,size=1G 0 0
12+
In "SOME_PATH/Hearthstone/Logs" folder:
13+
ln -s Power.log /tmp/hearthstone-redirected.log
2114
22-
this will create in-memory storage which is faster then SSD
23-
you need to restart the computer for this to take effect
24-
----------------------------------------------------------------------
25-
"""
15+
This will redirect all data coming into Power.log
16+
so we can access it from a RAM disk.
17+
----------------------------------------------------------------------
18+
For better performance make /tmp of type tmpfs (or another location)
2619
20+
In /etc/fstab add line:
21+
tmpfs /tmp tmpfs nodev,nosuid,size=1G 0 0
2722
28-
def main():
23+
This will create in-memory storage which is faster then SSD.
24+
You need to restart the computer for this to take effect.
25+
----------------------------------------------------------------------
26+
"""
2927
try:
3028
file = "/tmp/hearthstone-redirected.log"
3129
liveParser = LiveLogParser(file)

hslog/live/entities.py

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,56 @@
1-
from hearthstone.entities import Card, Entity
1+
from hearthstone.entities import Card, Entity, Player, Game
22
from hearthstone.enums import GameTag
33

4+
from hslog.live.utils import terminal_output
5+
46

57
class LiveEntity(Entity):
68

7-
def __init__(self, entity_id, parent, **kwargs):
8-
""" Entity requires an ID, store everything else in kwargs """
9-
self.parent = parent
10-
self.game_index = self.parent.parser.games.index(self.parent)
11-
super(LiveEntity, self).__init__(entity_id, **kwargs)
9+
def __init__(self, entity_id):
10+
super(LiveEntity, self).__init__(entity_id)
11+
self._game = None
1212

13-
# push data to an end-point
14-
print(f"GAME {self.game_index} --- ENTITY CREATED:", self)
13+
@property
14+
def game(self):
15+
return self._game
16+
17+
@game.setter
18+
def game(self, value):
19+
# this happens when game calls register_entity and entity sets self.game
20+
self._game = value
21+
if value is not None:
22+
terminal_output('ENTITY CREATED', self)
23+
# serialize entity data to JSON
24+
print(self.__dict__)
25+
# push data to an end-point
1526

1627
def tag_change(self, tag, value):
1728
if tag == GameTag.CONTROLLER and not self._initial_controller:
1829
self._initial_controller = self.tags.get(GameTag.CONTROLLER, value)
1930
self.tags[tag] = value
31+
32+
#terminal_output('TAG UPDATED', self, tag, value)
2033

21-
# update notify
22-
self.update_callback()
23-
24-
def update_callback(self):
34+
def update_callback(self, caller):
35+
pass
36+
#terminal_output('ENTITY UPDATED', self)
2537
# push data to an end-point
26-
print(f"GAME {self.game_index} --- ENTITY UPDATED:", self)
27-
28-
29-
"""
30-
* Card is called on export from game
31-
* LiveCard replaces Card and inserts update_callback
32-
* The point is to become able to route update events towards an API end-point
33-
"""
3438

3539

3640
class LiveCard(Card, LiveEntity):
37-
38-
def __init__(self, entity_id, card_id, parent):
39-
super(LiveCard, self).__init__(
40-
entity_id=entity_id,
41-
card_id=card_id,
42-
parent=parent)
43-
44-
""" if card_id doesn"t change, there"s no need to pass it as the argument.
45-
we can use self.card_id instead as it is set by Card class """
41+
"""
42+
Card is called on export from game
43+
LiveCard replaces Card and inserts update_callback
44+
The point is to become able to route update events towards an API end-point
45+
"""
46+
47+
def __init__(self, entity_id, card_id):
48+
super(LiveCard, self).__init__(entity_id, card_id)
49+
50+
"""
51+
if card_id doesn't change, there's no need to pass it as the argument.
52+
we can use self.card_id instead as it is set by Card class
53+
"""
4654
def reveal(self, card_id, tags):
4755
self.revealed = True
4856
self.card_id = card_id
@@ -51,13 +59,13 @@ def reveal(self, card_id, tags):
5159
self.tags.update(tags)
5260

5361
# update notify
54-
self.update_callback()
62+
self.update_callback(self)
5563

5664
def hide(self):
5765
self.revealed = False
5866

5967
# update notify
60-
self.update_callback()
68+
self.update_callback(self)
6169

6270
""" same comment as for reveal """
6371
def change(self, card_id, tags):
@@ -67,4 +75,14 @@ def change(self, card_id, tags):
6775
self.tags.update(tags)
6876

6977
# update notify
70-
self.update_callback()
78+
self.update_callback(self)
79+
80+
class LivePlayer(Player, LiveEntity):
81+
82+
def __init__(self, packet_id, player_id, hi, lo, name=None):
83+
super(LivePlayer, self).__init__(packet_id, player_id, hi, lo, name)
84+
85+
class LiveGame(Game, LiveEntity):
86+
87+
def __init__(self, entity_id):
88+
super(LiveGame, self).__init__(entity_id)

hslog/live/export.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1+
from hearthstone.enums import GameTag
2+
13
from hslog.export import EntityTreeExporter
24
from hslog.live.entities import LiveCard
5+
from hslog.live.entities import LiveGame
6+
from hslog.live.entities import LivePlayer
7+
from hslog.live.utils import ACCESS_DEBUG
38

49

510
class LiveEntityTreeExporter(EntityTreeExporter):
11+
"""
12+
Inherits EntityTreeExporter to provide Live entities
13+
"""
14+
15+
game_class = LiveGame
16+
player_class = LivePlayer
617
card_class = LiveCard
718

819
def __init__(self, packet_tree):
920
super(LiveEntityTreeExporter, self).__init__(packet_tree)
1021

11-
def handle_full_entity(self, packet):
12-
entity_id = packet.entity
13-
14-
# Check if the entity already exists in the game first.
15-
# This prevents creating it twice.
16-
# This can legitimately happen in case of GAME_RESET
17-
if entity_id <= len(self.game.entities):
18-
# That first if check is an optimization to prevent always looping over all of
19-
# the game"s entities every single FULL_ENTITY packet...
20-
# FIXME: Switching to a dict for game.entities would simplify this.
21-
existing_entity = self.game.find_entity_by_id(entity_id)
22-
if existing_entity is not None:
23-
existing_entity.card_id = packet.card_id
24-
existing_entity.tags = dict(packet.tags)
25-
return existing_entity
26-
27-
entity = self.card_class(entity_id, packet.card_id, self.packet_tree)
22+
def handle_player(self, packet):
23+
ACCESS_DEBUG(self.__class__, 'handle_player')
24+
entity_id = int(packet.entity)
25+
26+
if hasattr(self.packet_tree, "manager"):
27+
# If we have a PlayerManager, first we mutate the CreateGame.Player packet.
28+
# This will have to change if we're ever able to immediately get the names.
29+
player = self.packet_tree.manager.get_player_by_id(entity_id)
30+
packet.name = player.name
31+
entity = self.player_class(entity_id, packet.player_id, packet.hi, packet.lo, packet.name)
2832
entity.tags = dict(packet.tags)
2933
self.game.register_entity(entity)
30-
return entity
34+
entity.initial_hero_entity_id = entity.tags.get(GameTag.HERO_ENTITY, 0)
35+
return entity

hslog/live/packets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ def __init__(self, ts, parser):
1010
super(LivePacketTree, self).__init__(ts)
1111

1212
def live_export(self, packet):
13+
"""
14+
Triggers packet export which will run the proper handler for the packet.
15+
This will also run update_callback for entity being updated by the packet.
16+
"""
1317
return self.liveExporter.export_packet(packet)

hslog/live/parser.py

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
import time
21
from collections import deque
32
from threading import Thread
3+
import time
4+
5+
from hearthstone.enums import GameType, FormatType
46

57
from hslog import packets, tokens
8+
from hslog.exceptions import RegexParsingError
69
from hslog.live.packets import LivePacketTree
10+
from hslog.live.player import LivePlayerManager
711
from hslog.parser import LogParser
8-
from hslog.player import LazyPlayer, PlayerManager
9-
from hslog.utils import parse_tag
12+
from hslog.player import LazyPlayer
13+
from hslog.utils import parse_tag, parse_enum
1014

1115

1216
class LiveLogParser(LogParser):
17+
"""
18+
LiveLogParser adds live log translation into useful data.
19+
20+
Lines are read and pushed into a deque by a separate thread.
21+
Deque is emptied by parse_worker which replaces the read()
22+
function of LogParser and it's also in a separate thread.
23+
24+
This approach is non-blocking and allows for live parsing
25+
of incoming lines.
26+
"""
1327

1428
def __init__(self, filepath):
1529
super(LiveLogParser, self).__init__()
@@ -18,55 +32,98 @@ def __init__(self, filepath):
1832
self.lines_deque = deque([])
1933

2034
def new_packet_tree(self, ts):
35+
"""
36+
LivePacketTree is introduced here because it instantiates LiveEntityTreeExporter
37+
and keeps track of the parser parent. It also contains a function that
38+
utilizes the liveExporter instance across all the games.
39+
40+
self.parser = parser
41+
self.liveExporter = LiveEntityTreeExporter(self)
42+
"""
2143
self._packets = LivePacketTree(ts, self)
2244
self._packets.spectator_mode = self.spectator_mode
23-
self._packets.manager = PlayerManager()
45+
self._packets.manager = LivePlayerManager()
2446
self.current_block = self._packets
2547
self.games.append(self._packets)
2648

27-
""" why is this return important? """
49+
"""
50+
why is this return important?
51+
it's called only here:
52+
53+
def create_game(self, ts):
54+
self.new_packet_tree(ts)
55+
"""
2856
return self._packets
57+
58+
def handle_game(self, ts, data):
59+
if data.startswith("PlayerID="):
60+
sre = tokens.GAME_PLAYER_META.match(data)
61+
if not sre:
62+
raise RegexParsingError(data)
63+
player_id, player_name = sre.groups()
64+
65+
# set the name of the player
66+
players = self.games[-1].liveExporter.game.players
67+
for p in players:
68+
if p.player_id == int(player_id): p.name = player_name
69+
70+
player_id = int(player_id)
71+
else:
72+
key, value = data.split("=")
73+
key = key.strip()
74+
value = value.strip()
75+
if key == "GameType":
76+
value = parse_enum(GameType, value)
77+
elif key == "FormatType":
78+
value = parse_enum(FormatType, value)
79+
else:
80+
value = int(value)
2981

82+
self.game_meta[key] = value
83+
3084
def tag_change(self, ts, e, tag, value, def_change):
3185
entity_id = self.parse_entity_or_player(e)
3286
tag, value = parse_tag(tag, value)
3387
self._check_for_mulligan_hack(ts, tag, value)
3488

35-
""" skipping LazyPlayer here because it doesn"t have data """
36-
skip = False
3789
if isinstance(entity_id, LazyPlayer):
38-
entity_id = self._packets.manager.register_player_name_on_tag_change(
39-
entity_id, tag, value
40-
)
41-
skip = True
90+
entity_id = self._packets.manager.register_player_name_on_tag_change(entity_id, tag, value)
91+
4292
has_change_def = def_change == tokens.DEF_CHANGE
4393
packet = packets.TagChange(ts, entity_id, tag, value, has_change_def)
44-
45-
if not skip:
46-
self.register_packet(packet)
94+
if entity_id: self.register_packet(packet)
4795
return packet
4896

4997
def register_packet(self, packet, node=None):
50-
""" make sure we"re registering packets to the current game"""
98+
"""
99+
LogParser.register_packet override
100+
101+
This uses the live_export functionality introduces by LivePacketTree
102+
It also keeps track of which LivePacketTree is being used when there
103+
are multiple in parser.games
104+
105+
A better naming for a PacketTree/LivePacketTree would be HearthstoneGame?
106+
Then parser.games would contain HearthstoneGame instances and would
107+
be more obvious what the purpose is.
108+
"""
109+
110+
# make sure we're registering packets to the current game
51111
if not self._packets or self._packets != self.games[-1]:
52112
self._packets = self.games[-1]
53113

54114
if node is None:
55115
node = self.current_block.packets
56116
node.append(packet)
57-
58-
""" line below triggers packet export which will
59-
run update_callback for entity being
60-
updated by the packet.
61-
62-
self._packets == EntityTreeExporter
63-
"""
64-
self._packets.live_export(packet)
65-
66117
self._packets._packet_counter += 1
67118
packet.packet_id = self._packets._packet_counter
119+
self._packets.live_export(packet)
68120

69121
def file_worker(self):
122+
"""
123+
File reader thread. (Naive implementation)
124+
Reads the log file continuously and appends to deque.
125+
"""
126+
70127
file = open(self.filepath, "r")
71128
while self.running:
72129
line = file.readline()
@@ -76,6 +133,9 @@ def file_worker(self):
76133
time.sleep(0.2)
77134

78135
def parse_worker(self):
136+
"""
137+
If deque contains lines, this initiates parsing.
138+
"""
79139
while self.running:
80140
if len(self.lines_deque):
81141
line = self.lines_deque.popleft()

0 commit comments

Comments
 (0)