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:
- No support for player Abomnification.
- 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”.
- 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:
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:
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:
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:
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