mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d370ada65 | ||
|
|
35c78ea6be | ||
|
|
ad83eb9575 | ||
|
|
8995e82627 |
@@ -35,7 +35,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
|
||||
+36
@@ -152,6 +152,7 @@ build instructions.
|
||||
- [`IsLeftControlKeyDown()` / `IsRightControlKeyDown()`](#isleftcontrolkeydown--isrightcontrolkeydown)
|
||||
- [`IsLeftAltKeyDown()` / `IsRightAltKeyDown()`](#isleftaltkeydown--isrightaltkeydown)
|
||||
- [`IsModifierKeyDown()`](#ismodifierkeydown)
|
||||
- [`IsMouseButtonDown([button])`](#ismousebuttondownbutton)
|
||||
|
||||
- [Instance](#instance)
|
||||
- [`GetInstanceInfo()`](#getinstanceinfo)
|
||||
@@ -3337,6 +3338,41 @@ otherwise. Equivalent to
|
||||
`IsShiftKeyDown() or IsControlKeyDown() or IsAltKeyDown()` but in one
|
||||
call.
|
||||
|
||||
### `IsMouseButtonDown([button])`
|
||||
|
||||
Returns `true` if the given mouse button is currently held, `false`
|
||||
otherwise. With no argument (or `nil`), returns `true` if **any**
|
||||
mouse button is held.
|
||||
|
||||
`button` is either a 1-based ID or a name string:
|
||||
|
||||
| ID | Name |
|
||||
|-----|------|
|
||||
| `1` | `"LeftButton"` |
|
||||
| `2` | `"RightButton"` |
|
||||
| `3` | `"MiddleButton"` |
|
||||
| `4` | `"Button4"` (XBUTTON1 / first side button) |
|
||||
| `5` | `"Button5"` (XBUTTON2 / second side button) |
|
||||
|
||||
Unrecognized IDs / names return `false` (matches modern semantics:
|
||||
the named button just isn't held — bad input doesn't fall through
|
||||
to the any-button check).
|
||||
|
||||
State is maintained from the same `WH_GETMESSAGE` hook that drives
|
||||
`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP`, so press/release transitions
|
||||
register at exactly the moment the corresponding event fires. Hook
|
||||
scopes to WoW's message queue — clicks made while the game is
|
||||
alt-tabbed out don't deliver, which means the state can stay
|
||||
"down" if a button was held while focus left the window. Most uses
|
||||
of `IsMouseButtonDown` are gated behind a frame that just received
|
||||
mouse focus, so this rarely matters in practice.
|
||||
|
||||
```lua
|
||||
if IsMouseButtonDown("RightButton") then
|
||||
-- right-button-held bindings
|
||||
end
|
||||
```
|
||||
|
||||
## Instance
|
||||
|
||||
### `GetInstanceInfo()`
|
||||
|
||||
@@ -1488,6 +1488,35 @@ enum Offsets {
|
||||
VAR_SKILL_LINE_COUNT = 0x00C0D930,
|
||||
OFF_SKILL_LINE_NAME = 0x0C,
|
||||
|
||||
// Player's known-skills table — separate from the DBC. Populated
|
||||
// by the engine on login from server data; entries are pointers
|
||||
// to per-skill records. Each record's `+0x00` is the
|
||||
// `SkillLine.dbc` row (or `0` for header / category rows), and
|
||||
// `+0x10` is the current skill rank (other rank/modifier fields
|
||||
// follow). Source: `Script_GetSkillLineInfo` at `0x004D3610`,
|
||||
// which walks this array indexed by the Lua arg.
|
||||
VAR_PLAYER_SKILL_LIST = 0x00B7318C,
|
||||
VAR_PLAYER_SKILL_COUNT = 0x00B731B8,
|
||||
OFF_PLAYER_SKILL_LINE_ID = 0x00,
|
||||
OFF_PLAYER_SKILL_RANK = 0x10,
|
||||
|
||||
// Per-itemClass proficiency bitmap maintained by the engine.
|
||||
// 17 u32 entries (indexed by Item.dbc `m_class`, 0..16). Each
|
||||
// entry is a bitmask where bit N is set if the player has
|
||||
// proficiency in subclass N of that item class — i.e.
|
||||
// `table[4] & (1 << 3)` = "can wear Mail" (armor=class 4,
|
||||
// mail=subclass 3).
|
||||
//
|
||||
// Populated by SMSG_SET_PROFICIENCY's handler at
|
||||
// `FUN_005E7B70`, which writes `table[itemClass] = mask`
|
||||
// verbatim from the packet. Same source the engine itself uses
|
||||
// for equip eligibility (the tooltip "Mail" red-text logic
|
||||
// and the cursor-drop-on-paperdoll check both read this). Note:
|
||||
// covers **weapons too** (class 2), making it strictly more
|
||||
// general than walking the per-skill-line table for individual
|
||||
// armor proficiencies.
|
||||
VAR_PROFICIENCY_TABLE = 0x00C4D4A0,
|
||||
|
||||
// Spell.dbc `BaseLevel` — the level a spell becomes available
|
||||
// (trainer offers it, quest reward grants it, etc.). Distinct
|
||||
// from `SpellLevel` at +0x74 which is the effective level used
|
||||
|
||||
+12
-1
@@ -115,13 +115,24 @@ int PushFieldOrNil(void *L, FieldByName_t accessor) {
|
||||
// `C_AddOns.GetAddOnName(indexOrName)` — the addon's directory name.
|
||||
// For numeric input, returns the engine's canonical casing. For
|
||||
// string input, returns the input verbatim once existence is
|
||||
// confirmed.
|
||||
// confirmed; nil for an unregistered name.
|
||||
//
|
||||
// String-input existence isn't free: `ResolveAddOnName` echoes the
|
||||
// string for the field accessors below (`GetAddOnTitle`, etc.) which
|
||||
// short-circuit cleanly on unknown names via the engine's own NULL-
|
||||
// return paths. `GetAddOnName` doesn't call any engine accessor, so
|
||||
// it'd otherwise echo any string. Probe the registry explicitly.
|
||||
int __fastcall Script_GetAddOnName(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING &&
|
||||
!AddOnExistsByName(reinterpret_cast<const char *>(entry))) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, reinterpret_cast<const char *>(entry));
|
||||
return 1;
|
||||
}
|
||||
|
||||
+114
-38
@@ -424,60 +424,136 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
}
|
||||
int nextFreeSlot = 0;
|
||||
|
||||
// Pre-snapshot each set item's location against the ORIGINAL
|
||||
// state, before any swap packets fly. This mirrors how 4.3.4's
|
||||
// `Script_UseEquipmentSet` (`0x009A15D0`) builds its action list:
|
||||
// resolve all 19 source locations up front, then execute. The
|
||||
// engine's CGItem / InvMgr state lags behind our packet sends
|
||||
// (waits for server ACK), so any `FindGUID` mid-execution would
|
||||
// return stale answers and we'd re-resolve items into the wrong
|
||||
// place.
|
||||
int sourceLoc[SLOT_COUNT];
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED) {
|
||||
sourceLoc[i] = 0;
|
||||
continue;
|
||||
}
|
||||
sourceLoc[i] = Locations::FindGUID(g);
|
||||
}
|
||||
|
||||
// Virtual paperdoll occupant model — seeded from the engine's
|
||||
// initial paperdoll GUID array, then updated in-place after each
|
||||
// swap so phase 1 can chain rotations and phase 3 can spot
|
||||
// already-empty slots.
|
||||
uint64_t virtualPaperdoll[SLOT_COUNT];
|
||||
Locations::SnapshotPaperdoll(virtualPaperdoll);
|
||||
|
||||
// Locates the current virtual slot of a GUID on the paperdoll,
|
||||
// or 0 if it's not currently there. Used during phase 1 so
|
||||
// chained paperdoll-to-paperdoll moves operate on the *current*
|
||||
// virtual position, not the stale pre-snapshot.
|
||||
auto currentPaperdollSlot = [&](uint64_t g) -> int {
|
||||
if (g == 0)
|
||||
return 0;
|
||||
for (int slot = 1; slot <= SLOT_COUNT; ++slot) {
|
||||
if (virtualPaperdoll[slot - 1] == g)
|
||||
return slot;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// ── Phase 1: paperdoll → paperdoll ─────────────────────────────
|
||||
// Move items already on the paperdoll into their target slots.
|
||||
// Source and destination are both paperdoll, so atomic swaps
|
||||
// never touch bag state — phase 2's bag lookups stay accurate.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
|
||||
// GUID_IGNORED: leave whatever the player has in this slot
|
||||
// as-is. Distinct from GUID_EMPTY (= 0), which means "this
|
||||
// slot should be empty after the swap" — handled below.
|
||||
if (g == GUID_IGNORED)
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
|
||||
if (g == GUID_EMPTY) {
|
||||
// Set wants this slot empty. If it already is, no-op.
|
||||
// Otherwise unequip whatever's there into the next bag
|
||||
// slot we reserved during the pre-loop snapshot. If
|
||||
// we've exhausted the snapshot, silently skip — matches
|
||||
// the modern API which surfaces no per-slot reason.
|
||||
const uint8_t *equipped =
|
||||
Item::Location::ResolveEquipmentSlot(targetSlot);
|
||||
if (equipped == nullptr)
|
||||
continue;
|
||||
if (nextFreeSlot >= freeSlotCount)
|
||||
continue;
|
||||
const FreeSlot &fs = freeSlots[nextFreeSlot++];
|
||||
Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int loc = Locations::FindGUID(g);
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue; // missing — nothing to do
|
||||
|
||||
// Already in its target slot? Skip.
|
||||
if ((loc & LOC_BAGS) == 0 && (loc & LOC_BANK) == 0 &&
|
||||
(loc & LOC_SLOT_MASK) == targetSlot)
|
||||
continue;
|
||||
|
||||
// In bank (main or bank-bag) — server rejects equip-from-bank.
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
continue; // server rejects equip-from-bank
|
||||
if (loc & LOC_BAGS)
|
||||
continue; // phase 2
|
||||
|
||||
const int srcSlot = currentPaperdollSlot(g);
|
||||
if (srcSlot == 0 || srcSlot == targetSlot)
|
||||
continue; // missing or already there
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
if (loc & LOC_BAGS) {
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot);
|
||||
} else {
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
Item::Swap::FromPaperdoll(item, srcSlot, targetSlot);
|
||||
const uint64_t displaced = virtualPaperdoll[targetSlot - 1];
|
||||
if (Item::Swap::FromPaperdoll(item, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
virtualPaperdoll[srcSlot - 1] = displaced;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: bag → paperdoll ───────────────────────────────────
|
||||
// Move items from bag slots onto the paperdoll. Each atomic swap
|
||||
// displaces the current paperdoll occupant into the bag slot the
|
||||
// incoming item vacated. Anything we still wanted on the paperdoll
|
||||
// was already placed by phase 1, so phase 2 displacements can't
|
||||
// strand a future set item in a bag.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
const uint64_t g = guids[i];
|
||||
if (g == GUID_EMPTY || g == GUID_IGNORED)
|
||||
continue;
|
||||
if (virtualPaperdoll[targetSlot - 1] == g)
|
||||
continue; // already at target (probably by phase 1)
|
||||
|
||||
const int loc = sourceLoc[i];
|
||||
if (loc == 0)
|
||||
continue;
|
||||
if (loc & LOC_BANK)
|
||||
continue;
|
||||
if ((loc & LOC_BAGS) == 0)
|
||||
continue; // not from a bag — phase 1 handled, or item missing
|
||||
|
||||
const uint8_t *item = Locations::ResolveItemByGUID(g);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
|
||||
const int srcBag = (loc >> LOC_BAG_SHIFT) & 0xFF;
|
||||
const int srcSlot = loc & LOC_SLOT_MASK;
|
||||
if (Item::Swap::FromBag(item, srcBag, srcSlot, targetSlot)) {
|
||||
virtualPaperdoll[targetSlot - 1] = g;
|
||||
// The displaced item lands in (srcBag, srcSlot). We
|
||||
// don't track bag-side virtual state — by phase 3 we
|
||||
// only care about the paperdoll occupant.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: unequip slots the set wants empty ─────────────────
|
||||
// For each `GUID_EMPTY` slot, send the current paperdoll occupant
|
||||
// (if any) to a pre-snapshotted free bag slot. Runs last so any
|
||||
// displaced items from phases 1/2 that landed in the slot get
|
||||
// unequipped here too.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i) {
|
||||
const int targetSlot = i + 1;
|
||||
if (guids[i] != GUID_EMPTY)
|
||||
continue;
|
||||
const uint64_t occupant = virtualPaperdoll[targetSlot - 1];
|
||||
if (occupant == 0)
|
||||
continue;
|
||||
if (nextFreeSlot >= freeSlotCount)
|
||||
continue;
|
||||
const uint8_t *equipped = Locations::ResolveItemByGUID(occupant);
|
||||
if (equipped == nullptr)
|
||||
continue;
|
||||
const FreeSlot &fs = freeSlots[nextFreeSlot++];
|
||||
if (Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot))
|
||||
virtualPaperdoll[targetSlot - 1] = 0;
|
||||
}
|
||||
|
||||
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
|
||||
if (evt >= 0)
|
||||
Event::Custom::Fire(evt, "%d%d", 1, static_cast<int>(setID));
|
||||
|
||||
@@ -177,4 +177,15 @@ const uint8_t *ResolveItemByGUID(uint64_t guid) {
|
||||
return Item::Location::ResolveByGUID(guid);
|
||||
}
|
||||
|
||||
void SnapshotPaperdoll(uint64_t *out) {
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = 0;
|
||||
auto *invMgr = ResolvePlayerInvMgr();
|
||||
auto *guids = InvMgrGuidArray(invMgr);
|
||||
if (guids == nullptr)
|
||||
return;
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
out[i] = guids[i];
|
||||
}
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -42,4 +42,16 @@ int FindGUID(uint64_t targetGuid);
|
||||
// isn't known to the client. Side-effect free.
|
||||
const uint8_t *ResolveItemByGUID(uint64_t guid);
|
||||
|
||||
// Copies the current paperdoll GUIDs into `out[0..SLOT_COUNT-1]`,
|
||||
// where `out[i]` is the GUID at 1-based equipment slot `i+1`.
|
||||
// Empty / unequipped slots write 0. Reads from the player invMgr's
|
||||
// flat GUID array directly — no Lua stack, no engine state changes.
|
||||
//
|
||||
// Used by `UseEquipmentSet` to seed a "virtual paperdoll" model that
|
||||
// stays current across in-flight swaps. The engine's CGItem state
|
||||
// lags behind packet sends (server has to ACK each swap), so any
|
||||
// subsequent `FindGUID` after the first packet would return stale
|
||||
// pre-swap locations. Tracking moves locally bypasses that lag.
|
||||
void SnapshotPaperdoll(uint64_t *out);
|
||||
|
||||
} // namespace EquipmentSet::Locations
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
// per-frame `OnMouseDown` scripts — these fire even when the click
|
||||
// misses any UI frame.
|
||||
//
|
||||
// `IsMouseButtonDown([button])` — query the current held state of a
|
||||
// specific button by name or 1-based ID, or "any button held" when
|
||||
// called with no args. State is maintained from the same hook stream
|
||||
// as the events (single source of truth — events firing and the
|
||||
// state being readable happen at the same point in the pipeline).
|
||||
//
|
||||
// Same `WH_GETMESSAGE` thread-message hook pattern as
|
||||
// [Input::Modifier](Modifier.cpp): we intercept the WM_*BUTTON*
|
||||
// messages on the engine's main thread *before* dispatch. The hook
|
||||
@@ -28,6 +34,9 @@
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Input::GlobalMouse {
|
||||
@@ -37,6 +46,19 @@ namespace {
|
||||
constexpr const char *kDownEvent = "GLOBAL_MOUSE_DOWN";
|
||||
constexpr const char *kUpEvent = "GLOBAL_MOUSE_UP";
|
||||
|
||||
// Held-state bitmap — bit `i` set means the button with 1-based
|
||||
// `IsMouseButtonDown` ID `i+1` is currently held. Modern API IDs:
|
||||
// 1 = LeftButton, 2 = RightButton, 3 = MiddleButton,
|
||||
// 4 = Button4 (XBUTTON1), 5 = Button5 (XBUTTON2).
|
||||
constexpr int BIT_LEFT_BUTTON = 0;
|
||||
constexpr int BIT_RIGHT_BUTTON = 1;
|
||||
constexpr int BIT_MIDDLE_BUTTON = 2;
|
||||
constexpr int BIT_BUTTON4 = 3;
|
||||
constexpr int BIT_BUTTON5 = 4;
|
||||
constexpr uint32_t MASK_ANY_BUTTON = 0x1F;
|
||||
|
||||
uint32_t g_mouseButtonMask = 0;
|
||||
|
||||
HHOOK g_msgHook = nullptr;
|
||||
|
||||
// Maps WM_*BUTTON{DOWN,UP} → ("LeftButton"|..., isDown). Returns
|
||||
@@ -60,16 +82,60 @@ const char *DecodeButton(UINT msg, WPARAM wParam, bool *outDown) {
|
||||
}
|
||||
}
|
||||
|
||||
// Maps a "LeftButton"/.../"Button5" name to its bit index. -1 for
|
||||
// any other string.
|
||||
int NameToBitIdx(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
if (std::strcmp(name, "LeftButton") == 0) return BIT_LEFT_BUTTON;
|
||||
if (std::strcmp(name, "RightButton") == 0) return BIT_RIGHT_BUTTON;
|
||||
if (std::strcmp(name, "MiddleButton") == 0) return BIT_MIDDLE_BUTTON;
|
||||
if (std::strcmp(name, "Button4") == 0) return BIT_BUTTON4;
|
||||
if (std::strcmp(name, "Button5") == 0) return BIT_BUTTON5;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ProcessMouseMessage(UINT msg, WPARAM wParam) {
|
||||
bool down = false;
|
||||
const char *name = DecodeButton(msg, wParam, &down);
|
||||
if (name == nullptr)
|
||||
return;
|
||||
|
||||
const int bitIdx = NameToBitIdx(name);
|
||||
if (bitIdx >= 0) {
|
||||
const uint32_t bit = 1u << bitIdx;
|
||||
if (down) g_mouseButtonMask |= bit;
|
||||
else g_mouseButtonMask &= ~bit;
|
||||
}
|
||||
|
||||
const int slot = Event::Custom::Lookup(down ? kDownEvent : kUpEvent);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "%s", name);
|
||||
}
|
||||
|
||||
int __fastcall Script_IsMouseButtonDown(void *L) {
|
||||
bool down;
|
||||
if (Game::Lua::GetTop(L) == 0) {
|
||||
// No-arg form: any button held.
|
||||
down = (g_mouseButtonMask & MASK_ANY_BUTTON) != 0;
|
||||
} else {
|
||||
int bitIdx = -1;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int id = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (id >= 1 && id <= 5)
|
||||
bitIdx = id - 1;
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
bitIdx = NameToBitIdx(Game::Lua::ToString(L, 1));
|
||||
}
|
||||
// Unrecognized button (bad id, unknown name) → false, matching
|
||||
// the modern API's "did this specific button happen to be held"
|
||||
// semantic. Bad input doesn't promote to the any-button check.
|
||||
down = (bitIdx >= 0) && (g_mouseButtonMask & (1u << bitIdx)) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, down);
|
||||
return 1;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
||||
// Only process when the message is being removed from the queue
|
||||
// (`PM_REMOVE` / `GetMessage`); `PM_NOREMOVE` peeks would deliver
|
||||
@@ -88,7 +154,11 @@ void InstallHook() {
|
||||
GetCurrentThreadId());
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() { InstallHook(); }
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsMouseButtonDown",
|
||||
&Script_IsMouseButtonDown);
|
||||
InstallHook();
|
||||
}
|
||||
|
||||
const Event::Custom::AutoReserve _reserveDown{kDownEvent};
|
||||
const Event::Custom::AutoReserve _reserveUp{kUpEvent};
|
||||
|
||||
@@ -105,6 +105,47 @@ bool FitsSlot(int slot1Based, uint32_t invType) {
|
||||
return (kSlotMaskByInvType[invType] & (1u << (slot1Based - 1))) != 0;
|
||||
}
|
||||
|
||||
// ── Proficiency gate ──────────────────────────────────────────────
|
||||
// `FitsSlot` only checks invType→slot shape compatibility; without
|
||||
// also gating on the player's proficiencies this would return (e.g.)
|
||||
// mail helms for a warlock since both are `invType=HEAD`. Modern
|
||||
// WoW's equipment-set picker filters by what the player can actually
|
||||
// equip; we mirror that.
|
||||
//
|
||||
// Source of truth: the engine's per-itemClass proficiency bitmap at
|
||||
// `[VAR_PROFICIENCY_TABLE]` (17 u32 entries indexed by `m_class`).
|
||||
// SMSG_SET_PROFICIENCY's handler at `0x005E7B70` writes the bitmask
|
||||
// for each class verbatim from the wire. Bit N of `table[class]` =
|
||||
// "player can equip subclass N of this class". Same data the engine
|
||||
// itself reads for the tooltip "Mail/Leather/Plate" red-text and the
|
||||
// cursor-drop-on-paperdoll-slot check — covers both armor and weapon
|
||||
// proficiencies in one lookup.
|
||||
//
|
||||
// 4.3.4's `Script_GetInventoryItemsForSlot` does the equivalent check
|
||||
// via `FUN_00553AD0(itemClass) & (1 << subclass)`, reading from
|
||||
// `DAT_00DD4D38` (its own copy of the same packet-fed table).
|
||||
|
||||
bool PlayerHasProficiency(uint32_t itemClass, uint32_t subclass) {
|
||||
if (itemClass > 16)
|
||||
return true; // table only covers 0..16; be permissive past that
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PROFICIENCY_TABLE) + itemClass * 4);
|
||||
if (mask == 0)
|
||||
return true; // table not populated yet (pre-SMSG_SET_PROFICIENCY)
|
||||
// — fail open so empty-mask doesn't blank the list
|
||||
return (mask & (1u << subclass)) != 0;
|
||||
}
|
||||
|
||||
bool PlayerCanEquip(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return true; // missing record — be permissive, don't hide the item
|
||||
const uint32_t classCode = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subclass = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
return PlayerHasProficiency(classCode, subclass);
|
||||
}
|
||||
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
@@ -112,11 +153,14 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
uint32_t ReadInventoryType(const uint8_t *cgItem) {
|
||||
const uint8_t *ItemRecordForCGItem(const uint8_t *cgItem) {
|
||||
const int itemID = Item::ID::FromCGItem(cgItem);
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
return nullptr;
|
||||
return PeekItemRecord(static_cast<uint32_t>(itemID));
|
||||
}
|
||||
|
||||
uint32_t ReadInventoryType(const uint8_t *record) {
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
@@ -132,7 +176,10 @@ void CollectIfFits(int slot, const uint8_t *cgItem, int location,
|
||||
std::vector<Hit> &out) {
|
||||
if (cgItem == nullptr)
|
||||
return;
|
||||
if (!FitsSlot(slot, ReadInventoryType(cgItem)))
|
||||
const uint8_t *record = ItemRecordForCGItem(cgItem);
|
||||
if (!FitsSlot(slot, ReadInventoryType(record)))
|
||||
return;
|
||||
if (!PlayerCanEquip(record))
|
||||
return;
|
||||
out.push_back({location, cgItem});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user