A Few Middling Concerns Remain

The playthrough of Omnified Assassin’s Creed: Valhalla live on stream has been a most joyous affair. I can already tell that the Omnification of the game is going to cause the playtime required to beat it to balloon in size. It’s going to be quite a long time still until we can consider it beaten and dead.

That being said, given that we have so much playtime of the game remaining, I want to be sure that as much of that time is spent enjoying the playthrough as opposed to worrying over it. I definitely have my set of strange quirks that set me off, and when certain things go wrong or don’t work right with the changes I’ve made to a game, they are liable to make me upset.

While this has been the most stable and bug-free Omnified game yet, there exist a few problems causing a bit of consternation with me, and I need them corrected before I can continue this playthrough in peace. Here’s a list of what’s bugging me:

  1. No support for player Abomnification.
  2. At a medium distance, NPCs begin to periodically lose their Abomnified scales, causing them to revert to their normal size briefly. This causes a bit of a janky, jittering shape-changing effect to be observed. This is my main “pain point”.
  3. During kill animations, where the player is killing an enemy, the Apocalypse system is somehow being triggered as if the player is the one receiving the damage.

So, instead of fretting about any of these things anymore, let’s get to fixing the problems my friends.

Adding Support for Player Abomnification

The reason why no toggleable option exists to enable player Abomnification (where the player’s shape begins changing randomly like other NPCs) is because the coordinate polling function where the Abomnification initiation point is inserted into only polls for NPC coordinates, never the player’s. So, we just need to find a polling function that only accesses the player’s coordinates at a similar frequency to the other polling function we’re using for NPCs.

Right clicking on the player’s coordinates and looking for code accessing it, we get the following:

Shows code accessing our player's coordinates, with the highlighted instruction being one of interest.
The highlighted instruction isn’t executing a ton, but it’s using the proper offset.

There are so many instructions accessing our player’s coordinates. It’s good to know that we have lots of viable candidates at our disposal. For our convenience, however, I’ve decided to choose one that is accessing our player’s coordinates at an offset of 0x50, which means I don’t have to adjust the address at all to match it up with the known player coordinate pointer.

The highlighted instruction viewable in the image above is where I’m going to inject another Abomnification initiation point into.

Player Abomnification Initiation Hook

// Initiates the Abomnification system for the player.
// UNIQUE AOB: 0F 57 C9 0F 10 73 50
define(omnifyPlayerAbomnificationHook, "ACValhalla.exe" + 16590FD)

assert(omnifyPlayerAbomnificationHook, 0F 57 C9 0F 10 73 50)
alloc(initiatePlayerAbomnification,$1000, omnifyPlayerAbomnificationHook)

registersymbol(omnifyPlayerAbomnificationHook)

initiatePlayerAbomnification:
  pushf
  // Back up the registers used to hold return values.
  push rax
  push rbx
  push rcx
  // Push the identifying address parameter and call the Abomnification system.
  push rbx
  call executeAbomnification
  // Restore the preserved values on the stack.
  pop rcx
  pop rbx
  pop rax
initiatePlayerAbomnificationOriginalCode:
  popf
  xorps xmm1,xmm1
  movups xmm6,[rbx+50]
  jmp initiatePlayerAbomnificationReturn


omnifyPlayerAbomnificationHook:
  jmp initiatePlayerAbomnification
  nop 2
initiatePlayerAbomnificationReturn:

With scale generation now implemented for the player, we just need to add a toggle option in the scale application hook in order to allow for the player to become Abomnified.

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)
alloc(abomnifyPlayer,8)

registersymbol(omnifyApplyAbomnificationHook)
registersymbol(abomnifyPlayer)

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    
  cmp [abomnifyPlayer],1
  je continueAbomnification
  // Ensure that the player isn't going to be Abomnified.
  mov rax,playerLocation
  cmp rbx,[rax]
  je applyAbomnificationExit
continueAbomnification:
  // 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:


abomnifyPlayer:
  dd 0

And voila! We now have an Abomnifiable player:

Shows the player Abomnified and fat.
The player is looking nice, Abomnified, and fat.

Wish it wasn’t always dark in my game. And of course, as I’m typing this, the setting sun is now shining on my character, but too late! Already took the picture.

Now that the player is Abomnified, perhaps it’ll make this next…much more difficult issue easier to solve.

Fixing the Periodic Loss of Abomnified Scales

This is one that has been bugging me for a bit, but I didn’t want to draw attention to it on stream, for fear of people being able to see the problem when they otherwise might’ve not even noticed. So I’ve been suffering with it, silently.

When NPCs are a certain distance from the player, they will begin to lose their Abomnified scales, reverting back to their normal size, but only briefly. Then, after a brief period of time, they will go back to their Abomnified scales. All of this will then repeat. This results in a sort of “stuttering” back and forth between unnatural size and natural size.

I knew of this potentially being a problem when I initially implemented the Abomnification system, however any change relating to custom scaling code needs to be evaluated using a cost-benefit analysis approach. I have lots of other things that needs doing, so unless something is desperately needed, it makes sense to try to make do with what we got as far as custom scaling stuff goes.

Anyway, clearly some secondary rendering function is taking over, causing our hook to essentially become superseded. There are many, many functions in the game that do the math responsible for laying out the character model polygonal mesh. I even tried finding some new ones to hook into, in order to take care of this particular problem.

That was just going to end up taking too much time. Instead of all that, I stopped myself, took another look at the problem, and decided to attack it a different way. The scales were most often starting to drop off once NPCs were at a certain distance from the player. Why not then add in some functionality that would only allow for Abomnified scales to be applied if NPCs were within a particular radius?

We already have the functionality required for determining coordinate distances with the Predator system’s getCoordinateDistance function. So, I simply made a few additions to the Abomnification scale application hook in order to incorporate a new abomnificationRadius floating point symbol.

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)
alloc(abomnifyPlayer,8)
alloc(abomnificationRadius,8)

registersymbol(omnifyApplyAbomnificationHook)
registersymbol(abomnifyPlayer)
registersymbol(abomnificationRadius)

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+50]
  call checkBadPointer
  cmp ecx,0
  pop rcx
  jne applyAbomnificationExit    
  cmp [abomnifyPlayer],1
  je continueAbomnification
  // Ensure that the player isn't going to be Abomnified.
  mov rax,playerLocation
  cmp rbx,[rax]
  je applyAbomnificationExit
continueAbomnification:
  // Push the player's and rendering target's coordinates to the stack
  // and then calculate the distance between them.
  mov rax,playerLocation
  mov rcx,[rax]
  push [rcx+50]
  push [rcx+58]
  push [rbx+50]
  push [rbx+58]  
  call findCoordinateDistance
  // If the rendering target isn't within the radius, we abort Abomnification.
  movd xmm0,eax
  ucomiss xmm0,[abomnificationRadius]
  ja 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:


abomnifyPlayer:
  dd 0
  
abomnificationRadius:
  dd (float)30.0

Initially we will be rocking a radius of 30.0, as this seemed to be around where the NPCs started glitching out. After trying out the above code for a bit, I can say that things appear to be much, much better! This is a good workaround for this particular issue — any solution more complete than this just isn’t going to be worth the time investment for me.

The radius might need tweaking, we’ll have to do some serious play to find out. If I do change it at all, you’ll be able to see the final value when I post the tombstone for this game.

Player Kill Animations Killing…The Player

This is a bit of a strange one, although its strangeness is dwarfed by how irritating it was to deal with! When fighting only a very specific type of enemy, I noticed the Player Apocalypse was being triggered during kill animations where the enemy was getting its head smashed. Interestingly, the damage wouldn’t carry over to the player’s health, however any effect unrelated to damage (such as teleportitis) would still occur.

So, enemy would be on ground, we go up to enemy and stomp on their head. Then, for whatever reason, a Player Apocalypse roll goes off, lands on teleportitis, launching us up into the air. We then fall down, and die. Right as the enemy’s head is exploding. A very strange affair indeed.

Naturally, if kill animations (against an enemy) were triggering the Player Apocalypse in general, I’d definitely would have found that during the Apocalypse implementation. Or at least sometime not long afterwards. But no, it’s only for a very, very specific type of enemy. I have no idea what the name of the enemy is, but they look like this:

Shows two shady characters who, when performing a kill animation on, cause the Apocalypse system to think that the player is the one being damaged.
Look at these two shady characters.

They have these sticks, they throw dust at you, and they’re kind of annoying. The specific move in which the Player Apocalypse was getting triggered looks like this:

Shows the player character about to smash an enemy's head.
We’re clearly about to smash the enemy’s head, but we get a Player Apocalypse instead!

It’s a bit hard to make out from above, but the player is standing over the enemy, ready to squash their head. And of course, once that occurs, an entry is entered into the Apocalypse event log, saying we got 69x’d, or double damaged, but with no change actually happening on our player’s health. Incredibly annoying!

So, I put a breakpoint into where the Apocalypse initiation point is inserted, and after triggering it, a very strange picture was painted indeed. It truly, truly appeared as if the game was calling this function of code (the damage application code, where damage is about to be applied to a creature’s health) as if it was indeed the player being damaged.

The current health being worked upon was the player’s health, not the enemy’s. The location structure loaded into the rdi register (what we use to identify the target of the damage in this game) is the player’s, not the enemy’s. Finally, the damage being done appears proportionate to what one expects to do when squashing a head (around 853 damage).

At the end of the function, the huge damage amount is subtracted from our character’s health, giving a fat negative value. But, after all of this, nothing actually carries over to our player’s health. I have no idea why the game is doing this, but it is screwing up our handling of damage. Very annoying.

After trying to find patterns by looking high and low through the available data, I eventually determined (by taking a diff between a stack snapshot of me doing the kill animation and normal damage being done to the player) that a value of 0x3F800000 (which is 1.0 in floating point) would be at [rsp+20] (this isn’t its normal location, rather it is where to find it after we do our stack preservation) only during this unique situation.

So, in order to fix this bug, I simply added a check for this value. Initial testing seems to indicate that the Player Apocalypse no longer goes off during the kill animation, and all other actual damage to the player seems to be handled properly.

Updated Apocalypse Initiation 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
  // Let's grab the damage source right now before the stack shifts
  // anymore.
  mov rcx,[rsp+44A]
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:
  // During kill animations on some enemies, the game will sometimes call
  // this code as if the player is receiving the damage. 
  // [rsp+20] seems to be set to (float)1.0 when this is the case.
  mov rcx,[rsp+20]  
  cmp rcx,0x3F800000
  je initiateApocalypseExit
  // Realign the player's coordinates so it begins at the X coordinate.
  mov rbx,playerLocation
  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 rcx
  pop rbx  
  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

Also, I discovered we were restoring values backed up on the stack in the wrong order. We were restoring rbx prior to restoring rcx. Big oops!

Let the Games Continue!

Alright. We just took care of these middling concerns. I look forward to more of this awesome Omnified gameplay, and you should look forward to it too! Remember, to catch this or any other Omnified gameplay live, you have to tune into my stream at: https://twitch.tv/omni

Drop on by the Discord server to say hello, or ask any questions if you have any at: https://discord.gg/omni

Take care, and keep it comfy.

~Omni