]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Add two static methods used in parsing.
[xonotic/xonstat.git] / xonstat / views / submission.py
index 5f30aaead783943502bf266d8dbe74a5501f2847..8a00bc8a6d60c0d8a72b2dcd56f74e4f0748c159 100644 (file)
+import calendar
+import collections
 import datetime
 import logging
 import datetime
 import logging
-import os
-import pyramid.httpexceptions
 import re
 import re
-import time
-from pyramid.response import Response
+
+import pyramid.httpexceptions
 from sqlalchemy import Sequence
 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
 from sqlalchemy import Sequence
 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
-from xonstat.d0_blind_id import d0_blind_id_verify
-from xonstat.elo import process_elos
-from xonstat.models import *
-from xonstat.util import strip_colors, qfont_decode
+from xonstat.elo import EloProcessor
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
+from xonstat.models import PlayerRank, PlayerCaptime
+from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
+from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
 
 log = logging.getLogger(__name__)
 
 
 
 log = logging.getLogger(__name__)
 
 
-def is_blank_game(players):
+def is_real_player(events):
+    """
+    Determines if a given set of events correspond with a non-bot
+    """
+    return not events['P'].startswith('bot')
+
+
+def played_in_game(events):
+    """
+    Determines if a given set of player events correspond with a player who
+    played in the game (matches 1 and scoreboardvalid 1)
+    """
+    return 'matches' in events and 'scoreboardvalid' in events
+
+
+class Submission(object):
+    """Parses an incoming POST request for stats submissions."""
+
+    def __init__(self, body, headers):
+        # a copy of the HTTP headers
+        self.headers = headers
+
+        # a copy of the HTTP POST body
+        self.body = body
+
+        # the submission code version (from the server)
+        self.version = None
+
+        # the revision string of the server
+        self.revision = None
+
+        # the game type played
+        self.game_type_cd = None
+
+        # the active game mod
+        self.mod = None
+
+        # the name of the map played
+        self.map_name = None
+
+        # unique identifier (string) for a match on a given server
+        self.match_id = None
+
+        # the name of the server
+        self.server_name = None
+
+        # the number of cvars that were changed to be different than default
+        self.impure_cvar_changes = None
+
+        # the port number the game server is listening on
+        self.port_number = None
+
+        # how long the game lasted
+        self.duration = None
+
+        # which ladder is being used, if any
+        self.ladder = None
+
+        # players involved in the match (humans, bots, and spectators)
+        self.players = []
+
+        # raw team events
+        self.teams = []
+
+        # the parsing deque (we use this to allow peeking)
+        self.q = collections.deque(self.body.split("\n"))
+
+        ############################################################################################
+        # Below this point are fields useful in determining if the submission is valid or
+        # performance optimizations that save us from looping over the events over and over again.
+        ############################################################################################
+
+        # humans who played in the match
+        self.humans = []
+
+        # bots who played in the match
+        self.bots = []
+
+        # distinct weapons that we have seen fired
+        self.weapons = set()
+
+        # has a human player fired a shot?
+        self.human_fired_weapon = False
+
+        # does any human have a non-zero score?
+        self.human_nonzero_score = False
+
+        # does any human have a fastest cap?
+        self.human_fastest = False
+
+    def next_item(self):
+        """Returns the next key:value pair off the queue."""
+        try:
+            items = self.q.popleft().strip().split(' ', 1)
+            if len(items) == 1:
+                # Some keys won't have values, like 'L' records where the server isn't actually
+                # participating in any ladders. These can be safely ignored.
+                return None, None
+            else:
+                return items
+        except:
+            return None, None
+
+    def add_weapon_fired(self, sub_key):
+        """Adds a weapon to the set of weapons fired during the match (a set)."""
+        self.weapons.add(sub_key.split("-")[1])
+
+    @staticmethod
+    def is_human_player(player):
+        """
+        Determines if a given set of events correspond with a non-bot
+        """
+        return not player['P'].startswith('bot')
+
+    @staticmethod
+    def played_in_game(player):
+        """
+        Determines if a given set of player events correspond with a player who
+        played in the game (matches 1 and scoreboardvalid 1)
+        """
+        return 'matches' in player and 'scoreboardvalid' in player
+
+    def parse_player(self, key, pid):
+        """Construct a player events listing from the submission."""
+
+        # all of the keys related to player records
+        player_keys = ['i', 'n', 't', 'e']
+
+        player = {key: pid}
+
+        player_fired_weapon = False
+        player_nonzero_score = False
+        player_fastest = False
+
+        # Consume all following 'i' 'n' 't'  'e' records
+        while len(self.q) > 0:
+            (key, value) = self.next_item()
+            if key is None and value is None:
+                continue
+            elif key == 'e':
+                (sub_key, sub_value) = value.split(' ', 1)
+                player[sub_key] = sub_value
+
+                if sub_key.endswith("cnt-fired"):
+                    player_fired_weapon = True
+                    self.add_weapon_fired(sub_key)
+                elif sub_key == 'scoreboard-score' and int(sub_value) != 0:
+                    player_nonzero_score = True
+                elif sub_key == 'scoreboard-fastest':
+                    player_fastest = True
+            elif key == 'n':
+                player[key] = unicode(value, 'utf-8')
+            elif key in player_keys:
+                player[key] = value
+            else:
+                # something we didn't expect - put it back on the deque
+                self.q.appendleft("{} {}".format(key, value))
+                break
+
+        played = self.played_in_game(player)
+        human = self.is_human_player(player)
+
+        if played and human:
+            self.humans.append(player)
+
+            if player_fired_weapon:
+                self.human_fired_weapon = True
+
+            if player_nonzero_score:
+                self.human_nonzero_score = True
+
+            if player_fastest:
+                self.human_fastest = True
+
+        elif played and not human:
+            self.bots.append(player)
+        else:
+            self.players.append(player)
+
+    def parse_team(self, key, tid):
+        """Construct a team events listing from the submission."""
+        team = {key: tid}
+
+        # Consume all following 'e' records
+        while len(self.q) > 0 and self.q[0].startswith('e'):
+            (_, value) = self.next_item()
+            (sub_key, sub_value) = value.split(' ', 1)
+            team[sub_key] = sub_value
+
+        self.teams.append(team)
+
+    def parse(self):
+        """Parses the request body into instance variables."""
+        while len(self.q) > 0:
+            (key, value) = self.next_item()
+            if key is None and value is None:
+                continue
+            elif key == 'V':
+                self.version = value
+            elif key == 'R':
+                self.revision = value
+            elif key == 'G':
+                self.game_type_cd = value
+            elif key == 'O':
+                self.mod = value
+            elif key == 'M':
+                self.map_name = value
+            elif key == 'I':
+                self.match_id = value
+            elif key == 'S':
+                self.server_name = unicode(value, 'utf-8')
+            elif key == 'C':
+                self.impure_cvar_changes = int(value)
+            elif key == 'U':
+                self.port_number = int(value)
+            elif key == 'D':
+                self.duration = datetime.timedelta(seconds=int(round(float(value))))
+            elif key == 'L':
+                self.ladder = value
+            elif key == 'Q':
+                self.parse_team(key, value)
+            elif key == 'P':
+                self.parse_player(key, value)
+            else:
+                raise Exception("Invalid submission")
+
+        return self
+
+
+def elo_submission_category(submission):
+    """Determines the Elo category purely by what is in the submission data."""
+    mod = submission.meta.get("O", "None")
+
+    vanilla_allowed_weapons = {"shotgun", "devastator", "blaster", "mortar", "vortex", "electro",
+                               "arc", "hagar", "crylink", "machinegun"}
+    insta_allowed_weapons = {"vaporizer", "blaster"}
+    overkill_allowed_weapons = {"hmg", "vortex", "shotgun", "blaster", "machinegun", "rpc"}
+
+    if mod == "Xonotic":
+        if len(submission.weapons - vanilla_allowed_weapons) == 0:
+            return "vanilla"
+    elif mod == "InstaGib":
+        if len(submission.weapons - insta_allowed_weapons) == 0:
+            return "insta"
+    elif mod == "Overkill":
+        if len(submission.weapons - overkill_allowed_weapons) == 0:
+            return "overkill"
+    else:
+        return "general"
+
+    return "general"
+
+
+def parse_stats_submission(body):
+    """
+    Parses the POST request body for a stats submission
+    """
+    # storage vars for the request body
+    game_meta = {}
+    events = {}
+    players = []
+    teams = []
+
+    # we're not in either stanza to start
+    in_P = in_Q = False
+
+    for line in body.split('\n'):
+        try:
+            (key, value) = line.strip().split(' ', 1)
+
+            # Server (S) and Nick (n) fields can have international characters.
+            if key in 'S' 'n':
+                value = unicode(value, 'utf-8')
+
+            if key not in 'P' 'Q' 'n' 'e' 't' 'i':
+                game_meta[key] = value
+
+            if key == 'Q' or key == 'P':
+                #log.debug('Found a {0}'.format(key))
+                #log.debug('in_Q: {0}'.format(in_Q))
+                #log.debug('in_P: {0}'.format(in_P))
+                #log.debug('events: {0}'.format(events))
+
+                # check where we were before and append events accordingly
+                if in_Q and len(events) > 0:
+                    #log.debug('creating a team (Q) entry')
+                    teams.append(events)
+                    events = {}
+                elif in_P and len(events) > 0:
+                    #log.debug('creating a player (P) entry')
+                    players.append(events)
+                    events = {}
+
+                if key == 'P':
+                    #log.debug('key == P')
+                    in_P = True
+                    in_Q = False
+                elif key == 'Q':
+                    #log.debug('key == Q')
+                    in_P = False
+                    in_Q = True
+
+                events[key] = value
+
+            if key == 'e':
+                (subkey, subvalue) = value.split(' ', 1)
+                events[subkey] = subvalue
+            if key == 'n':
+                events[key] = value
+            if key == 't':
+                events[key] = value
+        except:
+            # no key/value pair - move on to the next line
+            pass
+
+    # add the last entity we were working on
+    if in_P and len(events) > 0:
+        players.append(events)
+    elif in_Q and len(events) > 0:
+        teams.append(events)
+
+    return (game_meta, players, teams)
+
+
+def is_blank_game(gametype, players):
     """Determine if this is a blank game or not. A blank game is either:
 
     1) a match that ended in the warmup stage, where accuracy events are not
     """Determine if this is a blank game or not. A blank game is either:
 
     1) a match that ended in the warmup stage, where accuracy events are not
-    present
+    present (for non-CTS games)
 
     2) a match in which no player made a positive or negative score AND was
     on the scoreboard
 
     2) a match in which no player made a positive or negative score AND was
     on the scoreboard
+
+    ... or for CTS, which doesn't record accuracy events
+
+    1) a match in which no player made a fastest lap AND was
+    on the scoreboard
+
+    ... or for NB, in which not all maps have weapons
+
+    1) a match in which no player made a positive or negative score
     """
     r = re.compile(r'acc-.*-cnt-fired')
     flg_nonzero_score = False
     flg_acc_events = False
     """
     r = re.compile(r'acc-.*-cnt-fired')
     flg_nonzero_score = False
     flg_acc_events = False
+    flg_fastest_lap = False
 
     for events in players:
 
     for events in players:
-        if is_real_player(events):
+        if is_real_player(events) and played_in_game(events):
             for (key,value) in events.items():
                 if key == 'scoreboard-score' and value != 0:
                     flg_nonzero_score = True
                 if r.search(key):
                     flg_acc_events = True
             for (key,value) in events.items():
                 if key == 'scoreboard-score' and value != 0:
                     flg_nonzero_score = True
                 if r.search(key):
                     flg_acc_events = True
+                if key == 'scoreboard-fastest':
+                    flg_fastest_lap = True
+
+    if gametype == 'cts':
+        return not flg_fastest_lap
+    elif gametype == 'nb':
+        return not flg_nonzero_score
+    else:
+        return not (flg_nonzero_score and flg_acc_events)
 
 
-    return not (flg_nonzero_score and flg_acc_events)
 
 def get_remote_addr(request):
     """Get the Xonotic server's IP address"""
 
 def get_remote_addr(request):
     """Get the Xonotic server's IP address"""
@@ -46,40 +389,98 @@ def get_remote_addr(request):
         return request.remote_addr
 
 
         return request.remote_addr
 
 
-def is_supported_gametype(gametype):
+def is_supported_gametype(gametype, version):
     """Whether a gametype is supported or not"""
     """Whether a gametype is supported or not"""
-    flg_supported = True
-
-    if gametype == 'cts' or gametype == 'lms':
-        flg_supported = False
-
-    return flg_supported
-
+    is_supported = False
+
+    # if the type can be supported, but with version constraints, uncomment
+    # here and add the restriction for a specific version below
+    supported_game_types = (
+            'as',
+            'ca',
+            # 'cq',
+            'ctf',
+            'cts',
+            'dm',
+            'dom',
+            'duel',
+            'ft', 'freezetag',
+            'ka', 'keepaway',
+            'kh',
+            # 'lms',
+            'nb', 'nexball',
+            # 'rc',
+            'rune',
+            'tdm',
+        )
+
+    if gametype in supported_game_types:
+        is_supported = True
+    else:
+        is_supported = False
 
 
-def verify_request(request):
-    try:
-        (idfp, status) = d0_blind_id_verify(
-                sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
-                querystring='',
-                postdata=request.body)
+    # some game types were buggy before revisions, thus this additional filter
+    if gametype == 'ca' and version <= 5:
+        is_supported = False
 
 
-        log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
-    except: 
-        idfp = None
-        status = None
+    return is_supported
 
 
-    return (idfp, status)
 
 
+def do_precondition_checks(request, game_meta, raw_players):
+    """Precondition checks for ALL gametypes.
+       These do not require a database connection."""
+    if not has_required_metadata(game_meta):
+        msg = "Missing required game metadata"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPUnprocessableEntity(
+            body=msg,
+            content_type="text/plain"
+        )
 
 
-def num_real_players(player_events, count_bots=False):
+    try:
+        version = int(game_meta['V'])
+    except:
+        msg = "Invalid or incorrect game metadata provided"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPUnprocessableEntity(
+            body=msg,
+            content_type="text/plain"
+        )
+
+    if not is_supported_gametype(game_meta['G'], version):
+        msg = "Unsupported game type ({})".format(game_meta['G'])
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
+
+    if not has_minimum_real_players(request.registry.settings, raw_players):
+        msg = "Not enough real players"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
+
+    if is_blank_game(game_meta['G'], raw_players):
+        msg = "Blank game"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
+
+
+def num_real_players(player_events):
     """
     """
-    Returns the number of real players (those who played 
+    Returns the number of real players (those who played
     and are on the scoreboard).
     """
     real_players = 0
 
     for events in player_events:
     and are on the scoreboard).
     """
     real_players = 0
 
     for events in player_events:
-        if is_real_player(events, count_bots):
+        if is_real_player(events) and played_in_game(events):
             real_players += 1
 
     return real_players
             real_players += 1
 
     return real_players
@@ -107,22 +508,6 @@ def has_minimum_real_players(settings, player_events):
     return flg_has_min_real_players
 
 
     return flg_has_min_real_players
 
 
-def verify_requests(settings):
-    """
-    Determines whether or not to verify requests using the blind_id algorithm
-    """
-    try:
-        val_verify_requests = settings['xonstat.verify_requests']
-        if val_verify_requests == "true":
-            flg_verify_requests = True
-        else:
-            flg_verify_requests = False
-    except:
-        flg_verify_requests = True
-
-    return flg_verify_requests
-
-
 def has_required_metadata(metadata):
     """
     Determines if a give set of metadata has enough data to create a game,
 def has_required_metadata(metadata):
     """
     Determines if a give set of metadata has enough data to create a game,
@@ -130,8 +515,7 @@ def has_required_metadata(metadata):
     """
     flg_has_req_metadata = True
 
     """
     flg_has_req_metadata = True
 
-    if 'T' not in metadata or\
-        'G' not in metadata or\
+    if 'G' not in metadata or\
         'M' not in metadata or\
         'I' not in metadata or\
         'S' not in metadata:
         'M' not in metadata or\
         'I' not in metadata or\
         'S' not in metadata:
@@ -140,25 +524,22 @@ def has_required_metadata(metadata):
     return flg_has_req_metadata
 
 
     return flg_has_req_metadata
 
 
-def is_real_player(events, count_bots=False):
-    """
-    Determines if a given set of player events correspond with a player who
+def should_do_weapon_stats(game_type_cd):
+    """True of the game type should record weapon stats. False otherwise."""
+    if game_type_cd in 'cts':
+        return False
+    else:
+        return True
 
 
-    1) is not a bot (P event does not look like a bot)
-    2) played in the game (matches 1)
-    3) was present at the end of the game (scoreboardvalid 1)
 
 
-    Returns True if the player meets the above conditions, and false otherwise.
-    """
-    flg_is_real = False
+def gametype_elo_eligible(game_type_cd):
+    """True of the game type should process Elos. False otherwise."""
+    elo_game_types = ('duel', 'dm', 'ca', 'ctf', 'tdm', 'ka', 'ft')
 
 
-    # removing 'joins' here due to bug, but it should be here
-    if 'matches' in events and 'scoreboardvalid' in events:
-        if (events['P'].startswith('bot') and count_bots) or \
-            not events['P'].startswith('bot'):
-            flg_is_real = True
-
-    return flg_is_real
+    if game_type_cd in elo_game_types:
+        return True
+    else:
+        return False
 
 
 def register_new_nick(session, player, new_nick):
 
 
 def register_new_nick(session, player, new_nick):
@@ -191,7 +572,7 @@ def register_new_nick(session, player, new_nick):
     session.add(player)
 
 
     session.add(player)
 
 
-def update_fastest_cap(session, player_id, game_id,  map_id, captime):
+def update_fastest_cap(session, player_id, game_id, map_id, captime, mod):
     """
     Check the fastest cap time for the player and map. If there isn't
     one, insert one. If there is, check if the passed time is faster.
     """
     Check the fastest cap time for the player and map. If there isn't
     one, insert one. If there is, check if the passed time is faster.
@@ -205,7 +586,7 @@ def update_fastest_cap(session, player_id, game_id,  map_id, captime):
     # then check to see if the new captime is faster
     try:
         cur_fastest_cap = session.query(PlayerCaptime).filter_by(
     # then check to see if the new captime is faster
     try:
         cur_fastest_cap = session.query(PlayerCaptime).filter_by(
-            player_id=player_id, map_id=map_id).one()
+            player_id=player_id, map_id=map_id, mod=mod).one()
 
         # current captime is faster, so update
         if captime < cur_fastest_cap.fastest_cap:
 
         # current captime is faster, so update
         if captime < cur_fastest_cap.fastest_cap:
@@ -216,54 +597,101 @@ def update_fastest_cap(session, player_id, game_id,  map_id, captime):
 
     except NoResultFound, e:
         # none exists, so insert
 
     except NoResultFound, e:
         # none exists, so insert
-        cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime)
+        cur_fastest_cap = PlayerCaptime(player_id, game_id, map_id, captime,
+                mod)
         session.add(cur_fastest_cap)
         session.flush()
 
 
         session.add(cur_fastest_cap)
         session.flush()
 
 
-def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
-        revision=None):
+def update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
+    """
+    Updates the server in the given DB session, if needed.
+
+    :param server: The found server instance.
+    :param name: The incoming server name.
+    :param hashkey: The incoming server hashkey.
+    :param ip_addr: The incoming server IP address.
+    :param port: The incoming server port.
+    :param revision: The incoming server revision.
+    :param impure_cvars: The incoming number of impure server cvars.
+    :return: bool
+    """
+    # ensure the two int attributes are actually ints
+    try:
+        port = int(port)
+    except:
+        port = None
+
+    try:
+        impure_cvars = int(impure_cvars)
+    except:
+        impure_cvars = 0
+
+    updated = False
+    if name and server.name != name:
+        server.name = name
+        updated = True
+    if hashkey and server.hashkey != hashkey:
+        server.hashkey = hashkey
+        updated = True
+    if ip_addr and server.ip_addr != ip_addr:
+        server.ip_addr = ip_addr
+        updated = True
+    if port and server.port != port:
+        server.port = port
+        updated = True
+    if revision and server.revision != revision:
+        server.revision = revision
+        updated = True
+    if impure_cvars and server.impure_cvars != impure_cvars:
+        server.impure_cvars = impure_cvars
+        server.pure_ind = True if impure_cvars == 0 else False
+        updated = True
+
+    return updated
+
+
+def get_or_create_server(session, name, hashkey, ip_addr, revision, port, impure_cvars):
     """
     Find a server by name or create one if not found. Parameters:
 
     session - SQLAlchemy database session factory
     name - server name of the server to be found or created
     hashkey - server hashkey
     """
     Find a server by name or create one if not found. Parameters:
 
     session - SQLAlchemy database session factory
     name - server name of the server to be found or created
     hashkey - server hashkey
+    ip_addr - the IP address of the server
+    revision - the xonotic revision number
+    port - the port number of the server
+    impure_cvars - the number of impure cvar changes
     """
     """
-    try:
-        # find one by that name, if it exists
-        server = session.query(Server).filter_by(name=name).one()
-
-        # store new hashkey
-        if server.hashkey != hashkey:
-            server.hashkey = hashkey
-            session.add(server)
+    servers_q = DBSession.query(Server).filter(Server.active_ind)
 
 
-        # store new IP address
-        if server.ip_addr != ip_addr:
-            server.ip_addr = ip_addr
-            session.add(server)
-
-        # store new revision
-        if server.revision != revision:
-            server.revision = revision
-            session.add(server)
-
-        log.debug("Found existing server {0}".format(server.server_id))
+    if hashkey:
+        # if the hashkey is provided, we'll use that
+        servers_q = servers_q.filter((Server.name == name) or (Server.hashkey == hashkey))
+    else:
+        # otherwise, it is just by name
+        servers_q = servers_q.filter(Server.name == name)
 
 
-    except MultipleResultsFound, e:
-        # multiple found, so also filter by hashkey
-        server = session.query(Server).filter_by(name=name).\
-                filter_by(hashkey=hashkey).one()
-        log.debug("Found existing server {0}".format(server.server_id))
+    # order by the hashkey, which means any hashkey match will appear first if there are multiple
+    servers = servers_q.order_by(Server.hashkey, Server.create_dt).all()
 
 
-    except NoResultFound, e:
-        # not found, create one
+    if len(servers) == 0:
         server = Server(name=name, hashkey=hashkey)
         session.add(server)
         session.flush()
         server = Server(name=name, hashkey=hashkey)
         session.add(server)
         session.flush()
-        log.debug("Created server {0} with hashkey {1}".format(
-            server.server_id, server.hashkey))
+        log.debug("Created server {} with hashkey {}.".format(server.server_id, server.hashkey))
+    else:
+        server = servers[0]
+        if len(servers) == 1:
+            log.info("Found existing server {}.".format(server.server_id))
+
+        elif len(servers) > 1:
+            server_id_list = ", ".join(["{}".format(s.server_id) for s in servers])
+            log.warn("Multiple servers found ({})! Using the first one ({})."
+                     .format(server_id_list, server.server_id))
+
+    if update_server(server, name, hashkey, ip_addr, port, revision, impure_cvars):
+        session.add(server)
 
     return server
 
 
     return server
 
@@ -278,7 +706,7 @@ def get_or_create_map(session=None, name=None):
     try:
         # find one by the name, if it exists
         gmap = session.query(Map).filter_by(name=name).one()
     try:
         # find one by the name, if it exists
         gmap = session.query(Map).filter_by(name=name).one()
-        log.debug("Found map id {0}: {1}".format(gmap.map_id, 
+        log.debug("Found map id {0}: {1}".format(gmap.map_id,
             gmap.name))
     except NoResultFound, e:
         gmap = Map(name=name)
             gmap.name))
     except NoResultFound, e:
         gmap = Map(name=name)
@@ -298,9 +726,8 @@ def get_or_create_map(session=None, name=None):
     return gmap
 
 
     return gmap
 
 
-def create_game(session=None, start_dt=None, game_type_cd=None, 
-        server_id=None, map_id=None, winner=None, match_id=None,
-        duration=None):
+def create_game(session, start_dt, game_type_cd, server_id, map_id,
+        match_id, duration, mod, winner=None):
     """
     Creates a game. Parameters:
 
     """
     Creates a game. Parameters:
 
@@ -310,12 +737,20 @@ def create_game(session=None, start_dt=None, game_type_cd=None,
     server_id - server identifier of the server hosting the game
     map_id - map on which the game was played
     winner - the team id of the team that won
     server_id - server identifier of the server hosting the game
     map_id - map on which the game was played
     winner - the team id of the team that won
+    duration - how long the game lasted
+    mod - mods in use during the game
     """
     seq = Sequence('games_game_id_seq')
     game_id = session.execute(seq)
     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
                 server_id=server_id, map_id=map_id, winner=winner)
     game.match_id = match_id
     """
     seq = Sequence('games_game_id_seq')
     game_id = session.execute(seq)
     game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
                 server_id=server_id, map_id=map_id, winner=winner)
     game.match_id = match_id
+    game.mod = mod[:64]
+
+    # There is some drift between start_dt (provided by app) and create_dt
+    # (default in the database), so we'll make them the same until this is 
+    # resolved.
+    game.create_dt = start_dt
 
     try:
         game.duration = datetime.timedelta(seconds=int(round(float(duration))))
 
     try:
         game.duration = datetime.timedelta(seconds=int(round(float(duration))))
@@ -328,7 +763,7 @@ def create_game(session=None, start_dt=None, game_type_cd=None,
 
         log.debug("Error: game with same server and match_id found! Ignoring.")
 
 
         log.debug("Error: game with same server and match_id found! Ignoring.")
 
-        # if a game under the same server and match_id found, 
+        # if a game under the same server and match_id found,
         # this is a duplicate game and can be ignored
         raise pyramid.httpexceptions.HTTPOk('OK')
     except NoResultFound, e:
         # this is a duplicate game and can be ignored
         raise pyramid.httpexceptions.HTTPOk('OK')
     except NoResultFound, e:
@@ -336,7 +771,7 @@ def create_game(session=None, start_dt=None, game_type_cd=None,
         session.add(game)
         session.flush()
         log.debug("Created game id {0} on server {1}, map {2} at \
         session.add(game)
         session.flush()
         log.debug("Created game id {0} on server {1}, map {2} at \
-                {3}".format(game.game_id, 
+                {3}".format(game.game_id,
                     server_id, map_id, start_dt))
 
     return game
                     server_id, map_id, start_dt))
 
     return game
@@ -352,7 +787,7 @@ def get_or_create_player(session=None, hashkey=None, nick=None):
     nick - nick of the player (in case of a first time create)
     """
     # if we have a bot
     nick - nick of the player (in case of a first time create)
     """
     # if we have a bot
-    if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
+    if re.search('^bot#\d+', hashkey):
         player = session.query(Player).filter_by(player_id=1).one()
     # if we have an untracked player
     elif re.search('^player#\d+$', hashkey):
         player = session.query(Player).filter_by(player_id=1).one()
     # if we have an untracked player
     elif re.search('^player#\d+$', hashkey):
@@ -389,88 +824,131 @@ def get_or_create_player(session=None, hashkey=None, nick=None):
 
     return player
 
 
     return player
 
-def create_player_game_stat(session=None, player=None, 
-        game=None, player_events=None):
-    """
-    Creates game statistics for a given player in a given game. Parameters:
 
 
-    session - SQLAlchemy session factory
-    player - Player record of the player who owns the stats
-    game - Game record for the game to which the stats pertain
-    player_events - dictionary for the actual stats that need to be transformed
-    """
+def create_default_game_stat(session, game_type_cd):
+    """Creates a blanked-out pgstat record for the given game type"""
 
 
-    # in here setup default values (e.g. if game type is CTF then
-    # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
-    # TODO: use game's create date here instead of now()
+    # this is what we have to do to get partitioned records in - grab the
+    # sequence value first, then insert using the explicit ID (vs autogenerate)
     seq = Sequence('player_game_stats_player_game_stat_id_seq')
     pgstat_id = session.execute(seq)
     seq = Sequence('player_game_stats_player_game_stat_id_seq')
     pgstat_id = session.execute(seq)
-    pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, 
+    pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
             create_dt=datetime.datetime.utcnow())
 
             create_dt=datetime.datetime.utcnow())
 
-    # set player id from player record
-    pgstat.player_id = player.player_id
+    if game_type_cd == 'as':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.collects = 0
+
+    if game_type_cd in 'ca' 'dm' 'duel' 'rune' 'tdm':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = 0
 
 
-    #set game id from game record
-    pgstat.game_id = game.game_id
+    if game_type_cd == 'cq':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
+        pgstat.drops = 0
 
 
-    # all games have a score and every player has an alivetime
-    pgstat.score = 0
-    pgstat.alivetime = datetime.timedelta(seconds=0)
+    if game_type_cd == 'ctf':
+        pgstat.kills = pgstat.captures = pgstat.pickups = pgstat.drops = 0
+        pgstat.returns = pgstat.carrier_frags = 0
 
 
-    if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':
-        pgstat.kills = 0
+    if game_type_cd == 'cts':
         pgstat.deaths = 0
         pgstat.deaths = 0
-        pgstat.suicides = 0
-    elif game.game_type_cd == 'ctf':
-        pgstat.kills = 0
-        pgstat.captures = 0
-        pgstat.pickups = 0
+
+    if game_type_cd == 'dom':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
         pgstat.drops = 0
         pgstat.drops = 0
-        pgstat.returns = 0
+
+    if game_type_cd == 'ft':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.revivals = 0
+
+    if game_type_cd == 'ka':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
         pgstat.carrier_frags = 0
         pgstat.carrier_frags = 0
+        pgstat.time = datetime.timedelta(seconds=0)
+
+    if game_type_cd == 'kh':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.pickups = 0
+        pgstat.captures = pgstat.drops = pgstat.pushes = pgstat.destroys = 0
+        pgstat.carrier_frags = 0
+
+    if game_type_cd == 'lms':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.lives = 0
+
+    if game_type_cd == 'nb':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.captures = 0
+        pgstat.drops = 0
+
+    if game_type_cd == 'rc':
+        pgstat.kills = pgstat.deaths = pgstat.suicides = pgstat.laps = 0
+
+    return pgstat
+
+
+def create_game_stat(session, game_meta, game, server, gmap, player, events):
+    """Game stats handler for all game types"""
+
+    game_type_cd = game.game_type_cd
+
+    pgstat = create_default_game_stat(session, game_type_cd)
 
 
-    for (key,value) in player_events.items():
-        if key == 'n': 
-            pgstat.nick = value[:128]
-            pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
+    # these fields should be on every pgstat record
+    pgstat.game_id       = game.game_id
+    pgstat.player_id     = player.player_id
+    pgstat.nick          = events.get('n', 'Anonymous Player')[:128]
+    pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
+    pgstat.score         = int(round(float(events.get('scoreboard-score', 0))))
+    pgstat.alivetime     = datetime.timedelta(seconds=int(round(float(events.get('alivetime', 0.0)))))
+    pgstat.rank          = int(events.get('rank', None))
+    pgstat.scoreboardpos = int(events.get('scoreboardpos', pgstat.rank))
+
+    if pgstat.nick != player.nick \
+            and player.player_id > 2 \
+            and pgstat.nick != 'Anonymous Player':
+        register_new_nick(session, player, pgstat.nick)
+
+    wins = False
+
+    # gametype-specific stuff is handled here. if passed to us, we store it
+    for (key,value) in events.items():
+        if key == 'wins': wins = True
         if key == 't': pgstat.team = int(value)
         if key == 't': pgstat.team = int(value)
-        if key == 'rank': 
-            pgstat.rank = int(value)
-            # to support older servers who don't send scoreboardpos values
-            if pgstat.scoreboardpos is None:
-                pgstat.scoreboardpos = pgstat.rank
-        if key == 'alivetime': 
-            pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
+
         if key == 'scoreboard-drops': pgstat.drops = int(value)
         if key == 'scoreboard-returns': pgstat.returns = int(value)
         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
         if key == 'scoreboard-pickups': pgstat.pickups = int(value)
         if key == 'scoreboard-caps': pgstat.captures = int(value)
         if key == 'scoreboard-drops': pgstat.drops = int(value)
         if key == 'scoreboard-returns': pgstat.returns = int(value)
         if key == 'scoreboard-fckills': pgstat.carrier_frags = int(value)
         if key == 'scoreboard-pickups': pgstat.pickups = int(value)
         if key == 'scoreboard-caps': pgstat.captures = int(value)
-        if key == 'scoreboard-score': pgstat.score = int(value)
+        if key == 'scoreboard-score': pgstat.score = int(round(float(value)))
         if key == 'scoreboard-deaths': pgstat.deaths = int(value)
         if key == 'scoreboard-kills': pgstat.kills = int(value)
         if key == 'scoreboard-suicides': pgstat.suicides = int(value)
         if key == 'scoreboard-deaths': pgstat.deaths = int(value)
         if key == 'scoreboard-kills': pgstat.kills = int(value)
         if key == 'scoreboard-suicides': pgstat.suicides = int(value)
-        if key == 'scoreboard-captime':
-            pgstat.fastest_cap = datetime.timedelta(seconds=float(value)/100)
-        if key == 'avglatency': pgstat.avg_latency = float(value)
-        if key == 'teamrank': pgstat.teamrank = int(value)
-        if key == 'scoreboardpos': pgstat.scoreboardpos = int(value)
+        if key == 'scoreboard-objectives': pgstat.collects = int(value)
+        if key == 'scoreboard-captured': pgstat.captures = int(value)
+        if key == 'scoreboard-released': pgstat.drops = int(value)
+        if key == 'scoreboard-fastest':
+            pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
+        if key == 'scoreboard-takes': pgstat.pickups = int(value)
+        if key == 'scoreboard-ticks': pgstat.drops = int(value)
+        if key == 'scoreboard-revivals': pgstat.revivals = int(value)
+        if key == 'scoreboard-bctime':
+            pgstat.time = datetime.timedelta(seconds=int(value))
+        if key == 'scoreboard-bckills': pgstat.carrier_frags = int(value)
+        if key == 'scoreboard-losses': pgstat.drops = int(value)
+        if key == 'scoreboard-pushes': pgstat.pushes = int(value)
+        if key == 'scoreboard-destroyed': pgstat.destroys = int(value)
+        if key == 'scoreboard-kckills': pgstat.carrier_frags = int(value)
+        if key == 'scoreboard-lives': pgstat.lives = int(value)
+        if key == 'scoreboard-goals': pgstat.captures = int(value)
+        if key == 'scoreboard-faults': pgstat.drops = int(value)
+        if key == 'scoreboard-laps': pgstat.laps = int(value)
 
 
-    # check to see if we had a name, and if
-    # not use an anonymous handle
-    if pgstat.nick == None:
-        pgstat.nick = "Anonymous Player"
-        pgstat.stripped_nick = "Anonymous Player"
-
-    # otherwise process a nick change
-    elif pgstat.nick != player.nick and player.player_id > 2:
-        register_new_nick(session, player, pgstat.nick)
+        if key == 'avglatency': pgstat.avg_latency = float(value)
+        if key == 'scoreboard-captime':
+            pgstat.fastest = datetime.timedelta(seconds=float(value)/100)
+            if game.game_type_cd == 'ctf':
+                update_fastest_cap(session, player.player_id, game.game_id,
+                        gmap.map_id, pgstat.fastest, game.mod)
 
 
-    # if the player is ranked #1 and it is a team game, set the game's winner
-    # to be the team of that player
-    # FIXME: this is a hack, should be using the 'W' field (not present)
-    if pgstat.rank == 1 and pgstat.team:
+    # there is no "winning team" field, so we have to derive it
+    if wins and pgstat.team is not None and game.winner is None:
         game.winner = pgstat.team
         session.add(game)
 
         game.winner = pgstat.team
         session.add(game)
 
@@ -479,20 +957,79 @@ def create_player_game_stat(session=None, player=None,
     return pgstat
 
 
     return pgstat
 
 
-def create_player_weapon_stats(session=None, player=None, 
-        game=None, pgstat=None, player_events=None, game_meta=None):
-    """
-    Creates accuracy records for each weapon used by a given player in a
-    given game. Parameters:
-
-    session - SQLAlchemy session factory object
-    player - Player record who owns the weapon stats
-    game - Game record in which the stats were created
-    pgstat - Corresponding PlayerGameStat record for these weapon stats
-    player_events - dictionary containing the raw weapon values that need to be
-        transformed
-    game_meta - dictionary of game metadata (only used for stats version info)
-    """
+def create_anticheats(session, pgstat, game, player, events):
+    """Anticheats handler for all game types"""
+
+    anticheats = []
+
+    # all anticheat events are prefixed by "anticheat"
+    for (key,value) in events.items():
+        if key.startswith("anticheat"):
+            try:
+                ac = PlayerGameAnticheat(
+                    player.player_id,
+                    game.game_id,
+                    key,
+                    float(value)
+                )
+                anticheats.append(ac)
+                session.add(ac)
+            except Exception as e:
+                log.debug("Could not parse value for key %s. Ignoring." % key)
+
+    return anticheats
+
+
+def create_default_team_stat(session, game_type_cd):
+    """Creates a blanked-out teamstat record for the given game type"""
+
+    # this is what we have to do to get partitioned records in - grab the
+    # sequence value first, then insert using the explicit ID (vs autogenerate)
+    seq = Sequence('team_game_stats_team_game_stat_id_seq')
+    teamstat_id = session.execute(seq)
+    teamstat = TeamGameStat(team_game_stat_id=teamstat_id,
+            create_dt=datetime.datetime.utcnow())
+
+    # all team game modes have a score, so we'll zero that out always
+    teamstat.score = 0
+
+    if game_type_cd in 'ca' 'ft' 'lms' 'ka':
+        teamstat.rounds = 0
+
+    if game_type_cd == 'ctf':
+        teamstat.caps = 0
+
+    return teamstat
+
+
+def create_team_stat(session, game, events):
+    """Team stats handler for all game types"""
+
+    try:
+        teamstat = create_default_team_stat(session, game.game_type_cd)
+        teamstat.game_id = game.game_id
+
+        # we should have a team ID if we have a 'Q' event
+        if re.match(r'^team#\d+$', events.get('Q', '')):
+            team = int(events.get('Q').replace('team#', ''))
+            teamstat.team = team
+
+        # gametype-specific stuff is handled here. if passed to us, we store it
+        for (key,value) in events.items():
+            if key == 'scoreboard-score': teamstat.score = int(round(float(value)))
+            if key == 'scoreboard-caps': teamstat.caps = int(value)
+            if key == 'scoreboard-goals': teamstat.caps = int(value)
+            if key == 'scoreboard-rounds': teamstat.rounds = int(value)
+
+        session.add(teamstat)
+    except Exception as e:
+        raise e
+
+    return teamstat
+
+
+def create_weapon_stats(session, game_meta, game, player, pgstat, events):
+    """Weapon stats handler for all game types"""
     pwstats = []
 
     # Version 1 of stats submissions doubled the data sent.
     pwstats = []
 
     # Version 1 of stats submissions doubled the data sent.
@@ -508,10 +1045,15 @@ def create_player_weapon_stats(session=None, player=None,
     except:
         is_doubled = False
 
     except:
         is_doubled = False
 
-    for (key,value) in player_events.items():
+    for (key,value) in events.items():
         matched = re.search("acc-(.*?)-cnt-fired", key)
         if matched:
             weapon_cd = matched.group(1)
         matched = re.search("acc-(.*?)-cnt-fired", key)
         if matched:
             weapon_cd = matched.group(1)
+
+            # Weapon names changed for 0.8. We'll convert the old
+            # ones to use the new scheme as well.
+            mapped_weapon_cd = weapon_map.get(weapon_cd, weapon_cd)
+
             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
             pwstat_id = session.execute(seq)
             pwstat = PlayerWeaponStat()
             seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
             pwstat_id = session.execute(seq)
             pwstat = PlayerWeaponStat()
@@ -519,28 +1061,28 @@ def create_player_weapon_stats(session=None, player=None,
             pwstat.player_id = player.player_id
             pwstat.game_id = game.game_id
             pwstat.player_game_stat_id = pgstat.player_game_stat_id
             pwstat.player_id = player.player_id
             pwstat.game_id = game.game_id
             pwstat.player_game_stat_id = pgstat.player_game_stat_id
-            pwstat.weapon_cd = weapon_cd
+            pwstat.weapon_cd = mapped_weapon_cd
 
 
-            if 'n' in player_events:
-                pwstat.nick = player_events['n']
+            if 'n' in events:
+                pwstat.nick = events['n']
             else:
             else:
-                pwstat.nick = player_events['P']
+                pwstat.nick = events['P']
 
 
-            if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
+            if 'acc-' + weapon_cd + '-cnt-fired' in events:
                 pwstat.fired = int(round(float(
                 pwstat.fired = int(round(float(
-                        player_events['acc-' + weapon_cd + '-cnt-fired'])))
-            if 'acc-' + weapon_cd + '-fired' in player_events:
+                        events['acc-' + weapon_cd + '-cnt-fired'])))
+            if 'acc-' + weapon_cd + '-fired' in events:
                 pwstat.max = int(round(float(
                 pwstat.max = int(round(float(
-                        player_events['acc-' + weapon_cd + '-fired'])))
-            if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
+                        events['acc-' + weapon_cd + '-fired'])))
+            if 'acc-' + weapon_cd + '-cnt-hit' in events:
                 pwstat.hit = int(round(float(
                 pwstat.hit = int(round(float(
-                        player_events['acc-' + weapon_cd + '-cnt-hit'])))
-            if 'acc-' + weapon_cd + '-hit' in player_events:
+                        events['acc-' + weapon_cd + '-cnt-hit'])))
+            if 'acc-' + weapon_cd + '-hit' in events:
                 pwstat.actual = int(round(float(
                 pwstat.actual = int(round(float(
-                        player_events['acc-' + weapon_cd + '-hit'])))
-            if 'acc-' + weapon_cd + '-frags' in player_events:
+                        events['acc-' + weapon_cd + '-hit'])))
+            if 'acc-' + weapon_cd + '-frags' in events:
                 pwstat.frags = int(round(float(
                 pwstat.frags = int(round(float(
-                        player_events['acc-' + weapon_cd + '-frags'])))
+                        events['acc-' + weapon_cd + '-frags'])))
 
             if is_doubled:
                 pwstat.fired = pwstat.fired/2
 
             if is_doubled:
                 pwstat.fired = pwstat.fired/2
@@ -555,76 +1097,23 @@ def create_player_weapon_stats(session=None, player=None,
     return pwstats
 
 
     return pwstats
 
 
-def parse_body(request):
+def get_ranks(session, player_ids, game_type_cd):
     """
     """
-    Parses the POST request body for a stats submission
+    Gets the rank entries for all players in the given list, returning a dict
+    of player_id -> PlayerRank instance. The rank entry corresponds to the
+    game type of the parameter passed in as well.
     """
     """
-    # storage vars for the request body
-    game_meta = {}
-    player_events = {}
-    current_team = None
-    players = []
-
-    for line in request.body.split('\n'):
-        try:
-            (key, value) = line.strip().split(' ', 1)
-
-            # Server (S) and Nick (n) fields can have international characters.
-            # We convert to UTF-8.
-            if key in 'S' 'n':
-                value = unicode(value, 'utf-8')
-
-            if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I' 'D':
-                game_meta[key] = value
+    ranks = {}
+    for pr in session.query(PlayerRank).\
+            filter(PlayerRank.player_id.in_(player_ids)).\
+            filter(PlayerRank.game_type_cd == game_type_cd).\
+            all():
+                ranks[pr.player_id] = pr
 
 
-            if key == 'P':
-                # if we were working on a player record already, append
-                # it and work on a new one (only set team info)
-                if len(player_events) != 0:
-                    players.append(player_events)
-                    player_events = {}
+    return ranks
 
 
-                player_events[key] = value
 
 
-            if key == 'e':
-                (subkey, subvalue) = value.split(' ', 1)
-                player_events[subkey] = subvalue
-            if key == 'n':
-                player_events[key] = value
-            if key == 't':
-                player_events[key] = value
-        except:
-            # no key/value pair - move on to the next line
-            pass
-
-    # add the last player we were working on
-    if len(player_events) > 0:
-        players.append(player_events)
-
-    return (game_meta, players)
-
-
-def create_player_stats(session=None, player=None, game=None, 
-        player_events=None, game_meta=None):
-    """
-    Creates player game and weapon stats according to what type of player
-    """
-    pgstat = create_player_game_stat(session=session, 
-        player=player, game=game, player_events=player_events)
-
-    # fastest cap "upsert"
-    if game.game_type_cd == 'ctf' and pgstat.fastest_cap is not None:
-        update_fastest_cap(session, pgstat.player_id, game.game_id, 
-                game.map_id, pgstat.fastest_cap)
-
-    # bots don't get weapon stats. sorry, bots!
-    if not re.search('^bot#\d+$', player_events['P']):
-        create_player_weapon_stats(session=session, 
-            player=player, game=game, pgstat=pgstat,
-            player_events=player_events, game_meta=game_meta)
-
-
-def stats_submit(request):
+def submit_stats(request):
     """
     Entry handler for POST stats submissions.
     """
     """
     Entry handler for POST stats submissions.
     """
@@ -636,94 +1125,112 @@ def stats_submit(request):
                 "----- END REQUEST BODY -----\n\n")
 
         (idfp, status) = verify_request(request)
                 "----- END REQUEST BODY -----\n\n")
 
         (idfp, status) = verify_request(request)
-        if verify_requests(request.registry.settings):
-            if not idfp:
-                log.debug("ERROR: Unverified request")
-                raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
+        (game_meta, raw_players, raw_teams) = parse_stats_submission(request.body)
+        revision = game_meta.get('R', 'unknown')
+        duration = game_meta.get('D', None)
 
 
-        (game_meta, players) = parse_body(request)
+        # only players present at the end of the match are eligible for stats
+        raw_players = filter(played_in_game, raw_players)
 
 
-        if not has_required_metadata(game_meta):
-            log.debug("ERROR: Required game meta missing")
-            raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
-
-        if not is_supported_gametype(game_meta['G']):
-            log.debug("ERROR: Unsupported gametype")
-            raise pyramid.httpexceptions.HTTPOk("OK")
-
-        if not has_minimum_real_players(request.registry.settings, players):
-            log.debug("ERROR: Not enough real players")
-            raise pyramid.httpexceptions.HTTPOk("OK")
-
-        if is_blank_game(players):
-            log.debug("ERROR: Blank game")
-            raise pyramid.httpexceptions.HTTPOk("OK")
+        do_precondition_checks(request, game_meta, raw_players)
 
         # the "duel" gametype is fake
 
         # the "duel" gametype is fake
-        if num_real_players(players, count_bots=True) == 2 and \
-                game_meta['G'] == 'dm':
+        if len(raw_players) == 2 \
+            and num_real_players(raw_players) == 2 \
+            and game_meta['G'] == 'dm':
             game_meta['G'] = 'duel'
 
             game_meta['G'] = 'duel'
 
-
-        # fix for DTG, who didn't #ifdef WATERMARK to set the revision info
-        try:
-            revision = game_meta['R']
-        except:
-            revision = "unknown"
-
         #----------------------------------------------------------------------
         #----------------------------------------------------------------------
-        # This ends the "precondition" section of sanity checks. All
-        # functions not requiring a database connection go ABOVE HERE.
+        # Actual setup (inserts/updates) below here
         #----------------------------------------------------------------------
         session = DBSession()
 
         #----------------------------------------------------------------------
         session = DBSession()
 
-        server = get_or_create_server(session=session, hashkey=idfp, 
-                name=game_meta['S'], revision=revision,
-                ip_addr=get_remote_addr(request))
+        game_type_cd = game_meta['G']
+
+        # All game types create Game, Server, Map, and Player records
+        # the same way.
+        server = get_or_create_server(
+                session      = session,
+                hashkey      = idfp,
+                name         = game_meta['S'],
+                revision     = revision,
+                ip_addr      = get_remote_addr(request),
+                port         = game_meta.get('U', None),
+                impure_cvars = game_meta.get('C', 0))
+
+        gmap = get_or_create_map(
+                session = session,
+                name    = game_meta['M'])
+
+        game = create_game(
+                session      = session,
+                start_dt     = datetime.datetime.utcnow(),
+                server_id    = server.server_id,
+                game_type_cd = game_type_cd,
+                map_id       = gmap.map_id,
+                match_id     = game_meta['I'],
+                duration     = duration,
+                mod          = game_meta.get('O', None))
+
+        # keep track of the players we've seen
+        player_ids = []
+        pgstats = []
+        hashkeys = {}
+        for events in raw_players:
+            player = get_or_create_player(
+                session = session,
+                hashkey = events['P'],
+                nick    = events.get('n', None))
+
+            pgstat = create_game_stat(session, game_meta, game, server,
+                    gmap, player, events)
+            pgstats.append(pgstat)
+
+            if player.player_id > 1:
+                anticheats = create_anticheats(session, pgstat, game, player, events)
+
+            if player.player_id > 2:
+                player_ids.append(player.player_id)
+                hashkeys[player.player_id] = events['P']
+
+            if should_do_weapon_stats(game_type_cd) and player.player_id > 1:
+                pwstats = create_weapon_stats(session, game_meta, game, player,
+                        pgstat, events)
+
+        # store them on games for easy access
+        game.players = player_ids
+
+        for events in raw_teams:
+            try:
+                teamstat = create_team_stat(session, game, events)
+            except Exception as e:
+                raise e
+
+        if server.elo_ind and gametype_elo_eligible(game_type_cd):
+            ep = EloProcessor(session, game, pgstats)
+            ep.save(session)
 
 
-        gmap = get_or_create_map(session=session, name=game_meta['M'])
+        session.commit()
+        log.debug('Success! Stats recorded.')
 
 
-        # duration is optional
-        if 'D' in game_meta:
-            duration = game_meta['D']
-        else:
-            duration = None
-
-        game = create_game(session=session, 
-                start_dt=datetime.datetime.utcnow(),
-                #start_dt=datetime.datetime(
-                    #*time.gmtime(float(game_meta['T']))[:6]), 
-                server_id=server.server_id, game_type_cd=game_meta['G'], 
-                   map_id=gmap.map_id, match_id=game_meta['I'],
-                   duration=duration)
-
-        # find or create a record for each player
-        # and add stats for each if they were present at the end
-        # of the game
-        for player_events in players:
-            if 'n' in player_events:
-                nick = player_events['n']
-            else:
-                nick = None
+        # ranks are fetched after we've done the "real" processing
+        ranks = get_ranks(session, player_ids, game_type_cd)
 
 
-            if 'matches' in player_events and 'scoreboardvalid' \
-                in player_events:
-                player = get_or_create_player(session=session, 
-                    hashkey=player_events['P'], nick=nick)
-                log.debug('Creating stats for %s' % player_events['P'])
-                create_player_stats(session=session, player=player, game=game, 
-                        player_events=player_events, game_meta=game_meta)
+        # plain text response
+        request.response.content_type = 'text/plain'
 
 
-        # update elos
-        try:
-            process_elos(game, session)
-        except Exception as e:
-            log.debug('Error (non-fatal): elo processing failed.')
+        return {
+                "now"        : calendar.timegm(datetime.datetime.utcnow().timetuple()),
+                "server"     : server,
+                "game"       : game,
+                "gmap"       : gmap,
+                "player_ids" : player_ids,
+                "hashkeys"   : hashkeys,
+                "elos"       : ep.wip,
+                "ranks"      : ranks,
+        }
 
 
-        session.commit()
-        log.debug('Success! Stats recorded.')
-        return Response('200 OK')
     except Exception as e:
         if session:
             session.rollback()
     except Exception as e:
         if session:
             session.rollback()
-        return e
+        raise e