410 lines
11 KiB
C++
410 lines
11 KiB
C++
![]() |
/*
|
||
|
** Command & Conquer Generals Zero Hour(tm)
|
||
|
** Copyright 2025 Electronic Arts Inc.
|
||
|
**
|
||
|
** This program is free software: you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation, either version 3 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
#ifdef _WIN32
|
||
|
#include <process.h>
|
||
|
#endif
|
||
|
#include <cstdlib>
|
||
|
#include <csignal>
|
||
|
#include <iostream>
|
||
|
|
||
|
#ifdef _WIN32
|
||
|
#include <direct.h>
|
||
|
#else
|
||
|
#include <sys/types.h>
|
||
|
#include <sys/stat.h>
|
||
|
#endif
|
||
|
|
||
|
//#define THREADSAFE_HEADER
|
||
|
|
||
|
#include <wstring.h>
|
||
|
#include <wdebug.h>
|
||
|
#include <filed.h>
|
||
|
#include <stdoutd.h>
|
||
|
#include <wstypes.h>
|
||
|
#include <xtime.h>
|
||
|
#include "global.h"
|
||
|
#include "generals.h"
|
||
|
#include "timezone.h"
|
||
|
#include <threadfac.h>
|
||
|
|
||
|
#include <tcp.h>
|
||
|
#include "mydebug.h"
|
||
|
|
||
|
#ifdef _UNIX
|
||
|
using namespace std;
|
||
|
#else
|
||
|
#define sleep(x) Sleep(1000 * (x))
|
||
|
#endif
|
||
|
|
||
|
static char *Program_Usage = "A config filename can be given on the command line (default=matchbot.cfg)\n";
|
||
|
void logMonitor(void *);
|
||
|
void paranoidLogMonitor(void *);
|
||
|
|
||
|
OutputDevice * output_device = NULL;
|
||
|
OutputDevice * paranoid_output_device = NULL;
|
||
|
|
||
|
|
||
|
void Signal_Quit(int)
|
||
|
{
|
||
|
INFMSG("Exiting due to signal.");
|
||
|
exit(2);
|
||
|
}
|
||
|
|
||
|
void Setup_Signals(void)
|
||
|
{
|
||
|
#ifdef _UNIX
|
||
|
struct sigaction act, oact;
|
||
|
act.sa_handler = Signal_Quit;
|
||
|
sigemptyset(&act.sa_mask);
|
||
|
act.sa_flags = 0;
|
||
|
sigaction(SIGTERM, &act, &oact);
|
||
|
sigaction(SIGINT, &act, &oact);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
int VerifyFileDescriptors(int requested)
|
||
|
{
|
||
|
#ifdef _UNIX
|
||
|
struct rlimit limit;
|
||
|
if (!getrlimit(_SC_OPEN_MAX, &limit))
|
||
|
{
|
||
|
INFMSG("Hard limit on file descriptors: " << limit.rlim_max);
|
||
|
if (limit.rlim_max < (unsigned int)requested)
|
||
|
{
|
||
|
ERRMSG("Too many file descriptors requested");
|
||
|
ERRMSG("Hard Limit: " << limit.rlim_max << ", requested: " << requested);
|
||
|
requested = limit.rlim_max;
|
||
|
}
|
||
|
|
||
|
limit.rlim_cur = limit.rlim_max; /* make soft limit the max */
|
||
|
if (setrlimit(_SC_OPEN_MAX, &limit) == -1)
|
||
|
{
|
||
|
ERRMSG("Error setting max file descriptors to " << limit.rlim_cur);
|
||
|
exit(-1);
|
||
|
}
|
||
|
INFMSG("Soft limit on file descriptors: " << limit.rlim_cur);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ERRMSG("Couldn't get limit for _SC_OPEN_MAX");
|
||
|
exit(-1);
|
||
|
}
|
||
|
#endif
|
||
|
return requested;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
GeneralsMatcher *s_generalsMatcher = NULL;
|
||
|
GeneralsClientMatcher *s_generalsClientMatcher = NULL;
|
||
|
|
||
|
int main(int argc, char ** argv)
|
||
|
{
|
||
|
Wstring config_fname = "matchbot.cfg";
|
||
|
|
||
|
// You can specify the config file on the command line
|
||
|
if (argc == 2)
|
||
|
config_fname = argv[1];
|
||
|
|
||
|
// Read the config file
|
||
|
FILE *fp;
|
||
|
if ((fp = fopen(config_fname.get(), "r")) == NULL)
|
||
|
{
|
||
|
cerr << "\nCan't open the config file '" << config_fname.get() << "'\n\n";
|
||
|
cerr << Program_Usage << endl;
|
||
|
exit( -1);
|
||
|
}
|
||
|
fclose(fp);
|
||
|
|
||
|
Global.ReadFile(config_fname.get());
|
||
|
|
||
|
// Setup debugging & logging output
|
||
|
Wstring output_file;
|
||
|
output_file.set("matchbot.log");
|
||
|
Global.config.getString("OUTPUTFILE", output_file);
|
||
|
if (output_file != "STDOUT")
|
||
|
{
|
||
|
int append = 1;
|
||
|
Global.config.getInt("APPENDTOLOG", append);
|
||
|
if (append)
|
||
|
output_device = new FileD(output_file.get(), "a");
|
||
|
else
|
||
|
output_device = new FileD(output_file.get(), "w");
|
||
|
if (!output_device)
|
||
|
{
|
||
|
cerr << "Could not open " << output_file.get() << " for writing!" << endl;
|
||
|
exit( -1);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
output_device = new StdoutD;
|
||
|
}
|
||
|
MsgManager::setAllStreams(output_device);
|
||
|
DBGMSG("Matching bot started!");
|
||
|
INFMSG("Matching bot " << argv[0] << " started!");
|
||
|
|
||
|
// Setup logging of suspicious activity
|
||
|
Wstring paranoid_output_file;
|
||
|
paranoid_output_file.set("hacks.log");
|
||
|
Global.config.getString("PARANOIDFILE", paranoid_output_file);
|
||
|
if (paranoid_output_file != "STDOUT")
|
||
|
{
|
||
|
paranoid_output_device = new FileD(paranoid_output_file.get(), "a");
|
||
|
if (!paranoid_output_device)
|
||
|
{
|
||
|
cerr << "Could not open " << paranoid_output_file.get() << " for writing!" << endl;
|
||
|
exit( -1);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
paranoid_output_device = new StdoutD;
|
||
|
}
|
||
|
MyMsgManager::setParanoidStream(paranoid_output_device);
|
||
|
DBGMSG("Hack log started!");
|
||
|
PARANOIDMSG("Hack log started!");
|
||
|
|
||
|
Setup_Signals();
|
||
|
|
||
|
#ifdef _WINDOWS
|
||
|
// ----- Initialize Winsock -----
|
||
|
WORD verReq = MAKEWORD(2, 2);
|
||
|
WSADATA wsadata;
|
||
|
|
||
|
int err = WSAStartup(verReq, &wsadata);
|
||
|
if (err != 0)
|
||
|
{
|
||
|
ERRMSG("Winsock Init failed.");
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
if ((LOBYTE(wsadata.wVersion) != 2) || (HIBYTE(wsadata.wVersion) !=2))
|
||
|
{
|
||
|
ERRMSG("Winsock DLL is not 2.2");
|
||
|
WSACleanup();
|
||
|
ERRMSG("Winsock Init failed.");
|
||
|
return 1;
|
||
|
}
|
||
|
INFMSG("Winsock Init done.");
|
||
|
#endif
|
||
|
|
||
|
// Check game type & start matcher
|
||
|
Wstring gametype = "unknown";
|
||
|
Global.config.getString("GAME", gametype);
|
||
|
|
||
|
// Command-line override for gamemode.
|
||
|
// This is for test suites, so they can use
|
||
|
// the same config file as the corresponding
|
||
|
// matchbot.
|
||
|
const char *s = argv[0] + strlen(argv[0]);
|
||
|
while (s > argv[0] && *s != '/')
|
||
|
--s;
|
||
|
if (*s == '/')
|
||
|
++s;
|
||
|
Wstring exe = s;
|
||
|
exe.toLower();
|
||
|
DBGMSG("Executable file is [" << exe.get() << "]");
|
||
|
|
||
|
if (gametype == "Generals")
|
||
|
{
|
||
|
DBGMSG("Generals matching behavior");
|
||
|
s_generalsMatcher = new GeneralsMatcher;
|
||
|
s_generalsMatcher->connectAndLoop();
|
||
|
}
|
||
|
else if (gametype == "GeneralsClient")
|
||
|
{
|
||
|
DBGMSG("Generals TEST client matching behavior");
|
||
|
s_generalsClientMatcher = new GeneralsClientMatcher;
|
||
|
s_generalsClientMatcher->connectAndLoop();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
cerr << "\nNo valid GAME entry found!" << endl;
|
||
|
exit( -1);
|
||
|
}
|
||
|
|
||
|
if (s_generalsMatcher)
|
||
|
delete s_generalsMatcher;
|
||
|
if (s_generalsClientMatcher)
|
||
|
delete s_generalsClientMatcher;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*----------------------------------------------------------------------+
|
||
|
| THREAD: logMonitor |
|
||
|
| This thread is spawned once per execution. It will activate after |
|
||
|
| midnight and create a new log file. The old one gets put into the |
|
||
|
| logfiles directory. |
|
||
|
`----------------------------------------------------------------------*/
|
||
|
void logMonitor(void *)
|
||
|
{
|
||
|
#ifdef _UNIX
|
||
|
Xtime xtime;
|
||
|
time_t curtime;
|
||
|
//char timebuf[40];
|
||
|
char filenamebuf[128];
|
||
|
int delay = -1;
|
||
|
Global.config.getInt("ROTATEDELAY", delay);
|
||
|
DBGMSG("ROTATEDELAY: " << delay);
|
||
|
if (delay == -1)
|
||
|
return ;
|
||
|
while (1)
|
||
|
{
|
||
|
curtime = time(NULL);
|
||
|
// get the number of seconds that have passed since midnight
|
||
|
// of the current day.
|
||
|
curtime -= TimezoneOffset();
|
||
|
time_t timeofday = curtime % (delay);
|
||
|
if ((timeofday > 0) && (timeofday <= 300))
|
||
|
{
|
||
|
// We're within 5 minutes of midnight, switch the files.
|
||
|
|
||
|
DBGMSG("about to switch.");
|
||
|
Wstring logfilename = "matchbot.log";
|
||
|
Global.config.getString("OUTPUTFILE", logfilename);
|
||
|
Wstring newfilename = "tmp.log";
|
||
|
Global.config.getString("ROTATEFILE", newfilename);
|
||
|
MsgManager::ReplaceAllStreams((FileD*)output_device, logfilename.get(), newfilename.get());
|
||
|
Wstring logpath = "logs";
|
||
|
Global.config.getString("LOGPATH", logpath);
|
||
|
xtime.update();
|
||
|
sprintf(filenamebuf, "%s/%02d%02d%04d_%02d%02d%02d_log", logpath.get(), xtime.getMonth(),
|
||
|
xtime.getMDay(), xtime.getYear(), xtime.getHour(), xtime.getMinute(), xtime.getSecond());
|
||
|
rename(newfilename.get(), filenamebuf);
|
||
|
DBGMSG("Normal: Just been switched. " << logfilename.get() << ", " << newfilename.get());
|
||
|
sleep(60*60*23); // sleep the next 23 hours
|
||
|
}
|
||
|
sleep(300);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void rotateOutput(void)
|
||
|
{
|
||
|
Xtime xtime;
|
||
|
char filenamebuf[128];
|
||
|
Wstring logfilename = "matchbot.log";
|
||
|
Wstring newfilename = "tmp.log";
|
||
|
Wstring logpath = "logs";
|
||
|
|
||
|
DBGMSG("About to switch.");
|
||
|
|
||
|
Global.config.getString("OUTPUTFILE", logfilename);
|
||
|
Global.config.getString("ROTATEFILE", newfilename);
|
||
|
Global.config.getString("LOGPATH", logpath);
|
||
|
|
||
|
// This grabs the semaphore, renames the file, and switches the output device
|
||
|
MsgManager::ReplaceAllStreams((FileD*)output_device, logfilename.get(),
|
||
|
newfilename.get());
|
||
|
|
||
|
// clean up the tmp filename and move it to the log dir.
|
||
|
sprintf(filenamebuf, "%s/%02d%02d%04d_%02d%02d%02d_log", logpath.get(),
|
||
|
xtime.getMonth(), xtime.getMDay(), xtime.getYear(), xtime.getHour(),
|
||
|
xtime.getMinute(), xtime.getSecond());
|
||
|
#ifdef _WINDOWS
|
||
|
mkdir(logpath.get());
|
||
|
#else
|
||
|
mkdir(logpath.get(), 00666);
|
||
|
#endif
|
||
|
rename(newfilename.get(), filenamebuf);
|
||
|
|
||
|
DBGMSG("Normal: Just been switched. " << logfilename.get() << ", " <<
|
||
|
newfilename.get());
|
||
|
}
|
||
|
|
||
|
void paranoidLogMonitor(void *)
|
||
|
{
|
||
|
#ifdef _UNIX
|
||
|
Xtime xtime;
|
||
|
time_t curtime;
|
||
|
//char timebuf[40];
|
||
|
char filenamebuf[128];
|
||
|
int delay = -1;
|
||
|
Global.config.getInt("ROTATEDELAY", delay);
|
||
|
PARANOIDMSG("ROTATEDELAY: " << delay);
|
||
|
if (delay == -1)
|
||
|
return ;
|
||
|
while (1)
|
||
|
{
|
||
|
curtime = time(NULL);
|
||
|
// get the number of seconds that have passed since midnight
|
||
|
// of the current day.
|
||
|
curtime -= TimezoneOffset();
|
||
|
time_t timeofday = curtime % (delay);
|
||
|
if ((timeofday > 0) && (timeofday <= 300))
|
||
|
{
|
||
|
// We're within 5 minutes of midnight, switch the files.
|
||
|
|
||
|
PARANOIDMSG("about to switch.");
|
||
|
Wstring logfilename = "matchbot.log";
|
||
|
Global.config.getString("PARANOIDFILE", logfilename);
|
||
|
Wstring newfilename = "tmp.log";
|
||
|
Global.config.getString("ROTATEPARANOIDFILE", newfilename);
|
||
|
MyMsgManager::ReplaceAllStreams((FileD*)paranoid_output_device, logfilename.get(), newfilename.get());
|
||
|
Wstring logpath = "logs";
|
||
|
Global.config.getString("PARANOIDLOGPATH", logpath);
|
||
|
xtime.update();
|
||
|
sprintf(filenamebuf, "%s/%02d%02d%04d_%02d%02d%02d_log", logpath.get(), xtime.getMonth(),
|
||
|
xtime.getMDay(), xtime.getYear(), xtime.getHour(), xtime.getMinute(), xtime.getSecond());
|
||
|
rename(newfilename.get(), filenamebuf);
|
||
|
PARANOIDMSG("Paranoid: Just been switched. " << logfilename.get() << ", " << newfilename.get());
|
||
|
sleep(60*60*23); // sleep the next 23 hours
|
||
|
}
|
||
|
sleep(300);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void rotateParanoid(void)
|
||
|
{
|
||
|
Xtime xtime;
|
||
|
char filenamebuf[128];
|
||
|
Wstring logfilename = "matchbot.log";
|
||
|
Wstring newfilename = "tmp.log";
|
||
|
Wstring logpath = "logs";
|
||
|
|
||
|
PARANOIDMSG("About to switch.");
|
||
|
|
||
|
Global.config.getString("PARANOIDFILE", logfilename);
|
||
|
Global.config.getString("ROTATEPARANOIDFILE", newfilename);
|
||
|
Global.config.getString("PARANOIDLOGPATH", logpath);
|
||
|
|
||
|
// This grabs the semaphore, renames the file, and switches the output device
|
||
|
MyMsgManager::ReplaceAllStreams((FileD*)output_device, logfilename.get(),
|
||
|
newfilename.get());
|
||
|
|
||
|
// clean up the tmp filename and move it to the log dir.
|
||
|
sprintf(filenamebuf, "%s/%02d%02d%04d_%02d%02d%02d_log", logpath.get(),
|
||
|
xtime.getMonth(), xtime.getMDay(), xtime.getYear(), xtime.getHour(),
|
||
|
xtime.getMinute(), xtime.getSecond());
|
||
|
#ifdef _WINDOWS
|
||
|
mkdir(logpath.get());
|
||
|
#else
|
||
|
mkdir(logpath.get(), 00666);
|
||
|
#endif
|
||
|
rename(newfilename.get(), filenamebuf);
|
||
|
|
||
|
PARANOIDMSG("Paranoid: Just been switched. " << logfilename.get() << ", " <<
|
||
|
newfilename.get());
|
||
|
}
|
||
|
|
||
|
|