Compare commits

...
5 Commits
Author SHA1 Message Date
Brues 54e503d1dd InCombatLockdown just returns false now 2026-05-25 14:59:00 -05:00
Brues 0d5c392c20 C_Map.GetBestMapForUnit 2026-05-25 00:20:53 -05:00
Brues da183c3f9a Update API.md 2026-05-24 19:38:27 -05:00
Brues 4a78208c6a corrected GetInstanceInfo shape. added mapid 2026-05-24 19:37:52 -05:00
Brues f61c014973 GetInstanceInfo 2026-05-24 19:33:54 -05:00
6 changed files with 482 additions and 35 deletions
+2
View File
@@ -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
View File
@@ -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
+59
View File
@@ -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,
+128
View File
@@ -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
+185
View File
@@ -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
View File
@@ -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;
}