Reverse-engineer meh321 discovered the underlying cause of the "ability condition bug." As it turns out, the bug is less specific to abilities and to conditions, and more specific to active effects' elapsed time. It's an issue with floating-point precision, which prevents the game from properly adding small numbers (e.g. the seconds per frame, like 0.016 at 60FPS) to an active effect's elapsed time once the elapsed time has increased high enough.

If you're not familiar with floating-point imprecision, the video below examines the topic from the perspective of Super Mario 64, and will hopefully do a good job of demonstrating the idea.



It's widely known that spells' conditions update every second; what's less known is that this isn't a global timer. The game doesn't update all spells' conditions every second; rather, an active effect's conditions will update at one-second intervals after that effect has been applied. This staggers out all of the condition checks, so that the game doesn't have to do absolutely all of the work for spell conditions on one single frame, every second. The game, then, handles conditions by re-checking them when the following expression is true:

floor(elapsed_time / interval) ≠ floor((elapsed_time + frame_time) / interval)

So if we can't add the frame time (seconds per frame) to the elapsed time, then we can't re-process conditions. When an effect has been active for about three real-world days, the elapsed time in seconds will be high enough that attempting to add 0.016 to it (the frame time for 60FPS) produces no change in the value. This problem mainly affects abilities, which track elapsed time despite being temporary, but it also affects temporary effects with long durations: the elapsed time itself can't advance, so if someone makes a spell with, say, a 7-day duration, then that spell will effectively be endless.

There's a wrinkle here: fast-traveling, waiting, sleeping, and serving time in jail will simulate the passage of real-world time. If you use the default timescale of 20, then 5 in-game hours will pass in 900 real-world seconds; accordingly, if you skip 5 in-game hours by waiting or a similar means, the game will actually perform that conversion in reverse and advance real-time timers on actors (including the elapsed times for active effects) by 900 seconds. These advancements can be enough to overcome floating-point imprecision, allowing stuck effects to reprocess conditions (or expire) for just one frame.

The approach that Cobb Bug Fixes takes is to patch the code that advances the elapsed time, and the code that checks the condition update interval, to synch both to a custom timer if the elapsed time is high enough for the vanilla approach to fail. Our custom timer ticks up from zero and resets one frame after it exceeds the condition update interval; broken active effects will act when the timer exceeds the interval. In other words: if we can't advance the elapsed time by 0.016 every 0.016 seconds, then let's advance it by 1 second every 1 second. With this change, conditions should always reprocess correctly (because we use a looping timer divorced from the elapsed time), and the elapsed time should properly advance until an effect has been active for about 194 real-world days.

It must be noted that I was only able to test this fix "in the lab:" during testing, I used a version of the fix that always used my custom timer, even for effects with small elapsed times. I don't think I have any PC saves available that not only have condition-sensitive abilities on the player, but that have had those abilities for longer than three straight days' worth of playtime.

Article information

Added on

Edited on

Written by

DavidJCobb

8 comments

  1. Darklustre
    Darklustre
    • member
    • 4 kudos
    I love nerd talk. It's just so damn sexy.
  2. Nund
    Nund
    • member
    • 14 kudos
    Wouldn't the best solution be to make something like thirty "update points" distributed equidistantly over one second, have each of them update its assigned magic effects every second, and then just have the game assign new magic effects randomly to one of the currently least occupied ones? I. e., circumvent the elapsed time trackkeeping altogether, while still keeping the game from having to do everything on the same frame? Or is that hard-coded in a way so that a .dll cannot access it?
    1. DavidJCobb
      DavidJCobb
      • premium
      • 383 kudos
      Your wording is unclear, but it sounds like you're encouraging the use of two timers on each effect: one for the elapsed time, and a second cyclical timer that counts up to one second and resets (this being both the mechanism needed for your "update points," and a system that would make those update points redundant). Implementing it that way would require me to store additional data in memory for every active effect, which requires me to patch a lot of unrelated code (either to make more room in every effect's data for the new timer, or to be notified of effects' deletion so I can safely track information about them remotely).

      As it stands, the current approach is to use a shared cyclical timer for all active effects whose elapsed times are now too distant to use as a timer. This shared timer is one thing in one place, easy to access from anywhere.
    2. Nund
      Nund
      • member
      • 14 kudos
      That's not quite what I meant. I'll try to word it precisely, and in-depth.

      Each of the update points is essentially an object that has a cyclical timer with a period of one second and a list of magic effects that are assigned to it. The timer is inactive by default, as long as the list is empty. When a new effect is assigned to the update point, a check is made whether the list was empty before the assignment, in which case the timer gets activated. Likewise, when an assigned magic effect ends (elapsed time exceeds its duration or external dispel via script or dispel effect), it gets removed from the list, and a check is made whether the list is now empty, in which case the timer is deactivated. This way, only the timers that are needed at the moment are ever active, and the amount of checks needed for determining the activation status is kept to a minimum. Also, the magic effects themselves need no additional data on them, because the timers run indepently of the effects and call updates on their assigned effects every second.

      The timers between the n different update points are spread apart by one nth of a second. There is a global list of least occupied update points that is updated whenever one of the points has one of the aforementioned checks. Whenever a new magic effect starts, it gets randomly assigned to an element of that list. This assures that the game never has to check too many conditions on the same frame.

      So essentially what I mean is n potentially shared cyclical timers that circumvent the original update code completely, but still manage to spread apart the update events reasonably, which, as I understand from your article, is the sole reason for the janky original code existing in the first place. The problem I see with one shared cyclical timer that stores all the effects that fall victim to the jank is that it might get overfilled eventually, so that whole singing and dancing about spreading the updates apart in the first place becomes useless.
    3. DavidJCobb
      DavidJCobb
      • premium
      • 383 kudos
      That's an interesting idea. It could potentially be doable if I detect effect dispelling by relying on the internal TESActiveEffectApplyRemoveEvent event (used to power Papyrus's OnEffectStart and OnEffectFinish events; it should fire even if an effect has no scripts on it), but I don't know how to get a reference to the specific ActiveEffect via the internal event; I'd have to do more research.

      One possible performance hazard is that I'd have to store the active effects' memory addresses in some kind of map or list, since as mentioned adding more data (such as information on what timer they're sharing) to the ActiveEffect itself is difficult. Lookups could incur a small performance cost, and we'd need to do those lookups every time the timer needs to be checked (i.e. every frame, for every effect).

      I'm currently occupied with other projects, but I can pursue this line of investigation if people report performance issues in long-running savegames, particularly if the reported issues go away when this mod is disabled.
    4. Nund
      Nund
      • member
      • 14 kudos
      Unfortunately I have absolutely no clue about reverse engineering, Skyrim's internal coding, dll injection or how to even begin finding out about these things, or else I'd do some research myself.

      One possible performance hazard is that I'd have to store the active effects' memory addresses in some kind of map or list, since as mentioned adding more data (such as information on what timer they're sharing) to the ActiveEffect itself is difficult. Lookups could incur a small performance cost, and we'd need to do those lookups every time the timer needs to be checked (i.e. every frame, for every effect).
      I can't imagine that those lookups would incur more cost than the actual condition checks on the magic effects themselves, and the game seems to manage quite a lot of them every second, so I'd assume that wouldn't be the bottleneck. You wouldn't have to do it every frame, only on the frames that coincide with the zero pass of one of the update point timers, so that is some reason for careful optimism with regards to performance, I suppose.
  3. AlaQedeso
    AlaQedeso
    • member
    • 15 kudos
    Nice! It correlate with my thoughts on matter and testing results. Thanks!

    We have spell (ability, whatever) with long duration. I will refer its current uptime as [X].
    Time range = seconds - seconds (hh:mm:ss - hh:mm:ss)
    Under what conditions a bug will happen:
    If [x] = 131072 - 262144 (36:24:32 - 72:49:04) and your FPS > 128
    If [x] = 262144 - 524288 (72:49:04 - 145:38:08) and your FPS > 64
    If [x] = 524288 - 1048576 (145:38:08 - 291:16:16) and your FPS > 32
    If [x] = 1048576 - 2097152 (291:16:16 - 582:32:32) and your FPS > 16


    Guide - how to quickly test it yourself:
    install this esp: https://mega.nz/#!aHJliCgb!UPV9kJImqYokFpDkUQEnh7RsGDRPjixbBn7It8ytV5g
    cap fps to 20
    launch game
    player.addspell xx000801 (where xx = place of my esp)
    set timescale to 0.0033
    use ACB_test_SPEL
    "wait menu" > wait 1 hour (game can freeze for about 1 minute, it's ok)
    check active effects - "Time left" won't change!


    If you have plans on other 2 bugs fixed by meh and need help in testing - let me know.
    And another thanks for not dropping LE and especially for doing such godlike job with those fixes!
  4. smokeybear187
    smokeybear187
    • premium
    • 19 kudos
    In Cobb We Trust.