Skip to content

Commit 834f369

Browse files
authored
feat(gui): Implement replay info tooltips in Replay Menu (#1720)
Hovering a replay in the list will now show a tooltip with match duration, fps and human player names
1 parent 87adc1a commit 834f369

File tree

2 files changed

+144
-68
lines changed
  • GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus
  • Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus

2 files changed

+144
-68
lines changed

Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@
4545
#include "GameClient/GameWindowManager.h"
4646
#include "GameClient/MessageBox.h"
4747
#include "GameClient/MapUtil.h"
48+
#include "GameClient/Mouse.h"
4849
#include "GameClient/GameText.h"
4950
#include "GameClient/GameWindowTransitions.h"
5051

52+
typedef UnicodeString ReplayName;
53+
typedef UnicodeString TooltipString;
54+
typedef std::map<ReplayName, TooltipString> ReplayTooltipMap;
55+
56+
static ReplayTooltipMap replayTooltipCache;
5157

5258
// window ids -------------------------------------------------------------------------------------
5359
static NameKeyType parentReplayMenuID = NAMEKEY_INVALID;
@@ -166,11 +172,70 @@ static UnicodeString createMapName(const AsciiString& filename, const ReplayGame
166172
return mapName;
167173
}
168174

175+
//-------------------------------------------------------------------------------------------------
176+
// TheSuperHackers @feature Stubbjax 21/10/2025 Show extra info tooltip when hovering over a replay.
177+
178+
static void showReplayTooltip(GameWindow* window, WinInstanceData* instData, UnsignedInt mouse)
179+
{
180+
Int x, y, row, col;
181+
x = LOLONGTOSHORT(mouse);
182+
y = HILONGTOSHORT(mouse);
183+
184+
GadgetListBoxGetEntryBasedOnXY(window, x, y, row, col);
185+
186+
if (row == -1 || col == -1)
187+
{
188+
TheMouse->setCursorTooltip(UnicodeString::TheEmptyString);
189+
return;
190+
}
191+
192+
UnicodeString replayFileName = GetReplayFilenameFromListbox(window, row);
193+
194+
ReplayTooltipMap::const_iterator it = replayTooltipCache.find(replayFileName);
195+
if (it != replayTooltipCache.end())
196+
TheMouse->setCursorTooltip(it->second, -1, NULL, 1.5f);
197+
else
198+
TheMouse->setCursorTooltip(UnicodeString::TheEmptyString);
199+
}
200+
201+
static UnicodeString buildReplayTooltip(RecorderClass::ReplayHeader header, ReplayGameInfo info)
202+
{
203+
UnicodeString tooltipStr;
204+
205+
if (header.endTime < header.startTime)
206+
header.startTime = header.endTime;
207+
208+
time_t totalSeconds = header.endTime - header.startTime;
209+
UnsignedInt hours = totalSeconds / 3600;
210+
UnsignedInt mins = (totalSeconds % 3600) / 60;
211+
UnsignedInt secs = totalSeconds % 60;
212+
Real fps = totalSeconds > 0 ? header.frameCount / totalSeconds : 0;
213+
tooltipStr.format(L"%02u:%02u:%02u (%g fps)", hours, mins, secs, fps);
214+
215+
if (header.localPlayerIndex >= 0)
216+
{
217+
// MP game
218+
for (Int i = 0; i < MAX_SLOTS; ++i)
219+
{
220+
const GameSlot* slot = info.getConstSlot(i);
221+
if (slot && slot->isHuman())
222+
{
223+
tooltipStr.concat(L"\n");
224+
tooltipStr.concat(info.getConstSlot(i)->getName());
225+
}
226+
}
227+
}
228+
229+
return tooltipStr;
230+
}
231+
169232
//-------------------------------------------------------------------------------------------------
170233
/** Populate the listbox with the names of the available replay files */
171234
//-------------------------------------------------------------------------------------------------
172235
void PopulateReplayFileListbox(GameWindow *listbox)
173236
{
237+
replayTooltipCache.clear();
238+
174239
if (!TheMapCache)
175240
return;
176241

@@ -234,39 +299,12 @@ void PopulateReplayFileListbox(GameWindow *listbox)
234299
// map
235300
UnicodeString mapStr = createMapName(asciistr, info, mapData);
236301

237-
// // extra
238-
// UnicodeString extraStr;
239-
// if (header.localPlayerIndex >= 0)
240-
// {
241-
// // MP game
242-
// time_t totalSeconds = header.endTime - header.startTime;
243-
// Int mins = totalSeconds/60;
244-
// Int secs = totalSeconds%60;
245-
// Real fps = header.frameCount/totalSeconds;
246-
// extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":"");
247-
//
248-
// for (Int i=0; i<MAX_SLOTS; ++i)
249-
// {
250-
// const GameSlot *slot = info.getConstSlot(i);
251-
// if (slot && slot->isHuman())
252-
// {
253-
// if (i)
254-
// extraStr.concat(L", ");
255-
// if (header.playerDiscons[i])
256-
// extraStr.concat(L'*');
257-
// extraStr.concat(info.getConstSlot(i)->getName());
258-
// }
259-
// }
260-
// }
261-
// else
262-
// {
263-
// // solo game
264-
// time_t totalSeconds = header.endTime - header.startTime;
265-
// Int mins = totalSeconds/60;
266-
// Int secs = totalSeconds%60;
267-
// Real fps = header.frameCount/totalSeconds;
268-
// extraStr.format(L"%d:%d (%g fps)", mins, secs, fps);
269-
// }
302+
// tooltip
303+
UnicodeString tooltipStr = buildReplayTooltip(header, info);
304+
305+
UnicodeString key;
306+
key.translate(asciistr);
307+
replayTooltipCache[key] = tooltipStr;
270308

271309
// pick a color
272310
Color color;
@@ -321,7 +359,6 @@ void PopulateReplayFileListbox(GameWindow *listbox)
321359
GadgetListBoxAddEntryText(listbox, displayTimeBuffer, color, insertionIndex, 1);
322360
GadgetListBoxAddEntryText(listbox, header.versionString, color, insertionIndex, 2);
323361
GadgetListBoxAddEntryText(listbox, mapStr, mapColor, insertionIndex, 3);
324-
//GadgetListBoxAddEntryText(listbox, extraStr, color, insertionIndex, 4);
325362
}
326363
}
327364
GadgetListBoxSetSelected(listbox, 0);
@@ -346,6 +383,7 @@ void ReplayMenuInit( WindowLayout *layout, void *userData )
346383
buttonLoad = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonLoadID );
347384
buttonBack = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonBackID );
348385
listboxReplayFiles = TheWindowManager->winGetWindowFromId( parentReplayMenu, listboxReplayFilesID );
386+
listboxReplayFiles->winSetTooltipFunc(showReplayTooltip);
349387
buttonDelete = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonDeleteID );
350388
buttonCopy = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonCopyID );
351389

GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/ReplayMenu.cpp

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@
4545
#include "GameClient/GameWindowManager.h"
4646
#include "GameClient/MessageBox.h"
4747
#include "GameClient/MapUtil.h"
48+
#include "GameClient/Mouse.h"
4849
#include "GameClient/GameText.h"
4950
#include "GameClient/GameWindowTransitions.h"
5051

52+
typedef UnicodeString ReplayName;
53+
typedef UnicodeString TooltipString;
54+
typedef std::map<ReplayName, TooltipString> ReplayTooltipMap;
55+
56+
static ReplayTooltipMap replayTooltipCache;
5157

5258
// window ids -------------------------------------------------------------------------------------
5359
static NameKeyType parentReplayMenuID = NAMEKEY_INVALID;
@@ -166,11 +172,70 @@ static UnicodeString createMapName(const AsciiString& filename, const ReplayGame
166172
return mapName;
167173
}
168174

175+
//-------------------------------------------------------------------------------------------------
176+
// TheSuperHackers @feature Stubbjax 21/10/2025 Show extra info tooltip when hovering over a replay.
177+
178+
static void showReplayTooltip(GameWindow* window, WinInstanceData* instData, UnsignedInt mouse)
179+
{
180+
Int x, y, row, col;
181+
x = LOLONGTOSHORT(mouse);
182+
y = HILONGTOSHORT(mouse);
183+
184+
GadgetListBoxGetEntryBasedOnXY(window, x, y, row, col);
185+
186+
if (row == -1 || col == -1)
187+
{
188+
TheMouse->setCursorTooltip(UnicodeString::TheEmptyString);
189+
return;
190+
}
191+
192+
UnicodeString replayFileName = GetReplayFilenameFromListbox(window, row);
193+
194+
ReplayTooltipMap::const_iterator it = replayTooltipCache.find(replayFileName);
195+
if (it != replayTooltipCache.end())
196+
TheMouse->setCursorTooltip(it->second, -1, NULL, 1.5f);
197+
else
198+
TheMouse->setCursorTooltip(UnicodeString::TheEmptyString);
199+
}
200+
201+
static UnicodeString buildReplayTooltip(RecorderClass::ReplayHeader header, ReplayGameInfo info)
202+
{
203+
UnicodeString tooltipStr;
204+
205+
if (header.endTime < header.startTime)
206+
header.startTime = header.endTime;
207+
208+
time_t totalSeconds = header.endTime - header.startTime;
209+
UnsignedInt hours = totalSeconds / 3600;
210+
UnsignedInt mins = (totalSeconds % 3600) / 60;
211+
UnsignedInt secs = totalSeconds % 60;
212+
Real fps = totalSeconds > 0 ? header.frameCount / totalSeconds : 0;
213+
tooltipStr.format(L"%02u:%02u:%02u (%g fps)", hours, mins, secs, fps);
214+
215+
if (header.localPlayerIndex >= 0)
216+
{
217+
// MP game
218+
for (Int i = 0; i < MAX_SLOTS; ++i)
219+
{
220+
const GameSlot* slot = info.getConstSlot(i);
221+
if (slot && slot->isHuman())
222+
{
223+
tooltipStr.concat(L"\n");
224+
tooltipStr.concat(info.getConstSlot(i)->getName());
225+
}
226+
}
227+
}
228+
229+
return tooltipStr;
230+
}
231+
169232
//-------------------------------------------------------------------------------------------------
170233
/** Populate the listbox with the names of the available replay files */
171234
//-------------------------------------------------------------------------------------------------
172235
void PopulateReplayFileListbox(GameWindow *listbox)
173236
{
237+
replayTooltipCache.clear();
238+
174239
if (!TheMapCache)
175240
return;
176241

@@ -234,39 +299,12 @@ void PopulateReplayFileListbox(GameWindow *listbox)
234299
// map
235300
UnicodeString mapStr = createMapName(asciistr, info, mapData);
236301

237-
// // extra
238-
// UnicodeString extraStr;
239-
// if (header.localPlayerIndex >= 0)
240-
// {
241-
// // MP game
242-
// time_t totalSeconds = header.endTime - header.startTime;
243-
// Int mins = totalSeconds/60;
244-
// Int secs = totalSeconds%60;
245-
// Real fps = header.frameCount/totalSeconds;
246-
// extraStr.format(L"%d:%d (%g fps) %hs", mins, secs, fps, header.desyncGame?"OOS ":"");
247-
//
248-
// for (Int i=0; i<MAX_SLOTS; ++i)
249-
// {
250-
// const GameSlot *slot = info.getConstSlot(i);
251-
// if (slot && slot->isHuman())
252-
// {
253-
// if (i)
254-
// extraStr.concat(L", ");
255-
// if (header.playerDiscons[i])
256-
// extraStr.concat(L'*');
257-
// extraStr.concat(info.getConstSlot(i)->getName());
258-
// }
259-
// }
260-
// }
261-
// else
262-
// {
263-
// // solo game
264-
// time_t totalSeconds = header.endTime - header.startTime;
265-
// Int mins = totalSeconds/60;
266-
// Int secs = totalSeconds%60;
267-
// Real fps = header.frameCount/totalSeconds;
268-
// extraStr.format(L"%d:%d (%g fps)", mins, secs, fps);
269-
// }
302+
// tooltip
303+
UnicodeString tooltipStr = buildReplayTooltip(header, info);
304+
305+
UnicodeString key;
306+
key.translate(asciistr);
307+
replayTooltipCache[key] = tooltipStr;
270308

271309
// pick a color
272310
Color color;
@@ -321,7 +359,6 @@ void PopulateReplayFileListbox(GameWindow *listbox)
321359
GadgetListBoxAddEntryText(listbox, displayTimeBuffer, color, insertionIndex, 1);
322360
GadgetListBoxAddEntryText(listbox, header.versionString, color, insertionIndex, 2);
323361
GadgetListBoxAddEntryText(listbox, mapStr, mapColor, insertionIndex, 3);
324-
//GadgetListBoxAddEntryText(listbox, extraStr, color, insertionIndex, 4);
325362
}
326363
}
327364
GadgetListBoxSetSelected(listbox, 0);
@@ -346,6 +383,7 @@ void ReplayMenuInit( WindowLayout *layout, void *userData )
346383
buttonLoad = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonLoadID );
347384
buttonBack = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonBackID );
348385
listboxReplayFiles = TheWindowManager->winGetWindowFromId( parentReplayMenu, listboxReplayFilesID );
386+
listboxReplayFiles->winSetTooltipFunc(showReplayTooltip);
349387
buttonDelete = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonDeleteID );
350388
buttonCopy = TheWindowManager->winGetWindowFromId( parentReplayMenu, buttonCopyID );
351389

0 commit comments

Comments
 (0)