Our Hacking of Dark Souls III Has Been Tested on Stream
With the initial hacking of Dark Souls III now complete, a playtest of it was in order. The game received its debut earlier today on stream to much acclaim and applause (insert Kappa?). I look forward to playing through it in its entirety — it is sure to be both punishing and beautiful. If you miss any of the live streams, be sure to check out previous broadcasts uploaded to my YouTube.
Although it was a fairly smooth few hours of gameplay, a number of issues did pop up that required some fixing off-stream. I’ll be documenting those fixes in this article, and I feel this Omnified game is definitely going to be very enjoyable and playable following these fixes. What I’m doing here will more or less be a process template for future Omnified hackings: initial Omnification articles will be written while I’m hacking the game, with a follow up post-playtest article published containing any critical fixes.
I’ll be going over all critical problems encountered with a link to the original article the particular hack in question was written in and then how I went about fixing it. Any other changes or fixes to the game in the future will get an individual article if notable enough!
I. Shifting Character Modules Issue
In the first hacking Dark Souls III article on data structure analysis, we charted out the contents of the player’s root structure, including how we can find the SprjChrDataModule (which contains the player’s vitals) and the SprjChrPhysicsModule (which contains the player’s coordinates) inside of it. The player hook was further updated in the follow-up article I wrote on Dark Soul III’s Apocalypse implementation.
We found that the these data and physics modules could be found at offsets 0x23D8 and 0x2428 respectively. It was assumed, at the time of writing, that these offsets were static and wouldn’t change — this is a reasonable assumption to make. However, during the stream, I noticed that the location of these modules inside the root structure would randomly shift around. Sometimes the physics module would be found at 0x2408, sometimes it would be found at 0x2218, etc.
Anyway, adding support for all the various places the modules could show up would be foolish, as there’s no guarantee there wouldn’t be any more places to add support for later on; it clearly isn’t how the data types were designed to be interacted with.
What I did was look for code that was accessing the various modules inside the root structure, and tried to determine how the game code itself went about finding these modules. Eventually I stumbled on code that revealed the use of a module collection pointer, also found on the root structure, and located at the offset 0x1F90. This pointer points to the starting address of all the modules for that character, and the collection pointer is indeed static.
I realized that this pointer was our golden ticket right away, as I previously saw a similar practice being followed in Sekiro’s code, which shares many similarities with Dark Souls III.
Armed with a more reliable way to map our player data, the player hook has been updated to the following code:
Player Hook With Shifting Module Support
// Gets the player's root, coordinates, and vitals structs. // rcx: Address of player's root struct. define(omniPlayerHook, "DarkSoulsIII.exe" + C1A78C) assert(omniPlayerHook, 48 8B 01 FF 90 C0 02 00 00) alloc(getPlayer,$1000, omniPlayerHook) alloc(player,8) alloc(playerCoords,8) alloc(playerVitals,8) registersymbol(omniPlayerHook) registersymbol(player) registersymbol(playerCoords) registersymbol(playerVitals) getPlayer: push rbx push rcx push rdx mov rbx,player mov [rbx],rcx mov rdx,[rcx+1F90] mov rcx,rdx mov rdx,[rcx+68] mov rbx,playerCoords mov [rbx],rdx mov rdx,[rcx+18] mov rbx,playerVitals mov [rbx],rdx pop rdx pop rcx pop rbx getPlayerOriginalCode: mov rax,[rcx] call qword ptr [rax+000002C0] jmp getPlayerReturn omniPlayerHook: jmp getPlayer nop 4 getPlayerReturn:
II. Unreliable Morphing Scale ID Location
In the Dark Souls III Abomnification implementation article, we found a place we thought suitable in memory to hold creatures’ morphing scale IDs. Specifically, we chose the offset 0x39C inside the SprjCharPhysicsModule to be the home for the creature’s ID.
While this seemed to be OK at the time, it was found to actually be unreliable during the playtest. A new and easier to find place was designated as the home for the morph scale ID: offset 0x1194 in the PlayerIns and EnemyIns root structures.
There were also difficulties in retrieving the EnemyIns structure while executing inside our Abomnification application code (where our custom scaling occurs). We previously identified a reliable way for retrieving it to be by dereferencing the r12 register and looking at offset 0x58. However, this caused a number of Abomnifications to fail, and it was discovered we needed to also add support for looking at the 0x68 offset as well, as the EnemyIns could also appear there.
Here is the updated code for the Abomnification initiation point hook using the new morph scale index location.
Updated Abomnification Initiation Hook
// Initiates the Abomnification system, generating new scaling parameters // for a creature. // rbx: Target physics module. define(omnifyAbomnificationHook,"DarkSoulsIII.exe"+9D2360) assert(omnifyAbomnificationHook,66 0F 7F B3 80 00 00 00) alloc(initiateAbomnification,$1000,omnifyAbomnificationHook) registersymbol(omnifyAbomnificationHook) initiateAbomnification: pushf // Ensure that the player coordinates pointer has been initialized. push rax mov rax,playerCoords cmp [rax],0 pop rax je initiateAbomnificationOriginalCode // Ensure that the we aren't about the Abomnify the player. push rax mov rax,playerCoords cmp rbx,[rax] pop rax je initiateAbomnificationOriginalCode // Load the address to the morph scale ID and then call the Abomnification // system. push rax push rcx // The root structure can be found here on the physics // module. mov rcx,[rbx+8] lea rax,[rcx+1194] pop rcx push rax call executeAbomnification pop rax initiateAbomnificationOriginalCode: popf movdqa [rbx+00000080],xmm6 jmp initiateAbomnificationReturn omnifyAbomnificationHook: jmp initiateAbomnification nop 3 initiateAbomnificationReturn:
The initiation point now grabs the EnemyIns structure from the physics module, and uses the new offset instead. This new one works great!
Here is the updated code for the Abomnification application hook:
Updated Abomnification Application Hook
// Applies the Abomnification system. // [rax]: Width matrix values // [rax+10]: Height matrix values // [rax+20]: Depth matrix values define(omnifyApplyAbomnificationHook, "DarkSoulsIII.exe" + D279FC) assert(omnifyApplyAbomnificationHook, 0F 28 00 4B 8D 14 76) alloc(applyAbomnification,$1000, omnifyApplyAbomnificationHook) alloc(abomnifyPlayer,8) alloc(disableAbomnify,8) alloc(skipTypeCheck,8) registersymbol(omnifyApplyAbomnificationHook) registersymbol(abomnifyPlayer) registersymbol(disableAbomnify) registersymbol(skipTypeCheck) applyAbomnification: pushf cmp [disableAbomnify],1 je applyAbomnificationOriginalCode // Ensure that the player root structure pointer has been initialized. push rax mov rax,player cmp [rax],0 pop rax je applyAbomnificationOriginalCode // Backing up a few registers we'll be using to perform calculations // on the original matrix values with updated scaling parameters. sub rsp,10 movdqu [rsp],xmm0 sub rsp,10 movdqu [rsp],xmm1 push rbx push rcx // We take a look at the address stored at the place we expect to // find the enemy root structure, and see if it is a valid pointer. mov rbx,[r12+58] lea rcx,[rbx] call checkBadPointer cmp ecx,0 je checkAbomnifyType // The root structure sometimes appears at 0x68 instead. mov rbx,[r12+68] lea rcx,[rbx] call checkBadPointer cmp ecx,0 jne applyAbomnificationExit // We do another check to ensure that it isn't a humanoid model. // Human scaling is done in the applyAbomnificationHuman hook. checkAbomnifyType: cmp [skipTypeCheck],1 je checkAbomnifyPlayer push rax mov rax,[rbx] cmp ax,0x3EB8 pop rax jne applyAbomnificationExit // We then further check to see if the root structure address differs // from the player's own root structure. checkAbomnifyPlayer: cmp [abomnifyPlayer],1 je loadScaleData mov rcx,player cmp rbx,[rcx] je applyAbomnificationExit loadScaleData: // Now that we know our data is good, we want to check if a morph scale // ID has actually been assigned yet by the Abomnification system. mov rcx,[rbx+1194] cmp rcx,0 je applyAbomnificationExit // This should never happen, but we want to ensure we aren't going to access // memory beyond the allocated bounds of our sandbox region. cmp ecx,#999 ja applyAbomnificationExit // Each creature has morph scale data that takes up a total of 48 bytes. // So we want to multiply the size of each section by our ID to get the // offset to apply to our base morphScaleData, which will give us the // location to our creature-specific morph scale data. push rax push rdx mov rax,rcx mov rcx,#48 mul rcx mov ecx,eax pop rdx pop rax mov rbx,morphScaleData add rbx,rcx applyAbomnificationScaleAdjustment: // We want to then do one final check to ensure that some sort of morph // scale data has actually been initialized for the creature. lea rcx,[rbx+4] call checkBadPointer cmp ecx,0 jne applyAbomnificationExit mov rcx,[rbx+4] cmp rcx,0 je applyAbomnificationExit // We load the scaling parameter for the creature's width, and then multiply // the live width matrix values by it. movss xmm0,[rbx+4] shufps xmm0,xmm0,0 movdqu xmm1,[rax] mulps xmm1,xmm0 movdqu [rax],xmm1 // We load the scaling parameter for the creature's height, and then multiply // the live height matrix values by it. movss xmm0,[rbx+8] shufps xmm0,xmm0,0 movdqu xmm1,[rax+10] mulps xmm1,xmm0 movdqu [rax+10],xmm1 // We load the scaling parameter for the creature's depth, and then multiply // the live depth matrix values by it. movss xmm0,[rbx+C] shufps xmm0,xmm0,0 movdqu xmm1,[rax+20] mulps xmm1,xmm0 movdqu [rax+20],xmm1 applyAbomnificationExit: // Restore backed up values. pop rcx pop rbx movdqu xmm1,[rsp] add rsp,10 movdqu xmm0,[rsp] add rsp,10 applyAbomnificationOriginalCode: popf movaps xmm0,[rax] lea rdx,[r14+r14*2] jmp applyAbomnificationReturn omnifyApplyAbomnificationHook: jmp applyAbomnification nop 2 applyAbomnificationReturn:
III. Humanoid Bodies and Armor Not Morphing
Another issue with the Abomnification system that I observed was that most humanoid NPCs were failing to morph at all, as well as more complicated armors worn by them. This was no good! Everything needs to be Abomnified!
I determined that our current Abomnification application hook was successfully applying scaling to all incoming rendering requests. That meant that there was another function in code purposed to handle the rendering of humanoid NPC models and their outfits. I located this code by putting on an outfit that had a nice wavy cloak, and then repeating the steps I followed in the Abomnification implementation article for Dark Souls III to find the original Abomnification application hook location.
Eventually I found a matrix multiplication routine at “DarkSoulsIII.exe”+C2FBCE, and implemented another Abomnification application hook there, basically identical to the original one.
After wiring it up (and enabling it to work on the player character temporarily) I was super thrilled to see that the player’s cloak was actually morphing! This was a big deal because, although I’d been able to get more and more parts of the models morphing with each game I’ve done, I’ve never been able to get long wavy cloaks and dresses in FromSoftware games to morph.
Unfortunately, the hook has no effect (from what I’ve seen) on enemy dresses. Cloaks and complicated armor do appear to be working now however. As well as faces and weapons, which were not previously. All in all, I think we’re good here. Game is way more streamable now for me.
Here is the new Abomnification application hook for human NPC body types:
Humanoid/Clothing Abomnification Application Hook
// Applies the Abomnification for humans. define(omnifyApplyAbomnificationHumanHook, "DarkSoulsIII.exe" + C2FBCE) assert(omnifyApplyAbomnificationHumanHook, 0F 28 00 4B 8D 0C 76) alloc(applyAbomnificationHuman,$1000, omnifyApplyAbomnificationHumanHook) alloc(disableAbomnifyHuman,8) registersymbol(omnifyApplyAbomnificationHumanHook) registersymbol(disableAbomnifyHuman) applyAbomnificationHuman: pushf cmp [disableAbomnifyHuman],1 je applyAbomnificationHumanOriginalCode // Ensure that the player root structure pointer has been initialized. push rax mov rax,player cmp [rax],0 pop rax je applyAbomnificationHumanOriginalCode // Backing up a few registers we'll be using to perform calculations // on the original matrix values with updated scaling parameters. sub rsp,10 movdqu [rsp],xmm0 sub rsp,10 movdqu [rsp],xmm1 push rbx push rcx // We take a look at the address stored at the place we expect to // find the enemy root structure, and see if it is a valid pointer. mov rbx,[rsp+162] lea rcx,[rbx] call checkBadPointer cmp ecx,0 jne applyAbomnificationHumanExit // We then further check to see if the root structure address differs // from the player's own root structure. cmp [abomnifyPlayer],1 je loadScaleHumanData mov rcx,player cmp rbx,[rcx] je applyAbomnificationHumanExit loadScaleHumanData: // Now that we know our data is good, we want to check if a morph scale // ID has actually been assigned yet by the Abomnification system. mov rcx,[rbx+1194] cmp rcx,0 je applyAbomnificationHumanExit // This should never happen, but we want to ensure we aren't going to access // memory beyond the allocated bounds of our sandbox region. cmp ecx,#999 ja applyAbomnificationHumanExit // Each creature has morph scale data that takes up a total of 48 bytes. // So we want to multiply the size of each section by our ID to get the // offset to apply to our base morphScaleData, which will give us the // location to our creature-specific morph scale data. push rax push rdx mov rax,rcx mov rcx,#48 mul rcx mov ecx,eax pop rdx pop rax mov rbx,morphScaleData add rbx,rcx applyAbomnificationHumanScaleAdjustment: // We want to then do one final check to ensure that some sort of morph // scale data has actually been initialized for the creature. lea rcx,[rbx+4] call checkBadPointer cmp ecx,0 jne applyAbomnificationHumanExit mov rcx,[rbx+4] cmp rcx,0 je applyAbomnificationHumanExit // We load the scaling parameter for the creature's width, and then multiply // the live width matrix values by it. movss xmm0,[rbx+4] shufps xmm0,xmm0,0 movdqu xmm1,[rax] mulps xmm1,xmm0 movdqu [rax],xmm1 // We load the scaling parameter for the creature's height, and then multiply // the live height matrix values by it. movss xmm0,[rbx+8] shufps xmm0,xmm0,0 movdqu xmm1,[rax+10] mulps xmm1,xmm0 movdqu [rax+10],xmm1 // We load the scaling parameter for the creature's depth, and then multiply // the live depth matrix values by it. movss xmm0,[rbx+C] shufps xmm0,xmm0,0 movdqu xmm1,[rax+20] mulps xmm1,xmm0 movdqu [rax+20],xmm1 applyAbomnificationHumanExit: // Restore backed up values. pop rcx pop rbx movdqu xmm1,[rsp] add rsp,10 movdqu xmm0,[rsp] add rsp,10 applyAbomnificationHumanOriginalCode: popf movaps xmm0,[rax] lea rcx,[r14+r14*2] jmp applyAbomnificationHumanReturn omnifyApplyAbomnificationHumanHook: jmp applyAbomnificationHuman nop 2 applyAbomnificationHumanReturn:
IV. Predator Parameter Optimization
Like Sekiro, Dark Souls III has a sensitive movement system that experiences a type of “resonance” when manipulated from the outside, resulting in enemies phasing out of existence if boosted a bit.
99% of movement anomalies appear to have been resolved by tuning some of the Predator system’s external parameters to the following values:
- positiveLimit: 0.8
- negativeLimit: -0.8
- positiveLimitCorrection: 0
- negativeLimitCorrection: 0
- enemySpeedX: 2.0
For an explanation as to what these parameters mean, and what the significance of the new values might be, you’ll need to consult the Predator general overview article, which is unfortunately not available yet! I’ll put a link here when it is.
Ready to Get This Game On
Alright! With these fixes, the game is damn well ready for showtime. Hacking Dark Souls III and getting it Omnified (and writing about it all on this site) has been a great experience, and with this fixes the game is ready to play. Really looking forward to it. And I hope to see you there! I’ll be streaming it most days on my stream at: https://twitch.tv/omni.
Make sure to check out the Stream Info section on my hackpad for the latest stream schedule news and information.
Thanks for your interest! Have a good one.
~Omni