mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 15:10:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b36bb10e6 | ||
|
|
6628dcca19 |
+8
-24
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>(
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user