Skip to content

Commit a2c5db7

Browse files
committed
time: add GetTimeCached() frame-stable timestamp
Backports retail 4.x's frame-cached GetTime() behavior under a distinct name, leaving vanilla GetTime() live/per-call. 1.12's Script_GetTime recomputes the OS tick on every call, so same-frame calls can differ; GetTimeCached() samples once per frame (via the shared Tick::WorldTick hook) and returns that until the next frame, on the same epoch as GetTime() so the two are directly comparable. Falls back to a live sample before the first world tick so it never returns 0. Deliberately NOT a perf feature and documented as such: the 1.12 tick source is cheap (GetTickCount shared-page read or rdtsc) and both functions pay the same dominant Lua->C dispatch, so caching saves nothing measurable. The only benefit is frame-stable semantics; the real hot-loop fix is caching GetTime() in a Lua local, which no C function can do for the caller. New global chosen over modifying GetTime() to avoid changing a core vanilla function's behavior.
1 parent 46597f6 commit a2c5db7

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
6666
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
6767
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
6868
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
69-
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
69+
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTimeCached` |
7070
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
7171
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
7272
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |

docs/API.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ build instructions.
494494

495495
- [Time](#time)
496496
- [`GetServerTime()`](#getservertime)
497+
- [`GetTimeCached()`](#gettimecached)
497498
- [`C_Timer.After(seconds, callback)`](#c_timeraftersseconds-callback)
498499
- [`C_Timer.NewTimer(seconds, callback)`](#c_timernewtimerseconds-callback)
499500
- [`C_Timer.NewTicker(seconds, callback, [iterations])`](#c_timernewtickerseconds-callback-iterations)
@@ -11860,6 +11861,40 @@ right call for calendar / log-timestamp / cooldown-sync use cases.
1186011861
> boundary and the timestamp is accurate to within a second of the
1186111862
> engine's clock for as long as the session continues.
1186211863

11864+
### `GetTimeCached()`
11865+
11866+
Returns the same value as [`GetTime()`](https://warcraft.wiki.gg/wiki/API_GetTime)
11867+
— seconds on the engine's uptime clock — but **frame-stable**: every call
11868+
within a single frame returns the identical value, refreshed once per
11869+
frame. Same epoch as `GetTime()`, so the two are directly comparable.
11870+
11871+
Backports the retail 4.x `GetTime()` behavior (retail samples the clock
11872+
once per frame in the main loop) under a distinct name, leaving vanilla's
11873+
`GetTime()` untouched — 1.12's `GetTime()` is *live*, recomputing the OS
11874+
tick on every call, so two same-frame `GetTime()` calls can differ.
11875+
11876+
```lua
11877+
-- Frame-stable: consistent timestamps across a frame's work
11878+
local now = GetTimeCached()
11879+
if now - lastFire >= interval then
11880+
lastFire = now
11881+
...
11882+
end
11883+
```
11884+
11885+
> **Not a performance feature.** The underlying tick source is cheap in
11886+
> 1.12 (`GetTickCount`, a user-mode shared-page read, or `rdtsc`), and
11887+
> both functions pay the same Lua→C call overhead, which dominates. So
11888+
> `GetTimeCached()` is not meaningfully faster than `GetTime()` — its
11889+
> only advantage is the frame-stable semantics. To actually cut cost in a
11890+
> hot `OnUpdate`, cache the value in a Lua local
11891+
> (`local now = GetTime()`) and reuse it; that removes the per-call Lua
11892+
> dispatch, which a cached C function cannot.
11893+
11894+
The cache is refreshed from the shared once-per-frame `Tick::WorldTick`
11895+
hook; before the first world tick (pre-login / glue) it falls back to a
11896+
live sample, so it never returns 0.
11897+
1186311898
### `C_Timer.After(seconds, callback)`
1186411899

1186511900
Schedules `callback` to fire once after `seconds`. Returns nothing.

src/time/Cached.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `GetTimeCached()` — a frame-stable companion to vanilla `GetTime()`.
15+
//
16+
// 1.12's `GetTime()` is *live*: `Script_GetTime` (`0x00515ea0`) calls the
17+
// OS tick source (`FUN_OS_TICKCOUNT_MS` → `GetTickCount`, or QPC in
18+
// timing-mode 1) on every invocation and scales by 0.001, so two calls in
19+
// the same frame can differ. Retail 4.x changed `GetTime()` to read a
20+
// value sampled once per frame in the main loop, so all same-frame calls
21+
// return the identical timestamp. Rather than change vanilla `GetTime`'s
22+
// long-standing live behavior, we expose the frame-stable value under a
23+
// new name and leave `GetTime()` alone.
24+
//
25+
// `GetTimeCached()` returns seconds on the SAME epoch as `GetTime()` (the
26+
// OS millisecond counter × 0.001), so the two are directly comparable —
27+
// the only difference is that `GetTimeCached()` holds constant for the
28+
// duration of a frame. We refresh the cache from the shared once-per-frame
29+
// `Tick::WorldTick` hook (which fires at the tail of each world frame).
30+
31+
#include "Game.h"
32+
#include "Offsets.h"
33+
#include "tick/WorldTick.h"
34+
35+
#include <cstdint>
36+
37+
namespace Time::Cached {
38+
39+
namespace {
40+
41+
// OS millisecond counter sampled at the last frame boundary. 0 until the
42+
// first world tick fires (pre-world). Read as uint32 — `GetTickCount`
43+
// wraps at ~24.86 days, matching how the rest of the codebase treats this
44+
// counter (see aura/Data.cpp), and how `Script_GetTime` masks it to 32
45+
// bits before scaling.
46+
uint32_t g_frameMs = 0;
47+
48+
uint32_t SampleTickMs() {
49+
using TickCount_t = uint32_t(__fastcall *)();
50+
return reinterpret_cast<TickCount_t>(
51+
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
52+
}
53+
54+
// Tail-of-frame refresh. Tail-of-frame-N ≈ start-of-frame-(N+1) minus the
55+
// inter-frame gap, which is all a frame-stable timestamp needs: every read
56+
// during a frame returns the value stamped at the previous boundary.
57+
void OnWorldTick() {
58+
g_frameMs = SampleTickMs();
59+
}
60+
61+
// `GetTimeCached()` → number (seconds). Frame-stable analog of
62+
// `GetTime()`, same epoch. Falls back to a live sample when no frame has
63+
// ticked yet (pre-world / glue), so it never returns 0.
64+
int __fastcall Script_GetTimeCached(void *L) {
65+
uint32_t ms = g_frameMs;
66+
if (ms == 0)
67+
ms = SampleTickMs();
68+
Game::Lua::PushNumber(L, static_cast<double>(ms) * 0.001);
69+
return 1;
70+
}
71+
72+
void RegisterLuaFunctions() {
73+
Game::Lua::RegisterGlobalFunction("GetTimeCached", &Script_GetTimeCached);
74+
}
75+
76+
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
77+
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
78+
79+
} // namespace
80+
81+
} // namespace Time::Cached

0 commit comments

Comments
 (0)