Skip to content

Commit 23b102c

Browse files
committed
spell/Cast: clear player channel on early stop; unify player-object access
UnitChannelInfo showed a stale "Ritual of Summoning" countdown after helping a warlock summon: g_channel only cleared at its computed endMs or when a timed cast superseded it, so a ritual that ends early (when the summon fills) lingered, and a following instant cast -- which supersedes neither cast timing nor the channel -- couldn't dislodge it. OnWorldTick now polls the player's UNIT_FIELD_CHANNEL_SPELL (+0x228) and clears g_channel when it drops to 0, gated by a confirm latch so the field's ~1-tick start lag can't wipe a freshly stamped channel. Both channel-stamp sites route through a new StampChannel helper that re-arms the latch. Also fixes early-ended channels generally (interrupt / cancel / LoS break), which previously self-expired only at endMs. Cleanup: factor the player-object/descriptor read into shared Unit::Identity::PlayerObject() / PlayerDescriptor(), now used by State.cpp, Cast.cpp's poll, and ComboDuration. PlayerObject() uses the object-manager GUID resolve (FUN_OBJECT_RESOLVE_BY_GUID) -- the fastest player-object path, non-throwing (null pre-world) -- replacing the duplicated resolves (State.cpp's token resolver, ComboDuration's hand-rolled GUID plumbing).
1 parent e165aa6 commit 23b102c

5 files changed

Lines changed: 105 additions & 49 deletions

File tree

src/aura/ComboDuration.cpp

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,12 @@ uint32_t NowMs() {
4141
// ---- Combo-point snapshot (NetClient send co-hook) ------------------------
4242

4343
// The player's live combo points, read the same way Script_GetComboPoints
44-
// (0x0051A190) does: the CP byte on the CGPlayer +0xE68 sub-struct. The
45-
// player is resolved via the non-throwing object resolver (guid comes from
46-
// the login global; 0 pre-world -> resolver returns null -> 0 CP), so this
47-
// is safe from any context the send hook can fire in.
44+
// (0x0051A190) does: the CP byte on the CGPlayer +0xE68 sub-struct. The player
45+
// comes from the shared Unit::Identity::PlayerObject() (the object-manager GUID
46+
// resolve, our fastest player-object path — returns null pre-world rather than
47+
// throwing), so this stays safe from any context the send hook can fire in.
4848
uint8_t CurrentComboPoints() {
49-
const uint64_t guid = Unit::Identity::PlayerGuid();
50-
if (guid == 0)
51-
return 0;
52-
using ResolveByGUID_t = void *(__fastcall *)(uint32_t typeMask,
53-
const char *debugName,
54-
uint32_t guidLo, uint32_t guidHi,
55-
int line);
56-
auto resolve =
57-
reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
58-
auto *player = static_cast<const uint8_t *>(
59-
resolve(Offsets::OBJ_TYPE_PLAYER, "ClassicAPI",
60-
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
61-
0x1029));
49+
const uint8_t *player = Unit::Identity::PlayerObject();
6250
if (player == nullptr)
6351
return 0;
6452
const uint8_t *info = *reinterpret_cast<const uint8_t *const *>(

src/spell/Cast.cpp

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ TrackedSpell g_channel{0, 0, 0, 0};
115115
// the WorldTick VAR==0 clear must not touch them.
116116
bool g_castFromServer = false;
117117

118+
// Armed once the broadcast UNIT_FIELD_CHANNEL_SPELL (+0x228) has been seen to
119+
// reflect g_channel's spell — the enable for OnWorldTick's channel-stop poll.
120+
// StampChannel re-arms it (false) on every channel stamp so that field's
121+
// ~1-tick start lag can't false-clear a channel we just stamped.
122+
bool g_channelConfirmed = false;
123+
124+
// Stamp the player's channel and re-arm the stop latch. A channel supersedes
125+
// any in-flight cast. Both channel-stamp sites (SMSG_SPELL_START's instant-
126+
// channel path and the MSG_CHANNEL_START handler) route through here so
127+
// neither can forget to reset the latch.
128+
void StampChannel(int spellID, int startMs, int endMs) {
129+
g_channel = TrackedSpell{spellID, startMs, endMs, 0};
130+
g_channelConfirmed = false;
131+
g_cast.spellID = 0;
132+
}
133+
118134
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
119135

120136
void *Resolve(const char *token) {
@@ -355,10 +371,8 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
355371
// Info) at endMs. (MSG_CHANNEL_START re-stamps moments later with
356372
// the server's duration — see ChannelStart_h.)
357373
const int dur = ChannelDurationMs(spellID);
358-
if (dur > 0) {
359-
g_channel = TrackedSpell{spellID, now, now + dur, 0};
360-
g_cast.spellID = 0; // a channel supersedes any cast
361-
}
374+
if (dur > 0)
375+
StampChannel(spellID, now, now + dur);
362376
return;
363377
}
364378
// channel && castTime > 0 → a CAST-THEN-CHANNEL spell (Mind Control:
@@ -474,9 +488,7 @@ int __stdcall ChannelStart_h(uint32_t *opCode, Net::CDataStore *packet) {
474488
packet->m_read = saved;
475489
if (spellID != 0 && duration > 0) {
476490
const int now = NowMs();
477-
g_channel =
478-
TrackedSpell{spellID, now, now + static_cast<int>(duration), 0};
479-
g_cast.spellID = 0; // the channel supersedes the completed cast
491+
StampChannel(spellID, now, now + static_cast<int>(duration));
480492
}
481493
}
482494
return g_origChannelStart(opCode, packet);
@@ -615,10 +627,32 @@ void OnWorldTick() {
615627
*reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL) == 0)
616628
g_cast.spellID = 0;
617629

618-
// Channels are stamped from SMSG_SPELL_START (HandleSpellStart) and expire
619-
// on their computed endMs (self-expiry in PushChannelInfo). We don't poll
620-
// the broadcast +0x228 field for the player — it lags the channel start by
621-
// ~1 tick, so the bar would show ~1s late.
630+
// Detect a player CHANNEL that ended before its computed endMs — the
631+
// summon-ritual case (UNIT_FIELD_CHANNEL_SPELL = 698, cleared when the
632+
// summon fills), and equally an interrupted / cancelled / LoS-broken
633+
// channel. Without this, g_channel only clears at endMs (self-expiry in
634+
// PushChannelInfo) or when a later TIMED cast supersedes it — so an early
635+
// end followed by an INSTANT cast (which supersedes nothing) leaves a
636+
// ghost bar counting down to a duration that no longer reflects reality.
637+
//
638+
// We deliberately don't use +0x228 to START the bar — it's a broadcast
639+
// UpdateField that lags the channel start by ~1 tick (HandleSpellStart
640+
// stamps from the packet instead). For the STOP that lag is harmless, but a
641+
// naive "clear when field == 0" would wipe a channel we just stamped during
642+
// the lag window. Guard with a latch: only honor the 0 once the field has
643+
// been seen to reflect our channel spell at least once. A channel too short
644+
// to ever appear in the field just self-expires at endMs.
645+
if (g_channel.spellID != 0) {
646+
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
647+
if (desc != nullptr) {
648+
const int chan = *reinterpret_cast<const int *>(
649+
desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
650+
if (chan == g_channel.spellID)
651+
g_channelConfirmed = true;
652+
else if (chan == 0 && g_channelConfirmed)
653+
g_channel.spellID = 0;
654+
}
655+
}
622656
}
623657

624658
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};

src/unit/Identity.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,37 @@ uint64_t PlayerGuid() {
9696
return g_charSelectGuid; // early-login fallback (0 until char-select)
9797
}
9898

99+
const uint8_t *PlayerObject() {
100+
// Resolve the player's CGObject straight from the object manager by GUID —
101+
// one hash lookup, no token-string parsing, and non-throwing: the resolver
102+
// returns null when the GUID isn't loaded (pre-world) instead of raising,
103+
// so this is safe from any context (world tick, packet send hook, Lua
104+
// callback). The GUID comes from PlayerGuid() — 0 before char-select, which
105+
// short-circuits to null here. NOT VAR_LOCAL_PLAYER_PTR: its +0xC0 is only
106+
// the GUID, not the canonical CGPlayer whose descriptor is readable.
107+
const uint64_t guid = PlayerGuid();
108+
if (guid == 0)
109+
return nullptr;
110+
using ResolveByGuid_t = void *(__fastcall *)(uint32_t typeMask,
111+
const char *debugName,
112+
uint32_t guidLo, uint32_t guidHi,
113+
int line);
114+
auto resolve =
115+
reinterpret_cast<ResolveByGuid_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
116+
return static_cast<const uint8_t *>(
117+
resolve(Offsets::OBJ_TYPE_PLAYER, "ClassicAPI",
118+
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
119+
/*line*/ 0));
120+
}
121+
122+
const uint8_t *PlayerDescriptor() {
123+
const uint8_t *player = PlayerObject();
124+
if (player == nullptr)
125+
return nullptr;
126+
return *reinterpret_cast<const uint8_t *const *>(
127+
player + Offsets::OFF_UNIT_DESCRIPTOR);
128+
}
129+
99130
uint64_t GuidForObject(const void *unitObject) {
100131
if (unitObject == nullptr)
101132
return 0;

src/unit/Identity.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ namespace Unit::Identity {
2828
// re-dereferencing the globals inline.
2929
uint64_t PlayerGuid();
3030

31+
// The local player's in-world object pointer, resolved straight from the
32+
// object manager by GUID (`FUN_OBJECT_RESOLVE_BY_GUID`) — the fastest path we
33+
// have (one hash lookup, no token-string work) and naturally non-throwing:
34+
// null before the player object exists (pre-world) or before char-select,
35+
// never a raise. Safe from any context — Lua callback, world tick, packet
36+
// hook. Deliberately NOT `VAR_LOCAL_PLAYER_PTR`: that global carries the
37+
// player GUID at +0xC0 but is NOT the CGPlayer whose descriptor and
38+
// CGPlayer-local fields are readable (see Offsets.h).
39+
const uint8_t *PlayerObject();
40+
41+
// The local player's `m_objectFields` descriptor (`PlayerObject() +
42+
// OFF_UNIT_DESCRIPTOR`), or null. This is the broadcast UpdateField block
43+
// (health / power / flags / UNIT_FIELD_CHANNEL_SPELL / …). Prefer this over
44+
// re-deriving the player object + descriptor offset in each module.
45+
const uint8_t *PlayerDescriptor();
46+
3147
// The 64-bit GUID of a resolved CGUnit/CGObject pointer, read through the
3248
// pointer at `+0x08` to an instance block whose first 8 bytes are the GUID
3349
// (verified in `Script_GetInventoryItemLink`; see `OFF_UNIT_GUID_PTR`).

src/unit/State.cpp

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,18 @@
2121
#include "Offsets.h"
2222
#include "aura/Data.h"
2323
#include "spell/Lookup.h"
24+
#include "unit/Identity.h"
2425

2526
#include <cstdint>
2627

2728
namespace Unit::State {
2829

2930
namespace {
3031

31-
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
3232
using CancelAuraSend_t = void(__fastcall *)(int spellID);
3333

34-
const uint8_t *Player() {
35-
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
36-
return static_cast<const uint8_t *>(fn("player"));
37-
}
38-
39-
const uint8_t *PlayerDescriptor() {
40-
auto *player = Player();
41-
if (player == nullptr)
42-
return nullptr;
43-
return *reinterpret_cast<const uint8_t *const *>(
44-
player + Offsets::OFF_UNIT_DESCRIPTOR);
45-
}
46-
4734
uint32_t PlayerMovementFlags() {
48-
auto *player = Player();
35+
auto *player = Unit::Identity::PlayerObject();
4936
if (player == nullptr)
5037
return 0;
5138
return *reinterpret_cast<const uint32_t *>(
@@ -63,7 +50,7 @@ uint32_t MountDisplayID(const uint8_t *desc) {
6350
// `UNIT_FIELD_MOUNTDISPLAYID` from the descriptor; non-zero means
6451
// the engine is rendering a mount under the player.
6552
int __fastcall Script_IsMounted(void *L) {
66-
auto *desc = PlayerDescriptor();
53+
auto *desc = Unit::Identity::PlayerDescriptor();
6754
Game::Lua::PushBool(L, MountDisplayID(desc) != 0);
6855
return 1;
6956
}
@@ -80,11 +67,11 @@ int __fastcall Script_IsMounted(void *L) {
8067
// No-op if the player isn't mounted (early-outs on the
8168
// `UNIT_FIELD_MOUNTDISPLAYID` check before walking auras).
8269
int __fastcall Script_Dismount(void *L) {
83-
auto *desc = PlayerDescriptor();
70+
auto *desc = Unit::Identity::PlayerDescriptor();
8471
if (MountDisplayID(desc) == 0)
8572
return 0;
8673

87-
auto *player = Player();
74+
auto *player = Unit::Identity::PlayerObject();
8875
if (player == nullptr)
8976
return 0;
9077

@@ -117,7 +104,7 @@ int __fastcall Script_Dismount(void *L) {
117104
// `OFF_PLAYER_FIELD_VIS_BYTES` in Offsets.h for the full bit map
118105
// and known limitations (untested for shapeshift forms).
119106
int __fastcall Script_IsStealthed(void *L) {
120-
auto *desc = PlayerDescriptor();
107+
auto *desc = Unit::Identity::PlayerDescriptor();
121108
bool stealthed = false;
122109
if (desc != nullptr) {
123110
const uint32_t visBytes = *reinterpret_cast<const uint32_t *>(
@@ -177,12 +164,12 @@ int __fastcall Script_IsSwimming(void *L) {
177164
// vanilla's summoning ritual has no UI surface, which is what
178165
// motivates this function.
179166
int __fastcall Script_IsAssistingRitual(void *L) {
180-
auto *player = Player();
167+
auto *player = Unit::Identity::PlayerObject();
181168
if (player == nullptr) {
182169
Game::Lua::PushBool(L, 0);
183170
return 1;
184171
}
185-
auto *desc = PlayerDescriptor();
172+
auto *desc = Unit::Identity::PlayerDescriptor();
186173
if (desc == nullptr) {
187174
Game::Lua::PushBoolean(L, 0);
188175
return 1;

0 commit comments

Comments
 (0)