Compare commits

...
2 Commits
Author SHA1 Message Date
Brues 6b36bb10e6 fix(spell): report GetSpellPowerCost in displayed power units
The engine's cost helper (FUN_006E31B0) answers in stored units, and
1.12.1 stores rage in tenths, so Shield Slam's 20 rage came back as
cost = 200. Divide by the engine's own display divisor
(GetPowerDisplayDivisor, 0x006E7130: [0x0086F978 + 4*type], 1 for a
negative type; table {1, 10, 1, 1, 1000}) before pushing, the same way
the engine and our UnitPower / GetSpellInfo / combat-log paths do.

Every power amount surfaced to Lua now goes through one helper,
Unit::Power::DisplayDivisor, which calls that engine function. That
retires the duplicate FUN_GET_POWER_DISPLAY_DIVISOR offset, the two
local typedef copies, and UnitPower's direct read of the divisor table
(VAR_UNIT_POWER_DIVISOR_TABLE, now removed).

Fixes #129
2026-11-05 08:51:59 -06:00
Brues 6628dcca19 fix(loot): keep LootAllCorpses walking past refused and stale corpses
The walk ignored the server's refusal. A failed CMSG_LOOT ("too far
away", not ours, ...) arrives as SMSG_LOOT_RESPONSE with loot type 0;
CGPlayer_C::ReadLootResponse (0x005EB900) shows the error and clears the
held loot target (+0x1D28) without opening a window, and the release
response (0x005EC090) clears it the same way. The walk only advanced on
the loot controller hook or a 180-frame timeout, so each refusal stalled
it for seconds. It now advances as soon as the held target is no longer
the corpse it sent for.

A send the engine refused dropped the whole queue. LootUnit (0x005DF2A0)
sends nothing while the mover flags & 0x20FF are set (moving, turning,
falling), and the walk treated each refusal as "skip this corpse", so
moving during a walk skipped every corpse left. A refused send now stays
the current corpse and is retried each tick; after 3 s the walk ends.

The queue's range check went stale. Range was tested once when the walk
began, but each corpse costs a round trip and the player can move in
between. With CanAutoInteract set, CanLootNow (0x005EC110) skips its own
reach test, so the request went out and the server refused it. Each
corpse is now checked for existence, DYNFLAG_LOOTABLE and reach right
before its send, and skipped when it fails.

The reach test is CanLootNow's own: both units' +0x1F0 reach plus the
engine's leeway, at least its minimum, with both constants read live
(VAR_MELEE_REACH_LEEWAY / VAR_MELEE_REACH_MIN) instead of local copies.
It and the lootable test move to Loot::Request, shared by
GetNearbyLootableUnits and the walk. The step timeout uses Time::Clock
instead of a frame count.
2026-11-04 20:04:35 -06:00
10 changed files with 157 additions and 227 deletions
+8 -24
View File
@@ -2894,23 +2894,14 @@ enum Offsets {
UNIT_POWER_MIN_TYPE = 0,
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
// Per-power-type display divisor. Raw descriptor values get
// divided by this before they're surfaced through Lua. Vanilla
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
// show 0..100; happiness is stored at 1000x scale and divides
// by 1000. Engine reads from `Script_UnitMana` at
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
// 3.3.5/4.x clients use; the table is per-client, the design is
// stable.
//
// [0] = 1 MANA
// [1] = 10 RAGE
// [2] = 1 FOCUS
// [3] = 1 ENERGY
// [4] = 1000 HAPPINESS
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
// GetPowerDisplayDivisor — `int __fastcall(int powerType)`, the reader of
// the table above (1 for a negative type). benilla-re spell symbols.
// GetPowerDisplayDivisor — `uint32_t __fastcall(int powerType)`: the
// divisor that turns a stored power amount (descriptor POWER/MAXPOWER,
// Spell.dbc ManaCost, the cost helper's answer) into displayed units.
// Returns `[0x0086F978 + 4·type]`, or 1 for a negative type (verified by
// disassembly). Table {1, 10, 1, 1, 1000}: rage is stored in tenths
// (Shield Slam's 20 rage is 200), happiness at 1000x. Same table as
// 3.3.5's `s_powerDisplayMod`. Read it only through
// `Unit::Power::DisplayDivisor`.
FUN_POWER_DISPLAY_DIVISOR = 0x006E7130,
OFF_UNIT_FIELD_LEVEL = 0x70,
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
@@ -3259,13 +3250,6 @@ enum Offsets {
// 4.3.4 `GetSpellInfo` reports this instead of the cost (with
// isFunnel = true) when it is positive.
FUN_GET_SPELL_POWER_PER_SECOND = 0x006E3300,
// Divisor that turns a stored power amount into its displayed units:
// `__fastcall uint32_t(int powerType)` → `[0x0086F978 + 4·type]`, 1 for a
// negative type. Table {1, 10, 1, 1, 1000} — rage is stored in tenths
// (Heroic Strike's 15 rage is 150 in Spell.dbc). Verified by disassembly;
// same table as 3.3.5's `s_powerDisplayMod`.
FUN_GET_POWER_DISPLAY_DIVISOR = 0x006E7130,
// The engine's spell-castability verdict for the LOCAL player — the
// function behind `IsUsableAction` for a player spell slot (the spell
// branch of the per-slot recompute `FUN_004E5050`) and behind the
+2 -3
View File
@@ -45,6 +45,7 @@
#include "time/Clock.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/Power.h"
#include <ctime>
@@ -684,9 +685,7 @@ void SetItem(Entry &e, int itemId) {
}
int DisplayAmount(int amount, int powerType) {
using Divisor_t = int(__fastcall *)(int powerType);
const int divisor = reinterpret_cast<Divisor_t>(
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
return divisor != 0 ? amount / divisor : amount;
}
+5 -98
View File
@@ -15,6 +15,7 @@
#include "../Offsets.h"
#include "../guid/Guid.h"
#include "../object/Resolve.h"
#include "Request.h"
#include <cstdint>
@@ -22,22 +23,6 @@ namespace Loot::Nearby {
namespace {
// Engine interact-range constants, read out of `.rdata` once and
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
// (the master interact-state checker) and propagated through
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
// values verified at runtime addresses `DAT_0080B058` and
// `DAT_0080A1E8`.
//
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
// for the unclamped range; modeled as the "reach beyond your touch"
// constant.
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
// targets with near-zero bounding radius still get the standard
// 5-yard right-click reach.
constexpr float INTERACT_REACH = 1.3333333f;
constexpr float MIN_INTERACT_RANGE = 5.0f;
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
// visible-object table. The callback shape's `void *unusedEdx` slot
// absorbs the second `__fastcall` register so MSVC lays out the trailing
@@ -49,84 +34,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
using ClntObjMgrEnumVisibleObjects_t =
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
// caller-provided 12-byte buffer with the unit's world position and
// return it. Same call shape `FUN_005EC110` uses to read the
// player / target positions when deciding interact range.
struct C3Vector {
float x, y, z;
};
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
C3Vector *outBuf);
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
// — the server-driven bit it sets when the local player has loot
// rights on this unit. Works uniformly for live and dead CGUnit
// subclasses; the bit is simply 0 for non-lootable targets.
//
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
// m_objectFields, which is the CORPSE dynamic-flags slot for
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
// slot for CGUnit_C. Routing live units through it returns the low
// bit of *race* — yields false positives for any Human / Dwarf /
// Undead / Gnome player and false negatives for dead mobs.
bool IsLootable(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
const uint32_t flags =
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
// Reads the bounding-radius float from a unit's m_objectFields. The
// engine uses this as the contribution from both sides (player + target)
// in its interact-range computation.
float BoundingRadius(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
// of the returned pointer.
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
auto vtable = *reinterpret_cast<void *const *const *>(unit);
auto fn = reinterpret_cast<GetPosition_t>(
*reinterpret_cast<const void *const *>(
reinterpret_cast<const uint8_t *>(vtable) +
VTBL_GET_POSITION_OFFSET));
return fn(unit, outBuf);
}
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
// player-state checks (mounted / casting / prior-interaction GUID
// gate) because they fire intermittently during normal play and would
// make the enumerator return an empty list whenever the player has
// already clicked something — surprising for a "what's lootable
// nearby" reader. The pure distance test is what controls cursor
// highlight, which is the contract the user asked for.
bool InInteractRange(const void *player, const void *target) {
C3Vector pBuf{};
C3Vector tBuf{};
const C3Vector *pPos = GetPosition(player, &pBuf);
const C3Vector *tPos = GetPosition(target, &tBuf);
if (pPos == nullptr || tPos == nullptr)
return false;
const float dx = pPos->x - tPos->x;
const float dy = pPos->y - tPos->y;
const float dz = pPos->z - tPos->z;
const float distSq = dx * dx + dy * dy + dz * dz;
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
if (range < MIN_INTERACT_RANGE)
range = MIN_INTERACT_RANGE;
return distSq <= range * range;
}
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
// creatures stay as CGCreature_C objects with the lootable bit flipped
// on, rather than transitioning to a separate corpse object. Dead
@@ -135,7 +42,7 @@ bool InInteractRange(const void *player, const void *target) {
struct ScanCtx {
void *L;
const void *player; // resolved once outside the iterator
void *player; // resolved once outside the iterator
int idx; // 1-based slot of the next entry to append
};
@@ -144,10 +51,10 @@ int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
if (obj == nullptr)
return 1; // GUID isn't a unit; continue
if (!IsLootable(obj))
if (!Request::IsLootable(obj))
return 1; // unit isn't lootable to us; continue
if (!InInteractRange(ctx->player, obj))
if (!Request::InReach(ctx->player, obj))
return 1; // out of click-loot range; continue
char guidBuf[Guid::STRING_SIZE];
@@ -180,7 +87,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
// Resolve the local player once — the canonical CGPlayer_C the
// engine's own interact-range checks use, distinct from the
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
const void *player = Game::ResolveUnitToken("player");
void *player = Game::ResolveUnitToken("player");
if (player == nullptr)
return 1;
+35 -2
View File
@@ -15,6 +15,7 @@
#include "../Game.h"
#include "../Offsets.h"
#include "../unit/Position.h"
namespace Loot::Request {
@@ -31,8 +32,11 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
char earlyReturnOnGameObject,
char showError);
uint64_t HeldTarget(const void *player) {
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
// The per-unit reach `CanLootNow` adds for each side (`[m_objectFields +
// 0x1F0]`).
float Reach(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
} // namespace
@@ -42,6 +46,35 @@ bool IsMounted(const void *player) {
return Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID) != 0;
}
bool IsLootable(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS) &
Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
bool InReach(void *player, void *target) {
float p[3];
float t[3];
if (!Unit::Position::Read(player, p) || !Unit::Position::Read(target, t))
return false;
// `CanLootNow`'s reach for a unit (`0x005EC142`..`0x005EC17E`), with the
// engine's own constants read live.
float reach = Reach(target) + Reach(player) +
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
const float minReach =
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
if (reach < minReach)
reach = minReach;
const float dx = p[0] - t[0];
const float dy = p[1] - t[1];
const float dz = p[2] - t[2];
return dx * dx + dy * dy + dz * dz <= reach * reach;
}
uint64_t HeldTarget(const void *player) {
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
}
bool Send(void *player, void *target, uint64_t guid) {
if (IsMounted(player))
return false;
+17 -2
View File
@@ -28,11 +28,26 @@ namespace Loot::Request {
// (`CGUnit_C::Interact`, `0x0060BEA0`) applies before its loot leg.
bool IsMounted(const void *player);
// Whether the server grants us loot on `unit` (`UNIT_DYNFLAG_LOOTABLE`).
bool IsLootable(const void *unit);
// Whether `target` is within the reach `CanLootNow` (`0x005EC110`) demands
// when the player is not walked up: both units' `+0x1F0` reach plus the
// engine's live leeway, at least its live minimum, against the 3D distance.
// `Send` never walks, so a target outside this reach is one the server
// refuses with "too far away".
bool InReach(void *player, void *target);
// The player's held loot target. The engine writes it when the request goes
// out and clears it when the server answers with a failure or a release, so
// a caller waiting on `guid` sees the refusal as this no longer being `guid`.
uint64_t HeldTarget(const void *player);
// Sends the loot request the way a right-click would: refused while mounted
// (the click's gate, which `FUN_CMSG_LOOT_UNIT` itself lacks), never
// auto-walks. Returns whether the request went out — false when mounted or
// when the engine's own pre-checks refused (another loot held, falling, not
// in control, ...), in which case no response is coming.
// when the engine's own pre-checks refused (another loot held, moving,
// falling, not in control, ...), in which case no response is coming.
bool Send(void *player, void *target, uint64_t guid);
// For a caller that gave up waiting: if the player still holds `guid` as
+63 -74
View File
@@ -42,6 +42,7 @@
#include "../guid/Guid.h"
#include "../object/Resolve.h"
#include "../tick/WorldTick.h"
#include "../time/Clock.h"
#include "Request.h"
#include <cstdint>
@@ -55,12 +56,11 @@ namespace {
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
const Event::Custom::AutoReserve _reserve{kEventCompleted};
// Per-step timeout for the controller hook to fire on a queued
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
// servers, short enough that a single dropped response doesn't hang
// the whole scan.
constexpr int kStepTimeoutTicks = 180;
// How long one step may wait: for the server to answer a CMSG_LOOT it
// never answers, or for the engine to accept a send it keeps refusing
// (the player is moving). Long enough for high-latency servers, short
// enough that a dropped response doesn't hang the walk.
constexpr uint32_t kStepTimeoutMs = 3000;
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
// loot-window transitions. The hook swallows these only when the
@@ -71,6 +71,7 @@ constexpr int EVENT_LOOT_CLOSED = 0x10D;
enum class State {
Idle,
WaitingToSend, // the engine refused the send for now; retried each tick
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
};
@@ -90,7 +91,7 @@ struct ScanCtx {
State state = State::Idle;
void *player = nullptr;
uint64_t currentGuid = 0;
int ticksSinceTransition = 0;
uint32_t stepStartMs = 0;
bool suppressEvents = false; // read by the FireEvent hook
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
std::vector<uint64_t> queue;
@@ -128,55 +129,6 @@ using LootMoney_t = void(__fastcall *)(void *player);
using LootController_t = void(__fastcall *)(int lootStructPtr,
int coin,
int unk);
struct C3Vector {
float x, y, z;
};
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
C3Vector *outBuf);
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
constexpr float INTERACT_REACH = 1.3333333f;
constexpr float MIN_INTERACT_RANGE = 5.0f;
// === Range / lootability helpers (identical to Loot::Nearby) ===
bool IsLootableUnit(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
const uint32_t flags =
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
}
float BoundingRadius(const void *unit) {
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
}
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
auto vtable = *reinterpret_cast<void *const *const *>(unit);
auto fn = reinterpret_cast<GetPosition_t>(
*reinterpret_cast<const void *const *>(
reinterpret_cast<const uint8_t *>(vtable) +
VTBL_GET_POSITION_OFFSET));
return fn(unit, outBuf);
}
bool InInteractRange(const void *player, const void *target) {
C3Vector pBuf{};
C3Vector tBuf{};
const C3Vector *pPos = GetPosition(player, &pBuf);
const C3Vector *tPos = GetPosition(target, &tBuf);
if (pPos == nullptr || tPos == nullptr)
return false;
const float dx = pPos->x - tPos->x;
const float dy = pPos->y - tPos->y;
const float dz = pPos->z - tPos->z;
const float distSq = dx * dx + dy * dy + dz * dz;
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
if (range < MIN_INTERACT_RANGE)
range = MIN_INTERACT_RANGE;
return distSq <= range * range;
}
// === Scrape ===
@@ -278,17 +230,37 @@ void TryStartNext() {
StartLoot(guid);
}
void StartLoot(uint64_t guid) {
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
// Not sent (mounted mid-walk, or the engine's pre-checks refused): no
// response is coming, so skip without waiting out the timeout.
if (target == nullptr || !Request::Send(s_scan.player, target, guid)) {
void BeginStep(State state) {
s_scan.state = state;
s_scan.stepStartMs = Time::Clock::NowMs();
}
// Sends the current corpse's loot request. The corpse is checked again
// here, not only when the queue was built: the walk takes a round trip per
// corpse, and by now the corpse may be gone, looted, or out of reach
// because the player moved. The server refuses an out-of-reach corpse
// with an error, so skip it instead.
//
// When the engine refuses the send itself (the player is moving or
// falling, ...), the corpse stays current and `OnTick` retries it: the
// refusal is about the player, not the corpse, so skipping would drop
// every corpse left in the queue.
void SendCurrent() {
void *target =
Object::ByGuid(Offsets::TYPEMASK_UNIT, s_scan.currentGuid, nullptr, 0);
if (target == nullptr || !Request::IsLootable(target) ||
!Request::InReach(s_scan.player, target)) {
TryStartNext();
return;
}
if (Request::Send(s_scan.player, target, s_scan.currentGuid))
BeginStep(State::WaitingForResponse);
}
void StartLoot(uint64_t guid) {
s_scan.currentGuid = guid;
s_scan.state = State::WaitingForResponse;
s_scan.ticksSinceTransition = 0;
BeginStep(State::WaitingToSend);
SendCurrent();
}
// === Hooks ===
@@ -383,18 +355,35 @@ const Game::HookAutoRegister _hookController{
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
// === Tick (timeout only) ===
// === Tick (refusals and timeouts) ===
void OnTick() {
if (s_scan.state == State::Idle)
return;
++s_scan.ticksSinceTransition;
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
// Controller hook never fired — the server answered with a
// failure, or never answered. A failure already cleared the
// held target; an unanswered request still holds it, which
// would block all looting, so release it. Skip this corpse.
s_scan.suppressEvents = false;
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
if (s_scan.state == State::WaitingToSend) {
// Still refused: the player has not stopped moving, or mounted,
// or lost control. Every corpse left would be refused the same
// way, so end the walk.
if (timedOut) {
Complete();
return;
}
SendCurrent();
return;
}
// The server refused the corpse ("too far away", not ours, ...): its
// failure response and its release response both clear the held
// target, and no window opens. Go on to the next corpse now.
if (Request::HeldTarget(s_scan.player) != s_scan.currentGuid) {
TryStartNext();
return;
}
if (timedOut) {
// No answer at all. The held target would block all looting, so
// release it, and skip this corpse.
Request::ReleaseIfUnanswered(s_scan.player, s_scan.currentGuid);
TryStartNext();
}
@@ -405,7 +394,7 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
// === Enumeration ===
struct EnumCtx {
const void *player;
void *player;
std::vector<uint64_t> *out;
};
@@ -413,9 +402,9 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
if (obj == nullptr)
return 1;
if (!IsLootableUnit(obj))
if (!Request::IsLootable(obj))
return 1;
if (!InInteractRange(ctx->player, obj))
if (!Request::InReach(ctx->player, obj))
return 1;
ctx->out->push_back(guid);
return 1;
+2 -3
View File
@@ -20,6 +20,7 @@
#include "spell/Lookup.h"
#include "spell/Range.h"
#include "unit/Position.h"
#include "unit/Power.h"
#include <cstdint>
#include <cstdio>
@@ -147,10 +148,8 @@ static bool ReadSpellInfo(int spellID, int pet, SpellInfoData &out) {
out.powerType = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
using PowerHelper_t = int32_t(__fastcall *)(int spellID, int pet);
using DisplayDivisor_t = uint32_t(__fastcall *)(int powerType);
using CastTime_t = int32_t(__fastcall *)(int spellID, int pet, int allowNegative);
const auto divisor = static_cast<int32_t>(reinterpret_cast<DisplayDivisor_t>(
static_cast<uintptr_t>(Offsets::FUN_GET_POWER_DISPLAY_DIVISOR))(out.powerType));
const auto divisor = static_cast<int32_t>(Unit::Power::DisplayDivisor(out.powerType));
const int32_t cost = reinterpret_cast<PowerHelper_t>(
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_COST))(spellID, pet) / divisor;
const int32_t costPerSecond = reinterpret_cast<PowerHelper_t>(
+8 -4
View File
@@ -26,7 +26,9 @@
// type Enum.PowerType (0 mana, 1 rage, 2 focus, 3 energy,
// 4 happiness) — the spell's PowerType (+0x7C)
// name power token ("MANA", "RAGE", …)
// cost effective cost (see above)
// cost effective cost (see above), in displayed units — the
// helper answers in stored units (rage in tenths), so it
// is divided by `Unit::Power::DisplayDivisor`
// minCost == cost; vanilla has no "optional" cost component
// costPercent ManaCostPercent (+0x270) — the spell's %-of-base-
// resource cost, or 0 for a flat cost
@@ -49,7 +51,8 @@ namespace {
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int pet);
// The engine's cost helper, in STORED units (rage in tenths); 0xFFFFFFFF on
// error. Callers divide by `Unit::Power::DisplayDivisor` before surfacing it.
uint32_t EffectiveCost(int spellID) {
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
}
@@ -60,11 +63,12 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
if (rec == nullptr)
return 0; // nil — spell not found
const uint32_t cost = EffectiveCost(spellID);
if (cost == 0xFFFFFFFF)
const uint32_t storedCost = EffectiveCost(spellID);
if (storedCost == 0xFFFFFFFF)
return 0; // engine error (no player context, etc.)
const int powerType = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
const uint32_t cost = storedCost / Unit::Power::DisplayDivisor(powerType);
const int costPercent = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
if (cost == 0 && costPercent == 0)
return 0; // nil — no resource cost
+11 -17
View File
@@ -19,7 +19,7 @@
// is the same: read POWER[type] / MAXPOWER[type] off the unit's
// descriptor, defaulting `type` to the unit's primary power (the
// `UNIT_FIELD_BYTES_0` byte 3 at `descriptor + 0x7B`), then divide
// by the per-power-type display divisor (`VAR_UNIT_POWER_DIVISOR_TABLE`)
// by the engine's per-power-type display divisor (`DisplayDivisor`)
// before pushing. Rage internally stores 0..1000 but displays 0..100
// (divisor 10); happiness uses divisor 1000. Same divisor logic
// vanilla's own `Script_UnitMana` (`0x00517670`) applies.
@@ -58,6 +58,12 @@ const char *PowerTypeToken(int type) {
return "UNKNOWN";
}
unsigned int DisplayDivisor(int type) {
using Divisor_t = unsigned int(__fastcall *)(int powerType);
return reinterpret_cast<Divisor_t>(
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(type);
}
namespace {
const uint8_t *Descriptor(const uint8_t *unit) {
@@ -73,18 +79,6 @@ const uint8_t *ResolveUnit(const char *token) {
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
}
// Per-power-type display divisor. Vanilla stores rage as 0..1000
// internally and displays 0..100 (divide by 10); happiness is
// stored at 1000x scale. Same table the engine's own `Script_UnitMana`
// (`0x00517670`) reads at `VAR_UNIT_POWER_DIVISOR_TABLE`.
uint32_t PowerDivisor(int type) {
if (type < 0)
return 1;
auto *table = reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_UNIT_POWER_DIVISOR_TABLE));
return table[type];
}
// `Enum.PowerType` — modern Blizzard-style enum table. Slot 4 is
// `Happiness` in 1.12 (pet happiness 0..3); modern retail relabels
@@ -167,7 +161,7 @@ double RosterPowerFallback(void *L, RosterKind kind) {
return 0.0;
const uint32_t divisor =
Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(s.powerType);
Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(s.powerType);
const uint32_t curDisp = s.power / divisor;
const uint32_t maxDisp = s.maxPower / divisor;
switch (kind) {
@@ -202,7 +196,7 @@ static int __fastcall Script_UnitPower(void *L) {
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
// `unmodified` (arg 3, truthy) returns the raw internal value; otherwise
// apply the per-type display divisor. Matches retail's third UnitPower arg.
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
return 1;
}
@@ -223,7 +217,7 @@ static int __fastcall Script_UnitPowerMax(void *L) {
}
const uint32_t raw = *reinterpret_cast<const uint32_t *>(
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
return 1;
}
@@ -255,7 +249,7 @@ static int __fastcall Script_UnitPowerMissing(void *L) {
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
const uint32_t max = *reinterpret_cast<const uint32_t *>(
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
const uint32_t curDisp = cur / divisor;
const uint32_t maxDisp = max / divisor;
Game::Lua::PushNumber(
+6
View File
@@ -20,4 +20,10 @@ namespace Unit::Power {
// Used by `UnitPowerType`'s second return and by spell power-cost APIs.
const char *PowerTypeToken(int type);
// The engine's divisor from a stored power amount to displayed units
// (`FUN_POWER_DISPLAY_DIVISOR`): 10 for rage, 1000 for happiness, 1 otherwise.
// Every power amount surfaced to Lua — unit power, spell costs, combat-log
// amounts — goes through this.
unsigned int DisplayDivisor(int type);
} // namespace Unit::Power