/*
** 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. //
// //
////////////////////////////////////////////////////////////////////////////////
// FILE: GameState.cpp ////////////////////////////////////////////////////////////////////////////
// Author: Colin Day, September 2002
// Desc: Game state singleton from which to load and save the game state
///////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES ///////////////////////////////////////////////////////////////////////////////////////
#include "PreRTS.h"
#include "Common/File.h"
#include "Common/FileSystem.h"
#include "Common/GameEngine.h"
#include "Common/GameState.h"
#include "Common/GameStateMap.h"
#include "Common/LatchRestore.h"
#include "Common/MapObject.h"
#include "Common/PlayerList.h"
#include "Common/RandomValue.h"
#include "Common/Radar.h"
#include "Common/Team.h"
#include "Common/WellKnownKeys.h"
#include "Common/XferLoad.h"
#include "Common/XferSave.h"
#include "GameClient/CampaignManager.h"
#include "GameClient/GadgetListBox.h"
#include "GameClient/GameClient.h"
#include "GameClient/GameText.h"
#include "GameClient/MapUtil.h"
#include "GameClient/MessageBox.h"
#include "GameClient/InGameUI.h"
#include "GameClient/ParticleSys.h"
#include "GameClient/TerrainVisual.h"
#include "GameLogic/GameLogic.h"
#include "GameLogic/GhostObject.h"
#include "GameLogic/PartitionManager.h"
#include "GameLogic/ScriptEngine.h"
#include "GameLogic/SidesList.h"
#include "GameLogic/TerrainLogic.h"
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
// PUBLIC DATA ////////////////////////////////////////////////////////////////////////////////////
GameState *TheGameState = NULL;
// PRIVATE DATA ///////////////////////////////////////////////////////////////////////////////////
static const Char *SAVE_FILE_EOF = "SG_EOF";
static const Char *SAVE_GAME_EXTENSION = ".sav";
static const Char *ZERO_NAME_ONLY = "00000000";
static const Int MAX_SAVE_FILE_NUMBER = 99999999;
///////////////////////////////////////////////////////////////////////////////////////////////////
#define GAME_STATE_BLOCK_STRING "CHUNK_GameState" // block of save game data with game info data
#define CAMPAIGN_BLOCK_STRING "CHUNK_Campaign" // block of game data that has campaign info
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
SaveGameInfo::SaveGameInfo( void )
{
date.day = 0;
date.dayOfWeek = 0;
date.hour = 0;
date.milliseconds = 0;
date.minute = 0;
date.month = 0;
date.second = 0;
date.year = 0;
missionNumber = 0;
saveFileType = SAVE_FILE_TYPE_NORMAL;
} // end SaveGameInfo
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
SaveGameInfo::~SaveGameInfo( void )
{
} // end ~SaveGameInfo
// ------------------------------------------------------------------------------------------------
/** Is this date newer than the other one passed in? */
// ------------------------------------------------------------------------------------------------
Bool SaveDate::isNewerThan( SaveDate *other )
{
// year
if( year > other->year )
return TRUE;
else if( year < other->year )
return FALSE;
else
{
// month
if( month > other->month )
return TRUE;
else if( month < other->month )
return FALSE;
else
{
// day
if( day > other->day )
return TRUE;
else if( day < other->day )
return FALSE;
else
{
// hour
if( hour > other->hour )
return TRUE;
else if( hour < other->hour )
return FALSE;
else
{
// minute
if( minute > other->minute )
return TRUE;
else if( minute < other->minute )
return FALSE;
else
{
// second
if( second > other->second )
return TRUE;
else if( second < other->second )
return FALSE;
else
{
// millisecond
if( milliseconds > other->milliseconds )
return TRUE;
else
return FALSE;
} // end else
} // end else
} // end else
} // end else
} // end else
} // end else
} // end isNewerThan
// ------------------------------------------------------------------------------------------------
/** Find a snapshot block info that matches the token passed in */
// ------------------------------------------------------------------------------------------------
GameState::SnapshotBlock *GameState::findBlockInfoByToken( AsciiString token, SnapshotType which )
{
// sanity
if( token.isEmpty() )
return NULL;
// search for match our list
SnapshotBlock *blockInfo;
SnapshotBlockListIterator it;
for( it = m_snapshotBlockList[which].begin(); it != m_snapshotBlockList[which].end(); ++it )
{
// get info
blockInfo = &(*it);
// check for match
if( blockInfo->blockName == token )
return blockInfo;
} // end for
// not found
return NULL;
} // end findLexiconEntryByToken
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
UnicodeString getUnicodeDateBuffer(SYSTEMTIME timeVal)
{
// setup date buffer for local region date format
#define DATE_BUFFER_SIZE 256
OSVERSIONINFO osvi;
osvi.dwOSVersionInfoSize=sizeof(OSVERSIONINFO);
UnicodeString displayDateBuffer;
if (GetVersionEx(&osvi))
{ //check if we're running Win9x variant since they may need different characters
if (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
{
char dateBuffer[ DATE_BUFFER_SIZE ];
GetDateFormat( LOCALE_SYSTEM_DEFAULT,
DATE_SHORTDATE,
&timeVal,
NULL,
dateBuffer, sizeof(dateBuffer) );
displayDateBuffer.translate(dateBuffer);
return displayDateBuffer;
}
}
wchar_t dateBuffer[ DATE_BUFFER_SIZE ];
GetDateFormatW( LOCALE_SYSTEM_DEFAULT,
DATE_SHORTDATE,
&timeVal,
NULL,
dateBuffer, sizeof(dateBuffer) );
displayDateBuffer.set(dateBuffer);
return displayDateBuffer;
//displayDateBuffer.format( L"%ls", dateBuffer );
}
UnicodeString getUnicodeTimeBuffer(SYSTEMTIME timeVal)
{
// setup time buffer for local region time format
UnicodeString displayTimeBuffer;
OSVERSIONINFO osvi;
osvi.dwOSVersionInfoSize=sizeof(OSVERSIONINFO);
if (GetVersionEx(&osvi))
{ //check if we're running Win9x variant since they may need different characters
if (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
{
char timeBuffer[ DATE_BUFFER_SIZE ];
GetTimeFormat( LOCALE_SYSTEM_DEFAULT,
TIME_NOSECONDS|TIME_FORCE24HOURFORMAT|TIME_NOTIMEMARKER,
&timeVal,
NULL,
timeBuffer, sizeof(timeBuffer) );
displayTimeBuffer.translate(timeBuffer);
return displayTimeBuffer;
}
}
// setup time buffer for local region time format
#define TIME_BUFFER_SIZE 256
wchar_t timeBuffer[ TIME_BUFFER_SIZE ];
GetTimeFormatW( LOCALE_SYSTEM_DEFAULT,
TIME_NOSECONDS,
&timeVal,
NULL,
timeBuffer,
sizeof(timeBuffer) );
displayTimeBuffer.set(timeBuffer);
return displayTimeBuffer;
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
GameState::GameState( void )
{
m_availableGames = NULL;
m_isInLoadGame = FALSE;
} // end GameState
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
GameState::~GameState( void )
{
// clear our snapshot block list
for (Int i=0; inext;
delete m_availableGames;
m_availableGames = gameInfo;
} // end while
} // end clearAvailableGames
// ------------------------------------------------------------------------------------------------
/** Add a snapshot and block name pair to the systems used to load and save */
// ------------------------------------------------------------------------------------------------
void GameState::addSnapshotBlock( AsciiString blockName, Snapshot *snapshot, SnapshotType which )
{
// sanity
if( blockName.isEmpty() || snapshot == NULL )
{
DEBUG_CRASH(( "addSnapshotBlock: Invalid parameters\n" ));
return;
} // end if
// add to the list
SnapshotBlock blockInfo;
blockInfo.snapshot = snapshot;
blockInfo.blockName = blockName;
m_snapshotBlockList[which].push_back( blockInfo );
} // end addSnapshotBlock
// ------------------------------------------------------------------------------------------------
/** Given the filename of a save file, find the highest filename number */
// ------------------------------------------------------------------------------------------------
static void findHighFileNumber( AsciiString filename, void *userData )
{
// sanity
if( filename.isEmpty() )
return;
// sanity check for ".sav" at the end of the filename
if( filename.endsWithNoCase( SAVE_GAME_EXTENSION ) == FALSE )
return;
// strip off the extension at the end of the filename
AsciiString nameOnly = filename;
for( Int count = 0; count < strlen( SAVE_GAME_EXTENSION ); count++ )
nameOnly.removeLastChar();
// convert filename (which is only numbers) to a number
Int fileNumber = atoi( nameOnly.str() );
//
// atoi will return zero if the string could not be converted, if the filename is
// not literally "00000000.sav" then that means the conversion could not be done so
// we should reject this filename from further processing
//
if( fileNumber == 0 && nameOnly.compare( ZERO_NAME_ONLY ) != 0 )
return;
// compare against the file number we're keeping tally of in the user data parameter
Int *highFileNumber = (Int *)userData;
if( fileNumber >= *highFileNumber )
*highFileNumber = fileNumber;
} // end findHighFileNumber
// ------------------------------------------------------------------------------------------------
/** Given the save files on disk, find the "next" filename to use when saving a game */
// ------------------------------------------------------------------------------------------------
AsciiString GameState::findNextSaveFilename( UnicodeString desc )
{
// works, but needs approval from mgmt (srj)
// GS activating for patch
#define COCKBEER_DONT_USE_NORMAL_FILE_NAMES_THANKS
#ifdef USE_NORMAL_FILE_NAMES_THANKS
Int i;
AsciiString adesc;
for (i = 0; i < desc.getLength(); ++i)
{
char c = (char)desc.getCharAt(i);
if (isalnum(c))
adesc.concat(c);
else
adesc.concat('_');
}
for (i = 1; i <= 9999; ++i)
{
AsciiString leaf;
leaf.format("%s_%04d%s", adesc.str(), i, SAVE_GAME_EXTENSION);
AsciiString path = getFilePathInSaveDirectory(leaf);
if( _access( path.str(), 0 ) == -1 )
return leaf; // note that this returns the leaf, not the full path
}
#else
//
// This method has code to support two modes of finding the next filename, one of
// them starts with a filename of 00000000.sav and counts up the number looking for
// a file that doesn't exist and therefore a filename we can use (LOWEST_NUMBER).
// The other method iterates all save files and returns a filename that is one larger
// than the largest filename number encountered (HIGHEST_NUMBER)
//
enum FindNextFileType { LOWEST_NUMBER = 0, HIGHEST_NUMBER = 1 };
FindNextFileType searchType = LOWEST_NUMBER;
if( searchType == HIGHEST_NUMBER )
{
// iterate all the save files in the directory and find the highest file number
Int highFileNumber = -1;
iterateSaveFiles( findHighFileNumber, &highFileNumber );
// check for a filename that is too big (this is unlikely but theoretically possible)
if( highFileNumber + 1 > MAX_SAVE_FILE_NUMBER )
return AsciiString::TheEmptyString;
// construct filename with a number higher than the highest one we found
AsciiString filename;
filename.format( "%08d%s", highFileNumber + 1, SAVE_GAME_EXTENSION );
return filename;
} // end if
else if( searchType == LOWEST_NUMBER )
{
AsciiString filename;
AsciiString fullPath;
Int i = 0;
while( TRUE )
{
// construct filename (########.sav)
filename.format( "%08d%s", i, SAVE_GAME_EXTENSION );
// construct full path to file given the filename
fullPath = getFilePathInSaveDirectory(filename);
// if file does not exist we're all good
if( _access( fullPath.str(), 0 ) == -1 )
return filename;
// test the text filename
i++;
// check for at the max limit (this is highly unlikely but possible)
if( i > MAX_SAVE_FILE_NUMBER )
return AsciiString::TheEmptyString;
} // end while
} // end else if
else
{
DEBUG_CRASH(( "GameState::findNextSaveFilename - Unknown file search type '%d'\n", searchType ));
return AsciiString::TheEmptyString;
} // end else
#endif
// no appropriate filename could be found, return the empty string
return AsciiString::TheEmptyString;
} // end findNextSaveFilename
// ------------------------------------------------------------------------------------------------
/** Save the current state of the engine in a save file
* NOTE: filename is a *filename only* */
// ------------------------------------------------------------------------------------------------
SaveCode GameState::saveGame( AsciiString filename, UnicodeString desc,
SaveFileType saveType, SnapshotType which )
{
// if there is no filename, this is a new file being created, find an appropriate filename
if( filename.isEmpty() )
filename = findNextSaveFilename( desc );
if( filename.isEmpty() )
{
DEBUG_CRASH(( "GameState::saveGame - Unable to find valid filename for save game\n" ));
return SC_NO_FILE_AVAILABLE;
} // end if
// make absolutely sure the save directory exists
CreateDirectory( getSaveDirectory().str(), NULL );
// construct path to file
AsciiString filepath = getFilePathInSaveDirectory(filename);
// save description as current description in the game state
m_gameInfo.description = desc;
// open the save file
XferSave xferSave;
try {
xferSave.open( filepath );
} catch(...) {
// print error message to the user
TheInGameUI->message( "GUI:Error" );
DEBUG_LOG(( "Error opening file '%s'\n", filepath.str() ));
return SC_ERROR;
}
// save our save file type
SaveGameInfo *gameInfo = getSaveGameInfo();
gameInfo->saveFileType = saveType;
// save our mission map name if applicable
if( saveType == SAVE_FILE_TYPE_MISSION )
gameInfo->missionMapName = TheCampaignManager->getCurrentMap();
else
gameInfo->missionMapName.clear();
// set the pristine map to the current campaign map
// this is now done during startNewGame()
// gameInfo->pristineMapName = TheCampaignManager->getCurrentMap();
// write the save file
try
{
// save file
xferSaveData( &xferSave, which );
} // end try
catch( ... )
{
UnicodeString ufilepath;
ufilepath.translate(filepath);
UnicodeString msg;
msg.format( TheGameText->fetch("GUI:ErrorSavingGame"), ufilepath.str() );
MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, NULL);
// close the file and get out of here
xferSave.close();
return SC_ERROR;
} // end catch
// close the file
xferSave.close();
// print message to the user for game successfully saved
UnicodeString msg = TheGameText->fetch( "GUI:GameSaveComplete" );
TheInGameUI->message( msg );
return SC_OK;
} // end saveGame
// ------------------------------------------------------------------------------------------------
/** A mission save */
// ------------------------------------------------------------------------------------------------
SaveCode GameState::missionSave( void )
{
// get campaign
Campaign *campaign = TheCampaignManager->getCurrentCampaign();
// get mission #
Int missionNumber = TheCampaignManager->getCurrentMissionNumber() + 1;
// format a string for the mission save description
UnicodeString format = TheGameText->fetch( "GUI:MissionSave" );
UnicodeString desc;
desc.format( format, TheGameText->fetch( campaign->m_campaignNameLabel ).str(), missionNumber );
// do an automatic mission save
return TheGameState->saveGame( AsciiString(""), desc, SAVE_FILE_TYPE_MISSION );
} // end missionSave
// ------------------------------------------------------------------------------------------------
/** Load the save game pointed to by filename */
// ------------------------------------------------------------------------------------------------
SaveCode GameState::loadGame( AvailableGameInfo gameInfo )
{
// sanity check for file
if( doesSaveGameExist( gameInfo.filename ) == FALSE )
return SC_FILE_NOT_FOUND;
// clear game data just like loading from the debug map load screen for mission saves
if( gameInfo.saveGameInfo.saveFileType == SAVE_FILE_TYPE_MISSION )
{
if (TheGameLogic->isInGame())
TheGameLogic->clearGameData( FALSE );
} // end if
//
// clear the save directory of any temporary "scratch pad" maps that were extracted
// from any previously loaded save game files
//
TheGameStateMap->clearScratchPadMaps();
// construct path to file
AsciiString filepath = getFilePathInSaveDirectory(gameInfo.filename);
// open the save file
XferLoad xferLoad;
xferLoad.open( filepath );
// clear out the game engine
TheGameEngine->reset();
// lock creation of new ghost objects
TheGhostObjectManager->saveLockGhostObjects( TRUE );
LatchRestore inLoadGame(m_isInLoadGame, TRUE);
// load the save data
Bool error = FALSE;
try
{
// load file
xferSaveData( &xferLoad, SNAPSHOT_SAVELOAD );
} // end try
catch( ... )
{
error = TRUE;
} // end catch
// close the file
xferLoad.close();
// un-savelock the ghost objects
TheGhostObjectManager->saveLockGhostObjects( FALSE );
try
{
// do the post-process from a save game load
gameStatePostProcessLoad();
}
catch (...)
{
error = TRUE;
}
// check for error
if( error == TRUE )
{
// clear it out, again
if (TheGameLogic->isInGame())
TheGameLogic->clearGameData( FALSE );
TheGameEngine->reset();
// print error message to the user
UnicodeString ufilepath;
ufilepath.translate(filepath);
UnicodeString msg;
msg.format( TheGameText->fetch("GUI:ErrorLoadingGame"), ufilepath.str() );
MessageBoxOk(TheGameText->fetch("GUI:Error"), msg, NULL);
return SC_INVALID_DATA; // you can't use a naked "throw" outside of a catch statement!
} // end if
//
// when loading a mission save, we want to do as much normal loading stuff as we
// can cause we don't have any real save game data to load other than the
// game state map info and campaign manager stuff
//
if( getSaveGameInfo()->saveFileType == SAVE_FILE_TYPE_MISSION )
{
InitRandom(0);
TheWritableGlobalData->m_pendingFile = getSaveGameInfo()->missionMapName;
GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME );
msg->appendIntegerArgument(GAME_SINGLE_PLAYER);
msg->appendIntegerArgument(TheCampaignManager->getGameDifficulty());
msg->appendIntegerArgument(TheCampaignManager->getRankPoints());
// remove the mission save data, we've got all we need and have started the load
SaveGameInfo *gameInfo = getSaveGameInfo();
gameInfo->saveFileType = SAVE_FILE_TYPE_NORMAL;
gameInfo->missionMapName.clear();
} // end if
return SC_OK;
} // end loadGame
//-------------------------------------------------------------------------------------------------
AsciiString GameState::getSaveDirectory() const
{
AsciiString tmp = TheGlobalData->getPath_UserData();
tmp.concat("Save\\");
return tmp;
}
//-------------------------------------------------------------------------------------------------
AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const
{
AsciiString tmp = getSaveDirectory();
tmp.concat(leaf);
return tmp;
}
//-------------------------------------------------------------------------------------------------
Bool GameState::isInSaveDirectory(const AsciiString& path) const
{
return path.startsWithNoCase(getSaveDirectory());
}
// ------------------------------------------------------------------------------------------------
AsciiString GameState::getMapLeafName(const AsciiString& in) const
{
char* p = strrchr(in.str(), '\\');
if (p)
{
//
// p points to the last '\' (if found), however, if a '\' was found there better
// be another character beyond it, otherwise the map filename would actually
// be a *directory* Just move to the first character beyond it so we are looking
// at the name only
//
++p;
DEBUG_ASSERTCRASH( p != NULL && *p != 0, ("GameState::xfer - Illegal map name encountered\n") );
return p;
}
else
{
return in;
}
}
// ------------------------------------------------------------------------------------------------
static const char* findLastBackslashInRangeInclusive(const char* start, const char* end)
{
while (end >= start)
{
if (*end == '\\')
return end;
--end;
}
return NULL;
}
// ------------------------------------------------------------------------------------------------
static AsciiString getMapLeafAndDirName(const AsciiString& in)
{
const char* start = in.str();
const char* end = in.str() + in.getLength() - 1;
const char* p = findLastBackslashInRangeInclusive(start, end);
if (p)
{
const char* p2 = findLastBackslashInRangeInclusive(start, p-1);
if (p2)
{
// we have something like:
// maps\foo\foo.map
// c:\mydocs\c&cdata\maps\foo\foo.map
return p2 + 1;
}
else
{
// we have something like:
// save\foo.map
return in;
}
}
else
{
DEBUG_CRASH(("Illegal map-dir-name... should have at least one backslash"));
return in;
}
}
// ------------------------------------------------------------------------------------------------
static AsciiString removeExtension(const AsciiString& in)
{
char buf[1024];
strcpy(buf, in.str());
char* p = strrchr(buf, '.');
if (p)
{
*p = 0;
}
return AsciiString(buf);
}
// ------------------------------------------------------------------------------------------------
const char* PORTABLE_SAVE = "Save\\";
const char* PORTABLE_MAPS = "Maps\\";
const char* PORTABLE_USER_MAPS = "UserData\\Maps\\";
// ------------------------------------------------------------------------------------------------
AsciiString GameState::realMapPathToPortableMapPath(const AsciiString& in) const
{
AsciiString prefix;
if (in.startsWithNoCase(getSaveDirectory()))
{
prefix = PORTABLE_SAVE;
prefix.concat(getMapLeafName(in));
}
else if (in.startsWithNoCase(TheMapCache->getMapDir()))
{
prefix = PORTABLE_MAPS;
prefix.concat(getMapLeafAndDirName(in));
}
else if (in.startsWithNoCase(TheMapCache->getUserMapDir()))
{
prefix = PORTABLE_USER_MAPS;
prefix.concat(getMapLeafAndDirName(in));
}
else
{
DEBUG_CRASH(("Map file was not found in any of the expected directories; this is impossible"));
//throw INI_INVALID_DATA;
// uncaught exceptions crash us. better to just use a bad path.
prefix = in;
}
prefix.toLower();
return prefix;
}
// ------------------------------------------------------------------------------------------------
AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const
{
AsciiString prefix;
if (in.startsWithNoCase(PORTABLE_SAVE))
{
// the save dir ends with "\\"
prefix = getSaveDirectory();
prefix.concat(getMapLeafName(in));
}
else if (in.startsWithNoCase(PORTABLE_MAPS))
{
// the map dir DOES NOT end with "\\", must add it
prefix = TheMapCache->getMapDir();
prefix.concat("\\");
prefix.concat(getMapLeafAndDirName(in));
}
else if (in.startsWithNoCase(PORTABLE_USER_MAPS))
{
// the map dir DOES NOT end with "\\", must add it
prefix = TheMapCache->getUserMapDir();
prefix.concat("\\");
prefix.concat(getMapLeafAndDirName(in));
}
else
{
DEBUG_CRASH(("Map file was not found in any of the expected directories; this is impossible"));
//throw INI_INVALID_DATA;
// uncaught exceptions crash us. better to just use a bad path.
prefix = in;
}
prefix.toLower();
return prefix;
}
// ------------------------------------------------------------------------------------------------
/** Does the save game file exist */
// ------------------------------------------------------------------------------------------------
Bool GameState::doesSaveGameExist( AsciiString filename )
{
// construct full path to file
AsciiString filepath = getFilePathInSaveDirectory(filename);
// open file
XferLoad xfer;
try
{
// try to open it
xfer.open( filepath );
} // end try
catch( ... )
{
// unable to open file, it must not be here
return FALSE;
} // end catch
// close the file, we don't want to to anything with it right now
xfer.close();
return TRUE;
} // doesSaveGameExist
// ------------------------------------------------------------------------------------------------
/** Get save game info from the filename specified */
// ------------------------------------------------------------------------------------------------
void GameState::getSaveGameInfoFromFile( AsciiString filename, SaveGameInfo *saveGameInfo )
{
AsciiString token;
Int blockSize;
Bool done = FALSE;
SnapshotBlock *blockInfo;
// sanity
if( filename.isEmpty() == TRUE || saveGameInfo == NULL )
{
DEBUG_CRASH(( "GameState::getSaveGameInfoFromFile - Illegal parameters\n" ));
return;
} // end if
// open file for partial loading
XferLoad xferLoad;
xferLoad.open( filename );
//
// disable post processing cause we're not really doing a load of game data that
// needs post processing and we don't want to keep track of any snapshots we loaded
//
xferLoad.setOptions( XO_NO_POST_PROCESSING );
// read all data blocks in the file
while( done == FALSE )
{
// read next token
xferLoad.xferAsciiString( &token );
// check for end of file token
if( token.compareNoCase( SAVE_FILE_EOF ) == 0 )
{
// we should never get here, if we did, we didn't find block of data we needed
DEBUG_CRASH(( "GameState::getSaveGameInfoFromFile - Game info not found in file '%s'\n", filename.str() ));
done = TRUE;
} // end if
else
{
// find matching token in the save file lexicon
blockInfo = findBlockInfoByToken( token, SNAPSHOT_SAVELOAD );
if( blockInfo == NULL )
throw SC_UNKNOWN_BLOCK;
// read the data size of this block
blockSize = xferLoad.beginBlock();
// is this the block of game info data
if( stricmp( token.str(), GAME_STATE_BLOCK_STRING ) == 0 )
{
GameState tempGameState;
// parse this data
try
{
// load data
xferLoad.xferSnapshot( &tempGameState );
} // end try
catch( ... )
{
DEBUG_CRASH(( "GameState::getSaveGameInfoFromFile - Error loading block '%s' in file '%s'\n",
blockInfo->blockName.str(), filename.str() ));
throw;
} // end catch
// data was found, copy game state info over
*saveGameInfo = *tempGameState.getSaveGameInfo();
// we're all done with this file now
done = TRUE;
} // end if
else
{
// not a block we care about, just skip it
xferLoad.skip( blockSize );
// end of block
xferLoad.endBlock();
} // end else
} // end else, valid data block token
} // end while, not done
// close the file
xferLoad.close();
} // end getSaveGameInfoFromFile
// ------------------------------------------------------------------------------------------------
/** Create game info and add to available list */
// ------------------------------------------------------------------------------------------------
static void addGameToAvailableList( AsciiString filename, void *userData )
{
AvailableGameInfo **listHead = (AvailableGameInfo **)userData;
// sanity
DEBUG_ASSERTCRASH( listHead != NULL, ("addGameToAvailableList - Illegal parameters\n") );
DEBUG_ASSERTCRASH( filename.isEmpty() == FALSE, ("addGameToAvailableList - Illegal filename\n") );
try {
// get header info from this listbox
SaveGameInfo saveGameInfo;
TheGameState->getSaveGameInfoFromFile( filename, &saveGameInfo );
// allocate new info
AvailableGameInfo *newInfo = new AvailableGameInfo;
// assign data
newInfo->prev = NULL;
newInfo->next = NULL;
newInfo->saveGameInfo = saveGameInfo;
newInfo->filename = filename;
// attach to list
if( *listHead == NULL )
*listHead = newInfo;
else
{
AvailableGameInfo *curr, *prev;
// insert this info so that the most recent games are always at the top of this list
for( curr = *listHead; curr != NULL; curr = curr->next )
{
// save current as previous
prev = curr;
// check to see if curr is older than the new info, if so, put new info just ahead of curr
if( newInfo->saveGameInfo.date.isNewerThan( &curr->saveGameInfo.date ) )
{
if( curr->prev )
curr->prev->next = newInfo;
else
*listHead = newInfo;
newInfo->prev = curr->prev;
curr->prev = newInfo;
newInfo->next = curr;
break;
} // end if
} // end for
// if not inserted, put at end
if( curr == NULL )
{
prev->next = newInfo;
newInfo->prev = prev;
} // end if
} // end else
} catch(...) {
// Do nothing - just return.
}
} // end addGameToAvailableList
// ------------------------------------------------------------------------------------------------
/** Populate the listbox passed in with a list of the save games present on the hard drive */
// ------------------------------------------------------------------------------------------------
void GameState::populateSaveGameListbox( GameWindow *listbox, SaveLoadLayoutType layoutType )
{
Int index;
// sanity
if( listbox == NULL )
return;
// first clear all entries in the listbox
GadgetListBoxReset( listbox );
// setup the first entry of the listbox to be a new game when saving is allowed
if( layoutType != SLLT_LOAD_ONLY )
{
UnicodeString newGameText = TheGameText->fetch( "GUI:NewSaveGame" );
Color newGameColor = GameMakeColor( 200, 200, 255, 255 );
index = GadgetListBoxAddEntryText( listbox, newGameText, newGameColor, -1 );
GadgetListBoxSetItemData( listbox, NULL, index );
} // end if
// clear the available games
clearAvailableGames();
// iterate all the save files in the directory and populate the listbox
iterateSaveFiles( addGameToAvailableList, &m_availableGames );
// add all games found to the list box
AvailableGameInfo *info;
SaveGameInfo *saveGameInfo;
SYSTEMTIME systemTime;
UnsignedInt count = 0;
for( info = m_availableGames; info; info = info->next, count++ )
{
// get save game info
saveGameInfo = &info->saveGameInfo;
// setup a system time structure given the data we saved in the file
systemTime.wYear = saveGameInfo->date.year;
systemTime.wMonth = saveGameInfo->date.month;
systemTime.wDayOfWeek = saveGameInfo->date.dayOfWeek;
systemTime.wDay = saveGameInfo->date.day;
systemTime.wHour = saveGameInfo->date.hour;
systemTime.wMinute = saveGameInfo->date.minute;
systemTime.wSecond = saveGameInfo->date.second;
systemTime.wMilliseconds = saveGameInfo->date.milliseconds;
// setup date buffer for local region date format
UnicodeString displayDateBuffer = getUnicodeDateBuffer(systemTime);
// setup time buffer for local region time format
UnicodeString displayTimeBuffer = getUnicodeTimeBuffer(systemTime);
// description string
UnicodeString displayLabel = saveGameInfo->description;
if( displayLabel.isEmpty() == TRUE )
{
Bool exists = FALSE;
displayLabel = TheGameText->fetch( saveGameInfo->mapLabel, &exists );
if( exists == FALSE )
displayLabel.format( L"%S", saveGameInfo->mapLabel.str() );
} // end if
// pick color for text (we alternate it each game)
Color color;
if( saveGameInfo->saveFileType == SAVE_FILE_TYPE_MISSION )
color = GameMakeColor( 200, 255, 200, 255 );
else if( count & 0x1 )
color = GameMakeColor( 255, 128, 0, 255 );
else
color = GameMakeColor( 255, 192, 0, 255 );
// add string to listbox
index = GadgetListBoxAddEntryText( listbox, displayLabel, color, -1, 0 );
GadgetListBoxAddEntryText( listbox, displayTimeBuffer, color, index, 1 );
GadgetListBoxAddEntryText( listbox, displayDateBuffer, color, index, 2 );
// add this available game info in the user data pointer of that listbox item
GadgetListBoxSetItemData( listbox, info, index );
} // end for, info
// select the top "new game" entry
GadgetListBoxSetSelected( listbox, 0 );
} // end pupulateSaveGameListbox
///////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE METHODS ////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
// ------------------------------------------------------------------------------------------------
/** Iterate the save game files */
// ------------------------------------------------------------------------------------------------
void GameState::iterateSaveFiles( IterateSaveFileCallback callback, void *userData )
{
// sanity
if( callback == NULL )
return;
// save the current directory
char currentDirectory[ _MAX_PATH ];
GetCurrentDirectory( _MAX_PATH, currentDirectory );
// switch into the save directory
SetCurrentDirectory( getSaveDirectory().str() );
// iterate all items in the directory
WIN32_FIND_DATA item; // search item
HANDLE hFile = INVALID_HANDLE_VALUE; // handle for search resources
Bool done = FALSE;
Bool first = TRUE;
while( done == FALSE )
{
// if our first time through we need to start the search
if( first )
{
// start search
hFile = FindFirstFile( "*", &item );
if( hFile == INVALID_HANDLE_VALUE )
return;
// we are no longer on our first item
first = FALSE;
} // end if, first
// see if this is a file, and therefore a possible save file
if( !(item.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )
{
// see if there is a ".sav" at end of this filename
Char *c = strrchr( item.cFileName, '.' );
if( c && stricmp( c, ".sav" ) == 0 )
{
// construction asciistring filename
AsciiString filename;
filename.set( item.cFileName );
// call the callback
callback( filename, userData );
} // end if, a save file
} // end if
// on to the next file
if( FindNextFile( hFile, &item ) == 0 )
done = TRUE;
} // end while
// close search resources
FindClose( hFile );
// restore the current directory
SetCurrentDirectory( currentDirectory );
} // end iterateSaveFiles
// ------------------------------------------------------------------------------------------------
/** Save game to xfer or load game using xfer */
// ------------------------------------------------------------------------------------------------
void GameState::friend_xferSaveDataForCRC( Xfer *xfer, SnapshotType which )
{
DEBUG_LOG(("GameState::friend_xferSaveDataForCRC() - SnapshotType %d\n", which));
SaveGameInfo *gameInfo = getSaveGameInfo();
gameInfo->description.clear();
gameInfo->saveFileType = SAVE_FILE_TYPE_NORMAL;
gameInfo->missionMapName.clear();
gameInfo->pristineMapName.clear();
xferSaveData(xfer, which);
}
// ------------------------------------------------------------------------------------------------
/** Save game to xfer or load game using xfer */
// ------------------------------------------------------------------------------------------------
void GameState::xferSaveData( Xfer *xfer, SnapshotType which )
{
// sanity
if( xfer == NULL )
throw SC_INVALID_XFER;
// save or load all blocks
if( xfer->getXferMode() == XFER_SAVE )
{
DEBUG_LOG(("GameState::xferSaveData() - XFER_SAVE\n"));
// save all blocks
AsciiString blockName;
SnapshotBlock *blockInfo;
SnapshotBlockListIterator it;
for( it = m_snapshotBlockList[which].begin(); it != m_snapshotBlockList[which].end(); ++it )
{
// get list data
blockInfo = &(*it);
// get block name
blockName = blockInfo->blockName;
DEBUG_LOG(("Looking at block '%s'\n", blockName.str()));
//
// for mission save files, we only save the game state block and campaign manager
// because anything else is not needed.
//
if( getSaveGameInfo()->saveFileType != SAVE_FILE_TYPE_MISSION ||
(blockName.compareNoCase( GAME_STATE_BLOCK_STRING ) == 0 ||
blockName.compareNoCase( CAMPAIGN_BLOCK_STRING ) == 0) )
{
// xfer block name
xfer->xferAsciiString( &blockName );
// xfer this block
try
{
// begin new data block
xfer->beginBlock();
// xfer block data
xfer->xferSnapshot( blockInfo->snapshot );
// end this block
xfer->endBlock();
} // end try
catch( ... )
{
DEBUG_CRASH(( "Error saving block '%s' in file '%s'\n",
blockName.str(), xfer->getIdentifier() ));
throw;
} // end catch
} // end if
} // end for, all snapshots
// write an end of file token
AsciiString eofToken = SAVE_FILE_EOF;
xfer->xferAsciiString( &eofToken );
} // end if, save
else
{
DEBUG_LOG(("GameState::xferSaveData() - not XFER_SAVE\n"));
AsciiString token;
Int blockSize;
Bool done = FALSE;
SnapshotBlock *blockInfo;
// read all data blocks in the file
while( done == FALSE )
{
// read next token
xfer->xferAsciiString( &token );
// check for end of file token
if( token.compareNoCase( SAVE_FILE_EOF ) == 0 )
{
// all done
done = TRUE;
} // end if
else
{
// find matching token in the save file lexicon
blockInfo = findBlockInfoByToken( token, which );
if( blockInfo == NULL )
{
// log the block not found
DEBUG_LOG(( "GameState::xferSaveData - Skipping unknown block '%s'\n", token.str() ));
//
// block was not found, this could have been a block from an older file
// format where the block was removed, skip the block data and try to continue
//
Int dataSize = xfer->beginBlock();
xfer->skip( dataSize );
// continue with while loop reading block tokens
continue;
} // end if
try
{
// read block start
blockSize = xfer->beginBlock();
// parse this data
xfer->xferSnapshot( blockInfo->snapshot );
// read block end
xfer->endBlock();
} // end try
catch( ... )
{
DEBUG_CRASH(( "Error loading block '%s' in file '%s'\n",
blockInfo->blockName.str(), xfer->getIdentifier() ));
throw;
} // end catch
} // end else, valid data block token
} // end while, not done
} // end else, load
} // end xferSaveData
// ------------------------------------------------------------------------------------------------
/** Add a snapshot to the post process list for later */
// ------------------------------------------------------------------------------------------------
void GameState::addPostProcessSnapshot( Snapshot *snapshot )
{
// sanity
if( snapshot == NULL )
{
DEBUG_CRASH(( "GameState::addPostProcessSnapshot - invalid parameters\n" ));
return;
} // end if
/*
//
// This is n^2 and gets real real, REAL slow on game maps. jba.
// Please keep this code around tho, it can be useful in debugging save games
//
// verify the snapshot isn't in the list already
SnapshotListIterator it;
for( it = m_snapshotPostProcessList.begin(); it != m_snapshotPostProcessList.end(); ++it )
{
if( (*it) == snapshot )
{
DEBUG_CRASH(( "GameState::addPostProcessSnapshot - snapshot is already in list!\n" ));
return;
} // end if
} // end for, it
*/
// add to the list
m_snapshotPostProcessList.push_back( snapshot );
} // end addPostProcessSnapshot
// ------------------------------------------------------------------------------------------------
/** Post process entry point after all game data has been xferd from disk */
// ------------------------------------------------------------------------------------------------
void GameState::gameStatePostProcessLoad( void )
{
// post process each snapshot that registered with us
SnapshotListIterator it;
Snapshot *snapshot;
for( it = m_snapshotPostProcessList.begin(); it != m_snapshotPostProcessList.end(); /*emtpy*/ )
{
// get snapshot
snapshot = *it;
// increment iterator
++it;
// do processing
snapshot->loadPostProcess();
} // end for
// clear the snapshot post process list as we are now done with it
m_snapshotPostProcessList.clear();
// evil... must ensure this is updated prior to the script engine running the first time.
ThePartitionManager->update();
} // end loadPostProcess
// ------------------------------------------------------------------------------------------------
/** Xfer method for the game state itself
* Version Info:
* 1: Initial version
* 2: Added save file type and mission map name (regular save vs automatic mission save) */
// ------------------------------------------------------------------------------------------------
void GameState::xfer( Xfer *xfer )
{
// version
XferVersion currentVersion = 2;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
// get structure for our current game info
SaveGameInfo *saveGameInfo = getSaveGameInfo();
// version 2
if( version >= 2 )
{
// file type
xfer->xferUser( &saveGameInfo->saveFileType, sizeof( SaveFileType ) );
// mission map name
xfer->xferAsciiString( &saveGameInfo->missionMapName );
} // end if
// current system time
SYSTEMTIME systemTime;
GetLocalTime( &systemTime );
// date and time
saveGameInfo->date.year = systemTime.wYear;
xfer->xferUnsignedShort( &saveGameInfo->date.year );
saveGameInfo->date.month = systemTime.wMonth;
xfer->xferUnsignedShort( &saveGameInfo->date.month );
saveGameInfo->date.day = systemTime.wDay;
xfer->xferUnsignedShort( &saveGameInfo->date.day );
saveGameInfo->date.dayOfWeek = systemTime.wDayOfWeek;
xfer->xferUnsignedShort( &saveGameInfo->date.dayOfWeek );
saveGameInfo->date.hour = systemTime.wHour;
xfer->xferUnsignedShort( &saveGameInfo->date.hour );
saveGameInfo->date.minute = systemTime.wMinute;
xfer->xferUnsignedShort( &saveGameInfo->date.minute );
saveGameInfo->date.second = systemTime.wSecond;
xfer->xferUnsignedShort( &saveGameInfo->date.second );
saveGameInfo->date.milliseconds = systemTime.wMilliseconds;
xfer->xferUnsignedShort( &saveGameInfo->date.milliseconds );
// user description
xfer->xferUnicodeString( &saveGameInfo->description );
Bool exists = FALSE;
Dict *dict = MapObject::getWorldDict();
if( dict )
saveGameInfo->mapLabel = dict->getAsciiString( TheKey_mapName, &exists );
// if no label was found, we'll use the map name (just filename, no directory info)
if( exists == FALSE || saveGameInfo->mapLabel == AsciiString::TheEmptyString )
{
char string[ _MAX_PATH ];
strcpy( string, TheGlobalData->m_mapName.str() );
char *p = strrchr( string, '\\' );
if( p == NULL )
saveGameInfo->mapLabel = TheGlobalData->m_mapName;
else
{
p++; // skip the '\' we're on
saveGameInfo->mapLabel.set( p );
} // end else
} // end if
// xfer map label
xfer->xferAsciiString( &saveGameInfo->mapLabel );
// campaign info
Campaign *campaign = TheCampaignManager->getCurrentCampaign();
if( campaign )
{
// campaign side
saveGameInfo->campaignSide = campaign->m_name;
xfer->xferAsciiString( &saveGameInfo->campaignSide );
// campaign mission number
saveGameInfo->missionNumber = TheCampaignManager->getCurrentMissionNumber();
xfer->xferInt( &saveGameInfo->missionNumber );
} // end if
else
{
// write empty campaign side
saveGameInfo->campaignSide = AsciiString::TheEmptyString;
xfer->xferAsciiString( &saveGameInfo->campaignSide );
// invalid mission number
saveGameInfo->missionNumber = CampaignManager::INVALID_MISSION_NUMBER;
xfer->xferInt( &saveGameInfo->missionNumber );
} // end else
} // end xfer