Update: Code has changed due to evolving needs and patches to the game. Check out the Omnified Cyberpunk 2077‘s hooks at the official source repository for the latest code.
Good day fellow seekers of technological secrets! As I write this, I am in the middle of Omnifying Cyberpunk 2077, which is already shaping up to be quite the spectacle. Normally, at this point during the Omnification of a game, I would be writing an article about the implementation of the Apocalypse system.
But not today! Instead, I wanted to write about some additional observations I’ve made in regards to the basic data structures of Cyberpunk 2077. This includes, I’m proud to say, my newly acquired understanding on how the character data property resolution system works!
But first, let us speak of movement, location, and the coordinates that describe such things.
Location, Location, Location
The movement coordinate system in Cyberpunk 2077 is quite odd. I alluded to this particular feeling of mine in when writing the first article on hacking Cyberpunk 2077, in which I briefly delved into the matter of location. My suspicions and concerns have only grown deeper since that article; and while some statement I made have been shown to be true, I also made some incorrect assertions as well: inaccuracies that I shall point out and correct with this article!
First of all, my statement regarding how the double value type is used for the player’s source-of-truth coordinates still stands. My initial fear, that NPCs do not use a value type of double, but rather float (which was completely based on a hunch and some circumstantial evidence), has now been confirmed.
This is strange, but something I have seen before in games that I’ve hacked. There is a logicality to this design decision, as I find credibility in the argument that a player’s own movement requires greater precision than a particular NPC creature’s movement. This logic, however, was something I do not believe to have been present in the decision to use double for the player and float for NPCs. In fact, it would appear that the developer of this game had no choice essentially in the matter at all.
That’s because the player’s movement is handled by the PhysX3CharacterKinematic_x64.dll library (which is a NVIDIA creation), whereas the code for NPC movement is handled within the confines of Cyberpunk2077.exe. And yes (spoiler alert!), I did happen to find it (“it” being the NPC movement function). I found it while penning up this very article, actually.
So, the developer of Cyberpunk 2077 really had no say in the data type for the player’s coordinates, since they relied on a third-party library to handle it, making it more within NVIDIA’s purview than anything. It’s interesting that they used a NVIDIA library to handle only player movement and not also NPC movement, but I assume (look at me, always giving folks the benefit of the doubt) they had good reasons to do so.
And Now for the Strange Stuff
As I stated before, there is precedence in the player’s coordinates using a different data type than the coordinates belonging to NPCs. It’s never something I’m happy to see, as it almost guarantees I will have a harder time finding a suitable place in code to hook the Predator system into; but, it is also something I refuse to let myself become too aggravated over.
So, let’s get into some truly, strange observations. If you might recall, in my first Cyberpunk 2077 hacking article, I mentioned how I made use of the game’s photo mode in order to locate the player’s coordinates. I wrote about how we can see the coordinates of the camera while in photo mode, which gives us a nice starting place to begin the search.
My statement that photo mode, along with the coordinates it displays, “gives us an initial range of values we can use to guess where our player is”, is actually very much incorrect. Indeed, it took me several searches to find my player’s source-of-truth coordinates, and it was only during the first few (failed) searches that I actually used the photo mode coordinates as the initial values.
It wasn’t until I stopped using them as the initial values for the search that I ended up finding the coordinates. This fact merely slipped my mind while writing the previous article (oops). Why was I unable to find my coordinates when using photo mode’s coordinates as a starting point?
Because player movement and NPC movement are handled on entirely separate coordinate planes.
What does that mean? As an example, assume my player character and an NPC are standing on the very same spot: a spot that, according to my (source-of-truth, remember) coordinates, is located at, let’s say, (120.6, -340.7, 8.24). What do the NPC’s own coordinates say? Something very different: (-1927.38, 2731.26, 7.20).
Yeah, that’s something I’ve never seen before in any game. Even if there were different movement systems involved, the player’s source-of-truth coordinates always existed on the same plane as any NPC’s source-of-truth coordinates. This explains why it was so difficult for me to find NPC coordinates initially.
Finding Normalized Coordinates for the Player
This also complicates matters for my own Omnifying efforts, in particular when I have to implement the Predator system. One of the things the Predator system does is calculate the distance between the player and NPCs, which clearly isn’t going to work if they aren’t even on the same plane.
Fortunately, just because the player’s source-of-truth coordinates exist on a separate coordinate plane (essentially), it doesn’t mean there aren’t read-only values of the player’s coordinates as they would be represented if the player was on the NPC’s coordinate plane. And indeed, there are.
I found read-only location reference values normalized to the NPC’s coordinate plane by moving the character around and basing my search on the coordinates being reported through photo mode. I did the exact same thing in order to find NPC coordinates.
We’ll need a hook in order to grab the normalized coordinates for the player.
Normalized Player’s Location Structure Hook
// Gets a structure for the player's location that contains values // normalized to the NPC coordinate plane. // UNIQUE AOB: F3 0F 5C 83 08 01 00 00 define(omniPlayerLocationNormalizedHook,"Cyberpunk2077.exe"+4A3CED) assert(omniPlayerLocationNormalizedHook,F3 0F 5C 83 08 01 00 00) alloc(getPlayerLocationNormalized,$1000,omniPlayerLocationNormalizedHook) alloc(playerLocationNormalized,8) registersymbol(omniPlayerLocationNormalizedHook) registersymbol(playerLocationNormalized) getPlayerLocationNormalized: push rax mov rax,playerLocationNormalized mov [rax],rbx pop rax getPlayerLocationNormalizedOriginalCode: subss xmm0,[rbx+00000108] jmp getPlayerLocationNormalizedReturn omniPlayerLocationNormalizedHook: jmp getPlayerLocationNormalized nop 3 getPlayerLocationNormalizedReturn:
Excellent. When we need to, we can just grab the player’s normalized coordinates using this, with the actual coordinate values being available starting at
I mentioned earlier that I was able to find NPC coordinates as well. After doing this, I of course looked for and found the NPC movement function, which is needed for the Predator system. We’ll delve into that topic a bit deeper once I get around to writing the article for the implementation of the Predator system into Cyberpunk 2077, however.
And now, let’s talk about the mystery of character data structures in Cyberpunk 2077.
Active Character Vital Statistics and Existentialism
In my first article for Cyberpunk 2077, I discussed my puzzlement over how the game locates some of the various data structures for active player statistics. I found a pointer to a structure I referred to as being the player’s root structure and briefly talked about how the code involved in finding the player’s health structure from the root left me a bit slack-jawed.
I had the desire to figure out how it worked in order to find the location to the stamina structure as soon as possible, since no code I could hook into actually accesses the player’s stamina until stamina is used in some fashion. This is very inconvenient for code that relies on this kind of data, such as Vision display code.
Well, it turns out that the mechanics behind how this particular root structure found a particular type of vital statistic is completely unimportant. That’s because, in the case of the stamina structure (and probably other structures as well), it doesn’t even exist in memory until stamina is used.
How do I know this? Because using a bit of gumption mixed with some smarts, I was able to successfully predict where in memory the stamina structure would be read from before getting the address from any kind of hook. When looking at this place in memory after just loading a save, all I saw was essentially zeroed out or junk data.
Then, I jumped, which uses stamina. Lo and behold, the junk data turned into my stamina values. So, not only is this important player statistic not referenced by code until it is used, it doesn’t even exist in memory until it is used. Never seen that before. Truly, an on-demand statistic.
In light of these revelations, it quickly becomes clear to us that there is no point in figuring out how to locate these active vitals ahead of their use, as they don’t even exist. We needed a different approach.
How Static Character Statistics Are Resolved
On the day I debuted my new Vision application, it quickly became a pressing matter for me to discover how to get my character’s maximum stamina before using it. If you recall in the previous article, I mentioned that I had trouble finding the source-of-truth of maximum values for vital statistics (I wasn’t able to, in fact). The active structures had read-only values, but not the ones that matter.
Static character statistics is a term I’ll be using to refer to character stats that one might find on a screen such as “Stats”, which shows you all your attributes: strength, intelligence, maximum health, maximum stamina, etc. While the structure housing active vital statistics (thus containing a current value) don’t exist until said statistic is used, I found it hard to believe that an actual static dump of a character’s attributes wasn’t somewhere in memory immediately upon a save file load.
And in fact, such a dump does exist. I was able to find it very easily: all I did was do a search for the specific value that I knew a statistic’s maximum would be immediately upon save load, before actually using the stat (so, before jumping, when we’re talking about stamina).
Even though I could find the character’s stats through the Cheat Engine search interface, it wasn’t easy at all to find the particular stat we wanted through code (at least initially). The game appeared to find them using a very similar approach in regards to how it would locate the active vitals; namely, it would have a root, or source structure, and it would find the actual data of interest by calculating an offset that it would apply to a property of said root structure housing a base “data structure” address.
My usual bag of tricks to get around having to know the details were no use here. The offsets one would need to use in order to get a particular data structure shifts throughout a game session. The only way we would be able to find our character’s stats, would be to actually figure out how their addresses in memory are calculated.
A Look at the Character Statistic Access Code
The following image shows the code responsible for reading a particular source-of-truth maximum statistic value for the player. I was able to find the code by searching for the character’s known maximum stamina value immediately following a save file load, and then looking for what was accessing the results.
This code will access a particular statistic one time, and one time only, upon loading a saved game. The access occurs when the game needs to know about it (so, in the case of stamina: when I use it the first time). That made finding it a bit trickier, as I needed to reload again if I’d already jumped (once again, in the case of the desired statistic being stamina).
Subsequent accesses would be made if I opened up my character’s stat screen, however I couldn’t rely on that occurring in order to satisfy my need in discovering where the data is.
Looking back at the image above, as you can see, the stamina is being accessed using a calculated offset applied to a known base containing structure. Very luckily for us, there existed a one-to-one call ratio between a function located a few calls up the stack and the pictured bit of code here. This meant it was very easy to set up a trace to see how exactly the offset was being calculated.
But just because it was easy to trace, did not mean it was easy to understand.
Before I could pin down the specifics behind the data address resolution mechanics, I had to understand, in general, what the hell it was actually doing. This meant taking an example statistic and seeing if I could locate it myself manually (outside of code) after loading a separate save file.
I managed to piece it all together on stream, and will now share the process with you.
The Character Statistic Address Resolution Process
- We are given a root structure that we shall refer to as
root. It can be found by looking at the
rsiregister during the address calculation phase, which is a few lines above the previously shared image.
- Located at
[root+0x10]is a pointer that points to an array that we shall refer to as
statisticsArray, in which the statistic we’re interested in is stored.
- Putting that aside for now, we take a look at the base of the root structure, at
[root], in which exists a pointer to another structure that we shall refer to as
addressBook. This structure contains a sequence of 4-byte integer values. It turns out these 4-byte integer values correspond to static constants that indicate a type of statistic.
- The offset of
addressBookwhich contains the particular constant of interest, and whose address we shall refer to as
addressKey, is a required value for calculating the index of the desired statistic structure in
- The number of entries in this structure is dependent on the state of the game; a particular identifying value for a known data type is not guaranteed to be in the same place at any point in the future.
- The offset of
addressKey, the address within
[addressBook], that contained the identifying value, once known, then needs to have the address of
addressBooksubtracted from it.
- This value is then shifted two bits to the right (essentially dividing it by four) and multiplied by two, giving us the index to the statistic within
- We then find the desired offset that can be used to access the desired statistic within
[statisticsArray]by multiplying the index by eight, giving us the
- Finally, the maximum value for the desired statistic type can be found by accessing
While this process may seem simple enough (or not) when explained, it was difficult to piece together just by looking at assembly code. I am quite proud of myself for being able to do so on my stream! Thanks to everyone who sat through the period of time it took me to figure it out.
The Actual Mechanics Behind Statistic Address Resolution
Understanding the process behind statistic address resolution was one thing, actually implementing it in our own code was another. In particular, the process in which the game locates the
addressKey described above required a bit of pouring over.
After looking at the assembly for a bit, it became clear as to how the game was finding the identifying value within the
[addressBook]: through a binary search! It was doing its job quite efficiently too.
I’m no stranger to a binary search, but I certainly had never written one in assembly. Frankly, writing loops in assembly was something I was needing to practice in general anyway. And I had to do it live. So, with a bit of trepidation, I went ahead and wrote my own.
And, if I may brag, it worked first try (well, second try, due to a small typo). Thank you for letting me brag.
Where did we implement this search? In our new hook for the data structure, a structure which I now refer to as the player’s root structure. The player’s root structure hook is now responsible for retrieving the root structure and any other statistical information we need, all in one fell swoop.
Player Root and Statistical Structures Hook
// Gets the player's root and statistical structures. // Magic numbers: 0x36A is Stamina's house address on Happy Stats Street. define(omniPlayerHook,"Cyberpunk2077.exe"+1A03A28) assert(omniPlayerHook,4C 8B 0E 48 C7 C5 FF FF FF FF) alloc(getPlayer,$1000,omniPlayerHook) alloc(player,8) alloc(playerMaxStamina,8) registersymbol(omniPlayerHook) registersymbol(player) registersymbol(playerMaxStamina) getPlayer: pushf // Isolate the player root structure through pinpointed comparing of comparable comaprables. cmp r13,0 jne getPlayerOriginalCode // This is a constant identifying another statistic that is constantly queried for. // We're going to piggyback off of it in order to find the statistics we care about. cmp r14,0x1c9 jne getPlayerOriginalCode push rax push rbx push rcx push rdx push rdi mov rax,player mov [rax],rsi // Load the "address book" for statistics. mov rax,[rsi] // Get the number of address book entries. mov ebx,[rsi+C] mov rcx,rbx // This is the constant used to identify the stamina statistic. mov rdx,0x36A searchStatistics: // Divide and conquer! Cleaveth the search areath in halfeth! mov rbx,rcx sar rbx,1 // Check if the current element is what we're looking for. cmp [rax+rbx*4],edx // Save our position in case we need to search the upper half. lea rdi,[rax+rbx*4] jae moveToLowerHalf // The current element was below what we're looking for, so we move to the upper half. sub rcx,rbx sub rcx,0x1 lea rax,[rdi+0x4] jmp isStatisticFound moveToLowerHalf: mov rcx,rbx isStatisticFound: test rcx,rcx // Continue the search unless we've found our match or have already looked everywhere // (leaving us with, yup, no match). jg searchStatistics // From my observations, the stamina statistic will always be found, additional logic // that will handle failed searches will be added if needed. mov rcx,[rsi] // rax holds the addressKey, we then have the base address of the address book subtracted // from it. sub rax,rcx // Taking one half of this value gives us the index to the desired data in the statistics array. sar rax,2 add rax,rax // Here's our statistics array. mov rcx,[rsi+0x10] // And here's the address to our desired statistic. Hooray. lea rbx,[rcx+rax*8+0xC] // Let's create a pointer directly to the max stamina statistic. mov rax,playerMaxStamina mov [rax],rbx pop rdi pop rdx pop rcx pop rbx pop rax getPlayerOriginalCode: popf mov r9,[rsi] mov rbp,FFFFFFFFFFFFFFFF jmp getPlayerReturn omniPlayerHook: jmp getPlayer nop 5 getPlayerReturn:
We’re actually locating and hooking into our root structure by piggybacking off another statistic being searched for, identified by the constant
0x1C9. The exact nature of this statistic is unknown to me. What it is matters not; what does matter, however, is that unlike stamina (only queried for once and only upon use), this particular statistic is constantly being queried.
And that’s how we locate data the game doesn’t want to share with us until we happen to jump in the air like idiots.
Apocalypse Write-Up Is Next
I’ve already implemented the Apocalypse system, but I’ll hold off on writing about it until the next article!
The latest source code can be found on the Bad Echo technologies repository. Don’t forget that you can come chill with the community and get a nice notification when my stream goes live by joining my Discord.
Hope you found this interesting, and/or learned something.