]> git.sesse.net Git - pkanalytics/blob - events.cpp
Make the events view scroll the current event into view when playing.
[pkanalytics] / events.cpp
1 #include <algorithm>
2 #include <string>
3 #include <map>
4 #include <vector>
5 #include <optional>
6 #include <sqlite3.h>
7 #include "events.h"
8
9 using namespace std;
10
11 string format_timestamp(uint64_t pos);
12
13 EventsModel::EventsModel(sqlite3 *db) : db(db)
14 {
15         load_data();
16 }
17
18 QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int role) const
19 {
20         if (role != Qt::DisplayRole) {
21                 return QVariant();
22         }
23         if (orientation == Qt::Horizontal) {
24                 if (section == 0) {
25                         return "Time";
26                 } else if (section == 1) {
27                         return "Player";
28                 } else {
29                         return "Type";
30                 }
31         } else {
32                 return "";
33         }
34 }
35
36 QVariant EventsModel::data(const QModelIndex &index, int role) const
37 {
38         if (role != Qt::DisplayRole) {
39                 return QVariant();
40         }
41         if (index.column() == 0) {
42                 return QString::fromUtf8(format_timestamp(events[index.row()].t));
43         } else if (index.column() == 1) {
44                 optional<int> player_id = events[index.row()].player_id;
45                 if (player_id) {
46                         auto p_it = players.find(*player_id);
47                         const Player &p = p_it->second;
48                         return QString::fromUtf8(p.name + " (" + p.number + ")");
49                 } else {
50                         return QVariant();
51                 }
52         } else if (index.column() == 2) {
53                 return QString::fromUtf8(events[index.row()].type);
54         }
55         return QVariant();
56 }
57
58 void EventsModel::load_data()
59 {
60         players.clear();
61         events.clear();
62
63         // Read the players.
64         sqlite3_stmt *stmt;
65         int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player", -1, &stmt, 0);
66         if (ret != SQLITE_OK) {
67                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
68                 abort();
69         }
70         for ( ;; ) {
71                 ret = sqlite3_step(stmt);
72                 if (ret == SQLITE_ROW) {
73                         Player p;
74                         p.player_id = sqlite3_column_int(stmt, 0);
75                         p.number = (const char *)sqlite3_column_text(stmt, 1);
76                         p.name = (const char *) sqlite3_column_text(stmt, 2);
77                         players[p.player_id] = std::move(p);
78                 } else if (ret == SQLITE_DONE) {
79                         break;
80                 } else {
81                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
82                         abort();
83                 }
84         }
85         ret = sqlite3_finalize(stmt);
86         if (ret != SQLITE_OK) {
87                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
88                 abort();
89         }
90
91         // Read the events.
92         ret = sqlite3_prepare_v2(db, "SELECT event, t, player, type FROM event ORDER BY t", -1, &stmt, 0);
93         if (ret != SQLITE_OK) {
94                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
95                 abort();
96         }
97         for ( ;; ) {
98                 ret = sqlite3_step(stmt);
99                 if (ret == SQLITE_ROW) {
100                         Event e;
101                         e.event_id = sqlite3_column_int(stmt, 0);
102                         e.t = sqlite3_column_int(stmt, 1);
103                         if (sqlite3_column_type(stmt, 2) == SQLITE_INTEGER) {  // Non-NULL.
104                                 e.player_id = sqlite3_column_int(stmt, 2);
105                         }
106                         e.type = (const char *)sqlite3_column_text(stmt, 3);
107                         events.push_back(std::move(e));
108                 } else if (ret == SQLITE_DONE) {
109                         break;
110                 } else {
111                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
112                         abort();
113                 }
114         }
115         ret = sqlite3_finalize(stmt);
116         if (ret != SQLITE_OK) {
117                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
118                 abort();
119         }
120 }
121
122 unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, const string &type)
123 {
124         auto it = lower_bound(events.begin(), events.end(), t,
125                 [](const Event &e, uint64_t t) { return e.t < t; });
126         unsigned pos = distance(events.begin(), it);
127         beginInsertRows(QModelIndex(), pos, pos);
128
129         Event e;
130         e.t = t;
131         e.player_id = player_id;
132         e.type = type;
133         events.insert(events.begin() + pos, e);
134
135         endInsertRows();
136
137         // Insert the new row into the database.
138         sqlite3_stmt *stmt;
139         int ret = sqlite3_prepare_v2(db, "INSERT INTO event (t, player, type) VALUES (?, ?, ?)", -1, &stmt, 0);
140         if (ret != SQLITE_OK) {
141                 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
142                 abort();
143         }
144
145         sqlite3_bind_int64(stmt, 1, t);
146         if (player_id) {
147                 sqlite3_bind_int64(stmt, 2, *player_id);
148         } else {
149                 sqlite3_bind_null(stmt, 2);
150         }
151         sqlite3_bind_text(stmt, 3, type.data(), type.size(), SQLITE_STATIC);
152
153         ret = sqlite3_step(stmt);
154         if (ret == SQLITE_ROW) {
155                 fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
156                 abort();
157         }
158
159         ret = sqlite3_finalize(stmt);
160         if (ret != SQLITE_OK) {
161                 fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db));
162                 abort();
163         }
164
165         events[pos].event_id = sqlite3_last_insert_rowid(db);
166         return pos;
167 }
168
169 void EventsModel::delete_event(unsigned pos)
170 {
171         int event_id = events[pos].event_id;
172
173         beginRemoveRows(QModelIndex(), pos, pos);
174         events.erase(events.begin() + pos);
175         endRemoveRows();
176
177         // Delete the row from the database.
178         sqlite3_stmt *stmt;
179         int ret = sqlite3_prepare_v2(db, "DELETE FROM event WHERE event=?", -1, &stmt, 0);
180         if (ret != SQLITE_OK) {
181                 fprintf(stderr, "DELETE prepare: %s\n", sqlite3_errmsg(db));
182                 abort();
183         }
184
185         sqlite3_bind_int64(stmt, 1, event_id);
186
187         ret = sqlite3_step(stmt);
188         if (ret == SQLITE_ROW) {
189                 fprintf(stderr, "DELETE step: %s\n", sqlite3_errmsg(db));
190                 abort();
191         }
192
193         ret = sqlite3_finalize(stmt);
194         if (ret != SQLITE_OK) {
195                 fprintf(stderr, "DELETE finalize: %s\n", sqlite3_errmsg(db));
196                 abort();
197         }
198 }
199
200 void EventsModel::set_event_type(unsigned pos, const string &type)
201 {
202         events[pos].type = type;
203         emit dataChanged(createIndex(pos, 0), createIndex(pos, 2));
204
205         sqlite3_stmt *stmt;
206         int ret = sqlite3_prepare_v2(db, "UPDATE event SET type=? WHERE event=?", -1, &stmt, 0);
207         if (ret != SQLITE_OK) {
208                 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
209                 abort();
210         }
211
212         sqlite3_bind_text(stmt, 1, type.data(), type.size(), SQLITE_STATIC);
213         sqlite3_bind_int64(stmt, 2, events[pos].event_id);
214
215         ret = sqlite3_step(stmt);
216         if (ret == SQLITE_ROW) {
217                 fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
218                 abort();
219         }
220
221         ret = sqlite3_finalize(stmt);
222         if (ret != SQLITE_OK) {
223                 fprintf(stderr, "UPDATE finalize: %s\n", sqlite3_errmsg(db));
224                 abort();
225         }
226 }
227
228 unsigned EventsModel::get_last_event_pos(uint64_t t) const
229 {
230         // upper_bound() gives first where e.t > t,
231         // and the one before that is the one we want.
232         auto it = upper_bound(events.begin(), events.end(), t,
233                 [](uint64_t t, const Event &e) { return t < e.t; });
234         if (it == events.begin()) {
235                 return 0;
236         } else {
237                 return distance(events.begin(), it - 1);
238         }
239 }
240
241 EventsModel::Status EventsModel::get_status_at(uint64_t t)
242 {
243         Status s;
244         s.our_score = 0;
245         s.their_score = 0;
246         s.attack_state = Status::NOT_STARTED;
247         s.stoppage = false;
248         s.pull_state = Status::SHOULD_PULL;
249         uint64_t last_gained_possession = 0;
250         uint64_t last_stoppage = 0;
251         uint64_t time_spent_in_stoppage = 0;
252         unsigned num_touches = 0;
253
254         auto set_offense = [&s] { s.attack_state = Status::OFFENSE; };
255         auto set_defense = [&s] { s.attack_state = Status::DEFENSE; };
256
257         for (const Event &e : events) {
258                 if (e.t > t) {
259                         break;
260                 }
261
262                 if (e.type == "goal" || e.type == "their_goal") {
263                         s.pull_state = Status::SHOULD_PULL;
264                 } 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") {
265                         // No effect on pull status.
266                 } else if (e.type == "pull") {
267                         s.pull_state = Status::PULL_IN_AIR;
268                 } else {
269                         s.pull_state = Status::NOT_PULLING;  // Includes pull_landed and pull_oob.
270                 }
271
272                 if (e.type == "set_offense") {
273                         set_offense();
274                 } else if (e.type == "set_defense") {
275                         set_defense();
276                 }
277
278                 if (e.type == "goal") {
279                         ++s.our_score;
280                         set_defense();
281                         num_touches = 0;
282                 }
283                 if (e.type == "their_goal") {
284                         ++s.their_score;
285                         set_offense();
286                         num_touches = 0;
287                 }
288                 if (e.type == "catch") {
289                         if (num_touches == 0) {  // Pick up.
290                                 last_gained_possession = e.t;
291                                 time_spent_in_stoppage = 0;
292                         }
293                         ++num_touches;
294                 }
295                 if (e.type == "interception") {
296                         num_touches = 1;
297                         set_offense();
298                         last_gained_possession = e.t;
299                         time_spent_in_stoppage = 0;
300                 }
301                 if (e.type == "defense" || e.type == "their_throwaway") {
302                         set_offense();
303                         num_touches = 0;
304                         time_spent_in_stoppage = 0;
305                 }
306                 if (e.type == "drop" || e.type == "throwaway") {
307                         set_defense();
308                         num_touches = 0;
309                 }
310                 if (e.type == "stoppage") {
311                         s.stoppage = true;
312                         last_stoppage = e.t;
313                 }
314                 if (e.type == "restart") {
315                         s.stoppage = false;
316                         if (last_stoppage != 0) {
317                                 time_spent_in_stoppage += (e.t - last_stoppage);
318                                 last_stoppage = 0;
319                         }
320                 }
321         }
322         if (s.stoppage && last_stoppage != 0) {
323                 time_spent_in_stoppage += (t - last_stoppage);
324         }
325
326         s.num_passes = (num_touches == 0) ? 0 : num_touches - 1;
327         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;
328         s.stoppage_sec = (s.attack_state == Status::OFFENSE && last_gained_possession != 0 && num_touches != 0) ? time_spent_in_stoppage / 1000 : 0;
329         return s;
330 }
331
332 set<int> EventsModel::get_team_at(uint64_t t)
333 {
334         set<int> team;
335         for (const Event &e : events) {
336                 if (e.t > t) {
337                         break;
338                 }
339                 if (e.type == "in") {
340                         team.insert(*e.player_id);
341                 }
342                 if (e.type == "out") {
343                         team.erase(*e.player_id);
344                 }
345         }
346         return team;
347 }
348
349 void EventsModel::set_team_at(uint64_t t, const set<int> &new_team)
350 {
351         // Backdate to the last goal or stoppage, _or_ the last time someone
352         // going out is mentioned. (We don't really track injuries yet;
353         // do we want an explicit injury type? If we had one, it would probably
354         // be the simplest.)
355         uint64_t backdate_point = 0;
356         for (const Event &e : events) {
357                 if (e.t > t) {
358                         break;
359                 }
360                 if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset") {
361                         backdate_point = e.t + 1;
362                 }
363                 if (e.player_id.has_value() && !new_team.count(*e.player_id)) {
364                         backdate_point = e.t + 1;
365                 }
366         }
367
368         // Delete all in/outs already at the backdate point.
369         for (unsigned i = 0; i < events.size(); ) {
370                 if (events[i].t > backdate_point) {
371                         break;
372                 }
373                 if (events[i].t == backdate_point && (events[i].type == "in" || events[i].type == "out")) {
374                         delete_event(i);
375                 } else {
376                         ++i;
377                 }
378         }
379
380         // Finally make the subs we need.
381         set<int> old_team = get_team_at(backdate_point);
382         for (int player_id : old_team) {
383                 if (!new_team.count(player_id)) {
384                         insert_event(backdate_point, player_id, "out");
385                 }
386         }
387         for (int player_id : new_team) {
388                 if (!old_team.count(player_id)) {
389                         insert_event(backdate_point, player_id, "in");
390                 }
391         }
392 }