src/pages/game.cpp
browsing at commit = 9641d231a053fbc233ff87e47a5439df3a59c146
#include "pages.hpp"
#include "../common/common.hpp"
#include <ctemplate/template.h>
#include <fstream>
#include <sstream>
#include <set>
#include <algorithm>
static bool compareWeaponKillsDesc(const std::pair<std::string, int>& a, const std::pair<std::string, int>& b) {
return a.second > b.second;
}
static void addPlayerToTemplateDict(ctemplate::TemplateDictionary* dict, const OastatPlayer& player) {
dict->SetValue("PLAYER_ID", std::to_string(player.playerid));
dict->SetValue("PLAYER_HEADMODEL", player.headmodel);
dict->SetValue("PLAYER_ISBOT", player.isBot);
dict->SetValue("PLAYER_LASTSEEN", getTimeStamp(player.lastseen));
dict->SetValue("PLAYER_MODEL", player.model);
dict->SetValue("PLAYER_NICKNAME", player.nickname);
}
void write_html_game(cppdb::session& database, const OastatGame& game, const std::string& output_dir) {
// Skip if already generated
auto& generated_games = get_generated_games();
if (generated_games.count(game.gamenumber)) {
return;
}
generated_games.insert(game.gamenumber);
ctemplate::TemplateDictionary game_tpl("templates/game.tpl");
game_tpl.SetValue("GENERATION_DATE", timestamp_now_as_string(database));
game_tpl.SetValue("GAME_NUMBER", std::to_string(game.gamenumber));
game_tpl.SetValue("GAME_MAP", game.mapname);
game_tpl.SetValue("GAME_SERVERNAME", game.servername);
std::vector<std::pair<int,int>> scores;
getGameScoreTotal(database, game.gamenumber, scores);
for (size_t i = 0; i < scores.size(); ++i) {
ctemplate::TemplateDictionary* sub_dict = game_tpl.AddSectionDictionary("SCORES_LIST");
const OastatPlayer& p = getPlayer(database, scores.at(i).first);
sub_dict->SetValue("ID", std::to_string(scores.at(i).first));
sub_dict->SetValue("SCORE", std::to_string(scores.at(i).second));
addPlayerToTemplateDict(sub_dict, p);
}
// Get score progression for chart
std::vector<ScorePoint> progression;
getGameScoreProgression(database, game.gamenumber, progression);
// Build JSON data for D3.js chart
// Group by player to create series
std::map<int, std::vector<ScorePoint>> playerSeries;
for (const auto& point : progression) {
playerSeries[point.playerid].push_back(point);
}
// Generate JSON for each player's score progression
std::stringstream json_data;
json_data << "[";
bool first_player = true;
for (const auto& series : playerSeries) {
if (!first_player) json_data << ",";
first_player = false;
const OastatPlayer& p = getPlayer(database, series.first);
json_data << "{\"player\":\"" << p.nickname << "\",\"playerid\":" << series.first << ",\"data\":[";
bool first_point = true;
for (const auto& point : series.second) {
if (!first_point) json_data << ",";
first_point = false;
json_data << "{\"second\":" << point.second << ",\"score\":" << point.score << "}";
}
json_data << "]}";
}
json_data << "]";
// Write JSON to separate file
std::ofstream json_file;
json_file.open(output_dir + "/game/" + std::to_string(game.gamenumber) + ".json");
json_file << json_data.str();
json_file.close();
// Get kill matrix
std::map<std::pair<int,int>, int> kill_matrix;
std::vector<int> matrix_players;
getGameKillMatrix(database, game.gamenumber, kill_matrix, matrix_players);
// Add kill matrix to template
if (!matrix_players.empty()) {
game_tpl.ShowSection("HAS_KILL_MATRIX");
// Add header row with player names
for (int playerid : matrix_players) {
ctemplate::TemplateDictionary* header_dict = game_tpl.AddSectionDictionary("MATRIX_HEADER");
const OastatPlayer& p = getPlayer(database, playerid);
header_dict->SetValue("PLAYER_NICKNAME", p.nickname);
}
// Add data rows
for (int killer : matrix_players) {
ctemplate::TemplateDictionary* row_dict = game_tpl.AddSectionDictionary("MATRIX_ROWS");
const OastatPlayer& killer_player = getPlayer(database, killer);
row_dict->SetValue("KILLER_NICKNAME", killer_player.nickname);
for (int victim : matrix_players) {
ctemplate::TemplateDictionary* cell_dict = row_dict->AddSectionDictionary("MATRIX_CELLS");
auto key = std::make_pair(killer, victim);
int kills = kill_matrix.count(key) ? kill_matrix[key] : 0;
cell_dict->SetValue("KILL_COUNT", std::to_string(kills));
if (kills > 0) {
cell_dict->ShowSection("HAS_KILLS");
}
if (killer == victim) {
cell_dict->ShowSection("IS_DIAGONAL");
}
}
}
}
// Get weapon kills for this game
std::map<int, int> weapon_kills;
getGameWeaponKills(database, game.gamenumber, weapon_kills);
// Combine splash damage with base weapon damage
std::map<std::string, int> combined_weapon_kills;
combined_weapon_kills["Shotgun"] = weapon_kills[MOD_SHOTGUN];
combined_weapon_kills["Gauntlet"] = weapon_kills[MOD_GAUNTLET];
combined_weapon_kills["Machinegun"] = weapon_kills[MOD_MACHINEGUN];
combined_weapon_kills["Grenade"] = weapon_kills[MOD_GRENADE] + weapon_kills[MOD_GRENADE_SPLASH];
combined_weapon_kills["Rocket"] = weapon_kills[MOD_ROCKET] + weapon_kills[MOD_ROCKET_SPLASH];
combined_weapon_kills["Plasma"] = weapon_kills[MOD_PLASMA] + weapon_kills[MOD_PLASMA_SPLASH];
combined_weapon_kills["Railgun"] = weapon_kills[MOD_RAILGUN];
combined_weapon_kills["Lightning"] = weapon_kills[MOD_LIGHTNING];
combined_weapon_kills["Nailgun"] = weapon_kills[MOD_NAIL];
combined_weapon_kills["Chaingun"] = weapon_kills[MOD_CHAINGUN];
combined_weapon_kills["BFG"] = weapon_kills[MOD_BFG] + weapon_kills[MOD_BFG_SPLASH];
combined_weapon_kills["Telefrag"] = weapon_kills[MOD_TELEFRAG];
combined_weapon_kills["Falling"] = weapon_kills[MOD_FALLING];
// Sort weapons by kill count (descending)
std::vector<std::pair<std::string, int>> sorted_weapons;
for (const auto& wk : combined_weapon_kills) {
if (wk.second > 0) {
sorted_weapons.push_back(wk);
}
}
std::stable_sort(sorted_weapons.begin(), sorted_weapons.end(), compareWeaponKillsDesc);
if (!sorted_weapons.empty()) {
game_tpl.ShowSection("HAS_WEAPON_KILLS");
for (const auto& wk : sorted_weapons) {
ctemplate::TemplateDictionary* sub_dict = game_tpl.AddSectionDictionary("WEAPON_KILLS");
sub_dict->SetValue("WEAPON_NAME", wk.first);
sub_dict->SetValue("KILL_COUNT", std::to_string(wk.second));
}
}
std::string output;
ctemplate::ExpandTemplate("templates/game.tpl", ctemplate::DO_NOT_STRIP, &game_tpl, &output);
std::ofstream myfile;
myfile.open(output_dir + "/game/" + std::to_string(game.gamenumber) + ".html");
myfile << output;
myfile.close();
}
void getRecentGames(cppdb::session& database, std::vector<OastatGame>& games) {
std::string sql = "select gamenumber, gametype, mapname, time, basegame, second, servername from oastat.oastat_games order by gamenumber desc limit 10";
cppdb::statement st = database.prepare(sql);
cppdb::result res = st.query();
while(res.next()) {
OastatGame game;
res >> game.gamenumber >> game.gametype >> game.mapname >> game.time >> game.basegame >> game.second >> game.servername;
games.push_back(game);
}
}
void getGameScoreTotal(cppdb::session& database, int gamenumber, std::vector<std::pair<int,int>>& scores) {
std::string sql = "SELECT player, score FROM oastat.oastat_points p1 WHERE gamenumber = ? AND second = (SELECT MAX(second) FROM oastat.oastat_points p2 WHERE p2.player = p1.player AND p2.gamenumber = ?) ORDER BY score DESC";
cppdb::statement st = database.prepare(sql);
st.bind(1, gamenumber);
st.bind(2, gamenumber);
cppdb::result res = st.query();
while(res.next()) {
int playerid;
int score;
res >> playerid >> score;
scores.push_back(std::pair<int,int>(playerid, score));
}
}
void getGameScoreProgression(cppdb::session& database, int gamenumber, std::vector<ScorePoint>& progression) {
std::string sql = "SELECT player, `second` as the_second, score FROM oastat.oastat_points where gamenumber = ? ORDER BY the_second, player";
cppdb::statement st = database.prepare(sql);
st.bind(1, gamenumber);
cppdb::result res = st.query();
while(res.next()) {
ScorePoint point;
res >> point.playerid >> point.second >> point.score;
progression.push_back(point);
}
}
void getGameKillMatrix(cppdb::session& database, int gamenumber, std::map<std::pair<int,int>, int>& matrix, std::vector<int>& players) {
// Get all players in the game
std::set<int> player_set;
std::string sql_players = "SELECT DISTINCT player FROM oastat.oastat_points WHERE gamenumber = ?";
cppdb::statement st_players = database.prepare(sql_players);
st_players.bind(1, gamenumber);
cppdb::result res_players = st_players.query();
while(res_players.next()) {
int playerid;
res_players >> playerid;
player_set.insert(playerid);
}
// Convert to vector for ordered iteration
players.assign(player_set.begin(), player_set.end());
// Get kill data
std::string sql = "SELECT attacker, target, COUNT(0) as kills FROM oastat.oastat_kills WHERE gamenumber = ? AND attacker <> target GROUP BY attacker, target";
cppdb::statement st = database.prepare(sql);
st.bind(1, gamenumber);
cppdb::result res = st.query();
while(res.next()) {
int attacker, target, kills;
res >> attacker >> target >> kills;
matrix[std::make_pair(attacker, target)] = kills;
}
}
void getGameWeaponKills(cppdb::session& database, int gamenumber, std::map<int, int>& weapon_kills) {
std::string sql = "SELECT modtype, COUNT(0) as count FROM oastat.oastat_kills "
"WHERE gamenumber = ? AND attacker <> target "
"GROUP BY modtype ORDER BY count DESC";
cppdb::statement st = database.prepare(sql);
st.bind(1, gamenumber);
cppdb::result res = st.query();
while(res.next()) {
int modtype;
int count;
res >> modtype >> count;
weapon_kills[modtype] = count;
}
}