[jak2/3] Fix same-attack-invulnerable-timeout clock drift original game bug (#3978)

So `target` has some logic here which checks if the pending attack has
the same ID as the last attack, and if so it checks for a 2s grace
period `(-> *TARGET-bank* same-attack-invulnerable-timeout)` before the
attack will actually count:

https://github.com/open-goal/jak-project/blob/7320bfc068acfa385f929b176f61caf3b7aabbbe/goal_src/jak3/engine/target/target-util.gc#L1664-L1674

This `same-attack-invulnerable-timeout` check uses `time-elapsed?`,
which under the hood references `(current-time)` AKA `(-> PP clock
frame-counter)`, which makes sense.

However the code that actually stores the `attack-time` uses a different
clock `(-> *display* base-clock frame-counter)`:

https://github.com/open-goal/jak-project/blob/7320bfc068acfa385f929b176f61caf3b7aabbbe/goal_src/jak3/engine/target/target-util.gc#L1765-L1768

So if these two clocks get out of sync - say the `target` process clock
falls behind the `*display*` clock - then we can end up storing an
`attack-time` that's "in the future" from `target`'s perspective,
effectively increasing the `same-attack-invulnerable-timeout`.

This clock drift can happen in real gameplay - Usual today was having it
happen consistently with the route he was attempting for NoOOB. I was
able to reproduce it consistently in OpenGOAL as well:
- get "invuln 2" (i.e. you have `(target-flags disable-attacks)` but not
`(focus-status dead ignore)`)
- restart mission at the top of temple before the glider mission trigger
- immediately go into the trigger and fall off the cliff (during the
black screen)
- you'll get the glider cutscene, but should respawn back at the bottom
of temple

Somewhere in this^ cutscene/blackout, the two clocks drift apart -
presumably `target`'s clock is paused but the other is not. Later in the
speedrun, this causes the extra long invuln timeout bug, which wastes
time while trying to intentionally lower health.

https://www.youtube.com/watch?v=WD2MLj8ccfg

As far as I can tell, any other code interacting with `attack-time` also
uses `(current-time)` or one of the wrapping macros like `set-time!` or
`time-elapsed?`
This commit is contained in:
Matt Dallmeyer
2025-12-27 19:38:33 -08:00
committed by GitHub
parent f8d5aaffe9
commit 04f1f93bec
2 changed files with 4 additions and 3 deletions
+2 -2
View File
@@ -1456,8 +1456,8 @@
(dotimes (a2-2 8)
(let ((v1-9 (-> self attack-info-old a2-2)))
(when (= (-> arg0 id) (-> v1-9 id))
(if (not (time-elapsed? (-> v1-9 attack-time) (the-as time-frame (-> *TARGET-bank* same-attack-invulnerable-timeout)))
)
;; og:preserve-this fix clock drift bug which can lead to longer invuln timeouts (use base-clock instead of target-clock)
(if (not (>= (- (-> *display* base-clock frame-counter) (-> v1-9 attack-time)) (the-as time-frame (-> *TARGET-bank* same-attack-invulnerable-timeout))))
(return #f)
)
(cond
+2 -1
View File
@@ -1669,7 +1669,8 @@
(dotimes (a2-2 8)
(let ((v1-9 (-> self attack-info-old a2-2)))
(when (= (-> arg0 id) (-> v1-9 id))
(if (not (time-elapsed? (-> v1-9 attack-time) (-> *TARGET-bank* same-attack-invulnerable-timeout)))
;; og:preserve-this fix clock drift bug which can lead to longer invuln timeouts (use base-clock instead of target-clock)
(if (not (>= (- (-> *display* base-clock frame-counter) (-> v1-9 attack-time)) (-> *TARGET-bank* same-attack-invulnerable-timeout)))
(return #f)
)
(cond