229 lines
6.7 KiB
Python
229 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
#
|
|
|
|
"""
|
|
q3stats.lib.scripts.import_games
|
|
================================
|
|
|
|
This module contains the Q3A game importer.
|
|
"""
|
|
|
|
from argparse import ArgumentParser
|
|
import datetime
|
|
import codecs
|
|
import logging
|
|
import os
|
|
|
|
import lxml.etree
|
|
|
|
from q3stats.lib.config import get_config
|
|
from q3stats.lib.scripts import utils
|
|
from q3stats.models import Game, Score
|
|
|
|
__all__ = ['script_main']
|
|
|
|
XML_STAT_TO_MODEL_FIELD = {
|
|
'Score': 'score',
|
|
'Kills': 'kills',
|
|
'Deaths': 'deaths',
|
|
'Suicides': 'suicides',
|
|
'Net': 'net',
|
|
'DamageGiven': 'damage_given',
|
|
'DamageTaken': 'damage_taken',
|
|
'HealthTotal': 'total_health',
|
|
'ArmorTotal': 'total_armor',
|
|
}
|
|
|
|
XML_STAT_POWERUP_NAMES = ['Quad']
|
|
|
|
|
|
def _find_stats_files(stats_path):
|
|
"""Finds Q3A stats files in *stats_path*."""
|
|
stats_files = []
|
|
for root, dirs, files in os.walk(stats_path):
|
|
for filename in files:
|
|
_, ext = os.path.splitext(filename)
|
|
if ext.lower() == '.xml':
|
|
stats_files.append(os.path.join(root, filename))
|
|
|
|
return stats_files
|
|
|
|
|
|
def _read_match(file_path):
|
|
"""Reads a Q3A match file from *file_path*."""
|
|
with codecs.open(file_path, 'r', 'utf-8') as stats_f:
|
|
stats_xml = lxml.etree.parse(stats_f)
|
|
|
|
xmlroot = stats_xml.getroot()
|
|
if xmlroot.tag != 'match':
|
|
return None
|
|
|
|
return xmlroot
|
|
|
|
|
|
def _read_game(match):
|
|
"""Creates and returns an instance of :py:class:`q3stats.models.game.Game`
|
|
filled with game data from *match*."""
|
|
ts = datetime.datetime.strptime(
|
|
match.get('datetime'), '%Y/%m/%d %H:%M:%S'
|
|
)
|
|
|
|
is_team = False
|
|
if match.get('isTeamGame') == 'true':
|
|
is_team = True
|
|
|
|
game = Game(
|
|
map=match.get('map').upper(),
|
|
date=ts.date(),
|
|
time=ts.time(),
|
|
attrs={
|
|
'type': match.get('type'),
|
|
'team_game': is_team,
|
|
'duration': int(match.get('duration')),
|
|
}
|
|
)
|
|
|
|
game.update_uuid()
|
|
|
|
return game
|
|
|
|
|
|
def _read_scores(match):
|
|
"""Reads game data from *match* and extracts player scores.
|
|
|
|
Returns an array of :py:class:`q3stats.models.score.Score` objects and
|
|
the actual fraglimit (aka the highest score)."""
|
|
scores = []
|
|
|
|
current_score = None
|
|
fraglimit = 0
|
|
for element in match.iter():
|
|
if element.tag == 'player':
|
|
if current_score is not None:
|
|
if current_score.score > fraglimit:
|
|
fraglimit = current_score.score
|
|
|
|
scores.append(current_score)
|
|
|
|
current_score = Score(player=element.get('name'))
|
|
current_score.weapons = {}
|
|
current_score.items = {}
|
|
current_score.powerups = {}
|
|
elif element.tag == 'stat':
|
|
name = element.get('name')
|
|
if name in XML_STAT_TO_MODEL_FIELD:
|
|
setattr(
|
|
current_score, XML_STAT_TO_MODEL_FIELD[name],
|
|
int(element.get('value'))
|
|
)
|
|
elif element.tag == 'weapon':
|
|
current_score.weapons[element.get('name')] = {
|
|
'hits': int(element.get('hits')),
|
|
'shots': int(element.get('shots')),
|
|
'kills': int(element.get('kills')),
|
|
}
|
|
elif element.tag == 'item':
|
|
name = element.get('name')
|
|
if name in XML_STAT_POWERUP_NAMES:
|
|
current_score.powerups[name] = [
|
|
int(element.get('pickups')), int(element.get('time'))
|
|
]
|
|
else:
|
|
current_score.items[name] = int(element.get('pickups'))
|
|
|
|
if current_score is not None:
|
|
if current_score.score > fraglimit:
|
|
fraglimit = current_score.score
|
|
|
|
scores.append(current_score)
|
|
|
|
return scores, fraglimit
|
|
|
|
|
|
def _read_stats_file(file_path):
|
|
"""Reads stats file from *file_path*.
|
|
|
|
Returns an instance of :py:class:`q3stats.models.game.Game` with
|
|
associated instances of :py:class:`q3stats.models.score.Score`."""
|
|
match = _read_match(file_path)
|
|
if match is None:
|
|
raise RuntimeError('Invalid stats file: %s' % file_path)
|
|
|
|
game = _read_game(match)
|
|
|
|
scores, fraglimit = _read_scores(match)
|
|
game.scores.extend(scores)
|
|
game.fraglimit = fraglimit
|
|
|
|
return game
|
|
|
|
|
|
def _get_argument_parser():
|
|
"""Creates and returns an instance of ``ArgumentParser`` configured for
|
|
the script."""
|
|
parser = ArgumentParser(description='Import Q3A stats')
|
|
parser.add_argument('stats_path', help='path to stats directory')
|
|
parser.add_argument(
|
|
'-v', '--verbose', action='store_true', default=False,
|
|
help='be verbose'
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def script_main(argv, config=None):
|
|
"""Script entry point."""
|
|
result = utils.RET_OK
|
|
|
|
parser = _get_argument_parser()
|
|
args = parser.parse_args(args=argv)
|
|
|
|
logger = utils.get_logger('import_games', args)
|
|
|
|
try:
|
|
if config is None:
|
|
config = get_config()
|
|
|
|
stats_files = _find_stats_files(args.stats_path)
|
|
logger.debug('Found %d stat files.' % len(stats_files))
|
|
|
|
with utils.db_session(config) as session:
|
|
for stats_file in stats_files:
|
|
logger.debug('Processing file: %s' % stats_file)
|
|
game = _read_stats_file(stats_file)
|
|
|
|
current_game = session.query(Game).\
|
|
filter_by(uuid=game.uuid).\
|
|
first()
|
|
|
|
if current_game is not None:
|
|
logger.debug('Skipping!')
|
|
else:
|
|
session.add(game)
|
|
|
|
session.commit()
|
|
except Exception as exc:
|
|
logger.error('Unhandled exception', exc_info=True)
|
|
result = utils.RET_ERROR
|
|
finally:
|
|
return result
|