fix: stamp the chat log and FrameXML.log in local time (#723)

The reference stamps each log line from GetLocalTime (0x65a871); benilla stamped UTC. Unix reads
localtime_r, Windows calls GetLocalTime. Built and tested on macOS; the unix path type-checked for
x86_64 Linux and the Windows path for x86_64-pc-windows-msvc in a scratch crate.

Fixes #586

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathih13
2026-10-01 07:21:57 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4772489aad
commit 87f65faffd
3 changed files with 76 additions and 40 deletions
+8 -3
View File
@@ -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]
+67 -36
View File
@@ -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::<SYSTEMTIME>();
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::<libc::tm>() };
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]]);
}
}
+1 -1
View File
@@ -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.