This summer there was another game from the Batman Arkham series, in the PC version of which there were so many bugs that an unprecedented decision was made to remove it from sales. I decided to see what was so terrible there.
Among others, there is such a bug, at first glance, random: sometimes, when Batman jumps from the roof, instead of planning exactly, he heats for a while, then takes a rather deep dive, and only then levels off. As a result, at best, a very undesirable loss of height, at worst, you can scare away enemies, or even fall on their heads altogether. ')
The screenshot shows this moment: instead of flying forward, Batman turned upside down, demonstrating utter disregard for what is happening. A similar bug was in the previous game (Arkham Origins), and it has not yet been fixed . Apparently the same curve code was transferred to the new game. Let's try to find out what mistakes programmers make in games of this level, and fix them. In Origins, the bug was not so unpleasant: Batman burst out, but did not lose the height. And then he falls down, which is very annoying and interferes with the game. To begin, let us try to clarify the conditions under which the bug appears. This is not so easy. Trying to repeat the glitch, you can jump hundreds of times from the roof, and everything will be fine. However, as soon as you start to play, it appears, and at the most unfortunate moments. Here's what it looks like:
After numerous experiments, it turned out that the bug manifests itself only if you jump at a certain angle, and even then move the mouse. It is not surprising that at first glance the error seems random. When you jump from the roof, you usually look at the place where you are going to fly. During the flight, the direction is corrected by the mouse, and with a certain movement, and only at the very beginning (about a second after the jump), this bug occurs.
Find some variable relating to the flight, for example, speed. First, we take a snapshot of all the memory at the moment when Batman flies slowly. Then we dive (speed increases) and look for values ​​that have increased. Then we align again (the speed decreases) and look for among the previously found values, which now, on the contrary, have decreased. After several repetitions, it is usually possible to find the desired memory location. Then by it (using the hardware breakpoint) we find the code that changes it. Here he is:
It turned out to be the middle of a long subroutine, almost half of floating point calculations. Apparently this is the code that fully defines the entire flight of Batman. Let's start to study it on the sly. As the debugger tells us, the speed changes when the commands are highlighted in green, which means it is contained in [rdi + 0000144C], and before that it is calculated in the xmm8 register (highlighted in red).
Check it out. Replace the subss command with addss, and now the speed will not fall, but only increase. It was a funny ride: you can run at great speed through the streets, even faster than in a batmobile, without losing height. Getting into corners at this speed becomes difficult. In a normal game, this is of course impossible, that is, we made sure that we really found the code responsible for the flight of Batman.
Now let's try the opposite. Replace both commands with "subss xmm8, xmm8", that is, subtract the register from itself, as a result, the speed should be equal to zero. Run the game. And here we find that before the flight there is a transitional stage, just that first second, from the moment when Batman takes a step into the void, before he fully spreads his wings. At this moment, the team we changed is triggered, and Batman freezes in the air.
It turned out to be very convenient. Now you do not need to constantly jump from the roof at a certain angle, while trying to pull the mouse at the right time to reproduce the bug. We caught that very moment, the transition between the jump and the flight, when it occurs. Now it’s perfectly clear: while the camera is looking above a certain angle (approximately on the horizon line), everything is fine, but as soon as it goes below, Batman goes down with it, and after that any movement of the mouse upwards leads to a bug.
It is time to look for an error in the code. Attempts to disconnect pieces of the program at random did not give anything. Batman freezes in ridiculous positions, flies not where it should be, turns like a top, but the bug does not disappear. It remains painstakingly to analyze what is stored and what calculations are made. I start to look through the same subroutine from the beginning, and soon there is such a fragment:
None other than three 16-bit values ​​(in the registers eax, edx, ecx) make the extension of the sign. This immediately suggests three dimensions. Well, let's see what's in the registers? Small numbers with a different sign. Move the mouse - the values ​​change. Apparently this is a reaction to the manipulator in the form of vectors. Let us zero one of the values, and make sure that Batman now does not react to mouse movement up and down, but only to the sides. Specifically, this is the first number of the three, and it is stored in memory in the [rsp + 70] cell (highlighted in green). This will help us repeat the bug at any desired moment.
Having tried to carry out the program further in steps, I quickly became entangled in conditional transitions and calculations. However, having reached a familiar place where speed is calculated, I noticed in one of the registers a number similar to the previously found vectors. Only now it turned out to be not a mouse, but Batman himself, namely, the angle of his inclination to the horizon, the pitch, if one can say so. In the normal state, it has values ​​from -2548 to -15000, that is, from a position just below the horizon, to almost vertically down.
Now reproduce the bug. And it turns out that when Batman sausages, this register is not a normal orientation, but some arbitrary number, and not even a 16-bit one, but a 32-bit one! It explains everything. Somewhere in the calculations an error occurs, an overflow, or something like that, and as a result, Batman shakes like an autumn leaf in the wind.
To test this, we simulate the situation. Find the place in memory where the “erroneous” value is stored, and replace it with a 32-bit constant. We get Batman, frozen in flight at a fixed angle.
In response to the movement of the mouse, it turns, spreads its wings, moves its head. But its slope to the horizon remains constant, and wrong. Batman cannot fly like this, he was not going to fly like that, an error in the calculation leads to this. An indefinite number is added to the current angle, and although it is adapted to the desired limits for rendering, it is stored in memory in an indecent form. In each subsequent cycle, they take out this number, take the lowest 16 bits from it, and get what the hell are. Therefore, the process of "jerking" continues for quite a long time, until by some miracle the number still falls into the correct range. Then Batman calms down and starts flying normally.
And here I am again wandering in the code, trying to determine where in the calculations something went wrong. Of the entire 9 kilobyte subroutine, the search section narrowed down to about 1 kilobyte, but it’s still hard to understand here. After a while, I began to lean toward the idea that it would all be too difficult, when I suddenly noticed that in many SSE registers there is the so-called Nan (not much) . Wonderful. So what's the matter! Somewhere in the calculations it turned out Nan, and it is worth non-number to appear once, as all operations with his participation will also lead to Nan, and they went. Now it’s enough for us to go through the whole cycle in steps, carefully watching when Nan appears for the first time, and we will find what we need:
Here after the highlighted call, it arises. At this point, xmm1 = 0.5, a xmm0 = -0.01. We go inside, and we find ourselves in msvcr100.dll, the function powf (exponentiation), that is, in this case, the root is taken from the negative number. Where did it come from, and why is it so rare? After a detailed study, we managed to find out what is being calculated here. Consider the example of a normal situation:
In xmm0 we have 600 - this is Batman's cruising speed. From [rdi + 0000144C] in xmm2, 731 is loaded - this is the current speed of Batman (note that this is the same offset as in the first code fragment, considered at the very beginning). Then they are subtracted (subss xmm2, xmm0), it turns out 131. Then 2200 is taken from [rdi + 000013EC] - the maximum speed, multiplied by a constant, xmm0 (600) again subtracted, 1270 is obtained. Divide the first difference by the second (divss xmm2, xmm1 ), we get 0.1031. Now it is multiplied by the coefficient previously calculated in xmm14 (it depends on the angle, but now it does not matter, the main thing is that it is always positive), as a result we get 0.0268. Further, we already know, we calculate from this the root, everything is fine.
And now what happens when Batman jumps from the roof. This whole branch is executed only if you move the mouse up to calculate how much you can turn up for the next time slice. At this point, the speed of Batman was equal to 599. 600 is subtracted from it, it turns out -1, the result of the whole formula is negative, and take the square root. This is where Nan comes out. All further calculations obviously go down the drain, “non-numbers” multiply, translate into a 32-bit integer, and as a result we get what we saw.
Find the same place in the previous game - Arkham Origins. It turned out there is almost the same thing: Batman's cruising speed is also 600, so the subroutine was found almost immediately. True planning angle is slightly lower, calculations go to FPU, and root is calculated by another call to msvcr100.dll (because of double precision)
Let's summarize the research. Batman's flight was conceived in such a way that his speed should never be less than 600. When he flies smoothly, his speed is 600. If he leans down a bit, the speed increases. When it is aligned, it gradually decreases to 600, but should never be less. As a result, programmers thought that the difference in speeds would never be negative, and quietly calculate the square root from this.
They made 3 mistakes. They did not take into account that under certain conditions the speed calculated by their formula may fall below 600. Then they did not check that they take the root from a negative number. And then they calculated and stored the result in a 32-bit variable, and they took only 16 bits from it, as a result, the root of the negative number can be taken only once, and then Batman will be sausage for about ten seconds.
By the way, it is interesting that when managing a gamepad (even on a PC) this bug does not exist. I think, because the gamepad does not allow making such sharp movements up and down, like a mouse. Therefore, most likely, on consoles this error also exists, it just does not manifest itself.
How to fix the error? For example, for Arkham Knight, before calculating the root, we add the command “maxps xmm2, xmm9” (maximum), since we have zero in xmm9, the result will always be positive. For Origins we use the fabs (module) command. We start the game and make sure that there are no more glitches: Batman does not twitch, but flies where necessary.
You can even write scripts that will find the code by a unique sequence of bytes and fix it. In principle, for Origins, you can directly change .hehe-shnik, but Arkham Knight is protected by Denuvo, so the code can only be changed in memory when the game is already loaded, which is what the Cheat Engine does.
Those who do not want to climb into the code of the game, can simply not touch the mouse at the beginning of the jump, or at least not move it up. Well, hope that someday the developers will fix it. By the way, while I was preparing the post, they released another patch, after which, it seems, everything became even worse. For many, the game began to fly, and has not yet been returned to the sale. However, now I am not surprised.