Uncapping Critical Hit Rate


This is probably something really simple for people who actually know what they're doing, but I figure I'd toss it up here anyway for casual users. It's also helpful for a sort of very basic tutorial for people who want to mess with the assembly code of the game but are having trouble figuring out how.

Goal: removing or altering the cap for critical hit rate determined from the LCK stat

If anyone's modified their client to have a LCK stat cap notably higher than the base game's value, it quickly becomes obvious that the effect doesn't have the desired result. If the stat scales linearly, going to 200 LCK should increase critical hit rate to ~40%, but it becomes very obvious that this is not the case with minimal testing.

It's recommended, to make the effect the most obvious while testing, to use a class whose LCK cap is modified to be above 100. Fire up Cheat Engine and first thing find your LCK stat. This is very basic Cheat Engine usage, so I won't walk through every step. It's a direct number and doesn't require many iterations; a unit on and off should do it.

Once you've got it pinpointed and put the entry in your table, right click and "Find out what accesses this address". This is going to look at the code that's running and anything that accesses it will pop up in the debugger window. Multiple lines will pop up, most of them running multiple times per second. Sadly, if you try attacking something as a test, it won't make anything new pop up, either.


Now that you've got that, it's time to look at the code itself for each function and see what's happening around it. Click one of the lines of code, and then click "Show disassembler" on the right. If you click on a line of clode and then "Insert Breakpoint" the code will stop running (the game will freeze) and it will pop up a secondary window on the right that shows you the state of memory at the time. Remember to remove the breakpoint and hit run at the top so the game doesn't stay frozen and the server boot you.


However, you can tell just from the fact that these lines are being read multiple times per second that this isn't where it's checking what the LCK stat is when determining if you crit. So one of these lines of code is writing it somewhere else where it is checking that. Let's test with the above. The line of code we're highlighting is this one and the one after:

movzx esi,word ptr [esi+0C]
mov [edi+00002D6,si]

This is moving a value from the program into esi, and then writing this value to a location referenced by edi. If you look at those values, we can determine where this is supposed to be. The [esi+0C] is where it's checking your luck, since it accessed that spot in the memory, so let's figure out what edi would be by checking the register value on the right. In the case above, this would be A8D7970+2D6, or A8D7C46. Back in the Cheat Engine main window, there's a button above and to the right of your table to "Add Address Manually". Use that button and enter the address you just got.

That doesn't look right...

Ah, right. It's a small value. Let's change the type to 2 bytes.

That's more like it. You can see that it's the same number as your LCK value and quickly updates if you change it. Now, once again right click and see what accesses this address.

One function happening a lot, huh? Well, let's try attacking something.

There we are. We now have something that looks at this value when we attack something. So something somewhere in that area of code is how the game is determining whether or not you crit. Starting at the access function, we run into the following block of code:
%Prior to this code running, the floating register has two values already in it
%The first one is the LCK cap value
%The second one is the threshold to crit.
movesx eax,word ptr [ebp+00002D6] %This is reading the LCK check
mov [esp+14],eax %This stores the value as esp+14
fild dword ptr [esp+14] %This stores the value as a floating point register
fcom st(0),st(1) %This compares your luck check to the cap value, and writes to the status register
fnstsw ax %This stores the status register as eax
sahf %This takes eax and puts it in flags
fxch st(1) %This swaps the first two register values, putting the cap first
jna 02 %This uses the flags (from sahf) to jump past the command if the value is under cap
fst st(1) %This overwrites your LCK with the cap if it didn't jump
fstp st(0) %This clears out the initial value
fcompp %Compares the current check value to the crit threshold
fnstsw ax %Stores comparison results into ax...
sahf %...And from ax into flags.
jb 2C %Jumps past the crit program if you don't crit.
lea eax,[esp] %If you break here, you'll see it only triggers if you crit.

I went ahead and annotated the code's specific functions above, but the best way to figure these things out is to test what code runs under various conditions, and to check the status of registers when that happens. The important ones for the above to test and see are "jna" to see the values it's just tested against and the status of flags, and the "fcompp" to see what it's checking before it clears the values. Let's look at jna first. Put a breakpoint on that line and try attacking something (remember to run the code again after).

Once your breakpoint triggers, you can see there's an arrow to the right of your Registers. Click that to pul up the FPU window.

Change the first dropdown to "FPURegisters" to see the floating point values it's working with.

In this example, you can see that the game's cap value (100 LCK) is first, followed by your current LCK, and lastly is the decimal value that you need to beat in order to crit. And if you let the code run and it doesn't jump, it's going to overwrite your LCK with that cap, then continue in its check.

Now you can decide how you want to handle this. If you want to change what the cap is, you can go further back in the code and find when it wrote the last floating point value. Coincidentally, it seems to be writing it immediately above the line you came in on with an fld statement. So you can look at where it's peeking and change this value.

Alternatively, you can just remove the cap entirely. The simplest way to do this is just by removing the line of code where it overwrites your current LCK with the cap. The "fst st(1)" line is the one that does it, so if you NOP that line out, your LCK will function as if the cap wasn't there.

Conveniently for removing the cap, Cheat Engine has a function to do just that. Right click the line in question and choose "Replace with code that does nothing". After this, put a breakpoint on the fcompp function a little ways below (the "pp" on the end means it's double-popping and thus clearing the checked values), and try swinging at something again...

And voila, it now has our LCK stat as st(0) to compare instead of the capped value of 100. Feel free to run some tests and see that when your LCK is above the threshold value in st(1), it results in a crit.

Now that you know what code you need to cut out, you can go alter the original psobb.exe file to make this a permanent change. As a rank amateur, I just used xdvi and searched nearby code until I found one in just the one spot, and manually replaced the "DDD1" with "9090".