Compare commits

...
1 Commits
Author SHA1 Message Date
Brues 39d22ac124 fix(time): centralize engine ms-tick reads in Time::Clock
The engine's OS millisecond counter (FUN_OS_TICKCOUNT_MS, same epoch
GetTime() scales by 0.001) is unsigned and wraps at 2^32 ms. Nine files
each re-derived the reader; several stored the tick into a signed int,
so past 2^31 ms (~24.9 days uptime) every timestamp went negative and
`startTime + duration` landed in the past -- callers saw every cooldown,
aura, and lockout as already elapsed. The same bug shipped three times
(the cooldown by-ID getters and C_LossOfControl).

Add one source of truth -- Time::Clock (src/time/Clock.h) -- with a
uint32 NowMs() and wrap-safe Reached/Remaining/Elapsed helpers, and route
every tick reader through it. No module re-derives the reader or hand-
rolls a `now < deadline` compare (which inverts across the wrap) anymore.

Also fixes the reported cooldown/LoC getters (unsigned out-params,
wrap-safe compares) and three latent instances of the same `now < end`
inversion found while sweeping: totem/Tracker's two timer getters and
aura/Source's cache-eviction pass.

Named Time::Clock, not Time::Tick, because a Time::Tick inner name
shadows the global Tick namespace (the WorldTick hook) from inside any
Time::* file.

Discovered by fuffc in #17.
2026-08-12 01:58:54 -05:00
16 changed files with 176 additions and 106 deletions
+2 -6
View File
@@ -23,6 +23,7 @@
#include "net/PacketReader.h"
#include "net/SendObserver.h"
#include "spell/Mod.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -32,12 +33,7 @@ namespace Aura::ComboDuration {
namespace {
using Net::CDataStore;
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
using Time::Clock::NowMs;
// ---- Combo-point snapshot (NetClient send co-hook) ------------------------
+6 -12
View File
@@ -21,6 +21,7 @@
#include "guid/Guid.h"
#include "spell/CrowdControl.h"
#include "spell/IsSelfBuff.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -144,12 +145,10 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
// Permanent auras (paladin auras, passives, racials) carry an infinite
// Spell.dbc duration (SpellDuration base -1); the server stores the fixed
// cast-time in this slot for them, so it's always "already elapsed" and the
// engine shows no countdown. The signed `(int)` compare also absorbs the
// ~24.86-day GetTickCount wrap (both operands wrap together).
using TickCount_t = uint32_t(__fastcall *)();
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
if (static_cast<int32_t>(nowMs - expirationMs) >= 0)
// engine shows no countdown. `Time::Clock::Reached` takes the signed diff, so
// it also absorbs the ~24.86-day GetTickCount wrap (both operands wrap
// together).
if (Time::Clock::Reached(expirationMs))
return 0.0; // expiration reached/passed → permanent or no timer
return static_cast<double>(expirationMs) * 0.001;
}
@@ -209,12 +208,7 @@ double SpellBaseDurationSeconds(uint32_t spellID, int unitLevel) {
// the ~24.86-day GetTickCount wrap). An already-elapsed cached expiration is
// not meaningful — the caster is kept regardless, but the time is reported as
// unknown rather than as a negative remaining.
bool ExpirationElapsed(uint32_t expMs) {
using TickCount_t = uint32_t(__fastcall *)();
const uint32_t nowMs = reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
return static_cast<int32_t>(nowMs - expMs) >= 0;
}
bool ExpirationElapsed(uint32_t expMs) { return Time::Clock::Reached(expMs); }
int PlayerLevel(const uint8_t *player) {
auto *desc = Descriptor(player);
+3 -6
View File
@@ -26,6 +26,7 @@
#include "spell/CastEvents.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "totem/Tracker.h"
#include "unit/Identity.h"
@@ -54,11 +55,7 @@ bool SpellAppliesAura(const uint8_t *spellRecord) {
return false;
}
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
using Time::Clock::NowMs;
// Server-authoritative duration (ms). When the local player is the caster
// we let the engine fold in the player's duration modifiers (skipMod = 0);
@@ -618,7 +615,7 @@ void OnWorldTick() {
const uint32_t now = NowMs();
for (auto &e : g_cache) {
if (!e.used || e.expirationMs == 0 || now < e.expirationMs)
if (!e.used || e.expirationMs == 0 || !Time::Clock::Reached(now, e.expirationMs))
continue;
// A timer elapse doesn't prove the aura is gone — expirationMs is only
// a base-duration estimate for non-player casters. Keep any entry whose
+10 -4
View File
@@ -42,16 +42,22 @@ namespace {
// `uint *` (the ItemStats cache lookup uses the integer both as a
// hash key and as the field-zero match target, which is how it
// inherits the pointer typing in the decomp).
//
// `outDuration`/`outStart` are *unsigned* millisecond ticks (same OS counter
// and epoch as `GetTime()`). Reading `outStart` into a signed `int` makes
// every timestamp negative once the machine has been up past 2^31 ms
// (~24.9 days), so `startTime + duration` lands in the past and every item
// reads as off cooldown. Keep them unsigned; see `Time::Clock`.
using QueryItemCooldown_t = bool(__fastcall *)(uint32_t itemID,
int *outDuration,
int *outStart,
uint32_t *outDuration,
uint32_t *outStart,
uint32_t *outEnable);
} // namespace
void PushCooldown(void *L, int itemID) {
int durationMs = 0;
int startMs = 0;
uint32_t durationMs = 0;
uint32_t startMs = 0;
uint32_t enable = 0;
if (itemID > 0) {
auto fn = reinterpret_cast<QueryItemCooldown_t>(
+2 -4
View File
@@ -79,6 +79,7 @@
#include "item/Location.h"
#include "object/Resolve.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -128,10 +129,7 @@ bool g_seeded = false; // true once the login baseline has been taken
uint64_t g_playerGuid = 0; // re-arm the baseline when this changes (relog)
uint32_t g_loginMs = 0; // engine ms at which the current player resolved
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
}
using Time::Clock::NowMs;
bool Contains(const uint64_t *arr, int count, uint64_t guid) {
for (int i = 0; i < count; ++i)
+2 -8
View File
@@ -49,6 +49,7 @@
#include "Offsets.h"
#include "dbc/Names.h"
#include "event/Custom.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -63,14 +64,7 @@ T Read(const void *base, int off) {
return *reinterpret_cast<const T *>(static_cast<const uint8_t *>(base) + off);
}
// Engine millisecond clock — same source (and epoch) as Lua's `GetTime()`
// (which reads this scaled by 0.001), so a value stored here is directly
// comparable to `GetTime()` once divided by 1000.
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
using Time::Clock::NowMs;
// Modern rollType constants (what C_LootHistory.GetPlayerInfo returns).
constexpr int ROLL_PASS = 0;
+25 -23
View File
@@ -48,6 +48,7 @@
#include "spell/CrowdControl.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -56,11 +57,8 @@ namespace LossOfControl {
namespace {
using TickMs_t = uint32_t(__fastcall *)();
int NowMs() {
return static_cast<int>(
reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)());
}
using Time::Clock::NowMs;
using Time::Clock::Reached;
const char *SpellName(const uint8_t *rec) {
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
@@ -90,12 +88,14 @@ int SpellSchoolIndex(const uint8_t *rec) {
// ---- School-interrupt lockout state (fed by SMSG_SPELL_COOLDOWN) -----------
// One active lockout per school index (0..6); active while now < endMs.
// One active lockout per school index (0..6); active until `Reached(endMs)`.
// Self-expiring, per-process — a stale entry from before a relog is already
// in the past on the shared uptime clock, so no reset is needed.
// in the past on the shared uptime clock, so no reset is needed. Unsigned,
// like every engine-tick store: the OS counter wraps at 2^32 ms and a signed
// `int` goes negative past 2^31 ms (~24.9 days uptime). See `Time::Clock`.
struct SchoolLock {
int startMs;
int endMs;
uint32_t startMs;
uint32_t endMs;
};
SchoolLock g_schoolLock[Offsets::SPELL_SCHOOL_COUNT] = {};
@@ -137,9 +137,9 @@ void CooldownSub(uint32_t opcode, Net::CDataStore *packet) {
// (ProhibitSpellSchool); single/varied packets are normal cooldowns and
// ignored.
if (count >= 2 && uniform && firstDur > 0 && schoolIdx >= 0) {
const int now = NowMs();
const uint32_t now = NowMs();
g_schoolLock[schoolIdx].startMs = now;
g_schoolLock[schoolIdx].endMs = now + firstDur;
g_schoolLock[schoolIdx].endMs = now + static_cast<uint32_t>(firstDur);
}
}
@@ -152,17 +152,17 @@ struct LocEntry {
int spellID; // 0 = unknown (school interrupt)
const uint8_t *rec; // spell record for name/icon; null for school interrupt
int schoolMask; // lockoutSchool
int startMs; // 0 = unknown
int endMs; // 0 = unknown / no timing
uint32_t startMs; // 0 = unknown
uint32_t endMs; // 0 = unknown / no timing
};
int BuildList(LocEntry *out, int maxOut) {
const int now = NowMs();
const uint32_t now = NowMs();
int n = 0;
// School-interrupt lockouts first.
for (int s = 0; s < Offsets::SPELL_SCHOOL_COUNT && n < maxOut; ++s) {
if (g_schoolLock[s].endMs != 0 && now < g_schoolLock[s].endMs) {
if (g_schoolLock[s].endMs != 0 && !Reached(now, g_schoolLock[s].endMs)) {
LocEntry &e = out[n++];
e.locType = "SCHOOL_INTERRUPT";
e.spellID = 0;
@@ -204,9 +204,9 @@ int BuildList(LocEntry *out, int maxOut) {
uint32_t expMs = 0, durMs = 0;
if (Aura::Source::Get(playerGuid, spellID, &caster, &expMs, &durMs) &&
expMs != 0) {
e.endMs = static_cast<int>(expMs);
e.endMs = expMs;
if (durMs != 0)
e.startMs = static_cast<int>(expMs - durMs);
e.startMs = expMs - durMs;
}
}
}
@@ -302,7 +302,7 @@ int __fastcall Script_GetActiveLossOfControlData(void *L) {
return 0; // nil
const LocEntry &e = entries[index - 1];
const int now = NowMs();
const uint32_t now = NowMs();
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "locType", e.locType);
@@ -314,12 +314,14 @@ int __fastcall Script_GetActiveLossOfControlData(void *L) {
// Timing (seconds, GetTime() epoch). Nullable — leave a field unset (nil)
// when we don't know it, per the modern contract.
if (e.endMs != 0 && now < e.endMs) {
Game::Lua::SetFieldNumber(L, "timeRemaining", (e.endMs - now) * 0.001);
if (e.endMs != 0 && !Reached(now, e.endMs)) {
Game::Lua::SetFieldNumber(L, "timeRemaining",
static_cast<double>(e.endMs - now) * 0.001);
if (e.startMs != 0) {
Game::Lua::SetFieldNumber(L, "startTime", e.startMs * 0.001);
Game::Lua::SetFieldNumber(L, "duration",
(e.endMs - e.startMs) * 0.001);
Game::Lua::SetFieldNumber(L, "startTime",
static_cast<double>(e.startMs) * 0.001);
Game::Lua::SetFieldNumber(
L, "duration", static_cast<double>(e.endMs - e.startMs) * 0.001);
}
}
+6 -2
View File
@@ -76,6 +76,7 @@
#include "spell/Lookup.h"
#include "spell/CastEvents.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -85,7 +86,6 @@ namespace Spell::Cast {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using TickMs_t = uint32_t(__fastcall *)();
using GetCastTime_t = uint32_t(__fastcall *)(int spellID, int unit, int flag);
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, int skipMod);
@@ -132,7 +132,11 @@ void StampChannel(int spellID, int startMs, int endMs) {
g_cast.spellID = 0;
}
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
// Cast tracking keeps ticks in a signed int internally by design — see PushMs
// for why that's safe (small deltas cancel; the Lua boundary re-casts to
// uint32 so it matches GetTime()*1000 across the wrap). The reader itself
// still comes from the one canonical source.
int NowMs() { return static_cast<int>(Time::Clock::NowMs()); }
// Pushes an engine-ms timestamp to Lua as an UNSIGNED 32-bit value.
//
+5 -5
View File
@@ -19,6 +19,7 @@
#include "net/PacketReader.h"
#include "net/SendObserver.h"
#include "spell/Lookup.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdio>
@@ -261,11 +262,10 @@ bool IsChanneledSpell(int spellID) {
Offsets::SPELL_ATTR_EX_CHANNELED) != 0;
}
int NowMs() {
using TickMs_t = uint32_t(__fastcall *)();
return static_cast<int>(reinterpret_cast<TickMs_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))());
}
// Only used for relative windows (`now - stampMs < window`), where the int
// deltas cancel and stay correct across the wrap. Reader from the one
// canonical source; see `Time::Clock`.
int NowMs() { return static_cast<int>(Time::Clock::NowMs()); }
// ---- UNIT_SPELLCAST_FAILED (client-side cast rejection) ----------------
// Co-hook `Spell_C_SpellFailed` — the local player's own cast failures
+10 -4
View File
@@ -31,10 +31,16 @@ namespace Spell::Cooldown {
namespace {
// The engine writes *unsigned* millisecond ticks into `outStart`/`outDuration`
// (same OS counter, same epoch as `GetTime()`). Reading `outStart` into a
// signed `int` makes every timestamp negative once the machine has been up
// past 2^31 ms (~24.9 days) — `startTime + duration` then lands a month in the
// past and callers see every cooldown as already elapsed. Keep them unsigned;
// see `Time::Clock` for the full rationale.
using QueryCooldown_t = void(__fastcall *)(int spellID, int bookType,
int *outDuration,
int *outStart,
int *outEnable);
uint32_t *outDuration,
uint32_t *outStart,
uint32_t *outEnable);
int __fastcall Script_C_Spell_GetSpellCooldown(void *L) {
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
@@ -43,7 +49,7 @@ int __fastcall Script_C_Spell_GetSpellCooldown(void *L) {
auto fn = reinterpret_cast<QueryCooldown_t>(
static_cast<uintptr_t>(Offsets::FUN_SPELL_QUERY_COOLDOWN));
int durationMs = 0, startMs = 0, enable = 0;
uint32_t durationMs = 0, startMs = 0, enable = 0;
fn(spellID, 0 /* bookType=player */, &durationMs, &startMs, &enable);
Game::Lua::NewTable(L);
+2 -5
View File
@@ -44,6 +44,7 @@
#include "Offsets.h"
#include "guid/Guid.h"
#include "object/Resolve.h"
#include "time/Clock.h"
#include "unit/Position.h"
#include <cmath>
@@ -77,7 +78,7 @@ using Predicate_t = int(__fastcall *)(void *player, void *cand, int mode);
using SetTargetByGuid_t = void(__fastcall *)(const uint64_t *guid);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
using TickMs_t = uint32_t(__fastcall *)();
using Time::Clock::NowMs;
bool PassesFilter(void *player, void *cand, uint64_t guid, Filter f) {
auto valid = reinterpret_cast<Predicate_t>(Offsets::FUN_TARGET_CANDIDATE_VALID);
@@ -176,10 +177,6 @@ uint64_t CurrentSelection() {
return (static_cast<uint64_t>(hi) << 32) | lo;
}
uint32_t NowMs() {
return reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
}
// Ascending insertion sort by 3D distance (candidate counts are tiny).
void SortByDistance(Candidate *a, int n) {
for (int i = 1; i < n; ++i) {
+3 -6
View File
@@ -31,6 +31,7 @@
#include "Game.h"
#include "Offsets.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include <cstdint>
@@ -41,15 +42,11 @@ namespace {
// OS millisecond counter sampled at the last frame boundary. 0 until the
// first world tick fires (pre-world). Read as uint32 — `GetTickCount`
// wraps at ~24.86 days, matching how the rest of the codebase treats this
// counter (see aura/Data.cpp), and how `Script_GetTime` masks it to 32
// counter (see `Time::Clock`), and how `Script_GetTime` masks it to 32
// bits before scaling.
uint32_t g_frameMs = 0;
uint32_t SampleTickMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
uint32_t SampleTickMs() { return Time::Clock::NowMs(); }
// Tail-of-frame refresh. Tail-of-frame-N ≈ start-of-frame-(N+1) minus the
// inter-frame gap, which is all a frame-stable timestamp needs: every read
+26
View File
@@ -0,0 +1,26 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Clock.h"
#include "Offsets.h"
namespace Time::Clock {
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
} // namespace Time::Clock
+64
View File
@@ -0,0 +1,64 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// The engine's OS millisecond tick — the ONE place the codebase reads it.
//
// `FUN_OS_TICKCOUNT_MS` (→ `GetTickCount`, or a scaled rdtsc in timing-mode 1)
// returns an **unsigned** 32-bit millisecond counter on the SAME epoch that
// Lua's `GetTime()` scales by 0.001 (`Script_GetTime` masks the tick to the
// low 32 bits UNSIGNED, then × 0.001). Two hard consequences:
//
// 1. It is UNSIGNED and wraps at 2^32 ms (~49.7 days). Reading a tick into a
// signed `int` makes every timestamp negative once the counter passes
// 2^31 ms (~24.9 days of uptime) — and the rdtsc backend can already be
// past 2^31 right after a warm reboot. A negative `startTime + duration`
// then lands in the past and callers see every cooldown/aura as already
// elapsed. This exact bug shipped three times (cooldown by-ID getters,
// LossOfControl); it must never come back.
//
// 2. Absolute ticks are only comparable through wrap-safe arithmetic. Never
// write `now < deadline` on two raw ticks — it inverts for the ~24.9 days
// after a wrap. Use `Reached()` / `Remaining()` below, which take the
// signed difference (both operands wrap together, so the delta stays
// correct as long as the interval is under 2^31 ms — always true here).
//
// RULE: read ticks only through `Time::Clock::NowMs()` (returns `uint32_t` — do
// not store it in a signed `int`), and compare them only through the helpers
// here. Push a tick to Lua as `(double)(uint32_t)tick` so it matches
// `GetTime()*1000` across the wrap (see `Spell::Cast::PushMs`).
namespace Time::Clock {
// Current engine ms tick. Unsigned by construction — assign to `uint32_t`.
uint32_t NowMs();
// Wrap-safe "has `deadlineMs` arrived?" — true once `now` is at or past it.
inline bool Reached(uint32_t nowMs, uint32_t deadlineMs) {
return static_cast<int32_t>(nowMs - deadlineMs) >= 0;
}
inline bool Reached(uint32_t deadlineMs) { return Reached(NowMs(), deadlineMs); }
// Wrap-safe ms elapsed since an earlier stamp (assumes < 2^31 ms elapsed).
inline uint32_t Elapsed(uint32_t sinceMs, uint32_t nowMs) { return nowMs - sinceMs; }
inline uint32_t Elapsed(uint32_t sinceMs) { return NowMs() - sinceMs; }
// Wrap-safe ms remaining until `deadlineMs`; 0 once it is reached/passed.
inline uint32_t Remaining(uint32_t nowMs, uint32_t deadlineMs) {
return Reached(nowMs, deadlineMs) ? 0u : deadlineMs - nowMs;
}
inline uint32_t Remaining(uint32_t deadlineMs) { return Remaining(NowMs(), deadlineMs); }
} // namespace Time::Clock
+7 -15
View File
@@ -48,6 +48,7 @@
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "ui/ColorData.h"
#include "unit/Identity.h"
@@ -87,11 +88,7 @@ uint32_t g_lastScanMs = 0;
uint32_t g_toolItem[kSlots] = {};
int g_spellsChangedId = -1;
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
using Time::Clock::NowMs;
// The totem tool item a spell requires (`Totem[0]`), or 0.
uint32_t TotemToolItem(const uint8_t *rec) {
@@ -428,10 +425,9 @@ int __fastcall Script_GetTotemTimeLeft(void *L) {
if (slot >= 1 && slot <= kSlots) {
const Slot &t = g_slots[slot - 1];
if (t.active && t.durationMs != 0) {
const uint32_t now = NowMs();
const uint32_t end = t.startMs + t.durationMs;
if (now < end)
seconds = static_cast<double>(end - now) / 1000.0;
const uint32_t left = Time::Clock::Remaining(t.startMs + t.durationMs);
if (left != 0)
seconds = static_cast<double>(left) / 1000.0;
}
}
Game::Lua::PushNumber(L, seconds);
@@ -552,12 +548,8 @@ int __fastcall Script_GameTooltipSetTotem(void *L) {
// Milliseconds remaining (0 = unknown / expired duration).
uint32_t msLeft = 0;
if (t.durationMs != 0) {
const uint32_t now = NowMs();
const uint32_t end = t.startMs + t.durationMs;
if (now < end)
msLeft = end - now;
}
if (t.durationMs != 0)
msLeft = Time::Clock::Remaining(t.startMs + t.durationMs);
reinterpret_cast<ClearTooltip_t>(Offsets::FUN_GAMETOOLTIP_CLEAR)(self);
AddColoredLine(self, name, kNameColor);
+3 -6
View File
@@ -45,6 +45,7 @@
#include "net/SendObserver.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "time/Clock.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -89,11 +90,7 @@ uint64_t g_target = 0; // unit the armed Bite's combo points / DoTs belong to
uint32_t g_untilMs = 0; // window expiry
uint8_t g_lastCp = 0; // last combo-point value seen since arming
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
}
using Time::Clock::NowMs;
// The CGPlayer +0xE68 sub-struct that holds combo points + combo target
// (Script_GetComboPoints, 0x0051A190). Null pre-world.
@@ -129,7 +126,7 @@ void OnSend(uint32_t opcode, Net::CDataStore *packet) {
void OnTick() {
if (g_target == 0)
return;
if (static_cast<int32_t>(NowMs() - g_untilMs) >= 0) {
if (Time::Clock::Reached(g_untilMs)) {
g_target = 0;
return;
}