Alright.. Been studying the Divinity Engine these past few days and learned quite a lot. The below is
my interpretation and it's not to be considered a manual, thorough explanation or how the developers intended this to work. I'd appreciate it if you consider it for what it is and not make of it a mistake-free approach. Text is subject to criticism, adjustments and any feedback from your end. Note that I will still be using
4.1.83.3931. I know the game has updated, though I won't spend time again to re-offset everything to the latest build. That's life, suck it. You can aob it later. Want my executables, let me know and I'll upload them somewhere. They work fine, as the update only changed the executables, not other data parts (hope developers don't add stupid checks to force you into running only the latest).
First-up, I tried to understand how the
HealthComponent is acquired and where from. There isn't any spot I could find myself (perhaps there is, I dunno, haven't found one) where Player information is stored like in other Engines. As such, I've set an exception breakpoint on the HealthComponent structure and let the Caustic Brine damage me. And I got this list, like I mentioned earlier in
this post (note those addresses are from 4.1.83.2651; if you want them fresh, just find your HP, then find out who writes to it, navigate to the structure base, select a big ass range and set an exception breakpoint; get hit and you should see that list).
So the function in which this is obtained is here:
It runs the moment you get any type of damage. It runs for the enemy as well!
Before I continue with the HealthComponent acquiring logic, I took another approach. I decided to scan for a random value/address, 8 bytes (QWORD) when I
clicked to select each member of my party:
Why? Because I am most certain there is an address, somewhere, where, once you change playable character, something will change. Considering posts from previous page, I was looking for values that resemble those
hashes, like
here. After doing anything possible in-game to narrow down the number of addresses, I got his many:
None of them in there are static, so don't bother checking them out on your end. Point of the matter is I started inspecting them. And I found this in one of them:
That's my Player name. So then I scrolled up a bit to check which structure I'm in (forming up the member-functions ptr) and got this layout:
So that led me to this layout:
Quick pause - if you want to find your spot, then simply scan the game's memory for this array:
Code: Select all
44 00 D0 24 00 00 00 00 53 00 75 00 6E 00 42 00 65 00 61 00 6D 00 00 00 07
Where you have to make the following adjustments:
- 44 00 D0 24 00 00 00 00 - stay the same
- 53 00 75 00 6E 00 42 00 65 00 61 00 6D 00 represent "SunBeam" in UNICODE; change it to your player's name in unicode
- 00 00 07 - the last byte represents the string size -> len( "SunBeam" ) = 0x7
So when I select (click the portrait to select character)
Tav, this happens:
The id is 0x0001000000000396.
Then when I select
Us, this happens:
The id is 0x000100000000037E.
Then when I select
Lae'zel, this happens:
The id is 0x00010000000003C0.
And who is our base pointer, based on its member-functions? Looking at the references, I gathered it fiddles with a
BindingExpression:
Then I thought "but what if I don't have 3 characters in my party to click the portraits of? I cannot unselect a character and re-select it by clicking its portrait". So then I've set an
access breakpoint on that id to see what's reading it/using it. And got this:
And out of it:
You may notice I've already marked various lines with comments as I've parkoured the analysis. At that prologue, if you check RCX, you will see this:
What I've pixelated is
my user id Notice that I've also started labeling some member-functions based on what the errors they'd throw internally:
So.. in short.. when you start a game, there's a client and a server created. The communication is done through messages and there's a handshake occurring to validate them, hence the lag you're experiencing when you do things in-game. As in damage happening over 0.5s or 1s from the actual action you see. Much like how Diablo 3 works, syncing server and client's actions. All server related functions are called
esv (e.g.: esv::GameServer::<function_name> -> esv::GameServer::OnNewIncomingConnection). All client related functions are called
ecl (e.g.: ecl::JoinProtocol::<function_name> -> ecl::JoinProtocol::ProcessMsg). There are some local server functions called
ls. Or "listening"? No idea on that part. Anything you do in-game is a
message sent between client and server. Whether or not it's a multi-player game or a local one, there's
always a server. Not the full-fledged meaning of a server, as the regular user knows. You catch my drift. Anything in the game world has a GUID. This isn't unique to the object it represents, you can have 2 identical objects with different GUIDs. They look like this:
da072fe7-fdd5-42ae-9139-8bd4b9fca406 in the Engine itself.
Why I dragged you through all of this.. the id that you see changing as you click on Character portraits is used in the HealthComponent address acquiring. To get the selected Character's id, we need to find our Player. And that player information can be retrieved from esv::GameServer
So.. esv::GameServer -> Player -> selected Character_id -> any Component we need for said Character.
Now.. if you head out of the function I pointed out earlier..
..you will land here, where I have additional comments:
Notice the "class esv::Character>(void)"? That's why the stuff I presented on the previous page is useful; the DWORD value and how it ties back to what it represents?
Handy now, is it?
Code: Select all
00007FF7F0618891 | 48:8B5C24 28 | MOV RBX,QWORD PTR SS:[RSP+28] | id -> 0x00010000000003C0 (Lae'zel)
00007FF7F0618896 | 4C:8B05 8B275503 | MOV R8,QWORD PTR DS:[7FF7F3B6B028] | 0xFFFFFFFFFFFFFFFF
00007FF7F061889D | 49:3BD8 | CMP RBX,R8 | if id is invalid, skip
00007FF7F06188A0 | 74 6B | JE bg3_dx11.7FF7F061890D |
Then:
Code: Select all
00007FF7F06188A2 | 48:8B05 17196403 | MOV RAX,QWORD PTR DS:[7FF7F3C5A1C0] | g_World
00007FF7F06188A9 | 48:8B50 18 | MOV RDX,QWORD PTR DS:[RAX+18] |
That is the pointer I've talked about in the first post of this topic: leading to a
World instance (EntityWorld).
Then the next part..
Code: Select all
00007FF7F06188AD | 48:6305 40B66503 | MOVSXD RAX,DWORD PTR DS:[7FF7F3C73EF4] | "class esv::Character>(void)"
00007FF7F06188B4 | 48:8D0C40 | LEA RCX,QWORD PTR DS:[RAX+RAX*2] |
00007FF7F06188B8 | 48:C1E1 06 | SHL RCX,6 |
00007FF7F06188BC | 48:038A 38160000 | ADD RCX,QWORD PTR DS:[RDX+1638] |
00007FF7F06188C3 | 48:8B09 | MOV RCX,QWORD PTR DS:[RCX] |
00007FF7F06188C6 | 48:8B01 | MOV RAX,QWORD PTR DS:[RCX] |
00007FF7F06188C9 | 41:B0 01 | MOV R8B,1 |
00007FF7F06188CC | 48:8D5424 28 | LEA RDX,QWORD PTR SS:[RSP+28] |
00007FF7F06188D1 | FF50 40 | CALL QWORD PTR DS:[RAX+40] |
00007FF7F06188D4 | 48:85C0 | TEST RAX,RAX |
..gets, as result in RAX, this address (for me): 00000238E5006D88. Looking at it in dump, I see the following:
Then this:
Code: Select all
00007FF7F06188D9 | 48:B9 EDDEEDF151A5C1DE | MOV RCX,DEC1A551F1EDDEED |
00007FF7F06188E3 | 48:3BC1 | CMP RAX,RCX |
00007FF7F06188E6 | 74 19 | JE bg3_dx11.7FF7F0618901 |
00007FF7F06188E8 | 48:8D48 F8 | LEA RCX,QWORD PTR DS:[RAX-8] | notice the slight adjustment of our RAX
00007FF7F06188EC | 48:85C9 | TEST RCX,RCX |
00007FF7F06188EF | 74 10 | JE bg3_dx11.7FF7F0618901 |
00007FF7F06188F1 | 48:8BD7 | MOV RDX,RDI |
00007FF7F06188F4 | E8 D7CBBFFF | CALL bg3_dx11.7FF7F02154D0 |
00007FF7F06188F9 | 84C0 | TEST AL,AL |
00007FF7F06188FB | 0F84 E8000000 | JE bg3_dx11.7FF7F06189E9 |
So 00000238E5006D88 adjusted to -0x8 -> 00000238E5006D80.
Now.. if we go back to the function where HealthComponent is acquired and try to combine what we learned of with the function flow, I see the following:
RAX and RDX contain the same value/address -> 00000238E5006D80
From here, RBX is populated with [RDX+10], which is:
So what's put into RBX is 0x02800001000000E4. This RBX is then used here..
..and continuing here:
Which seems to get a pointer, in my case: 0x00007FF3DDD18770.
Then:
Which gets this value 0x3AC0000100000078. This is checked against 0xFFC0000000000000 here:
Code: Select all
00007FF7F025A992 | 49:3BC4 | CMP RAX,R12 |
00007FF7F025A995 | 74 2D | JE bg3_dx11.7FF7F025A9C4 |
00007FF7F025A997 | 48:8B8F 38160000 | MOV RCX,QWORD PTR DS:[RDI+1638] |
Then the rest of the code in the branch and our HealthComponent in RAX
Much to post, last part ambiguous, I know. Will post a script so you can get the idea soon.