mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d55e8dea |
+22
-16
@@ -7951,15 +7951,19 @@ 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: only casts that began while the
|
||||
> unit was in range are seen, and — since 1.12 has no per-unit interrupt
|
||||
> packet to hook — an *interrupted* remote cast lingers until its computed
|
||||
> `endTimeMs` rather than clearing instantly.
|
||||
>
|
||||
> `isTradeskill` is real — the spell's `SPELL_ATTR_TRADESPELL` flag, so
|
||||
> profession recipe casts (smelting, cooking/enchanting recipes, …)
|
||||
@@ -7991,13 +7995,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,17 @@ 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,
|
||||
|
||||
// `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,
|
||||
|
||||
+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
|
||||
+146
-10
@@ -30,13 +30,17 @@
|
||||
// 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 +49,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>
|
||||
|
||||
@@ -204,6 +210,118 @@ bool ArgIsPlayer(void *L) {
|
||||
return u != nullptr && u == Resolve("player");
|
||||
}
|
||||
|
||||
// ---- 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;
|
||||
}
|
||||
|
||||
void RecordRemoteStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
if (caster == 0 || spellID == 0)
|
||||
return;
|
||||
// The local player uses the self-tracked g_cast / g_channel path.
|
||||
if (caster == Unit::Identity::PlayerGuid())
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
const int now = NowMs();
|
||||
const bool channel = IsChannelSpell(rec);
|
||||
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
|
||||
RecordRemoteStart(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)};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Per-frame: detect the player's cast/channel start, stamp times.
|
||||
@@ -237,15 +355,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 +400,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