mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39d22ac124 | ||
|
|
b497bb7fb0 | ||
|
|
4d5fe8d41b |
@@ -60,7 +60,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
@@ -188,6 +188,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
|
||||
### Unit tokens
|
||||
|
||||
|
||||
+63
@@ -217,6 +217,8 @@ build instructions.
|
||||
- [`Enum.ItemClass`](#enumitemclass)
|
||||
- [`Enum.ItemQuality`](#enumitemquality)
|
||||
- [`Enum.PowerType`](#enumpowertype)
|
||||
- [`Enum.SpellBookSpellBank`](#enumspellbookspellbank)
|
||||
- [`Enum.SpellBookItemType`](#enumspellbookitemtype)
|
||||
|
||||
- [Glue](#glue)
|
||||
- [`C_Glue.IsFirstLoadThisSession()`](#c_glueisfirstloadthissession)
|
||||
@@ -467,6 +469,7 @@ build instructions.
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
- [`C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`](#c_spellbookgetspellbookiteminfoslotindex-spellbank)
|
||||
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
|
||||
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
|
||||
- [`C_SpellBook.GetSkillLineName(skillLineID)`](#c_spellbookgetskilllinenameskilllineid)
|
||||
@@ -5333,6 +5336,32 @@ not modern's `ComboPoints` reuse of the same number.
|
||||
local rage = UnitPower("player", Enum.PowerType.Rage)
|
||||
```
|
||||
|
||||
### `Enum.SpellBookSpellBank`
|
||||
|
||||
Selects which spellbook a `C_SpellBook.*` slot query reads. Passed as the
|
||||
`spellBank` argument to
|
||||
[`C_SpellBook.GetSpellBookItemInfo`](#c_spellbookgetspellbookiteminfoslotindex-spellbank).
|
||||
|
||||
| Value | Field |
|
||||
|------:|----------|
|
||||
| `0` | `Player` |
|
||||
| `1` | `Pet` |
|
||||
|
||||
### `Enum.SpellBookItemType`
|
||||
|
||||
Categorizes a spellbook slot. Vanilla's spellbook only ever holds real
|
||||
spells, so `C_SpellBook.GetSpellBookItemInfo` returns `Spell` for a
|
||||
player-book slot and `PetAction` for a pet-book slot. The other values
|
||||
exist for signature parity with retail but never occur in 1.12.
|
||||
|
||||
| Value | Field | Notes |
|
||||
|------:|---------------|-------|
|
||||
| `0` | `None` | Empty slot. Never returned — an empty slot yields `nil` instead. |
|
||||
| `1` | `Spell` | A player-book spell. |
|
||||
| `2` | `FutureSpell` | A not-yet-learned trainer spell. Never occurs in 1.12. |
|
||||
| `3` | `PetAction` | A pet-book spell. |
|
||||
| `4` | `Flyout` | A flyout group — a later-expansion concept. Never occurs. |
|
||||
|
||||
## Glue
|
||||
|
||||
Backports of two `C_Glue` session helpers. `C_Glue` is registered on the glue
|
||||
@@ -11697,6 +11726,40 @@ end
|
||||
Equivalent to the legacy function of the same name introduced in 3.0
|
||||
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
|
||||
|
||||
### `C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)`
|
||||
|
||||
Returns a table describing the spell in a spellbook slot. `slotIndex` is
|
||||
1-based across the whole book. `spellBank` picks the book —
|
||||
[`Enum.SpellBookSpellBank.Player`](#enumspellbookspellbank) (`0`) or
|
||||
`Enum.SpellBookSpellBank.Pet` (`1`).
|
||||
|
||||
```lua
|
||||
local info = C_SpellBook.GetSpellBookItemInfo(1, Enum.SpellBookSpellBank.Player)
|
||||
-- info.name, info.spellID, info.itemType, info.isPassive, info.iconID, ...
|
||||
```
|
||||
|
||||
Table fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `itemType` | [`Enum.SpellBookItemType`](#enumspellbookitemtype) | `Spell` (1) for the player book, `PetAction` (3) for the pet book. |
|
||||
| `actionID` | number | The spellID. Equals `spellID` — vanilla has no spell-override system. |
|
||||
| `spellID` | number | The spellID in the slot. |
|
||||
| `name` | string | The localized spell name. |
|
||||
| `subName` | string | The rank text (e.g. `"Rank 3"`), or `""` when the spell has no rank. |
|
||||
| `iconID` | string | The icon **path** — feed it straight to `texture:SetTexture(...)`. |
|
||||
| `isPassive` | boolean | `true` for a passive spell. |
|
||||
| `isOffSpec` | boolean | Always `false`. |
|
||||
|
||||
Returns `nil` for an empty or out-of-range slot, matching the modern API.
|
||||
|
||||
> **Deviations from retail, forced by 1.12's data.** `iconID` is a
|
||||
> texture path, not a `fileID` (vanilla has no fileID system) — the same
|
||||
> deviation as [`C_Spell.GetSpellInfo`](#c_spellgetspellinfospellid).
|
||||
> `skillLineIndex` is not returned (`nil`); spellbook tabs aren't
|
||||
> SkillLines. `isOffSpec` is always `false` — vanilla has no
|
||||
> specializations.
|
||||
|
||||
### `C_SpellBook.GetSpellLevelLearned(spellID)`
|
||||
|
||||
Returns the level at which a spell becomes available — the
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -647,15 +647,25 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "macro")) {
|
||||
char macro[512];
|
||||
if (!ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro) &&
|
||||
!ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro))
|
||||
return false;
|
||||
// Prefer an addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) — it
|
||||
// handles named macros and extended macro text; fall back to the stock
|
||||
// ChatEdit_ParseText path when no RunMacro global is present.
|
||||
if (!Game::Lua::CallGlobalString(L, "RunMacro", macro))
|
||||
// `macrotext` is raw macro text (the modern default). Run it as text
|
||||
// through the stock chat parser so each line dispatches via
|
||||
// SlashCmdList — that routes /cast etc. through any addon slash hooks
|
||||
// (e.g. SuperCleveRoidMacros' conditional /cast). A name-based RunMacro
|
||||
// (which is what SuperCleveRoidMacros ships) can't interpret text and
|
||||
// would silently resolve nothing.
|
||||
if (ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro)) {
|
||||
RunMacroTextC(L, macro);
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
// Deprecated `macro` form: a saved-macro name/index. Prefer an
|
||||
// addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) to run the
|
||||
// named macro's body; fall back to the stock parser when none exists.
|
||||
if (ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro)) {
|
||||
if (!Game::Lua::CallGlobalString(L, "RunMacro", macro))
|
||||
RunMacroTextC(L, macro);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "stop") || Ascii::EqualCI(verb, "stopcasting")) {
|
||||
Game::Lua::CallGlobal(L, "SpellStopCasting");
|
||||
|
||||
+10
-4
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -617,6 +617,69 @@ static const Game::HookAutoRegister _hookUnlearnSpell{
|
||||
Offsets::FUN_UNLEARN_SPELL, reinterpret_cast<void *>(&UnlearnSpell_h),
|
||||
reinterpret_cast<void **>(&g_origUnlearnSpell)};
|
||||
|
||||
// Enum.SpellBookItemType values (retail). Vanilla's spellbook only ever
|
||||
// produces real spells, so we emit Spell (player book) or PetAction (pet
|
||||
// book); None/FutureSpell/Flyout don't occur in 1.12.
|
||||
static constexpr int kSpellBookItemTypeSpell = 1;
|
||||
static constexpr int kSpellBookItemTypePetAction = 3;
|
||||
|
||||
static const Game::Lua::EnumIntegerEntry kSpellBookSpellBankEntries[] = {
|
||||
{"Player", 0}, {"Pet", 1},
|
||||
};
|
||||
static const Game::Lua::EnumIntegerEntry kSpellBookItemTypeEntries[] = {
|
||||
{"None", 0}, {"Spell", 1}, {"FutureSpell", 2}, {"PetAction", 3},
|
||||
{"Flyout", 4},
|
||||
};
|
||||
|
||||
// `C_SpellBook.GetSpellBookItemInfo(slotIndex, spellBank)` -> table.
|
||||
// `slotIndex` is 1-based across the whole book; `spellBank` is
|
||||
// Enum.SpellBookSpellBank (0 = Player, 1 = Pet). Returns nil for an empty or
|
||||
// out-of-range slot. Vanilla's spellbook holds only real spells, so `itemType`
|
||||
// is always Spell (player) or PetAction (pet).
|
||||
//
|
||||
// Field deviations from retail, all forced by 1.12 lacking the data:
|
||||
// - `iconID` is the icon PATH string (vanilla has no fileID), same as
|
||||
// C_Spell.GetSpellInfo.iconID -- feed it straight to texture:SetTexture.
|
||||
// - `isOffSpec` is always false (vanilla has no specializations).
|
||||
// - `skillLineIndex` is omitted (nil) -- spellbook tabs aren't SkillLines.
|
||||
static int __fastcall Script_C_SpellBook_GetSpellBookItemInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // modern returns nil (not an error) for a bad index
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int bank = Game::Lua::IsNumber(L, 2)
|
||||
? static_cast<int>(Game::Lua::ToNumber(L, 2))
|
||||
: 0;
|
||||
const int bookType = (bank == 1) ? 1 : 0; // 1 = pet book, else player
|
||||
|
||||
const int spellID = Spell::Lookup::SpellbookSlotToID(slot, bookType);
|
||||
if (spellID <= 0)
|
||||
return 0; // empty slot / out of range -> nil
|
||||
|
||||
SpellInfoData info;
|
||||
if (!ReadSpellInfo(spellID, info))
|
||||
return 0;
|
||||
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
const uint32_t attr =
|
||||
record ? *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_ATTRIBUTES)
|
||||
: 0u;
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
SetField(L, "itemType",
|
||||
static_cast<double>(bookType == 1 ? kSpellBookItemTypePetAction
|
||||
: kSpellBookItemTypeSpell));
|
||||
SetField(L, "actionID", static_cast<double>(spellID));
|
||||
SetField(L, "spellID", static_cast<double>(spellID));
|
||||
SetField(L, "name", info.name);
|
||||
// subName carries the rank text ("Rank N"); modern uses "" when absent.
|
||||
SetField(L, "subName", info.rank ? info.rank : "");
|
||||
SetField(L, "iconID", info.iconPath); // path string -- see note above
|
||||
SetFieldBool(L, "isPassive", (attr & Offsets::SPELL_ATTR_PASSIVE) != 0);
|
||||
SetFieldBool(L, "isOffSpec", false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellInfo", &Script_GetSpellInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetSpellLink", &Script_GetSpellLink);
|
||||
@@ -637,6 +700,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_C_Spell_IsSpellHarmful);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellHelpful",
|
||||
&Script_C_Spell_IsSpellHelpful);
|
||||
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellBookItemInfo",
|
||||
&Script_C_SpellBook_GetSpellBookItemInfo);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookSpellBank",
|
||||
kSpellBookSpellBankEntries, 2);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookItemType",
|
||||
kSpellBookItemTypeEntries, 5);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user