Baldur's Gate 3

Add topics here with methods, analysis, code snippets, mods etc. for a certain game that normally won't make it in the Tables or Requests sections.
User avatar
asthebloody
Table Makers
Table Makers
Posts: 187
Joined: Fri Oct 20, 2017 2:03 am
Reputation: 107

Re: Baldur's Gate 3

Post by asthebloody »

Image

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

With the information from previous page you can do this:

Image

Whenever you move the mouse over some shit that can't be killed (aka "infinite HP"), you will see this:

Image

0xFFC0000000000000 is the Engine's default "invalid" value :)

bg3_dx11.CT
1.0b
(10.94 KiB) Downloaded 463 times

EDIT: Hmm, those are just visual. Meh :D

BR,
Sun

tfigment
Table Makers
Table Makers
Posts: 273
Joined: Sat Apr 15, 2017 12:49 am
Reputation: 254

Re: Baldur's Gate 3

Post by tfigment »

Looks like all of those strings are just references to tracing statements in the those specific function. Useful I guess. Are Components the underlying data or the visual indicators or something else? Have you ever used Ghidra? Works really well on this game in that it does not scan forever. Makes it easy to do references and get decent C code.

Edit: Oh see your update. Visual only.

If you just want where health is updated its here but I assume you already know that.
Spoiler
// ORIGINAL CODE - INJECTION POINT: "bg3.exe"+229F929

"bg3.exe"+229F8FB: 41 8D 50 02 - lea edx,[r8+02]
"bg3.exe"+229F8FF: 48 8D 4C 24 38 - lea rcx,[rsp+38]
"bg3.exe"+229F904: E8 57 05 F5 FE - call bg3.exe+11EFE60
"bg3.exe"+229F909: 48 8D 4C 24 38 - lea rcx,[rsp+38]
"bg3.exe"+229F90E: 83 7C 24 44 40 - cmp dword ptr [rsp+44],40
"bg3.exe"+229F913: 48 0F 4F 4C 24 38 - cmovg rcx,[rsp+38]
"bg3.exe"+229F919: 48 8B 01 - mov rax,[rcx]
"bg3.exe"+229F91C: 48 0F BA E8 01 - bts rax,01
"bg3.exe"+229F921: 48 89 01 - mov [rcx],rax
"bg3.exe"+229F924: 48 8B 44 24 28 - mov rax,[rsp+28]
// ---------- INJECTING HERE ----------
"bg3.exe"+229F929: 89 78 50 - mov [rax+50],edi
// ---------- DONE INJECTING ----------
"bg3.exe"+229F92C: 48 8B 03 - mov rax,[rbx]
"bg3.exe"+229F92F: 48 89 44 24 78 - mov [rsp+78],rax
"bg3.exe"+229F934: 48 8B 5E 38 - mov rbx,[rsi+38]
"bg3.exe"+229F938: 48 85 DB - test rbx,rbx
"bg3.exe"+229F93B: 74 2E - je bg3.exe+229F96B
"bg3.exe"+229F93D: 48 63 46 48 - movsxd rax,dword ptr [rsi+48]
"bg3.exe"+229F941: 48 8D 0C C0 - lea rcx,[rax+rax*8]
"bg3.exe"+229F945: 48 8D 3C CB - lea rdi,[rbx+rcx*8]
"bg3.exe"+229F949: 48 3B DF - cmp rbx,rdi
"bg3.exe"+229F94C: 74 1D - je bg3.exe+229F96B
"bg3.exe"+229F94E: 66 90 - nop 2
My experience was strange. Pinning the value in CE did not actually help but alt-tabbing out and back into game somehow made the value get picked up. Will note that CE is behaving strangely with this game and does not seem to update as I expect it to.


Edit 2: Regarding Console. Looks like they use lua based on strings like "lua_debug>" so the strings see like DebugGuaranteeCriticalSuccess look like they might be populating a lua table I'm thinking. Still don't see the binding for the underlying function but might be informative.
Last edited by tfigment on Thu Oct 08, 2020 3:35 am, edited 1 time in total.

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

^ Game uses client-server logic. Sometimes it takes 1-2s till data is synced and that's why your CE "lags" :P Yeah, I'll check the write spot and compare against what the mouse over shows.

EDIT: Seems to work only for static objects. The "Mouse Over Hook". Hovering over Imps, for example, doesn't get any pointer. So it's the "Type" that's filtering the data.

tfigment
Table Makers
Table Makers
Posts: 273
Joined: Sat Apr 15, 2017 12:49 am
Reputation: 254

Re: Baldur's Gate 3

Post by tfigment »

Just to be clear on that health pointer. What I did was in initial area. Take fire damage. Find the location. Change the value I showed back to original value. UI will still show like 8 HP or whatever but the value is 14. Next time I get fire damage the UI and that value update to like 13 because only 1 pt damage so it took the 14 max value override but UI only updated once I was healed or damaged.

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

^ I guess you didn't have time to read all of my posts. I explained in the exact same manner as you what I did :P Yes, HP updates when events occur. That sync.

tfigment
Table Makers
Table Makers
Posts: 273
Joined: Sat Apr 15, 2017 12:49 am
Reputation: 254

Re: Baldur's Gate 3

Post by tfigment »

SunBeam wrote:
Thu Oct 08, 2020 3:43 am
^ I guess you didn't have time to read all of my posts. I explained in the exact same manner as you what I did :P Yes, HP updates when events occur. That sync.
Guess not. I read enough that it seemed like you were going after the god mode flag which is generally different approach if its in built.

Zanzer
RCE Fanatics
RCE Fanatics
Posts: 441
Joined: Fri Mar 03, 2017 10:48 pm
Reputation: 877

Re: Baldur's Gate 3

Post by Zanzer »

God mode is in the same structure as health values. +50, +54, and +9C

Lynoa
Noobzor
Noobzor
Posts: 9
Joined: Fri Oct 05, 2018 11:23 am
Reputation: 0

Re: Baldur's Gate 3

Post by Lynoa »

hey, you are awesome :) I can only make sence of about 10 to 20% of what you are doing. I tried to "freeze " my health with 4bite search (it works most of the time but once I restart the game or load, i have to do it again).
unfortunately even though you write kind of a walk through I am not able to do it on my own.
I hope that there is a way to make a table. Looking forward to it!! And thanks again for doing this!

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

Gotta love "contributions" with already mentioned crap. Guys/gals, please read my shit? Thanks!

Image

Image

^ That's on page 1...

Zanzer
RCE Fanatics
RCE Fanatics
Posts: 441
Joined: Fri Mar 03, 2017 10:48 pm
Reputation: 877

Re: Baldur's Gate 3

Post by Zanzer »

I wasn't contributing. I stole God Mode from you. :) Was just reiterating.

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

fireundubh wrote:
Wed Oct 07, 2020 7:08 pm
...
Now I understand your interest for this title :D -> [Link]

fireundubh
Expert Cheater
Expert Cheater
Posts: 133
Joined: Sun Sep 24, 2017 1:53 am
Reputation: 29

Re: Baldur's Gate 3

Post by fireundubh »

SunBeam wrote:
Thu Oct 08, 2020 7:30 pm
Now I understand your interest for this title :D -> [Link]
That's old stuff! :lol: In the DOS2 mod world, I'm probably better known for [Link]. I also wrote the CLI for Norbyte's [Link], which should support BG3 file formats soon-ish.

tfigment
Table Makers
Table Makers
Posts: 273
Joined: Sat Apr 15, 2017 12:49 am
Reputation: 254

Re: Baldur's Gate 3

Post by tfigment »

fireundubh wrote:
Thu Oct 08, 2020 8:35 pm
That's old stuff! :lol: In the DOS2 mod world, I'm probably better known for [Link]. I also wrote the CLI for Norbyte's [Link], which should support BG3 file formats soon-ish.
Interesting work. Are they still using Granny for animation/mesh for this game? I've worked on tools like that a lot in the past (mostly Morrowind,Obivion,Skyrim,NWN2,FO3). I wrote a 3dsmax importer/exporter for NWN2 (I think) and animation was in Granny. I liked the format since skeleton was usually included making import easier but I remember having issues figuring out some of the heavily compressed rotation controllers in the GR2 files. Not sure I wanted to try for this game yet but was thinking about it and seems like there is no need.

Anyway I guess I'll stop "contributing" at this point and play the game a little.

User avatar
SunBeam
Administration
Administration
Posts: 3011
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 2151

Re: Baldur's Gate 3

Post by SunBeam »

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:

Image

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:

Image

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:

Image

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:

Image

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:

Image

So that led me to this layout:

Image

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:

Image

Image

The id is 0x0001000000000396.

Then when I select Us, this happens:

Image

Image

The id is 0x000100000000037E.

Then when I select Lae'zel, this happens:

Image

Image

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:

Image

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:

Image

And out of it:

Image

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:

Image

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:

Image

Image

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..

Image

..you will land here, where I have additional comments:

Image

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:

Image

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:

Image

RAX and RDX contain the same value/address -> 00000238E5006D80 :) From here, RBX is populated with [RDX+10], which is:

Image

So what's put into RBX is 0x02800001000000E4. This RBX is then used here..

Image

..and continuing here:

Image

Which seems to get a pointer, in my case: 0x00007FF3DDD18770.

Then:

Image

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.

Post Reply

Who is online

Users browsing this forum: No registered users