mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 04:17:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7103de564 | ||
|
|
c00eb24a8d | ||
|
|
b65c9e0319 | ||
|
|
de033896b1 | ||
|
|
dc8d4caf12 | ||
|
|
20069f3815 | ||
|
|
f2fa112f0a | ||
|
|
fca967c827 |
@@ -221,7 +221,7 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, use an item, or open the flight map. It also lets you talk to NPCs and use objects in druid forms and Ghost Wolf. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
|
||||
+1001
-905
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -80,10 +80,13 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
// for any custom event still waiting. By the first FrameXML registration
|
||||
// the engine's table is fully populated and SuperWoWhook / other DLLs have
|
||||
// done their post-rebuild writes, so the walk from the low end finds
|
||||
// genuinely free slots. A registration that arrives EARLIER (another DLL's
|
||||
// frame, between LoadScriptFunctions and the table build — issue #134)
|
||||
// still sees glue's 26-entry table; `RetryClaims` refuses that table and
|
||||
// re-validates its cached slots against the live one on every call.
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
|
||||
+2
-1
@@ -12071,7 +12071,8 @@ enum Offsets {
|
||||
// column 11: the parsed 1.12 DBC gives 0x7 for the warrior stances, 0x1
|
||||
// Stealth, 0x9 Shadowform, matching 3.3.5's column. 0x100 is the form
|
||||
// flag 3.3.5 / 4.3.4 `CGUnit_C::CanAutoUnshift` refuses on (set there
|
||||
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it.
|
||||
// only on Spirit of Redemption and the Ghoul forms); no 1.12 row has it,
|
||||
// so Spell::AutoPrepare adds it to Spirit of Redemption (form 32).
|
||||
VAR_SPELL_SHAPESHIFT_FORM_RECORDS = 0x00C0D774,
|
||||
VAR_SPELL_SHAPESHIFT_FORM_COUNT = 0x00C0D778,
|
||||
OFF_SPELL_SHAPESHIFT_FORM_FLAGS = 0x2C,
|
||||
|
||||
+2
-9
@@ -1023,16 +1023,9 @@ CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, kPeriodicDamage) ||
|
||||
Spell::Lookup::HasAuraEffect(rec, kPeriodicLeech);
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
|
||||
@@ -618,17 +618,6 @@ uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
auraName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
Entry e;
|
||||
e.event = event;
|
||||
|
||||
@@ -167,10 +167,6 @@ uint32_t SchoolMask(uint32_t school);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "aura/Data.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -501,7 +502,7 @@ const uint8_t *ShieldSpell(uint64_t owner, uint32_t school) {
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(spellId));
|
||||
if (spell == nullptr ||
|
||||
Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL) != school ||
|
||||
!HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
!Spell::Lookup::HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
continue;
|
||||
if (found != nullptr && found != spell)
|
||||
return nullptr;
|
||||
|
||||
@@ -131,10 +131,7 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
Game::Lua::PushBool(L, RequestLoad(static_cast<uint32_t>(creatureID)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -381,4 +378,9 @@ const uint8_t *CacheRecord(uint32_t creatureID) {
|
||||
return creatureID != 0 ? PeekCreature(creatureID) : nullptr;
|
||||
}
|
||||
|
||||
bool RequestLoad(uint32_t creatureID) {
|
||||
return Cache::QueryLoad::RequestLoad(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
creatureID);
|
||||
}
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace Creature::Info {
|
||||
// engine's SetDisplay takes, and `PlayerModel:SetCreature` passes it there.
|
||||
const uint8_t *CacheRecord(uint32_t creatureID);
|
||||
|
||||
// Starts loading `creatureID` into the creature cache; fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when it lands (at once
|
||||
// when already cached). False only when the load could not be tracked.
|
||||
// Backs `C_CreatureInfo.RequestLoadCreatureByID`.
|
||||
bool RequestLoad(uint32_t creatureID);
|
||||
|
||||
// The faction group a player race belongs to, resolved the way
|
||||
// `Script_UnitFactionGroup` does it for a player whose object isn't in the
|
||||
// world (its name-cache branch, which reads the race): ChrRaces -> FactionTemplate
|
||||
|
||||
@@ -17,7 +17,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an objec
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, talk to an NPC, use an object or an item, or open the flight map.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, use an item, or open the flight map. Also lets you talk to NPCs and use objects in druid forms and Ghost Wolf.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
|
||||
@@ -19,7 +19,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при примене
|
||||
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, разговоре с NPC, использовании объекта или предмета, а также при открытии карты полетов.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, использовании предмета, а также при открытии карты полетов. Также позволяет говорить с NPC и использовать объекты в обликах друида и призрачного волка.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
|
||||
|
||||
@@ -19,7 +19,7 @@ CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、与NPC对话、使用物体或物品、或打开飞行地图时自动取消该形态。";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、使用物品或打开飞行地图时自动取消该形态。同时允许在德鲁伊形态和幽魂之狼形态下与NPC对话和使用物体。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
|
||||
+81
-19
@@ -55,6 +55,10 @@ struct Reservation {
|
||||
int slot; // -1 until claimed
|
||||
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
|
||||
int fixedId; // an `EngineName`'s engine id (its slot for good); -1 = claimed
|
||||
// The SStrDup'd name `Claim` wrote into `slot`'s entry; nullptr while
|
||||
// unclaimed. `RetryClaims` compares it against the live entry to notice
|
||||
// that the engine rebuilt the table underneath a cached slot.
|
||||
const char *claimed;
|
||||
};
|
||||
Reservation g_reserved[MAX_RESERVED];
|
||||
int g_reservedCount = 0;
|
||||
@@ -63,7 +67,7 @@ bool g_writesEnabled = false;
|
||||
// claim) is logged once per session instead of silently dropping the
|
||||
// event — the failure mode that would otherwise rot unnoticed. Re-armed on
|
||||
// `/reload` (the table is rebuilt fresh). In practice this never triggers:
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `TryClaim`) against
|
||||
// the engine leaves ~115 claimable gaps below 549 (see `Claim`) against
|
||||
// our ~77 reservations, and `Grow` adds room past that. The log is
|
||||
// purely a backstop if some future install somehow drains the gap pool.
|
||||
bool g_warnedTableFull = false;
|
||||
@@ -147,23 +151,39 @@ int ClaimDemand() {
|
||||
// tail. Below 549 only nampower writes, at 369 and 540, both before any Lua
|
||||
// runs, so skip-occupied steps over them. Claiming low keeps us out of
|
||||
// everyone's way and independent of their clamp / hook order.
|
||||
int TryClaim(const char *eventName) {
|
||||
//
|
||||
// Claims only into the IN-GAME table. `CGGameUI::Initialize` (0x0048FE00)
|
||||
// calls `RegisterScriptFunctions` (our `EnableWrites` hook) and only THEN
|
||||
// `SetScriptEventTable(0xBE1198, 0x225)`, so between the two the live table
|
||||
// is still glue's 26-entry one. A `Frame::RegisterEvent` in that window
|
||||
// (a third-party DLL registering a frame from its own LoadScriptFunctions
|
||||
// hook — issue #134) used to grow the glue table and claim into it; the
|
||||
// engine then freed that buffer and our cached ids silently aliased engine
|
||||
// events in the real table. The in-game build always passes 0x225 names
|
||||
// (clamps by other DLLs only enlarge it), so a smaller count is not it.
|
||||
bool GameTableInstalled(int count) {
|
||||
return count >= Offsets::EVENT_NAME_TABLE_STATIC_COUNT;
|
||||
}
|
||||
|
||||
void Claim(Reservation &r) {
|
||||
if (!g_writesEnabled)
|
||||
return -1;
|
||||
return;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (Claimable(base, i)) {
|
||||
auto **namePtr = Game::Ptr<const char *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE,
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
char *copy = SStrDup(eventName);
|
||||
char *copy = SStrDup(r.name);
|
||||
if (copy == nullptr)
|
||||
return -1;
|
||||
return;
|
||||
*namePtr = copy;
|
||||
return i;
|
||||
r.slot = i;
|
||||
r.claimed = copy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Writes are enabled and the table is valid, but every slot is taken —
|
||||
@@ -173,9 +193,39 @@ int TryClaim(const char *eventName) {
|
||||
Debug::Log::Printf(
|
||||
"[event] table full (%d slots, no NULL left) — cannot claim '%s'"
|
||||
" and any further custom events; even Grow couldn't make room.",
|
||||
count, eventName ? eventName : "?");
|
||||
count, r.name);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// True when a claimed reservation's slot no longer holds its name: the
|
||||
// engine rebuilt the table (every entry re-`SStrDup`'d at a fresh buffer)
|
||||
// without our `PrepareForReload` seeing it. The pointer test is the cheap
|
||||
// common case; the strcmp covers an allocator handing the engine our freed
|
||||
// block for a different name.
|
||||
bool Stale(const Reservation &r) {
|
||||
if (r.claimed == nullptr)
|
||||
return false;
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || r.slot >= count)
|
||||
return true;
|
||||
const char *live = Game::Read<const char *>(
|
||||
base + r.slot * Offsets::EVENT_ENTRY_STRIDE, Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (live == r.claimed)
|
||||
return false;
|
||||
return live == nullptr || std::strcmp(live, r.name) != 0;
|
||||
}
|
||||
|
||||
// Forget every claimed slot: the table they were claimed in is gone. The
|
||||
// engine freed our name copies with its per-entry teardown, so nothing to
|
||||
// release here. Re-arms the per-build one-shots.
|
||||
void ForgetClaims() {
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
g_reserved[i].claimed = nullptr;
|
||||
}
|
||||
g_warnedTableFull = false;
|
||||
g_growLatched = false;
|
||||
}
|
||||
|
||||
// On-demand event-table grow. Correctly TIMED: called once per in-game
|
||||
@@ -205,12 +255,14 @@ int TryClaim(const char *eventName) {
|
||||
void Grow() {
|
||||
if (g_growLatched || !g_writesEnabled)
|
||||
return;
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int count = Game::Read<int>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || !GameTableInstalled(count))
|
||||
return; // still glue's table — the in-game build hasn't run yet
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int demand = ClaimDemand();
|
||||
if (base == nullptr || count <= 0 || demand <= 0)
|
||||
if (demand <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
@@ -320,6 +372,7 @@ AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
|
||||
g_reserved[g_reservedCount].slot = -1;
|
||||
g_reserved[g_reservedCount].doc = doc;
|
||||
g_reserved[g_reservedCount].fixedId = -1;
|
||||
g_reserved[g_reservedCount].claimed = nullptr;
|
||||
index_ = g_reservedCount;
|
||||
++g_reservedCount;
|
||||
}
|
||||
@@ -339,7 +392,7 @@ EngineName::EngineName(int id, const char *name, const Game::Doc::Event *doc) {
|
||||
if (*slot != nullptr)
|
||||
return; // the engine names it; never take an engine event's slot
|
||||
*slot = name;
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id};
|
||||
g_reserved[g_reservedCount] = {name, id, doc, id, nullptr};
|
||||
++g_reservedCount;
|
||||
}
|
||||
|
||||
@@ -428,6 +481,18 @@ void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
// A table rebuilt behind our back (no FrameScript_Initialize in between)
|
||||
// leaves every cached slot pointing at whatever engine event now sits at
|
||||
// that id. Check the claims against the live table and start over if any
|
||||
// moved — we run before the original registers, so no frame has joined a
|
||||
// chain in the new table yet.
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (Stale(g_reserved[i])) {
|
||||
Debug::Log::Printf("[event] table rebuilt under cached slots — re-claiming");
|
||||
ForgetClaims();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Grow the table first if the low gap pool is short (dormant valve).
|
||||
// Runs at most once per table build; on the first call every chain is
|
||||
// still empty (this is the first registration), which is the only safe
|
||||
@@ -435,18 +500,15 @@ void RetryClaims() {
|
||||
Grow();
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
g_reserved[i].slot = TryClaim(g_reserved[i].name);
|
||||
Claim(g_reserved[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void EnableWrites() { g_writesEnabled = true; }
|
||||
|
||||
void PrepareForReload() {
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
g_reserved[i].slot = g_reserved[i].fixedId;
|
||||
ForgetClaims();
|
||||
g_writesEnabled = false;
|
||||
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
|
||||
g_growLatched = false; // re-arm the one-shot grow for the new table
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ inline void FireIdSuccess(int eventID, int id, bool success) {
|
||||
// catch any reservations that couldn't claim earlier.
|
||||
void RetryClaims();
|
||||
|
||||
// Internal: permit `TryClaim` to write to the event table. Held
|
||||
// Internal: permit `Claim` to write to the event table. Held
|
||||
// closed until `LoadScriptFunctions_h` returns, so the boot phase
|
||||
// (during which the engine and SuperWoWhook fire many internal
|
||||
// `RegisterEvent` calls) can't trigger our writes. Writing during
|
||||
|
||||
+61
-4
@@ -34,6 +34,15 @@
|
||||
// state machine — we no longer need `WaitingOpen` / `WaitingClose` /
|
||||
// per-tick `VAR_LOOT_GUID` reads, because the controller hook *is* the
|
||||
// state transition.
|
||||
//
|
||||
// Loot mode remembers corpses whose window opened EMPTY but which stay
|
||||
// LOOTABLE: the server keeps the flag while any loot is left (another
|
||||
// member's quest item, condition-gated loot) yet never sends this player a
|
||||
// slot it can't take, so an empty window is the server's own "nothing here
|
||||
// for you". Later walks skip such a corpse. A corpse whose window held
|
||||
// anything (bags full, a roll or master-loot assignment in progress) is not
|
||||
// remembered and is retried, because those refusals clear on their own.
|
||||
// See `s_emptyCorpses` for when an entry is forgotten.
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
@@ -45,6 +54,7 @@
|
||||
#include "../time/Clock.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -68,6 +78,8 @@ constexpr uint32_t kStepTimeoutMs = 3000;
|
||||
// fire unmodified.
|
||||
constexpr int EVENT_LOOT_OPENED = 0x10B;
|
||||
constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
// Observed, never swallowed (see `s_emptyCorpses`).
|
||||
constexpr int EVENT_QUEST_LOG_UPDATE = 0x134;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
@@ -100,6 +112,21 @@ struct ScanCtx {
|
||||
|
||||
ScanCtx s_scan;
|
||||
|
||||
// Loot-mode corpses whose window opened empty while the unit stayed
|
||||
// LOOTABLE (see the file comment). Outlives a walk. An entry is forgotten:
|
||||
// - on the first world tick where its unit is gone (despawned, out of
|
||||
// view) or no longer LOOTABLE (alive again after a respawn
|
||||
// with the same GUID, or emptied by someone else), so a corpse back in
|
||||
// view or a fresh corpse is opened again;
|
||||
// - all at once on QUEST_LOG_UPDATE: quest items are per-player loot, so
|
||||
// a quest change can turn an empty window into one with an item in it.
|
||||
std::vector<uint64_t> s_emptyCorpses;
|
||||
|
||||
bool IsKnownEmpty(uint64_t guid) {
|
||||
return std::find(s_emptyCorpses.begin(), s_emptyCorpses.end(), guid) !=
|
||||
s_emptyCorpses.end();
|
||||
}
|
||||
|
||||
// === Typedefs for engine helpers ===
|
||||
|
||||
using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
@@ -130,6 +157,19 @@ using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
|
||||
// Run every world tick, so no gone-and-back or respawn-and-die-again
|
||||
// can happen between two checks.
|
||||
void PruneEmptyCorpses() {
|
||||
s_emptyCorpses.erase(
|
||||
std::remove_if(s_emptyCorpses.begin(), s_emptyCorpses.end(),
|
||||
[](uint64_t guid) {
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT,
|
||||
guid, nullptr, 0);
|
||||
return unit == nullptr || !Request::IsLootable(unit);
|
||||
}),
|
||||
s_emptyCorpses.end());
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
|
||||
// Reads the post-population `VAR_LOOT_SLOTS` table into a LootEntry.
|
||||
@@ -325,6 +365,10 @@ void __fastcall LootController_h(int lootStructPtr, int coin, int unk) {
|
||||
LootEntry entry;
|
||||
entry.guid = s_scan.currentGuid;
|
||||
ScrapeCurrentLoot(&entry);
|
||||
// Each corpse is queued once per walk and known-empty ones are not
|
||||
// queued in loot mode, so this never adds a duplicate.
|
||||
if (s_scan.lootMode && entry.coin == 0 && entry.items.empty())
|
||||
s_emptyCorpses.push_back(entry.guid);
|
||||
s_scan.results.push_back(std::move(entry));
|
||||
|
||||
// Loot mode: actually take everything (coin + all slots) before the
|
||||
@@ -355,9 +399,19 @@ const Game::HookAutoRegister _hookController{
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (refusals and timeouts) ===
|
||||
// Observes only: the event still fires.
|
||||
bool ForgetEmptyCorpsesOnQuestChange(int eventID) {
|
||||
if (eventID == EVENT_QUEST_LOG_UPDATE)
|
||||
s_emptyCorpses.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _questIntercept{&ForgetEmptyCorpsesOnQuestChange};
|
||||
|
||||
// === Tick (memory upkeep, refusals and timeouts) ===
|
||||
|
||||
void OnTick() {
|
||||
PruneEmptyCorpses();
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
|
||||
@@ -396,6 +450,7 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
struct EnumCtx {
|
||||
void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
bool skipKnownEmpty;
|
||||
};
|
||||
|
||||
int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
@@ -404,17 +459,19 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
return 1;
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1;
|
||||
if (ctx->skipKnownEmpty && IsKnownEmpty(guid))
|
||||
return 1;
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void BuildQueue(void *player, size_t maxCount) {
|
||||
void BuildQueue(void *player, size_t maxCount, bool skipKnownEmpty) {
|
||||
s_scan.queue.clear();
|
||||
auto Enum = reinterpret_cast<ClntObjMgrEnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
EnumCtx ctx{player, &s_scan.queue};
|
||||
EnumCtx ctx{player, &s_scan.queue, skipKnownEmpty};
|
||||
Enum(reinterpret_cast<ClntObjMgrEnumVisibleObjectsCallback_t>(&EnumCallback),
|
||||
&ctx);
|
||||
// Cap to `maxCount` corpses (0 = unlimited). Order is engine
|
||||
@@ -440,7 +497,7 @@ bool BeginWalk(bool lootMode, size_t maxCount) {
|
||||
s_scan.results.clear();
|
||||
s_scan.player = player;
|
||||
s_scan.lootMode = lootMode;
|
||||
BuildQueue(player, maxCount);
|
||||
BuildQueue(player, maxCount, /*skipKnownEmpty=*/lootMode);
|
||||
|
||||
if (s_scan.queue.empty()) {
|
||||
Complete(); // nothing nearby — fire LOOT_SCAN_COMPLETED immediately
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_MountJournal.GetMountFromSpell(spellID)` -> mountID | nil
|
||||
// `C_MountJournal.GetMountInfoExtraByID(mountID)` -> creatureDisplayInfoID,
|
||||
// description, source, isSelfMount, mountTypeID, uiModelSceneID, animID,
|
||||
// spellVisualKitID, disablePlayerMountPreview
|
||||
//
|
||||
// Retail's mount ID indexes its Mount table. 1.12 has no such table, so here
|
||||
// a mount's ID IS its spell ID: GetMountFromSpell(458) returns 458. That
|
||||
// keeps every mount the client knows addressable (Turtle's own mounts are
|
||||
// in no retail table), and retail's numbering means nothing to a 1.12 addon.
|
||||
//
|
||||
// A spell is a mount when one of its effects applies SPELL_AURA_MOUNTED
|
||||
// (78). Spell.dbc is read directly, so a mount not yet learned resolves too.
|
||||
// On Turtle a mount ITEM does not carry its mount spell: every one carries
|
||||
// 46499 "Add Mount to Collection", which teaches a spell from a server-only
|
||||
// item table. So only the mount spell (spellbook, spell link) resolves; 46499
|
||||
// is not a mount. See docs/notes/model-scene.md.
|
||||
//
|
||||
// creatureDisplayInfoID is the display the server mounts the caster on: the
|
||||
// aura's misc value is a creature entry, and the server takes a display from
|
||||
// that creature's template (tortoise-wow Aura::HandleAuraMounted ->
|
||||
// Creature::ChooseDisplayId). The client's creature cache holds the
|
||||
// template's first display (SMSG_CREATURE_QUERY_RESPONSE sends display_id[0])
|
||||
// and every mount creature has exactly one, so the cached display is the
|
||||
// mounted one (23219 Swift Mistsaber -> creature 14555 -> 14332). An
|
||||
// uncached creature returns nil and starts the cache load;
|
||||
// CREATURE_DATA_LOAD_RESULT fires when it lands, as for
|
||||
// C_CreatureInfo.RequestLoadCreatureByID.
|
||||
//
|
||||
// The rest follows the retail Mount table where a 1.12 mount spell has a
|
||||
// row (kRetailRows), else retail's common values: mount type 230 (ground),
|
||||
// model scene 4, seat animation 91 (Mount — what a retail probe returned for
|
||||
// every mount, and what 1.12's world mount seats the rider with), no visual
|
||||
// kit. 1.12 has no self-mounts and no shop source text. The description is
|
||||
// the spell's own, tokens filled in.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "creature/Info.h"
|
||||
#include "spell/Arg.h"
|
||||
#include "spell/Description.h"
|
||||
#include "spell/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Mount::Journal {
|
||||
|
||||
namespace {
|
||||
|
||||
// Retail Mount rows (12.1.7.70740 export) for 1.12 mount spells whose scene
|
||||
// or mount type differs from the defaults below. Only rows whose spell has
|
||||
// the same name in Turtle's Spell.dbc: retail's 3363 (Nether-Swept Drake) is
|
||||
// Turtle's "Summon Riding Gryphon", so it is left out. The other 92 of the
|
||||
// 100 matching spells are scene 4 / type 230.
|
||||
struct RetailRow {
|
||||
uint32_t spellID;
|
||||
uint32_t mountTypeID;
|
||||
uint32_t uiModelSceneID;
|
||||
};
|
||||
constexpr RetailRow kRetailRows[] = {
|
||||
{25863, 230, 44}, // Black Qiraji Battle Tank
|
||||
{25953, 241, 44}, // Blue Qiraji Battle Tank
|
||||
{26054, 241, 44}, // Red Qiraji Battle Tank
|
||||
{26055, 241, 44}, // Yellow Qiraji Battle Tank
|
||||
{26056, 241, 44}, // Green Qiraji Battle Tank
|
||||
{26655, 230, 44}, // Black Qiraji Battle Tank
|
||||
{30174, 231, 4}, // Riding Turtle
|
||||
};
|
||||
constexpr uint32_t kDefaultMountTypeID = 230;
|
||||
constexpr uint32_t kDefaultUiModelSceneID = 4;
|
||||
constexpr uint32_t kSeatAnimation = 91; // Mount
|
||||
|
||||
// The mount spell's record for `mountID`, or null when it is not a mount.
|
||||
const uint8_t *MountRecord(int mountID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(mountID);
|
||||
return Spell::Lookup::HasAuraEffect(record, Offsets::SPELL_AURA_MOUNTED) ? record : nullptr;
|
||||
}
|
||||
|
||||
// The display the mount aura's creature is cached with, 0 when the creature
|
||||
// is not cached yet (and then its load is started).
|
||||
uint32_t MountDisplayID(const uint8_t *record) {
|
||||
const int effect = Spell::Lookup::FindAuraEffect(record, Offsets::SPELL_AURA_MOUNTED);
|
||||
const int creatureID =
|
||||
Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_EFFECT_MISC_VALUE + effect * 4);
|
||||
if (creatureID <= 0)
|
||||
return 0;
|
||||
const uint8_t *creature = Creature::Info::CacheRecord(static_cast<uint32_t>(creatureID));
|
||||
if (creature == nullptr) {
|
||||
Creature::Info::RequestLoad(static_cast<uint32_t>(creatureID));
|
||||
return 0;
|
||||
}
|
||||
return Game::Read<uint32_t>(creature, Offsets::OFF_CREATURE_DISPLAYID);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMountFromSpell(void *L) {
|
||||
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
|
||||
if (MountRecord(spellID) == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMountInfoExtraByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
const int mountID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const uint8_t *record = MountRecord(mountID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
|
||||
uint32_t mountTypeID = kDefaultMountTypeID;
|
||||
uint32_t uiModelSceneID = kDefaultUiModelSceneID;
|
||||
for (const RetailRow &row : kRetailRows) {
|
||||
if (row.spellID == static_cast<uint32_t>(mountID)) {
|
||||
mountTypeID = row.mountTypeID;
|
||||
uiModelSceneID = row.uiModelSceneID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (const uint32_t displayID = MountDisplayID(record))
|
||||
Game::Lua::PushNumber(L, static_cast<double>(displayID));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
char description[1024];
|
||||
Spell::Description::Expand(record, description, sizeof description);
|
||||
Game::Lua::PushString(L, description);
|
||||
Game::Lua::PushString(L, ""); // source
|
||||
Game::Lua::PushBool(L, false); // isSelfMount
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mountTypeID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(uiModelSceneID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(kSeatAnimation));
|
||||
Game::Lua::PushNumber(L, 0); // spellVisualKitID
|
||||
Game::Lua::PushBool(L, false); // disablePlayerMountPreview
|
||||
return 9;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kGetMountFromSpellArgs[] = {
|
||||
Game::Doc::Req("spellID", "SpellIdentifier", "A spell ID, spell link, or spell name."),
|
||||
};
|
||||
const Game::Doc::Field kGetMountFromSpellRets[] = {
|
||||
Game::Doc::Opt("mountID", "number", nullptr,
|
||||
"The mount's ID, which is its spell ID. Nil when the spell is not a mount."),
|
||||
};
|
||||
const Game::Doc::Function kGetMountFromSpell{
|
||||
"The mount a spell summons. A mount's ID is the ID of the spell that summons it.",
|
||||
kGetMountFromSpellArgs, kGetMountFromSpellRets};
|
||||
|
||||
const Game::Doc::Field kGetMountInfoExtraByIDArgs[] = {
|
||||
Game::Doc::Req("mountID", "number", "A mount ID, which is the mount's spell ID."),
|
||||
};
|
||||
const Game::Doc::Field kGetMountInfoExtraByIDRets[] = {
|
||||
Game::Doc::Opt("creatureDisplayInfoID", "number", nullptr,
|
||||
"The display the player is mounted on. Nil until the mount's creature "
|
||||
"is cached; the call starts loading it, and CREATURE_DATA_LOAD_RESULT "
|
||||
"fires when it arrives."),
|
||||
Game::Doc::Req("description", "cstring", "The mount spell's description."),
|
||||
Game::Doc::Req("source", "cstring", "Always empty."),
|
||||
Game::Doc::Req("isSelfMount", "bool", "Always false."),
|
||||
Game::Doc::Req("mountTypeID", "number", "The mount type; 230 for most mounts."),
|
||||
Game::Doc::Req("uiModelSceneID", "number", "The model scene that shows the mount."),
|
||||
Game::Doc::Req("animID", "number", "The animation the rider plays while mounted."),
|
||||
Game::Doc::Req("spellVisualKitID", "number", "Always 0."),
|
||||
Game::Doc::Req("disablePlayerMountPreview", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Function kGetMountInfoExtraByID{
|
||||
"Display and preview details of a mount. Returns nothing when the ID is not a mount.",
|
||||
kGetMountInfoExtraByIDArgs, kGetMountInfoExtraByIDRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_MountJournal", "GetMountFromSpell",
|
||||
&Script_GetMountFromSpell, &kGetMountFromSpell);
|
||||
Game::Lua::RegisterTableFunction("C_MountJournal", "GetMountInfoExtraByID",
|
||||
&Script_GetMountInfoExtraByID, &kGetMountInfoExtraByID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Mount::Journal
|
||||
@@ -40,6 +40,13 @@
|
||||
// cancel / stand before the cast packet that follows it.
|
||||
//
|
||||
// autoUnshift and autoDismount also cover interaction, as in 4.3.4:
|
||||
// - the interact flag: the later clients' SpellShapeshiftForm.dbc gives
|
||||
// SHAPESHIFT_FORM_FLAG_CAN_INTERACT to the druid forms and Ghost Wolf,
|
||||
// so 4.3.4 talks and uses objects in them without leaving the form.
|
||||
// 1.12's rows lack it; with autoUnshift on, the gates read it as the
|
||||
// later clients give it, so the cancels below are only for a form that
|
||||
// cannot interact in either client. Spirit of Redemption likewise
|
||||
// reads the later clients' NO_AUTO_UNSHIFT, so it is never cancelled.
|
||||
// - NPC right-click: 4.3.4's CGPlayer_C::HandleOnRightClick cancels a form
|
||||
// without SHAPESHIFT_FORM_FLAG_CAN_INTERACT in place of
|
||||
// ERR_CANT_INTERACT_SHAPESHIFTED, before its range check, so we do the
|
||||
@@ -200,10 +207,43 @@ bool CanAutoDismount(const void *unit) {
|
||||
return IsActivePlayer(unit) && !OnTaxi(unit) && Enabled(g_autoDismount);
|
||||
}
|
||||
|
||||
// 4.3.4 CGUnit_C::CanAutoUnshift.
|
||||
// SpellShapeshiftForm flags that 3.3.5 and 4.4.2 give (both DBCs parsed)
|
||||
// but 1.12's rows lack, for the forms a 1.12 player can take:
|
||||
// - CAN_INTERACT on Cat (1), Travel (3), Aquatic (4), Bear (5), Dire Bear
|
||||
// (8), Ghost Wolf (16), and 11, which Turtle uses for Swift Travel Form;
|
||||
// - NO_AUTO_UNSHIFT on Spirit of Redemption (32, 0x4100 there, 0 in 1.12).
|
||||
// Every other 1.12 player form either has the same flags or has flag bit 0
|
||||
// (Moonkin, Tree of Life, the stances, Stealth), so IsShapeShifted does not
|
||||
// count it and no gate or form rung tests it.
|
||||
struct LaterFormFlags {
|
||||
int form;
|
||||
uint32_t flags;
|
||||
};
|
||||
constexpr LaterFormFlags kLaterFormFlags[] = {
|
||||
{1, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{3, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{4, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{5, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{8, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{11, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{16, Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT},
|
||||
{32, Offsets::SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT},
|
||||
};
|
||||
|
||||
uint32_t AddedFormFlags(const void *unit) {
|
||||
const int form = Form(unit);
|
||||
for (const LaterFormFlags &entry : kLaterFormFlags) {
|
||||
if (entry.form == form)
|
||||
return entry.flags;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 4.3.4 CGUnit_C::CanAutoUnshift, on the later clients' NO_AUTO_UNSHIFT.
|
||||
bool CanAutoUnshift(const void *unit) {
|
||||
return IsActivePlayer(unit) && !OnTaxi(unit) && Enabled(g_autoUnshift) &&
|
||||
(FormFlags(unit) & Offsets::SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT) == 0;
|
||||
((FormFlags(unit) | AddedFormFlags(unit)) &
|
||||
Offsets::SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT) == 0;
|
||||
}
|
||||
|
||||
int StandState(const void *unit) {
|
||||
@@ -238,15 +278,31 @@ char IsShapeShifted(const void *unit) {
|
||||
return reinterpret_cast<IsShapeShifted_t>(Offsets::FUN_UNIT_IS_SHAPESHIFTED)(unit);
|
||||
}
|
||||
|
||||
// The gates' test after IsShapeShifted: a form with no row, or one
|
||||
// without the interact flag.
|
||||
bool FormBlocksInteraction(const void *unit) {
|
||||
return (FormFlags(unit) & Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT) == 0;
|
||||
// The gates' test after IsShapeShifted: the form row's interact flag. With
|
||||
// autoUnshift on, a form reads the flag that the later clients give it, so
|
||||
// the form stays on for the forms that can interact there. No 1.12 server
|
||||
// tests the form on an NPC or a plain object use (vmangos / tortoise-wow
|
||||
// CanInteractWithNPC, the use handlers, and no form spell's AuraInterrupt-
|
||||
// Flags has the use bit 0x800), so the client gates are the only refusal.
|
||||
// A locked object opens by a spell instead, which the server's CheckCast
|
||||
// form test refuses on 1.12's spell data (Opening 6478 keeps NOT_SHAPESHIFT,
|
||||
// which 3.3.5 dropped), so the cast's form rung must still unshift there.
|
||||
bool FormCanInteract(const void *unit) {
|
||||
if ((FormFlags(unit) & Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT) != 0)
|
||||
return true;
|
||||
return Enabled(g_autoUnshift) &&
|
||||
(AddedFormFlags(unit) & Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT) != 0;
|
||||
}
|
||||
|
||||
// True when the gate would refuse and autoUnshift takes the form off instead.
|
||||
bool UnshiftsForInteraction(const void *player) {
|
||||
return IsShapeShifted(player) && FormBlocksInteraction(player) && CanAutoUnshift(player);
|
||||
return IsShapeShifted(player) && !FormCanInteract(player) && CanAutoUnshift(player);
|
||||
}
|
||||
|
||||
// The gates' IsShapeShifted: "not shifted" when the form can interact, so
|
||||
// the engine's own flag test that follows does not refuse it.
|
||||
char ShiftedForGate(const void *player) {
|
||||
return IsShapeShifted(player) && !FormCanInteract(player);
|
||||
}
|
||||
|
||||
// The NPC right-click's IsShapeShifted: leaves the form and reports "not
|
||||
@@ -256,14 +312,14 @@ char __fastcall IsShapeShiftedForTalk(const void *player) {
|
||||
CancelForm(player);
|
||||
return 0;
|
||||
}
|
||||
return IsShapeShifted(player);
|
||||
return ShiftedForGate(player);
|
||||
}
|
||||
|
||||
// The game-object use check's and CanInteractNow's IsShapeShifted (4.3.4's
|
||||
// CanUseNow / CanInteractNow): the same verdict, nothing done. Both are
|
||||
// hover-cursor queries, so the cursor shows the service while shifted.
|
||||
char __fastcall IsShapeShiftedForQuery(const void *player) {
|
||||
return UnshiftsForInteraction(player) ? 0 : IsShapeShifted(player);
|
||||
return UnshiftsForInteraction(player) ? 0 : ShiftedForGate(player);
|
||||
}
|
||||
|
||||
// The mount reads of the gates 4.3.4 passes on CanAutoDismount (an object
|
||||
|
||||
@@ -53,8 +53,12 @@
|
||||
// +15%/rank SpellMod), so an 18-point gap leaked into healing when that pure
|
||||
// +damage buff was gained. Reading aura 175 directly (above) sidesteps it.
|
||||
//
|
||||
// 3. Set bonuses — the bonus spells of every equipped set whose member
|
||||
// count is met (ItemSet.dbc, the same rule the tooltip's set-bonus
|
||||
// lines use), decoded through the same aura walk.
|
||||
//
|
||||
// Residual: assumes talent ranks supersede (only the learned rank is known —
|
||||
// otherwise summed) and misses set-bonus healing granted via a set spell.
|
||||
// otherwise summed) and ignores a set's profession-skill requirement.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -185,11 +189,52 @@ void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
|
||||
}
|
||||
}
|
||||
|
||||
// Set bonuses the equipped gear has earned. The engine's own set-bonus lines
|
||||
// (the item tooltip, 0x52DFB9..) count how many of the set's members the
|
||||
// player has equipped and light a bonus whose member count (ItemSet.dbc
|
||||
// `+0x8C`) is met; the server applies the same bonus spells as auras. Those
|
||||
// auras are neither buffs nor known spells, so this is the only pass that
|
||||
// sees them — Lawbringer (3) is "Increase Healing 31", Judgement (3) is 57.
|
||||
// A set member's record names its set (`OFF_ITEMSTATS_ITEM_SET`), so the
|
||||
// equipped count per set is the equipped members' count.
|
||||
constexpr int kEquipSlots = 19;
|
||||
|
||||
void AddSetBonusHealing(long &total, int32_t spirit, int32_t armor) {
|
||||
struct Worn { uint32_t setID; int count; };
|
||||
Worn worn[kEquipSlots];
|
||||
int sets = 0;
|
||||
for (int slot = 1; slot <= kEquipSlots; ++slot) {
|
||||
const int itemID = Item::ID::FromCGItem(Item::Location::ResolveEquipmentSlot(slot));
|
||||
const uint8_t *record = itemID > 0 ? Item::PeekRecord(static_cast<uint32_t>(itemID)) : nullptr;
|
||||
const uint32_t setID = record ? Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_ITEM_SET) : 0;
|
||||
if (setID == 0)
|
||||
continue;
|
||||
int s = 0;
|
||||
while (s < sets && worn[s].setID != setID)
|
||||
++s;
|
||||
if (s == sets)
|
||||
worn[sets++] = {setID, 0};
|
||||
++worn[s].count;
|
||||
}
|
||||
for (int s = 0; s < sets; ++s) {
|
||||
const uint8_t *set = DBC::Record(Offsets::VAR_ITEMSET_RECORDS, Offsets::VAR_ITEMSET_COUNT,
|
||||
worn[s].setID);
|
||||
if (set == nullptr)
|
||||
continue;
|
||||
auto spells = Game::Ptr<const uint32_t>(set, Offsets::OFF_ITEMSET_SPELL_IDS);
|
||||
auto needed = Game::Ptr<const uint32_t>(set, Offsets::OFF_ITEMSET_THRESHOLDS);
|
||||
for (int i = 0; i < Offsets::ITEMSET_MAX_BONUSES; ++i)
|
||||
if (spells[i] != 0 && needed[i] <= static_cast<uint32_t>(worn[s].count))
|
||||
AddSpellHealingAuras(total, static_cast<int>(spells[i]), spirit, armor,
|
||||
/*requirePassive=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
long ComputeHealing() {
|
||||
long total = 0;
|
||||
|
||||
// Items — flat +healing via the shared StatAccum engine.
|
||||
for (int slot = 1; slot <= 19; ++slot)
|
||||
for (int slot = 1; slot <= kEquipSlots; ++slot)
|
||||
total += ItemFlatHealing(Item::Location::ResolveEquipmentSlot(slot));
|
||||
|
||||
// Player Spirit / Armor for the aura-175 / aura-199 conversions.
|
||||
@@ -199,6 +244,8 @@ long ComputeHealing() {
|
||||
const int32_t armor = desc ? Game::Read<int32_t>(
|
||||
desc, Offsets::OFF_UNIT_FIELD_RESISTANCE_ARMOR) : 0;
|
||||
|
||||
AddSetBonusHealing(total, spirit, armor);
|
||||
|
||||
// Active buffs — flat healing + Spirit/Armor conversions, one pass.
|
||||
auto *table = reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_TABLE));
|
||||
|
||||
@@ -21,14 +21,7 @@ namespace Spell::CrowdControl {
|
||||
const char *Classify(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
auto has = [&](int type) {
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == type)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
auto has = [rec](int type) { return Spell::Lookup::HasAuraEffect(rec, type); };
|
||||
if (has(Offsets::SPELL_AURA_MOD_POSSESS)) return "POSSESS";
|
||||
if (has(Offsets::SPELL_AURA_MOD_CHARM)) return "CHARM";
|
||||
if (has(Offsets::SPELL_AURA_MOD_STUN)) return "STUN";
|
||||
@@ -50,12 +43,7 @@ bool IsCrowdControl(uint32_t spellID) {
|
||||
// slows / snares, which `Classify` (the LoC set) deliberately excludes.
|
||||
if (Classify(rec) != nullptr)
|
||||
return true;
|
||||
const int32_t *aura = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i)
|
||||
if (aura[i] == Offsets::SPELL_AURA_MOD_DECREASE_SPEED)
|
||||
return true;
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(rec, Offsets::SPELL_AURA_MOD_DECREASE_SPEED);
|
||||
}
|
||||
|
||||
} // namespace Spell::CrowdControl
|
||||
|
||||
+5
-9
@@ -534,15 +534,11 @@ static int __fastcall Script_GetPlayerSpellsByAura(void *L) {
|
||||
|
||||
int n = 0;
|
||||
ForEachKnownSpell([&](int spellID, const uint8_t *record) {
|
||||
auto *auras = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int e = 0; e < Offsets::SPELL_RECORD_EFFECT_COUNT; ++e) {
|
||||
if (auras[e] == auraName) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
break; // one entry per spell, however many effects match
|
||||
}
|
||||
// One entry per spell, however many effects match.
|
||||
if (Spell::Lookup::HasAuraEffect(record, auraName)) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -62,6 +62,21 @@ bool HasEffect(const uint8_t *spellRecord, int effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int FindAuraEffect(const uint8_t *spellRecord, int aura) {
|
||||
if (spellRecord == nullptr)
|
||||
return -1;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spellRecord, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
aura)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spellRecord, int aura) {
|
||||
return FindAuraEffect(spellRecord, aura) >= 0;
|
||||
}
|
||||
|
||||
int SpellbookSlotToID(int slot1Based, int bookType) {
|
||||
const int slot = slot1Based - 1;
|
||||
if (slot < 0 || slot >= Offsets::SPELLBOOK_MAX_SLOTS)
|
||||
|
||||
@@ -35,6 +35,15 @@ bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
|
||||
// SpellHasEffect. False for a null record.
|
||||
bool HasEffect(const uint8_t *spellRecord, int effect);
|
||||
|
||||
// The index (0..2) of the first of the spell's three effects that applies
|
||||
// the aura `aura` (EffectApplyAuraName), or -1 when none does or the record
|
||||
// is null. For reading that effect's other columns (its misc value, ...).
|
||||
int FindAuraEffect(const uint8_t *spellRecord, int aura);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `aura` —
|
||||
// 4.3.4's SpellHasAuraEffect. False for a null record.
|
||||
bool HasAuraEffect(const uint8_t *spellRecord, int aura);
|
||||
|
||||
// Resolves a 1-based Lua-facing spellbook slot to the spellID stored
|
||||
// there. `bookType` is `0` for the player spellbook, `1` for the pet
|
||||
// spellbook (matches the engine's encoding from `Script_GetSpellName`).
|
||||
|
||||
+3
-10
@@ -46,16 +46,9 @@ static constexpr int32_t AURA_TRACK_STEALTHED = 151;
|
||||
// True iff `spellID`'s record has any of the three Track* effects.
|
||||
static bool IsTrackingSpell(int spellID) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
auto *aura = Game::Ptr<const int32_t>(
|
||||
record, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (aura[i] == AURA_TRACK_CREATURES || aura[i] == AURA_TRACK_RESOURCES ||
|
||||
aura[i] == AURA_TRACK_STEALTHED)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return Spell::Lookup::HasAuraEffect(record, AURA_TRACK_CREATURES) ||
|
||||
Spell::Lookup::HasAuraEffect(record, AURA_TRACK_RESOURCES) ||
|
||||
Spell::Lookup::HasAuraEffect(record, AURA_TRACK_STEALTHED);
|
||||
}
|
||||
|
||||
// Walks the player spellbook in slot order counting tracking spells. If
|
||||
|
||||
Reference in New Issue
Block a user