Skip to content

Commit 60fc9c8

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

7 files changed

Lines changed: 252 additions & 88 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: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,53 @@
1-
from hearthstone.entities import Card, Entity
1+
from hearthstone.entities import Card, Entity, Game, Player
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+
# push data to an end-point
1524

1625
def tag_change(self, tag, value):
1726
if tag == GameTag.CONTROLLER and not self._initial_controller:
1827
self._initial_controller = self.tags.get(GameTag.CONTROLLER, value)
1928
self.tags[tag] = value
20-
21-
# update notify
22-
self.update_callback()
23-
24-
def update_callback(self):
29+
terminal_output("TAG UPDATED", self, tag, value)
2530
# push data to an end-point
26-
print(f"GAME {self.game_index} --- ENTITY UPDATED:", self)
2731

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-
"""
32+
def update_callback(self, caller):
33+
terminal_output("ENTITY UPDATED", self)
34+
# push data to an end-point
3435

3536

3637
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 """
38+
"""
39+
Card is called on export from game
40+
LiveCard replaces Card and inserts update_callback
41+
The point is to become able to route update events towards an API end-point
42+
"""
43+
44+
def __init__(self, entity_id, card_id):
45+
super(LiveCard, self).__init__(entity_id, card_id)
46+
47+
"""
48+
if card_id doesn"t change, there"s no need to pass it as the argument.
49+
we can use self.card_id instead as it is set by Card class
50+
"""
4651
def reveal(self, card_id, tags):
4752
self.revealed = True
4853
self.card_id = card_id
@@ -51,13 +56,13 @@ def reveal(self, card_id, tags):
5156
self.tags.update(tags)
5257

5358
# update notify
54-
self.update_callback()
59+
self.update_callback(self)
5560

5661
def hide(self):
5762
self.revealed = False
5863

5964
# update notify
60-
self.update_callback()
65+
self.update_callback(self)
6166

6267
""" same comment as for reveal """
6368
def change(self, card_id, tags):
@@ -67,4 +72,16 @@ def change(self, card_id, tags):
6772
self.tags.update(tags)
6873

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

hslog/live/export.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,33 @@
1+
from hearthstone.enums import GameTag
2+
13
from hslog.export import EntityTreeExporter
2-
from hslog.live.entities import LiveCard
4+
from hslog.live.entities import LiveCard, LiveGame, LivePlayer
5+
from hslog.live.utils import ACCESS_DEBUG
36

47

58
class LiveEntityTreeExporter(EntityTreeExporter):
9+
"""
10+
Inherits EntityTreeExporter to provide Live entities
11+
"""
12+
13+
game_class = LiveGame
14+
player_class = LivePlayer
615
card_class = LiveCard
716

817
def __init__(self, packet_tree):
918
super(LiveEntityTreeExporter, self).__init__(packet_tree)
1019

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
20+
def handle_player(self, packet):
21+
ACCESS_DEBUG(self.__class__, "handle_player")
22+
entity_id = int(packet.entity)
2623

27-
entity = self.card_class(entity_id, packet.card_id, self.packet_tree)
24+
if hasattr(self.packet_tree, "manager"):
25+
# If we have a PlayerManager, first we mutate the CreateGame.Player packet.
26+
# This will have to change if we"re ever able to immediately get the names.
27+
player = self.packet_tree.manager.get_player_by_id(entity_id)
28+
packet.name = player.name
29+
entity = self.player_class(entity_id, packet.player_id, packet.hi, packet.lo, packet.name)
2830
entity.tags = dict(packet.tags)
2931
self.game.register_entity(entity)
32+
entity.initial_hero_entity_id = entity.tags.get(GameTag.HERO_ENTITY, 0)
3033
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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@
22
from collections import deque
33
from threading import Thread
44

5+
from hearthstone.enums import FormatType, GameType
6+
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_enum, parse_tag
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,101 @@ 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
2957

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):
69+
p.name = player_name
70+
71+
player_id = int(player_id)
72+
else:
73+
key, value = data.split("=")
74+
key = key.strip()
75+
value = value.strip()
76+
if key == "GameType":
77+
value = parse_enum(GameType, value)
78+
elif key == "FormatType":
79+
value = parse_enum(FormatType, value)
80+
else:
81+
value = int(value)
82+
83+
self.game_meta[key] = value
84+
3085
def tag_change(self, ts, e, tag, value, def_change):
3186
entity_id = self.parse_entity_or_player(e)
3287
tag, value = parse_tag(tag, value)
3388
self._check_for_mulligan_hack(ts, tag, value)
3489

35-
""" skipping LazyPlayer here because it doesn"t have data """
36-
skip = False
3790
if isinstance(entity_id, LazyPlayer):
3891
entity_id = self._packets.manager.register_player_name_on_tag_change(
39-
entity_id, tag, value
40-
)
41-
skip = True
92+
entity_id, tag, value)
93+
4294
has_change_def = def_change == tokens.DEF_CHANGE
4395
packet = packets.TagChange(ts, entity_id, tag, value, has_change_def)
44-
45-
if not skip:
96+
if entity_id:
4697
self.register_packet(packet)
4798
return packet
4899

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

54117
if node is None:
55118
node = self.current_block.packets
56119
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-
66120
self._packets._packet_counter += 1
67121
packet.packet_id = self._packets._packet_counter
122+
self._packets.live_export(packet)
68123

69124
def file_worker(self):
125+
"""
126+
File reader thread. (Naive implementation)
127+
Reads the log file continuously and appends to deque.
128+
"""
129+
70130
file = open(self.filepath, "r")
71131
while self.running:
72132
line = file.readline()
@@ -76,6 +136,9 @@ def file_worker(self):
76136
time.sleep(0.2)
77137

78138
def parse_worker(self):
139+
"""
140+
If deque contains lines, this initiates parsing.
141+
"""
79142
while self.running:
80143
if len(self.lines_deque):
81144
line = self.lines_deque.popleft()

0 commit comments

Comments
 (0)