X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=events.cpp;h=9826f0ecbbc487e8a23ec1932f4b4b52e0d66a39;hb=6506c1cff280fa5eecc29f59de9ad21b8dddecc3;hp=bce262555feda49dbd9718e0d4caa751750158f5;hpb=ab2491c55e8de3f207476637032ccf194512cb85;p=pkanalytics diff --git a/events.cpp b/events.cpp index bce2625..9826f0e 100644 --- a/events.cpp +++ b/events.cpp @@ -10,7 +10,7 @@ using namespace std; string format_timestamp(uint64_t pos); -EventsModel::EventsModel(sqlite3 *db) : db(db) +EventsModel::EventsModel(sqlite3 *db, int match_id) : db(db), match_id(match_id) { load_data(); } @@ -24,7 +24,7 @@ QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int r if (section == 0) { return "Time"; } else if (section == 1) { - return "Player"; + return "Who/what"; } else { return "Type"; } @@ -38,19 +38,50 @@ QVariant EventsModel::data(const QModelIndex &index, int role) const if (role != Qt::DisplayRole) { return QVariant(); } + const Event &e = events[index.row()]; if (index.column() == 0) { - return QString::fromUtf8(format_timestamp(events[index.row()].t)); + return QString::fromUtf8(format_timestamp(e.t)); } else if (index.column() == 1) { - optional player_id = events[index.row()].player_id; + optional player_id = e.player_id; + optional formation_id = e.formation_id; if (player_id) { auto p_it = players.find(*player_id); const Player &p = p_it->second; return QString::fromUtf8(p.name + " (" + p.number + ")"); + } else if (formation_id) { + auto f_it = formations.find(*formation_id); + const Formation &f = f_it->second; + return QString::fromUtf8(f.name); + } else if (e.type == "formation_offense" || e.type == "formation_defense") { + return "(None/unknown)"; } else { return QVariant(); } } else if (index.column() == 2) { - return QString::fromUtf8(events[index.row()].type); + string type = e.type; + type[0] = toupper(e.type[0]); + for (char &ch : type) { + if (ch == '_') { + ch = ' '; + } + } + + // Various fixups. + if (type == "Pull oob") { + type = "Pull OOB"; + } else if (type == "Formation defense") { + type = "Defensive formation"; + } else if (type == "Formation offense") { + type = "Offensive formation"; + } else if (type == "Set offense") { + type = "On offense"; + } else if (type == "Set defense") { + type = "On defense"; + } else if (type == "Catch") { + type = "Catch/take"; + } + + return QString::fromUtf8(type); } return QVariant(); } @@ -60,13 +91,14 @@ void EventsModel::load_data() players.clear(); events.clear(); - // Read the players. + // Read the players. (The ordering is used to build the order map.) sqlite3_stmt *stmt; - int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player", -1, &stmt, 0); + int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player ORDER BY gender, (number+0), number", -1, &stmt, 0); if (ret != SQLITE_OK) { fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db)); abort(); } + int order = 0; for ( ;; ) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { @@ -75,6 +107,33 @@ void EventsModel::load_data() p.number = (const char *)sqlite3_column_text(stmt, 1); p.name = (const char *) sqlite3_column_text(stmt, 2); players[p.player_id] = std::move(p); + player_ordering[p.player_id] = order++; + } else if (ret == SQLITE_DONE) { + break; + } else { + fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db)); + abort(); + } + } + ret = sqlite3_finalize(stmt); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db)); + abort(); + } + + // Read the formations. + ret = sqlite3_prepare_v2(db, "SELECT formation, name FROM formation", -1, &stmt, 0); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db)); + abort(); + } + for ( ;; ) { + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) { + Formation f; + f.formation_id = sqlite3_column_int(stmt, 0); + f.name = (const char *) sqlite3_column_text(stmt, 1); + formations[f.formation_id] = std::move(f); } else if (ret == SQLITE_DONE) { break; } else { @@ -89,11 +148,12 @@ void EventsModel::load_data() } // Read the events. - ret = sqlite3_prepare_v2(db, "SELECT event, t, player, type FROM event ORDER BY t", -1, &stmt, 0); + ret = sqlite3_prepare_v2(db, "SELECT event, t, player, formation, type FROM event WHERE match=? ORDER BY t", -1, &stmt, 0); if (ret != SQLITE_OK) { fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db)); abort(); } + sqlite3_bind_int64(stmt, 1, match_id); for ( ;; ) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { @@ -103,7 +163,10 @@ void EventsModel::load_data() if (sqlite3_column_type(stmt, 2) == SQLITE_INTEGER) { // Non-NULL. e.player_id = sqlite3_column_int(stmt, 2); } - e.type = (const char *)sqlite3_column_text(stmt, 3); + if (sqlite3_column_type(stmt, 3) == SQLITE_INTEGER) { // Non-NULL. + e.formation_id = sqlite3_column_int(stmt, 3); + } + e.type = (const char *)sqlite3_column_text(stmt, 4); events.push_back(std::move(e)); } else if (ret == SQLITE_DONE) { break; @@ -119,7 +182,7 @@ void EventsModel::load_data() } } -unsigned EventsModel::insert_event(uint64_t t, optional player_id, const string &type) +unsigned EventsModel::insert_event(uint64_t t, optional player_id, optional formation_id, const string &type) { auto it = lower_bound(events.begin(), events.end(), t, [](const Event &e, uint64_t t) { return e.t < t; }); @@ -129,6 +192,7 @@ unsigned EventsModel::insert_event(uint64_t t, optional player_id, const st Event e; e.t = t; e.player_id = player_id; + e.formation_id = formation_id; e.type = type; events.insert(events.begin() + pos, e); @@ -136,19 +200,25 @@ unsigned EventsModel::insert_event(uint64_t t, optional player_id, const st // Insert the new row into the database. sqlite3_stmt *stmt; - int ret = sqlite3_prepare_v2(db, "INSERT INTO event (t, player, type) VALUES (?, ?, ?)", -1, &stmt, 0); + int ret = sqlite3_prepare_v2(db, "INSERT INTO event (match, t, player, formation, type) VALUES (?, ?, ?, ?, ?)", -1, &stmt, 0); if (ret != SQLITE_OK) { fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db)); abort(); } - sqlite3_bind_int64(stmt, 1, t); + sqlite3_bind_int64(stmt, 1, match_id); + sqlite3_bind_int64(stmt, 2, t); if (player_id) { - sqlite3_bind_int64(stmt, 2, *player_id); + sqlite3_bind_int64(stmt, 3, *player_id); } else { - sqlite3_bind_null(stmt, 2); + sqlite3_bind_null(stmt, 3); } - sqlite3_bind_text(stmt, 3, type.data(), type.size(), SQLITE_STATIC); + if (formation_id) { + sqlite3_bind_int64(stmt, 4, *formation_id); + } else { + sqlite3_bind_null(stmt, 4); + } + sqlite3_bind_text(stmt, 5, type.data(), type.size(), SQLITE_STATIC); ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { @@ -225,39 +295,66 @@ void EventsModel::set_event_type(unsigned pos, const string &type) } } +unsigned EventsModel::get_last_event_pos(uint64_t t) const +{ + // upper_bound() gives first where e.t > t, + // and the one before that is the one we want. + auto it = upper_bound(events.begin(), events.end(), t, + [](uint64_t t, const Event &e) { return t < e.t; }); + if (it == events.begin()) { + return 0; + } else { + return distance(events.begin(), it - 1); + } +} + EventsModel::Status EventsModel::get_status_at(uint64_t t) { Status s; s.our_score = 0; s.their_score = 0; - s.offense = true; + s.attack_state = Status::NOT_STARTED; + s.offensive_formation = 0; + s.defensive_formation = 0; s.stoppage = false; - s.should_pull = true; + s.pull_state = Status::SHOULD_PULL; uint64_t last_gained_possession = 0; uint64_t last_stoppage = 0; uint64_t time_spent_in_stoppage = 0; unsigned num_touches = 0; + + auto set_offense = [&s] { s.attack_state = Status::OFFENSE; }; + auto set_defense = [&s] { s.attack_state = Status::DEFENSE; }; + for (const Event &e : events) { if (e.t > t) { break; } if (e.type == "goal" || e.type == "their_goal") { - s.should_pull = true; - } else if (e.type == "in" || e.type == "out" || e.type == "stoppage" || e.type == "restart" || e.type == "unknown") { + s.pull_state = Status::SHOULD_PULL; + } else if (e.type == "in" || e.type == "out" || e.type == "stoppage" || e.type == "restart" || e.type == "unknown" || e.type == "set_defense" || e.type == "set_offense") { // No effect on pull status. + } else if (e.type == "pull") { + s.pull_state = Status::PULL_IN_AIR; } else { - s.should_pull = false; + s.pull_state = Status::NOT_PULLING; // Includes pull_landed and pull_oob. + } + + if (e.type == "set_offense") { + set_offense(); + } else if (e.type == "set_defense") { + set_defense(); } if (e.type == "goal") { ++s.our_score; - s.offense = false; + set_defense(); num_touches = 0; } if (e.type == "their_goal") { ++s.their_score; - s.offense = true; + set_offense(); num_touches = 0; } if (e.type == "catch") { @@ -269,17 +366,17 @@ EventsModel::Status EventsModel::get_status_at(uint64_t t) } if (e.type == "interception") { num_touches = 1; - s.offense = true; + set_offense(); last_gained_possession = e.t; time_spent_in_stoppage = 0; } if (e.type == "defense" || e.type == "their_throwaway") { - s.offense = true; + set_offense(); num_touches = 0; time_spent_in_stoppage = 0; } if (e.type == "drop" || e.type == "throwaway") { - s.offense = false; + set_defense(); num_touches = 0; } if (e.type == "stoppage") { @@ -293,14 +390,28 @@ EventsModel::Status EventsModel::get_status_at(uint64_t t) last_stoppage = 0; } } + if (e.type == "formation_offense") { + if (e.formation_id) { + s.offensive_formation = *e.formation_id; + } else { + s.offensive_formation = 0; + } + } + if (e.type == "formation_defense") { + if (e.formation_id) { + s.defensive_formation = *e.formation_id; + } else { + s.defensive_formation = 0; + } + } } if (s.stoppage && last_stoppage != 0) { time_spent_in_stoppage += (t - last_stoppage); } s.num_passes = (num_touches == 0) ? 0 : num_touches - 1; - s.possession_sec = (s.offense && last_gained_possession != 0 && num_touches != 0) ? (t - last_gained_possession - time_spent_in_stoppage) / 1000 : 0; - s.stoppage_sec = (s.offense && last_gained_possession != 0 && num_touches != 0) ? time_spent_in_stoppage / 1000 : 0; + s.possession_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? (t - last_gained_possession - time_spent_in_stoppage) / 1000 : 0; + s.stoppage_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? time_spent_in_stoppage / 1000 : 0; return s; } @@ -332,7 +443,7 @@ void EventsModel::set_team_at(uint64_t t, const set &new_team) if (e.t > t) { break; } - if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset") { + if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset" || e.type == "set_offense" || e.type == "set_defense") { backdate_point = e.t + 1; } if (e.player_id.has_value() && !new_team.count(*e.player_id)) { @@ -356,12 +467,49 @@ void EventsModel::set_team_at(uint64_t t, const set &new_team) set old_team = get_team_at(backdate_point); for (int player_id : old_team) { if (!new_team.count(player_id)) { - insert_event(backdate_point, player_id, "out"); + insert_event(backdate_point, player_id, nullopt, "out"); } } for (int player_id : new_team) { if (!old_team.count(player_id)) { - insert_event(backdate_point, player_id, "in"); + insert_event(backdate_point, player_id, nullopt, "in"); } } } + +void EventsModel::set_formation_at(uint64_t t, bool offense, unsigned formation) +{ + // If there's another goal/stoppage no more than 20 seconds ago, + // we assume that the formation started at that point (it just took + // the operator a bit of time to see it). If not, we assume we + // changed in the middle of a point. + uint64_t backdate_point = 0; + for (const Event &e : events) { + if (e.t > t) { + break; + } + if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset" || e.type == "set_offense" || e.type == "set_defense" || e.type == "in" || e.type == "out" || e.type == "pull" || e.type == "their_pull") { + backdate_point = e.t + 1; + } + if (e.type == "formation_offense" || e.type == "formation_defense") { + backdate_point = 0; + } + } + if (backdate_point != 0 && t - backdate_point < 20000) { + t = backdate_point; + } + if (offense) { + insert_event(t, nullopt, formation == 0 ? nullopt : optional{formation}, "formation_offense"); + } else { + insert_event(t, nullopt, formation == 0 ? nullopt : optional{formation}, "formation_defense"); + } +} + +vector EventsModel::sort_team(const set &team) const +{ + vector ret(team.begin(), team.end()); + std::sort(ret.begin(), ret.end(), [this](int a, int b) { + return player_ordering.find(a)->second < player_ordering.find(b)->second; + }); + return ret; +}