Skip to content

Commit 8d6e26c

Browse files
committed
[GEN][ZH] Implement multi instance support for the game client (#794)
1 parent 1eecc01 commit 8d6e26c

File tree

15 files changed

+393
-43
lines changed

15 files changed

+393
-43
lines changed

Generals/Code/GameEngine/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ set(GAMEENGINE_SRC
132132
Include/GameClient/AnimateWindowManager.h
133133
Include/GameClient/CampaignManager.h
134134
Include/GameClient/CDCheck.h
135+
Include/GameClient/ClientInstance.h
135136
Include/GameClient/ClientRandomValue.h
136137
Include/GameClient/Color.h
137138
Include/GameClient/CommandXlat.h
@@ -633,6 +634,7 @@ set(GAMEENGINE_SRC
633634
Source/Common/Thing/ThingTemplate.cpp
634635
Source/Common/UserPreferences.cpp
635636
Source/Common/version.cpp
637+
Source/GameClient/ClientInstance.cpp
636638
Source/GameClient/Color.cpp
637639
Source/GameClient/Credits.cpp
638640
Source/GameClient/Display.cpp
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
#include "Lib/BaseType.h"
19+
20+
namespace rts
21+
{
22+
23+
// TheSuperHackers @feature Adds support for launching multiple game clients and keeping track of their instance id.
24+
25+
class ClientInstance
26+
{
27+
public:
28+
// Can be called N times, but is initialized just once.
29+
static bool initialize();
30+
31+
static bool isInitialized();
32+
33+
// Returns the instance index of this game client. Starts at 0.
34+
static UnsignedInt getInstanceIndex();
35+
36+
// Returns the instance id of this game client. Starts at 1.
37+
static UnsignedInt getInstanceId();
38+
39+
// Returns the instance name of the first game client.
40+
static const char* getFirstInstanceName();
41+
42+
private:
43+
static HANDLE s_mutexHandle;
44+
static UnsignedInt s_instanceIndex;
45+
};
46+
47+
} // namespace rts

Generals/Code/GameEngine/Source/Common/Recorder.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "Common/Player.h"
3131
#include "Common/GlobalData.h"
3232
#include "Common/GameEngine.h"
33+
#include "GameClient/ClientInstance.h"
3334
#include "GameClient/GameWindow.h"
3435
#include "GameClient/GameWindowManager.h"
3536
#include "GameClient/InGameUI.h"
@@ -1595,7 +1596,17 @@ AsciiString RecorderClass::getLastReplayFileName()
15951596
}
15961597
}
15971598
#endif
1598-
return AsciiString(lastReplayFileName);
1599+
1600+
AsciiString filename;
1601+
if (rts::ClientInstance::getInstanceId() > 1u)
1602+
{
1603+
filename.format("%s_Instance%.2u", lastReplayFileName, rts::ClientInstance::getInstanceId());
1604+
}
1605+
else
1606+
{
1607+
filename = lastReplayFileName;
1608+
}
1609+
return filename;
15991610
}
16001611

16011612
/**

Generals/Code/GameEngine/Source/Common/System/Debug.cpp

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
#include "Common/Registry.h"
6464
#include "Common/SystemInfo.h"
6565
#include "Common/UnicodeString.h"
66+
#include "GameClient/ClientInstance.h"
6667
#include "GameClient/GameText.h"
6768
#include "GameClient/Keyboard.h"
6869
#include "GameClient/Mouse.h"
@@ -86,14 +87,14 @@ extern const char *gAppPrefix; /// So WB can have a different log file name.
8687
#ifdef DEBUG_LOGGING
8788

8889
#if defined(RTS_INTERNAL)
89-
#define DEBUG_FILE_NAME "DebugLogFileI.txt"
90-
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrevI.txt"
90+
#define DEBUG_FILE_NAME "DebugLogFileI"
91+
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrevI"
9192
#elif defined(RTS_DEBUG)
92-
#define DEBUG_FILE_NAME "DebugLogFileD.txt"
93-
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrevD.txt"
93+
#define DEBUG_FILE_NAME "DebugLogFileD"
94+
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrevD"
9495
#else
95-
#define DEBUG_FILE_NAME "DebugLogFile.txt"
96-
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrev.txt"
96+
#define DEBUG_FILE_NAME "DebugLogFile"
97+
#define DEBUG_FILE_NAME_PREV "DebugLogFilePrev"
9798
#endif
9899

99100
#endif
@@ -363,6 +364,11 @@ void DebugInit(int flags)
363364

364365
#ifdef DEBUG_LOGGING
365366

367+
// TheSuperHackers @info Debug initialization can happen very early.
368+
// Therefore, initialize the client instance now.
369+
if (!rts::ClientInstance::initialize())
370+
return;
371+
366372
char dirbuf[ _MAX_PATH ];
367373
::GetModuleFileName( NULL, dirbuf, sizeof( dirbuf ) );
368374
char *pEnd = dirbuf + strlen( dirbuf );
@@ -379,10 +385,16 @@ void DebugInit(int flags)
379385
strcpy(theLogFileNamePrev, dirbuf);
380386
strcat(theLogFileNamePrev, gAppPrefix);
381387
strcat(theLogFileNamePrev, DEBUG_FILE_NAME_PREV);
388+
if (rts::ClientInstance::getInstanceId() > 1u)
389+
sprintf(theLogFileNamePrev + strlen(theLogFileNamePrev), "_Instance%.2u", rts::ClientInstance::getInstanceId());
390+
strcat(theLogFileNamePrev, ".txt");
382391

383392
strcpy(theLogFileName, dirbuf);
384393
strcat(theLogFileName, gAppPrefix);
385394
strcat(theLogFileName, DEBUG_FILE_NAME);
395+
if (rts::ClientInstance::getInstanceId() > 1u)
396+
sprintf(theLogFileName + strlen(theLogFileName), "_Instance%.2u", rts::ClientInstance::getInstanceId());
397+
strcat(theLogFileName, ".txt");
386398

387399
remove(theLogFileNamePrev);
388400
rename(theLogFileName, theLogFileNamePrev);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
#include "PreRTS.h"
19+
#include "GameClient/ClientInstance.h"
20+
21+
#define GENERALS_GUID "685EAFF2-3216-4265-B047-251C5F4B82F3"
22+
23+
namespace rts
24+
{
25+
HANDLE ClientInstance::s_mutexHandle = NULL;
26+
UnsignedInt ClientInstance::s_instanceIndex = 0;
27+
28+
bool ClientInstance::initialize()
29+
{
30+
if (isInitialized())
31+
{
32+
return true;
33+
}
34+
35+
// Create a mutex with a unique name to Generals in order to determine if our app is already running.
36+
// WARNING: DO NOT use this number for any other application except Generals.
37+
while (true)
38+
{
39+
#ifdef RTS_MULTI_INSTANCE
40+
std::string guidStr = getFirstInstanceName();
41+
if (s_instanceIndex > 0u)
42+
{
43+
char idStr[33];
44+
itoa(s_instanceIndex, idStr, 10);
45+
guidStr.push_back('-');
46+
guidStr.append(idStr);
47+
}
48+
s_mutexHandle = CreateMutex(NULL, FALSE, guidStr.c_str());
49+
if (GetLastError() == ERROR_ALREADY_EXISTS)
50+
{
51+
if (s_mutexHandle != NULL)
52+
{
53+
CloseHandle(s_mutexHandle);
54+
s_mutexHandle = NULL;
55+
}
56+
// Try again with a new instance.
57+
++s_instanceIndex;
58+
continue;
59+
}
60+
#else
61+
s_mutexHandle = CreateMutex(NULL, FALSE, getFirstInstanceName());
62+
if (GetLastError() == ERROR_ALREADY_EXISTS)
63+
{
64+
if (s_mutexHandle != NULL)
65+
{
66+
CloseHandle(s_mutexHandle);
67+
s_mutexHandle = NULL;
68+
}
69+
return false;
70+
}
71+
#endif
72+
break;
73+
}
74+
75+
return true;
76+
}
77+
78+
bool ClientInstance::isInitialized()
79+
{
80+
return s_mutexHandle != NULL;
81+
}
82+
83+
UnsignedInt ClientInstance::getInstanceIndex()
84+
{
85+
DEBUG_ASSERTLOG(!isInitialized(), ("ClientInstance::isInitialized() failed"));
86+
return s_instanceIndex;
87+
}
88+
89+
UnsignedInt ClientInstance::getInstanceId()
90+
{
91+
return getInstanceIndex() + 1;
92+
}
93+
94+
const char* ClientInstance::getFirstInstanceName()
95+
{
96+
return GENERALS_GUID;
97+
}
98+
99+
} // namespace rts

Generals/Code/GameEngine/Source/GameClient/GameText.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "GameClient/GameText.h"
4949
#include "Common/Language.h"
5050
#include "Common/Registry.h"
51+
#include "GameClient/ClientInstance.h"
5152
#include "GameClient/LanguageFilter.h"
5253
#include "Common/Debug.h"
5354
#include "Common/UnicodeString.h"
@@ -370,6 +371,14 @@ void GameTextManager::init( void )
370371
qsort( m_stringLUT, m_textCount, sizeof(StringLookUp), compareLUT );
371372

372373
UnicodeString ourName = fetch("GUI:Command&ConquerGenerals");
374+
375+
if (rts::ClientInstance::getInstanceId() > 1u)
376+
{
377+
UnicodeString s;
378+
s.format(L"Instance:%.2u - %s", rts::ClientInstance::getInstanceId(), ourName.str());
379+
ourName = s;
380+
}
381+
373382
extern HWND ApplicationHWnd; ///< our application window handle
374383
if (ApplicationHWnd) {
375384
::SetWindowTextW(ApplicationHWnd, ourName.str());

Generals/Code/Main/WinMain.cpp

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "Common/StackDump.h"
5454
#include "Common/MessageStream.h"
5555
#include "Common/Team.h"
56+
#include "GameClient/ClientInstance.h"
5657
#include "GameClient/InGameUI.h"
5758
#include "GameClient/GameClient.h"
5859
#include "GameLogic/GameLogic.h" ///< @todo for demo, remove
@@ -83,8 +84,6 @@ const Char *g_strFile = "data\\Generals.str";
8384
const Char *g_csfFile = "data\\%s\\Generals.csf";
8485
const char *gAppPrefix = ""; /// So WB can have a different debug log file name.
8586

86-
static HANDLE GeneralsMutex = NULL;
87-
#define GENERALS_GUID "685EAFF2-3216-4265-B047-251C5F4B82F3"
8887
#define DEFAULT_XRESOLUTION 800
8988
#define DEFAULT_YRESOLUTION 600
9089

@@ -942,23 +941,16 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
942941
#endif
943942

944943

945-
//Create a mutex with a unique name to Generals in order to determine if
946-
//our app is already running.
947-
//WARNING: DO NOT use this number for any other application except Generals.
948-
GeneralsMutex = CreateMutex(NULL, FALSE, GENERALS_GUID);
949-
if (GetLastError() == ERROR_ALREADY_EXISTS)
944+
// TheSuperHackers @refactor The instance mutex now lives in its own class.
945+
946+
if (!rts::ClientInstance::initialize())
950947
{
951-
HWND ccwindow = FindWindow(GENERALS_GUID, NULL);
948+
HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), NULL);
952949
if (ccwindow)
953950
{
954951
SetForegroundWindow(ccwindow);
955952
ShowWindow(ccwindow, SW_RESTORE);
956953
}
957-
if (GeneralsMutex != NULL)
958-
{
959-
CloseHandle(GeneralsMutex);
960-
GeneralsMutex = NULL;
961-
}
962954

963955
DEBUG_LOG(("Generals is already running...Bail!\n"));
964956
delete TheVersion;
@@ -967,7 +959,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
967959
DEBUG_SHUTDOWN();
968960
return 0;
969961
}
970-
DEBUG_LOG(("Create GeneralsMutex okay.\n"));
962+
DEBUG_LOG(("Create Generals Mutex okay.\n"));
971963

972964
#ifdef DO_COPY_PROTECTION
973965
if (!CopyProtect::notifyLauncher())

GeneralsMD/Code/GameEngine/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ set(GAMEENGINE_SRC
139139
Include/GameClient/CampaignManager.h
140140
Include/GameClient/CDCheck.h
141141
Include/GameClient/ChallengeGenerals.h
142+
Include/GameClient/ClientInstance.h
142143
Include/GameClient/ClientRandomValue.h
143144
Include/GameClient/Color.h
144145
Include/GameClient/CommandXlat.h
@@ -676,6 +677,7 @@ set(GAMEENGINE_SRC
676677
Source/Common/Thing/ThingTemplate.cpp
677678
Source/Common/UserPreferences.cpp
678679
Source/Common/version.cpp
680+
Source/GameClient/ClientInstance.cpp
679681
Source/GameClient/Color.cpp
680682
Source/GameClient/Credits.cpp
681683
Source/GameClient/Display.cpp
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
#include "Lib/BaseType.h"
19+
20+
namespace rts
21+
{
22+
23+
// TheSuperHackers @feature Adds support for launching multiple game clients and keeping track of their instance id.
24+
25+
class ClientInstance
26+
{
27+
public:
28+
// Can be called N times, but is initialized just once.
29+
static bool initialize();
30+
31+
static bool isInitialized();
32+
33+
// Returns the instance index of this game client. Starts at 0.
34+
static UnsignedInt getInstanceIndex();
35+
36+
// Returns the instance id of this game client. Starts at 1.
37+
static UnsignedInt getInstanceId();
38+
39+
// Returns the instance name of the first game client.
40+
static const char* getFirstInstanceName();
41+
42+
private:
43+
static HANDLE s_mutexHandle;
44+
static UnsignedInt s_instanceIndex;
45+
};
46+
47+
} // namespace rts

0 commit comments

Comments
 (0)