From 04f1f93bec010d9cc508cbe7613bb93ebcc6015f Mon Sep 17 00:00:00 2001 From: Matt Dallmeyer Date: Sat, 27 Dec 2025 19:38:33 -0800 Subject: [PATCH] [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?` --- goal_src/jak2/engine/target/target-util.gc | 4 ++-- goal_src/jak3/engine/target/target-util.gc | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/goal_src/jak2/engine/target/target-util.gc b/goal_src/jak2/engine/target/target-util.gc index f3fc5a141b..6fbfe14362 100644 --- a/goal_src/jak2/engine/target/target-util.gc +++ b/goal_src/jak2/engine/target/target-util.gc @@ -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 diff --git a/goal_src/jak3/engine/target/target-util.gc b/goal_src/jak3/engine/target/target-util.gc index ce61a7558b..b0eadc9051 100644 --- a/goal_src/jak3/engine/target/target-util.gc +++ b/goal_src/jak3/engine/target/target-util.gc @@ -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