#include "pages.hpp" #include "../common/common.hpp" #include #include #include #include #include static bool compareWeaponKillsDesc(const std::pair& a, const std::pair& 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> 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 progression; getGameScoreProgression(database, game.gamenumber, progression); // Build JSON data for D3.js chart // Group by player to create series std::map> 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, int> kill_matrix; std::vector 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 weapon_kills; getGameWeaponKills(database, game.gamenumber, weapon_kills); // Combine splash damage with base weapon damage std::map 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> 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& 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>& 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(playerid, score)); } } void getGameScoreProgression(cppdb::session& database, int gamenumber, std::vector& 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, int>& matrix, std::vector& players) { // Get all players in the game std::set 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& 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; } }