From 45825b0609870f9b0fcaddb9f8a9f5f739a149a1 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:17:58 +0700 Subject: [PATCH] ui_battlefield: a queued battleground's time waited is the time since the status plus the wire's waited ms, computed forwards so no Instant precedes the clock's origin (#628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A queued battleground's time waited was computed backwards: `*at - Duration::from_millis(waited)`, with `waited` the server's u32 milliseconds. On Windows, std's `Instant` is a `Duration` from the performance counter's origin, near boot, so a wait longer than the machine's uptime panics there. On macOS and Linux, `Instant` is a signed timespec and the subtraction cannot panic; the issue's platform note overstated that. The time waited is now taken forwards, as `now.saturating_duration_since(*at) + waited`. That is the same value the reference's `[slot+0x1c] = now − Δ`, read back as `now − stamp`, gives, and the old formula's value everywhere it did not panic. `a_waited_span_longer_than_any_uptime_reads_forwards` pins a 49.7-day wait reading forwards and saturating at the getter's u32. `the_slot_view_reduces_the_clocks` still gives 7 000 ms for 5 s waited plus 2 s since. Built and gated on macOS; the Windows target was not built. Fixes #615 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-app/src/ui_battlefield.rs | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/benilla-app/src/ui_battlefield.rs b/crates/benilla-app/src/ui_battlefield.rs index 98df9e61..cc410bc6 100644 --- a/crates/benilla-app/src/ui_battlefield.rs +++ b/crates/benilla-app/src/ui_battlefield.rs @@ -153,11 +153,12 @@ fn slot_view( let deadline = *at + std::time::Duration::from_millis(u64::from(delta)); view.port_expiration_ms = ms(deadline.saturating_duration_since(now)); } - // Status 1: the raw estimate, and `[slot+0x1c] = now − Δ` read back as `now − stamp`. + // Status 1: the raw estimate, and `[slot+0x1c] = now − Δ` read back as `now − stamp`, which is + // the time since the status plus Δ, taken forwards so no `Instant` precedes the clock's origin. if let Some((estimate, waited)) = status.queued { view.estimated_wait_ms = estimate; - let stamp = *at - std::time::Duration::from_millis(u64::from(waited)); - view.time_waited_ms = ms(now.saturating_duration_since(stamp)); + let since = now.saturating_duration_since(*at); + view.time_waited_ms = ms(since + std::time::Duration::from_millis(u64::from(waited))); } view } @@ -389,6 +390,26 @@ mod tests { } } + #[test] + fn a_waited_span_longer_than_any_uptime_reads_forwards() { + let at = Instant::now(); + let mut queued = status(0, 489, 1); + queued.queued = Some((0, u32::MAX)); + let v = slot_view(Some(&(queued.clone(), at)), None, at); + assert_eq!( + v.time_waited_ms, + u32::MAX, + "49.7 days waited, no stamp needed" + ); + let later = at + std::time::Duration::from_secs(9); + let v = slot_view(Some(&(queued, at)), None, later); + assert_eq!( + v.time_waited_ms, + u32::MAX, + "saturates as the getter's u32 does" + ); + } + #[test] fn the_slot_view_reduces_the_clocks() { let at = Instant::now();