diff --git a/Gameleap/code/mw4/Code/mw4print/ChildView.cpp b/Gameleap/code/mw4/Code/mw4print/ChildView.cpp index 80eb871a..24569bcb 100644 --- a/Gameleap/code/mw4/Code/mw4print/ChildView.cpp +++ b/Gameleap/code/mw4/Code/mw4print/ChildView.cpp @@ -11,6 +11,7 @@ #include "mw4dummy.h" #include "recscore.h" +#include "dbexport.h" typedef unsigned long UINT4; typedef unsigned char* POINTER; #include "logreport.h" @@ -23,6 +24,7 @@ static char THIS_FILE[] = __FILE__; #include "mw4dummy.cpp" #include "recscore.cpp" +#include "dbexport.cpp" #include "logreport.cpp" #define IDT_TIMER 1000 @@ -37,6 +39,12 @@ BEGIN_MESSAGE_MAP(CChildView,CWnd ) ON_COMMAND(ID_FILE_OPEN, OnFileOpen) ON_COMMAND(ID_FILE_LOGREPORT, OnFileLogReport) ON_COMMAND(ID_FILE_LOG2DATES, OnFileLog2Dates) + ON_COMMAND(ID_FILE_DBSETTINGS, OnFileDbSettings) + ON_COMMAND(ID_FILE_BANNERSETTING, OnFileBannerSetting) +END_MESSAGE_MAP() + +BEGIN_MESSAGE_MAP(CDlgDBSettings, CDialog) + ON_BN_CLICKED(IDC_DB_TEST, OnTestConnection) END_MESSAGE_MAP() BEGIN_MESSAGE_MAP(CDlgGet2Dates, CDialog) @@ -299,7 +307,9 @@ int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd ::OnCreate(lpCreateStruct) == -1) return -1; - + + DB_LoadConfig(); + m_uTimer = SetTimer(IDT_TIMER, 1 * 1000, NULL); if (!m_uTimer) return -1; @@ -343,6 +353,121 @@ void CChildView::OnTimer(UINT nIDEvent) CWnd ::OnTimer(nIDEvent); } +void CChildView::OnFileBannerSetting() +{ + CDlgBannerSetting dlg(this); + dlg.DoModal(); +} + +///////////////////////////////////////////////////////////////////////////// +// CDlgBannerSetting + +CDlgBannerSetting::CDlgBannerSetting(CWnd* pParent) + : CDialog(CDlgBannerSetting::IDD, pParent) +{ +} + +BEGIN_MESSAGE_MAP(CDlgBannerSetting, CDialog) +END_MESSAGE_MAP() + +void CDlgBannerSetting::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); +} + +BOOL CDlgBannerSetting::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner); + return TRUE; +} + +void CDlgBannerSetting::OnOK() +{ + GetDlgItemText(IDC_BANNER_TEXT, g_szPrintBanner, sizeof(g_szPrintBanner)); + + // Persist to options.ini so it survives a restart. + char szINI[MAX_PATH]; + sprintf(szINI, "%s\\options.ini", AssetsDirectory1); + WritePrivateProfileString("battle tech print", "BannerText", g_szPrintBanner, szINI); + + CDialog::OnOK(); +} + +void CChildView::OnFileDbSettings() +{ + CDlgDBSettings dlg(this); + dlg.DoModal(); +} + +///////////////////////////////////////////////////////////////////////////// +// CDlgDBSettings + +CDlgDBSettings::CDlgDBSettings(CWnd* pParent) + : CDialog(CDlgDBSettings::IDD, pParent) +{ +} + +void CDlgDBSettings::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); +} + +BOOL CDlgDBSettings::OnInitDialog() +{ + CDialog::OnInitDialog(); + + CheckDlgButton(IDC_DB_ENABLED, g_dbConfig.bEnabled ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_DB_EXPORTEVENTS, g_dbConfig.bExportEvents ? BST_CHECKED : BST_UNCHECKED); + + SetDlgItemText(IDC_DB_HOST, g_dbConfig.szHost); + SetDlgItemInt (IDC_DB_PORT, g_dbConfig.nPort, FALSE); + SetDlgItemText(IDC_DB_DATABASE, g_dbConfig.szDatabase); + SetDlgItemText(IDC_DB_USERNAME, g_dbConfig.szUsername); + SetDlgItemText(IDC_DB_PASSWORD, g_dbConfig.szPassword); + + SetDlgItemText(IDC_DB_STATUS, ""); + + return TRUE; +} + +void CDlgDBSettings::OnOK() +{ + g_dbConfig.bEnabled = IsDlgButtonChecked(IDC_DB_ENABLED) == BST_CHECKED; + g_dbConfig.bExportEvents = IsDlgButtonChecked(IDC_DB_EXPORTEVENTS) == BST_CHECKED; + + GetDlgItemText(IDC_DB_HOST, g_dbConfig.szHost, sizeof(g_dbConfig.szHost)); + g_dbConfig.nPort = (int)GetDlgItemInt(IDC_DB_PORT, NULL, FALSE); + GetDlgItemText(IDC_DB_DATABASE, g_dbConfig.szDatabase, sizeof(g_dbConfig.szDatabase)); + GetDlgItemText(IDC_DB_USERNAME, g_dbConfig.szUsername, sizeof(g_dbConfig.szUsername)); + GetDlgItemText(IDC_DB_PASSWORD, g_dbConfig.szPassword, sizeof(g_dbConfig.szPassword)); + + DB_SaveConfig(); + CDialog::OnOK(); +} + +void CDlgDBSettings::OnTestConnection() +{ + // Read fields into a temporary config so we test what's in the UI, + // not necessarily what was last saved. + SDBConfig saved = g_dbConfig; + + GetDlgItemText(IDC_DB_HOST, g_dbConfig.szHost, sizeof(g_dbConfig.szHost)); + g_dbConfig.nPort = (int)GetDlgItemInt(IDC_DB_PORT, NULL, FALSE); + GetDlgItemText(IDC_DB_DATABASE, g_dbConfig.szDatabase, sizeof(g_dbConfig.szDatabase)); + GetDlgItemText(IDC_DB_USERNAME, g_dbConfig.szUsername, sizeof(g_dbConfig.szUsername)); + GetDlgItemText(IDC_DB_PASSWORD, g_dbConfig.szPassword, sizeof(g_dbConfig.szPassword)); + + SetDlgItemText(IDC_DB_STATUS, "Testing..."); + UpdateWindow(); + + char szError[512] = ""; + bool bOK = DB_TestConnection(szError, sizeof(szError)); + SetDlgItemText(IDC_DB_STATUS, bOK ? "OK - Connected successfully." : szError); + + g_dbConfig = saved; // restore; OnOK() will save the final values +} + void CChildView::OnFileOpen() { // TODO: Add your command handler code here diff --git a/Gameleap/code/mw4/Code/mw4print/ChildView.h b/Gameleap/code/mw4/Code/mw4print/ChildView.h index 52bc4ed3..14edcc75 100644 --- a/Gameleap/code/mw4/Code/mw4print/ChildView.h +++ b/Gameleap/code/mw4/Code/mw4print/ChildView.h @@ -9,11 +9,41 @@ #pragma once #endif // _MSC_VER > 1000 +class CDlgBannerSetting : public CDialog +{ +public: + CDlgBannerSetting(CWnd* pParent = NULL); + enum { IDD = IDD_BANNER_SETTING }; +protected: + virtual void DoDataExchange(CDataExchange* pDX); + virtual BOOL OnInitDialog(); + virtual void OnOK(); + DECLARE_MESSAGE_MAP() +}; + ///////////////////////////////////////////////////////////////////////////// // CChildView window class MW4PRINT_COPYDATASTRUCT; +///////////////////////////////////////////////////////////////////////////// +// CDlgDBSettings dialog + +class CDlgDBSettings : public CDialog +{ +public: + CDlgDBSettings(CWnd* pParent = NULL); + + enum { IDD = IDD_DB_SETTINGS }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + virtual BOOL OnInitDialog(); + virtual void OnOK(); + afx_msg void OnTestConnection(); + DECLARE_MESSAGE_MAP() +}; + class CMyPrintInfo : public TDBLNK(CMyPrintInfo*) { public: @@ -131,6 +161,8 @@ protected: afx_msg void OnFileOpen(); afx_msg void OnFileLogReport(); afx_msg void OnFileLog2Dates(); + afx_msg void OnFileDbSettings(); + afx_msg void OnFileBannerSetting(); DECLARE_MESSAGE_MAP() }; diff --git a/Gameleap/code/mw4/Code/mw4print/dbexport.cpp b/Gameleap/code/mw4/Code/mw4print/dbexport.cpp new file mode 100644 index 00000000..d0a9af1c --- /dev/null +++ b/Gameleap/code/mw4/Code/mw4print/dbexport.cpp @@ -0,0 +1,486 @@ +// 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; +} diff --git a/Gameleap/code/mw4/Code/mw4print/dbexport.h b/Gameleap/code/mw4/Code/mw4print/dbexport.h new file mode 100644 index 00000000..c47a9e1c --- /dev/null +++ b/Gameleap/code/mw4/Code/mw4print/dbexport.h @@ -0,0 +1,59 @@ +// dbexport.h - MySQL export interface for mw4print +// +// Connects to an external MySQL server and writes match results after each +// print job. MySQL is loaded at runtime via late binding (libmysql.dll) so +// no MySQL SDK is required at compile time. +// +// Config is stored in mw4print.ini next to the exe: +// [MySQLExport] +// Enabled=0 +// Host=192.168.1.100 +// Port=3306 +// Database=firestorm_stats +// Username=mw4 +// Password= +// ExportEvents=0 (1 = also export per-event SRecScore rows) +// + +#ifndef __DBEXPORT_H__ +#define __DBEXPORT_H__ + +// ---- Config struct ------------------------------------------------------- + +struct SDBConfig +{ + bool bEnabled; + char szHost[256]; + int nPort; + char szDatabase[64]; + char szUsername[64]; + char szPassword[64]; + bool bExportEvents; // export individual SRecScore events (can be many rows) +}; + +extern SDBConfig g_dbConfig; + +// ---- Public API ---------------------------------------------------------- + +// Load / save config from mw4print.ini in the app directory. +void DB_LoadConfig(); +void DB_SaveConfig(); + +// Try to open a connection using current g_dbConfig, run a trivial query, +// then disconnect. Returns true on success; on failure fills pszError +// (up to nErrBufSize bytes) with the MySQL error string. +bool DB_TestConnection(char* pszError, int nErrBufSize); + +// Export one completed match to the database. Called from +// CRecScoreFull::DoPrint() just before PrintDlg(). +// Returns true on success. +bool DB_ExportMatch( + const CRecScoreFull& rsf, + PCRecScoreObject aRSOs[], + int nPlayers, + const int naTeams[8], + int nTeamCount, + const MWNetMissionParameters& params +); + +#endif // __DBEXPORT_H__ diff --git a/Gameleap/code/mw4/Code/mw4print/mw4print.rc b/Gameleap/code/mw4/Code/mw4print/mw4print.rc index 5fd2b40b..ce7b57aa 100644 --- a/Gameleap/code/mw4/Code/mw4print/mw4print.rc +++ b/Gameleap/code/mw4/Code/mw4print/mw4print.rc @@ -93,6 +93,9 @@ BEGIN MENUITEM "&Log report...\t^L", ID_FILE_LOGREPORT MENUITEM "Log report(&Block)...\t^B", ID_FILE_LOG2DATES MENUITEM SEPARATOR + MENUITEM "&Database Settings...\t^D", ID_FILE_DBSETTINGS + MENUITEM "Ban&ner Setting...", ID_FILE_BANNERSETTING + MENUITEM SEPARATOR MENUITEM "E&xit", ID_APP_EXIT END POPUP "&Help" @@ -110,6 +113,7 @@ END IDR_MAINFRAME ACCELERATORS PRELOAD MOVEABLE PURE BEGIN "B", ID_FILE_LOG2DATES, VIRTKEY, CONTROL, NOINVERT + "D", ID_FILE_DBSETTINGS, VIRTKEY, CONTROL, NOINVERT "L", ID_FILE_LOGREPORT, VIRTKEY, CONTROL, NOINVERT "O", ID_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT END @@ -132,6 +136,44 @@ BEGIN DEFPUSHBUTTON "OK",IDOK,178,7,50,14,WS_GROUP END +IDD_BANNER_SETTING DIALOG DISCARDABLE 0, 0, 300, 65 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Banner Setting" +FONT 8, "MS Sans Serif" +BEGIN + LTEXT "Banner text:",IDC_STATIC,7,9,50,8 + EDITTEXT IDC_BANNER_TEXT,60,7,233,14,ES_AUTOHSCROLL + DEFPUSHBUTTON "OK",IDOK,189,43,50,14 + PUSHBUTTON "Cancel",IDCANCEL,244,43,50,14 + LTEXT "(Printed at the bottom of every score sheet. Leave blank to hide.)", + IDC_STATIC,7,28,286,16 +END + +IDD_DB_SETTINGS DIALOG DISCARDABLE 0, 0, 300, 175 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Database Export Settings" +FONT 8, "MS Sans Serif" +BEGIN + CONTROL "Enable MySQL export",IDC_DB_ENABLED,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,7,7,200,12 + LTEXT "Host:",IDC_STATIC,7,26,30,8 + EDITTEXT IDC_DB_HOST,55,24,170,14,ES_AUTOHSCROLL + LTEXT "Port:",IDC_STATIC,7,46,30,8 + EDITTEXT IDC_DB_PORT,55,44,50,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Database:",IDC_STATIC,7,66,40,8 + EDITTEXT IDC_DB_DATABASE,55,64,170,14,ES_AUTOHSCROLL + LTEXT "Username:",IDC_STATIC,7,86,40,8 + EDITTEXT IDC_DB_USERNAME,55,84,170,14,ES_AUTOHSCROLL + LTEXT "Password:",IDC_STATIC,7,106,40,8 + EDITTEXT IDC_DB_PASSWORD,55,104,170,14,ES_AUTOHSCROLL | ES_PASSWORD + CONTROL "Export individual events (many rows)",IDC_DB_EXPORTEVENTS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,123,200,12 + PUSHBUTTON "Test Connection",IDC_DB_TEST,226,104,68,14 + LTEXT "",IDC_DB_STATUS,7,141,286,18,SS_NOPREFIX + DEFPUSHBUTTON "OK",IDOK,189,155,50,14 + PUSHBUTTON "Cancel",IDCANCEL,244,155,50,14 +END + IDD_GET_2DATES DIALOG DISCARDABLE 0, 0, 187, 75 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Date selection" diff --git a/Gameleap/code/mw4/Code/mw4print/recscore.cpp b/Gameleap/code/mw4/Code/mw4print/recscore.cpp index 24ad0a2b..c66f6e93 100644 --- a/Gameleap/code/mw4/Code/mw4print/recscore.cpp +++ b/Gameleap/code/mw4/Code/mw4print/recscore.cpp @@ -268,6 +268,10 @@ static RECT s_rcTextMargin; // check!!! - margin 2 panes on bottom relative to t // so g_aFonts[FNTI_INFO_TEXT/FNTI_INFO_TEXT_BOLD].m_nTextHeight should less then s_nTextHeight static int s_nTextHeight; // check!!! - text line height +// Configurable banner text printed at the bottom of each sheet. +// Set via [battle tech print] BannerText= in options.ini next to the exe. +char g_szPrintBanner[256] = "Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!"; + void _stdcall GetFileName4GUID(char szBuf[256], const char* pcszDrv, const char* pcszPath, const char* pcszExt, const GUID& guid = *(const GUID*)NULL); void WriteMString(MemoryStream& mf, const MString& ms) @@ -2602,6 +2606,15 @@ int CRecScoreFull::DoPrint() const MemoryStream stream(m_pMissionParam, m_nMissionParamSize, 0); params.LoadParameters(&stream); +#ifdef MW4PRINT + // ---- Database export (before printing) ---- + // Export match data to the external MySQL database if configured. + if (g_dbConfig.bEnabled) { + DB_ExportMatch(*this, aRSOs, nPlayers, naTeams, nTeamCount, params); + } + // ------------------------------------------- +#endif // MW4PRINT + static int s_nIndex = -1; // for job name - not important!! if (++s_nIndex >= 10) s_nIndex = 0; @@ -2925,8 +2938,7 @@ void CRecScoreFull::DoPrint(HDC hDC, const RECT& rcDraw, const POINT& DPI, int n Rectangle(hDC, rcMH.left, rcURL.top, rcMH.right, rcURL.bottom); - static const char s_szURL[] = "Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!"; - DrawText(hDC, s_szURL, strlen(s_szURL), &rcURL, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER); + DrawText(hDC, g_szPrintBanner, strlen(g_szPrintBanner), &rcURL, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER); SelectObject(hDC, hFontOld); // end URL Text @@ -3498,6 +3510,11 @@ void CRecScoreFull::LoadPrintParams() const LoadPrintParam(strOI, "TextMarginR", s_rcTextMargin.right, U_point(300), 0, U_cm(100)); LoadPrintParam(strOI, "TextMarginB", s_rcTextMargin.bottom, U_point(300), 0, U_cm(100)); LoadPrintParam(strOI, "TextHeight", s_nTextHeight, U_point(1100), U_point(600), U_point(1500)); + + // Configurable banner/URL text printed at the bottom of each sheet. + GetPrivateProfileString("battle tech print", "BannerText", + "Join the worlds greatest Battletech community at WWW.MECHJOCK.COM today!", + g_szPrintBanner, sizeof(g_szPrintBanner), strOI); LoadPrintParam(strOI, "ImageTitleHeight", g_aFonts[FNTI_IMAGE_TITLE].m_nTextHeight, U_point(1500), U_point(900), U_point(2000)); LoadPrintParam(strOI, "TableTextHeight", g_aFonts[FNTI_TABLE_TEXT].m_nTextHeight, U_point(900), U_point(600), U_point(1200)); LoadPrintParam(strOI, "MHTextHeight", g_aFonts[FNTI_MISSION_HIGHLIGHTS].m_nTextHeight, U_point(1200), U_point(800), U_point(1600)); diff --git a/Gameleap/code/mw4/Code/mw4print/resource.h b/Gameleap/code/mw4/Code/mw4print/resource.h index a47f8b32..d4ffdffc 100644 --- a/Gameleap/code/mw4/Code/mw4print/resource.h +++ b/Gameleap/code/mw4/Code/mw4print/resource.h @@ -10,15 +10,31 @@ #define IDC_DNT_END 1001 #define ID_FILE_LOGREPORT 32771 #define ID_FILE_LOG2DATES 32772 +#define ID_FILE_DBSETTINGS 32773 +#define ID_FILE_BANNERSETTING 32774 + +#define IDD_DB_SETTINGS 131 +#define IDD_BANNER_SETTING 132 + +#define IDC_DB_ENABLED 1002 +#define IDC_DB_HOST 1003 +#define IDC_DB_PORT 1004 +#define IDC_DB_DATABASE 1005 +#define IDC_DB_USERNAME 1006 +#define IDC_DB_PASSWORD 1007 +#define IDC_DB_TEST 1008 +#define IDC_DB_STATUS 1009 +#define IDC_DB_EXPORTEVENTS 1010 +#define IDC_BANNER_TEXT 1011 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_3D_CONTROLS 1 -#define _APS_NEXT_RESOURCE_VALUE 131 -#define _APS_NEXT_COMMAND_VALUE 32772 -#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_RESOURCE_VALUE 133 +#define _APS_NEXT_COMMAND_VALUE 32775 +#define _APS_NEXT_CONTROL_VALUE 1012 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif diff --git a/Gameleap/mw4/libmysql.dll b/Gameleap/mw4/libmysql.dll new file mode 100644 index 00000000..b0273e13 --- /dev/null +++ b/Gameleap/mw4/libmysql.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4e8981f67a2d8174ba16c82239bc82af1b02756848f7f5ed326afe36c9a253 +size 4630016