Giving Omnified AC: Valhalla Some Good Ole Testin’
Before debuting Omnified Assassin’s Creed: Valhalla on my stream, we need to subject it to some fairly rigorous testing so that as few bugs pop up as possible. While I wish for my playthroughs of new games to be as blind as possible, I’ll subject myself to an hour or two of gameplay to make sure I’m not constantly fixing things on go-live day.
I’ll be sharing with all of you a record of my observations as they happen. If I encounter something that needs fixing, you will all get to join me through any of the required deep dives through the code, and you’ll get to see the solutions I come up with.
I hope we don’t run into too many problems! But, even if we do, I’ve given myself quite a bit of time to fix any ensuing issues. My intention is to debut the playthrough on January 1st, 2021.
So…let’s see if I can do it!
Living That Toddler Viking Life
One little sequence in the game I was aware of (due to having to set up the initial save file to work from) occurs at the very start, as we witness our hero attending a feast as a very young boy.
Naturally, this is a situation in which all of the player specific hooks we’ve built could easily fail. So, it is with some slight trepidation that I start my playtesting from this very spot in the game.
Luckily, the player-specific hooks seemed to all hold (player’s vitals are being tracked, player isn’t speed boosted, player isn’t Abomnified). The same player-specific code is employed for the hero when they’re a toddler as when they’re all grown up. This bodes well for overall stability for our player hooks for different situations that might pop up in the game.
That being said, I only had to take a few steps out of the character’s bedroom to start noticing some things were amiss.
Dancing People Morphed, but Not Morphing
In the main feast hall, we’re treated to a large crowd of drunken Vikings doing some jigs and having a good time. While they were all obviously Abomnified (shape and size was different from default) none of them were actually Abomnifying (shape and size was not continuously changing).
This is most likely due to the Abomnification initiation point only executing a single time (or zero times?) for each NPC. Previously, it was our belief that the location in code in which we hooked our initiation point into was a continuously executing coordinate polling function for NPCs, even if they’re not moving; however, that does not appear to be the case here.
Let’s open up our NPC polling function and see what addresses it is accessing.
There are a number of NPC location structures being accessed consistently here, however there are more than 11 NPCs on the screen at the moment. Perhaps these 11 addresses belong to NPCs that are actually morphing, though it is rather dark and hard to see in the feast hall.
We’re going to need to see if there’s a more suitable NPC coordinate polling function. We want to find code that is accessing NPC coordinates with an execution rate similar to the one we were using, and we want it to not be affected by whether or not the NPC is moving.
We’ll be looking for the new function by looking at what other functions are accessing one of the addresses shown in the above image. We’ll be looking at what addresses those functions access, and we’ll be choosing the first one we find that is accessing more than 11, while still appearing to only be accessing NPC coordinates (and not coordinates of a snowflake or something).
There are a number of functions to choose from. It is important to note that the one we’re currently using (highlighted in the image) only has an execution count of 420 whereas there are a few other functions with a much higher number of executions.
If we do end up choosing a function with a higher execution rate, we’ll need to account for that by adjusting the number of morph steps on average for each morph cycle (otherwise morph cycles will occur much too fast).
Looking at the first instruction on the list, it is accessing 87 addresses. That’s far too many. This trend continues with the second and third instructions. The fourth instruction, however, is accessing only 20. Is that enough?
Let’s try to do an NPC headcount…it’s so difficult to see in this dark room! I’m having trouble getting an exact count, but there’s at least 30+, so we still need to keep on looking.
Looking through the rest of the instructions, we see a number of them accessing 20 addresses, some accessing 140 addresses, etc. Nothing that perfectly fits. Reviewing everything I just observed, it seems that all of them are accessing either too few or too many addresses. Out of all the ones accessing too many, the ones accessing the least were the first and third instructions we looked at.
The first instruction in the list is executing at approximately four times the current one’s execution rate, whereas the third instruction is executing at the same rate. So, let’s hook our Abomnification initiation point into the area of code the third instruction lives in.
Updated Abomnification Initiation Point Hook
// Initiates the Abomnification system. // UNIQUE AOB: 0F 10 40 30 0F 29 45 00 0F 28 define(omnifyAbomnificationHook, "ACValhalla.exe" + E80275) assert(omnifyAbomnificationHook, 0F 10 40 30 0F 29 45 00) alloc(initiateAbomnification,$1000, omnifyAbomnificationHook) registersymbol(omnifyAbomnificationHook) initiateAbomnification: pushf // Ensure that the player location struct has been initialized. push rbx mov rbx,playerLocation cmp [rbx],0 pop rbx je initiateAbomnificationOriginalCode // Back up the registers used to hold return values. push rax push rbx push rcx // The addresses in this function are aligned so that the coordinates // begin at 0x30, whereas their alignment in most of the other places we // work with them is such that the coordinates start at 0x50. add rax,-20 // Ensure that the player isn't going to be Abomnified. mov rbx,playerLocation mov rcx,[rbx] cmp rcx,rax je initiateAbomnificationExit // Push the identifying address parameter and call the Abomnification system. push rax call executeAbomnification initiateAbomnificationExit: // Restore the preserved values on the stack. pop rcx pop rbx pop rax initiateAbomnificationOriginalCode: popf movups xmm0,[rax+30] movaps [rbp+00],xmm0 jmp initiateAbomnificationReturn omnifyAbomnificationHook: jmp initiateAbomnification nop 3 initiateAbomnificationReturn:
After saving the changes and re-applying our hack, everything is working like a charm! All dancing villagers were merrily morphing.
With this fix in, I must say that everything is looking fantastic. Holy crap, the morphing is spectacular here in this opening scene. There’s going to be so many amazing moments on stream! All randomly determined of course haha. It looks really freaky when the adults come up to the kid and put their misshapen hands around his head, etc.
More Tiny Dancing People Issues
I do notice that there are a number of dancers in this scene that are all smallified — which is essentially when the static unnatural morphing mode has been selected for a creature, and the static randomly determined size is beneath a threshold; this results in an even greater reduction in size in order to exaggerate the smallness.
Back to my observation: I noticed a number of NPCs are all using this same morphing mode, with what appears to be the same determined size. Either their coordinates are still not being hit in the initiation point method, or we’re getting morph scale data lookup collisions with the new morph scale data resolution method.
I’m only seeing this for a small number of NPCs, all next to each other and in a dancing line. In order to see if it is due to lookup collision, I restart the program. If it’s still the same NPCs, all still small, then the chances of a lookup collision are extraordinarily tiny.
After restarting the program, I observe that they are indeed all still small. This is a bit perplexing. Even if all of these NPCs only received a single Abomnification initiation call, they should all be stuck at random sizes, not all at a small size. To get to the bottom of this, we’re going to want to look at what the Abomnification system is returning and for what location addresses.
Looking at the
getAbomnifiedScales function, I look at all the addresses being accessed by the instruction
mov eax,[rdx+04]. This is the instruction that is responsible for loading the Abomnified width scale for a creature.
Oops. A bunch of NPCs are essentially getting 0x scale multipliers. It has nothing to do with static unnatural smallified morphing modes at all! Although it does bring up a semi-related issue to smallified creatures, but we’ll touch on that later.
After writing down the location coordinates for one of these NPCs, I found that they are unfortunately not being hit up by our new NPC location polling function.
Ahh what a shame. So, our new initiation point hook includes much, much more of the NPCs appearing on screen, but it still isn’t hitting up all of them. So, we’re going to have to try….once again. This time, at least, I have one of those dancing, stationary NPCs coordinates, so let’s see what functions are accessing them.
Alright, we have a list of NPC coordinate polling functions which is more refined than the one we had earlier. For whatever reason, these stationary, dancing NPCs were not being covered by some of the earlier polling functions; but, now that we have this list, we can ensure all NPCs are covered by looking for the instructions that show up both on this list and the one from earlier.
The first instruction highlighted here was also on the first list of location polling functions, and it also happened to have the same rate of execution as the function we’re hooking into now. Looking at the number of addresses this one is accessing, we get 114 addresses vs the 87 addresses being accessed by the older function.
Alright. Let’s update the Abomnification initiation point…one (hopefully) more time.
Final (?) Abomnification Initiation Point Hook
// Initiates the Abomnification system. // UNIQUE AOB: 0F 10 56 50 4C 8D 4C 24 30 define(omnifyAbomnificationHook, "ACValhalla.exe" + 28117E0) 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:
Because the location structure is aligned more closely to how we use it in other places, we were able to remove the location structure realignment part of the code.
After plugging in this new code, all visible NPCs were morphing! Eureka! Everything is fantastic now!
Default Scales for Lookup Fails
One other issue brought to my attention by the tiny dancing people was the fact that a scale multiplier of 0x is returned if a creature doesn’t have any morphing scale data registered.
While this is somewhat useful in bringing to my attention NPCs whose coordinates aren’t being covered by the Abomnification initiation point, I would rather that a creature simply retain its normal dimensions if something isn’t working right — especially given that I’d prefer any bumpiness incurred from unforeseen problems reduced as much as possible when streaming this live.
This requires changes to the
getAbomnifiedScales function found in the Omnified framework. I’ll just go over the particular changes since Omnified system code is under the purview of Omnified Design articles, and I still need to write one for the Abomnification system. And I will soon!
getAbomnifiedScales Unregistered Morphing Scale Data Update
. . . mov eax,[rdx+4] mov ebx,[rdx+8] mov ecx,[rdx+C] // Check if we are getting empty data for return values. // If so, we'll return default values instead. cmp eax,0 je getAbomnifiedScalesDefault jmp getAbomnifiedScalesCleanup getAbomnifiedScalesDefault: mov eax,[defaultScaleX] mov ebx,[defaultScaleX] mov ecx,[defaultScaleX] getAbomnifiedScalesCleanup: pop rdx ret 8
Now if there are any morphing mishaps on stream then at least the character will be normally sized. Hopefully there won’t be any mishaps though!
Smallified Is Too Small
Another issue brought up by the 0x scale problem had to do with how smallified NPCs were most likely going to end up being too small for my taste. The issue here has to do with the clothes of the NPCs; basically, clothes scale with the rest of the body, up to a certain point. Once the body starts getting too small, the clothes, or at least part of the clothes, stop getting smaller.
So you end up basically with a tiny bundle of clothes running around, unable to actually see the tiny NPC inside that bundle. It sort of ruins the point of the smallified morph mode, which is to have a super tiny NPC…not a tiny NPC with big clothes.
The reason behind the tiny NPC in clothing bundle problem has to do with the existence of a specific scaling routine for a particular section of clothes. I stumbled onto a dedicated rendering function for this very thing when searching for what ended up being the code we hooked the Abomnification scale application hook into.
I’m not going to bother adding an additional hook into this clothing function, instead I’m just going to tweak the
unnaturalSmallX parameter, which controls how much the NPC gets smallified during…smallification. Currently, it is set to the default, which is 0.3.
Static unnatural morphs are uniform in nature, with limits set by the
abominifyHeightResultUpper parameters, with the upper limit being set to 1.6x for this game. The intent behind smallification is for it to give us a noticeably tinier creature. So, given that the upper height limit has been reduced for the game, we have a bit more latitude as far as the
unnaturalSmallX parameter goes.
If we want to achieve a 90% normal size when the maximum possible height is rolled, then we’ll want to use an
unnaturalSmallX parameter value of 0.5625. This isn’t really perfect, as 90% size is too close to the normal size…we might just have to change how we do the static unnatural morphing mode in general, where we have a specific range of small and/or big sizes we generate for.
We’ll explore that possibility if we need to. Until then…
Updated Abomnification External Parameters
abominifyWidthResultUpper: dd #250 abominifyHeightResultUpper: dd #160 abominifyDepthResultUpper: dd #200 unnaturalSmallX: dd (float)0.5625
This may require additional tweaking, or the additional logic I alluded to just a bit ago.
With all of this done, I confidently proceeded to join in the festivities at the feast hall, and finished the opening scenario in the game without any further incident. Yay.
Grown Up Viking Man
With the opening sequence completed, we’re now playing the game as the grown up Viking hero guy. Eivar, I think his name is. The playtest will continue until we get past the first few combats are so, which will give us some good opportunities to see if everything is tuned correctly in that regard.
Tuning the Damage
Right away, after the first combat, it became evident to me that it will be best to play this on Normal difficulty, otherwise we’re going to get a lot of one shots from the extra damage Apocalypse effect. One shots should really only occur for the sixty nine damage Apocalypse effect.
As far as I understand, it seems that the difficulty options in the game only affect the enemy health and damage. I will play games at a higher difficulty if it affects things like AI and all that, but I refuse to play difficulties that just make enemies “bullet sponges”. The best kind of difficulty is one where you die fast and enemies die fast.
Conversely, it seems like enemies do way too little damage on average with Normal difficulty when using the stock extra damage multiplier of 2.0. I really think it will need to be raised to 3.0 instead. Doing so will cause the average damage increase for this game to be 3x then. Not including all those insane sixty nine and teleportitis-to-our-death damage amounts, of course.
Updated Apocalypse External Parameters
negativeVerticalDisplacementEnabled: dd 0 teleportitisDisplacementX: dd (float)5.0 yIsVertical: dd 0 extraDamageX: dd (float)3.0
Triple damage on Normal difficulty might be perfect. A strong (charged) attack from an enemy did the following damage to the character:
16:01-Enemy rolls a 3: 3x DAMAGE causing 96 damage to the player! Player now has 4 health.
So it almost killed the character. Obviously, if I had anything much less than 100% health, I would’ve been dead. Remember, we removed the one hit kill protection from this game, which would protect against dying from something like that.
In comparison, this is the damage logged from a single strong attack while we were on the hardest difficulty:
15:50-Enemy rolls a 7: RISK OF MURDER! 15:50-Enemy rolls a 3: WHEW! Just normal damage causing 74 damage to the player! Player now has 26 health.
We were lucky here, and got a normal damage roll. We would’ve been murdered regardless of our health if we hit the most common effect (extra damage). So, the 3x damage on normal difficulty is an improvement, however it leaves very little breathing room.
Despite the tiny breathing room, I believe it just might be perfect, and gives us incentive for making sure our health is topped off at all times. If I’m asked by a viewer what difficulty I’m playing on, I’ll be sure to say “Omnified difficulty”, as this is nothing even close to Normal.
Fake Tom Petty: Teleportitis Glitchin’
In an Omnified game, a clip from Tom Petty’s Free Fallin’ plays if the character is hit and receives a teleportitis effect such that it will result in them falling to their death. Pretty cool. Anyway, I noticed some instances of Tom Petty singing without any sort of falling death actually occurring.
Looking at the Apocalypse log, we see that the player should have definitely fallen to their death:
16:10-Enemy rolls a 5: SUDDEN TELEPORTITIS (40.209999084473) causing 32 damage to the player! Player now has 68 health.
The number in parenthesis represents the vertical shift in the player’s Z coordinate that should’ve happened. The player was supposed to have been launched up 40 units into the sky, which is way off the ground. In reality however, no kind of shift happened at all.
This means that the teleportation was a dud. I’ve seen this sort of thing before in one other game I’ve Omnified: Dark Souls (the first one). Basically, any modification of the player’s coordinates during specific animations, in particular animations occurring when getting smacked around, would be ignored.
The reason why our teleportitis coordinates were being ignored in Dark Souls can be best illustrated by listing the sequence of events that would play out:
- The player is about to receive some damage.
- Apocalypse system takes over, and determines that a teleportitis is going to occur. The player’s coordinates are directly manipulated to the new location the player is being teleported to.
- A “receiving damage” animation begins on the player in response to some damage.
- The location update code is executed as normal (basically, like in most games, in response to a tick, a timed event) with new coordinate values based on where the character should be due to the damage animation.
- The damage animation coordinate values are calculated based on the character’s position before receiving the damage. They are committed to memory, overwriting our teleportitis-based coordinates.
The way we worked around this with Dark Souls was to have a
teleported flag set after a teleportitis effect went off, which would then result in the next execution of the location update code in the game to be ignored.
teleported flag would then be reset and everything would continue on as normal, as the “receiving damage” animation would, from that point on, be based on the coordinates set during the teleportitis effect, not the coordinates as they were prior to receiving the damage.
From observations made during the implementation of the Predator system, there is only one function in code responsible for updating the location coordinates of a creature. Would implementing a similar workaround work for Assassin’s Creed: Valhalla? It might, and it might not. Let’s try it out.
Bypass Teleportitis Overwrite 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" + 7E9D8A) 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:
This is a new hook that actually hooks directly into the location update code. The Predator system, if you remember, always hooks into the movement application code (ideally at least), which means the location update code, until now, was free for the hookin’.
After writing up this hack and plugging it in, it appears (6 teleports or so later) that everything is working great! Hell yeah! No more fake Tom Petty! I am very pleased to see the exact same type of workaround that worked for Dark Souls also worked for this.
Double Dipping Apocalypse From Fall Damage
It was while I was fixing the teleportitis issue that I noticed another problem popping out at my face: the Apocalypse system was being triggered by fall damage.
This shouldn’t have been a surprise, but Apocalypse-triggering fall damage is something I always I forget to check for and address during initial Apocalypse implementation. In some games the execution path for both falling damage and damage from an enemy is the same, and in some games it is not. Hard to give statistics here off the top of my head.
Well anyways, this is one of those games where it is the former case. Usually it is much too punishing to allow for fall damage to trigger an Apocalypse roll, as it makes normal leaps (which often the game can force you into making) potentially fatal. We’re going to have to filter fall damage out.
If you don’t remember, we found a place on the stack during Apocalypse implementation where the root structure for the character responsible for doing the damage is stored. Well…fall damage shouldn’t have any kind of root structure, so I wonder what will be on the stack when we’re getting hurt because of a fall?
After placing a breakpoint in our Apocalypse hook and falling down, it was as I hoped: nothing. There is no address in the damage source location on the stack if we’ve fallen. So, we’ll just filter that scenario out.
Updated Apocalypse Initiation Point Hook
// Initiates the Apocalypse system. // Unique AOB: 2B 45 BC 89 44 24 58 define(omnifyApocalypseHook, "ACValhalla.exe" + 213DA31) 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] // 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 // 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:
With the new code in, fall damage no longer triggers any Apocalypse rolls! And…it appears all other damage is still being correctly processed! Hope I don’t get bit on the butt by any last minute edge cases.
Alright. Think we’ve fixed all the immediate issues. If there are any other issues, I guess I’ll have to fix them live! Things seem super playable! I’m going to update my graphics drivers and do one more quick and tiny playthrough (just in case lol), but other than that, I think we’re good to go!
I cannot wait to play this game! Omnified Assassin’s Creed: Valhalla is going to be quite the trip. Catch it live on my stream at: https://twitch.tv/omni
See ya there.