/* ** Command & Conquer Generals(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 . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/Recorder.h" #include "Common/FileSystem.h" #include "Common/playerlist.h" #include "Common/Player.h" #include "Common/GlobalData.h" #include "Common/GameEngine.h" #include "GameClient/GameWindow.h" #include "GameClient/GameWindowManager.h" #include "GameClient/InGameUI.h" #include "GameClient/Shell.h" #include "GameClient/GameText.h" #include "GameNetwork/LANAPICallbacks.h" #include "GameNetwork/GameMessageParser.h" #include "GameNetwork/GameSpy/PeerDefs.h" #include "GameNetwork/NetworkUtil.h" #include "GameLogic/GameLogic.h" #include "Common/RandomValue.h" #include "Common/CRCDebug.h" #include "Common/Version.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif Int REPLAY_CRC_INTERVAL = 100; const char *replayExtention = ".rep"; const char *lastReplayFileName = "00000000"; // a name the user is unlikely to ever type, but won't cause panic & confusion static time_t startTime; static const UnsignedInt startTimeOffset = 6; static const UnsignedInt endTimeOffset = startTimeOffset + sizeof(time_t); static const UnsignedInt framesOffset = endTimeOffset + sizeof(time_t); static const UnsignedInt desyncOffset = framesOffset + sizeof(UnsignedInt); static const UnsignedInt quitEarlyOffset = desyncOffset + sizeof(Bool); static const UnsignedInt disconOffset = quitEarlyOffset + sizeof(Bool); void RecorderClass::logGameStart(AsciiString options) { if (!m_file) return; time(&startTime); UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, startTimeOffset, SEEK_SET)) { // save off start time fwrite(&startTime, sizeof(time_t), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING Int res = #endif fseek(m_file, fileSize, SEEK_SET); DEBUG_ASSERTCRASH(res == 0, ("Could not seek to end of file!")); #if defined(_DEBUG) || defined(_INTERNAL) if (TheNetwork && TheGlobalData->m_saveStats) { //if (TheLAN) { unsigned long bufSize = MAX_COMPUTERNAME_LENGTH + 1; char computerName[MAX_COMPUTERNAME_LENGTH + 1]; if (!GetComputerName(computerName, &bufSize)) { strcpy(computerName, "unknown"); } AsciiString statsFile = TheGlobalData->m_baseStatsDir; TheFileSystem->createDirectory(statsFile); statsFile.concat(computerName); statsFile.concat(".txt"); FILE *logFP = fopen(statsFile.str(), "a+"); if (!logFP) { // try again locally TheWritableGlobalData->m_baseStatsDir = TheGlobalData->getPath_UserData(); statsFile = TheGlobalData->m_baseStatsDir; statsFile.concat(computerName); statsFile.concat(".txt"); logFP = fopen(statsFile.str(), "a+"); } if (logFP) { struct tm *t2 = localtime(&startTime); fprintf(logFP, "\nGame start at %s\tOptions are %s\n", asctime(t2), options.str()); fclose(logFP); } } } #endif } void RecorderClass::logPlayerDisconnect(UnicodeString player, Int slot) { if (!m_file) return; DEBUG_ASSERTCRASH((slot >= 0) && (slot < MAX_SLOTS), ("Attempting to disconnect an invalid slot number")); if ((slot < 0) || (slot >= (MAX_SLOTS))) { return; } UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, disconOffset + slot*sizeof(Bool), SEEK_SET)) { // save off discon status Bool b = TRUE; fwrite(&b, sizeof(Bool), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING Int res = #endif fseek(m_file, fileSize, SEEK_SET); DEBUG_ASSERTCRASH(res == 0, ("Could not seek to end of file!")); #if defined(_DEBUG) || defined(_INTERNAL) if (TheGlobalData->m_saveStats) { unsigned long bufSize = MAX_COMPUTERNAME_LENGTH + 1; char computerName[MAX_COMPUTERNAME_LENGTH + 1]; if (!GetComputerName(computerName, &bufSize)) { strcpy(computerName, "unknown"); } AsciiString statsFile = TheGlobalData->m_baseStatsDir; statsFile.concat(computerName); statsFile.concat(".txt"); FILE *logFP = fopen(statsFile.str(), "a+"); if (logFP) { time_t t; time(&t); struct tm *t2 = localtime(&t); fprintf(logFP, "\tPlayer %ls dropped at %s", player.str(), asctime(t2)); fclose(logFP); } } #endif } void RecorderClass::logCRCMismatch( void ) { if (!m_file) return; UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, desyncOffset, SEEK_SET)) { // save off desync status Bool b = TRUE; fwrite(&b, sizeof(Bool), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING Int res = #endif fseek(m_file, fileSize, SEEK_SET); DEBUG_ASSERTCRASH(res == 0, ("Could not seek to end of file!")); #if defined(_DEBUG) || defined(_INTERNAL) if (TheGlobalData->m_saveStats) { m_wasDesync = TRUE; unsigned long bufSize = MAX_COMPUTERNAME_LENGTH + 1; char computerName[MAX_COMPUTERNAME_LENGTH + 1]; if (!GetComputerName(computerName, &bufSize)) { strcpy(computerName, "unknown"); } AsciiString statsFile = TheGlobalData->m_baseStatsDir; statsFile.concat(computerName); statsFile.concat(".txt"); FILE *logFP = fopen(statsFile.str(), "a+"); if (logFP) { time_t t; time(&t); struct tm *t2 = localtime(&t); fprintf(logFP, "\tCRC mismatch at %s", asctime(t2)); fclose(logFP); } } #endif } void RecorderClass::logGameEnd( void ) { if (!m_file) return; time_t t; time(&t); UnsignedInt duration = TheGameLogic->getFrame(); UnsignedInt fileSize = ftell(m_file); // move to appropriate offset if (!fseek(m_file, endTimeOffset, SEEK_SET)) { // save off end time fwrite(&t, sizeof(time_t), 1, m_file); } // move to appropriate offset if (!fseek(m_file, framesOffset, SEEK_SET)) { // save off duration fwrite(&duration, sizeof(UnsignedInt), 1, m_file); } // move back to end of stream #ifdef DEBUG_CRASHING Int res = #endif fseek(m_file, fileSize, SEEK_SET); DEBUG_ASSERTCRASH(res == 0, ("Could not seek to end of file!")); #if defined(_DEBUG) || defined(_INTERNAL) if (TheNetwork && TheGlobalData->m_saveStats) { //if (TheLAN) { unsigned long bufSize = MAX_COMPUTERNAME_LENGTH + 1; char computerName[MAX_COMPUTERNAME_LENGTH + 1]; if (!GetComputerName(computerName, &bufSize)) { strcpy(computerName, "unknown"); } AsciiString statsFile = TheGlobalData->m_baseStatsDir; statsFile.concat(computerName); statsFile.concat(".txt"); FILE *logFP = fopen(statsFile.str(), "a+"); if (logFP) { struct tm *t2 = localtime(&t); duration = t - startTime; Int minutes = duration/60; Int seconds = duration%60; fprintf(logFP, "Game end at %s(%d:%2.2d elapsed time)\n", asctime(t2), minutes, seconds); fclose(logFP); } } } #endif } #ifdef DEBUG_LOGGING #if defined(_INTERNAL) #define DEBUG_FILE_NAME "DebugLogFileI.txt" #define DEBUG_FILE_NAME_PREV "DebugLogFilePrevI.txt" #elif defined(_DEBUG) #define DEBUG_FILE_NAME "DebugLogFileD.txt" #define DEBUG_FILE_NAME_PREV "DebugLogFilePrevD.txt" #else #define DEBUG_FILE_NAME "DebugLogFile.txt" #define DEBUG_FILE_NAME_PREV "DebugLogFilePrev.txt" #endif #endif void RecorderClass::cleanUpReplayFile( void ) { #if defined(_DEBUG) || defined(_INTERNAL) if (TheGlobalData->m_saveStats) { char fname[_MAX_PATH+1]; strncpy(fname, TheGlobalData->m_baseStatsDir.str(), _MAX_PATH); strncat(fname, m_fileName.str(), _MAX_PATH - strlen(fname)); DEBUG_LOG(("Saving replay to %s\n", fname)); AsciiString oldFname; oldFname.format("%s%s", getReplayDir().str(), m_fileName.str()); CopyFile(oldFname.str(), fname, TRUE); #ifdef DEBUG_FILE_NAME AsciiString debugFname = fname; debugFname.removeLastChar(); debugFname.removeLastChar(); debugFname.removeLastChar(); debugFname.concat("txt"); UnsignedInt fileSize = 0; FILE *fp = fopen(DEBUG_FILE_NAME, "rb"); if (fp) { fseek(fp, 0, SEEK_END); fileSize = ftell(fp); fclose(fp); fp = NULL; DEBUG_LOG(("Log file size was %d\n", fileSize)); } const int MAX_DEBUG_SIZE = 65536; if (fileSize <= MAX_DEBUG_SIZE || TheGlobalData->m_saveAllStats) { DEBUG_LOG(("Using CopyFile to copy %s\n", DEBUG_FILE_NAME)); CopyFile(DEBUG_FILE_NAME, debugFname.str(), TRUE); } else { DEBUG_LOG(("manual copy of %s\n", DEBUG_FILE_NAME)); FILE *ifp = fopen(DEBUG_FILE_NAME, "rb"); FILE *ofp = fopen(debugFname.str(), "wb"); if (ifp && ofp) { fseek(ifp, fileSize-MAX_DEBUG_SIZE, SEEK_SET); char buf[4096]; Int len; while ( (len=fread(buf, 1, 4096, ifp)) > 0 ) { fwrite(buf, 1, len, ofp); } fclose(ofp); fclose(ifp); ifp = NULL; ofp = NULL; } else { if (ifp) fclose(ifp); if (ofp) fclose(ofp); ifp = NULL; ofp = NULL; } } #endif // DEBUG_FILE_NAME } #endif } /** * The recorder object. */ RecorderClass *TheRecorder = NULL; /** * Constructor */ RecorderClass::RecorderClass() { m_originalGameMode = GAME_NONE; m_mode = RECORDERMODETYPE_RECORD; m_file = NULL; m_fileName.clear(); m_currentFilePosition = 0; //Added By Sadullah Nader //Initializtion(s) inserted m_doingAnalysis = FALSE; m_nextFrame = 0; m_wasDesync = FALSE; // init(); // just for the heck of it. } /** * Destructor */ RecorderClass::~RecorderClass() { } /** * Initialization * The recorder will record by default since every game will be recorded. * Obviously a game that is being played back will not be recorded. * Since the playback is done through a special interface, that interface * will set the recorder mode to RECORDERMODETYPE_PLAYBACK. */ void RecorderClass::init() { m_originalGameMode = GAME_NONE; m_mode = RECORDERMODETYPE_NONE; m_file = NULL; m_fileName.clear(); m_currentFilePosition = 0; m_gameInfo.clearSlotList(); m_gameInfo.reset(); if (TheGlobalData->m_pendingFile.isEmpty()) m_gameInfo.setMap(TheGlobalData->m_mapName); else m_gameInfo.setMap(TheGlobalData->m_pendingFile); m_gameInfo.setSeed(GetGameLogicRandomSeed()); m_wasDesync = FALSE; m_doingAnalysis = FALSE; } /** * Reset the recorder to the "initialized state." */ void RecorderClass::reset() { if (m_file != NULL) { fclose(m_file); m_file = NULL; } m_fileName.clear(); init(); } /** * update * Do the update for this frame. */ void RecorderClass::update() { if (m_mode == RECORDERMODETYPE_RECORD || m_mode == RECORDERMODETYPE_NONE) { updateRecord(); } else if (m_mode == RECORDERMODETYPE_PLAYBACK) { updatePlayback(); } } /** * Do the update for the next frame of this playback. */ void RecorderClass::updatePlayback() { cullBadCommands(); // Remove any bad commands that have been inserted by the local user that shouldn't be // executed during playback. if (m_nextFrame == -1) { // This is reached if there are no more commands to be executed. return; } UnsignedInt curFrame = TheGameLogic->getFrame(); if (m_doingAnalysis) curFrame = m_nextFrame; // While there are commands to be queued up for this frame, do it. while (m_nextFrame == curFrame) { appendNextCommand(); // append the next command to TheCommandQueue readNextFrame(); // Read the next command's frame number for playback. } } /** * Stop the currently running playback. This is probably due either to the user exiting out of the playback or * reaching the end of the playback file. */ void RecorderClass::stopPlayback() { if (m_file != NULL) { fclose(m_file); m_file = NULL; } m_fileName.clear(); // Don't clear the game data if the replay is over - let things continue //#ifdef DEBUG_CRC if (!m_doingAnalysis) TheMessageStream->appendMessage(GameMessage::MSG_CLEAR_GAME_DATA); //#endif } /** * Update function for recording a game. Basically all the pertinant logic commands for this frame are written out * to a file. */ void RecorderClass::updateRecord() { Bool needFlush = FALSE; static Int lastFrame = -1; GameMessage *msg = TheCommandList->getFirstMessage(); while (msg != NULL) { if (msg->getType() == GameMessage::MSG_NEW_GAME && msg->getArgument(0)->integer != GAME_SHELL && msg->getArgument(0)->integer != GAME_NONE) { m_originalGameMode = msg->getArgument(0)->integer; DEBUG_LOG(("RecorderClass::updateRecord() - original game is mode %d\n", m_originalGameMode)); lastFrame = 0; GameDifficulty diff = DIFFICULTY_NORMAL; if (msg->getArgumentCount() >= 2) diff = (GameDifficulty)msg->getArgument(1)->integer; Int rankPoints = 0; if (msg->getArgumentCount() >= 3) rankPoints = msg->getArgument(2)->integer; Int maxFPS = 0; if (msg->getArgumentCount() >= 4) maxFPS = msg->getArgument(3)->integer; startRecording(diff, m_originalGameMode, rankPoints, maxFPS); } else if (msg->getType() == GameMessage::MSG_CLEAR_GAME_DATA) { if (m_file != NULL) { lastFrame = -1; writeToFile(msg); stopRecording(); } m_fileName.clear(); } else { if (m_file != NULL) { if ((msg->getType() > GameMessage::MSG_BEGIN_NETWORK_MESSAGES) && (msg->getType() < GameMessage::MSG_END_NETWORK_MESSAGES)) { // Only write the important messages to the file. writeToFile(msg); needFlush = TRUE; } } } msg = msg->next(); } if (needFlush) { fflush(m_file); } } /** * Start a new file for recording. This will always overwrite the "LastReplay.rep" file with the new one. * So don't call this unless you really mean it. */ void RecorderClass::startRecording(GameDifficulty diff, Int originalGameMode, Int rankPoints, Int maxFPS) { DEBUG_ASSERTCRASH(m_file == NULL, ("Starting to record game while game is in progress.")); reset(); m_mode = RECORDERMODETYPE_RECORD; AsciiString filepath = getReplayDir(); // We have to make sure the replay dir exists. TheFileSystem->createDirectory(filepath); m_fileName = getLastReplayFileName(); m_fileName.concat(getReplayExtention()); filepath.concat(m_fileName); m_file = fopen(filepath.str(), "wb"); if (m_file == NULL) { DEBUG_ASSERTCRASH(m_file != NULL, ("Failed to create replay file")); return; } fprintf(m_file, "GENREP"); // // save space for stats to be filled in. // // **** if this changes, change the LAN Playtest code above **** // time_t t = 0; fwrite(&t, sizeof(time_t), 1, m_file); // reserve space for start time fwrite(&t, sizeof(time_t), 1, m_file); // reserve space for end time UnsignedInt frames = 0; fwrite(&frames, sizeof(UnsignedInt), 1, m_file); // reserve space for duration in frames Bool b = FALSE; fwrite(&b, sizeof(Bool), 1, m_file); // reserve space for flag (true if we desync) fwrite(&b, sizeof(Bool), 1, m_file); // reserve space for flag (true if we quit early) for (Int i=0; ifetch("GUI:LastReplay"); fwprintf(m_file, L"%ws", replayName.str()); fputwc(0, m_file); // Date and Time SYSTEMTIME systemTime; GetLocalTime( &systemTime ); fwrite(&systemTime, sizeof(SYSTEMTIME), 1, m_file); // write out version info UnicodeString versionString = TheVersion->getUnicodeVersion(); UnicodeString versionTimeString = TheVersion->getUnicodeBuildTime(); UnsignedInt versionNumber = TheVersion->getVersionNumber(); fwprintf(m_file, L"%ws", versionString.str()); fputwc(0, m_file); fwprintf(m_file, L"%ws", versionTimeString.str()); fputwc(0, m_file); fwrite(&versionNumber, sizeof(UnsignedInt), 1, m_file); fwrite(&(TheGlobalData->m_exeCRC), sizeof(UnsignedInt), 1, m_file); fwrite(&(TheGlobalData->m_iniCRC), sizeof(UnsignedInt), 1, m_file); // Number of players /* Int numPlayers = ThePlayerList->getPlayerCount(); fwrite(&numPlayers, sizeof(numPlayers), 1, m_file); */ // Write the slot list. AsciiString theSlotList; Int localIndex = -1; if (TheNetwork) { if (TheLAN) { GameInfo *game = TheLAN->GetMyGame(); DEBUG_ASSERTCRASH(game, ("Starting a LAN game with no LANGameInfo object!")); theSlotList = GameInfoToAsciiString(game); for (Int i=0; igetLocalIP() == game->getSlot(i)->getIP()) { localIndex = i; break; } } } else { theSlotList = GameInfoToAsciiString(TheGameSpyGame); localIndex = TheGameSpyGame->getLocalSlotNum(); } } else { if(TheSkirmishGameInfo) { TheSkirmishGameInfo->setCRCInterval(REPLAY_CRC_INTERVAL); theSlotList = GameInfoToAsciiString(TheSkirmishGameInfo); DEBUG_LOG(("GameInfo String: %s\n",theSlotList.str())); localIndex = 0; } else { // single player. format the generic (empty) slotlist m_gameInfo.setCRCInterval(REPLAY_CRC_INTERVAL); theSlotList = GameInfoToAsciiString(&m_gameInfo); } } logGameStart(theSlotList); DEBUG_LOG(("RecorderClass::startRecording - theSlotList = %s\n", theSlotList.str())); // write slot list (starting spots, color, alliances, etc fwrite(theSlotList.str(), theSlotList.getLength() + 1, 1, m_file); fprintf(m_file, "%d", localIndex); fputc(0, m_file); /* /// @todo fix this to use starting spots and player alliances when those are put in the game. for (Int i = 0; i < numPlayers; ++i) { Player *player = ThePlayerList->getNthPlayer(i); if (player == NULL) { continue; } UnicodeString name = player->getPlayerDisplayName(); fwprintf(m_file, L"%s", name.str()); fputwc(0, m_file); UnicodeString faction = player->getFaction()->getFactionDisplayName(); fwprintf(m_file, L"%s", faction.str()); fputwc(0, m_file); Int color = player->getColor()->getAsInt(); fwrite(&color, sizeof(color), 1, m_file); Int team = 0; Int startingSpot = 0; fwrite(&startingSpot, sizeof(Int), 1, m_file); fwrite(&team, sizeof(Int), 1, m_file); } */ // Write the game difficulty. fwrite(&diff, sizeof(Int), 1, m_file); // Write original game mode fwrite(&originalGameMode, sizeof(originalGameMode), 1, m_file); // Write rank points to add at game start fwrite(&rankPoints, sizeof(rankPoints), 1, m_file); // Write maxFPS chosen fwrite(&maxFPS, sizeof(maxFPS), 1, m_file); DEBUG_LOG(("RecorderClass::startRecording() - diff=%d, mode=%d, FPS=%d\n", diff, originalGameMode, maxFPS)); /* // Write the map name. fprintf(m_file, "%s", (TheGlobalData->m_mapName).str()); fputc(0, m_file); */ /// @todo Need to write game options when there are some to be written. } /** * This will stop the current recording session and close the file. This should always be called at the end of * every game. */ void RecorderClass::stopRecording() { logGameEnd(); if (TheNetwork) { //if (TheLAN) { if (m_wasDesync) cleanUpReplayFile(); m_wasDesync = FALSE; } } if (m_file != NULL) { fclose(m_file); m_file = NULL; } m_fileName.clear(); } /** * Write this game message to the record file. This also writes the game message's execution frame. */ void RecorderClass::writeToFile(GameMessage * msg) { // Write the frame number for this command. UnsignedInt frame = TheGameLogic->getFrame(); fwrite(&frame, sizeof(frame), 1, m_file); // Write the command type GameMessage::Type type = msg->getType(); fwrite(&type, sizeof(type), 1, m_file); // Write the player index Int playerIndex = msg->getPlayerIndex(); fwrite(&playerIndex, sizeof(playerIndex), 1, m_file); #ifdef DEBUG_LOGGING AsciiString commandName = msg->getCommandAsAsciiString(); if (type < GameMessage::MSG_BEGIN_NETWORK_MESSAGES || type > GameMessage::MSG_END_NETWORK_MESSAGES) { commandName.concat(" (Non-Network message!)"); } else if (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES) { AsciiString tmp; tmp.format(" (CRC 0x%8.8X)", msg->getArgument(0)->integer); commandName.concat(tmp); } //DEBUG_LOG(("RecorderClass::writeToFile - Adding %s command from player %d to TheCommandList on frame %d\n", //commandName.str(), msg->getPlayerIndex(), TheGameLogic->getFrame())); #endif // DEBUG_LOGGING GameMessageParser *parser = newInstance(GameMessageParser)(msg); UnsignedByte numTypes = parser->getNumTypes(); fwrite(&numTypes, sizeof(numTypes), 1, m_file); GameMessageParserArgumentType *argType = parser->getFirstArgumentType(); while (argType != NULL) { UnsignedByte type = (UnsignedByte)(argType->getType()); fwrite(&type, sizeof(type), 1, m_file); UnsignedByte argTypeCount = (UnsignedByte)(argType->getArgCount()); fwrite(&argTypeCount, sizeof(argTypeCount), 1, m_file); argType = argType->getNext(); } // UnsignedByte lasttype = (UnsignedByte)ARGUMENTDATATYPE_UNKNOWN; Int numArgs = msg->getArgumentCount(); for (Int i = 0; i < numArgs; ++i) { // UnsignedByte type = (UnsignedByte)(msg->getArgumentDataType(i)); // if (lasttype != type) { // fwrite(&type, sizeof(type), 1, m_file); // lasttype = type; // } writeArgument(msg->getArgumentDataType(i), *(msg->getArgument(i))); } parser->deleteInstance(); parser = NULL; fflush(m_file); ///< @todo should this be in the final release? } void RecorderClass::writeArgument(GameMessageArgumentDataType type, const GameMessageArgumentType arg) { if (type == ARGUMENTDATATYPE_INTEGER) { fwrite(&(arg.integer), sizeof(arg.integer), 1, m_file); } else if (type == ARGUMENTDATATYPE_REAL) { fwrite(&(arg.real), sizeof(arg.real), 1, m_file); } else if (type == ARGUMENTDATATYPE_BOOLEAN) { fwrite(&(arg.boolean), sizeof(arg.boolean), 1, m_file); } else if (type == ARGUMENTDATATYPE_OBJECTID) { fwrite(&(arg.objectID), sizeof(arg.objectID), 1, m_file); } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { fwrite(&(arg.drawableID), sizeof(arg.drawableID), 1, m_file); } else if (type == ARGUMENTDATATYPE_TEAMID) { fwrite(&(arg.teamID), sizeof(arg.teamID), 1, m_file); } else if (type == ARGUMENTDATATYPE_LOCATION) { fwrite(&(arg.location), sizeof(arg.location), 1, m_file); } else if (type == ARGUMENTDATATYPE_PIXEL) { fwrite(&(arg.pixel), sizeof(arg.pixel), 1, m_file); } else if (type == ARGUMENTDATATYPE_PIXELREGION) { fwrite(&(arg.pixelRegion), sizeof(arg.pixelRegion), 1, m_file); } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { fwrite(&(arg.timestamp), sizeof(arg.timestamp), 1, m_file); } else if (type == ARGUMENTDATATYPE_WIDECHAR) { fwrite(&(arg.wChar), sizeof(arg.wChar), 1, m_file); } } /** * Read in a replay header, for (1) populating a replay listbox or (2) starting playback. In * case (2), set FILE *m_file. */ Bool RecorderClass::readReplayHeader(ReplayHeader& header) { AsciiString filepath = getReplayDir(); filepath.concat(header.filename.str()); m_file = fopen(filepath.str(), "rb"); if (m_file == NULL) { DEBUG_LOG(("Can't open %s (%s)\n", filepath.str(), header.filename.str())); return FALSE; } // Read the GENREP header. char genrep[7]; fread(&genrep, sizeof(char), 6, m_file); genrep[6] = 0; if (strncmp(genrep, "GENREP", 6)) { DEBUG_LOG(("RecorderClass::readReplayHeader - replay file did not have GENREP at the start.\n")); fclose(m_file); m_file = NULL; return FALSE; } // read in some stats fread(&header.startTime, sizeof(time_t), 1, m_file); fread(&header.endTime, sizeof(time_t), 1, m_file); fread(&header.frameDuration, sizeof(UnsignedInt), 1, m_file); fread(&header.desyncGame, sizeof(Bool), 1, m_file); fread(&header.quitEarly, sizeof(Bool), 1, m_file); for (Int i=0; i= MAX_SLOTS) { DEBUG_LOG(("RecorderClass::readReplayHeader - invalid local slot number.\n")); m_gameInfo.endGame(); m_gameInfo.reset(); fclose(m_file); m_file = NULL; return FALSE; } if (header.localPlayerIndex >= 0) { Int localIP = m_gameInfo.getSlot(header.localPlayerIndex)->getIP(); m_gameInfo.setLocalIP(localIP); } if (!header.forPlayback) { m_gameInfo.endGame(); m_gameInfo.reset(); fclose(m_file); m_file = NULL; } return TRUE; } #if defined _DEBUG || defined _INTERNAL Bool RecorderClass::analyzeReplay( AsciiString filename ) { m_doingAnalysis = TRUE; return playbackFile(filename); } Bool RecorderClass::isAnalysisInProgress( void ) { return m_mode == RECORDERMODETYPE_PLAYBACK && m_nextFrame != -1; } #endif AsciiString RecorderClass::getCurrentReplayFilename( void ) { if (m_mode == RECORDERMODETYPE_PLAYBACK) { return m_currentReplayFilename; } return AsciiString::TheEmptyString; } class CRCInfo { public: CRCInfo(); void addCRC(UnsignedInt val); UnsignedInt readCRC(void); void setLocalPlayer(UnsignedInt index) { m_localPlayer = index; } UnsignedInt getLocalPlayer(void) { return m_localPlayer; } void setSawCRCMismatch(void) { m_sawCRCMismatch = TRUE; } Bool sawCRCMismatch(void) { return m_sawCRCMismatch; } protected: Bool m_sawCRCMismatch; Bool m_skippedOne; std::list m_data; UnsignedInt m_localPlayer; }; CRCInfo::CRCInfo() { m_localPlayer = ~0; m_skippedOne = FALSE; m_sawCRCMismatch = FALSE; } void CRCInfo::addCRC(UnsignedInt val) { //if (!m_skippedOne) //{ // m_skippedOne = TRUE; // return; //} m_data.push_back(val); //DEBUG_LOG(("CRCInfo::addCRC() - crc %8.8X pushes list to %d entries (full=%d)\n", val, m_data.size(), !m_data.empty())); } UnsignedInt CRCInfo::readCRC(void) { if (m_data.empty()) { //DEBUG_LOG(("CRCInfo::readCRC() - bailing, full=0, size=%d\n", m_data.size())); return 0; } UnsignedInt val = m_data.front(); m_data.pop_front(); //DEBUG_LOG(("CRCInfo::readCRC() - returning %8.8X, full=%d, size=%d\n", val, !m_data.empty(), m_data.size())); return val; } void RecorderClass::handleCRCMessage(UnsignedInt newCRC, Int playerIndex, Bool fromPlayback) { if (fromPlayback) { //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Adding CRC of %X from %d to m_crcInfo\n", newCRC, playerIndex)); m_crcInfo->addCRC(newCRC); return; } Int localPlayerIndex = m_crcInfo->getLocalPlayer(); Bool samePlayer = FALSE; AsciiString playerName; playerName.format("player%d", localPlayerIndex); const Player *p = ThePlayerList->getNthPlayer(playerIndex); if (!p || (p->getPlayerNameKey() == NAMEKEY(playerName))) samePlayer = TRUE; if (samePlayer || (localPlayerIndex < 0)) { UnsignedInt playbackCRC = m_crcInfo->readCRC(); //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Comparing CRCs of %8.8X/%8.8X from %d\n", newCRC, playbackCRC, playerIndex)); if (TheGameLogic->getFrame() > 0 && newCRC != playbackCRC && !m_crcInfo->sawCRCMismatch()) { m_crcInfo->setSawCRCMismatch(); // Since we don't seem to have any *visible* desyncs when replaying games, but get this warning // virtually every replay, the assumption is our CRC checking is faulty. Since we're at the // tail end of patch season, let's just disable the message, and hope the users believe the // problem is fixed. -MDC 3/20/2003 //TheInGameUI->message("GUI:CRCMismatch"); DEBUG_CRASH(("Replay has gone out of sync! All bets are off!\nOld:%8.8X New:%8.8X\nFrame:%d", playbackCRC, newCRC, TheGameLogic->getFrame())); } return; } //DEBUG_LOG(("RecorderClass::handleCRCMessage() - Skipping CRC of %8.8X from %d (our index is %d)\n", newCRC, playerIndex, localPlayerIndex)); } /** * Return true if this version of the file is the same as our version of the game */ Bool RecorderClass::testVersionPlayback(AsciiString filename) { ReplayHeader header; header.forPlayback = TRUE; header.filename = filename; Bool success = readReplayHeader( header ); if (!success) { return FALSE; } Bool versionStringDiff = header.versionString != TheVersion->getUnicodeVersion(); Bool versionTimeStringDiff = header.versionTimeString != TheVersion->getUnicodeBuildTime(); Bool versionNumberDiff = header.versionNumber != TheVersion->getVersionNumber(); Bool exeCRCDiff = header.exeCRC != TheGlobalData->m_exeCRC; Bool exeDifferent = versionStringDiff || versionTimeStringDiff || versionNumberDiff || exeCRCDiff; Bool iniDifferent = header.iniCRC != TheGlobalData->m_iniCRC; if(exeDifferent || iniDifferent) { return TRUE; } return FALSE; } /** * Start playback of the file. Return true or false depending on if the file is * a valid replay file or not. */ Bool RecorderClass::playbackFile(AsciiString filename) { if (!m_doingAnalysis) { if (TheGameLogic->isInGame()) { TheGameLogic->clearGameData(); } } m_mode = RECORDERMODETYPE_PLAYBACK; ReplayHeader header; header.forPlayback = TRUE; header.filename = filename; Bool success = readReplayHeader( header ); if (!success) { return FALSE; } #ifdef DEBUG_LOGGING Bool versionStringDiff = header.versionString != TheVersion->getUnicodeVersion(); Bool versionTimeStringDiff = header.versionTimeString != TheVersion->getUnicodeBuildTime(); Bool versionNumberDiff = header.versionNumber != TheVersion->getVersionNumber(); Bool exeCRCDiff = header.exeCRC != TheGlobalData->m_exeCRC; Bool exeDifferent = versionStringDiff || versionTimeStringDiff || versionNumberDiff || exeCRCDiff; Bool iniDifferent = header.iniCRC != TheGlobalData->m_iniCRC; AsciiString debugString; AsciiString tempStr; if (exeDifferent) { debugString = "EXE is different:\n"; if (versionStringDiff) { tempStr.format(" Version [%ls] vs [%ls]\n", TheVersion->getUnicodeVersion().str(), header.versionString.str()); debugString.concat(tempStr); } if (versionTimeStringDiff) { tempStr.format(" Build Time [%ls] vs [%ls]\n", TheVersion->getUnicodeBuildTime().str(), header.versionTimeString.str()); debugString.concat(tempStr); } if (versionNumberDiff) { tempStr.format(" Version Number %8.8X vs %8.8X\n", TheVersion->getVersionNumber(), header.versionNumber); debugString.concat(tempStr); } if (exeCRCDiff) { tempStr.format(" CRC %8.8X vs %8.8X\n", TheGlobalData->m_exeCRC, header.exeCRC); debugString.concat(tempStr); } } if (iniDifferent) { debugString.concat("INIs are different:\n"); tempStr.format(" CRC %8.8X vs %8.8X\n", TheGlobalData->m_iniCRC, header.iniCRC); debugString.concat(tempStr); } DEBUG_ASSERTCRASH(!exeDifferent && !iniDifferent, (debugString.str())); #endif TheWritableGlobalData->m_pendingFile = m_gameInfo.getMap(); #ifdef DEBUG_LOGGING if (header.localPlayerIndex >= 0) { DEBUG_LOG(("Local player is %ls (slot %d, IP %8.8X)\n", m_gameInfo.getSlot(header.localPlayerIndex)->getName().str(), header.localPlayerIndex, m_gameInfo.getSlot(header.localPlayerIndex)->getIP())); } #endif m_crcInfo = NEW CRCInfo; m_crcInfo->setLocalPlayer(header.localPlayerIndex); REPLAY_CRC_INTERVAL = m_gameInfo.getCRCInterval(); DEBUG_LOG(("Player index is %d, replay CRC interval is %d\n", m_crcInfo->getLocalPlayer(), REPLAY_CRC_INTERVAL)); Int difficulty = 0; fread(&difficulty, sizeof(difficulty), 1, m_file); fread(&m_originalGameMode, sizeof(m_originalGameMode), 1, m_file); Int rankPoints = 0; fread(&rankPoints, sizeof(rankPoints), 1, m_file); Int maxFPS = 0; fread(&maxFPS, sizeof(maxFPS), 1, m_file); DEBUG_LOG(("RecorderClass::playbackFile() - original game was mode %d\n", m_originalGameMode)); readNextFrame(); // send a message to the logic for a new game if (!m_doingAnalysis) { GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_REPLAY); msg->appendIntegerArgument(difficulty); msg->appendIntegerArgument(rankPoints); if( maxFPS != 0 ) msg->appendIntegerArgument(maxFPS); //InitGameLogicRandom( m_gameInfo.getSeed()); InitRandom( m_gameInfo.getSeed() ); } m_currentReplayFilename = filename; return TRUE; } /** * Read a unicode string from the current file position. The string is assumed to be 0-terminated. */ UnicodeString RecorderClass::readUnicodeString() { UnsignedShort str[1024] = L""; Int index = 0; Int c = fgetwc(m_file); if (c == EOF) { str[index] = 0; } str[index] = c; while (index < 1024 && str[index] != 0) { ++index; Int c = fgetwc(m_file); if (c == EOF) { str[index] = 0; break; } str[index] = c; } str[1023] = L'\0'; UnicodeString retval(str); return retval; } /** * Read an ascii string from the current file position. The string is assumed to be 0-terminated. */ AsciiString RecorderClass::readAsciiString() { char str[1024] = ""; Int index = 0; Int c = fgetc(m_file); if (c == EOF) { str[index] = 0; } str[index] = c; while (index < 1024 && str[index] != 0) { ++index; Int c = fgetc(m_file); if (c == EOF) { str[index] = 0; break; } str[index] = c; } str[1023] = '\0'; AsciiString retval(str); return retval; } /** * Read the frame number for the next command in the playback file. If the end of the file is reached, the playback * is stopped and the next frame is said to be -1. */ void RecorderClass::readNextFrame() { Int retcode = fread(&m_nextFrame, sizeof(m_nextFrame), 1, m_file); if (retcode != 1) { DEBUG_LOG(("RecorderClass::readNextFrame - fread failed on frame %d\n", TheGameLogic->getFrame())); m_nextFrame = -1; stopPlayback(); } } /** * This reads the next command from the replay file and appends it to TheCommandList. */ void RecorderClass::appendNextCommand() { GameMessage::Type type; Int retcode = fread(&type, sizeof(type), 1, m_file); if (retcode != 1) { DEBUG_LOG(("RecorderClass::appendNextCommand - fread failed on frame %d\n", m_nextFrame/*TheGameLogic->getFrame()*/)); return; } GameMessage *msg = newInstance(GameMessage)(type); if (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES || type == GameMessage::MSG_CLEAR_GAME_DATA) { } else { if (!m_doingAnalysis) { TheCommandList->appendMessage(msg); } } #ifdef DEBUG_LOGGING AsciiString commandName = msg->getCommandAsAsciiString(); if (type < GameMessage::MSG_BEGIN_NETWORK_MESSAGES || type > GameMessage::MSG_END_NETWORK_MESSAGES) { commandName.concat(" (Non-Network message!)"); } else if (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES) { commandName.concat(" (CRC message!)"); } #endif // DEBUG_LOGGING Int playerIndex = -1; fread(&playerIndex, sizeof(playerIndex), 1, m_file); msg->friend_setPlayerIndex(playerIndex); // don't debug log this if we're debugging sync errors, as it will cause diff problems between a game and it's replay... #ifdef DEBUG_LOGGING Bool logCommand = true; #ifdef DEBUG_CRC if (!m_doingAnalysis) logCommand = false; #endif if (logCommand) { DEBUG_LOG(("RecorderClass::appendNextCommand - Adding %s command from player %d to TheCommandList on frame %d\n", commandName.str(), (type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES)?0:msg->getPlayerIndex(), m_nextFrame/*TheGameLogic->getFrame()*/)); } #endif UnsignedByte numTypes = 0; Int totalArgs = 0; fread(&numTypes, sizeof(numTypes), 1, m_file); GameMessageParser *parser = newInstance(GameMessageParser)(); for (UnsignedByte i = 0; i < numTypes; ++i) { UnsignedByte type = (UnsignedByte)ARGUMENTDATATYPE_UNKNOWN; fread(&type, sizeof(type), 1, m_file); UnsignedByte numArgs = 0; fread(&numArgs, sizeof(numArgs), 1, m_file); parser->addArgType((GameMessageArgumentDataType)type, numArgs); totalArgs += numArgs; } GameMessageParserArgumentType *parserArgType = parser->getFirstArgumentType(); GameMessageArgumentDataType lasttype = ARGUMENTDATATYPE_UNKNOWN; Int argsLeftForType = 0; if (parserArgType != NULL) { lasttype = parserArgType->getType(); argsLeftForType = parserArgType->getArgCount(); } for (Int j = 0; j < totalArgs; ++j) { readArgument(lasttype, msg); --argsLeftForType; if (argsLeftForType == 0) { DEBUG_ASSERTCRASH(parserArgType != NULL, ("parserArgType was NULL when it shouldn't have been.")); if (parserArgType == NULL) { return; } parserArgType = parserArgType->getNext(); // parserArgType is allowed to be NULL here, this is the case if there are no more arguments. if (parserArgType != NULL) { argsLeftForType = parserArgType->getArgCount(); lasttype = parserArgType->getType(); } } } if (type == GameMessage::MSG_CLEAR_GAME_DATA || type == GameMessage::MSG_BEGIN_NETWORK_MESSAGES) { msg->deleteInstance(); msg = NULL; } if (m_doingAnalysis) { msg->deleteInstance(); msg = NULL; } parser->deleteInstance(); parser = NULL; } void RecorderClass::readArgument(GameMessageArgumentDataType type, GameMessage *msg) { if (type == ARGUMENTDATATYPE_INTEGER) { Int theint; fread(&theint, sizeof(theint), 1, m_file); msg->appendIntegerArgument(theint); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Integer argument: %d (%8.8X)\n", theint, theint)); } #endif } else if (type == ARGUMENTDATATYPE_REAL) { Real thereal; fread(&thereal, sizeof(thereal), 1, m_file); msg->appendRealArgument(thereal); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Real argument: %g (%8.8X)\n", thereal, *(int *)&thereal)); } #endif } else if (type == ARGUMENTDATATYPE_BOOLEAN) { Bool thebool; fread(&thebool, sizeof(thebool), 1, m_file); msg->appendBooleanArgument(thebool); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Bool argument: %d\n", thebool)); } #endif } else if (type == ARGUMENTDATATYPE_OBJECTID) { ObjectID theid; fread(&theid, sizeof(theid), 1, m_file); msg->appendObjectIDArgument(theid); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Object ID argument: %d\n", theid)); } #endif } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { DrawableID theid; fread(&theid, sizeof(theid), 1, m_file); msg->appendDrawableIDArgument(theid); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Drawable ID argument: %d\n", theid)); } #endif } else if (type == ARGUMENTDATATYPE_TEAMID) { UnsignedInt theid; fread(&theid, sizeof(theid), 1, m_file); msg->appendTeamIDArgument(theid); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Team ID argument: %d\n", theid)); } #endif } else if (type == ARGUMENTDATATYPE_LOCATION) { Coord3D loc; fread(&loc, sizeof(loc), 1, m_file); msg->appendLocationArgument(loc); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Coord3D argument: %g %g %g (%8.8X %8.8X %8.8X)\n", loc.x, loc.y, loc.z, *(int *)&loc.x, *(int *)&loc.y, *(int *)&loc.z)); } #endif } else if (type == ARGUMENTDATATYPE_PIXEL) { ICoord2D pixel; fread(&pixel, sizeof(pixel), 1, m_file); msg->appendPixelArgument(pixel); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Pixel argument: %d,%d\n", pixel.x, pixel.y)); } #endif } else if (type == ARGUMENTDATATYPE_PIXELREGION) { IRegion2D reg; fread(®, sizeof(reg), 1, m_file); msg->appendPixelRegionArgument(reg); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Pixel Region argument: %d,%d -> %d,%d\n", reg.lo.x, reg.lo.y, reg.hi.x, reg.hi.y)); } #endif } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { // Not to be confused with Terrance Stamp... Kneel before Zod!!! UnsignedInt stamp; fread(&stamp, sizeof(stamp), 1, m_file); msg->appendTimestampArgument(stamp); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("Timestamp argument: %d\n", stamp)); } #endif } else if (type == ARGUMENTDATATYPE_WIDECHAR) { WideChar theid; fread(&theid, sizeof(theid), 1, m_file); msg->appendWideCharArgument(theid); #ifdef DEBUG_LOGGING if (m_doingAnalysis) { DEBUG_LOG(("WideChar argument: %d (%lc)\n", theid, theid)); } #endif } } /** * This needs to be called for every frame during playback. Basically it prevents the user from inserting. */ void RecorderClass::cullBadCommands() { if (m_doingAnalysis) return; GameMessage *msg = TheCommandList->getFirstMessage(); GameMessage *next = NULL; while (msg != NULL) { next = msg->next(); if ((msg->getType() > GameMessage::MSG_BEGIN_NETWORK_MESSAGES) && (msg->getType() < GameMessage::MSG_END_NETWORK_MESSAGES) && (msg->getType() != GameMessage::MSG_LOGIC_CRC)) { msg->deleteInstance(); } msg = next; } } /** * returns the directory that holds the replay files. */ AsciiString RecorderClass::getReplayDir() { const char* replayDir = "Replays\\"; AsciiString tmp = TheGlobalData->getPath_UserData(); tmp.concat(replayDir); return tmp; } /** * returns the file extention for the replay files. */ AsciiString RecorderClass::getReplayExtention() { return AsciiString(replayExtention); } /** * returns the file name used for the replay file that is recorded to. */ AsciiString RecorderClass::getLastReplayFileName() { #if defined(_DEBUG) || defined(_INTERNAL) if (TheNetwork && TheGlobalData->m_saveStats) { GameInfo *game = NULL; if (TheLAN) game = TheLAN->GetMyGame(); else if (TheGameSpyInfo) game = TheGameSpyGame; if (game) { AsciiString players; AsciiString full; AsciiString fullPlusNum; AsciiString mapName = game->getMap(); const char *fname = mapName.reverseFind('\\'); if (fname) mapName = fname+1; for (Int i=0; igetSlot(i); if (slot && slot->isHuman()) { AsciiString player; player.format("%ls_", slot->getName().str()); players.concat(player); } } full.format("%s%s_%d_%d", players.str(), mapName.str(), game->getSeed(), game->getLocalSlotNum()); AsciiString testString; testString.format("%s%s%s", getReplayDir().str(), full.str(), replayExtention); FILE *fp; fp = fopen(testString.str(), "rb"); if (fp) { fclose(fp); } else { return full; } Int test = 1; while (test < 20) { fullPlusNum.format("%s_%d", full.str(), test); testString.format("%s%s%s", getReplayDir().str(), fullPlusNum.str(), replayExtention); fp = fopen(testString.str(), "rb"); if (fp) { fclose(fp); ++test; } else { return fullPlusNum; } } return fullPlusNum; } } #endif return AsciiString(lastReplayFileName); } /** * return the current operating mode of TheRecorder. */ RecorderModeType RecorderClass::getMode() { return m_mode; } ///< Show or Hide the Replay controls void RecorderClass::initControls() { NameKeyType parentReplayControlID = TheNameKeyGenerator->nameToKey( AsciiString("ReplayControl.wnd:ParentReplayControl") ); GameWindow *parentReplayControl = TheWindowManager->winGetWindowFromId( NULL, parentReplayControlID ); Bool show = (getMode() != RECORDERMODETYPE_PLAYBACK); if (parentReplayControl) { parentReplayControl->winHide(show); // show the replay control window. } } ///< is this a multiplayer game (record OR playback)? Bool RecorderClass::isMultiplayer( void ) { if (m_mode == RECORDERMODETYPE_PLAYBACK) { GameSlot *slot; for (int i=0; iisOccupied()) ///< slots default to closed for non-networked games return true; } } if (TheGameLogic->getGameMode()==GAME_SINGLE_PLAYER) { return false; // single player isn't multiplayer. } if (TheGameLogic->getGameMode()==GAME_SHELL) { return false; // shell isn't multiplayer. } if (TheNetwork || TheSkirmishGameInfo) return true; return false; } /** * Create a new recorder object. */ RecorderClass * createRecorder() { return NEW RecorderClass; }