Skip to content

Commit 443e4d8

Browse files
committed
Plugins can now yield PluginExit events.
1 parent c89ace6 commit 443e4d8

File tree

7 files changed

+153
-6
lines changed

7 files changed

+153
-6
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
Creating a GameEngine Plugin
2+
================================
3+
4+
Handling Events
5+
--------------------
6+
7+
Plugins can opt in to handle events with methods with the following naming convention::
8+
9+
def handleEventName(self, event, replay)
10+
11+
In addition to handling specific event types, plugins can also handle events more generally by handling built-in parent classes from the list below:
12+
13+
* handleEvent - called for every single event of all types
14+
* handleMessageEvent - called for events in replay.message.events
15+
* handleGameEvent - called for events in replay.game.events
16+
* handleTrackerEvent - called for events in replay.tracker.events
17+
* handlePlayerActionEvent - called for all game events indicating player actions
18+
* handleAbilityEvent - called for all types of ability events
19+
* handleHotkeyEvent - called for all player hotkey events
20+
21+
For every event in a replay, the GameEngine will loop over all of its registered plugins looking for functions to handle that event. Matching handlers are called in order of plugin registration from most general to most specific.
22+
23+
Given the following plugins::
24+
25+
class Plugin1():
26+
def handleAbilityEvent(self, event, replay):
27+
pass
28+
29+
class Plugin2():
30+
def handleEvent(self, event, replay):
31+
pass
32+
33+
def handleTargetAbilityEvent(self, event, replay):
34+
pass
35+
36+
An engine handling a ``TargetAbilityEvent`` would call handlers in the following order::
37+
38+
Plugin1.handleAbilityEvent(event, replay)
39+
Plugin2.handleEvent(event, replay)
40+
Plugin2.handleTargetAbilityEvent(event, replay)
41+
42+
Setup and Cleanup
43+
---------------------
44+
45+
Plugins may also handle special ``InitGame`` and ``EndGame`` events. These handlers for these events are called directly before and after the processing of the replay events:
46+
47+
* handleInitGame - is called prior to processing a new replay to provide
48+
an opportunity for the plugin to clear internal state and set up any
49+
replay state necessary.
50+
51+
* handleEndGame - is called after all events have been processed and
52+
can be used to perform post processing on aggrated data or clean up
53+
intermediate data caches.
54+
55+
Message Passing
56+
--------------------
57+
58+
Event handlers can choose to ``yield`` additional events which will be injected into the event stream directly after the event currently being processed. This feature allows for message passing between plugins. An ExpansionTracker plugin could notify all other plugins of a new ExpansionEvent that they could opt to process::
59+
60+
def handleUnitDoneEvent(self, event, replay):
61+
if event.unit.name == 'Nexus':
62+
yield ExpansionEvent(event.frame, event.unit)
63+
...
64+
65+
Early Exits
66+
--------------------
67+
68+
If a plugin wishes to stop processing a replay it can yield a PluginExit event::
69+
70+
def handleEvent(self, event, replay):
71+
if len(replay.tracker_events) == 0:
72+
yield PluginExit(self, code=0, details=dict(msg="tracker events required"))
73+
...
74+
75+
def handleAbilityEvent(self, event, replay):
76+
try:
77+
possibly_throwing_error()
78+
catch Error as e:
79+
logger.error(e)
80+
yield PluginExit(self, code=0, details=dict(msg="Unexpected exception"))
81+
82+
The GameEngine will intercept this event and remove the plugin from the list of active plugins for this replay. The exit code and details will be available from the replay::
83+
84+
code, details = replay.plugins['MyPlugin']

sc2reader/engine/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from __future__ import absolute_import, print_function, unicode_literals, division
33

44
import sys
5-
from sc2reader.engine.engine import GameEngine
5+
from sc2reader.engine.engine import GameEngine, PluginExit
6+
from sc2reader.engine.utils import GameState
67
from sc2reader.engine.plugins.apm import APMTracker
78
from sc2reader.engine.plugins.selection import SelectionTracker
89
from sc2reader.engine.plugins.context import ContextLoader

sc2reader/engine/engine.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ class EndGameEvent(object):
1313
name = 'EndGame'
1414

1515

16+
class PluginExit(object):
17+
name = 'PluginExit'
18+
19+
def __init__(self, plugin, code=0, details=None):
20+
self.plugin = plugin
21+
self.code = code
22+
self.details = details or {}
23+
24+
1625
class GameEngine(object):
1726
""" GameEngine Specification
1827
--------------------------
@@ -80,8 +89,35 @@ def handleEventName(self, event, replay)
8089
intermediate data caches.
8190
8291
Event handlers can choose to ``yield`` additional events which will be injected
83-
into the event stream directly after the event currently being processed. This is
84-
a great way to send messages to downstream plugins.
92+
into the event stream directly after the event currently being processed. This
93+
feature allows for message passing between plugins. An ExpansionTracker plugin
94+
could notify all other plugins of a new ExpansionEvent that they could opt to
95+
process::
96+
97+
def handleUnitDoneEvent(self, event, replay):
98+
if event.unit.name == 'Nexus':
99+
yield ExpansionEvent(event.frame, event.unit)
100+
....
101+
102+
If a plugin wishes to stop processing a replay it can yield a PluginExit event::
103+
104+
def handleEvent(self, event, replay):
105+
if len(replay.tracker_events) == 0:
106+
yield PluginExit(self, code=0, details=dict(msg="tracker events required"))
107+
...
108+
109+
def handleAbilityEvent(self, event, replay):
110+
try:
111+
possibly_throwing_error()
112+
catch Error as e:
113+
logger.error(e)
114+
yield PluginExit(self, code=0, details=dict(msg="Unexpected exception"))
115+
116+
The GameEngine will intercept this event and remove the plugin from the list of
117+
active plugins for this replay. The exit code and details will be available from the
118+
replay::
119+
120+
code, details = replay.plugins['MyPlugin']
85121
"""
86122
def __init__(self, plugins=[]):
87123
self._plugins = list()
@@ -99,6 +135,13 @@ def run(self, replay):
99135
# ranked from most generic to most specific
100136
handlers = dict()
101137

138+
# Create a local copy of the plugins list. As plugins exit we can
139+
# remove them from this list and regenerate event handlers.
140+
plugins = list(self._plugins)
141+
142+
# Create a dict for storing plugin exit codes and details
143+
replay.plugins = dict()
144+
102145
# Fill event event queue with the replay events, bookmarked by Init and End events.
103146
event_queue = collections.deque()
104147
event_queue.append(InitGameEvent())
@@ -110,9 +153,15 @@ def run(self, replay):
110153
while len(event_queue) > 0:
111154
event = event_queue.popleft()
112155

156+
if event.name == 'PluginExit':
157+
# Remove the plugin and reset the handlers.
158+
plugins.remove(event.plugin)
159+
handlers.clear()
160+
replay.plugins[event.plugin.name] = (event.code, event.details)
161+
113162
# If we haven't compiled a list of handlers for this event yet, do so!
114163
if event.name not in handlers:
115-
event_handlers = self._get_event_handlers(event)
164+
event_handlers = self._get_event_handlers(event, plugins)
116165
handlers[event.name] = event_handlers
117166
else:
118167
event_handlers = handlers[event.name]
@@ -127,8 +176,13 @@ def run(self, replay):
127176
# need to reverse the list first to have them added in order.
128177
event_queue.extendleft(new_events)
129178

130-
def _get_event_handlers(self, event):
131-
return sum([self._get_plugin_event_handlers(plugin, event) for plugin in self._plugins], [])
179+
# For any plugins that didn't yield a PluginExit event, record a successful
180+
# completion.
181+
for plugin in plugins:
182+
replay.plugins[plugin.name] = (0, dict())
183+
184+
def _get_event_handlers(self, event, plugins):
185+
return sum([self._get_plugin_event_handlers(plugin, event) for plugin in plugins], [])
132186

133187
def _get_plugin_event_handlers(self, plugin, event):
134188
handlers = list()

sc2reader/engine/plugins/apm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class APMTracker(object):
1515
1616
APM is 0 for games under 1 minute in length.
1717
"""
18+
name = 'APMTracker'
1819

1920
def handleInitGame(self, event, replay):
2021
for player in replay.players:

sc2reader/engine/plugins/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
@loggable
1010
class ContextLoader(object):
11+
name='ContextLoader'
1112

1213
def handleInitGame(self, event, replay):
1314
replay.units = set()

sc2reader/engine/plugins/selection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class SelectionTracker(object):
2323
2424
# TODO: list a few error inducing sitations
2525
"""
26+
name = 'SelectionTracker'
27+
2628
def handleInitGame(self, event, replay):
2729
for person in replay.entities:
2830
person.selection = dict()

test_replays/test_all.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ def test_engine_plugins(self):
297297
])
298298
)
299299

300+
code, details = replay.plugins['ContextLoader']
301+
self.assertEqual(code, 0)
302+
self.assertEqual(details, dict())
303+
300304
def test_factory_plugins(self):
301305
from sc2reader.factories.plugins.replay import APMTracker, SelectionTracker, toJSON
302306

0 commit comments

Comments
 (0)