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:
co-authored by
Claude Opus 5.5
parent
4772489aad
commit
87f65faffd
@@ -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]
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
Reference in New Issue
Block a user