Files
firestorm/Gameleap/code/mw4/Code/mw4print/dbexport.cpp
T
dicion b98cb87ef3 mw4print: MySQL export, configurable banner text
MySQL database export (dbexport.h / dbexport.cpp):
- New files dbexport.h / dbexport.cpp implement late-bound MySQL export.
  libmysql.dll is loaded at runtime via LoadLibrary/GetProcAddress so no
  MySQL SDK is required at compile time; the app runs normally if the DLL
  is absent.
- After each print job, match data is exported to a MySQL server before
  PrintDlg() is called: one row in 'match', one row per player in
  'player_result', one row per attacker/victim pair in 'pvp'.  An optional
  'event' table records every individual SRecScore entry (off by default).
  Tables are created automatically (CREATE TABLE IF NOT EXISTS) on first
  connect.
- Config stored in mw4print.ini (app directory), section [MySQLExport]:
  Enabled, Host, Port, Database, Username, Password, ExportEvents.
- Config loaded at startup (OnCreate); DB_LoadConfig() / DB_SaveConfig().
- Connection timeout set to 5 seconds so the app does not hang if the
  server is unreachable.
- libmysql.dll (MySQL Connector/C 32-bit) added to Gameleap/mw4/ so the
  deploy script copies it to MW4/ alongside mw4print.exe.

Database Settings dialog (File > Database Settings... / Ctrl+D):
- MFC dialog: enable checkbox, Host/Port/Database/Username/Password fields,
  Export Events checkbox, Test Connection button with live status label,
  OK/Cancel.  OK persists settings to mw4print.ini immediately.

Configurable banner text (File > Banner Setting...):
- The 'WWW.MECHJOCK.COM' URL string printed at the bottom of every score
  sheet is now configurable.  Stored as BannerText= in options.ini under
  [battle tech print] (same section/file as the other print layout params).
  File > Banner Setting... opens a dialog to edit it; OK saves to options.ini
  and takes effect on the next print job with no restart needed.
  Default value is the original MECHJOCK string if the key is absent.
2026-07-18 14:05:31 -05:00

487 lines
19 KiB
C++

// 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;
}