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.

After hacking Dark Souls III and making it Omnified, I noticed the character modules would shift randomly in location. This picture shows the recently discovered module collection pointer and its contents.
Here are the contents of the easily locatable module collection structure.

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