diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml index ef1e7e98..3d448e10 100644 --- a/crates/benilla-app/Cargo.toml +++ b/crates/benilla-app/Cargo.toml @@ -32,7 +32,8 @@ half = "2" include_dir = "0.7" # The 1.12 chat filters (`text_filter`), with one `RegexSet` for the any-match pass per line. regex = "1" -# `getrusage` for perf CPU time; the audio clock and render thread's realtime policy on macOS. +# `getrusage` for perf CPU time; `localtime_r` for the log stamp; the audio clock and render +# thread's realtime policy on macOS. libc = "0.2" # PNG encoding only: the macOS cursor's NSImage data and screenshots. image = { version = "0.25", default-features = false, features = ["png"] } @@ -93,9 +94,13 @@ objc2-foundation = { version = "0.2", features = ["NSArray", "NSData", "NSGeomet [target.'cfg(not(target_os = "macos"))'.dependencies] cpal = "0.17" -# Windows CPU clocks for `perf::clock`. +# Windows CPU clocks for `perf::clock`; `GetLocalTime` for the log stamp (`ui_chat::logging`). [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading"] } +windows-sys = { version = "0.59", features = [ + "Win32_Foundation", + "Win32_System_SystemInformation", + "Win32_System_Threading", +] } # The login cycle's fake realmd runs the server half of SRP6 (`net::io::cycle_tests`). [dev-dependencies] diff --git a/crates/benilla-app/src/ui_chat/logging.rs b/crates/benilla-app/src/ui_chat/logging.rs index ddc10ba3..bf27ae42 100644 --- a/crates/benilla-app/src/ui_chat/logging.rs +++ b/crates/benilla-app/src/ui_chat/logging.rs @@ -1,9 +1,9 @@ //! The `LoggingChat`/`LoggingCombat` files, `WoWChatLog.txt` and `WoWCombatLog.txt`: the stock //! `/chatlog` and `/combatlog` flip the VM's flag and print the notice (`ChatFrame.lua:675-695`); -//! this appends each line as the window shows it, stamped `M/D HH:MM:SS.mmm` in UTC, as the -//! process has no local time zone. Deviation: the files live in `benilla-config/Logs/`, not the -//! install's `Logs`, because the install is read-only. A file that fails to open leaves the flag -//! set: Lua already printed "enabled". +//! this appends each line as the window shows it, stamped `M/D HH:MM:SS.mmm` in local time. +//! Deviation: the files live in `benilla-config/Logs/`, not the install's `Logs`, because the +//! install is read-only. A file that fails to open leaves the flag set: Lua already printed +//! "enabled". use std::io::Write as _; @@ -71,37 +71,59 @@ impl ChatLogFiles { } } -/// `M/D HH:MM:SS.mmm` of now, UTC: the reference's log-line stamp (`"%u/%u %02u:%02u:%02u.%03u "`, -/// `0x866aa0`, from `GetLocalTime` at `0x65a871`), which `crate::ui_script::load_log` shares. +/// `M/D HH:MM:SS.mmm` of now, local time: the reference's log-line stamp +/// (`"%u/%u %02u:%02u:%02u.%03u "`, `0x866aa0`, from `GetLocalTime` at `0x65a871`), which +/// `crate::ui_script::load_log` shares. pub(crate) fn stamp() -> String { + let [month, day, hour, min, sec, milli] = local_now(); + format!("{month}/{day} {hour:02}:{min:02}:{sec:02}.{milli:03}") +} + +/// `[month, day, hour, minute, second, millisecond]` of now, from the reference's `GetLocalTime`. +#[cfg(windows)] +fn local_now() -> [u32; 6] { + use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::GetLocalTime}; + // SAFETY: `GetLocalTime` fully writes the out-param and reads nothing from it; zeroed is + // valid for a struct of plain integers. + let t = unsafe { + let mut t = std::mem::zeroed::(); + GetLocalTime(&mut t); + t + }; + [ + t.wMonth, + t.wDay, + t.wHour, + t.wMinute, + t.wSecond, + t.wMilliseconds, + ] + .map(u32::from) +} + +/// `[month, day, hour, minute, second, millisecond]` of now, in the process's time zone. +#[cfg(unix)] +fn local_now() -> [u32; 6] { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); - let secs = now.as_secs(); - let (_, month, day) = civil_from_days((secs / 86_400) as i64); - let of_day = secs % 86_400; - format!( - "{month}/{day} {:02}:{:02}:{:02}.{:03}", - of_day / 3600, - (of_day / 60) % 60, - of_day % 60, - now.subsec_millis() - ) + let [month, day, hour, min, sec] = local_fields(now.as_secs() as i64); + [month, day, hour, min, sec, now.subsec_millis()] } -/// Days since 1970-01-01 → `(year, month, day)`, proleptic Gregorian: the `civil_from_days` -/// algorithm. -fn civil_from_days(z: i64) -> (i64, u32, u32) { - let z = z + 719_468; - let era = z.div_euclid(146_097); - let doe = z.rem_euclid(146_097); - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = (doy - (153 * mp + 2) / 5 + 1) as u32; - let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; - (if m <= 2 { y + 1 } else { y }, m, d) +/// `[month, day, hour, minute, second]` of an epoch second, through `localtime_r`; UTC when that +/// fails, which POSIX allows only on `EOVERFLOW`, a year past `int`. +#[cfg(unix)] +fn local_fields(secs: i64) -> [u32; 5] { + let time = secs as libc::time_t; + // SAFETY: `localtime_r` fully writes the out-param and reads nothing from it; zeroed is valid + // for its integers and its nullable zone-name pointer. + let mut tm = unsafe { std::mem::zeroed::() }; + if unsafe { libc::localtime_r(&time, &mut tm) }.is_null() { + let c = benilla_ui::civil::from_unix(secs); + return [c.month, c.day, c.hour, c.min, c.sec]; + } + [tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec].map(|v| v as u32) } /// The VM's two flags → the two files, on the frame either flag moves. @@ -128,15 +150,24 @@ pub(super) fn plugin(app: &mut App) { ); } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests { - use super::civil_from_days; + use crate::local_state::test_env::{EnvGuard, ENV_LOCK}; + extern "C" { + fn tzset(); + } + + /// Under a zone 5:45 east of UTC, the epoch reads 05:45, and 20:00 UTC is past local midnight. #[test] - fn the_civil_conversion_lands_on_known_dates() { - assert_eq!(civil_from_days(0), (1970, 1, 1)); - assert_eq!(civil_from_days(19_723), (2024, 1, 1)); - assert_eq!(civil_from_days(20_700), (2026, 9, 4)); - assert_eq!(civil_from_days(-1), (1969, 12, 31)); + fn the_stamp_fields_follow_the_local_zone() { + let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let tz = EnvGuard::set("TZ", "NPT-05:45"); + // SAFETY: `tzset` rereads `TZ`; the lock keeps other env tests out. + unsafe { tzset() }; + let fields = [super::local_fields(0), super::local_fields(72_000)]; + drop(tz); + unsafe { tzset() }; + assert_eq!(fields, [[1, 1, 5, 45, 0], [1, 2, 1, 45, 0]]); } } diff --git a/crates/benilla-ui/src/civil.rs b/crates/benilla-ui/src/civil.rs index a919a1a7..60476560 100644 --- a/crates/benilla-ui/src/civil.rs +++ b/crates/benilla-ui/src/civil.rs @@ -1,5 +1,5 @@ //! Broken-down UTC time from an epoch second, the calendar behind `date()` and screenshot names. -//! Deviation: UTC where the reference's callers use local time, because the workspace has no +//! Deviation: UTC where the reference's callers use local time, because this crate has no //! timezone source. /// Broken-down UTC time.