mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 06:38:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e503d1dd | ||
|
|
0d5c392c20 | ||
|
|
da183c3f9a | ||
|
|
4a78208c6a | ||
|
|
f61c014973 |
@@ -34,8 +34,10 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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: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` |
|
||||
| [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.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `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) | `GetMacroSpell` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
|
||||
+101
-6
@@ -136,6 +136,9 @@ build instructions.
|
||||
- [`IsLeftAltKeyDown()` / `IsRightAltKeyDown()`](#isleftaltkeydown--isrightaltkeydown)
|
||||
- [`IsModifierKeyDown()`](#ismodifierkeydown)
|
||||
|
||||
- [Instance](#instance)
|
||||
- [`GetInstanceInfo()`](#getinstanceinfo)
|
||||
|
||||
- [Item](#item)
|
||||
- [`C_Item.IsBound(itemLocation)`](#c_itemisbounditemlocation)
|
||||
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
|
||||
@@ -179,6 +182,9 @@ build instructions.
|
||||
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
|
||||
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
|
||||
|
||||
- [Map](#map)
|
||||
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
|
||||
|
||||
- [MerchantFrame](#merchantframe)
|
||||
- [`C_MerchantFrame.GetItemInfo(slot)`](#c_merchantframegetiteminfoslot)
|
||||
- [`C_MerchantFrame.GetBuybackItemID(slot)`](#c_merchantframegetbuybackitemidslot)
|
||||
@@ -593,15 +599,22 @@ skipped.
|
||||
|
||||
### `InCombatLockdown()`
|
||||
|
||||
Returns `true` if the local player is currently in combat, `false`
|
||||
otherwise. Modern WoW gates secure-frame UI manipulation on this; 1.12
|
||||
has no secure-frame system, so the function reduces to a plain
|
||||
"is the player in combat" check. Useful for addons backporting modern
|
||||
code that does e.g. `if not InCombatLockdown() then SetBindingClick(...) end`.
|
||||
Always returns `false`. Combat lockdown gates secure-frame UI
|
||||
manipulation in modern WoW, and the secure-frame system didn't exist
|
||||
in 1.12 — there's nothing in vanilla to lock down. This function is
|
||||
provided as a no-op stub purely so addons backported from later
|
||||
expansions can call it without erroring on a missing global.
|
||||
|
||||
For "is the player actually in combat?", use vanilla's own
|
||||
`UnitAffectingCombat("player")`, which the stock 1.12 engine ships.
|
||||
|
||||
```lua
|
||||
if not InCombatLockdown() then
|
||||
-- safe to make UI changes
|
||||
-- always true in 1.12 — falls through unconditionally
|
||||
end
|
||||
|
||||
if UnitAffectingCombat("player") then
|
||||
-- this is the real check
|
||||
end
|
||||
```
|
||||
|
||||
@@ -2774,6 +2787,52 @@ otherwise. Equivalent to
|
||||
`IsShiftKeyDown() or IsControlKeyDown() or IsAltKeyDown()` but in one
|
||||
call.
|
||||
|
||||
## Instance
|
||||
|
||||
### `GetInstanceInfo()`
|
||||
|
||||
Returns the same 9-value tuple modern WoW does (TBC and later), with
|
||||
vanilla-degenerate values for the fields the 1.12 client doesn't
|
||||
actually track:
|
||||
|
||||
```
|
||||
name, instanceType, difficultyID, difficultyName, maxPlayers,
|
||||
dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
|
||||
```
|
||||
|
||||
- `name` — localized instance/zone name from `Map.dbc`.
|
||||
- `instanceType` — `"none"` (open world), `"party"` (5-man dungeon),
|
||||
`"raid"`, `"pvp"` (battleground), or `"arena"` (unused in vanilla).
|
||||
- `difficultyID` — always `1`. No heroic mode pre-TBC.
|
||||
- `difficultyName` — always `"Normal"`.
|
||||
- `maxPlayers` — type-default cap: `5` for dungeons, `40` for raids,
|
||||
`40` for battlegrounds, `0` for open world. **See caveat below.**
|
||||
- `dynamicDifficulty` — always `0` (dynamic difficulty was a Cataclysm
|
||||
addition).
|
||||
- `isDynamic` — always `false`.
|
||||
- `instanceID` — current map ID. Modern API calls this "instanceID" but
|
||||
it's really the `Map.dbc` row ID — both vanilla and modern WoW put
|
||||
the same value here.
|
||||
- `instanceGroupSize` — mirrors `maxPlayers` (no per-group-config
|
||||
variants in vanilla).
|
||||
|
||||
```lua
|
||||
/dump GetInstanceInfo()
|
||||
-- In Stormwind: "Kalimdor", "none", 1, "Normal", 0, 0, false, 1, 0
|
||||
-- In Deadmines: "Deadmines", "party",1, "Normal", 5, 0, false, 36, 5
|
||||
-- In Molten Core: "Molten Core","raid", 1, "Normal", 40, 0, false, 409, 40
|
||||
```
|
||||
|
||||
**Caveat on `maxPlayers`.** Vanilla genuinely has no per-instance cap
|
||||
data client-side — `MapDifficulty.dbc` was a TBC addition. The server
|
||||
enforces caps via `SMSG_TRANSFER_ABORTED` when entry is denied, but
|
||||
that information never reaches the client otherwise. So we return the
|
||||
type's canonical max. **Zul'Gurub and AQ20 return `40` instead of
|
||||
their true `20`; non-AV battlegrounds (WSG `10`, AB `15`) return `40`
|
||||
instead of their true cap; any custom raid on a private server
|
||||
(e.g. Turtle WoW) returns `40` regardless of its real cap.** Addons
|
||||
that need exact caps must supply their own per-mapID table.
|
||||
|
||||
## Item
|
||||
|
||||
> ### `itemLocation` argument shapes
|
||||
@@ -4027,6 +4086,42 @@ section tags them with the same spellID a `/cast` line would, so
|
||||
> opening them in the Macro UI and clicking Okay re-runs the parser
|
||||
> and the new behavior takes effect.
|
||||
|
||||
## Map
|
||||
|
||||
### `C_Map.GetBestMapForUnit(unitToken)`
|
||||
|
||||
Returns the `AreaTable.dbc` area ID the given unit is currently in,
|
||||
or `nil` if the unit isn't trackable. Vanilla 1.12 has no UiMap.db2
|
||||
concept, so the closest equivalent to modern WoW's "best map" is the
|
||||
zone-level AreaTable ID — exactly what the engine itself tracks for
|
||||
the local player (`GetRealZoneText` reads it) and what gets broadcast
|
||||
for party/raid members via `SMSG_PARTY_MEMBER_STATS_FULL`.
|
||||
|
||||
Coverage:
|
||||
|
||||
- `"player"` — always works.
|
||||
- `"party1".."party4"` — works even for members in other zones / on
|
||||
other continents (slot-indexed GUID table stays populated regardless
|
||||
of local CGUnit availability).
|
||||
- `"raid1".."raid40"` — same; covers the full raid roster.
|
||||
- `"target"`, `"mouseover"`, `"focus"` and any other token that
|
||||
resolves to the player or a group member — works.
|
||||
- NPCs, other players not in your group — `nil`.
|
||||
|
||||
```lua
|
||||
/dump C_Map.GetBestMapForUnit("player")
|
||||
-- 1519 (Stormwind City)
|
||||
/dump C_Map.GetBestMapForUnit("party3")
|
||||
-- 876 (party member on GM Island)
|
||||
/dump C_Map.GetBestMapForUnit("target")
|
||||
-- nil (target is an NPC)
|
||||
```
|
||||
|
||||
**The returned ID is a vanilla `AreaTable.dbc` area ID, not a modern
|
||||
UI map ID.** Stormwind City in this backport is `1519`, not retail's
|
||||
`84`. Addons that hardcode modern UI map IDs need a translation table
|
||||
(or, simpler: compare against IDs this same function produces).
|
||||
|
||||
## MerchantFrame
|
||||
|
||||
Backports the six `C_MerchantFrame.*` calls retail addons use when
|
||||
|
||||
@@ -1994,6 +1994,65 @@ enum Offsets {
|
||||
OFF_SPELLDISPEL_HAS_NAME = 0x28,
|
||||
OFF_SPELLDISPEL_NAME = 0x2C,
|
||||
|
||||
// Map.dbc — instance/zone metadata. Standard 5-DWORD class instance
|
||||
// shape; records and count are catalogued in `docs/DBCs.md`. Record
|
||||
// schema (vanilla, derived from `FUN_00495c90` IsInInstance helper
|
||||
// and `FUN_0049e1c0` raid-instance warning handler):
|
||||
// +0x08 uint32 instance type (0=world, 1=dungeon, 2=raid, 3=BG)
|
||||
// +0x10..+0x30 9 locale-string ptrs (localized display name)
|
||||
// No max-players column in vanilla — that data was added in TBC via
|
||||
// MapDifficulty.dbc.
|
||||
VAR_MAP_RECORDS = 0x00C0DAA8,
|
||||
VAR_MAP_COUNT = 0x00C0DAAC,
|
||||
OFF_MAP_INSTANCE_TYPE = 0x08,
|
||||
OFF_MAP_NAME = 0x10,
|
||||
|
||||
// Currently-loaded map ID (the Map.dbc ID for the zone/instance the
|
||||
// player is in). Written by the map-transition handler
|
||||
// `FUN_00495d10`; read by `FUN_00495c90` (the helper backing
|
||||
// `Script_IsInInstance`).
|
||||
VAR_CURRENT_MAP_ID = 0x00B4E378,
|
||||
|
||||
// Local player's current AreaTable.dbc area ID (u32 storage; only
|
||||
// the low 16 bits are used — the value is broadcast over the wire
|
||||
// as a u16 in `SMSG_PARTY_MEMBER_STATS`). Written by the zone-change
|
||||
// handler `FUN_00494780(areaID, parentAreaID, zoneName, subzone,
|
||||
// realZone, eventID)`; read by `Script_GetRealZoneText` and the
|
||||
// outbound party-stats builder `FUN_005f0880`.
|
||||
VAR_PLAYER_AREA_ID = 0x00B4E314,
|
||||
|
||||
// Unified party/raid member stats lookup —
|
||||
// `__fastcall(GUID *guidPair) -> uint8_t *`. Returns a pointer to
|
||||
// the member's stats data on hit, NULL on miss. Tail-jumps from
|
||||
// party storage (`FUN_004e8820`, 4 slots at `0x00BC70B0`) to raid
|
||||
// storage (`FUN_004bb0f0`, 40-slot pointer array at `0x00B712A8`).
|
||||
// Both sub-calls return pointers where the field at `+0x14` is the
|
||||
// u16 areaID (AreaTable.dbc ID) — for party that's at struct +0x14
|
||||
// directly, for raid it's at `*raidArr[i] + 0x10 + 0x14`. Caller
|
||||
// doesn't need to distinguish the two cases.
|
||||
//
|
||||
// Within the returned stats block: `+0x08` status flags, `+0x09`
|
||||
// powerType, `+0x0A..+0x10` HP/maxHP/power/maxPower, `+0x12` level,
|
||||
// `+0x14` u16 areaID, `+0x16/+0x18` position. Verified from the
|
||||
// outbound stats builder `FUN_005f0880` (mirrors inbound packet
|
||||
// shape from SMSG_PARTY_MEMBER_STATS_FULL).
|
||||
FUN_GROUP_MEMBER_STATS_LOOKUP = 0x00496400,
|
||||
OFF_GROUP_MEMBER_AREA_ID = 0x14,
|
||||
|
||||
// Per-slot GUID tables — used to map party/raid token strings
|
||||
// ("party1", "raid17") to GUIDs without going through
|
||||
// `FUN_RESOLVE_UNIT_TOKEN`, which only resolves to a local CGUnit
|
||||
// and returns NULL for group members in other zones.
|
||||
//
|
||||
// Party: 4 entries × 8 bytes (lo + hi). `guids[i]` corresponds to
|
||||
// "partyN" where N = i+1.
|
||||
//
|
||||
// Raid: 40 entries × 4 bytes — each is a `RaidMember *`. GUID is
|
||||
// at `*member + 0` (lo) and `*member + 4` (hi). Verified from
|
||||
// `FUN_004bb0f0`. Slot may be NULL (empty / offline raid member).
|
||||
VAR_PARTY_MEMBER_GUIDS = 0x00BC6F48,
|
||||
VAR_RAID_MEMBER_PTRS = 0x00B712A8,
|
||||
|
||||
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
|
||||
|
||||
LUA_IS_NUMBER = 0x6F34D0,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Instance::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Map.dbc instance-type enum, indexable from 0..4. Vanilla never uses
|
||||
// slot 4 (arena), but the modern API string set includes it.
|
||||
const char *const kTypeNames[] = {
|
||||
"none", // 0 — open world
|
||||
"party", // 1 — 5-man dungeon
|
||||
"raid", // 2 — raid
|
||||
"pvp", // 3 — battleground
|
||||
"arena", // 4 — unused in vanilla
|
||||
};
|
||||
|
||||
// Per-type player cap. Vanilla has no per-map cap stored client-side
|
||||
// (MapDifficulty.dbc was a TBC addition), so we return the type's
|
||||
// canonical max. ZG / AQ20 / WSG / AB return 40 instead of their true
|
||||
// caps; same answer for any custom raid on private servers. Addons
|
||||
// that need exact caps must supply their own table.
|
||||
int CapForType(uint32_t type) {
|
||||
switch (type) {
|
||||
case 1: return 5;
|
||||
case 2: return 40;
|
||||
case 3: return 40;
|
||||
case 4: return 5;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t *MapRecord(uint32_t mapID) {
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MAP_COUNT));
|
||||
if (static_cast<int>(mapID) < 0 || static_cast<int>(mapID) > count)
|
||||
return nullptr;
|
||||
const uint8_t *const *records =
|
||||
*reinterpret_cast<const uint8_t *const *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MAP_RECORDS));
|
||||
if (records == nullptr)
|
||||
return nullptr;
|
||||
return records[mapID];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `GetInstanceInfo()` — returns the modern 9-tuple shape, with vanilla
|
||||
// degeneracies for fields the 1.12 client doesn't track:
|
||||
// name, type, difficultyID, difficultyName, maxPlayers,
|
||||
// dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
|
||||
//
|
||||
// - `name` / `type` come from Map.dbc.
|
||||
// - `difficultyID` / `difficultyName` are always `1` / `"Normal"` (no
|
||||
// heroic mode pre-TBC; MapDifficulty.dbc doesn't exist).
|
||||
// - `maxPlayers` is the type's canonical cap (see `CapForType`).
|
||||
// - `dynamicDifficulty` is always `0` and `isDynamic` is always `false`
|
||||
// (dynamic difficulty was a Cataclysm-era addition).
|
||||
// - `instanceID` is the current Map.dbc ID. (Modern docs call this
|
||||
// "instanceID" but it's really the mapID — both vanilla and modern
|
||||
// put the same value here.)
|
||||
// - `instanceGroupSize` mirrors `maxPlayers` (no per-group-config
|
||||
// variants in vanilla).
|
||||
static int __fastcall Script_GetInstanceInfo(void *L) {
|
||||
const uint32_t mapID = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
|
||||
const uint8_t *rec = MapRecord(mapID);
|
||||
if (rec == nullptr) {
|
||||
Game::Lua::PushString(L, "");
|
||||
Game::Lua::PushString(L, "none");
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
Game::Lua::PushString(L, "Normal");
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mapID));
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 9;
|
||||
}
|
||||
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_MAP_INSTANCE_TYPE);
|
||||
const uint32_t locale = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_MAP_NAME + locale * 4);
|
||||
|
||||
const char *typeName =
|
||||
(type < sizeof(kTypeNames) / sizeof(kTypeNames[0])) ? kTypeNames[type]
|
||||
: "none";
|
||||
const int cap = CapForType(type);
|
||||
|
||||
Game::Lua::PushString(L, name != nullptr ? name : "");
|
||||
Game::Lua::PushString(L, typeName);
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
Game::Lua::PushString(L, "Normal");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(cap));
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(mapID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(cap));
|
||||
return 9;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetInstanceInfo",
|
||||
&Script_GetInstanceInfo);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Instance::Info
|
||||
@@ -0,0 +1,185 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Map::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
using GroupMemberStatsLookup_t = const uint8_t *(__fastcall *)(const uint64_t *guid);
|
||||
|
||||
// Parse a positive 1-based decimal integer from `s`. Returns -1 if `s`
|
||||
// is empty, has non-digit chars, or doesn't fit `[1, max]`. Used to
|
||||
// turn the trailing digits of "party3" / "raid17" into a 0-based slot.
|
||||
int ParseSlot(const char *s, int max) {
|
||||
if (s == nullptr || *s == '\0')
|
||||
return -1;
|
||||
int value = 0;
|
||||
for (const char *p = s; *p != '\0'; ++p) {
|
||||
if (*p < '0' || *p > '9')
|
||||
return -1;
|
||||
value = value * 10 + (*p - '0');
|
||||
if (value > max)
|
||||
return -1;
|
||||
}
|
||||
return value >= 1 ? value - 1 : -1;
|
||||
}
|
||||
|
||||
// Look up the GUID stored in the party member slot directly. Slots are
|
||||
// 0..3 in the parallel GUID table at `VAR_PARTY_MEMBER_GUIDS` (8-byte
|
||||
// stride). Returns 0 if the slot is empty (no party member there).
|
||||
uint64_t PartySlotGUID(int slot) {
|
||||
if (slot < 0 || slot >= 4)
|
||||
return 0;
|
||||
auto *guids = reinterpret_cast<const uint64_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PARTY_MEMBER_GUIDS));
|
||||
return guids[slot];
|
||||
}
|
||||
|
||||
// Raid slot storage at `0x00B712A8` is an array of 40 `RaidMember *`
|
||||
// pointers; the GUID sits at `*member + 0` (lo) and `*member + 4`
|
||||
// (hi). Verified from `FUN_004bb0f0`'s disassembly:
|
||||
// MOV EAX, [ECX*4 + 0xb712a8] ; entry = arr[i]
|
||||
// MOV EDI, [EAX] ; guid_lo
|
||||
// MOV EAX, [EAX + 4] ; guid_hi
|
||||
uint64_t RaidSlotGUID(int slot) {
|
||||
if (slot < 0 || slot >= 40)
|
||||
return 0;
|
||||
auto *const *arr = reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
|
||||
const uint8_t *entry = arr[slot];
|
||||
if (entry == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(entry);
|
||||
}
|
||||
|
||||
uint16_t LocalPlayerArea() {
|
||||
return static_cast<uint16_t>(*reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_AREA_ID)));
|
||||
}
|
||||
|
||||
uint64_t LocalPlayerGUID() {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
}
|
||||
|
||||
|
||||
// Pushes `area` as a number, or nil if `area == 0`. Vanilla zone 0
|
||||
// shows up before the engine has initialized the player's location;
|
||||
// we treat it as "unknown" rather than returning a spurious zero.
|
||||
void PushAreaOrNil(void *L, uint16_t area) {
|
||||
if (area == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
} else {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(area));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// `C_Map.GetBestMapForUnit(unitToken)` — returns the AreaTable.dbc area
|
||||
// ID for the given unit. Vanilla 1.12 has no UiMap.db2 concept; the
|
||||
// closest equivalent is the zone-level AreaTable ID that the engine
|
||||
// itself tracks for the local player and broadcasts for party/raid
|
||||
// members via SMSG_PARTY_MEMBER_STATS_FULL.
|
||||
//
|
||||
// Coverage:
|
||||
// - `"player"` — direct read of `VAR_PLAYER_AREA_ID`.
|
||||
// - `"target"`, `"mouseover"`, `"focus"`, etc. that resolve to the
|
||||
// local player — same.
|
||||
// - `"party1".."party4"`, `"raid1".."raid40"`, and tokens resolving to
|
||||
// any party/raid member's GUID — read via the unified
|
||||
// `FUN_GROUP_MEMBER_STATS_LOOKUP` engine helper.
|
||||
// - Anything else (NPCs, units outside the player's group) — nil.
|
||||
//
|
||||
// **The returned ID is a vanilla AreaTable.dbc ID, not a modern UI map
|
||||
// ID.** Addons that hardcode modern IDs (e.g. `if uiMapID == 84 then`
|
||||
// for retail Stormwind) will not match. Use IDs produced by this same
|
||||
// function or by `GetCurrentMapAreaID`-style helpers from this backport.
|
||||
static int __fastcall Script_GetBestMapForUnit(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fast path: "player" doesn't need token resolution.
|
||||
if (std::strcmp(token, "player") == 0) {
|
||||
PushAreaOrNil(L, LocalPlayerArea());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// "partyN" / "raidN" — parse directly so we work for members in
|
||||
// other zones (no local CGUnit, so the engine's token resolver
|
||||
// would return NULL). Slot-indexed GUID tables are populated
|
||||
// continuously by party/raid sync regardless of sync range.
|
||||
uint64_t guid = 0;
|
||||
if (std::strncmp(token, "party", 5) == 0) {
|
||||
guid = PartySlotGUID(ParseSlot(token + 5, 4));
|
||||
} else if (std::strncmp(token, "raid", 4) == 0) {
|
||||
guid = RaidSlotGUID(ParseSlot(token + 4, 40));
|
||||
} else {
|
||||
// Use TokenToGUID directly so we get the GUID for tokens like
|
||||
// "target" / "mouseover" even when the unit has no local CGUnit
|
||||
// (e.g., name-based /target of a party member in another zone).
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
|
||||
guid = fn(token);
|
||||
}
|
||||
if (guid == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Token resolved to the local player (e.g. "target" on yourself).
|
||||
if (guid == LocalPlayerGUID()) {
|
||||
PushAreaOrNil(L, LocalPlayerArea());
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto lookup = reinterpret_cast<GroupMemberStatsLookup_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP));
|
||||
const uint8_t *stats = lookup(&guid);
|
||||
if (stats == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint16_t area = *reinterpret_cast<const uint16_t *>(
|
||||
stats + Offsets::OFF_GROUP_MEMBER_AREA_ID);
|
||||
PushAreaOrNil(L, area);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Map", "GetBestMapForUnit",
|
||||
&Script_GetBestMapForUnit);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Map::Info
|
||||
+7
-29
@@ -12,39 +12,17 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Combat {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `InCombatLockdown()` — returns whether the local player is currently
|
||||
// in combat. Modern WoW gates secure-frame UI manipulation on this; 1.12
|
||||
// has no secure-frame system, so the function reduces to "is the player
|
||||
// in combat" — `UnitAffectingCombat("player")` would compute the same
|
||||
// answer, just via the slower string→token→unit→fields path. We read
|
||||
// the flag directly off the player CGUnit's m_objectFields.
|
||||
//
|
||||
// The bit (`UNIT_FLAG_IN_COMBAT = 0x00080000`) is the same one
|
||||
// `Script_UnitAffectingCombat` at `0x00517E4A`-`0x517E5C` tests.
|
||||
// `InCombatLockdown()` — always returns `false` in 1.12.1. The
|
||||
// secure-frame system that combat lockdown gates didn't exist in
|
||||
// vanilla; the function is provided purely so that addons backported
|
||||
// from later expansions can call it without erroring. Addons that
|
||||
// actually want "is the player in combat?" should use
|
||||
// `UnitAffectingCombat("player")`, which the stock 1.12 engine ships.
|
||||
static int __fastcall Script_InCombatLockdown(void *L) {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (fields == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_FLAGS);
|
||||
Game::Lua::PushBoolean(L, (flags & Offsets::UNIT_FLAG_IN_COMBAT) != 0);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user