diff --git a/cluster.sql b/cluster.sql index c9ed541..fbed6aa 100644 --- a/cluster.sql +++ b/cluster.sql @@ -7,6 +7,7 @@ CLUSTER VERBOSE player_stats_backing USING player_stats_pkey; CLUSTER VERBOSE player_stats_extra USING player_stats_extra_pkey; +CLUSTER VERBOSE killstreak USING killstreak_pkey; CLUSTER VERBOSE medic_stats USING medic_stats_pkey; CLUSTER VERBOSE class_stats USING class_stats_pkey; CLUSTER VERBOSE weapon_stats USING weapon_stats_pkey; diff --git a/trends/importer/logs.py b/trends/importer/logs.py index ab8955e..c0df674 100644 --- a/trends/importer/logs.py +++ b/trends/importer/logs.py @@ -357,6 +357,23 @@ def import_log(c, logid, log): %(dmg)s, %(avg_dmg)s, %(shots)s, %(hits)s );""", weapon) + for killstreak in log.get('killstreaks', ()): + try: + steamid = SteamID(killstreak['steamid']) + except ValueError: + continue + + try: + c.execute("SAVEPOINT before_killstreak;") + c.execute("""INSERT INTO killstreak ( + logid, playerid, time, kills + ) VALUES ( + %s, (SELECT playerid FROM player WHERE steamid64 = %s), %s, %s + );""", (logid, steamid, killstreak['time'], killstreak['streak'])) + except (psycopg2.errors.ForeignKeyViolation, psycopg2.errors.NotNullViolation): + logging.warning("%s is only present in killstreak for log %s", steamid, logid) + c.execute("ROLLBACK TO SAVEPOINT before_killstreak;") + for (seq, msg) in enumerate(log['chat']): try: steamid = SteamID(msg['steamid']) if msg['steamid'] != 'Console' else None @@ -784,6 +801,9 @@ def import_logs(c, fetcher, update_only): PRIMARY KEY ({}) );""".format(table[0], table[0], table[1])) # This doesn't include foreign keys, so include some which we want to handle in import_log + cur.execute("""ALTER TABLE killstreak + ADD FOREIGN KEY (logid, playerid) + REFERENCES player_stats_backing (logid, playerid);""") cur.execute("""ALTER TABLE heal_stats ADD FOREIGN KEY (logid, healer) REFERENCES player_stats_backing (logid, playerid), diff --git a/trends/migrations/killstreak.py b/trends/migrations/killstreak.py new file mode 100644 index 0000000..aeebdce --- /dev/null +++ b/trends/migrations/killstreak.py @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (C) 2024 Sean Anderson + +import logging +import sys + +import psycopg2.extras + +from ..importer.cli import init_logging +from ..sql import db_connect +from ..steamid import SteamID +from ..util import chunk + +def migrate(database): + init_logging(logging.DEBUG) + with db_connect(database) as c: + with c.cursor() as cur: + logging.info("BEGIN") + cur.execute("BEGIN"); + logging.info("CREATE TABLE killstreak") + cur.execute(""" + CREATE TABLE killstreak ( + logid INT NOT NULL, + playerid INT NOT NULL, + time INT NOT NULL, + kills INT NOT NULL CHECK (kills > 0), + PRIMARY KEY (playerid, logid, time) + );""") + + with c.cursor(name='streaks') as streaks: + streaks.execute(""" + SELECT + logid, + streak -> 'steamid' AS steamid, + streak -> 'time' AS time, + streak -> 'streak' AS kills + FROM (SELECT + logid, + json_array_elements(data -> 'killstreaks') AS streak + FROM log_json + ) AS killstreak;""") + for streaks in chunk(streaks, streaks.itersize): + values = [] + for streak in streaks: + try: + values.append((streak[0], SteamID(streak[1]), streak[2], streak[3])) + except ValueError: + continue + logging.info("INSERT killstreak") + psycopg2.extras.execute_values(cur, """ + INSERT INTO killstreak ( + logid, + playerid, + time, + kills + ) SELECT + logid, + playerid, + time, + killstreak.kills + FROM (VALUES %s) AS killstreak (logid, steamid64, time, kills) + JOIN player USING (steamid64) + JOIN player_stats USING (logid, playerid) + ON CONFLICT DO NOTHING;""", + values, "(%s, %s, %s, %s)") + + logging.info("ALTER TABLE") + cur.execute(""" + ALTER TABLE killstreak + ADD FOREIGN KEY (logid, playerid) + REFERENCES player_stats_backing (logid, playerid);""") + logging.info("COMMIT") + cur.execute("COMMIT;") + +if __name__ == "__main__": + migrate(sys.argv[1]) diff --git a/trends/schema.sql b/trends/schema.sql index d6cf8e0..7374d6c 100644 --- a/trends/schema.sql +++ b/trends/schema.sql @@ -608,6 +608,15 @@ CREATE TABLE IF NOT EXISTS player_stats_extra ( CHECK ((dmg_real ISNULL) = (dt_real ISNULL)) ); +CREATE TABLE IF NOT EXISTS killstreak ( + logid INT NOT NULL, + playerid INT NOT NULL, + time INT NOT NULL, + kills INT NOT NULL CHECK (kills > 0), + PRIMARY KEY (playerid, logid, time), + FOREIGN KEY (logid, playerid) REFERENCES player_stats_backing (logid, playerid) +); + CREATE TABLE IF NOT EXISTS medic_stats ( logid INT NOT NULL, playerid INT NOT NULL, diff --git a/trends/site/player.py b/trends/site/player.py index f4eed7b..ee15143 100644 --- a/trends/site/player.py +++ b/trends/site/player.py @@ -85,6 +85,8 @@ def get_overview(): 'headshots': "headshots", 'headshots_hit': "headshots_hit", 'sentries': "sentries", + # Not in extra but close enough... + 'mks': "mks", } # Columns not in player_stats @@ -175,6 +177,13 @@ def get_logs(c, playerid, filters, extra=False, order_clause="logid DESC", limit LEFT JOIN heal_stats_given AS hsg USING (logid, playerid) LEFT JOIN heal_stats_received AS hsr USING (logid, playerid) {"LEFT JOIN player_stats_extra AS pse USING (logid, playerid)" if extra else ""} + {'''LEFT JOIN (SELECT + logid, + playerid, + max(kills) AS mks + FROM killstreak + GROUP BY logid, playerid + ) AS ks USING (logid, playerid)''' if extra else ""} WHERE ps.playerid = %(playerid)s ORDER BY {order_clause} NULLS LAST, logid DESC LIMIT %(limit)s OFFSET %(offset)s;""", diff --git a/trends/site/root.py b/trends/site/root.py index b6f4014..49eb0a3 100644 --- a/trends/site/root.py +++ b/trends/site/root.py @@ -481,6 +481,35 @@ def player_key(player): GROUP BY event;""", params) events = { event_stats['event']: event_stats['events'] for event_stats in events.fetchall() } + killstreaks = db.cursor() + killstreaks.execute("""SELECT + logid, + title, + array_agg(json_build_object( + 'team', team, + 'steamid64', steamid64, + 'name', name, + 'time', killstreak.time, + 'kills', kills + ) ORDER BY killstreak.time) AS killstreaks + FROM (SELECT + logid, + playerid, + team, + name, + time, + killstreak.kills + FROM killstreak + JOIN player_stats USING (logid, playerid) + JOIN name USING (nameid) + WHERE logid IN %(logids)s + ) AS killstreak + JOIN log USING (logid) + JOIN player USING (playerid) + GROUP BY logid, title + ORDER BY array_position(%(llogids)s, logid);""", params) + killstreaks = killstreaks.fetchall() + chats = db.cursor() chats.execute("""SELECT logid, @@ -510,7 +539,8 @@ def player_key(player): return flask.render_template("log.html", logids=logids, logs=logs, matches=matches, rounds=rounds.fetchall(), players=players, totals=totals, - medics=medics, events=events, chats=chats) + medics=medics, events=events, killstreaks=killstreaks, + chats=chats) metrics_extension = PrometheusMetrics.for_app_factory(group_by='endpoint', path=None) diff --git a/trends/site/templates/log.html b/trends/site/templates/log.html index b9c2995..44c1830 100644 --- a/trends/site/templates/log.html +++ b/trends/site/templates/log.html @@ -627,6 +627,33 @@

Deaths

{{ event(events['death']) }}

Assists

{{ event(events['assist']) }} + {% if killstreaks | count %} +

Killstreaks

+ {% for killstreak in killstreaks %} +

{{ killstreak['title'] }}

+ + + + + + + + + {% for streak in killstreak['killstreaks'] %} + + {{ duration_col(streak.time) }} + + + + + {% endfor %} + +
TimeTeamPlayerKills
{{ streak.team }} + {{ playerlink(streak.steamid64, streak.name, + players[streak.steamid64]) }} + {{ streak.kills }}
+ {% endfor %} + {% endif %}

Chat

{% for chat in chats %}

{{ chat['title'] }}

diff --git a/trends/site/templates/macros/logs.html b/trends/site/templates/macros/logs.html index 6b7caee..3e592ae 100644 --- a/trends/site/templates/macros/logs.html +++ b/trends/site/templates/macros/logs.html @@ -28,6 +28,7 @@ HR/M Acc {% if extra %} + LKS K/1 AS MS @@ -86,6 +87,7 @@ {{ optint(log['hpm_recieved']) }} {{ optformat("{:.0%}", log['acc']) }} {% if extra %} + {{ optint(log.mks) }} {{ optint(log.lks) }} {{ optint(log.airshots) }} {{ optint(log.medkits) }} diff --git a/trends/site/templates/player/logs.html b/trends/site/templates/player/logs.html index 6410c08..4dcc0c3 100644 --- a/trends/site/templates/player/logs.html +++ b/trends/site/templates/player/logs.html @@ -30,6 +30,7 @@

Logs

'hrm': "Heals received per minute", 'acc': "Accuracy", 'date': "Date", + 'mks': "Longest killstreak", 'lks': "Most kills in one life", 'airshots': "Airshots", 'medkits': "Medkit score", diff --git a/trends/sql.py b/trends/sql.py index cc148a1..eec44b5 100644 --- a/trends/sql.py +++ b/trends/sql.py @@ -110,6 +110,7 @@ def db_init(c): log_tables = (('log', 'logid'), ('log_json', 'logid'), ('round', 'logid, seq'), ('player_stats_backing', 'playerid, logid'), ('player_stats_extra', 'playerid, logid'), + ('killstreak', 'playerid, logid, time'), ('medic_stats', 'playerid, logid'), ('heal_stats', 'logid, healer, healee'), ('class_stats', 'playerid, logid, classid'),