
After several months of working on the source code of the Cossacks: War Again game, I can finally wash my hands and present the
results of my labors . In this article I would like to share with you the experience of refactoring this extraordinary project, in particular, code curiosities. All lovers of necro programming is dedicated to ...
Start
The first and one of the most unpleasant obstacles was not in the code, but in the projects themselves. There are four of them:
- static library CommCore.lib (GSCp network protocol based on UDP)
- IntExplorer.dll dynamic library (game lobby on the server)
- IChat.dll dynamic library (chat in the game lobby)
- dmcr.exe executable
And these guys have seen some code! Because of the circular dependency between the IChat.dll projects and the dmcr.exe, at least one lib file from the previous build is required for linking. Project files themselves are incorrectly converted in Visual Studio 2015: they contain links to libraries that are not displayed in project properties, absolute paths to files in the system of one of the developers, and other surprises. In the end, I was bored with dancing with tambourines, and I created all the projects anew, having learned in passing that there are extra files in the archive with outdated source code, which when included in the project leads to conflicts. Well, where could it be without linking features, for which it is necessary to exclude libc.lib.
')
We start
So, with the projects figured out, now you can get down to business. The compiler is happy for us and welcomes with a lot of errors
C2065: undeclared identifier . We look at the code and see the following picture everywhere:
for (int i = 0; i < max1; i++) { } for (i = 0; i < max2; i++) { }
Of course, you could put
/ Zc: forScope- and forget about it, but we are not firemen and carpenters. Rule pens more than a hundred of these pieces of code, we continue.
The next obstacle was in the graphic element, more precisely in DirectDraw 7. It actively uses the system palette replacement mechanism. And if earlier it was common practice, then since Windows Vista such tricks no longer pass. The fact is that
DWM with Windows Aero closely work with the palette and do not tolerate competition. As a result, many old games suffer from color distortion.
Not being a DirectX expert, I began to look for a ready-made solution and found it in the version of "Cossacks", published on
Steam in 2010. In addition to the ddraw.dll library itself, there is an additional library mdraw.dll in the game folder, which exports the initialization function DirectDrawCreate (). To be honest, I don’t know what exactly the guys from GSC wrote in their
DDemu DirectDraw Emulator library in 2008, but it does an excellent job with its task. Without thinking, I added the
appropriate wrapper to Ddini.cpp and forgot about this issue.
Then there was the question of debugging a full-screen application. Here I was lucky again - the code provided a debug mode in which the game was launched in the corner of the screen in a frameless window with a fixed size. I only needed to bring it to mind, add a change of resolution, process the capture and return the cursor depending on whether the player is in the menu or in the active game and add the corresponding parameters at the start. Now it was possible to conveniently start the game in the debugger with the key / window.
Small retreatThe following are the weird, problematic and erroneous sections of the Cossacks: War Again game code that I encountered during my work. Please note that the purpose of this article in no way is to criticize or ridicule the developers of this game. I believe that “Cossacks: War Again” is an exceptional result of diligent robots and painstaking optimization of a small team of developers who have raised the bar for performance and scope of battles for RTS games very high. Thank you, GSC!
Fun arithmetic
One of my goals was to add settings for multiplayer games, for example, the ability to disable the diplomatic center and the market or limit the available ships in the shipyard. Expanding the interface of the game room and adding the necessary branches in the code, I increased the array PlayerInfo.UserParam [], in which these settings are stored, from seven to ten elements. But I couldn’t test new options in any way - when starting the game, the AI ​​started to control my peasants instead of their own and play for me, while its peasants stood motionless. It's fun, but it won't do that.
The reason for this behavior of the AI ​​was in the following
trick with the ears when copying settings from the game host to the clipboard:
And this is how the PlayerInfo structure is declared:
#pragma pack(1) struct PlayerInfo { char name[32]; DPID1 PlayerID; byte NationID; byte ColorID; byte GroupID; char MapName[36 + 8]; int ProfileID; DWORD Game_GUID; byte UserParam[7]; byte Rank; word COMPINFO[8];
As you can see, by offset MapName + 60 is COMPINFO [8]. Accordingly, as the UserParam [7] array increases, the memcpy () call misses, and incorrect data about which players the AI ​​should play for gets into the buffer. The problem is solved by replacing offset mathematics with a direct call at PINFO [HostID] .COMPINFO.
As a result, I still decided not to touch UserParam [], but to add UserParam2 [3] array at the end of the structure, since the client version is stored in one of the last elements and any change of structure before it is fraught with incorrect definition of versions in the game lobby. And so players with version 1.35 will see that others have an updated version of the game.
What lessons can you learn from this?
- In the structure transmitted over the network, the first element should be the client version.
- Never assume that the location of the structure in memory will be unchanged.
- Write serialization functions, not rely on #pragma pack (1) and byte-wise copying.
Invisible Joe Define
Understanding the mechanics of displaying in-game text messages in order to increase the display time and the maximum number of messages on the screen, I came across an
interesting constant :
#define SIGNBYTE ' ' void ShowCharUNICODE( int x, int y, byte* strptr, lpRLCFont lpr ) { if (strptr[0] == SIGNBYTE) { } else { } }
“So what's the big deal? - you ask - Just a space as a constant. " Well, firstly, a space would be an extremely strange choice to identify something in a line of text, and secondly, it is not a space at all. SIGNBYTE is defined as 0x7F, or the
escape character DEL. And if your browser is sufficiently circumspect and at least shows that there is something between the quotes, then Visual Studio 2015 faithlessly draws
'' , between which the cursor “stumbles” one character.
Please, if you use non-printable characters, specify them in the code in hexadecimal value, and not as a character.
Legal aspect
Every time I am surprised when administrator rights are required to start the game. And every time I think something like “well, how can you program games somehow.” But this time I had the code on my hands, and I collected it myself, and the
UAC window still did not give me rest.
The answer was found quite by accident, when I thought that it would be nice to write in the properties of the executable file that this version of the game is not original and is not supported by the developers. Naturally, there was no manifesto in the project, but there was a resource file
Script1.rc . What was my surprise when after changing the VS_VERSION_INFO block, the game no longer required administrator rights!
It turns out that Windows, starting with Windows Vista, uses a heuristic algorithm to identify applications that may require elevation of privileges. This feature is called “Installer Detection Technology” (see the
article in the Windows IT center ), and it usually responds to keywords like install or setup. But in our case, the culprit was
the CompanyName parameter - if it contains the string "-GSC- \ 0", then UAC wakes up and requires administrative rights.
How to protect your application from such heuristics on the part of Microsoft, existing and coming? No way. Today you are developing games, and tomorrow you are already on a par with Inno Setup and InstallShield.
Guerrilla sscanf ()
I came across this bug when after adding additional game settings when playing records of past games, a synchronization error occurred, i.e. the recording did not go along the course along which the game developed. At the same time, the error appeared, apparently by chance and for a long time I could not find the reason.
I'll skip the boring debugging details and go to the juice itself. The game settings, the map view and the data of the selected nations are transmitted through the file name of the random map, which has the format
RN0 1A3C 12345 0KFH31CJ 4501326.m3d , where
- RN0 : RN prefix and map size (0 - 2)
- 1A3C : PRNG value for random card initialization
- 12345 : Map view (landscape, mountains, deposits, etc.)
- 0KFH31CJ : Nations that Players Have Chosen (0 - K)
- 4501326 : Game Settings (Artillery, PT , etc.)
And now let's look at the
code segment that processes the game settings, reading the random card file name when loading the game record:
int v1, v2, v3, ADD_PARAM; char ccc[32]; int z = sscanf( Name, "%s%x%x%x%d", ccc, &v1, &v2, &v3, &ADD_PARAM ); if ( z == 5 ) { }
The catch here is that the fourth variable indicates the type
% x , while the range of characters in it goes beyond the hexadecimal system and extends to the letter
K. If there are players in the game who have chosen nations with an index higher than F, then sprintf () will prematurely finish parsing and return 4. The parameters will not be interpreted, the AI ​​will have incorrect information about the game and it will make other decisions, which will lead to out of sync.
In addition to this, there is the fact that sprintf () is called exclusively for ADD_PARAM — the other variables are not used anywhere. The solution is relatively simple:
int options = 0; int z = sscanf( Name, "%*s %*s %*s %*s %d", &options ); if ( 1 == z ) { }
The * flag indicates functions that the value should not be stored in a variable. By the way, see how I implemented the coding of 10 game settings in place of the same 7 digits
here .
“Why?” - you ask. But because changing the length of the line with the name of the map file at my own discretion seemed to me not a very good idea (see above in “Merry arithmetic”).The most interesting thing for me here is the fact that the bug manifested itself only when compiled in
MSVC 14 . It turns out that the implementation of the sscanf () function in the standard library over the years has become more stringent and will not continue to forgive such freedoms on the part of programmers.
Memo: Follow first the requirements of the documentation, and not the principle of "working code - the correct code."
Subtleties of language
Localization is a separate topic for any developer, but
this is the first time I saw this:
#define RUSSIAN #define _CRYPT_KEY_ 0x78CD #ifdef RUSSIAN #undef _CRYPT_KEY_ #define _CRYPT_KEY_ 0x4EBA #endif
If this is not enough for you, I suggest to look under the spoiler and see what this “crypto-key” is for.
Do not do this. You are welcome. VOID CGSCarch::MemDecrypt( LPBYTE lpbDestination, DWORD dwSize ) { BYTE Key = (BYTE) ~( HIBYTE( _CRYPT_KEY_ ) ); isiDecryptMem( lpbDestination, dwSize, Key ); } void isiDecryptMem( LPBYTE lpbBuffer, DWORD dwSize, BYTE dbKey ) { _asm { mov ecx, dwSize mov ebx, lpbBuffer mov ah, dbKey next_byte: mov al, [ebx] not al xor al, ah mov [ebx], al inc ebx loop next_byte } }
This is the simple way to screw up localization at the compilation stage. If, for example, the “English” dmcr.exe is slipped into the archive with resources from the Russian version, then all that remains of the game is the access violation error window. Because neither before nor after “isi memory decryption” the contents of the buffer are checked. But if we unpack the archive all.gsc, replace the files and pack it back, then the Russian interface will be waiting for us in the game.
Having looked at this XOR-orgy, I decided to limit myself to the English version, but with the support of the Cyrillic alphabet in the chat. Since all the text is drawn through the game's own fonts, I copied the mainfont.gp resource from the Russian version. It remains only to catch the characters that fall outside the ascii range, and correctly match the letter codes with the “frame index” of this file (the GP format is used everywhere in the game for storing graphics, including animation). Not the most elegant
solution , but it works flawlessly, and on the server in a chat with players under version 1.35, too.
UDP without holes
Unfortunately, the original Cossacks did not implement the
UDP hole punching mechanism , which would allow players to connect to the game rooms, even if their host is located on the other side of their provider’s
NAT .
Fortunately, the comrade, known as [-RUS-] AlliGator, who started and maintains the server cossacks-server.net, allocated some of his time and we agreed on an additional protocol by which the game host will support a UDP connection to the server, and by which the server will be able to communicate the host's external UDP port to players who wish to connect to it.
All implementation details can be viewed in the
UdpHolePuncher class. The connection is
initialized when the game room is created by the host, after which it
keeps in touch until the game starts, sending small packets. This is necessary because NAT can assign a different external port with each new UDP connection, and so the server probably knows that at this point in time the host is available because of NAT on the port from which the packets come.
Corresponding changes were made to the
procedure for processing server commands and to the
RoomInfo structure in the IChat.dll library. The following additional variables are supported when creating a game room:
- % PROF : Player ID. With it, the server can distinguish between hosts.
- % CG_HOLEHOST : address of the server processing UDP packets
- % CG_HOLEPORT : server port on which UDP is listened
- % CG_HOLEINT : the interval at which the client should send packets
After receiving this data, the game host opens an additional connection and maintains it. Those who wish to join receive an additional variable
% CG_PORT when connected to a room. And only if it is not there, the constant
DATA_PORT is used .
Although this whole mechanism has already been implemented in the game client, I haven’t managed to test it yet, because contact with the Alligator broke. Shortly before this, he laid out the
source code of his server for open access -
thank you for that! - so if someone is ready to raise the baton, I will be all for it.
Afterword
The article already came out longer than I had planned, so I will be brief. Although this project took a long time and significantly depleted my enthusiasm for reverse engineering and source code analysis, I am glad that I took it. I am glad that I wrote an article on Habr with a description of my first, assembler, crutch for Kazakov. I am glad that Max
fsou11 came to the comments and laid out the source code of the game for free access. I am also grateful to the
LCN community for valuable advice, explanations, and testing assistance.
References: