Page 1 of 1

Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Fri May 03, 2024 3:32 pm
by Ewe
Hi guys, let me know if you find these types of posts useful. I wanted to give some insight into investigating bugs in NWN2.

TL;DR: The issue of the Pale Master and critical immunity effect being broken is a longstanding problem, originating from the base game itself. This bug is not a result of custom content from bgtscc or any nwnx plug-ins.

Previous attempts to fix this problem did not address the root cause. The core issue lies in how the game treats attackers with the Alertness feat. If the defender’s racial type has a -1 set for their Ruin feat in the racial types 2da file, the attacker is treated as if they have the appropriate Ruin feat. The server does not read/accept the -1 value in the 2da file and defaults to 0 instead, which corresponds to the Alertness feat in the feats.2da file.

While it’s true that bgtscc custom content can occasionally introduce bugs (though we strive to avoid this), there are many base game bugs that are either forgotten or rediscovered. When such bugs resurface, they are often mistakenly attributed to recent module updates, even though they have always been present.

Sometimes, server customizations are believed to have fixed these issues. However, without a clear understanding of the root cause, these fixes are often ineffective. For instance, the attempted fix of giving the Pale Master an effect immunity upon login does not actually resolve the base game bug. To properly test this fix, the tester would need to know that the Pale Master must be a race with a -1 set for their Ruin feat and that the attacker must have the Alertness feat. Unfortunately, this information was not known at the time.


Uncovering a Base Game Bug in Critical Hit Immunity Mechanics

In my quest to understand the mechanics that can bypass critical hit immunity, I discovered three key elements:
1. Epic One Shot (Feat ID 1974)
2. Ranger Archery Style One Shot (Feat ID 1975)
3. Ruin Feats (Death’s Ruin, Elemental’s Ruin, Spirit’s Ruin, etc.)

My investigation began with a search for binary instructions using 1974 (0x7b6) or 1975 (0x7b7) as scalars. I found instructions on the server that compare these values and make jumps based on them. Essentially, the server compares these values to a CPU register. If they match, it jumps to another instruction; if not, it continues executing subsequent instructions. I labeled these two values as 'One Shot' and 'One Shot (Ranger)'.

Image

The next part of my investigation was particularly intriguing. If we didn't jump due to either One Shot feat, we prepared to make another call. This is a familiar pattern if you've read a lot of assembly. We set a pointer into the ECX register and push EBX. ECX is a clobberable register, but it's commonly used to hold the 'this' pointer. So, we can infer that we're storing a pointer to an object and calling that object's member function. We also push some value in EBX.

Image

EAX typically holds the return value from a function. In this case, right after we call the function, we perform a test to see if EAX is 0 or not. The 'test eax, eax' instruction performs a bitwise AND operation on the contents of the EAX register with itself. The result is not stored, but the flags in the EFLAGS register are updated based on the result. If EAX is zero, then the Zero Flag (ZF) is set to 1. If EAX is non-zero, then the Zero Flag is set to 0. The 'jne [address]' instruction stands for “Jump if Not Equal”. It jumps to the [address] if the Zero Flag is 0, meaning it jumps if EAX is not zero. Together, these two instructions check if EAX is not zero, and if it’s not, they jump to [address]. If EAX is zero, they do nothing and the program continues with the next instruction.

Through this process, I learned that this function is checking for a feat id. If the attacker has the feat id, they are allowed to ignore crit immunity. After much struggle, I realized this is for use with the third element above, the Ruin feat line.

Upon further research, I discovered that the Ruin feats are set in a 2da file, specifically the racialtypes.2da file. 2da stands for two-dimensional array, which can be thought of as a spreadsheet. In this file, the feat is set to -1 for many races, though some races do have a value, such as Construct with 2133.

Image

I noticed that the column in the 2da file is called FEATIgnoreCritImmunity. Using this, I searched the binary for strings, hoping to find the string FEATIgnoreCritImmunity within the binary. I found this string in the .rdata section of the program’s memory. The .rdata section in a Windows executable (PE file) is used for storing read-only data, including literal strings. I found the address of this string literal and then searched where it was used.

Image

Image

I found only one instruction which used this address. It's a push setting up the inputs to a function call. This is reading the value in the 2da for the column. This part gets executed once per row in the 2da. What's interesting is the tests on the return value. We do a jump (je) if the value returned is 0 – this is presumably a check on whether the call successfully read or not. Next, we load a value off the stack into EAX, presumably the memory used to store what was read from the 2da, and we jump if the value is less than or equal to 0 (jle). This is key! It means we're only actually reading from the 2da if the value is greater than 0!

So, these -1 values are not read/used when loading up the 2da file. What ends up happening in the engine is an object representing a race is instantiated, and we set members to values based on the contents of these 2da files. Since we don’t actually read the 2da value if it’s -1 for this column, we take whatever the default for this member of the object is during instantiation. Which happens to be 0. For other members, it's correctly invalid feat (0xFFFF).

This is the payoff: due to this bug of not reading the 2da if you set a -1 for a race, the engine creates a race object with feat id of 0 as the Ruin feat which can be used against this race to bypass crit immunity. And 0 is a valid FEAT!!! It is ALERTNESS!!

What does this mean? Well, if the attacker has the Alertness feat and the defender is a racial type with -1 set in the racialtypes.2da for their crit immunity bypassing feat, then the attacker is treated as if they have the correct Ruin feat and can bypass the target’s crit immunity!

This is why Palemaster crit immunity was considered “flaky”! It's because the player race had to be one which has -1 set in this 2da file and also the attacker had to have the Alertness feat. Both of which didn’t always happen in a lot of scenarios.

This is a bug all the way back in vanilla, whenever Ruin feats were introduced. PWs attempted to solve this issue erroneously believing the crit immunity simply fell off or didn’t exist. So they would create a crit immunity effect and apply it to the Palemaster on login. But this actually did nothing. Whether you have the Deathless Mastery feat or a crit immunity effect, you were still subject to the same old bug in vanilla.

It was not the fault of bgtscc custom code, it was not the fault of an nwnx4 plug-in, it was not some insidious memory corruption issue. No, it’s simply the way the 2da file is being read for a column where it only accepts values >= 0.

As for the ultimate fix, I submitted an engine patch which would default this feat to FEAT_INVALID rather than ALERTNESS. An alternative solution for PWs is to just create a dummy feat that is not given out and assign that for every race instead of -1, as any positive number greater than 0 and less than FEAT_INVALID will be properly read and set.

Special thanks to player Saharez who reported this bug with detailed reproduction steps including which specific creatures caused the problem. This sped up the investigation, and we determined all the reported creatures did actually have the Alertness feat.

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Fri May 03, 2024 4:14 pm
by Steve
Magic!!!!

:clap:

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Sat May 04, 2024 2:39 am
by DaloLorn
Steve wrote: Fri May 03, 2024 4:14 pmMagic!!!!
Speaking as an apprentice who just barely managed to follow some of it (but did manage!)... It was an interesting read, and I feel like it might have been educational even to those who couldn't follow the tech parts. Just a couple of things I wanted to ask or comment on:
  • What tools are you using in those screenshots? I recognize your 2DA reader as some spreadsheet program or another, and couldn't care less... but the rest of the tools shown I can identify only by their purpose. :(
  • You could have asked me how Ruin feats are determined instead of going to all that trouble digging them up. :lol:
  • Would it not also be feasible to assign FEAT_INVALID (or rather its numerical value) to races' Ruin feats in racialtypes.2da instead of creating (or recycling) a dummy feat for that purpose?

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Sat May 04, 2024 2:43 am
by Nemni
Awesome job :D

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Sat May 04, 2024 4:44 am
by Aspect of Sorrow
DaloLorn wrote: Sat May 04, 2024 2:39 am
  • What tools are you using in those screenshots? I recognize your 2DA reader as some spreadsheet program or another, and couldn't care less... but the rest of the tools shown I can identify only by their purpose. :(
The debugger is x32/64dbg. Link

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Sat May 04, 2024 12:02 pm
by Ewe
Diving into Neverwinter Nights 2 (NWN2) with reverse engineering is an adventure that blends curiosity with the technical know-how of gaming software. It’s a hands-on task that pushes you to keep learning and discovering. Like a puzzle, every code snippet and game asset you decode reveals part of the larger mystery, honing your skills as you go.

Eager to learn and grow, I’m always scouting for fresh strategies to add to my repertoire. If you have insights or pro-tips, I’m keen to hear them. Remember, mastering reverse engineering is a marathon, not a sprint. It’s okay if it doesn’t click immediately. Stay inquisitive, experiment, and most importantly, find joy in the process to stay keen and avoid burnout. :D

Here are some of the tools that I use regularly when investigating issues like above. The screenshots came from Microsoft Excel and x32dbg (as AoS mentioned!):

1. Visual Studio: A powerful integrated development environment (IDE) from Microsoft.
https://visualstudio.microsoft.com/

2. WinDBG: A multipurpose debugger for the Microsoft Windows computer operating system.
https://docs.microsoft.com/en-us/window ... load-tools

3. x64dbg (the x32dbg part): An open-source binary debugger for Windows, aimed at malware analysis and reverse engineering of executables you do not have the source code for.
https://x64dbg.com/#start

4. Ghidra: A software reverse engineering (SRE) suite of tools developed by NSA's Research Directorate.
https://ghidra-sre.org/

5. IDA: A Windows, Linux or Mac OS X hosted multi-processor disassembler and debugger.
https://www.hex-rays.com/products/ida/

6. Sublime: A sophisticated text editor for code, markup, and prose.
https://www.sublimetext.com/

7. Notepad++: A free source code editor and Notepad replacement that supports several languages.
https://notepad-plus-plus.org/

8. Beyond Compare: A data comparison utility that allows users to compare files and folders quickly and accurately.
https://www.scootersoftware.com/

9. xoreos-tools 0.0.6: A collection of tools to help with the reverse engineering of BioWare's Aurora engine games.
https://github.com/xoreos/xoreos-tools

10. NWN2 Toolset: A set of tools for creating and customizing Neverwinter Nights 2 content.
https://nwn2.fandom.com/wiki/Neverwinte ... _2_Toolset

Re: Dev Log: Crit Immunity Effect and Pale Master Bug

Posted: Sat May 04, 2024 1:35 pm
by Aspect of Sorrow
Would recommend ILSpy as well.

I've seen a few "AI" powered assembly explainers popping up for those that aren't familiar with the arch language, it's probably going to weasel it's way into the various IDEs. Betting on Copilot and VS being among the first.