// dbexport.cpp - MySQL export implementation for mw4print // // Included directly from ChildView.cpp (same pattern as recscore.cpp). // #include "dbexport.h" // ---- Config default values ----------------------------------------------- SDBConfig g_dbConfig = { false, // bEnabled "localhost", // szHost 3306, // nPort "firestorm_stats", // szDatabase "mw4", // szUsername "", // szPassword false // bExportEvents }; // ---- MySQL late binding -------------------------------------------------- // // We load libmysql.dll at runtime so the exe does not hard-link against it. // All function pointers are NULL until DB_LoadMySQL() succeeds. static HMODULE g_hMySQL = NULL; typedef void* MYSQLH; // opaque MYSQL* handle typedef unsigned long MYULONG; typedef unsigned __int64 MYULLONG; // MYSQL_OPT_CONNECT_TIMEOUT = 0 #define MY_OPT_CONNECT_TIMEOUT 0 typedef MYSQLH (__cdecl *PFN_mysql_init) (MYSQLH); typedef MYSQLH (__cdecl *PFN_mysql_real_connect) (MYSQLH, const char*, const char*, const char*, const char*, unsigned int, const char*, unsigned long); typedef void (__cdecl *PFN_mysql_options) (MYSQLH, int, const char*); typedef int (__cdecl *PFN_mysql_real_query) (MYSQLH, const char*, unsigned long); typedef MYULLONG (__cdecl *PFN_mysql_insert_id) (MYSQLH); typedef void (__cdecl *PFN_mysql_close) (MYSQLH); typedef const char* (__cdecl *PFN_mysql_error) (MYSQLH); typedef MYULONG (__cdecl *PFN_mysql_escape_string) (char*, const char*, MYULONG); static PFN_mysql_init pfn_init = NULL; static PFN_mysql_real_connect pfn_real_connect = NULL; static PFN_mysql_options pfn_options = NULL; static PFN_mysql_real_query pfn_real_query = NULL; static PFN_mysql_insert_id pfn_insert_id = NULL; static PFN_mysql_close pfn_close = NULL; static PFN_mysql_error pfn_error = NULL; static PFN_mysql_escape_string pfn_escape_string = NULL; static bool DB_LoadMySQL() { if (g_hMySQL) return true; // Try app directory first, then system path. char szPath[MAX_PATH]; sprintf(szPath, "%s\\libmysql.dll", AssetsDirectory1); g_hMySQL = LoadLibrary(szPath); if (!g_hMySQL) g_hMySQL = LoadLibrary("libmysql.dll"); if (!g_hMySQL) return false; #define GETPROC(name, pfn) pfn = (PFN_##name)GetProcAddress(g_hMySQL, #name); if (!pfn) { FreeLibrary(g_hMySQL); g_hMySQL=NULL; return false; } GETPROC(mysql_init, pfn_init) GETPROC(mysql_real_connect, pfn_real_connect) GETPROC(mysql_options, pfn_options) GETPROC(mysql_real_query, pfn_real_query) GETPROC(mysql_insert_id, pfn_insert_id) GETPROC(mysql_close, pfn_close) GETPROC(mysql_error, pfn_error) GETPROC(mysql_escape_string, pfn_escape_string) #undef GETPROC return true; } // ---- Helpers ------------------------------------------------------------- // Open a connection using current config. Returns handle on success, NULL // on failure. Caller must call pfn_close on the returned handle. static MYSQLH DB_Connect(char* pszError, int nErrBufSize) { if (!DB_LoadMySQL()) { if (pszError) strncpy(pszError, "libmysql.dll not found. Install MySQL Connector/C and place libmysql.dll next to mw4print.exe.", nErrBufSize - 1); return NULL; } MYSQLH h = pfn_init(NULL); if (!h) { if (pszError) strncpy(pszError, "mysql_init failed (out of memory?)", nErrBufSize - 1); return NULL; } // Short connect timeout so the app doesn't hang if server is unreachable. int nTimeout = 5; pfn_options(h, MY_OPT_CONNECT_TIMEOUT, (const char*)&nTimeout); MYSQLH conn = pfn_real_connect(h, g_dbConfig.szHost, g_dbConfig.szUsername, g_dbConfig.szPassword, g_dbConfig.szDatabase, (unsigned int)g_dbConfig.nPort, NULL, 0); if (!conn) { if (pszError) { strncpy(pszError, pfn_error(h), nErrBufSize - 1); pszError[nErrBufSize - 1] = '\0'; } pfn_close(h); return NULL; } return h; } // Execute a query string; return true on success. static bool DB_Exec(MYSQLH h, const char* pszSQL) { return pfn_real_query(h, pszSQL, (unsigned long)strlen(pszSQL)) == 0; } // Escape a string for use inside SQL single-quoted literals. // Writes into pszOut (must be at least 2*len+1 bytes). static void DB_Escape(char* pszOut, const char* pszIn) { if (pfn_escape_string) pfn_escape_string(pszOut, pszIn, (MYULONG)strlen(pszIn)); else { // Fallback: manual escape of ' and backslash char* dst = pszOut; for (const char* src = pszIn; *src; ++src) { if (*src == '\'' || *src == '\\') *dst++ = '\\'; *dst++ = *src; } *dst = '\0'; } } // ---- CREATE TABLE statements (run on first connect) ---------------------- static const char* s_szCreateMatch = "CREATE TABLE IF NOT EXISTS `match` (" " `match_id` INT UNSIGNED NOT NULL AUTO_INCREMENT," " `match_time` DATETIME NOT NULL," " `server_name` VARCHAR(256) NOT NULL DEFAULT ''," " `server_ip` VARCHAR(64) NOT NULL DEFAULT ''," " `map_name` VARCHAR(128) NOT NULL DEFAULT ''," " `scenario_name` VARCHAR(128) NOT NULL DEFAULT ''," " `game_type` INT NOT NULL DEFAULT 0," " `game_type_name` VARCHAR(64) NOT NULL DEFAULT ''," " `is_team_game` TINYINT(1) NOT NULL DEFAULT 0," " `player_count` INT NOT NULL DEFAULT 0," " `team_count` INT NOT NULL DEFAULT 0," " `team_scores` VARCHAR(64) NOT NULL DEFAULT ''," " `game_length` INT NOT NULL DEFAULT 0," " `kill_limit` INT NOT NULL DEFAULT 0," " `respawn_limit` INT NOT NULL DEFAULT 0," " `friendly_fire` INT NOT NULL DEFAULT 0," " `heat_on` TINYINT(1) NOT NULL DEFAULT 0," " `armor_mode` TINYINT(1) NOT NULL DEFAULT 0," " `unlimited_ammo` TINYINT(1) NOT NULL DEFAULT 0," " `is_night` TINYINT(1) NOT NULL DEFAULT 0," " `min_tonnage` INT NOT NULL DEFAULT 0," " `max_tonnage` INT NOT NULL DEFAULT 0," " PRIMARY KEY (`match_id`)" ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; static const char* s_szCreatePlayerResult = "CREATE TABLE IF NOT EXISTS `player_result` (" " `result_id` INT UNSIGNED NOT NULL AUTO_INCREMENT," " `match_id` INT UNSIGNED NOT NULL," " `player_name` VARCHAR(64) NOT NULL DEFAULT ''," " `mech_name` VARCHAR(64) NOT NULL DEFAULT ''," " `team` INT NOT NULL DEFAULT 0," " `score` INT NOT NULL DEFAULT 0," " `kills` INT NOT NULL DEFAULT 0," " `deaths` INT NOT NULL DEFAULT 0," " `is_bot` TINYINT(1) NOT NULL DEFAULT 0," " `pilot_decal` INT NOT NULL DEFAULT 0," " PRIMARY KEY (`result_id`)," " KEY `match_id` (`match_id`)" ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; static const char* s_szCreatePVP = "CREATE TABLE IF NOT EXISTS `pvp` (" " `pvp_id` INT UNSIGNED NOT NULL AUTO_INCREMENT," " `match_id` INT UNSIGNED NOT NULL," " `attacker` VARCHAR(64) NOT NULL DEFAULT ''," " `victim` VARCHAR(64) NOT NULL DEFAULT ''," " `kills` INT NOT NULL DEFAULT 0," " `score` INT NOT NULL DEFAULT 0," " PRIMARY KEY (`pvp_id`)," " KEY `match_id` (`match_id`)" ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; static const char* s_szCreateEvent = "CREATE TABLE IF NOT EXISTS `event` (" " `event_id` INT UNSIGNED NOT NULL AUTO_INCREMENT," " `match_id` INT UNSIGNED NOT NULL," " `event_type` INT NOT NULL DEFAULT 0," " `actor` VARCHAR(64) NOT NULL DEFAULT ''," " `target` VARCHAR(64) NOT NULL DEFAULT ''," " `weapon` INT NOT NULL DEFAULT 0," " `body_zone` INT NOT NULL DEFAULT 0," " `param` INT NOT NULL DEFAULT 0," " PRIMARY KEY (`event_id`)," " KEY `match_id` (`match_id`)" ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; // Map CBucketManager::Bucket_Type int to a game_type name string. static const char* GameTypeName(int n) { switch (n) { case 0: return "StockDestruction"; case 1: return "StockTeamDestruction"; case 2: return "StockAttrition"; case 3: return "StockTeamAttrition"; case 4: return "StockCaptureTheFlag"; case 5: return "StockKingOfTheHill"; case 6: return "StockTeamKingOfTheHill"; case 7: return "StockTerritories"; case 8: return "StockStealTheBacon"; case 9: return "StockCaptureBase"; case 10: return "StockDestroyObjective"; case 11: return "StockEscort"; case 12: return "StockMasterTrial"; case 13: return "StockSiegeAssault"; case 14: return "StockCampaign"; case 15: return "CustomDestruction"; case 16: return "CustomTeamDestruction"; case 17: return "CustomAttrition"; case 18: return "CustomTeamAttrition"; case 19: return "CustomCaptureTheFlag"; case 20: return "CustomKingOfTheHill"; case 21: return "CustomTeamKingOfTheHill"; case 22: return "CustomTerritories"; case 23: return "CustomStealTheBacon"; case 24: return "CustomCaptureBase"; case 25: return "CustomDestroyObjective"; case 26: return "CustomEscort"; case 27: return "CustomMasterTrial"; case 28: return "CustomSiegeAssault"; case 29: return "CustomCampaign"; default: return "Unknown"; } } // Resolve a ReplicatorID to a player name by scanning aRSOs[]. static const char* ResolvePlayerName( const ReplicatorID& id, PCRecScoreObject aRSOs[], int nPlayers) { for (int i = 0; i < nPlayers; i++) { if (aRSOs[i]->m_who == id) return aRSOs[i]->GetName(); } return ""; } // ---- Config I/O ---------------------------------------------------------- static void DB_GetINIPath(char* pszOut) { sprintf(pszOut, "%s\\mw4print.ini", AssetsDirectory1); } void DB_LoadConfig() { char szINI[MAX_PATH]; DB_GetINIPath(szINI); const char* sec = "MySQLExport"; g_dbConfig.bEnabled = !!GetPrivateProfileInt(sec, "Enabled", 0, szINI); GetPrivateProfileString(sec, "Host", "localhost", g_dbConfig.szHost, sizeof(g_dbConfig.szHost), szINI); g_dbConfig.nPort = GetPrivateProfileInt(sec, "Port", 3306, szINI); GetPrivateProfileString(sec, "Database", "firestorm_stats", g_dbConfig.szDatabase, sizeof(g_dbConfig.szDatabase), szINI); GetPrivateProfileString(sec, "Username", "mw4", g_dbConfig.szUsername, sizeof(g_dbConfig.szUsername), szINI); GetPrivateProfileString(sec, "Password", "", g_dbConfig.szPassword, sizeof(g_dbConfig.szPassword), szINI); g_dbConfig.bExportEvents = !!GetPrivateProfileInt(sec, "ExportEvents", 0, szINI); } void DB_SaveConfig() { char szINI[MAX_PATH]; DB_GetINIPath(szINI); const char* sec = "MySQLExport"; char szBuf[32]; WritePrivateProfileString(sec, "Enabled", g_dbConfig.bEnabled ? "1" : "0", szINI); WritePrivateProfileString(sec, "Host", g_dbConfig.szHost, szINI); _itoa(g_dbConfig.nPort, szBuf, 10); WritePrivateProfileString(sec, "Port", szBuf, szINI); WritePrivateProfileString(sec, "Database", g_dbConfig.szDatabase, szINI); WritePrivateProfileString(sec, "Username", g_dbConfig.szUsername, szINI); WritePrivateProfileString(sec, "Password", g_dbConfig.szPassword, szINI); WritePrivateProfileString(sec, "ExportEvents", g_dbConfig.bExportEvents ? "1" : "0", szINI); } // ---- Test connection ----------------------------------------------------- bool DB_TestConnection(char* pszError, int nErrBufSize) { if (pszError && nErrBufSize > 0) pszError[0] = '\0'; MYSQLH h = DB_Connect(pszError, nErrBufSize); if (!h) return false; bool bOK = DB_Exec(h, "SELECT 1"); if (!bOK && pszError) { strncpy(pszError, pfn_error(h), nErrBufSize - 1); pszError[nErrBufSize - 1] = '\0'; } pfn_close(h); return bOK; } // ---- Main export --------------------------------------------------------- bool DB_ExportMatch( const CRecScoreFull& rsf, PCRecScoreObject aRSOs[], int nPlayers, const int naTeams[8], int nTeamCount, const MWNetMissionParameters& params) { char szError[512]; MYSQLH h = DB_Connect(szError, sizeof(szError)); if (!h) return false; char szSQL[4096]; char szEsc[512]; bool bOK = true; // Ensure tables exist (silent if already present). DB_Exec(h, s_szCreateMatch); DB_Exec(h, s_szCreatePlayerResult); DB_Exec(h, s_szCreatePVP); if (g_dbConfig.bExportEvents) DB_Exec(h, s_szCreateEvent); // ---- Insert match row ------------------------------------------------ // Build team_scores as comma-separated string e.g. "100,80,0,0,0,0,0,0" char szTeamScores[64]; sprintf(szTeamScores, "%d,%d,%d,%d,%d,%d,%d,%d", rsf.m_naTeamScores[0], rsf.m_naTeamScores[1], rsf.m_naTeamScores[2], rsf.m_naTeamScores[3], rsf.m_naTeamScores[4], rsf.m_naTeamScores[5], rsf.m_naTeamScores[6], rsf.m_naTeamScores[7]); char szServerNameE[512], szServerIPE[512], szMapE[256], szScenarioE[256], szGameTypeNameE[128]; DB_Escape(szServerNameE, params.m_serverName); DB_Escape(szServerIPE, params.m_serverIP); DB_Escape(szMapE, params.m_mapName); DB_Escape(szScenarioE, params.m_scenarioName); DB_Escape(szGameTypeNameE, GameTypeName(params.m_ruleType)); sprintf(szSQL, "INSERT INTO `match` " "(match_time, server_name, server_ip, map_name, scenario_name, " " game_type, game_type_name, is_team_game, player_count, team_count, " " team_scores, game_length, kill_limit, respawn_limit, friendly_fire, " " heat_on, armor_mode, unlimited_ammo, is_night, min_tonnage, max_tonnage) " "VALUES (" "'%04d-%02d-%02d %02d:%02d:%02d'," // match_time "'%s','%s','%s','%s'," // server_name, server_ip, map_name, scenario_name "%d,'%s',%d,%d,%d," // game_type, game_type_name, is_team_game, player_count, team_count "'%s'," // team_scores "%d,%d,%d,%d," // game_length, kill_limit, respawn_limit, friendly_fire "%d,%d,%d,%d,%d,%d" // heat_on, armor_mode, unlimited_ammo, is_night, min_tonnage, max_tonnage ")", rsf.m_SysTime.wYear, rsf.m_SysTime.wMonth, rsf.m_SysTime.wDay, rsf.m_SysTime.wHour, rsf.m_SysTime.wMinute, rsf.m_SysTime.wSecond, szServerNameE, szServerIPE, szMapE, szScenarioE, params.m_ruleType, szGameTypeNameE, rsf.m_bTeamGame ? 1 : 0, nPlayers, nTeamCount, szTeamScores, params.m_gameLength, params.m_killLimit ? params.m_killLimitNumber : 0, params.m_respawnLimit ? params.m_respawnLimitNumber : 0, params.m_friendlyFirePercentage, params.m_heatOn ? 1 : 0, params.m_armormodeOn ? 1 : 0, params.m_unlimitedAmmo ? 1 : 0, params.m_isNight ? 1 : 0, params.m_minimumTonnage, params.m_maximumTonnage); bOK = DB_Exec(h, szSQL); if (!bOK) { pfn_close(h); return false; } MYULLONG nMatchID = pfn_insert_id(h); // ---- Insert player_result rows --------------------------------------- for (int i = 0; i < nPlayers && bOK; i++) { const CRecScoreObject& rso = *aRSOs[i]; char szNameE[128], szMechE[128]; DB_Escape(szNameE, rso.GetName()); DB_Escape(szMechE, rso.GetMech()); sprintf(szSQL, "INSERT INTO `player_result` " "(match_id, player_name, mech_name, team, score, kills, deaths, is_bot, pilot_decal) " "VALUES (%I64u,'%s','%s',%d,%d,%d,%d,%d,%d)", nMatchID, szNameE, szMechE, rso.m_nPilotTeam, rso.m_nScore, rso.m_nKills, rso.m_nDeaths, rso.m_bBOT ? 1 : 0, rso.m_nPilotDecal); bOK = DB_Exec(h, szSQL); } // ---- Insert pvp rows ------------------------------------------------- for (int p = 0; p < rsf.m_nPVPs && bOK; p++) { const CRecScoreFull::SPVP_Rec& pvp = rsf.m_pPVPs[p]; const char* pszAttacker = ResolvePlayerName(pvp.m_Inf, aRSOs, nPlayers); const char* pszVictim = ResolvePlayerName(pvp.m_Rcv, aRSOs, nPlayers); char szAttE[128], szVicE[128]; DB_Escape(szAttE, pszAttacker); DB_Escape(szVicE, pszVictim); sprintf(szSQL, "INSERT INTO `pvp` (match_id, attacker, victim, kills, score) " "VALUES (%I64u,'%s','%s',%d,%d)", nMatchID, szAttE, szVicE, pvp.m_nKills, pvp.m_nScore); bOK = DB_Exec(h, szSQL); } // ---- Insert event rows (optional) ------------------------------------ if (bOK && g_dbConfig.bExportEvents) { // Walk the CRecScorePack linked list. const CRecScorePack* pPack = rsf.m_pScorePackStart; while (pPack && bOK) { for (int e = 0; e < pPack->m_nCount && bOK; e++) { const SRecScore& rs = pPack->m_a[e]; const char* pszActor = ResolvePlayerName(rs.m_who, aRSOs, nPlayers); const char* pszTarget = ResolvePlayerName(rs.m_whom, aRSOs, nPlayers); char szActE[128], szTgtE[128]; DB_Escape(szActE, pszActor); DB_Escape(szTgtE, pszTarget); sprintf(szSQL, "INSERT INTO `event` (match_id, event_type, actor, target, weapon, body_zone, param) " "VALUES (%I64u,%d,'%s','%s',%d,%d,%d)", nMatchID, rs.m_nType, szActE, szTgtE, rs.m_nBy, rs.m_Where.m_xWhere, rs.m_nParam); bOK = DB_Exec(h, szSQL); } pPack = pPack->m_pNext; } } pfn_close(h); return bOK; }