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
This commit is contained in:
Brues
2026-11-05 08:51:59 -06:00
parent 6628dcca19
commit 6b36bb10e6
6 changed files with 37 additions and 51 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;
}
+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