-
Notifications
You must be signed in to change notification settings - Fork 3
/
gameLogReader.py
258 lines (234 loc) · 14 KB
/
gameLogReader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import os
import re
import threading
import time
from datetime import datetime
from html import unescape as html_unescape #might only be needed for: & > <
def unescape(string):
def repl(match): return(int(match.groups()[1]).to_bytes(1, "big"))
bytes = string.encode(encoding = 'UTF-8', errors = 'ignore')
string = re.sub(b"(<bf:nonprint>)(\d{1,3})(</bf:nonprint>)", repl, bytes).decode(encoding = 'UTF-8', errors = 'ignore')
return(html_unescape(string))
class GameLogReader:
def __init__(self, serverGamePath, serverGamePort = None):
self.serverGamePath = serverGamePath
self.serverGamePort = serverGamePort
self.registeredEventHandlers = []
self.thread = None
self.stopThread = False
self.initialized = False
self.currentFilePath = None
self.currentLine = -1
self.currentLogSize = 0
self.lastScoreEvent = {'score_type':''}
self.currentEventName = ""
self.currentParams = {}
self.currentPlayerStatID = None
ExtraEvents(self)
def getLogs(self):
logs = []
for modName in os.listdir(os.path.join(self.serverGamePath,"mods")):
logsPath = os.path.join(self.serverGamePath,"mods",modName,"logs")
if os.path.isdir(logsPath):
for dirItem in os.listdir(logsPath):
fileNameParts = re.split(r"[_\-.]", dirItem)
filePath = os.path.join(logsPath, dirItem)
if dirItem.endswith(".xml") and os.path.isfile(filePath) and fileNameParts[0] == 'ev' and (self.serverGamePort == None or str(self.serverGamePort) == fileNameParts[1]):
try:
creationDateTime = datetime.strptime(fileNameParts[2]+' '+fileNameParts[3], '%Y%m%d %H%M')
logs.append((creationDateTime, filePath))
except: pass # fileName format doesn't comply with the expected format
logs.sort(key=lambda x: x[0])
return([filePath for creationDateTime, filePath in logs])
def initialize(self):
if self.currentFilePath == None: self.currentFilePath = self.getLogs().pop()
self.readNewEvents()
self.initialized = True
def registerEventHandler(self, eventName, function):
self.registeredEventHandlers.append((eventName, function))
def eventHandler(self, eventName):
def decorator_addEvent(func):
self.registerEventHandler(eventName, func)
return func
return decorator_addEvent
def startTread(self, threadDelay = None):
if not self.initialized: self.initialize()
self.thread = threading.Thread(target = self.processLoop, daemon = True)
self.stopThread = False
self.thread.start()
return(self.thread)
def stopTread(self):
self.stopThread = True
def processLoop(self):
while not self.stopThread:
self.readNewEvents()
time.sleep(.2)
def readNewEvents(self):
foundCurrentFile = False
for filePath in self.getLogs():
if filePath == self.currentFilePath: foundCurrentFile = True
if foundCurrentFile == True: # Only process the current log and newer logs
logSize = os.path.getsize(filePath)
fileOffset = self.currentLine if filePath == self.currentFilePath else -1
if filePath != self.currentFilePath or logSize != self.currentLogSize:
self.currentLine = self.processLog(filePath, fileOffset)
self.currentFilePath = filePath
self.currentLogSize = logSize
def triggerEventHandlers(self, eventName, parameters):
for eventHandler in self.registeredEventHandlers:
if eventHandler[0] == eventName:
eventHandler[1](**parameters)
def processLog(self, logPath, startLine):
### Known problems with the xml logs: ###
# exit vehicle is not always in log
# suicide is not in log (it only shows as death) tk should be added to log
# gametype (coop etc) is always "GPM_CQ" (This is fixed in the Linux BFV server)
# setteam is not logged when swiched due to autoballance
# player IP is not logged
# bot creation is not logged
# In BF1942, radio messages sometimes (but not always) are shown as coming from wrong player_id. Haven't verified yet if this also happens in BFV.
# In Objective Mode games, scoreEvents for achieving an objective, or TKing an objective, are not generated, but are counted in score (5 score points).
# This applies to both the BF1942 engine (which at least has "placeholders" for counts of these objectives in the roundstats structure (although they aren't populated),
# and the BFV engine, which doesn't even list them in roundstats.
# Victory type is always logged as "4" in BF1942, despite the real victory type (it is fixed in the Linux BFV server).
### event Names and their parameters: ###
# mapStart()
# mapEnd()
# roundStart(dict: settings)
# roundEnd(dict: roundstats)
# chat(int: player_id, list: player_location, int: int: team, string: text)
# playerKeyHash(int: player_id, string: keyhash)
# disconnectPlayer(int: player_id, list: player_location)
# beginMedPack(int: player_id, list: player_location, int: medpack_status, int: healed_player)
# endMedPack(int: player_id, list: player_location, int: medpack_status)
# beginRepair(int: player_id, list: player_location, int: repair_status, string: vehicle_type, int: vehicle_player = None)
# endRepair(int: player_id, list: player_location, int: repair_status)
# createPlayer(int: player_id, string: name, list: player_location, bool: is_ai, int: team)
# destroyPlayer(int: player_id, list: player_location)
# destroyVehicle(string: vehicle, list: vehicle_pos, int: player_id = None, list: player_location = None)
# enterVehicle(int: player_id, list: player_location, string: vehicle, int: pco_id, bool: is_default, bool: is_fake)
# exitVehicle(int: player_id, list: player_location, string: vehicle, bool: is_fake)
# pickupKit(int: player_id, list: player_location, string: kit)
# radioMessage(int: player_id, list: player_location, int: message, bool: broadcast)
# restartMap(int: tickets_team1, int: tickets_team2)
# roundInit(int: tickets_team1, int: tickets_team2)
# scoreEvent(int: player_id, list: player_location, string: score_type, string: weapon, int: victim_id = None)
# setTeam(int: player_id, list: player_location, int: team)
# spawnEvent(int: player_id, list: player_location, int: team)
# reSpawnEvent(int: player_id, list: player_location, int: team)
# changePlayerName(int: player_id, list: player_location, string: name)
# connectPlayer(int: player_id, list: player_location)
# pickupFlag(int: player_id, list: player_location)
### Extra info on the different score_types in scoreEvent: ###
# FlagCapture: Captured CTF flag
# Attack: Captured ControPpoint
# Kill -> DeathNoMsg: Kill (weapon or killed)
# TK -> Death: TK and is no more
# Death: is no more (fall, suicide, teamswitch)
## used params:
# "FlagCapture", [player_id, player_name, player_location]
# "ControlPointCapture", [player_id, player_name, player_location, controlPoint.controlPointName]
# "Kill", [player_id, player_name, player_location, victim_player_id, victim_player_name, victim_player_location, weapon, isTK]
# "Death", [player_id, player_name, player_location]
### Extra Custom Events (derived from one or more scoreEvents): ###
# scoreEventKill(player_id, player_location, weapon, victim_id, victim_location)
# scoreEventTK(player_id, player_location, weapon, victim_id, victim_location)
# scoreEventFlagCapture(player_id, player_location):
# scoreEventAttack(player_id, player_location):
# scoreEventDeath(player_id, player_location):
lines = []
endLine = startLine
with open(logPath, 'r', encoding='utf-8') as file:
for endLine, line in enumerate(file):
if endLine > startLine:
if line[-2:] != ">\n": # the line should be terminated correctly. Otherwise the game is probably not done writing the log file
endLine = endLine - 1
break
lines.append(line.strip('\n'))
for lineRaw in lines:
line = lineRaw.strip()
# print(line)
if len(line) > 1:
lastOpening = line.rfind('<')
firstClosing = line.find('>')
value = None if lastOpening == 0 else line[firstClosing+1:lastOpening]
nameAndAtributesList = re.findall("(?:\".*?\"|\S)+", line[1:firstClosing])
name = nameAndAtributesList.pop(0)
closing = name.startswith('/')
if closing: name = name[1:]
atributes = {}
for atribute in nameAndAtributesList:
splited = atribute.split('=', 2)
atributes[splited[0]] = splited[1][1:-1]
if not closing:
if name == "?xml":
self.triggerEventHandlers('mapStart', {})
self.currentParams = {}
elif name == "bf:setting":
if atributes['name'] in ['server name', 'modid', 'mapid', 'map', 'game mode']:
value = unescape(value)
elif atributes['name'] in ['internet', 'allownosecam', 'freecamera', 'externalviews', 'autobalance', 'hitindication', 'tkpunish', 'crosshairpoint', 'sv_punkbuster']:
value = value == '1'
elif atributes['name'] in ['kickbacksplash']:
value = float(value)
else:
value = int(value)
self.currentParams[atributes['name']] = value
elif name == "bf:event":
self.currentEventName = atributes['name']
elif name == "bf:param":
if atributes['type'] == 'int':
value = int(value)
if atributes['name'].startswith('is_') or atributes['name'] == 'broadcast':
value = value == 1
elif atributes['type'] == 'vec3':
value = None if value == "(unknown)" else [float(i) for i in value.split('/')]
elif atributes['type'] == 'string':
value = unescape(value)
if self.currentEventName == 'scoreEvent' and atributes['name'] == 'weapon' and value == "(none)":
value = None
self.currentParams[atributes['name']] = value
elif name == "bf:roundstats":
self.currentParams['teamtickets'] = {}
self.currentParams['playerstats'] = {}
elif name in ["bf:winningteam", "bf:victorytype"]:
self.currentParams[name] = value
elif name == "bf:teamtickets":
self.currentParams['teamtickets'][atributes['team']] = value
elif name == "bf:playerstat":
self.currentPlayerStatID = atributes['playerid']
self.currentParams['playerstats'][self.currentPlayerStatID] = {}
elif name == "bf:statparam":
if atributes['name'] == 'player_name':
value = unescape(value)
elif atributes['name'] == 'is_ai':
value = value == '1'
else:
value = int(value)
self.currentParams['playerstats'][self.currentPlayerStatID][atributes['name']] = value
else: #closing
if name == "bf:log":
self.triggerEventHandlers('mapEnd', {})
elif name == "bf:server":
self.triggerEventHandlers('roundStart', {'settings': self.currentParams})
elif name == "bf:event":
self.triggerEventHandlers(self.currentEventName, self.currentParams)
elif name == "bf:playerstat":
pass
elif name == "bf:statparam":
pass
elif name == "bf:roundstats":
self.triggerEventHandlers('roundEnd', {'roundstats': self.currentParams})
if not name == "bf:playerstat": self.currentParams = {}
return(endLine)
class ExtraEvents:
def __init__(self, gameLogReader):
self.previous = {'score_type': None}
@gameLogReader.eventHandler("scoreEvent")
def scoreEvent(player_id, player_location, score_type, weapon, victim_id = None):
parameters = locals()
if (self.previous['score_type'] == "Kill" and score_type == "DeathNoMsg") or (self.previous['score_type'] == "TK" and score_type == "Death"):
gameLogReader.triggerEventHandlers("scoreEvent"+self.previous['score_type'], {'player_id': self.previous['player_id'], 'player_location': self.previous['player_location'], 'weapon': self.previous['weapon'], 'victim_id': player_id, 'victim_location': player_location})
if score_type in ["FlagCapture", "Attack", "Death"]:
gameLogReader.triggerEventHandlers("scoreEvent"+score_type, {'player_id': player_id, 'player_location': player_location})
self.previous = parameters