In the courtyard of the end of 2016, finally,
having caused a storm of delight among fans, the third part of “Cossacks” came out ... And I still had no rest a strange mistake in the network component of the first part. The oddity was that when creating a game on a local network, only two people could start the game normally. With three players, the download indicator grew painfully slowly, and starting from four, it remained at 0%. Well, let's start the investigation!
Error
The symptoms of the problem are several. The “max ping” value that is displayed in the game room is too high and grows in proportion to the number of players. Although all players are connected to the same switch and the usual icmp ping gives consistently less than 1 ms, delays of up to 350 ms are displayed in the game room. If there are only two players in the room, then “max ping” is first ~ 90 ms, then drops by seconds to ~ 10 ms.
The second symptom is the display of the warning “no direct connection established with:
player name ”. The three of us get a chance to get it somewhere around 50%, and if there are four players in a room, then it is shown constantly. Although this warning can be ignored by holding down the Ctrl key while clicking on “Start”, this indicates a certain “problem of perception” of the quality of the connection from the side of the game.
')
The third symptom is a slow download speed. When all the players clicked "Start", the game host displays a percentage indicator. It increases in steps of ~ 8%, reaches 100%, and only after that you can start the game. Considering that this indicator primarily displays the progress of transferring a file of a randomly created card from the host to the players, and the size of this file for a regular card is approximately 3.5 MB, then even 5 seconds of loading on a gigabit local network is a problem. And in the time it takes to “load” three players, you can copy the entire game folder.
Starting from the end
With your permission, I will spare you the details of the beginning of this reverse and the problems that have arisen. I can only say that I was wrong in thinking that it would be fun to disassemble the network component. Calling functions through pointers. Multithreading Working with sendto () and recvfrom () at the same time using DirectPlay. The absence of any intelligible documentation for this dinosaur, not to mention debug information. My kung fu here was clearly not enough.
So we will work in the old manner. We open everyone's favorite, the
most beautiful program , press Alt + T and look for "no direct connection". We find the region of memory containing the string, then through
xref we exit directly to the pointer, and then to the volume function of 32 kilobytes. Among other things, it initializes the elements of the game room interface, processes the messages and arranges the lines before they are displayed on the screen. Let's call it LanLobby (). Below you can see the algorithm for deciding whether to show a warning to a player and whether to deactivate the "Start" key:
Note: The disassembler listings shown in the article were processed to improve readability.
- Called a small function-loop SomeI changing (). Looking ahead, I will say that there is a call to a similar function in her body, let's call it ImportantIteration (). If the latter returns zero at least once, then SomeIteration () immediately leaves the loop and also returns zero. In this case, the transition is not made and we risk receiving a warning.
- Next, check the state of the Ctrl button. If it is clamped, the “Start” button will not be disabled.
- Deactivating the "Start" button. The variable hStartButton is initialized above the result of the function with the talking name "addVideoButton".
- The timer is checked against the passage of two seconds. Above the PreviousTick variable is assigned the current value of GetTickCount () each time the number of players in a room changes. If less than two seconds have passed since then, a transition is made and a warning is not displayed.
- In the end, the warning line is copied to the line to be displayed on the screen as the status bar of the game room.
Conclusion: After taking the player into the room, the game gives itself two seconds to establish the connection. Then, if the ImportantIteration () still returns zero, a warning of the absence of a direct connection is displayed.
Stumbling block
At this stage of the reverse, it was not clear to me what was happening in ImportantIteration () and I decided to find the layout of the load indicator line and continue to work from there. Earlier, in a vain attempt to analyze the max ping calculation algorithm, I noticed that all lines with numeric variables are first compiled via sprintf (), and then placed on the target line. Given the syntax of the format string, we look for the text “%%” and find a match in our LanLobby (). This is the code snippet that decides whether to show a number in percent in the load indicator field or a tick signaling the “readiness” of the game:
It turns out that the result of the function GetLoadPercentage (), if it is less than 100, one to one is transferred to the screen. The second branch must be drawing a tick. I wonder what is calculated in this function? GetLoadPercentage () consists of a loop that goes through an array of player data and ... oops!

And here the ImportantIteration () solves the issue. Yes, we were not mistaken with the name. Considering arithmetic, ImportantIteration () returns the player’s loading status in the range from 0x00 to 0x0C, that is, on this “scale” there are only 12 steps. Now it is clear why the percentage indicator increases in steps of ~ 8%.
Now that we understand what ImportantItration () should do, let's see where it is still used.
In addition to the known calls for checking the quality of the connection and calculating the percentages, ImportantIteration () is called in two cases: when formulating a warning about a bad connection — there it is used to make a list of players for the status bar — and to check if all players are ready. The latter is carried out in a function with a small cycle that runs through all the players and compares the load level with 0x0C. If even one player has less, then the cycle returns zero. We can safely assume that the result of the last function directly affects the activation of the “Start” button on the host and the ability to start the game.
Decision
So, the simplest solution is to make corrections to the logic, depending on the result of ImportantIteration (). Fortunately, in all cases, transitions are carried out with a positive result, that is, we only need to change all transitions from conditional to unconditional. In the case of the so-called "Windows 7 version" of the dmcr.exe file, the entire patch can be described as follows:
| | | ---------+-----------+-----------+----------------------------- 0x00CEEA | 0x7D | 0xEB | 0x098792 | 0x0F 0x8D | 0x90 0xE9 | 100% 0x09C389 | 0x0F 0x85 | 0x90 0xE9 |
And since all changes relate only to the logic of the game room for games on the local network, and the network message itself takes place in parallel and does not depend on it, there is no need to fear any side effects. The file of a randomly created card is distributed over the network almost instantly, and the room allows you to start the game without any delay. Here you can wash your hands, but ...
What is in the box?
After all, it is curious how the game room determines the status of loading players. Meet the ImportantIteration (), the hero of today's article:
Here you can notice a few interesting things:
- One of the two parameters is passed through the ecx register.
- All the necessary data is in memory next to each other, so why do we need to store their addresses? Instead of pointers Pointer_A and Pointer_B, we will have Pointer_A and (Pointer_A + 4).
- The memory region pointed to by the PlayersDataStruct pointer parameter is written in parallel streams and does not depend on what happens in LanLobby ().
When trying to understand what is happening in the specified region of memory, something similar to an array of structures was discovered. Each element has a size of 0x84 bytes, and inside it stores among other things the player's name, the name of the random map file used, the version of the client of the game, the value of the ping, several variables of Boolean type, as well as several integer values and / or pointers. All attempts to track records in this region run into memcpy () calls from other threads, and it is quite difficult to statically analyze it - xref gives 82 82 references, and except for initialization with the value 0x12345678, all of them are readable. Those. the address of the structure is loaded into registers, calculations of the address of the desired element are carried out, and only after this reading, writing, calling other functions with a pointer as a variable, or all of the above listed immediately.
In the end, I just put the tracking on the record in a certain part of this structure, and in the body of ImportantIteration () I added several conditional breakpoints. I turned off the debugger itself, and indicated a small
IDC function as a condition that displays the contents of the register in the message window:
Message("EDX: %08X\n", EDX), 0
After that, I started the game and connected to a host on the local network for a few seconds. Then I stopped the debugger and, after reviewing the tracking results and the contents of the message window (“Trace window” and “Output window”), I came to the following conclusion:
The region of memory, from which data is extracted, among other things, and ImportantIteration (), is constantly changing. However, some values are first reset before recording new data. Probably somewhere in the processing of network messages before storing new information, this region of memory is first reinitialized. And since we have DirectPlay and multithreading, these zeros may well be there just during the execution of our cycle, which leads to the behavior described at the beginning of the article.
Afterword
So, what moral can be learned from this fable?
You do not need to be a Yakuza to call in to the Yakuza. It is not always necessary to dig out the roots of a problem in order to solve it. By the way, in the process of reversing, a piece of code responsible for “auto-losing” when the game was minimized for more than a couple of seconds caught my eye. So now you can safely change the music during the game. True, I'm not sure about the benefits of such a patch for the gaming community, as it may facilitate the manipulation of the game using third-party programs.
Finally I would like to return to the issue of timing of the game, raised in the
first article . Then I did not fully understand the role of the functions QueryPerformanceFrequency () and QueryPerformanceCounter (). As Andrei
Smi1e correctly
noted , it did not seem to influence timing. Now I can say for sure that these functions are used in the game room of the first "Kazaks" as a pseudo-random number generator to create a random map file and / or the name of this file, and the game itself is timed exclusively via GetTickCount ().
That's all, see you soon!
Links