Assassin's Creed Valhalla has been updated to v1.1.1.
A new version! Hopefully we can salvage our Omnified hack.

A Somewhat Unexpected Update

Ah what joy. A new update for Assassin’s Creed: Valhalla has been released upon the masses. I’m sure most gamers, upon seeing an update for a game they enjoy queued for download, rejoice with much latent glee. Not this grumpy fellow, as any update to an Omnified game most of the time means that the binary has been altered, resulting in some inevitable breakage for our hacks.

I typically will prevent any updates from being downloaded while I’m streaming an Omnified game, however that dratted Uplay refused to launch the game unless I updated the game binaries. Perhaps if I left the launcher in offline mode it would’ve never known about the update. Well. That is neither here nor there now.

Here are the official patch notes, and they look to be describing a mighty fine set of changes indeed. Isn’t that just special. No time for dawdling — time to get to updating our hacks! Let’s pray to whatever-is-up-there that we’re able to locate all the areas of code we were injecting into previously.

Updating the Player Data Retrieval Code

The first item on the list for updating is the code responsible for retrieving all pertinent data associated with our player. This code grabs the player’s root structure, health structure, and location structure. Very essential and very much the first thing we’ll go after.

The unique array of bytes we had listed for the bit of code we were hooking into before was 2B 45 BC 89 44 24 58. Hopefully the updates to the application haven’t eaten up this area of the code.

Luckily it looks like our code is still present in the binary. Woot. Let’s update our hook then so that it gets injected into the proper place.

Updated Player Data Retrieval Hook

// Creates pointers for the player's root, health, and location
// structures.
// UNIQUE AOB: 8B BB 38 01 00 00 75
define(omniPlayerHook, "ACValhalla.exe" + 2195AC3)

assert(omniPlayerHook, 8B BB 38 01 00 00)
alloc(getPlayer,$1000, omniPlayerHook)
alloc(player,8)
alloc(playerHealth,8)
alloc(playerLocation,8)

registersymbol(omniPlayerHook)
registersymbol(player)
registersymbol(playerHealth)
registersymbol(playerLocation)

getPlayer:
  push rax
  push rbx
  push rcx
  mov rax,playerHealth
  mov [rax],rbx
  // Root structure can be found at [playerHealth+70].
  mov rax,player
  mov rcx,[rbx+70]
  mov [rax],rcx
  // Location structure aligned for movement can be found 
  // at [player+1D0].
  mov rax,playerLocation
  mov rbx,[rcx+1D0]
  mov [rax],rbx
  pop rcx
  pop rbx
  pop rax
getPlayerOriginalCode:
  mov edi,[rbx+00000138]
  jmp getPlayerReturn


omniPlayerHook:
  jmp getPlayer
  nop 
getPlayerReturn:

Excellent! Now, to get to all the code that will really be a pain to go look for again.

Updating the Apocalypse System’s Hooks

Our Apocalypse initiation point is located smack dab in the middle of the damage application code for the player. This would be quite the pain to have to find again. The unique array of bytes for this is 2B 45 BC 89 44 24 58. Let’s hope for the best.

Searching for this array yields back a single result, perfect! None of the updates in the patch touched the code that applies damage to creatures. Let’s get that hook updated then.

Updated Apocalypse Initiation Point Hook

// Initiates the Apocalypse system.
// Unique AOB: 2B 45 BC 89 44 24 58
define(omnifyApocalypseHook, "ACValhalla.exe" + 2149247)

assert(omnifyApocalypseHook, 2B 45 BC 89 44 24 58)
alloc(initiateApocalypse,$1000, omnifyApocalypseHook)

registersymbol(omnifyApocalypseHook)

initiateApocalypse:
  pushf
  // Backing up a few SSE registers to hold converted floating points.
  sub rsp,10
  movdqu [rsp],xmm0
  sub rsp,10
  movdqu [rsp],xmm1
  sub rsp,10
  movdqu [rsp],xmm2
  // Backing up the rbx register as it will be overwritten by
  // the Apocalypse register. We don't care about the rax register
  // as we'll be changing it anyway.
  push rbx
  // Backing up a working register to aid in value calculations, etc.
  push rcx
  // If we're freezing from cold water, the bits at [rsp+452] will be 
  // 0x14.
  //mov rcx,[rsp+452]
  //cmp rcx,0x14
  //je initiateApocalypseCheck
  // Damage from arrows lack a root structure damage source, however
  // the bits at [rsp+452] will be non-zero.
  //cmp rcx,0
  //jne skipLocationCheck
  // Let's grab the damage source right now before the stack shifts
  // anymore.
  mov rcx,[rsp+44A]
  // If no root structure is listed as the damage source, this means
  // that the damage is from an ambient source (i.e. falling), which
  // we want to prevent from triggering the Apocalypse system.
  //cmp rcx,0
  //je initiateApocalypseExit
skipLocationCheck:
  // We'll convert the working health and damage amount to floats.
  cvtsi2ss xmm0,eax
  cvtsi2ss xmm1,[rbp-44]
  // If the player is the one receiving the damage, then the rdi register
  // will point to the player's location structure.
  mov rbx,playerLocation
  cmp [rbx],rdi
  je initiatePlayerApocalypse
  // The player is the one damaging the NPC if the player's root structure
  // is listed as the damage source.
  mov rbx,player
  cmp [rbx],rcx
  je initiateEnemyApocalypse
  jmp initiateApocalypseExit
initiatePlayerApocalypse:
  // Realign the player's coordinates so it begins at the X coordinate.
  mov rcx,[rbx]
  lea rax,[rcx+50]
  // Convert the player's maximum health to floating point.
  mov rbx,playerHealth
  mov rcx,[rbx]  
  cvtsi2ss xmm2,[rcx+13C]
  // Push the damage amount parameter.
  sub rsp,8
  movd [rsp],xmm1
  // Push the working health value parameter.
  sub rsp,8
  movd [rsp],xmm0
  // Push the maximum health value parameter.
  sub rsp,8
  movd [rsp],xmm2
  // Push the aligned coordinates struct parameter.
  push rax
  call executePlayerApocalypse
  jmp initiateApocalypseUpdateDamage
initiateEnemyApocalypse:
  // Push the damage amount parameter.
  sub rsp,8
  movd [rsp],xmm1
  // Push the working health value parameter.
  sub rsp,8
  movd [rsp],xmm0
  call executeEnemyApocalypse
initiateApocalypseUpdateDamage:
  // Convert the updated damage amount to an integer.
  movd xmm0,eax
  cvtss2si eax,xmm0
  mov [rbp-44],eax
  // Convert the updated working health value to an integer.
  movd xmm0,ebx
  cvtss2si eax,xmm0
initiateApocalypseExit:
  pop rbx
  pop rcx
  movdqu xmm2,[rsp]
  add rsp,10
  movdqu xmm1,[rsp]
  add rsp,10
  movdqu xmm0,[rsp]
  add rsp,10
initiateApocalypseOriginalCode:
  popf
  sub eax,[rbp-44]
  mov [rsp+58],eax
  jmp initiateApocalypseReturn

omnifyApocalypseHook:
  jmp initiateApocalypse
  nop 2
initiateApocalypseReturn:


negativeVerticalDisplacementEnabled:
  dd 0
  
teleportitisDisplacementX:
  dd (float)5.0
  
yIsVertical:
  dd 0
  
extraDamageX:
  dd (float)2.0

In addition to the initiation point hook, we also have some code being inserted into the game that removes its one hit kill protection, which is a terrible thing really (the one hit kill protection, not my code duh). A world without one hit kills would be an awful world to live in!

The unique array of bytes we wrote down for this was 0F B6 8D EC 00 00 00, so let’s cross our fingers once again. Unfortunately, for this, we get two results back. So, we need to do some guessing. Disassembling the first result, I see an idiv instruction, indicating that some integer division is going on.

This is something that I do remember occurring nearby the hook we made to prevent the one hit kill protection from going off (it was the integer division that was essentially testing whether or not our health was below the safe threshold), so we’ll update our code to inject into where the first result is pointing to.

Updated Depussification Hook

// Bypasses the one hit kill protection system.
// Unique AOB: 0F B6 8D EC 00 00 00
define(omnifyDepussifyGameHook, "ACValhalla.exe" + 20EBE76)

assert(omnifyDepussifyGameHook, 0F B6 8D EC 00 00 00)
alloc(depussifyGame,$1000, omnifyDepussifyGameHook)

registersymbol(omnifyDepussifyGameHook)

depussifyGame:
  // This is a hardcoded value used as a divisor applied against
  // the player's maximum health to see if the player is protected
  // from being one shot. By increasing this to max we essentially
  // make all values of health unsafe.
  mov [rbp+EC],0xFF
depussifyGameOriginalCode:
  movzx ecx,byte ptr [rbp+000000EC]
  jmp depussifyGameReturn


omnifyDepussifyGameHook:
  jmp depussifyGame
  nop 2
depussifyGameReturn:

And we’re not quite done yet. We had another hook in place to allow for coordinate changes stemming from teleportitis to persist despite being in the middle of a damage-related animation. The unique array of bytes for this is 0F 28 48 30 0F 29 49 50. Crossing my fingers.

Only one result back. Fantastic. Time to update this then.

Updated Teleportitis Overwrite Bypass Hook

// Bypasses a single location update pulse so that coordinates changes
// from a teleportitis effect are not overwritten.
// Unique AOB: 0F 28 48 30 0F 29 49 50
define(omnifyBypassTeleportitisOverwriteHook, "ACValhalla.exe" + 7E878A)

assert(omnifyBypassTeleportitisOverwriteHook,0F 28 48 30 0F 29 49 50)
alloc(bypassTeleportitisOverwrite,$1000,omnifyBypassTeleportitisOverwriteHook)

registersymbol(omnifyBypassTeleportitisOverwriteHook)

bypassTeleportitisOverwrite:
  pushf
  // Ensure that the player location struct has been initialized.
  push rax
  mov rax,playerLocation
  cmp [rax],0  
  pop rax
  je bypassTeleportitisOverwriteOriginalCode  
  // Backup registers used to hold the player's location structures.
  push rax
  push rbx
  push rdx
  // Backup the "updated location" structure address.
  mov rdx,rax
  mov rax,playerLocation
  mov rbx,[rax]
  // If the player is the one moving, we make sure that any new coordinates
  // resulting from a recent teleport stick for one additional location
  // update tick.
  cmp rbx,rcx
  jne bypassTeleportitisOverwriteExit
  mov rax,teleported
  cmp [rax],1
  jne bypassTeleportitisOverwriteExit
  // Clear the flag so this only happens once.
  mov [rax],0
  // Take the current coordinates and store them in the "updated location" 
  // structure used in the original code as the source for the new coordinates.
  movups xmm1,[rcx+50]
  movups [rdx+30],xmm1
bypassTeleportitisOverwriteExit:  
  // Restore backed up values.
  pop rdx
  pop rbx
  pop rax
bypassTeleportitisOverwriteOriginalCode:
  popf
  movaps xmm1,[rax+30]
  movaps [rcx+50],xmm1
  jmp bypassTeleportitisOverwriteReturn

omnifyBypassTeleportitisOverwriteHook:
  jmp bypassTeleportitisOverwrite
  nop 3
bypassTeleportitisOverwriteReturn:

That’s it for the Apocalypse system.

Updating the Predator System’s Hooks

Next up to bat are hooks related to the Predator system. Fortunately, for this game-neutral system, there is only a single hook to update: its initiation point. This resides right where the movement application code is, which wasn’t the easiest thing to find, of course.

The unique array of bytes for this is 0F 10 56 50 4C 8D 4C 24 30. Please, please, give me some good results here.

Viola! A single result is returned. Let’s update our hook to work here.

Updated Predator Initiation Point Hook

// Initiates the Predator system.
// Unique AOB:0F 58 C3 0F 11 06 F3
// xmm0: Target's current coordinate values.
// xmm3: Target's movement offsets.
define(omnifyPredatorHook, "ACValhalla.exe" + F0CA43)

assert(omnifyPredatorHook, 0F 58 C3 0F 11 06)
alloc(initiatePredator,$1000, omnifyPredatorHook)
alloc(identityValue,8)
alloc(playerSpeedX,8)

registersymbol(omnifyPredatorHook)
registersymbol(playerSpeedX)
        
initiatePredator:
  pushf
  // Ensure that the player location struct has been initialized.
  push rax
  mov rax,playerLocation
  cmp [rax],0
  pop rax
  je initiatePredatorOriginalCode
  // Backing up a few SSE registers we'll be using to perform a little
  // memory management.
  sub rsp,10
  movdqu [rsp],xmm0
  sub rsp,10
  movdqu [rsp],xmm1
  // Backing up the registers used by the Predator system to hold its
  // return values.
  push rax
  push rbx
  push rcx
  // If it is the player moving, we'll want to avoid Predator execution
  // and simply apply the player speed modifier.
  // An interim data structure linking to the creature's location
  // structure can be found on the stack.
  mov rax,[rsp+14A]
  // The location structure is found at the 0x18 offset of this
  // interim structure.
  push rcx
  lea rcx,[rax+18]
  call checkBadPointer
  cmp rcx,0
  pop rcx
  jne initiatePredatorCleanup
  mov rbx,[rax+18]
  mov rax,playerLocation
  cmp rbx,[rax]
  je applyPlayerSpeed  
initiatePredatorExecute:
  // The player's coordinates are pushed as the first parameter.
  mov rax,playerLocation
  mov rbx,[rax]
  push [rbx+50]
  push [rbx+58]
  // Parameters values are expected to have been pushed to the stack as
  // if a m32 operand was used. Since two of the parameter sets are on
  // SSE registers, we'll need to split low and high words, and then
  // dump them onto the stack as quadwords (low and then high).
  movhlps xmm1,xmm0
  // Lets push the target's coordinates as the second parameter.
  sub rsp,8
  movq [rsp],xmm0
  sub rsp,8
  movq [rsp],xmm1  
  // An identity matrix is used as we have not made the determination as
  // of yet as to whether the game uses true scaling or not. 
  // We pass this as the third parameter.
  movss xmm0,[identityValue]
  shufps xmm0,xmm0,0
  sub rsp,10
  movdqu [rsp],xmm0
  // Now we split up the movement offset SSE register.
  movhlps xmm1,xmm3
  // Lets push the target's movement offsets as the final parameter.
  sub rsp,8
  movq [rsp],xmm3
  sub rsp,8
  movq [rsp],xmm1
  // Time to execute dat Predator system.
  call executePredator
  jmp initiatePredatorExit
applyPlayerSpeed:
  // We'll be applying the player speed multiplier to all movement offsets
  // except the Z (vertical) offset.
  sub rsp,10
  // We want to preserve the Z offset before applying the multiplier.
  movups [rsp],xmm3
  mov ecx,[rsp+8]
  movss xmm0,[playerSpeedX]
  shufps xmm0,xmm0,0
  mulps xmm3,xmm0
  movups [rsp],xmm3
  // Load the modifier X and Y offsets.
  mov eax,[rsp]
  mov ebx,[rsp+4]
  add rsp,10
initiatePredatorExit:
  // We'll need to make some room on the stack for temporary use so
  // we can load updated values back onto the SSE register used by
  // the original code to apply the movement offsets.
  sub rsp,10
  // Start with the original movement offsets.
  movups [rsp],xmm3
  // Apply the updated offsets.
  mov [rsp],eax
  mov [rsp+4],ebx
  mov [rsp+8],ecx
  movups xmm3,[rsp]
  add rsp,10
initiatePredatorCleanup:
  // Restore backed up values.
  pop rcx
  pop rbx
  pop rax
  movdqu xmm1,[rsp]
  add rsp,10
  movdqu xmm0,[rsp]
  add rsp,10  
initiatePredatorOriginalCode:
  popf
  addps xmm0,xmm3
  movups [rsi],xmm0
  jmp initiatePredatorReturn


omnifyPredatorHook:
  jmp initiatePredator
  nop 
initiatePredatorReturn:


identityValue:
  dd (float)1.0
  
playerSpeedX:
  dd (float)1.0
  
skipBoostY:
  dd 0
  
skipBoostZ:
  dd 1

We’re actually almost done! All that remains is the Abomnification system. Hopefully not much damage was done here.

Updating the Abomnification System Hooks

As we discussed at length earlier, Assassin’s Creed: Valhalla lacks built-in easy scaling, which means that two hooks were needed in order to implement the Abomnification system. The first, the initiation point for the Abomnification system, was placed into a coordinate polling function for NPCs. We wrote down its unique array of bytes as 0F 10 56 50 4C 8D 4C 24 30.

Searching for that, it looks like we have a single hit! Yay! Hopefully we are able to make our hacks compatible with this latest version without incident. Let’s update our initiation point hook then.

Updated Abomnification Initiation Point Hook

// Initiates the Abomnification system.
// UNIQUE AOB: 0F 10 56 50 4C 8D 4C 24 30
define(omnifyAbomnificationHook, "ACValhalla.exe" + 2823120)

assert(omnifyAbomnificationHook, 0F 10 56 50 4C 8D 4C 24 30)
alloc(initiateAbomnification,$1000, omnifyAbomnificationHook)

registersymbol(omnifyAbomnificationHook)

initiateAbomnification:
  pushf
  // Ensure that the player location struct has been initialized.
  push rax
  mov rax,playerLocation
  cmp [rax],0
  pop rax
  je initiateAbomnificationOriginalCode 
  // Back up the registers used to hold return values.
  push rax
  push rbx
  push rcx
  // Ensure that the player isn't going to be Abomnified.
  mov rax,playerLocation
  mov rbx,[rax]
  cmp rbx,rsi
  je initiateAbomnificationExit
  // Push the identifying address parameter and call the Abomnification system.  
  push rsi
  call executeAbomnification    
initiateAbomnificationExit:
  // Restore the preserved values on the stack.
  pop rcx
  pop rbx
  pop rax
initiateAbomnificationOriginalCode:
  popf
  movups xmm2,[rsi+50]
  lea r9,[rsp+30]
  jmp initiateAbomnificationReturn


omnifyAbomnificationHook:
  jmp initiateAbomnification
  nop 4
initiateAbomnificationReturn:


abominifyWidthResultUpper:
  dd #250
  
abominifyHeightResultUpper:
  dd #160
  
abominifyDepthResultUpper:
  dd #200
  
unnaturalSmallX:
  dd (float)0.5625

Fantabulous. All that remains now is the hook for applying the Abomnification generated scale multipliers. This was located in some rendering code, so for the sake out my sanity, let’s hope that we can find it again. The unique array of bytes for this was written down as 44 0F 59 05 15 B0 2C 03.

And…no results. Crap! Did the patch actually change something as low level as the rendering code we were hooking into? Looking at the actual original instructions that were at play here, I quickly realized that had nothing to do with it at all.

mulps xmm8,[ACValhalla.exe+415A470]

We were hooking into the above instruction. If you can’t remember, this instruction is taking what I termed as the “global scale multipliers” and multiplying them against some vertex offset values loaded into xmm8. These global scale multipliers were being stored and loaded from static memory. Well, of course there would be zero guarantee that this address wouldn’t change between versions.

In fact, you would be reasonably safe if you bet money on the chance that its address would change. So, note to self: don’t use the unique array of bytes for an instruction referencing static memory as a means to locate code in the future. Luckily for me, I had this lovely hackpad website I could use, which had a nice article on implementing the Abomnification system into this game, with screenshots showing code nearby the hook.

Using these screenshots, I found the proper place to insert the hook post-patch, and I wrote down a new unique array of bytes for code nearby, but not on top of where we were hooking into.

Updated Abomnification Scale Application Hook

// Applies Abomnification generated scale multipliers.
// Unique AOB: 0F 11 62 10 4C 03 C1
define(omnifyApplyAbomnificationHook, "ACValhalla.exe" + E8F813)

assert(omnifyApplyAbomnificationHook, 44 0F 59 05 55 DC 2E 03)
alloc(applyAbomnification,$1000, omnifyApplyAbomnificationHook)

registersymbol(omnifyApplyAbomnificationHook)

applyAbomnification:
  pushf
  // Ensure that the player location struct has been initialized.
  push rax
  mov rax,playerLocation
  cmp [rax],0
  pop rax
  je applyAbomnificationOriginalCode
  // Backing up an SSE register to hold our multiplier.
  sub rsp,10
  movdqu [rsp],xmm0  
  // Backing up the registers used to hold return values by the
  // Abomnification system.
  push rax
  push rbx
  push rcx  
  // A rendering supplementary data structure is stored on the stack.
  mov rax,[rsp+6A]
  // If the bytes at 0x14 are not zeroed, then the rendering data structure
  // is not pointing to a location structure.
  mov rbx,[rax+14]
  cmp ebx,0
  jne applyAbomnificationExit
  // The location structure is found here, however we need to check that 
  // it's a valid pointer, as there is still a chance for some junk data here.
  mov rbx,[rax+18]  
  push rcx
  lea rcx,[rbx]
  call checkBadPointer
  cmp ecx,0
  pop rcx
  jne applyAbomnificationExit    
  // Ensure that the player isn't going to be Abomnified.
  mov rax,playerLocation
  cmp rbx,[rax]
  je applyAbomnificationExit
  // Push the identifying address parameter and get the Abomnified scales.  
  push rbx
  call getAbomnifiedScales
  // Make some room on the stack so we can construct the multiplier SSE register.
  sub rsp,10
  movss xmm0,[identityValue]
  shufps xmm0,xmm0,0
  movups [rsp],xmm0
  mov [rsp],eax
  mov [rsp+4],ecx
  mov [rsp+8],ebx
  movups xmm0,[rsp]
  add rsp,10
  // Apply the Abomnified scale multipliers to the register that will be applied
  // against the global scale parameters.
  mulps xmm8,xmm0
applyAbomnificationExit:
  // Restore the preserved values on the stack.
  pop rcx
  pop rbx
  pop rax
  movdqu xmm0,[rsp]
  add rsp,10
applyAbomnificationOriginalCode:
  popf 
  mulps xmm8,[ACValhalla.exe+417D470]
  jmp applyAbomnificationReturn


omnifyApplyAbomnificationHook:
  jmp applyAbomnification
  nop 3
applyAbomnificationReturn:

Whew. We avoided having to have to look for that crap again.

Omnified AC: Valhalla is 1.1.1 Ready!

Hooray! Hopefully there won’t be another patch for a little bit. Time to enjoy the updates along with the rest of the world.

Until next time, take care.

~Omni