mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2403ff7233 | ||
|
|
b8d55e8dea |
+30
-16
@@ -7951,15 +7951,27 @@ C_Spell.UnitCastingInfo("player")
|
||||
`startTimeMs`/`endTimeMs` share `GetTime()`'s epoch (`GetTime()*1000`),
|
||||
so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
|
||||
|
||||
> **Local player only.** Vanilla 1.12 stores no cast times anywhere
|
||||
> readable (the cast bar is Lua-driven off `SPELLCAST_START`), and it
|
||||
> tracks regular casts for nobody but you — other units' casts exist
|
||||
> only as transient animation visuals. So `C_Spell.UnitCastingInfo(otherUnit)`
|
||||
> returns `nil`. We self-track the player's cast on a per-frame tick
|
||||
> (`VAR_CURRENT_CAST_SPELL`), stamping `startTimeMs = now` and
|
||||
> `endTimeMs = now + base cast time`. Instant casts return `nil` (no
|
||||
> cast bar). See [`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
> for channels, which *are* visible on other units.
|
||||
> **Works for any unit.** Vanilla 1.12 stores no cast times on the
|
||||
> CGUnit (the cast bar is Lua-driven off `SPELLCAST_START`), so we
|
||||
> source them two ways: the **local player** is self-tracked on a
|
||||
> per-frame tick (`VAR_CURRENT_CAST_SPELL`, stamping `startTimeMs = now`,
|
||||
> `endTimeMs = now + effective cast time`); **other units** come from a
|
||||
> co-hook on `SMSG_SPELL_START` (`Spell::Cast`), the one packet carrying
|
||||
> another unit's cast with a server-authoritative cast time, cached per
|
||||
> caster GUID. So `C_Spell.UnitCastingInfo("target")` now backs
|
||||
> enemy/target/focus/nameplate cast bars. Instant casts return `nil` (no
|
||||
> cast bar). Best-effort for remote units, in three ways: only casts that
|
||||
> began while the unit was in range are seen; since 1.12 has no per-unit
|
||||
> interrupt packet to hook, an *interrupted* remote cast lingers until its
|
||||
> computed `endTimeMs` rather than clearing instantly; and the remote
|
||||
> `startTimeMs`/`endTimeMs` are shifted later by roughly your latency.
|
||||
> (1.12's `SMSG_SPELL_START` carries only a single `castTime`, so we stamp
|
||||
> `start = now, end = now + castTime` on receipt. 3.3.5+ avoids the skew
|
||||
> because its packet carries *both* total and remaining cast time and
|
||||
> back-dates `start = now − elapsed`; the 1.12 packet has no field to
|
||||
> recover `elapsed` from.) The **local player is unaffected** by all of
|
||||
> this — its cast is detected client-side at the moment of cast, no packet
|
||||
> involved.
|
||||
>
|
||||
> `isTradeskill` is real — the spell's `SPELL_ATTR_TRADESPELL` flag, so
|
||||
> profession recipe casts (smelting, cooking/enchanting recipes, …)
|
||||
@@ -7991,13 +8003,15 @@ C_Spell.ChannelInfo()
|
||||
-- 907894314, 907902314, false, false, 10187
|
||||
```
|
||||
|
||||
Unlike casts, the channeled spellID **is** broadcast for every unit
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `C_Spell.UnitChannelInfo("target")`
|
||||
works on other units — returning `name`/`displayName`/`textureID`/`spellID`
|
||||
with **`nil` start/end times** (we only track the local player's channel
|
||||
start). For the player, the full timing is present
|
||||
(`endTimeMs - startTimeMs` = channel duration). Same placeholder fields
|
||||
as `C_Spell.UnitCastingInfo`.
|
||||
The channeled spellID **is** broadcast for every unit
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `C_Spell.UnitChannelInfo("target")` works
|
||||
on other units. Timing now comes from the same `SMSG_SPELL_START` co-hook
|
||||
as casts: when we observed the channel begin, the remote unit returns full
|
||||
**start/end times** (validated against the live `UNIT_FIELD_CHANNEL_SPELL`
|
||||
so a stale cache entry never applies to a different/ended channel);
|
||||
otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` times**. The player path is unchanged (full timing). Same
|
||||
placeholder fields as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
## SpellBook
|
||||
|
||||
|
||||
@@ -695,6 +695,25 @@ enum Offsets {
|
||||
// same Octo/Turtle 1.12.1 client). Per-cast frequency, not per-frame.
|
||||
FUN_SPELL_GO = 0x006E7A70,
|
||||
|
||||
// SMSG packet handler that processes `SMSG_SPELL_START` (opcode
|
||||
// `0x131`) among others — `int __fastcall(uint32_t unk, uint32_t
|
||||
// opCode, uint32_t unk2, CDataStore *packet)`. For `0x131` the body is
|
||||
// `itemGuid(packed), casterGuid(packed), spellId(u32), castFlags(u16),
|
||||
// castTime(u32), targetMask, …`. This is the only place the client sees
|
||||
// *another* unit's cast with a server-authoritative cast time, so
|
||||
// `Spell::Cast` co-hooks it (gated on `opCode == 0x131`) to back
|
||||
// remote-unit `UnitCastingInfo` / `UnitChannelInfo`. Mirrored from
|
||||
// nampower's SpellStartHandlerHook.
|
||||
FUN_SPELL_START_HANDLER = 0x006E7640,
|
||||
|
||||
// `SMSG_SPELL_DELAYED` handler — `int __stdcall(uint32_t *opCode,
|
||||
// CDataStore *packet)`. Body: `guid(u64), delayMs(u32)`. The server
|
||||
// only sends this to the affected caster, so in practice it's always
|
||||
// the local player's cast pushback (taking damage mid-cast). `Spell::
|
||||
// Cast` co-hooks it to extend the tracked cast's end time so the bar
|
||||
// reflects pushback. Mirrored from nampower's SpellDelayedHook.
|
||||
FUN_SPELL_DELAYED = 0x006E74F0,
|
||||
|
||||
// `CGUnit_C::OnAuraAdded` — `__thiscall void(CGUnit *unit, uint32_t
|
||||
// slot, uint32_t spellId)` (i.e. `__fastcall(unit /*ecx*/, edx_unused,
|
||||
// slot, spellId)`). Fires for EVERY aura that lands on a tracked unit,
|
||||
@@ -2354,6 +2373,18 @@ enum Offsets {
|
||||
// `FUN_006E3D10`. Non-zero means "the cast bar is showing this
|
||||
// spell"; cleared to 0 when the cast finishes or is cancelled.
|
||||
VAR_CURRENT_CAST_SPELL = 0x00CECA88,
|
||||
// The cast-start state writer — `__fastcall(int spellID, int
|
||||
// targetState)`. Sets `VAR_CURRENT_CAST_SPELL` (pushing any current
|
||||
// spell into `VAR_QUEUED_CAST_SPELL`; `spellID == 0` restores the queued
|
||||
// one). `Spell::Cast` co-hooks it for lag-free, client-side detection of
|
||||
// NORMAL player casts (stamps from the spellID arg before the original).
|
||||
// It does NOT fire for a chained same-spell recast: the caller
|
||||
// `Spell_C_CastSpell` (`0x006E4B60`) short-circuits at `spellID ==
|
||||
// VAR_CURRENT_CAST_SPELL` *before* reaching here, so those never run the
|
||||
// client cast path — they're caught by the `SMSG_SPELL_START` backstop
|
||||
// instead. nampower hooks the higher Spell_C_CastSpell, not this inner
|
||||
// writer, so contention is low.
|
||||
FUN_CAST_START_SET = 0x006E4AD0,
|
||||
// Previous / queued cast — holds the spellID that was active
|
||||
// before the latest one superseded it (mid-GCD queueing in
|
||||
// vanilla). When the current cast ends, `FUN_006E4AD0(0, ...)`
|
||||
|
||||
+5
-36
@@ -18,6 +18,7 @@
|
||||
#include "Data.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
@@ -28,39 +29,7 @@ namespace Aura::Source {
|
||||
|
||||
namespace {
|
||||
|
||||
// Minimal view of the engine's CDataStore packet reader. Only the fields
|
||||
// the read path touches; layout matches nampower's `CDataStore`. Incoming
|
||||
// SMSG packets are a single contiguous buffer (`m_base == 0`,
|
||||
// `m_alloc == size`), so the simple `m_buffer - m_base + m_read` read with
|
||||
// an `m_size` bound is sufficient — same critical guard as the engine's
|
||||
// `CDataStore::Get`.
|
||||
//
|
||||
// CDataStore is POLYMORPHIC in the engine (virtual Internal*/Reset/etc.),
|
||||
// so a vtable pointer occupies offset 0 and the data members start at
|
||||
// +0x04. Omitting `vtable` here would shift every field by 4 and make
|
||||
// `m_buffer` read the vptr — yielding a garbage read address (the cause of
|
||||
// an early ACCESS_VIOLATION in the SpellGo hook).
|
||||
struct CDataStore {
|
||||
void *vtable; // +0x00
|
||||
uint8_t *m_buffer; // +0x04
|
||||
uint32_t m_base; // +0x08
|
||||
uint32_t m_alloc; // +0x0C
|
||||
uint32_t m_size; // +0x10
|
||||
uint32_t m_read; // +0x14
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T ReadField(CDataStore *p) {
|
||||
T val{};
|
||||
const uint32_t end = p->m_read + sizeof(T);
|
||||
if (end > p->m_size) {
|
||||
p->m_read = p->m_size + 1; // poison: further reads short-circuit
|
||||
return val;
|
||||
}
|
||||
val = *reinterpret_cast<const T *>(p->m_buffer - p->m_base + p->m_read);
|
||||
p->m_read += sizeof(T);
|
||||
return val;
|
||||
}
|
||||
using Net::CDataStore;
|
||||
|
||||
// A cast applies an aura iff any of its three effects names an aura. The
|
||||
// EffectApplyAuraName[3] int32 array sits at `+0x16C` (already used by the
|
||||
@@ -181,10 +150,10 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
|
||||
if (packet != nullptr) {
|
||||
const uint32_t savedRead = packet->m_read;
|
||||
ReadField<int16_t>(packet); // castFlags (unused)
|
||||
const uint8_t numHit = ReadField<uint8_t>(packet);
|
||||
Net::Read<int16_t>(packet); // castFlags (unused)
|
||||
const uint8_t numHit = Net::Read<uint8_t>(packet);
|
||||
for (int i = 0; i < numHit; ++i) {
|
||||
const uint64_t guid = ReadField<uint64_t>(packet);
|
||||
const uint64_t guid = Net::Read<uint64_t>(packet);
|
||||
if (numTargets < kMaxTargets)
|
||||
targets[numTargets++] = guid;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Minimal reader for the engine's `CDataStore` packet buffer, shared by the
|
||||
// packet co-hooks (`Aura::Source`'s SpellGo, `Spell::Cast`'s SpellStart).
|
||||
//
|
||||
// `CDataStore` is POLYMORPHIC in the engine (virtual Internal*/Reset/etc.),
|
||||
// so a vtable pointer sits at offset 0 and the data members start at +0x04.
|
||||
// Omitting `vtable` shifts every field by 4 and makes `m_buffer` read the
|
||||
// vptr — a garbage read address (this caused an early ACCESS_VIOLATION in
|
||||
// the SpellGo hook before the slot was added).
|
||||
//
|
||||
// Incoming SMSG packets are a single contiguous buffer (`m_base == 0`,
|
||||
// `m_alloc == size`), so the simple `m_buffer - m_base + m_read` read with
|
||||
// an `m_size` bound is sufficient — the same critical guard as the engine's
|
||||
// own `CDataStore::Get`. Callers should save `m_read` before reading and
|
||||
// restore it before handing the packet to the original handler.
|
||||
|
||||
namespace Net {
|
||||
|
||||
struct CDataStore {
|
||||
void *vtable; // +0x00
|
||||
uint8_t *m_buffer; // +0x04
|
||||
uint32_t m_base; // +0x08
|
||||
uint32_t m_alloc; // +0x0C
|
||||
uint32_t m_size; // +0x10
|
||||
uint32_t m_read; // +0x14
|
||||
};
|
||||
|
||||
// Reads a fixed-width field and advances the cursor. On overrun it poisons
|
||||
// `m_read` (so subsequent reads short-circuit) and returns a zero value.
|
||||
template <typename T>
|
||||
T Read(CDataStore *p) {
|
||||
T val{};
|
||||
const uint32_t end = p->m_read + sizeof(T);
|
||||
if (end > p->m_size) {
|
||||
p->m_read = p->m_size + 1;
|
||||
return val;
|
||||
}
|
||||
val = *reinterpret_cast<const T *>(p->m_buffer - p->m_base + p->m_read);
|
||||
p->m_read += sizeof(T);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Reads a WoW packed GUID: a 1-byte mask followed by one byte per set bit,
|
||||
// each contributing the corresponding byte of the 64-bit value.
|
||||
inline uint64_t ReadPackedGuid(CDataStore *p) {
|
||||
uint64_t guid = 0;
|
||||
const uint8_t mask = Read<uint8_t>(p);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (mask & (1u << i))
|
||||
guid |= static_cast<uint64_t>(Read<uint8_t>(p)) << (i * 8);
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
} // namespace Net
|
||||
+303
-66
@@ -17,26 +17,41 @@
|
||||
// Vanilla 1.12 stores NO cast start/end times anywhere readable (unlike
|
||||
// 3.3.5+, which keeps them per-unit on the CGUnit). The cast bar is
|
||||
// Lua-driven off `SPELLCAST_START(duration)` + `GetTime()`. So we
|
||||
// self-track the LOCAL PLAYER's cast/channel on `WorldTick`:
|
||||
// - cast: `VAR_CURRENT_CAST_SPELL` (the player cast-bar spellID
|
||||
// global). On a 0->nonzero transition we stamp startMs = now and
|
||||
// endMs = now + base cast time (SpellCastTimes.dbc). Instant casts
|
||||
// (cast time 0) and channels are filtered out here — a channel has
|
||||
// no cast time and surfaces through the channel field instead.
|
||||
// self-track the LOCAL PLAYER's cast/channel with a HYBRID of two sources:
|
||||
// - Normal casts (client path): co-hook the engine's cast-start writer
|
||||
// `FUN_CAST_START_SET` and stamp from its spellID argument the instant
|
||||
// the cast begins (startMs = now, endMs = now + effective cast time) —
|
||||
// zero latency. `g_castFromServer = false`; cleared on `WorldTick` when
|
||||
// `VAR_CURRENT_CAST_SPELL` returns to 0 (normal end / cancel / interrupt).
|
||||
// - Chained same-spell recasts (server path): the engine SHORT-CIRCUITS
|
||||
// `Spell_C_CastSpell` at `spellID == VAR_CURRENT_CAST_SPELL` — it never
|
||||
// runs the client cast path, never sets the global, never fires
|
||||
// SPELLCAST_START. So a chained recast is invisible client-side; the
|
||||
// ONLY witness is the server's `SMSG_SPELL_START` (= nampower's
|
||||
// SPELL_START_SELF). We stamp `g_cast` from that packet (server cast
|
||||
// time), dedup'd against a just-made client stamp so a normal cast's
|
||||
// confirming packet doesn't restart its bar. `g_castFromServer = true`;
|
||||
// since the global was never set for these, the WorldTick VAR==0 clear
|
||||
// must skip them — they expire on their computed endMs (and pfUI clears
|
||||
// on nampower's SPELL_FAILED_SELF for interrupts).
|
||||
// - channel: the player's broadcast `UNIT_FIELD_CHANNEL_SPELL`
|
||||
// (descriptor +0x228). endMs = now + base channel duration
|
||||
// (SpellDuration.dbc).
|
||||
// (descriptor +0x228), polled on `WorldTick`. endMs = now + effective
|
||||
// channel duration.
|
||||
// `now` is the engine ms clock (`FUN_OS_TICKCOUNT_MS`), the same source
|
||||
// Lua's `GetTime()` scales by 0.001 — so startMs/endMs are directly
|
||||
// comparable to `GetTime()*1000` in addon progress math.
|
||||
//
|
||||
// Scope (vanilla ceiling, confirmed against the binary — see TODO #68):
|
||||
// - Player casts/channels: full timing.
|
||||
// - Other units' CHANNELS: spellID/name/texture from the broadcast
|
||||
// +0x228 field, but no times (we don't track per-remote-unit start).
|
||||
// - Other units' regular CASTS: nil. Vanilla doesn't store them — the
|
||||
// casting animation is driven by transient spell-visual objects, not
|
||||
// a queryable cast record.
|
||||
// Other units have no engine-stored cast record, so we capture their casts
|
||||
// from the `SMSG_SPELL_START` packet (the only place the client sees
|
||||
// another unit's cast + a server-authoritative cast time) into a per-caster
|
||||
// cache — see the SpellStart co-hook below. Scope:
|
||||
// - Player casts/channels: full timing (self-tracked, exact engine math).
|
||||
// - Other units' regular CASTS: full timing while within the cast window,
|
||||
// from the SpellStart cache. No clean per-unit interrupt signal exists
|
||||
// in 1.12, so an interrupted remote cast lingers until its computed end.
|
||||
// - Other units' CHANNELS: the broadcast +0x228 field gates "channeling
|
||||
// now"; the SpellStart cache adds real start/end times when we observed
|
||||
// the channel begin (else spellID/name/texture with nil times).
|
||||
//
|
||||
// Modern-signature fields vanilla can't fill are structurally-correct
|
||||
// placeholders: castID / castBarID = nil, notInterruptible = false,
|
||||
@@ -45,8 +60,10 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -75,12 +92,17 @@ struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
int startMs;
|
||||
int endMs;
|
||||
int delayMs; // accumulated pushback (SMSG_SPELL_DELAYED), 0 = none
|
||||
};
|
||||
|
||||
TrackedSpell g_cast{0, 0, 0};
|
||||
TrackedSpell g_channel{0, 0, 0};
|
||||
int g_lastCast = 0;
|
||||
int g_lastChannel = 0;
|
||||
TrackedSpell g_cast{0, 0, 0, 0};
|
||||
TrackedSpell g_channel{0, 0, 0, 0};
|
||||
|
||||
// True when g_cast was stamped from SMSG_SPELL_START (a chained same-spell
|
||||
// recast the client cast path bailed on) rather than the client-side
|
||||
// FUN_CAST_START_SET hook. Such casts never set VAR_CURRENT_CAST_SPELL, so
|
||||
// the WorldTick VAR==0 clear must not touch them.
|
||||
bool g_castFromServer = false;
|
||||
|
||||
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
|
||||
|
||||
@@ -110,18 +132,6 @@ int ChannelDurationMs(int spellID) {
|
||||
return ms > 0 ? ms : 0; // negative = "infinite"; channels are finite
|
||||
}
|
||||
|
||||
// The local player's currently-channeled spellID (broadcast +0x228), or 0.
|
||||
int PlayerChannelSpell() {
|
||||
void *p = Resolve("player");
|
||||
if (p == nullptr)
|
||||
return 0;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(p) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
}
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
@@ -150,6 +160,12 @@ const char *SpellIconPath(const uint8_t *rec) {
|
||||
int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
if (c.spellID == 0)
|
||||
return 0;
|
||||
// Self-expire: once the cast window has elapsed, report nothing even if
|
||||
// the slot wasn't explicitly cleared. Keeps a stale cast (e.g. one whose
|
||||
// clear we missed) from lingering on an addon's cast bar, and lets a
|
||||
// fresh stamp for the next cast take over cleanly.
|
||||
if (NowMs() >= c.endMs)
|
||||
return 0;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(c.spellID);
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
@@ -164,7 +180,7 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
Game::Lua::PushBool(L, false); // 8 notInterruptible
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.spellID)); // 9 castingSpellID
|
||||
Game::Lua::PushNil(L); // 10 castBarID
|
||||
Game::Lua::PushNumber(L, 0); // 11 delayTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.delayMs)); // 11 delayTimeMs
|
||||
return 11;
|
||||
}
|
||||
|
||||
@@ -173,6 +189,12 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes) {
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
// Self-expire a timed channel once its window elapses (mirrors
|
||||
// PushCastInfo) — this is how the player's channel clears, since we no
|
||||
// longer poll the +0x228 field. Remote callers gate on endMs before
|
||||
// passing haveTimes, so this is a no-op for them.
|
||||
if (haveTimes && endMs != 0 && NowMs() >= endMs)
|
||||
return 0;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
@@ -193,43 +215,240 @@ int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes
|
||||
return 8;
|
||||
}
|
||||
|
||||
// True if the unit at stack index 1 resolves to the local player.
|
||||
bool ArgIsPlayer(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return false;
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return false;
|
||||
void *u = Resolve(token);
|
||||
return u != nullptr && u == Resolve("player");
|
||||
// ---- Player cast detection (FUN_CAST_START_SET co-hook) ------------------
|
||||
|
||||
// The engine's cast-start writer — the client path for a NORMAL cast. We
|
||||
// stamp from its spellID argument the instant the cast begins (zero
|
||||
// latency), *before* the original so any SPELLCAST_START it triggers sees
|
||||
// fresh times. This does NOT fire for a chained same-spell recast — the
|
||||
// caller (Spell_C_CastSpell) bails at `spellID == VAR_CURRENT_CAST_SPELL`
|
||||
// before ever reaching this writer; those are caught by the SMSG_SPELL_START
|
||||
// path instead. The `spellID == 0` (queued-restore / clear) path is left to
|
||||
// the OnWorldTick VAR==0 poll so a restore doesn't prematurely wipe a bar.
|
||||
using CastStartSet_t = void(__fastcall *)(int spellID, int targetState);
|
||||
CastStartSet_t g_origCastStartSet = nullptr;
|
||||
|
||||
void __fastcall CastStartSet_h(int spellID, int targetState) {
|
||||
if (spellID != 0) {
|
||||
const int dur = CastTimeMs(spellID);
|
||||
if (dur > 0) {
|
||||
const int now = NowMs();
|
||||
g_cast = TrackedSpell{spellID, now, now + dur, 0};
|
||||
g_castFromServer = false; // client-tracked; VAR==0 clears it
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
}
|
||||
// dur == 0: instant (no bar) or channel (handled via +0x228 poll);
|
||||
// leave g_cast — don't clobber an unrelated active cast.
|
||||
}
|
||||
g_origCastStartSet(spellID, targetState);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _castStartHook{
|
||||
Offsets::FUN_CAST_START_SET, reinterpret_cast<void *>(&CastStartSet_h),
|
||||
reinterpret_cast<void **>(&g_origCastStartSet)};
|
||||
|
||||
// ---- Remote-unit cast tracking (SMSG_SPELL_START) ------------------------
|
||||
|
||||
// Vanilla stores no cast record for other units, so we capture their casts
|
||||
// from SMSG_SPELL_START — the only packet carrying another unit's cast with
|
||||
// a server-authoritative cast time. Keyed by caster GUID (a unit casts one
|
||||
// thing at a time). Regular casts back `UnitCastingInfo`; channels add real
|
||||
// times to `UnitChannelInfo` (validated against the live +0x228 field).
|
||||
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C; // Spell.dbc AttributesEx
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChannelSpell(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
// Channel duration for a non-player caster — base (skipMod=1), since we
|
||||
// only have the local player's spell-mod tables.
|
||||
int RemoteChannelDurationMs(const uint8_t *rec) {
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
Offsets::FUN_GET_SPELL_DURATION)(rec, 0, /*skipMod*/ 1);
|
||||
return ms > 0 ? ms : 0;
|
||||
}
|
||||
|
||||
struct RemoteCast {
|
||||
uint64_t casterGuid;
|
||||
int spellID;
|
||||
int startMs;
|
||||
int endMs;
|
||||
bool isChannel;
|
||||
bool used;
|
||||
};
|
||||
|
||||
constexpr int kRemoteCastSlots = 64;
|
||||
RemoteCast g_remoteCasts[kRemoteCastSlots];
|
||||
int g_remoteCursor = 0;
|
||||
|
||||
void StoreRemoteCast(uint64_t caster, int spellID, int startMs, int endMs,
|
||||
bool isChannel) {
|
||||
const RemoteCast entry{caster, spellID, startMs, endMs, isChannel, true};
|
||||
// One active cast per unit — replace any existing entry for this caster.
|
||||
for (auto &e : g_remoteCasts) {
|
||||
if (e.used && e.casterGuid == caster) {
|
||||
e = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const int now = NowMs();
|
||||
for (auto &e : g_remoteCasts) {
|
||||
if (!e.used || now >= e.endMs) {
|
||||
e = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_remoteCasts[g_remoteCursor] = entry;
|
||||
g_remoteCursor = (g_remoteCursor + 1) % kRemoteCastSlots;
|
||||
}
|
||||
|
||||
const RemoteCast *FindRemoteCast(uint64_t caster) {
|
||||
if (caster == 0)
|
||||
return nullptr;
|
||||
for (const auto &e : g_remoteCasts) {
|
||||
if (e.used && e.casterGuid == caster)
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// How recently the client-side FUN_CAST_START_SET hook must have stamped the
|
||||
// same spell for an SMSG_SPELL_START to count as that cast's confirming
|
||||
// packet (skip — don't restart the bar) rather than a new chained recast
|
||||
// (stamp). Must exceed worst-case latency but stay under a GCD / min cast
|
||||
// time so genuine chained casts aren't wrongly deduped.
|
||||
constexpr int kCastStartDedupMs = 500;
|
||||
|
||||
void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
if (caster == 0 || spellID == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
const int now = NowMs();
|
||||
const bool channel = IsChannelSpell(rec);
|
||||
|
||||
if (caster == Unit::Identity::PlayerGuid()) {
|
||||
if (channel) {
|
||||
// Player channel start. The broadcast +0x228 field that signals
|
||||
// "channeling" is server-pushed and lands ~1 tick (~1s) after the
|
||||
// channel actually begins, so a +0x228 poll shows the bar far too
|
||||
// late — the addon already polled nil at SPELLCAST_CHANNEL_START
|
||||
// and cleared. This packet IS the channel start: stamp g_channel
|
||||
// now with the engine-computed duration. We run before the
|
||||
// original handler that fires SPELLCAST_CHANNEL_START, so the data
|
||||
// is fresh when addons poll. Cleared by self-expiry (PushChannel-
|
||||
// Info) at endMs.
|
||||
const int dur = ChannelDurationMs(spellID);
|
||||
if (dur > 0) {
|
||||
g_channel = TrackedSpell{spellID, now, now + dur, 0};
|
||||
g_cast.spellID = 0; // a channel supersedes any cast
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (castTime == 0)
|
||||
return; // instant — no cast bar
|
||||
// Backstop for chained same-spell recasts: the engine short-circuits
|
||||
// Spell_C_CastSpell at `spellID == VAR_CURRENT_CAST_SPELL`, so the
|
||||
// client path (FUN_CAST_START_SET hook) never fires for them — this
|
||||
// server packet is the only signal. Dedup against a fresh client stamp
|
||||
// (same spell, started within ~latency) so a normal cast's confirming
|
||||
// packet doesn't restart its bar; otherwise it's a chained recast the
|
||||
// client bailed on — take it, and flag it so the VAR==0 poll (which
|
||||
// never saw it) won't clear it.
|
||||
if (g_cast.spellID == spellID && now < g_cast.endMs &&
|
||||
now - g_cast.startMs < kCastStartDedupMs)
|
||||
return;
|
||||
g_cast = TrackedSpell{spellID, now, now + static_cast<int>(castTime), 0};
|
||||
g_castFromServer = true;
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
return;
|
||||
}
|
||||
const int endMs = channel ? now + RemoteChannelDurationMs(rec)
|
||||
: now + static_cast<int>(castTime);
|
||||
StoreRemoteCast(caster, spellID, now, endMs, channel);
|
||||
}
|
||||
|
||||
// Co-hook on the SMSG_SPELL_START handler. Gated on opCode 0x131 so every
|
||||
// other packet this handler processes is a cheap early-out. Packet layout
|
||||
// mirrors nampower's SpellStartHandlerHook.
|
||||
using SpellStartHandler_t = int(__fastcall *)(uint32_t unk, uint32_t opCode,
|
||||
uint32_t unk2,
|
||||
Net::CDataStore *packet);
|
||||
SpellStartHandler_t g_origSpellStart = nullptr;
|
||||
|
||||
int __fastcall SpellStartHandler_h(uint32_t unk, uint32_t opCode,
|
||||
uint32_t unk2, Net::CDataStore *packet) {
|
||||
if (opCode == 0x131 && packet != nullptr) {
|
||||
const uint32_t saved = packet->m_read;
|
||||
Net::ReadPackedGuid(packet); // itemGuid (unused)
|
||||
const uint64_t caster = Net::ReadPackedGuid(packet);
|
||||
const int spellID = static_cast<int>(Net::Read<uint32_t>(packet));
|
||||
Net::Read<uint16_t>(packet); // castFlags (unused)
|
||||
const uint32_t castTime = Net::Read<uint32_t>(packet);
|
||||
packet->m_read = saved; // restore before the engine re-parses
|
||||
HandleSpellStart(caster, spellID, castTime);
|
||||
}
|
||||
return g_origSpellStart(unk, opCode, unk2, packet);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _spellStartHook{
|
||||
Offsets::FUN_SPELL_START_HANDLER,
|
||||
reinterpret_cast<void *>(&SpellStartHandler_h),
|
||||
reinterpret_cast<void **>(&g_origSpellStart)};
|
||||
|
||||
// Co-hook on the SMSG_SPELL_DELAYED handler — cast pushback. The server
|
||||
// only sends it to the affected caster, so it's effectively always the
|
||||
// local player; extend the tracked cast's end time (and report the
|
||||
// accumulated delay as delayTimeMs) so an addon's bar stretches on damage.
|
||||
using SpellDelayed_t = int(__stdcall *)(uint32_t *opCode,
|
||||
Net::CDataStore *packet);
|
||||
SpellDelayed_t g_origSpellDelayed = nullptr;
|
||||
|
||||
int __stdcall SpellDelayed_h(uint32_t *opCode, Net::CDataStore *packet) {
|
||||
if (packet != nullptr) {
|
||||
const uint32_t saved = packet->m_read;
|
||||
const uint64_t guid = Net::Read<uint64_t>(packet);
|
||||
const uint32_t delay = Net::Read<uint32_t>(packet);
|
||||
packet->m_read = saved;
|
||||
if (delay != 0 && g_cast.spellID != 0 &&
|
||||
guid == Unit::Identity::PlayerGuid()) {
|
||||
g_cast.endMs += static_cast<int>(delay);
|
||||
g_cast.delayMs += static_cast<int>(delay);
|
||||
}
|
||||
}
|
||||
return g_origSpellDelayed(opCode, packet);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _spellDelayedHook{
|
||||
Offsets::FUN_SPELL_DELAYED, reinterpret_cast<void *>(&SpellDelayed_h),
|
||||
reinterpret_cast<void **>(&g_origSpellDelayed)};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Per-frame: detect the player's cast/channel start, stamp times.
|
||||
// Per-frame upkeep for the player. The regular cast is stamped by the
|
||||
// FUN_CAST_START_SET hook; here we clear it when the engine's cast global
|
||||
// drops to 0, and poll the broadcast +0x228 field for the channel (which
|
||||
// has no comparable client-side start hook).
|
||||
void OnWorldTick() {
|
||||
const int now = NowMs();
|
||||
// Clear a CLIENT-tracked cast when the cast global returns to 0 — covering
|
||||
// normal end, cancel, and interrupt. No grace window is needed: the
|
||||
// FUN_CAST_START_SET hook stamps g_cast in the same call that writes the
|
||||
// global non-zero, so it's never transiently 0 right after a stamp.
|
||||
// Server-stamped chained casts (g_castFromServer) never set the global,
|
||||
// so they're exempt — they expire on their computed endMs (self-expiry in
|
||||
// PushCastInfo).
|
||||
if (g_cast.spellID != 0 && !g_castFromServer &&
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL) == 0)
|
||||
g_cast.spellID = 0;
|
||||
|
||||
const int cast = *reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL);
|
||||
if (cast != g_lastCast) {
|
||||
const int dur = (cast != 0) ? CastTimeMs(cast) : 0;
|
||||
if (cast != 0 && dur > 0) {
|
||||
g_cast = {cast, now, now + dur};
|
||||
} else {
|
||||
g_cast.spellID = 0; // 0, instant, or channel — no cast bar
|
||||
}
|
||||
g_lastCast = cast;
|
||||
}
|
||||
|
||||
const int chan = PlayerChannelSpell();
|
||||
if (chan != g_lastChannel) {
|
||||
if (chan != 0) {
|
||||
g_channel = {chan, now, now + ChannelDurationMs(chan)};
|
||||
} else {
|
||||
g_channel.spellID = 0;
|
||||
}
|
||||
g_lastChannel = chan;
|
||||
}
|
||||
// Channels are stamped from SMSG_SPELL_START (HandleSpellStart) and expire
|
||||
// on their computed endMs (self-expiry in PushChannelInfo). We don't poll
|
||||
// the broadcast +0x228 field for the player — it lags the channel start by
|
||||
// ~1 tick, so the bar would show ~1s late.
|
||||
}
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
@@ -237,15 +456,25 @@ static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
// `CastingInfo()` — local player's cast, no token lookup.
|
||||
static int __fastcall Script_CastingInfo(void *L) { return PushCastInfo(L, g_cast); }
|
||||
|
||||
// `UnitCastingInfo(unit)` — only the local player has regular-cast data
|
||||
// in vanilla; any other unit returns nil.
|
||||
// `UnitCastingInfo(unit)` — local player from self-tracking; other units
|
||||
// from the SMSG_SPELL_START cache (regular casts still within their cast
|
||||
// window). An interrupted remote cast lingers until its computed end time
|
||||
// (no clean per-unit interrupt signal in 1.12) — acceptable best-effort.
|
||||
static int __fastcall Script_UnitCastingInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Spell.UnitCastingInfo(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
if (ArgIsPlayer(L))
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
void *u = (token != nullptr) ? Resolve(token) : nullptr;
|
||||
if (u == nullptr)
|
||||
return 0;
|
||||
if (u == Resolve("player"))
|
||||
return PushCastInfo(L, g_cast);
|
||||
|
||||
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
|
||||
if (rc != nullptr && !rc->isChannel && NowMs() < rc->endMs)
|
||||
return PushCastInfo(L, TrackedSpell{rc->spellID, rc->startMs, rc->endMs});
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -272,7 +501,15 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
static_cast<const uint8_t *>(u) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
// The live +0x228 field is authoritative for "is this unit channeling
|
||||
// right now"; the SMSG_SPELL_START cache adds real start/end times when
|
||||
// we observed the channel begin (and still matches the current spell).
|
||||
const int spellID = *reinterpret_cast<const int *>(desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
|
||||
if (rc != nullptr && rc->isChannel && rc->spellID == spellID && NowMs() < rc->endMs)
|
||||
return PushChannelInfo(L, spellID, rc->startMs, rc->endMs, /*haveTimes*/ true);
|
||||
return PushChannelInfo(L, spellID, 0, 0, /*haveTimes*/ false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user