Skip to content

Commit 0af5e85

Browse files
committed
GameHeart plugin as a GameEngine plugin.
1 parent 01ec670 commit 0af5e85

File tree

6 files changed

+126
-113
lines changed

6 files changed

+126
-113
lines changed

docs/source/articles/creatingagameengineplugin.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ Event handlers can choose to ``yield`` additional events which will be injected
6565
Early Exits
6666
--------------------
6767

68-
If a plugin wishes to stop processing a replay it can yield a PluginExit event::
68+
If a plugin wishes to stop processing a replay it can yield a PluginExit event before returning::
6969

7070
def handleEvent(self, event, replay):
7171
if len(replay.tracker_events) == 0:
7272
yield PluginExit(self, code=0, details=dict(msg="tracker events required"))
73+
return
7374
...
7475

7576
def handleAbilityEvent(self, event, replay):
@@ -78,6 +79,7 @@ If a plugin wishes to stop processing a replay it can yield a PluginExit event::
7879
catch Error as e:
7980
logger.error(e)
8081
yield PluginExit(self, code=0, details=dict(msg="Unexpected exception"))
82+
return
8183

8284
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::
8385

sc2reader/engine/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
from __future__ import absolute_import, print_function, unicode_literals, division
33

44
import sys
5-
from sc2reader.engine.engine import GameEngine, PluginExit
5+
from sc2reader.engine.engine import GameEngine
6+
from sc2reader.engine.events import PluginExit
67
from sc2reader.engine.utils import GameState
7-
from sc2reader.engine.plugins.apm import APMTracker
8-
from sc2reader.engine.plugins.selection import SelectionTracker
9-
from sc2reader.engine.plugins.context import ContextLoader
8+
from sc2reader.engine import plugins
109

1110

1211
def setGameEngine(engine):
@@ -16,5 +15,5 @@ def setGameEngine(engine):
1615
module.register_plugins = engine.register_plugins
1716

1817
_default_engine = GameEngine()
19-
_default_engine.register_plugin(ContextLoader())
18+
_default_engine.register_plugin(plugins.ContextLoader())
2019
setGameEngine(_default_engine)

sc2reader/engine/engine.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,7 @@
33

44
import collections
55
from sc2reader.events import *
6-
7-
8-
class InitGameEvent(object):
9-
name = 'InitGame'
10-
11-
12-
class EndGameEvent(object):
13-
name = 'EndGame'
14-
15-
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-
6+
from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit
247

258
class GameEngine(object):
269
""" GameEngine Specification
@@ -77,7 +60,7 @@ def handleEventName(self, event, replay)
7760
* handleAbilityEvent - called for all types of ability events
7861
* handleHotkeyEvent - called for all player hotkey events
7962
80-
Plugins may also handle optional InitGame and EndGame events generated
63+
Plugins may also handle optional ``InitGame`` and ``EndGame`` events generated
8164
by the GameEngine before and after processing all the events:
8265
8366
* handleInitGame - is called prior to processing a new replay to provide
@@ -99,11 +82,12 @@ def handleUnitDoneEvent(self, event, replay):
9982
yield ExpansionEvent(event.frame, event.unit)
10083
....
10184
102-
If a plugin wishes to stop processing a replay it can yield a PluginExit event::
85+
If a plugin wishes to stop processing a replay it can yield a PluginExit event before returning::
10386
10487
def handleEvent(self, event, replay):
10588
if len(replay.tracker_events) == 0:
10689
yield PluginExit(self, code=0, details=dict(msg="tracker events required"))
90+
return
10791
...
10892
10993
def handleAbilityEvent(self, event, replay):
@@ -158,6 +142,7 @@ def run(self, replay):
158142
plugins.remove(event.plugin)
159143
handlers.clear()
160144
replay.plugins[event.plugin.name] = (event.code, event.details)
145+
continue
161146

162147
# If we haven't compiled a list of handlers for this event yet, do so!
163148
if event.name not in handlers:

sc2reader/engine/plugins/context.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,19 +239,19 @@ def load_message_game_player(self, event, replay):
239239
pass # This is a global event
240240

241241
def load_tracker_player(self, event, replay):
242-
if event.pid in replay.player:
243-
event.player = replay.player[event.pid]
242+
if event.pid in replay.entity:
243+
event.player = replay.entity[event.pid]
244244
else:
245245
self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frame))
246246

247247
def load_tracker_upkeeper(self, event, replay):
248-
if event.upkeep_pid in replay.player:
249-
event.unit_upkeeper = replay.player[event.upkeep_pid]
248+
if event.upkeep_pid in replay.entity:
249+
event.unit_upkeeper = replay.entity[event.upkeep_pid]
250250
elif event.upkeep_pid != 0:
251251
self.logger.error("Bad upkeep_pid ({0}) for event {1} at {2} [{3}].".format(event.upkeep_pid, event.__class__, Length(seconds=event.second), event.frame))
252252

253253
def load_tracker_controller(self, event, replay):
254-
if event.control_pid in replay.player:
255-
event.unit_controller = replay.player[event.control_pid]
254+
if event.control_pid in replay.entity:
255+
event.unit_controller = replay.entity[event.control_pid]
256256
elif event.control_pid != 0:
257257
self.logger.error("Bad control_pid ({0}) for event {1} at {2} [{3}].".format(event.control_pid, event.__class__, Length(seconds=event.second), event.frame))

sc2reader/engine/plugins/gameheart.py

Lines changed: 98 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import absolute_import, print_function, unicode_literals, division
33

4-
from sc2reader import objects, utils
4+
from datetime import datetime
5+
from sc2reader.utils import Length, get_real_type
6+
from sc2reader.objects import Observer, Team
7+
from sc2reader.engine.events import PluginExit
8+
from sc2reader.constants import GAME_SPEED_FACTOR
59

610

7-
def GameHeartNormalizer(replay):
11+
class GameHeartNormalizer(object):
812
"""
913
normalize a GameHeart replay to:
1014
1) reset frames to the game start
@@ -15,78 +19,96 @@ def GameHeartNormalizer(replay):
1519
Hopefully, the changes here will also extend to other replays that use
1620
in-game lobbies
1721
18-
This makes a few assumptions
19-
1) 1v1 game
22+
GameHeart games have some constraints we can use here:
23+
* They are all 1v1's.
24+
* You can't random in GameHeart
2025
"""
21-
22-
PRIMARY_BUILDINGS = set (['Hatchery', 'Nexus', 'CommandCenter'])
23-
start_frame = -1
24-
actual_players = {}
25-
26-
if not replay.tracker_events:
27-
return replay # necessary using this strategy
28-
29-
for event in replay.tracker_events:
30-
if start_frame != -1 and event.frame > start_frame + 5: # fuzz it a little
31-
break
32-
if event.name == 'UnitBornEvent' and event.control_pid and \
33-
event.unit_type_name in PRIMARY_BUILDINGS:
34-
if event.frame == 0: # it's a normal, legit replay
35-
return replay
36-
start_frame = event.frame
37-
actual_players[event.control_pid] = event.unit.race
38-
39-
# set game length starting with the actual game start
40-
replay.frames -= start_frame
41-
replay.game_length = utils.Length(seconds=replay.frames / 16)
42-
43-
# this should cover events of all types
44-
# not nuking entirely because there are initializations that may be relevant
45-
for event in replay.events:
46-
if event.frame < start_frame:
47-
event.frame = 0
48-
event.second = 0
49-
else:
50-
event.frame -= start_frame
51-
event.second = event.frame >> 4
52-
53-
# replay.humans is okay because they're all still humans
54-
# replay.person and replay.people is okay because the mapping is still true
55-
56-
# add observers
57-
# not reinitializing because players appear to have the properties of observers
58-
# TODO in a better world, these players would get reinitialized
59-
replay.observers += [player for player in replay.players if not player.pid in actual_players]
60-
for observer in replay.observers:
61-
observer.is_observer = True
62-
observer.team_id = None
63-
64-
# reset team
65-
# reset teams
66-
replay.team = {}
67-
replay.teams = []
68-
69-
# reset players
70-
replay.players = [player for player in replay.players if player.pid in actual_players]
71-
for i, player in enumerate(replay.players):
72-
race = actual_players[player.pid]
73-
player.pick_race = race
74-
player.play_race = race
75-
76-
team = objects.Team(i + 1)
77-
team.players.append(player)
78-
team.result = player.result
79-
player.team = team
80-
replay.team[i + 1] = team
81-
replay.teams.append(team)
82-
83-
# set winner
84-
if team.result == 'Win':
85-
replay.winner = team
86-
87-
# clear observers out of the players list
88-
for pid in replay.player.keys():
89-
if not pid in actual_players:
90-
del replay.player[pid]
91-
92-
return replay
26+
name = 'GameHeartNormalizer'
27+
28+
PRIMARY_BUILDINGS = dict(Hatchery="Zerg", Nexus="Protoss", CommandCenter="Terran")
29+
30+
def handleInitGame(self, event, replay):
31+
# without tracker events game heart games can't be fixed
32+
if len(replay.tracker_events) == 0:
33+
yield PluginExit(self, code=0, details=dict())
34+
return
35+
36+
start_frame = -1
37+
actual_players = {}
38+
for event in replay.tracker_events:
39+
if start_frame != -1 and event.frame > start_frame + 5: # fuzz it a little
40+
break
41+
if event.name == 'UnitBornEvent' and event.control_pid and event.unit_type_name in self.PRIMARY_BUILDINGS:
42+
# In normal replays, starting units are born on frame zero.
43+
if event.frame == 0:
44+
yield PluginExit(self, code=0, details=dict())
45+
return
46+
else:
47+
start_frame = event.frame
48+
actual_players[event.control_pid] = self.PRIMARY_BUILDINGS[event.unit_type_name]
49+
50+
self.fix_entities(replay, actual_players)
51+
self.fix_events(replay, start_frame)
52+
53+
replay.frames -= start_frame
54+
replay.game_length = Length(seconds=replay.frames / 16)
55+
replay.real_type = get_real_type(replay.teams)
56+
replay.real_length = Length(seconds=int(replay.game_length.seconds/GAME_SPEED_FACTOR[replay.speed]))
57+
replay.start_time = datetime.utcfromtimestamp(replay.unix_timestamp-replay.real_length.seconds)
58+
59+
def fix_events(self, replay, start_frame):
60+
# Set back the game clock for all events
61+
for event in replay.events:
62+
if event.frame < start_frame:
63+
event.frame = 0
64+
event.second = 0
65+
else:
66+
event.frame -= start_frame
67+
event.second = event.frame >> 4
68+
69+
def fix_entities(self, replay, actual_players):
70+
# Change the players that aren't playing into observers
71+
for p in [p for p in replay.players if p.pid not in actual_players]:
72+
obs = Observer(p.sid, p.slot_data, p.uid, p.init_data, p.pid)
73+
74+
# Because these obs start the game as players the client
75+
# creates various Beacon units for them.
76+
obs.units = p.units
77+
78+
# Remove all references to the old player
79+
del replay.player[p.pid]
80+
del replay.entity[p.pid]
81+
del replay.human[p.uid]
82+
replay.players.remove(p)
83+
replay.entities.remove(p)
84+
replay.humans.remove(p)
85+
86+
# Create all the necessary references for the new observer
87+
replay.observer[obs.uid] = obs
88+
replay.entity[obs.pid] = obs
89+
replay.human[obs.uid] = obs
90+
replay.observers.append(obs)
91+
replay.entities.append(obs)
92+
replay.humans.append(obs)
93+
94+
# Maintain order, just in case someone is depending on it
95+
replay.observers = sorted(replay.observers, key=lambda o: o.sid)
96+
replay.entities = sorted(replay.entities, key=lambda o: o.sid)
97+
replay.humans = sorted(replay.humans, key=lambda o: o.sid)
98+
99+
# Assume one player per team, should be valid for GameHeart games
100+
replay.team = dict()
101+
replay.teams = list()
102+
for index, player in enumerate(replay.players):
103+
team_id = index+1
104+
team = Team(team_id)
105+
replay.team[team_id] = team
106+
replay.teams.append(team)
107+
player.team = team
108+
team.result = player.result
109+
player.pick_race = actual_players[player.pid]
110+
player.play_race = player.pick_race
111+
team.players = [player]
112+
team.result = player.result
113+
if team.result == 'Win':
114+
replay.winner = team

test_replays/test_all.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,16 @@ def test_factory_plugins(self):
323323
self.assertTrue(result["is_ladder"])
324324

325325
def test_gameheartnormalizer_plugin(self):
326-
from sc2reader.engine.plugins.gameheart import GameHeartNormalizer
327-
factory = sc2reader.factories.SC2Factory()
328-
factory.register_plugin("Replay", GameHeartNormalizer())
326+
from sc2reader.engine.plugins import GameHeartNormalizer
327+
sc2reader.engine.register_plugin(GameHeartNormalizer())
328+
329+
# Not a GameHeart game!
330+
replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay")
331+
player_pids = set([ player.pid for player in replay.players])
332+
spawner_pids = set([ event.player.pid for event in replay.events if "TargetAbilityEvent" in event.name and event.ability.name == "SpawnLarva"])
333+
self.assertTrue(spawner_pids.issubset(player_pids))
329334

330-
replay = factory.load_replay("test_replays/gameheart/gameheart.SC2Replay")
335+
replay = sc2reader.load_replay("test_replays/gameheart/gameheart.SC2Replay")
331336
self.assertEqual(replay.events[0].frame, 0)
332337
self.assertEqual(replay.game_length.seconds, 636)
333338
self.assertEqual(len(replay.observers), 5)
@@ -340,7 +345,7 @@ def test_gameheartnormalizer_plugin(self):
340345
self.assertEqual(replay.teams[1].players[0].name, 'Stardust')
341346
self.assertEqual(replay.winner, replay.teams[1])
342347

343-
replay = factory.load_replay("test_replays/gameheart/gh_sameteam.SC2Replay")
348+
replay = sc2reader.load_replay("test_replays/gameheart/gh_sameteam.SC2Replay")
344349
self.assertEqual(replay.events[0].frame, 0)
345350
self.assertEqual(replay.game_length.seconds, 424)
346351
self.assertEqual(len(replay.observers), 5)

0 commit comments

Comments
 (0)