Skip to content

Commit

Permalink
improved docstrings and removed parent dependency for LiveEntities
Browse files Browse the repository at this point in the history
  • Loading branch information
dduric committed May 7, 2018
1 parent 1d49e1b commit e8bd1fe
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 92 deletions.
34 changes: 16 additions & 18 deletions example_live_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,26 @@
from hslog.live.parser import LiveLogParser


"""
----------------------------------------------------------------------
LiveLogParser assumes that you"ve configured Power.log to be a symlink
in "SOME_PATH/Hearthstone/Logs" folder:
ln -s Power.log /tmp/hearthstone-redirected.log
this will redirect all data coming into Power.log
so we can access it from a RAM disk
----------------------------------------------------------------------
For better performance make /tmp of type tmpfs (or another location)
def main():
"""
----------------------------------------------------------------------
LiveLogParser assumes that you"ve configured Power.log to be a symlink.
in /etc/fstab add line:
tmpfs /tmp tmpfs nodev,nosuid,size=1G 0 0
In "SOME_PATH/Hearthstone/Logs" folder:
ln -s Power.log /tmp/hearthstone-redirected.log
this will create in-memory storage which is faster then SSD
you need to restart the computer for this to take effect
----------------------------------------------------------------------
"""
This will redirect all data coming into Power.log
so we can access it from a RAM disk.
----------------------------------------------------------------------
For better performance make /tmp of type tmpfs (or another location)
In /etc/fstab add line:
tmpfs /tmp tmpfs nodev,nosuid,size=1G 0 0
def main():
This will create in-memory storage which is faster then SSD.
You need to restart the computer for this to take effect.
----------------------------------------------------------------------
"""
try:
file = "/tmp/hearthstone-redirected.log"
liveParser = LiveLogParser(file)
Expand Down
82 changes: 50 additions & 32 deletions hslog/live/entities.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,56 @@
from hearthstone.entities import Card, Entity
from hearthstone.entities import Card, Entity, Player, Game
from hearthstone.enums import GameTag

from hslog.live.utils import terminal_output


class LiveEntity(Entity):

def __init__(self, entity_id, parent, **kwargs):
""" Entity requires an ID, store everything else in kwargs """
self.parent = parent
self.game_index = self.parent.parser.games.index(self.parent)
super(LiveEntity, self).__init__(entity_id, **kwargs)
def __init__(self, entity_id):
super(LiveEntity, self).__init__(entity_id)
self._game = None

# push data to an end-point
print(f"GAME {self.game_index} --- ENTITY CREATED:", self)
@property
def game(self):
return self._game

@game.setter
def game(self, value):
# this happens when game calls register_entity and entity sets self.game
self._game = value
if value is not None:
terminal_output('ENTITY CREATED', self)
# serialize entity data to JSON
print(self.__dict__)
# push data to an end-point

def tag_change(self, tag, value):
if tag == GameTag.CONTROLLER and not self._initial_controller:
self._initial_controller = self.tags.get(GameTag.CONTROLLER, value)
self.tags[tag] = value

#terminal_output('TAG UPDATED', self, tag, value)

# update notify
self.update_callback()

def update_callback(self):
def update_callback(self, caller):
pass
#terminal_output('ENTITY UPDATED', self)
# push data to an end-point
print(f"GAME {self.game_index} --- ENTITY UPDATED:", self)


"""
* Card is called on export from game
* LiveCard replaces Card and inserts update_callback
* The point is to become able to route update events towards an API end-point
"""


class LiveCard(Card, LiveEntity):

def __init__(self, entity_id, card_id, parent):
super(LiveCard, self).__init__(
entity_id=entity_id,
card_id=card_id,
parent=parent)

""" if card_id doesn"t change, there"s no need to pass it as the argument.
we can use self.card_id instead as it is set by Card class """
"""
Card is called on export from game
LiveCard replaces Card and inserts update_callback
The point is to become able to route update events towards an API end-point
"""

def __init__(self, entity_id, card_id):
super(LiveCard, self).__init__(entity_id, card_id)

"""
if card_id doesn't change, there's no need to pass it as the argument.
we can use self.card_id instead as it is set by Card class
"""
def reveal(self, card_id, tags):
self.revealed = True
self.card_id = card_id
Expand All @@ -51,13 +59,13 @@ def reveal(self, card_id, tags):
self.tags.update(tags)

# update notify
self.update_callback()
self.update_callback(self)

def hide(self):
self.revealed = False

# update notify
self.update_callback()
self.update_callback(self)

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

# update notify
self.update_callback()
self.update_callback(self)

class LivePlayer(Player, LiveEntity):

def __init__(self, packet_id, player_id, hi, lo, name=None):
super(LivePlayer, self).__init__(packet_id, player_id, hi, lo, name)

class LiveGame(Game, LiveEntity):

def __init__(self, entity_id):
super(LiveGame, self).__init__(entity_id)
41 changes: 23 additions & 18 deletions hslog/live/export.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
from hearthstone.enums import GameTag

from hslog.export import EntityTreeExporter
from hslog.live.entities import LiveCard
from hslog.live.entities import LiveGame
from hslog.live.entities import LivePlayer
from hslog.live.utils import ACCESS_DEBUG


class LiveEntityTreeExporter(EntityTreeExporter):
"""
Inherits EntityTreeExporter to provide Live entities
"""

game_class = LiveGame
player_class = LivePlayer
card_class = LiveCard

def __init__(self, packet_tree):
super(LiveEntityTreeExporter, self).__init__(packet_tree)

def handle_full_entity(self, packet):
entity_id = packet.entity

# Check if the entity already exists in the game first.
# This prevents creating it twice.
# This can legitimately happen in case of GAME_RESET
if entity_id <= len(self.game.entities):
# That first if check is an optimization to prevent always looping over all of
# the game"s entities every single FULL_ENTITY packet...
# FIXME: Switching to a dict for game.entities would simplify this.
existing_entity = self.game.find_entity_by_id(entity_id)
if existing_entity is not None:
existing_entity.card_id = packet.card_id
existing_entity.tags = dict(packet.tags)
return existing_entity

entity = self.card_class(entity_id, packet.card_id, self.packet_tree)
def handle_player(self, packet):
ACCESS_DEBUG(self.__class__, 'handle_player')
entity_id = int(packet.entity)

if hasattr(self.packet_tree, "manager"):
# If we have a PlayerManager, first we mutate the CreateGame.Player packet.
# This will have to change if we're ever able to immediately get the names.
player = self.packet_tree.manager.get_player_by_id(entity_id)
packet.name = player.name
entity = self.player_class(entity_id, packet.player_id, packet.hi, packet.lo, packet.name)
entity.tags = dict(packet.tags)
self.game.register_entity(entity)
return entity
entity.initial_hero_entity_id = entity.tags.get(GameTag.HERO_ENTITY, 0)
return entity
4 changes: 4 additions & 0 deletions hslog/live/packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ def __init__(self, ts, parser):
super(LivePacketTree, self).__init__(ts)

def live_export(self, packet):
"""
Triggers packet export which will run the proper handler for the packet.
This will also run update_callback for entity being updated by the packet.
"""
return self.liveExporter.export_packet(packet)
108 changes: 84 additions & 24 deletions hslog/live/parser.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import time
from collections import deque
from threading import Thread
import time

from hearthstone.enums import GameType, FormatType

from hslog import packets, tokens
from hslog.exceptions import RegexParsingError
from hslog.live.packets import LivePacketTree
from hslog.live.player import LivePlayerManager
from hslog.parser import LogParser
from hslog.player import LazyPlayer, PlayerManager
from hslog.utils import parse_tag
from hslog.player import LazyPlayer
from hslog.utils import parse_tag, parse_enum


class LiveLogParser(LogParser):
"""
LiveLogParser adds live log translation into useful data.
Lines are read and pushed into a deque by a separate thread.
Deque is emptied by parse_worker which replaces the read()
function of LogParser and it's also in a separate thread.
This approach is non-blocking and allows for live parsing
of incoming lines.
"""

def __init__(self, filepath):
super(LiveLogParser, self).__init__()
Expand All @@ -18,55 +32,98 @@ def __init__(self, filepath):
self.lines_deque = deque([])

def new_packet_tree(self, ts):
"""
LivePacketTree is introduced here because it instantiates LiveEntityTreeExporter
and keeps track of the parser parent. It also contains a function that
utilizes the liveExporter instance across all the games.
self.parser = parser
self.liveExporter = LiveEntityTreeExporter(self)
"""
self._packets = LivePacketTree(ts, self)
self._packets.spectator_mode = self.spectator_mode
self._packets.manager = PlayerManager()
self._packets.manager = LivePlayerManager()
self.current_block = self._packets
self.games.append(self._packets)

""" why is this return important? """
"""
why is this return important?
it's called only here:
def create_game(self, ts):
self.new_packet_tree(ts)
"""
return self._packets

def handle_game(self, ts, data):
if data.startswith("PlayerID="):
sre = tokens.GAME_PLAYER_META.match(data)
if not sre:
raise RegexParsingError(data)
player_id, player_name = sre.groups()

# set the name of the player
players = self.games[-1].liveExporter.game.players
for p in players:
if p.player_id == int(player_id): p.name = player_name

player_id = int(player_id)
else:
key, value = data.split("=")
key = key.strip()
value = value.strip()
if key == "GameType":
value = parse_enum(GameType, value)
elif key == "FormatType":
value = parse_enum(FormatType, value)
else:
value = int(value)

self.game_meta[key] = value

def tag_change(self, ts, e, tag, value, def_change):
entity_id = self.parse_entity_or_player(e)
tag, value = parse_tag(tag, value)
self._check_for_mulligan_hack(ts, tag, value)

""" skipping LazyPlayer here because it doesn"t have data """
skip = False
if isinstance(entity_id, LazyPlayer):
entity_id = self._packets.manager.register_player_name_on_tag_change(
entity_id, tag, value
)
skip = True
entity_id = self._packets.manager.register_player_name_on_tag_change(entity_id, tag, value)

has_change_def = def_change == tokens.DEF_CHANGE
packet = packets.TagChange(ts, entity_id, tag, value, has_change_def)

if not skip:
self.register_packet(packet)
if entity_id: self.register_packet(packet)
return packet

def register_packet(self, packet, node=None):
""" make sure we"re registering packets to the current game"""
"""
LogParser.register_packet override
This uses the live_export functionality introduces by LivePacketTree
It also keeps track of which LivePacketTree is being used when there
are multiple in parser.games
A better naming for a PacketTree/LivePacketTree would be HearthstoneGame?
Then parser.games would contain HearthstoneGame instances and would
be more obvious what the purpose is.
"""

# make sure we're registering packets to the current game
if not self._packets or self._packets != self.games[-1]:
self._packets = self.games[-1]

if node is None:
node = self.current_block.packets
node.append(packet)

""" line below triggers packet export which will
run update_callback for entity being
updated by the packet.
self._packets == EntityTreeExporter
"""
self._packets.live_export(packet)

self._packets._packet_counter += 1
packet.packet_id = self._packets._packet_counter
self._packets.live_export(packet)

def file_worker(self):
"""
File reader thread. (Naive implementation)
Reads the log file continuously and appends to deque.
"""

file = open(self.filepath, "r")
while self.running:
line = file.readline()
Expand All @@ -76,6 +133,9 @@ def file_worker(self):
time.sleep(0.2)

def parse_worker(self):
"""
If deque contains lines, this initiates parsing.
"""
while self.running:
if len(self.lines_deque):
line = self.lines_deque.popleft()
Expand Down
Loading

0 comments on commit e8bd1fe

Please sign in to comment.