Skip to content

[GEN][ZH] Implement REPLAY NOT FOUND and MAP NOT FOUND dialogs when attempting to load a Replay that was deleted or whose map is missing #992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Core/GameEngine/Include/Common/GameDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@
#ifndef RETAIL_COMPATIBLE_XFER_SAVE
#define RETAIL_COMPATIBLE_XFER_SAVE (1) // Game is expected to be Xfer Save compatible with retail Generals 1.08, Zero Hour 1.04
#endif

#ifndef ENABLE_GAMETEXT_SUBSTITUTES
#define ENABLE_GAMETEXT_SUBSTITUTES (1) // The code can provide substitute texts when labels and strings are missing in the STR or CSF translation file
#endif
13 changes: 13 additions & 0 deletions Generals/Code/GameEngine/Include/GameClient/GameText.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class GameTextInterface : public SubsystemInterface

virtual UnicodeString fetch( const Char *label, Bool *exists = NULL ) = 0; ///< Returns the associated labeled unicode text
virtual UnicodeString fetch( AsciiString label, Bool *exists = NULL ) = 0; ///< Returns the associated labeled unicode text

// Do not call this directly, but use the FETCH_OR_SUBSTITUTE macro
virtual UnicodeString fetchOrSubstitute( const Char *label, const WideChar *substituteText ) = 0;

// This function is not performance tuned.. Its really only for Worldbuilder. jkmcd
virtual AsciiStringVec& getStringsWithLabelPrefix(AsciiString label) = 0;

Expand All @@ -92,5 +96,14 @@ extern GameTextInterface* CreateGameTextInterface( void );
// Inlining
//----------------------------------------------------------------------------

// TheSuperHackers @info This is meant to be used like:
// TheGameText->FETCH_OR_SUBSTITUTE("GUI:LabelName", L"Substitute Fallback Text")
// The substitute text will be compiled out if ENABLE_GAMETEXT_SUBSTITUTES is not defined.
#if ENABLE_GAMETEXT_SUBSTITUTES
#define FETCH_OR_SUBSTITUTE(labelA, substituteTextW) fetchOrSubstitute(labelA, substituteTextW)
#else
#define FETCH_OR_SUBSTITUTE(labelA, substituteTextW) fetch(labelA)
#endif


#endif // __GAMECLIENT_GAMETEXT_H_
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,75 @@ UnicodeString GetReplayFilenameFromListbox(GameWindow *listbox, Int index)
return fname;
}

//-------------------------------------------------------------------------------------------------

static Bool readReplayMapInfo(const AsciiString& filename, RecorderClass::ReplayHeader &header, ReplayGameInfo &info, const MapMetaData *&mapData)
{
header.forPlayback = FALSE;
header.filename = filename;

if (TheRecorder != NULL && TheRecorder->readReplayHeader(header))
{
if (ParseAsciiStringToGameInfo(&info, header.gameOptions))
{
if (TheMapCache != NULL)
mapData = TheMapCache->findMap(info.getMap());
else
mapData = NULL;

return true;
}
}
return false;
}

//-------------------------------------------------------------------------------------------------

static void removeReplayExtension(UnicodeString& replayName)
{
const Int extensionLength = TheRecorder->getReplayExtention().getLength();
for (Int k=0; k < extensionLength; ++k)
replayName.removeLastChar();
}

//-------------------------------------------------------------------------------------------------

static UnicodeString createReplayName(const AsciiString& filename)
{
AsciiString lastReplayFName = TheRecorder->getLastReplayFileName();
lastReplayFName.concat(TheRecorder->getReplayExtention());
UnicodeString replayName;

if (lastReplayFName.compareNoCase(filename) == 0)
{
replayName = TheGameText->fetch("GUI:LastReplay");
}
else
{
replayName.translate(filename);
removeReplayExtension(replayName);
}
return replayName;
}

//-------------------------------------------------------------------------------------------------

static UnicodeString createMapName(const AsciiString& filename, const ReplayGameInfo& info, const MapMetaData *mapData)
{
UnicodeString mapName;
if (!mapData)
{
// TheSuperHackers @bugfix helmutbuhler 08/03/2025 Just use the filename.
// Displaying a long map path string would break the map list gui.
const char* filename = info.getMap().reverseFind('\\');
mapName.translate(filename ? filename + 1 : info.getMap());
}
else
{
mapName = mapData->m_displayName;
}
return mapName;
}

//-------------------------------------------------------------------------------------------------
/** Populate the listbox with the names of the available replay files */
Expand Down Expand Up @@ -145,124 +214,97 @@ void PopulateReplayFileListbox(GameWindow *listbox)
// just want the filename
asciistr.set((*it).reverseFind('\\') + 1);

// lets get some info about the replay
RecorderClass::ReplayHeader header;
header.forPlayback = FALSE;
header.filename = asciistr;
Bool success = TheRecorder && TheMapCache && TheRecorder->readReplayHeader( header );
if (success)
{
ReplayGameInfo info;
if (ParseAsciiStringToGameInfo( &info, header.gameOptions ))
{
ReplayGameInfo info;
const MapMetaData *mapData;

// columns are: name, date, version, map, extra

// name
header.replayName.translate(asciistr);
for (Int tmp=0; tmp < TheRecorder->getReplayExtention().getLength(); ++tmp)
header.replayName.removeLastChar();
if (readReplayMapInfo(asciistr, header, info, mapData))
{
// columns are: name, date, version, map, extra

UnicodeString replayNameToShow = header.replayName;
// name
UnicodeString replayNameToShow = createReplayName(asciistr);

AsciiString lastReplayFName = TheRecorder->getLastReplayFileName();
lastReplayFName.concat(TheRecorder->getReplayExtention());
if (lastReplayFName.compareNoCase(asciistr) == 0)
replayNameToShow = TheGameText->fetch("GUI:LastReplay");
UnicodeString displayTimeBuffer = getUnicodeTimeBuffer(header.timeVal);

UnicodeString displayTimeBuffer = getUnicodeTimeBuffer(header.timeVal);
//displayTimeBuffer.format( L"%ls", timeBuffer);

//displayTimeBuffer.format( L"%ls", timeBuffer);
// version (no-op)

// version (no-op)
// map
UnicodeString mapStr = createMapName(asciistr, info, mapData);

// map
UnicodeString mapStr;
const MapMetaData *md = TheMapCache->findMap(info.getMap());
if (!md)
// // extra
// UnicodeString extraStr;
// if (header.localPlayerIndex >= 0)
// {
// // MP game
// time_t totalSeconds = header.endTime - header.startTime;
// Int mins = totalSeconds/60;
// Int secs = totalSeconds%60;
// Real fps = header.frameDuration/totalSeconds;
// extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":"");
//
// for (Int i=0; i<MAX_SLOTS; ++i)
// {
// const GameSlot *slot = info.getConstSlot(i);
// if (slot && slot->isHuman())
// {
// if (i)
// extraStr.concat(L", ");
// if (header.playerDiscons[i])
// extraStr.concat(L'*');
// extraStr.concat(info.getConstSlot(i)->getName());
// }
// }
// }
// else
// {
// // solo game
// time_t totalSeconds = header.endTime - header.startTime;
// Int mins = totalSeconds/60;
// Int secs = totalSeconds%60;
// Real fps = header.frameDuration/totalSeconds;
// extraStr.format(L"%d:%d (%g fps)", mins, secs, fps);
// }

// pick a color
Color color;
if (header.versionString == TheVersion->getUnicodeVersion() && header.versionNumber == TheVersion->getVersionNumber() &&
header.exeCRC == TheGlobalData->m_exeCRC && header.iniCRC == TheGlobalData->m_iniCRC)
{
// good version
if (header.localPlayerIndex >= 0)
{
// TheSuperHackers @bugfix helmutbuhler 08/03/2025 Just use the filename.
// Displaying a long map path string would break the map list gui.
const char* filename = info.getMap().reverseFind('\\');
mapStr.translate(filename ? filename + 1 : info.getMap());
// MP
color = colors[COLOR_MP];
}
else
{
mapStr = md->m_displayName;
// SP
color = colors[COLOR_SP];
}

// // extra
// UnicodeString extraStr;
// if (header.localPlayerIndex >= 0)
// {
// // MP game
// time_t totalSeconds = header.endTime - header.startTime;
// Int mins = totalSeconds/60;
// Int secs = totalSeconds%60;
// Real fps = header.frameDuration/totalSeconds;
// extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":"");
//
// for (Int i=0; i<MAX_SLOTS; ++i)
// {
// const GameSlot *slot = info.getConstSlot(i);
// if (slot && slot->isHuman())
// {
// if (i)
// extraStr.concat(L", ");
// if (header.playerDiscons[i])
// extraStr.concat(L'*');
// extraStr.concat(info.getConstSlot(i)->getName());
// }
// }
// }
// else
// {
// // solo game
// time_t totalSeconds = header.endTime - header.startTime;
// Int mins = totalSeconds/60;
// Int secs = totalSeconds%60;
// Real fps = header.frameDuration/totalSeconds;
// extraStr.format(L"%d:%d (%g fps)", mins, secs, fps);
// }

// pick a color
Color color;
if (header.versionString == TheVersion->getUnicodeVersion() && header.versionNumber == TheVersion->getVersionNumber() &&
header.exeCRC == TheGlobalData->m_exeCRC && header.iniCRC == TheGlobalData->m_iniCRC)
}
else
{
// bad version
if (header.localPlayerIndex >= 0)
{
// good version
if (header.localPlayerIndex >= 0)
{
// MP
color = colors[COLOR_MP];
}
else
{
// SP
color = colors[COLOR_SP];
}
// MP
color = colors[COLOR_MP_CRC_MISMATCH];
}
else
{
// bad version
if (header.localPlayerIndex >= 0)
{
// MP
color = colors[COLOR_MP_CRC_MISMATCH];
}
else
{
// SP
color = colors[COLOR_SP_CRC_MISMATCH];
}
// SP
color = colors[COLOR_SP_CRC_MISMATCH];
}

Int insertionIndex = GadgetListBoxAddEntryText(listbox, replayNameToShow, color, -1, 0);
GadgetListBoxAddEntryText(listbox, displayTimeBuffer, color, insertionIndex, 1);
GadgetListBoxAddEntryText(listbox, header.versionString, color, insertionIndex, 2);
GadgetListBoxAddEntryText(listbox, mapStr, color, insertionIndex, 3);
//GadgetListBoxAddEntryText(listbox, extraStr, color, insertionIndex, 4);
}

Int insertionIndex = GadgetListBoxAddEntryText(listbox, replayNameToShow, color, -1, 0);
GadgetListBoxAddEntryText(listbox, displayTimeBuffer, color, insertionIndex, 1);
GadgetListBoxAddEntryText(listbox, header.versionString, color, insertionIndex, 2);
GadgetListBoxAddEntryText(listbox, mapStr, color, insertionIndex, 3);
//GadgetListBoxAddEntryText(listbox, extraStr, color, insertionIndex, 4);
}
}
GadgetListBoxSetSelected(listbox, 0);
Expand Down Expand Up @@ -449,8 +491,32 @@ static void loadReplay(UnicodeString filename)
AsciiString asciiFilename;
asciiFilename.translate(filename);

if(!TheRecorder->replayMatchesGameVersion(asciiFilename))
RecorderClass::ReplayHeader header;
ReplayGameInfo info;
const MapMetaData *mapData;

if(!readReplayMapInfo(asciiFilename, header, info, mapData))
{
// TheSuperHackers @bugfix Prompts a message box when the replay was deleted by the user while the Replay Menu was opened.

UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFoundTitle", L"REPLAY NOT FOUND");
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayFileNotFound", L"This replay cannot be loaded because the file no longer exists on this device.");

MessageBoxOk(title, body, NULL);
}
else if(mapData == NULL)
{
// TheSuperHackers @bugfix Prompts a message box when the map used by the replay was not found.

UnicodeString title = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFoundTitle", L"MAP NOT FOUND");
UnicodeString body = TheGameText->FETCH_OR_SUBSTITUTE("GUI:ReplayMapNotFound", L"This replay cannot be loaded because the map was not found on this device.");

MessageBoxOk(title, body, NULL);
}
else if(!TheRecorder->replayMatchesGameVersion(header))
{
// Pressing OK loads the replay.

MessageBoxOkCancel(TheGameText->fetch("GUI:OlderReplayVersionTitle"), TheGameText->fetch("GUI:OlderReplayVersion"), reallyLoadReplay, NULL);
}
else
Expand Down
15 changes: 15 additions & 0 deletions Generals/Code/GameEngine/Source/GameClient/GameText.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ class GameTextManager : public GameTextInterface

virtual UnicodeString fetch( const Char *label, Bool *exists = NULL ); ///< Returns the associated labeled unicode text
virtual UnicodeString fetch( AsciiString label, Bool *exists = NULL ); ///< Returns the associated labeled unicode text
virtual UnicodeString fetchOrSubstitute( const Char *label, const WideChar *substituteText );

virtual AsciiStringVec& getStringsWithLabelPrefix(AsciiString label);

virtual void initMapStringFile( const AsciiString& filename );
Expand Down Expand Up @@ -1341,6 +1343,19 @@ UnicodeString GameTextManager::fetch( AsciiString label, Bool *exists )
return fetch(label.str(), exists);
}

//============================================================================
// GameTextManager::fetchOrSubstitute
//============================================================================

UnicodeString GameTextManager::fetchOrSubstitute( const Char *label, const WideChar *substituteText )
{
Bool exists;
UnicodeString str = fetch(label, &exists);
if (!exists)
str = substituteText;
return str;
}

//============================================================================
// GameTextManager::getStringsWithLabelPrefix
//============================================================================
Expand Down
13 changes: 13 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameClient/GameText.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class GameTextInterface : public SubsystemInterface

virtual UnicodeString fetch( const Char *label, Bool *exists = NULL ) = 0; ///< Returns the associated labeled unicode text
virtual UnicodeString fetch( AsciiString label, Bool *exists = NULL ) = 0; ///< Returns the associated labeled unicode text

// Do not call this directly, but use the FETCH_OR_SUBSTITUTE macro
virtual UnicodeString fetchOrSubstitute( const Char *label, const WideChar *substituteText ) = 0;

// This function is not performance tuned.. Its really only for Worldbuilder. jkmcd
virtual AsciiStringVec& getStringsWithLabelPrefix(AsciiString label) = 0;

Expand All @@ -92,5 +96,14 @@ extern GameTextInterface* CreateGameTextInterface( void );
// Inlining
//----------------------------------------------------------------------------

// TheSuperHackers @info This is meant to be used like:
// TheGameText->FETCH_OR_SUBSTITUTE("GUI:LabelName", L"Substitute Fallback Text")
// The substitute text will be compiled out if ENABLE_GAMETEXT_SUBSTITUTES is not defined.
#if ENABLE_GAMETEXT_SUBSTITUTES
#define FETCH_OR_SUBSTITUTE(labelA, substituteTextW) fetchOrSubstitute(labelA, substituteTextW)
#else
#define FETCH_OR_SUBSTITUTE(labelA, substituteTextW) fetch(labelA)
#endif


#endif // __GAMECLIENT_GAMETEXT_H_
Loading
Loading