Files
ClassicAPI/src/unit/RaidTarget.cpp
T
Brues 0c27d61839 unit: add markN raid-target-marker unit tokens
"mark1".."mark8" (star..skull) now work as unit tokens against the whole
Script_Unit* surface, and fire per-token unit events (UNIT_HEALTH, ...)
with arg1 == "markN" -- the same TokenObserver path focus/nameplateN use.

Resolution: a markN branch in the FUN_TOKEN_TO_GUID hook
(unit/TokenExtensions.cpp) reads the engine's 8-slot marker table
(VAR_RAID_TARGET_GUIDS = 0x00B71368, mark{1..8} = table[N-1]), so every
stock Unit* gains the token for free and "mark1target" composes via the
existing suffix walker.

Events: unit/RaidTarget.cpp runs a WorldTick watcher that diffs the marker
table and attaches/detaches one descriptor observer per marked unit,
deferring registration until the unit is in the object table (a unit can
be marked while out of range and enter later) -- the same range handling
Focus does, x8. One distinct callback per slot fires its fixed "markN"
token. Idle (no markers set) costs 8 compares and no object lookups; a
marker on a non-unit is never observed.

Verified the array shape against the binary (reverse lookup FUN_004bb190
walks 8 uint64 slots) and against nampower's CGRaidInfo_m_raidtargets.
Docs added under Unit tokens (markN); README token table updated.
2026-08-01 23:55:44 -05:00

181 lines
7.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
// `markN` raid-target-marker unit tokens (`mark1`=star … `mark8`=skull).
//
// Two halves, mirroring `Unit::Focus`:
//
// 1. Resolution — `GetGUIDByMark` reads the engine's marker table
// (`VAR_RAID_TARGET_GUIDS`); the `markN` branch of the token
// resolver in `unit/TokenExtensions.cpp` calls it so every stock
// `Script_Unit*` (UnitHealth, UnitExists, UnitName, …) accepts
// `"markN"` for free.
//
// 2. Events — `Unit::TokenObserver` watches each marked unit's
// descriptor fields so `UNIT_HEALTH` / `UNIT_MANA` / `UNIT_AURA` /
// … fire with `arg1 == "markN"`, exactly like a native token. The
// engine only auto-watches its own tokens (target/party/raid/pet/
// mouseover), so a marked mob that is none of those fires no unit
// events until we observe it.
//
// Why the WorldTick poll (vs. a packet hook): the marker table is written
// by the server (`SMSG_RAID_TARGET_UPDATE`), and — more importantly — a
// `TokenObserver` can only attach to a LIVE `CGUnit`. A unit marked while
// out of range has no object to observe; when it enters range there is no
// per-mark event announcing it, and when it leaves range its observer
// nodes die with the object silently. So each tick we diff the 8 slots
// and (re)attach/detach observers against the current live objects — the
// same range-transition handling `Unit::Focus::OnWorldTick` does, ×8. The
// idle case (no markers set) costs 8 compares and zero object lookups.
//
// A marker on a non-unit GUID (totem corpse / gameobject / loot) simply
// never resolves to a `CGUnit`, so it is never observed and fires no
// events — correct, and matches `UnitExists("markN")` being false there.
#include "unit/RaidTarget.h"
#include "Game.h"
#include "Offsets.h"
#include "tick/WorldTick.h"
#include "unit/TokenObserver.h"
#include <cstdint>
namespace Unit::RaidTarget {
namespace {
// `"mark1".."mark8"` — the token strings fired as `arg1`. Static so the
// pointers stay valid for the `FUN_FIRE_EVENT` "%s" argument.
const char *const kMarkTokens[kMarkCount] = {
"mark1", "mark2", "mark3", "mark4",
"mark5", "mark6", "mark7", "mark8",
};
// GUID we are currently tracking for each marker slot (mirrors the live
// marker table), or 0. Doubles as the callback's stale-fire guard.
uint64_t g_markGUID[kMarkCount] = {};
// Whether a live descriptor observer is currently registered on
// `g_markGUID[i]`. Cleared when that unit leaves the object table so the
// tick re-registers when it returns (nodes die with the object).
bool g_markObserved[kMarkCount] = {};
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
// Resolve a GUID to its live `CGUnit`, or null when it isn't currently in
// the client's object table (out of range / despawned / a non-unit
// marker). Non-throwing — safe from the tick. Mirrors `Focus::ResolveObject`.
const uint8_t *ResolveObject(uint64_t guid) {
if (guid == 0)
return nullptr;
auto resolve = reinterpret_cast<ResolveByGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
return static_cast<const uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "RaidTarget", static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32), 0x172));
}
// Descriptor-field observer callback for marker slot `Idx` (0-based).
// Fires the changed field's unit event (`fieldOffset >> 2`) with the
// `"markN"` token, so `arg1 == "markN"` works for `UNIT_HEALTH`,
// `UNIT_MANA`, `UNIT_AURA`, … — exactly like `FocusFieldCb`. Guarded on
// the live tracked GUID so an in-flight field change racing an unregister
// can't fire a stale token.
template <int Idx>
int __fastcall MarkFieldCb(uint32_t fieldOffset, uint32_t /*size*/,
uint32_t guidLo, uint32_t guidHi,
const uint32_t * /*oldValue*/, void * /*userArg*/) {
const uint64_t g = g_markGUID[Idx];
if (guidLo != static_cast<uint32_t>(g) || guidHi != static_cast<uint32_t>(g >> 32))
return 1;
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT))(
static_cast<int>(fieldOffset >> 2), "%s", kMarkTokens[Idx]);
return 1;
}
// One distinct callback per slot — `TokenObserver` keys observer nodes on
// the callback pointer, and each fires its own fixed token, so the slots
// must not share a function.
const TokenObserver::FieldCallback g_markCb[kMarkCount] = {
&MarkFieldCb<0>, &MarkFieldCb<1>, &MarkFieldCb<2>, &MarkFieldCb<3>,
&MarkFieldCb<4>, &MarkFieldCb<5>, &MarkFieldCb<6>, &MarkFieldCb<7>,
};
const uint64_t *MarkerTable() {
return reinterpret_cast<const uint64_t *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_TARGET_GUIDS));
}
// Keep each slot's observer pointed at the unit currently wearing that
// marker. Per slot: adopt a changed/cleared assignment (dropping the old
// observer), then (re)register on the live object or note a despawn. The
// heavy path (ResolveObject) runs only for slots that actually hold a
// marker, so an unmarked table is 8 cheap compares.
void OnWorldTick() {
const uint64_t *table = MarkerTable();
for (int i = 0; i < kMarkCount; ++i) {
const uint64_t want = table[i];
if (want != g_markGUID[i]) {
// Marker moved to a different GUID (or was cleared): stop
// watching the old unit, adopt the new assignment.
if (g_markObserved[i]) {
TokenObserver::Unregister(g_markGUID[i], g_markCb[i]);
g_markObserved[i] = false;
}
g_markGUID[i] = want;
}
if (g_markGUID[i] != 0) {
const bool live = ResolveObject(g_markGUID[i]) != nullptr;
if (live && !g_markObserved[i]) {
TokenObserver::Register(g_markGUID[i], g_markCb[i]);
g_markObserved[i] = true;
} else if (!live && g_markObserved[i]) {
// Marked unit left the object table (out of range /
// despawn); its observer nodes died with it. Re-register
// when it returns.
g_markObserved[i] = false;
}
}
}
}
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
// Reset tracking on logout→character-select. The DLL isn't reloaded
// across a relog, so stale per-slot GUIDs from character A must not leak
// into B. Observer nodes already died with the world teardown, and the
// tick re-reads the (fresh) marker table anyway, so we only clear our
// mirror. Same rationale as `Focus::ClearOnLogout`; glue hook so it fires
// on logout but NOT on /reload.
void ClearOnLogout() {
for (int i = 0; i < kMarkCount; ++i) {
g_markGUID[i] = 0;
g_markObserved[i] = false;
}
}
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
} // namespace
uint64_t GetGUIDByMark(int mark) {
if (mark < 1 || mark > kMarkCount)
return 0;
return MarkerTable()[mark - 1];
}
} // namespace Unit::RaidTarget