Compare commits

...
24 Commits
Author SHA1 Message Date
Brues d09137cae0 player: add C_PlayerInfo Get{Name,Class,Race,Sex} / IsConnected(playerLocation)
The PlayerLocation-taking C_PlayerInfo accessors. Resolve the location to a
unit token - unit locations use their token, GUID locations via
Unit::Identity::TokenFromGUID (while the GUID has a live token) - then:

- GetName / GetSex / IsConnected tail-call the engine's Script_UnitName /
  Script_UnitSex / Script_UnitIsConnected (the GetFactionInfoByID
  forward-the-return pattern), reusing all vanilla unit logic. GetSex must go
  through the engine because the raw UNIT_FIELD_BYTES_0 gender byte is 0/1
  while the API reports 2/3. IsConnected defaults to the local player when
  called with no location.
- GetRace / GetClass read UNIT_FIELD_BYTES_0 for the numeric ids vanilla's
  UnitRace / UnitClass don't return (login-time mirror globals for "player",
  else the descriptor bytes); GetClass resolves className / classFile from
  ChrClasses.dbc like C_CreatureInfo.GetClassInfo. classID/raceID round-trip
  with C_CreatureInfo.

Only the unit and guid location kinds resolve on 1.12; the chat/who/BG/
community/voice kinds return nil. New file - player/Info.cpp is the separate
NameCache/GetPlayerInfoByGUID module. Adds the three Script_Unit* offsets
from raw_globals.txt.
2026-07-17 01:20:01 -05:00
Brues 5721fb65a7 addon: add PlayerLocation / PlayerLocationMixin
Backport the modern PlayerLocation parameter object (Blizzard_ObjectAPI) - a
lightweight descriptor wrapping one way to identify a player (GUID, unit
token, chat line, /who index, battlefield-score index, ...). Pure table
bookkeeping like the existing ItemLocation mixin, so ported addon code can
construct and inspect it unchanged.

The GUID / unit / chatLine / whoIndex / battlefieldScore kinds map to real
1.12 concepts; community / voice / Battle.net are kept as harmless data
holders so the mixin matches retail exactly. The only two methods that reach
into systems 1.12 lacks - IsValid (Club) and IsBattleNetGUID (account info) -
are guarded so they degrade gracefully instead of erroring on the missing
namespaces.
2026-07-17 01:08:43 -05:00
Brues 3afd403eae container: add C_Container.HasContainerItem
Classic Era addition (present in the 1.15.x client, absent in 5.4.8) — a
plain occupancy check for a bag slot. Occupancy sibling of
GetContainerItemID: resolves (bagIndex, slotIndex) the same way and returns
whether the CGItem is non-null. Presence only, so it reports true for a
just-looted item even before its static data caches. Returns false for
empty slots and out-of-range indices.
2026-07-17 00:37:23 -05:00
Brues e1030213d3 container: add C_Container.CalculateTotalNumberOfFreeBagSlots
Sum of free slots across the player's general-purpose bags (backpack +
equipped bags 0..4 with BagFamily 0), matching FrameXML's implementation.
Reuses the module's existing ResolveBagInfo (slot count + family) and
CountFreeSlotsInBag, so it's just their sum with the family == 0 filter.

Same sparse-BagFamily caveat as GetContainerNumFreeSlots: vanilla server
data leaves most bags' family field at 0, so specialty bags retail would
exclude get counted here - consistent with the per-bag values it sums.
2026-07-16 23:30:02 -05:00
Brues c1cf437bf8 item: route all local player resolvers through Unit::Identity
Nine modules each rolled their own ResolvePlayer / PlayerInvMgr helper on the
throwing FUN_RESOLVE_UNIT_TOKEN("player"). Delete them all and route through
the canonical non-throwing Unit::Identity::PlayerObject(), plus a new
Unit::Identity::PlayerInventoryManager() (PlayerObject + inventory-manager
offset, mirroring the existing PlayerDescriptor accessor).

Verified equivalent by disassembly: FUN_RESOLVE_UNIT_TOKEN is TokenToGUID ->
FUN_OBJECT_RESOLVE_BY_GUID, the same object-manager lookup PlayerObject()
performs, differing only in a type-mask the player object passes either way.
Callers therefore get the identical CGPlayer (and inventory manager) but
non-throwing - null pre-world instead of a raised Lua error. That also makes
item/Location's equipment resolver safe to call outside a Lua callback (e.g.
the C_NewItems WorldTick bag scan), where the token resolver could have
raised.

Migrated: item/Count, item/AverageLevel, item/Swap, item/Location,
player/CanUseItem, spell/Cancel, equipmentset/Data, equipmentset/Locations,
merchant/Frame. debug/Probe keeps its own (deliberately standalone).
2026-07-16 23:17:24 -05:00
Brues 07a91feff0 item: add C_NewItems (new-item glow tracking)
Backport the modern C_NewItems system - IsNewItem / RemoveNewItem /
ClearAll - flagging items freshly added to bags as "new", cleared once
acknowledged. Vanilla has none of the machinery (no DBC, no server data),
so it is pure client-side bookkeeping, done entirely in C++ like retail.

Newness is keyed on each item's instance GUID (read off the CGItem), so a
flag survives the item being moved between slots - impossible for a Lua
addon, which can only track by (bag, slot). Bags 0..4 only; equipment and
bank are out of scope, matching retail.

The feed is event-driven, not polled: it subscribes to Bag::UpdateDelayed
(which co-hooks the engine's three BAG_UPDATE fire sites), so a bag rescan
runs only on frames where a bag actually changed. A one-time baseline taken
~1.5s after login (re-armed on character switch) keeps items owned at login
from being flagged. BAG_NEW_ITEMS_UPDATED fires when the set changes.

Consolidations along the way:
- New Bag::UpdateDelayed::AutoSubscribe exposes the authoritative
  "a bag changed this frame" signal as a C++ subscription.
- New Item::Location::ContainerInventory factors the CGContainer -> its
  inventory (vtable +0x10) dispatch hand-rolled in item/Count,
  item/AverageLevel, and equipmentset/Locations; all now share it.
- New Item::Location::EquippedBagInventory (equipped bag -> inventory) plus
  the FUN_GET_EQUIPPED_BAG_GUID / OFF_CONTAINER_GET_INVENTORY offsets.
- NewItems resolves the player via the shared Unit::Identity::PlayerObject.
2026-07-16 22:54:45 -05:00
Brues 88ea4356a0 creature: add UnitCreatureTypeID + C_CreatureInfo type lookups
Raw CreatureType id (CreatureType.dbc row: 1=Beast … 11=Totem), the
counterpart to the family additions:

  UnitCreatureTypeID(unit)              -> id, nil if none
  C_CreatureInfo.GetCreatureTypeInfo(id) -> { id, name }
  C_CreatureInfo.GetCreatureTypeIDs()    -> ascending id array (1..11)

UnitCreatureTypeID calls the engine's own type resolver at 0x00605570
(Script_UnitCreatureType's inner helper) directly — it handles all three
engine paths (display-override, creature-cache +0x18, player-race
ChrRaces+0x24), so it matches UnitCreatureType for every unit kind
(players resolve to Humanoid). The C_ lookups read CreatureType.dbc
(records 0x00C0DE2C, count 0x00C0DE30, name +0x04).

Also documented that 0x00605570 is really the creature-type resolver
(new canonical alias FUN_UNIT_CREATURE_TYPE); the existing
FUN_UNIT_PET_MINION_CLASS name is a historical label — Pet.cpp's
"!= 1" test works because it is effectively "is this a Beast (type 1)":
hunter pets are Beasts, warlock minions are Demons (3). Verified
in-game: target -> 7 Humanoid, IDs[8] -> 8 Critter, round-trips.
2026-07-16 21:20:47 -05:00
Brues 50909ce440 creature: add C_CreatureInfo.GetFactionInfo(raceID)
The by-raceID form of vanilla's UnitFactionGroup, returning
{ name (localized), groupTag (english "Alliance"/"Horde") } for a race
with no unit required. Mirrors the player branch of Script_UnitFactionGroup
(0x00516630) exactly: ChrRaces[race]+0x08 -> FactionTemplate id ->
FactionTemplate[id]+0x0C group mask -> scan FactionGroup.dbc for the row
whose bit the mask sets and whose localized name is non-empty (the
non-empty test is how the engine skips the always-set "Player" bit and
the nameless "Monster" row to land on Alliance/Horde). nil for
non-numeric / non-positive / unknown race or a race with no named group.

Every field offset ground-truthed against ChrRaces/FactionTemplate/
FactionGroup .dbc files. FactionGroup is stored contiguously (base +
i*0x30, direct field reads), so it's iterated by hand rather than via
DBC::Record. Its records-base/count globals (0x00C0DD5C / 0x00C0DD60)
are taken from the disassembly, which is authoritative over DBCs.md's
standard-shape tabulation for this table (0x00C0DD64 / 0x00C0DD68) --
verified in-game: race 1 -> Alliance, race 2 -> Horde.
2026-07-16 21:11:04 -05:00
Brues d48d003828 creature: add C_CreatureInfo.GetCreatureFamilyInfo / GetCreatureFamilyIDs
Retail backports over CreatureFamily.dbc (records 0x00C0DE7C, count
0x00C0DE80 — the globals Script_UnitCreatureFamily already indexes), on
the existing C_CreatureInfo table:

  GetCreatureFamilyInfo(id) -> { id, name, iconFile }
  GetCreatureFamilyIDs()    -> ascending array of populated family ids

Localized name at record +0x20 (via LocalizedField), icon at +0x44 (via
StringField). GetCreatureFamilyIDs skips the sparse unused rows
(10/13/14/22/...); each element round-trips with GetCreatureFamilyInfo.

Faithful deviation: retail's iconFile is a numeric fileID, but vanilla's
DBC stores the icon as a texture-path string, so iconFile is a string
(usable directly with SetTexture) and "" for families with no icon
(warlock pets). Verified in-game: 27 -> Wind Serpent + icon path, 15 ->
Felhunter + "", IDs array sparse, unused id 10 -> nil.
2026-07-16 21:02:01 -05:00
Brues d51f5f72a9 unit: add UnitCreatureFamilyID(unit)
Extension exposing the numeric CreatureFamily id (CreatureFamily.dbc
row) for a unit, the value addons want for stable checks — stock
UnitCreatureFamily only returns the locale-dependent display name.

It's the same id the engine's Script_UnitCreatureFamily (0x0051A310)
resolves internally: its family helper FUN_006055e0 returns
[unit+0xB30] ? [row+0x1C] : 0, then maps it through CreatureFamily.dbc
for the name. We stop at the id. Resolves the token via the same
FUN_RESOLVE_UNIT_TOKEN, returns nil for family 0 (players, non-beast
NPCs) and unsynced/unresolvable units — matching UnitCreatureFamily.
Shares UnitSubName's +0xB30 creature-cache read path. Verified in-game:
target (cat) -> 2, targetpet -> 2.
2026-07-16 20:52:31 -05:00
Brues 04efd4caf1 input: reconcile modifier state on focus regain
MODIFIER_STATE_CHANGED never fired for a modifier released while WoW was
in the background: WM_KEY* messages are only delivered to the focused
window, so the WH_GETMESSAGE hook never saw the release and the cached
bit stayed stuck down (also leaving IsLeft/RightShiftKeyDown etc. wrong).

Add a WH_CALLWNDPROC thread hook to catch WM_ACTIVATEAPP (sent, not
posted, so invisible to WH_GETMESSAGE). On app activation, resync the
cached 6-bit mask from GetAsyncKeyState (global physical key state,
unaffected by focus), firing MODIFIER_STATE_CHANGED for any bit that
drifted while WoW was away. Both the keyboard path and the resync now
funnel through a shared SetBit helper so the "only transitions fire"
invariant holds regardless of source.

Caveat (documented): the engine's native IsShiftKeyDown/IsControlKeyDown/
IsAltKeyDown bottom out at synchronous GetKeyState and share the blind
spot; they are not reconciled here — only ClassicAPI's L/R functions and
the event reflect the corrected state.
2026-07-16 20:18:43 -05:00
Brues 329d6b75ba creature: add C_CreatureInfo.GetRaceInfo / GetClassInfo
Retail backports over ChrRaces.dbc / ChrClasses.dbc, on the existing
C_CreatureInfo table. By-id lookups (no unit token needed, unlike
UnitRace/UnitClass) returning the localized name plus the non-localized
file token that round-trips with the unit APIs:

  GetRaceInfo(raceID)   -> { raceName, clientFileString, raceID }
  GetClassInfo(classID) -> { className, classFile, classID }

Reuses the already-verified DBC offsets (OFF_CHRRACES_* / OFF_CHRCLASSES_*),
StringField for the token and LocalizedField for the name. nil for
non-numeric / non-positive / unknown ids. Verified in-game: race 4 ->
Night Elf/NightElf, class 1 -> Warrior/WARRIOR, unknown id -> nil.
2026-07-16 18:32:35 -05:00
Brues 6a677ce447 taxi/map: capital city areaIDs + C_Map.GetMapAreaIDs
Taxi areaID resolution now tries the node's LOCATION part first
("Orgrimmar" in "Orgrimmar, Durotar") -- accepted only when it's a
mapped area whose WorldMapArea rect contains the node -- then the zone
suffix, then the geometric resolver. Capitals thus pin on their own city
map (Orgrimmar 1637, Undercity 1497) instead of the surrounding region,
while normal nodes (location is an unmapped subzone) still resolve to the
outdoor zone. Map::Area::PercentInZone is now containment-checked so a
candidate zone is taken only if it actually holds the point.

Add C_Map.GetMapAreaIDs() -> { [mapName] = areaID } from WorldMapArea:
the missing bridge (vanilla has no GetCurrentMapAreaID) for resolving a
browsed city/instance map -- which GetMapZones() doesn't enumerate -- to
its zone id. Keys on the WorldMapArea name (matches GetMapInfo()'s dir
for nearly all maps; a few misspelled Blizzard folders like "Ogrimmar"
differ, documented).

Verified in-game: Orgrimmar/Undercity flight masters pin on their city
maps; GetMapAreaIDs()["Orgrimmar"] = 1637.
2026-07-16 18:06:55 -05:00
Brues f6f88ab4ee taxi/map: accurate zone resolution + reachable flag
Zone assignment (Map::Area::ZonePercent, shared by GetTaxiNodesForMap
and GetAreaTriggerInfo) chose among overlapping WorldMapArea bounding
boxes by smallest rect, mis-assigning border points. Now resolve by the
zone whose DRAWN LANDMASS the point sits on -- WorldMapOverlay hit rects,
the runtime analog of an ADT area-grid lookup -- tie-broken by how deep
inside an overlay the point is (a node deep in its own zone's overlay
beats one clipping a neighbor's edge). Falls back to most-interior
bounding box when on no overlay. Fixes Ratchet, Un'Goro, Nijel's Point.

For taxi specifically, resolve areaID from the node's own name suffix
("..., Western Plaguelands" -> AreaTable, exact then word-prefix) --
authoritative, and right at borders geometry can't separate (Chillwind
Camp -> WPL, not the Alterac box it sits in). New Map::Area::PercentInZone
projects the position into the named zone; geometric resolver remains the
fallback and the area-trigger path.

Add a `reachable` field: a flight path leads to the node (in-degree > 0).
false marks departure-only nodes reachable only by other means -- druid
Teleport (Nighthaven, Moonglade) or a boat dock (Stormwind Harbor) -- so
a consumer can hide them from a "where can I fly" map without a hardcoded
druid guess. BuildTaxiGraph tracks endpoint + destination sets in one pass.

Verified in-game: Nijel's->Desolace(405), Marshal's->Un'Goro(490),
Ratchet->Barrens(17), Chillwind->WPL(28).
2026-07-16 16:44:58 -05:00
Brues 17c928028e taxi: GetTaxiNodesForMap() with no arg returns all continents
Previously no-arg meant the currently-viewed continent; make it return
every flight master on every continent instead -- a one-call flight DB.
A numeric mapID still filters to that continent. Each entry already
carries its own mapID, and the mount + TaxiPath-endpoint filters apply
uniformly. Verified: no-arg 97, GetTaxiNodesForMap(0) 48.
2026-07-16 14:20:41 -05:00
Brues a47f38417f taxi: add C_TaxiMap.GetTaxiNodesForMap / GetAllTaxiNodes
GetTaxiNodesForMap(mapID) reads TaxiNodes.dbc: real flight masters on a
continent, faction-tagged, discovered or not -- the static DB pfQuest's
pfDB.meta.flight wants. Non-flight rows are filtered out: a node must
have a flight mount (horde @+0x38 / alliance @+0x3C -- drops transports/
boats/zeppelins) AND be a TaxiPath.dbc endpoint (drops Northshire Abbey,
spurious duplicate hub rows, standalone markers with no connecting
path), plus a guard for Blizzard's lone "Generic, World target"
placeholder. Returns retail nodeID/name/faction/position (continent
0-1) + ext mapID/x/y/z and derived areaID/mapX/mapY (zone-relative).

GetAllTaxiNodes([uiMapID]) wraps the open flight master's live node
list (VAR_TAXI_NODE_ARRAY stride 0x20: slot+0 = TaxiNodes record ptr,
+0x04/+0x08 = flight-map x/y; count VAR_TAXI_NODE_COUNT; type via
FUN_004dc4e0): slotIndex (TakeTaxiNode), name, nodeID, state
(Enum.FlightPathState). Empty when no taxi map is open.

Registers Enum.FlightPathFaction / Enum.FlightPathState (retail values).
Extracts the zone-percent projection into shared Map::Area::ZonePercent
+ adds ContinentPercent; refactors AreaTriggers onto ZonePercent.
Verified: EK 48 real masters (Northshire/EPL/transports gone), Stormwind
Alliance / Undercity Horde, continent position matches projection; live
24 nodes with Stormwind state=Current.
2026-07-16 14:00:18 -05:00
Brues 1282e3f6db map: add C_MapExplorationInfo explored/unexplored map textures
GetExploredMapTextures(areaID) backports the retail (9.0-era) call:
only overlays the local player has discovered. GetUnexploredMapTextures
is the ClassicAPI-extension complement (the reveal-over-fog pieces); its
inverse, C_Map.GetMapOverlays, still returns all overlays.

Refactor the overlay walk into a shared Map::Overlays::PushZoneOverlays
with a Filter {All, Explored, Unexplored}; GetMapOverlays is now the
All wrapper. "Explored" is computed per-areaID from the player's
explored-areas bitfield ([player+0xE68]+0xE6C, indexed by AreaTable
AreaBit +0x0C, gated by +0x28) -- mirrors the engine's discovered-
overlay rebuild FUN_004a67a0 -- so the areaID arg works for any zone,
not just the current view. Also add a fileDataIDs field (ordered tile
paths, retail's field name) to every overlay entry.

New offsets: OFF_AREATABLE_AREA_BIT, OFF_AREATABLE_EXPLORE_GATE,
OFF_PLAYER_EXPLORED_BITS. Verified: Elwynn explored(10) + unexplored(2)
== all(12).
2026-07-16 02:25:10 -05:00
Brues 4e4fad9196 map: add C_Map.GetAreaInfo / GetAreas
Surface AreaTable.dbc's localized name column. GetAreaInfo(areaID) is
the retail backport -- localized name for one zone/subzone id, nil for
bad input / no row. GetAreas() is an extension: the full { [areaID] =
name } map (empty-name rows omitted, round-trips with GetAreaInfo), the
piece name databases need for name->id / reverse search. Same "surface
the hidden DBC" pattern as GetAreaTriggers / GetMapOverlays.

AreaTable.dbc is a client DBC (always loaded, synchronous, localized),
so no WDB cold-cache concerns. Uses existing VAR_AREATABLE_* /
OFF_AREATABLE_NAMES wiring; no new offsets. Verified: (12) Elwynn
Forest, (9) Northshire Valley, GetAreas()[12] Elwynn Forest.
2026-07-15 23:48:15 -05:00
Brues 67063876da spell: add C_SpellBook.GetSkillLineRank / GetSkillLineName
GetSkillLineRank(skillLineID) returns the local player's curRank,
maxRank, modifier for a SkillLine.dbc id, or nil if unlearned -- keyed
by id directly instead of forcing addons to map id -> localized name and
string-match GetSkillLineInfo's by-name, by-position walk. Resolves the
id to the player's skill slot via FUN_SKILL_LINE_TO_SLOT, then reads the
CGPlayer +0xE68 skill table (the storage Player::CanUseItem's
require-skill gate already uses). GetSkillLineName(skillLineID) is the
id -> localized name half (pure SkillLine.dbc read, learned or not).

Both live in the existing Spell::SkillLine module; no new offsets.
Verified: Enchanting(333) 171/225/15, Tailoring(197) 261/300/0,
name(182)=Herbalism, unlearned line -> nil.
2026-07-15 23:02:42 -05:00
Brues b34368ef14 map: expose overlay hit rect in C_Map.GetMapOverlays
Each overlay now carries hitRectTop/Left/Bottom/Right (WorldMapOverlay.dbc
+0x34..+0x40) -- the tight clickable landmass bounds in world-map canvas
px (~1002x668), the subzone-center source map addons scrape into their
zone tables. Verified: areaID 12 overlay 8 (NorthshireValley, areaID 9)
-> hitRect 164/400/388/617.

Also stop dropping texture-less rows: a row with an areaID + hit rect but
no texture now appears with textureName == "", zero size, and an empty
tiles table (ipairs-safe), instead of being skipped. No-op on stock/Octo
data (every overlay has a texture) but matches the schema and the "every
overlay of a zone" contract.
2026-07-15 22:07:35 -05:00
Brues 4983c9fd83 map: expose overlay areaIDs in C_Map.GetMapOverlays
Each overlay table now carries areaID (primary AreaTable id revealed,
first nonzero) and areaIDs (the full nonzero list -- WorldMapOverlay.dbc
areaID[4]; 52 of 707 overlays span 2-3 subzones). Lets consumers map an
overlay to its subzone(s) / exploration state. Verified: overlay 1 of
areaID 85 -> areaID 157.
2026-07-15 21:30:19 -05:00
Brues 9e5114141e map: add C_Map.GetMapWorldSize; extract shared Map::Area resolver
Backport the retail C_Map.GetMapWorldSize (9.0): return a zone's world
size in yards as width, height, read from the WorldMapArea.dbc loc rect
(width = locLeft - locRight east-west, height = locTop - locBottom
north-south). Takes an AreaTable areaID, or defaults to the world map's
current view like GetMapOverlays. Verified: (85) -> 4518.75, 3012.5.

Extract the two WorldMapArea row resolvers (areaID -> row, current view
-> row) out of map/Overlays.cpp into a shared Map::Area helper; both
GetMapOverlays and GetMapWorldSize key off the same rows, so the two
ways of naming a zone now live in one place.
2026-07-15 21:14:03 -05:00
Brues 6be25d78e0 map: add C_Map.GetAreaTriggerInfo / GetAreaTriggers
Expose AreaTrigger.dbc (517 static trigger volumes: subzone-entry /
exploration / teleport), which the client loads but never surfaces to
Lua -- the reason map addons ship hand-scraped trigger tables. Same
"read the hidden DBC" pattern as C_Map.GetMapOverlays.

Each trigger returns both the authoritative raw geometry (mapID, world
x/y/z, radius, isBox + box length/width/height/yaw) and a derived
consumable form (areaID + mapX/mapY as a 0..100 zone-relative percent)
via the WorldMapArea rect, picking the smallest zone whose rect
contains the point. GetAreaTriggerInfo(id) returns one trigger;
GetAreaTriggers([mapID]) enumerates all, optionally filtered to a map.

Adds the AreaTrigger.dbc record offsets and the WorldMapArea mapID /
loc-rect offsets to Offsets.h.
2026-07-15 20:04:08 -05:00
Brues 06e68d04a8 docs: document C_Item.PickupItem
Add the API.md entry (input shapes, equipment-then-bags search, no-op
conditions, direct-cursor-primitive note) plus its TOC link, and the
README Item-row index entry.
2026-07-15 19:56:25 -05:00
37 changed files with 3996 additions and 233 deletions
+1
View File
@@ -23,6 +23,7 @@ Util\CallbackRegistry.lua
Util\GlobalCallbackRegistry.lua
Util\ColorUtil.lua
Util\ItemLocation.lua
Util\PlayerLocation.lua
Util\EventUtil.lua
Util\ItemUtil.lua
Util\AddOnCompat.lua
@@ -0,0 +1,228 @@
-- Backport of the modern `PlayerLocation` / `PlayerLocationMixin` parameter
-- object (Blizzard_ObjectAPI). Like `ItemLocation`, it's a lightweight
-- descriptor that wraps one of several ways to identify a player — a GUID, a
-- unit token, a chat line, a /who index, a battlefield-scoreboard index, etc.
-- — so retail/Classic addon code can construct and inspect it unchanged.
--
-- It's pure table bookkeeping: every Create/Set/Get/Is is just fields, no
-- engine access. The GUID / unit / chatLine / whoIndex / battlefieldScore
-- kinds all map to things vanilla 1.12 genuinely has. The community, voice,
-- and Battle.net kinds have no vanilla producer, but they're kept as harmless
-- data holders so the mixin matches retail exactly for any code that builds
-- or reads them. Only the two methods that reach into systems 1.12 lacks —
-- `IsValid` (Club) and `IsBattleNetGUID` (account info) — are guarded so they
-- degrade gracefully instead of erroring on the missing `C_Club` /
-- `C_AccountInfo` namespaces.
local CreateFromMixins = CreateFromMixins
PlayerLocation = PlayerLocation or {}
PlayerLocationMixin = PlayerLocationMixin or {}
--[[static]] function PlayerLocation:CreateFromGUID(guid)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetGUID(guid)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromUnit(unit)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetUnit(unit)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromChatLineID(lineID)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetChatLineID(lineID)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromBattlefieldScoreIndex(battlefieldScoreIndex)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetBattlefieldScoreIndex(battlefieldScoreIndex)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromWhoIndex(whoIndex)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetWhoIndex(whoIndex)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromVoiceID(memberID, channelID)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetVoiceID(memberID, channelID)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromBattleNetID(battleNetID)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetBattleNetID(battleNetID)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromCommunityChatData(clubID, streamID, epoch, position)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetCommunityData(clubID, streamID, epoch, position)
return playerLocation
end
--[[static]] function PlayerLocation:CreateFromCommunityInvitation(clubID, guid)
local playerLocation = CreateFromMixins(PlayerLocationMixin)
playerLocation:SetCommunityInvitation(clubID, guid)
return playerLocation
end
--[[public api]]
function PlayerLocationMixin:SetGUID(guid)
self:ClearAndSetField("guid", guid)
end
function PlayerLocationMixin:IsValid()
if self:IsCommunityData() then
-- Vanilla has no Community/Club system; if the namespace is
-- absent a community location can never be resolved.
if C_Club and C_Club.CanResolvePlayerLocationFromClubMessageData then
return C_Club.CanResolvePlayerLocationFromClubMessageData(self.communityClubID, self.communityStreamID, self.communityEpoch, self.communityPosition)
end
return false
end
return true
end
function PlayerLocationMixin:IsGUID()
return self.guid ~= nil
end
function PlayerLocationMixin:IsBattleNetGUID()
-- No Battle.net accounts on 1.12; guarded so it returns false instead
-- of erroring on a missing C_AccountInfo.
if self.guid and C_AccountInfo and C_AccountInfo.IsGUIDBattleNetAccountType then
return C_AccountInfo.IsGUIDBattleNetAccountType(self.guid)
end
return false
end
function PlayerLocationMixin:GetGUID()
return self.guid or self.communityClubInviterGUID
end
function PlayerLocationMixin:SetUnit(unit)
self:ClearAndSetField("unit", unit)
end
function PlayerLocationMixin:IsUnit()
return self.unit ~= nil
end
function PlayerLocationMixin:GetUnit()
return self.unit
end
function PlayerLocationMixin:SetChatLineID(lineID)
self:ClearAndSetField("chatLineID", lineID)
end
function PlayerLocationMixin:IsChatLineID()
return self.chatLineID ~= nil
end
function PlayerLocationMixin:GetChatLineID()
return self.chatLineID
end
function PlayerLocationMixin:SetBattlefieldScoreIndex(index)
self:ClearAndSetField("battlefieldScoreIndex", index)
end
function PlayerLocationMixin:IsBattlefieldScoreIndex()
return self.battlefieldScoreIndex ~= nil
end
function PlayerLocationMixin:GetBattlefieldScoreIndex()
return self.battlefieldScoreIndex
end
function PlayerLocationMixin:SetWhoIndex(whoIndex)
self:ClearAndSetField("whoIndex", whoIndex)
end
function PlayerLocationMixin:IsWhoIndex()
return self.whoIndex ~= nil
end
function PlayerLocationMixin:GetWhoIndex()
return self.whoIndex
end
function PlayerLocationMixin:SetVoiceID(memberID, channelID)
self:Clear()
self.voiceMemberID = memberID
self.voiceChannelID = channelID
end
function PlayerLocationMixin:IsVoiceID()
return self.voiceMemberID ~= nil and self.voiceChannelID ~= nil
end
function PlayerLocationMixin:GetVoiceID()
return self.voiceMemberID, self.voiceChannelID
end
function PlayerLocationMixin:SetBattleNetID(battleNetID)
self:Clear()
self.battleNetID = battleNetID
end
function PlayerLocationMixin:IsBattleNetID()
return self.battleNetID ~= nil
end
function PlayerLocationMixin:GetBattleNetID()
return self.battleNetID
end
function PlayerLocationMixin:SetCommunityData(clubID, streamID, epoch, position)
self:Clear()
self.communityClubID = clubID
self.communityStreamID = streamID
self.communityEpoch = epoch
self.communityPosition = position
end
function PlayerLocationMixin:IsCommunityData()
return self.communityClubID ~= nil and self.communityStreamID ~= nil and self.communityEpoch ~= nil and self.communityPosition ~= nil
end
function PlayerLocationMixin:SetCommunityInvitation(clubID, guid)
self:Clear()
self.communityClubID = clubID
self.communityClubInviterGUID = guid
end
function PlayerLocationMixin:IsCommunityInvitation()
return self.communityClubID ~= nil and self.communityClubInviterGUID ~= nil
end
--[[private api]]
function PlayerLocationMixin:Clear()
self.guid = nil
self.unit = nil
self.chatLineID = nil
self.battlefieldScoreIndex = nil
self.whoIndex = nil
self.voiceMemberID = nil
self.voiceChannelID = nil
self.communityClubID = nil
self.communityStreamID = nil
self.communityEpoch = nil
self.communityPosition = nil
self.communityClubInviterGUID = nil
self.battleNetID = nil
end
function PlayerLocationMixin:ClearAndSetField(fieldName, field)
self:Clear()
self[fieldName] = field
end
+11 -7
View File
@@ -24,9 +24,9 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| [Coroutine](docs/API.md#coroutine) | `coroutine.create`, `coroutine.resume`, `coroutine.yield`, `coroutine.status`, `coroutine.wrap` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
@@ -41,28 +41,31 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `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.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `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.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit`, `C_Map.GetMapOverlays` |
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer` |
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
| [Table](docs/API.md#table) | `table.wipe` |
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
@@ -114,6 +117,7 @@ when launching with `-console`), not as Lua functions. See the
| Event | Payload |
|-------|---------|
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
| `BAG_UPDATE_DELAYED` | *(none)* |
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
| `EQUIPMENT_SWAP_PENDING` | `setID` |
+808
View File
@@ -40,11 +40,13 @@ build instructions.
- [Container](#container)
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
- [`C_Container.HasContainerItem(bagIndex, slotIndex)`](#c_containerhascontaineritembagindex-slotindex)
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
- [`C_Container.GetContainerItemDurability(containerIndex, slotIndex)`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
- [`C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`](#c_containergetcontaineritemrepaircostcontainerindex-slotindex)
- [`C_Container.GetContainerItemCharges(containerIndex, slotIndex)`](#c_containergetcontaineritemchargescontainerindex-slotindex)
- [`C_Container.GetContainerNumFreeSlots(bagID)`](#c_containergetcontainernumfreeslotsbagid)
- [`C_Container.CalculateTotalNumberOfFreeBagSlots()`](#c_containercalculatetotalnumberoffreebagslots)
- [`C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`](#c_containeriscontaineritemopenablecontainerindex-slotindex)
- [`C_Container.PlayerHasHearthstone()`](#c_containerplayerhashearthstone)
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
@@ -63,6 +65,13 @@ build instructions.
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
- [`C_CreatureInfo.GetCreatureInfoByID(creatureID)`](#c_creatureinfogetcreatureinfobyidcreatureid)
- [`C_CreatureInfo.RequestLoadCreatureByID(creatureID)`](#c_creatureinforequestloadcreaturebyidcreatureid)
- [`C_CreatureInfo.GetRaceInfo(raceID)`](#c_creatureinfogetraceinforaceid)
- [`C_CreatureInfo.GetClassInfo(classID)`](#c_creatureinfogetclassinfoclassid)
- [`C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)`](#c_creatureinfogetcreaturefamilyinfocreaturefamilyid)
- [`C_CreatureInfo.GetCreatureFamilyIDs()`](#c_creatureinfogetcreaturefamilyids)
- [`C_CreatureInfo.GetFactionInfo(raceID)`](#c_creatureinfogetfactioninforaceid)
- [`C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)`](#c_creatureinfogetcreaturetypeinfocreaturetypeid)
- [`C_CreatureInfo.GetCreatureTypeIDs()`](#c_creatureinfogetcreaturetypeids)
- [CVar](#cvar)
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
@@ -238,6 +247,7 @@ build instructions.
- [`C_Item.IsLocked(itemLocation)`](#c_itemislockeditemlocation)
- [`C_Item.LockItem(itemLocation)`](#c_itemlockitemitemlocation)
- [`C_Item.LockItemByGUID(itemGUID)`](#c_itemlockitembyguiditemguid)
- [`C_Item.PickupItem(itemInfo)`](#c_itempickupitemiteminfo)
- [`C_Item.RequestLoadItemDataByID(item)` / `C_Item.RequestLoadItemData(itemLocation)`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
- [`C_Item.UnlockAllItems()`](#c_itemunlockallitems)
- [`C_Item.UnlockItem(itemLocation)`](#c_itemunlockitemitemlocation)
@@ -281,8 +291,17 @@ build instructions.
- [`GetInboxItemLink(messageIndex[, attachmentIndex])`](#getinboxitemlinkmessageindex-attachmentindex)
- [Map](#map)
- [`C_Map.GetAreaInfo(areaID)`](#c_mapgetareainfoareaid)
- [`C_Map.GetAreas()`](#c_mapgetareas)
- [`C_Map.GetAreaTriggerInfo(triggerID)` / `C_Map.GetAreaTriggers([mapID])`](#c_mapgetareatriggerinfotriggerid--c_mapgetareatriggersmapid)
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
- [`C_Map.GetMapAreaIDs()`](#c_mapgetmapareaids)
- [`C_Map.GetMapOverlays([areaID])`](#c_mapgetmapoverlaysareaid)
- [`C_Map.GetMapWorldSize([areaID])`](#c_mapgetmapworldsizeareaid)
- [MapExplorationInfo](#mapexplorationinfo)
- [`C_MapExplorationInfo.GetExploredMapTextures([areaID])`](#c_mapexplorationinfogetexploredmaptexturesareaid)
- [`C_MapExplorationInfo.GetUnexploredMapTextures([areaID])`](#c_mapexplorationinfogetunexploredmaptexturesareaid)
- [MerchantFrame](#merchantframe)
- [`C_MerchantFrame.GetItemInfo(slot)`](#c_merchantframegetiteminfoslot)
@@ -309,9 +328,15 @@ build instructions.
- [`C_PlayerCache.SetScanEnabled(enabled)`](#c_playercachesetscanenabledenabled)
- [`C_PlayerCache.IsScanEnabled()`](#c_playercacheisscanenabled)
- [NewItems](#newitems)
- [`C_NewItems.IsNewItem(bagID, slotIndex)`](#c_newitemsisnewitembagid-slotindex)
- [`C_NewItems.RemoveNewItem(bagID, slotIndex)`](#c_newitemsremovenewitembagid-slotindex)
- [`C_NewItems.ClearAll()`](#c_newitemsclearall)
- [PlayerInfo](#playerinfo)
- [`C_PlayerInfo.CanUseItem(itemID)`](#c_playerinfocanuseitemitemid)
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
- [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation)
- [Quest](#quest)
- [`C_QuestLog.GetQuestIDForLogIndex(index)`](#c_questlogGetQuestIDForLogIndexindex)
- [`C_QuestLog.RequestLoadQuestByID(questID)`](#c_questlogrequestloadquestbyidquestid)
@@ -370,6 +395,8 @@ build instructions.
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
- [`C_SpellBook.GetSkillLineName(skillLineID)`](#c_spellbookgetskilllinenameskilllineid)
- [`C_SpellBook.GetSkillLineRank(skillLineID)`](#c_spellbookgetskilllinerankskilllineid)
- [`C_SpellBook.GetSpellSkillLine(spellID)`](#c_spellbookgetspellskilllinespellid)
- [`C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisautoattackspellbookitemslot-booktype)
- [`C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
@@ -394,6 +421,10 @@ build instructions.
- [`GetTalentSpellID(tabIndex, talentIndex, [rank])`](#gettalentspellidtabindex-talentindex-rank)
- [`GetTalentIDByIndex(tabIndex, talentIndex)`](#gettalentidbyindextabindex-talentindex)
- [TaxiMap](#taximap)
- [`C_TaxiMap.GetTaxiNodesForMap([mapID])`](#c_taximapgettaxinodesformapmapid)
- [`C_TaxiMap.GetAllTaxiNodes([uiMapID])`](#c_taximapgetalltaxinodesuimapid)
- [Time](#time)
- [`GetServerTime()`](#getservertime)
- [`C_Timer.After(seconds, callback)`](#c_timeraftersseconds-callback)
@@ -419,6 +450,8 @@ build instructions.
- [`UnitGUID(unit)`](#unitguidunit)
- [`UnitTokenFromGUID(guid)`](#unittokenfromguidguid)
- [`UnitSubName(unit)`](#unitsubnameunit)
- [`UnitCreatureFamilyID(unit)`](#unitcreaturefamilyidunit)
- [`UnitCreatureTypeID(unit)`](#unitcreaturetypeidunit)
- [`GetUnitSpeed(unit)`](#getunitspeedunit)
- [`UnitClassBase(unit)`](#unitclassbaseunit)
- [`UnitRaceBase(unit)`](#unitracebaseunit)
@@ -1030,6 +1063,25 @@ for slot = 1, 16 do
end
```
### `C_Container.HasContainerItem(bagIndex, slotIndex)`
Returns `true` if the given bag slot currently holds an item, `false`
otherwise (empty slot or out-of-range indices). The occupancy sibling of
`GetContainerItemID` — same slot resolution, but a plain presence check.
- `bagIndex = 0` — the player's main backpack; `1..4` — equipped bag slots.
- `slotIndex` — 1-based.
Presence only: it does **not** require the item's static data to be cached,
so it returns `true` for a just-looted item even before `GetContainerItemID`
/ `GetItemInfo` resolve for it.
```lua
if C_Container.HasContainerItem(0, 1) then
-- backpack slot 1 is occupied
end
```
### `GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`
Returns `(startTime, duration, enable)` for the cooldown of the
@@ -1234,6 +1286,31 @@ internally and counts slots that resolve to a null `CGItem *` (i.e.,
empty). Cross-checked in-game against a manual
`C_Container.GetContainerItemID` walk; counts match.
### `C_Container.CalculateTotalNumberOfFreeBagSlots()`
Returns the total number of free slots across the player's
**general-purpose** bags — backpack plus any equipped bags 0..4 whose
`BagFamily` is 0. Specialty bags (quivers, soul bags, herb bags, …) are
excluded, matching FrameXML's implementation:
```lua
local total = 0
for bag = 0, 4 do
local free, family = C_Container.GetContainerNumFreeSlots(bag)
if family == 0 then total = total + free end
end
-- total == C_Container.CalculateTotalNumberOfFreeBagSlots()
```
Takes no arguments.
> **Same sparse-`BagFamily` caveat** as
> [`GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid):
> because vanilla server data leaves most bags' family field at 0,
> specialty bags that retail would exclude are counted here. The result
> stays consistent with the per-bag `GetContainerNumFreeSlots` values it
> sums.
### `C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`
Positional-arg wrapper for [`C_Item.IsItemOpenable`](#c_itemisitemopenableitemlocation--c_itemisitemopenablebyiditem)
@@ -1665,6 +1742,134 @@ caches have their own, so there's no hook collision. Verified in-game: `RequestL
an uncached Onyxia fired the event and populated the cache (type 2
Dragonkin, rank 3 world boss).
### `C_CreatureInfo.GetRaceInfo(raceID)`
Localized race name + non-localized token for a race id, read from
`ChrRaces.dbc` (always loaded, so this works for any id at any time —
no unit token required, unlike `UnitRace`). Returns a `RaceInfo` table,
or `nil` for a non-numeric / non-positive id or one with no row.
```lua
local info = C_CreatureInfo.GetRaceInfo(4)
-- { raceName = "Night Elf", clientFileString = "NightElf", raceID = 4 }
```
| Field | Type | Notes |
|-------|------|-------|
| `raceName` | string | Localized display name (client's active locale). |
| `clientFileString` | string | Non-localized token (`"Human"`, `"NightElf"`, `"Scourge"`) — the same string `UnitRace` returns as its 2nd value, used for texture paths. |
| `raceID` | number | Echo of the input id. |
### `C_CreatureInfo.GetClassInfo(classID)`
Localized class name + locale-independent token for a class id, read
from `ChrClasses.dbc`. Returns a `ClassInfo` table, or `nil` for a
non-numeric / non-positive id or one with no row.
```lua
local info = C_CreatureInfo.GetClassInfo(1)
-- { className = "Warrior", classFile = "WARRIOR", classID = 1 }
```
| Field | Type | Notes |
|-------|------|-------|
| `className` | string | Localized display name (client's active locale). |
| `classFile` | string | Locale-independent token (`"WARRIOR"`, `"MAGE"`) — the same string `UnitClass` returns as its 2nd value, and the key used for class colors / icon coords. |
| `classID` | number | Echo of the input id. |
### `C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)`
Info for a pet/beast family (`CreatureFamily.dbc` row), read from the
always-loaded client DBC. Returns a `CreatureFamilyInfo` table, or `nil`
for a non-numeric / non-positive / unused id.
```lua
local info = C_CreatureInfo.GetCreatureFamilyInfo(27)
-- { id = 27, name = "Wind Serpent",
-- iconFile = "Interface\\Icons\\Ability_Hunter_Pet_WindSerpent" }
```
| Field | Type | Notes |
|-------|------|-------|
| `id` | number | Echo of the input id. |
| `name` | string | Localized family name (client's active locale). |
| `iconFile` | string | Icon **texture path**. Retail returns a numeric fileID here; vanilla's DBC stores the path, so this is a string usable directly with `texture:SetTexture(...)`. `""` for families with no icon (warlock pets: Imp, Voidwalker, Succubus, Felhunter, …). |
### `C_CreatureInfo.GetCreatureFamilyIDs()`
Array of every populated `CreatureFamily.dbc` id, in ascending order.
The id space is sparse (unused rows like 10, 13, 14, 22 are skipped), so
this is the load-bearing piece for iterating families — each element
round-trips with
[`GetCreatureFamilyInfo`](#c_creatureinfogetcreaturefamilyinfocreaturefamilyid).
```lua
local ids = C_CreatureInfo.GetCreatureFamilyIDs()
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 16, ... }
for _, id in ipairs(ids) do
local fam = C_CreatureInfo.GetCreatureFamilyInfo(id)
-- fam.name, fam.iconFile
end
```
### `C_CreatureInfo.GetFactionInfo(raceID)`
Faction-group info for a **race** (Alliance or Horde), read from the
client DBCs. Returns a `FactionInfo` table, or `nil` for a non-numeric /
non-positive / unknown race, or a race with no named group.
```lua
local info = C_CreatureInfo.GetFactionInfo(1) -- Human
-- { name = "Alliance", groupTag = "Alliance" }
C_CreatureInfo.GetFactionInfo(2) -- Orc → { name = "Horde", groupTag = "Horde" }
```
| Field | Type | Notes |
|-------|------|-------|
| `name` | string | Localized faction-group name (`"Alliance"`, `"Horde"`, or the locale's translation). |
| `groupTag` | string | Locale-independent tag: `"Alliance"` or `"Horde"`. |
This is the by-`raceID` form of vanilla's existing `UnitFactionGroup(unit)`
(which returns `groupTag, name` for a live unit) — same resolution, no unit
required. It mirrors that engine function's chain exactly: race →
`FactionTemplate` group mask → `FactionGroup.dbc`, selecting the row whose
bit the mask sets and whose localized name is non-empty (that non-empty test
is how the engine skips the always-present `"Player"` bit and the nameless
`"Monster"` row to land on Alliance / Horde).
### `C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)`
Info for a creature type (`CreatureType.dbc` row), read from the
always-loaded client DBC. Returns a `CreatureTypeInfo` table, or `nil`
for a non-numeric / non-positive / unused id.
```lua
local info = C_CreatureInfo.GetCreatureTypeInfo(7)
-- { id = 7, name = "Humanoid" }
```
| Field | Type | Notes |
|-------|------|-------|
| `id` | number | Echo of the input id. |
| `name` | string | Localized type name (client's active locale). |
The type ids: `1` Beast, `2` Dragonkin, `3` Demon, `4` Elemental, `5`
Giant, `6` Undead, `7` Humanoid, `8` Critter, `9` Mechanical, `10` Not
specified, `11` Totem. Pairs with
[`UnitCreatureTypeID`](#unitcreaturetypeidunit) (id → name) and
[`GetCreatureTypeIDs`](#c_creatureinfogetcreaturetypeids).
### `C_CreatureInfo.GetCreatureTypeIDs()`
Array of every `CreatureType.dbc` id (contiguous `1`..`11` in vanilla),
each round-tripping with
[`GetCreatureTypeInfo`](#c_creatureinfogetcreaturetypeinfocreaturetypeid).
```lua
local ids = C_CreatureInfo.GetCreatureTypeIDs()
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }
```
## CVar
### `C_CVar.GetCVarBool(cvar)`
@@ -2403,6 +2608,12 @@ Fires on every modifier-key press and release with `(key, down)`:
Only transitions fire — key autorepeat does not. Matches 2.4.3+
semantics.
Releases that happen while WoW is **not the focused window** are also
handled: keyboard messages only reach WoW while it has focus, so a
modifier let go in the background is never seen live. On regaining focus
the cached state is reconciled against the true physical key state and
any missed transition fires then (see the focus-regain note below).
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("MODIFIER_STATE_CHANGED")
@@ -2433,6 +2644,25 @@ renderer-state changes that recreate WoW's main window (e.g. toggling
vertical sync), where an `SetWindowLongPtr`-style `WNDPROC` subclass
would be left dangling.
**Focus-regain reconciliation.** `WM_KEY*` messages are only delivered
while WoW is the foreground window, so a modifier released while WoW is
in the background (alt-tabbed away) produces no message — the cached bit
would stay stuck down and the release event would never fire. A second
thread hook (`WH_CALLWNDPROC`, needed because `WM_ACTIVATEAPP` is *sent*
rather than *posted* and so is invisible to `WH_GETMESSAGE`) watches for
app activation; on regaining focus it polls `GetAsyncKeyState` for each
L/R modifier — which reads global physical key state regardless of focus
— and fires `MODIFIER_STATE_CHANGED` for any bit that changed while WoW
was away. So the release surfaces the moment you tab back, and
`IsLeft/RightShiftKeyDown` etc. read correct state immediately.
> Note: the engine's own `IsShiftKeyDown` / `IsControlKeyDown` /
> `IsAltKeyDown` bottom out at the synchronous `GetKeyState`, which has
> the same background-release blind spot and is *not* reconciled by this
> hook — only ClassicAPI's L/R query functions and the event reflect the
> corrected state. Prefer `IsModifierKeyDown` / the L/R functions if you
> need the un-stuck value.
### `NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events
Fire when nameplate state actually changes. Payloads:
@@ -5635,6 +5865,47 @@ C_Item.LockItemByGUID(guid)
Same caveats as `LockItem` — purely client-side, doesn't affect any
real engine transaction state.
### `C_Item.PickupItem(itemInfo)`
Finds the item matching `itemInfo` and picks it up onto the cursor —
the C-side equivalent of clicking it. From there the held item can be
dropped into a bag/equipment slot, deleted (`DeleteCursorItem`), sold
at a merchant, etc.
`itemInfo` accepts the same shapes as
[`C_Item.EquipItemByName`](#c_itemequipitembynameiteminfo--dstslot) —
itemID number, bare `"item:N"` string, full chat link, or a localized
item name (matched case-insensitively against each candidate's
*decorated* name, random suffix included). Unlike the by-name equip/use
functions, the search covers **equipment (slots 1..19, checked first)
and then bags (0..4)**, so `PickupItem` picks up a worn item as readily
as a bagged one.
Returns nothing. Silently no-ops when:
- the input is `nil`, an empty string, or otherwise unparseable
- no matching item is found in equipment or bags
- the cursor is already holding something (`CursorHasItem()` is true)
- the matched item is locked (mid-transaction)
Drives the engine's cursor primitives directly — the paperdoll-slot
pickup (`FUN_004C7300`) for an equipped match, the container pickup
(`FUN_00494B60` + client-side item lock) for a bag match — not the Lua
`PickupInventoryItem` / `PickupContainerItem` globals. Same shared
`Item::Cursor` wrappers `EquipItemByName`'s auto-slot path uses.
```lua
-- Pick up a bagged item by name, then drop it into the off-hand slot:
C_Item.PickupItem("Linen Cloth")
PickupInventoryItem(17) -- or EquipCursorItem(), etc.
-- Pick up a worn item by ID (unequips onto the cursor):
C_Item.PickupItem(2589)
-- From a chat link:
C_Item.PickupItem(itemLink)
```
### `C_Item.RequestLoadItemDataByID(item)` / `C_Item.RequestLoadItemData(itemLocation)`
Asks the engine to fetch the item's data from the server if not already
@@ -6705,6 +6976,51 @@ end
## Map
### `C_Map.GetAreaInfo(areaID)`
Backport of the retail `C_Map.GetAreaInfo`. Returns the localized
`AreaTable.dbc` name for an area id — zones and subzones alike — or `nil`
for non-numeric / non-positive input or an id with no row. `AreaTable.dbc`
is a client DBC (always loaded, synchronous, localized), so the name is
available for any id with no async/cache step — unlike creature/item names.
```lua
C_Map.GetAreaInfo(12) -- "Elwynn Forest"
C_Map.GetAreaInfo(9) -- "Northshire Valley"
```
The name is in the client's active locale — the same source as
`GetRealZoneText` and the paperdoll zone name — so it matches what the
game displays everywhere.
### `C_Map.GetAreas()`
ClassicAPI extension. Returns `{ [areaID] = name, … }` — every
`AreaTable.dbc` row with a non-empty localized name, as an id→name map.
No retail equivalent (modern WoW enumerates the `uiMapID` tree, not raw
`AreaTable`); same "surface the hidden DBC" pattern as
[`C_Map.GetAreaTriggers`](#c_mapgetareatriggerinfotriggerid--c_mapgetareatriggersmapid).
The enumeration — not the per-id `GetAreaInfo` — is what name databases
need: name→id resolution, reverse search, and iterating the full id set.
Every key round-trips with `GetAreaInfo`; empty-name rows are omitted. The
map includes *every* named area (technical/internal ones too), so
consumers wanting only "real" zones should filter.
```lua
local areas = C_Map.GetAreas()
print(areas[12]) -- "Elwynn Forest"
-- build a name -> id index
local byName = {}
for id, name in pairs(areas) do byName[name] = id end
```
Lets a name-DB addon drop its shipped zone-name tables (pfQuest's
`db/<locale>/zones.lua`): repoint `pfDB["zones"]["loc"] = C_Map.GetAreas()`
(merging so the fork's overrides still apply). Live client data in the
active locale — no locale/rename drift vs a scraped table.
### `C_Map.GetBestMapForUnit(unitToken)`
Returns the `AreaTable.dbc` area ID the given unit is currently in,
@@ -6739,6 +7055,33 @@ 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).
### `C_Map.GetMapAreaIDs()`
ClassicAPI extension. Returns `{ [mapName] = areaID, … }` — each
`WorldMapArea.dbc` map's name (the dir the engine uses for its map
textures) mapped to its `AreaTable` areaID.
Vanilla has no `GetCurrentMapAreaID`, and `GetMapZones()` enumerates only a
continent's *outdoor* zones — so a browsed **city or instance map**
(Orgrimmar, Undercity, Alterac Valley) can't be resolved to a zone id, which
means addons can't match zone-keyed content (a flight master, a POI) to it.
This maps those names to their zone ids:
```lua
-- resolve a browsed map to an areaID, cities included
local areaID = C_Map.GetMapAreaIDs()[GetMapInfo()]
```
> The key is the `WorldMapArea` name, which matches `GetMapInfo()`'s returned
> dir for the vast majority of maps — but a few Blizzard map folders are
> spelled differently from the area name (the Orgrimmar map's folder is
> `"Ogrimmar"`), so `GetMapAreaIDs()[GetMapInfo()]` can miss for those. For
> those, resolve by zone name instead (`GetMapZones` → name → areaID).
Continent-spanning rows (areaID 0) and unnamed rows are omitted. Pairs with
[`C_Map.GetAreas`](#c_mapgetareas) (areaID → name) and
[`C_Map.GetMapWorldSize`](#c_mapgetmapworldsizeareaid) (areaID → yards).
### `C_Map.GetMapOverlays([areaID])`
ClassicAPI extension. Returns every `WorldMapOverlay.dbc` entry for a
@@ -6762,11 +7105,39 @@ Each overlay table:
|---|---|
| `textureName` | bare DBC name (`"GOLDSHIRE"`) |
| `texturePath` | `Interface\WorldMap\<dir>\GOLDSHIRE` (engine format) |
| `areaID` | primary `AreaTable` id the overlay reveals (first nonzero) |
| `areaIDs` | array of all `AreaTable` ids it reveals (usually one; 52 of 707 overlays span 2–3 subzones) |
| `textureWidth` / `textureHeight` | the DBC placement rect |
| `offsetX` / `offsetY` | placement on the zone canvas |
| `mapPointX` / `mapPointY` | DBC map-point fields |
| `hitRectTop` / `hitRectLeft` / `hitRectBottom` / `hitRectRight` | hit rectangle in world-map canvas px (≈1002×668); the tight clickable bounds of the landmass |
| `tileCols` / `tileRows` / `upscaled` | the **resolved** tile grid |
| `tiles` | ready-to-draw tile array (below) |
| `fileDataIDs` | ordered tile texture paths (retail's field name; redundant with `tiles[].file`) |
The `hitRect*` fields are the source addons use to place a **subzone
center** (the texture rect includes transparent padding, so its center is
off; the hit rect is the tight landmass bounds). Normalize against the
≈1002×668 world-map canvas:
```lua
local cx = (ov.hitRectLeft + ov.hitRectRight) / 2 / 1002 * 100 -- center X %
local cy = (ov.hitRectTop + ov.hitRectBottom) / 2 / 668 * 100 -- center Y %
local w = (ov.hitRectRight - ov.hitRectLeft) / 1002 * 100 -- width %
local h = (ov.hitRectBottom - ov.hitRectTop) / 668 * 100 -- height %
```
Overlays with `areaID > 0` cover the [subzone → parent-zone + center/size]
mapping that map addons hand-scrape (pfQuest's `pfDB["zones"]["data"]`).
Reading it live matches *this* client's maps — on a modified client
(Turtle) those differ from the scraped vanilla numbers, which is the point.
A row may be **texture-less** — a clickable subzone region with no distinct
art. It still appears, with `textureName == ""`, zero `textureWidth`/
`textureHeight`, and an empty `tiles` table (so `ipairs(ov.tiles)` iterates
zero times), carrying its `areaID`/`areaIDs` + `hitRect*`. Filter art
consumers on `ov.textureName ~= ""`. (Stock 1.12 and Octo happen to give
every overlay a texture, but the schema permits texture-less rows.)
Each `tiles` entry: `file` (texture path including the tile number,
`SetTexture`-ready), `width` / `height` (draw size in map pixels),
@@ -6805,6 +7176,106 @@ end
> reliable for placement; it's only the tile count they imply that
> lies.
### `C_Map.GetMapWorldSize([areaID])`
Returns a zone's world size in yards as `width, height` — the physical extent the
map represents, read straight from the `WorldMapArea.dbc` placement rect.
Addons use it to turn map-relative percents into yard distances (range
rings, "N yards away" readouts, proximity checks).
Retail takes a `uiMapID`; here it's an `areaID` (`AreaTable.dbc` id — the
same identity
[`C_Map.GetBestMapForUnit`](#c_mapgetbestmapforunitunittoken) returns and
[`C_Map.GetMapOverlays`](#c_mapgetmapoverlaysareaid) accepts), returns that
zone's size; with no argument, the world map's currently **viewed** zone
(same resolution `GetMapOverlays()` uses with no arg). Returns `nil, nil`
when the zone can't be resolved.
`width` is the east-west span (world Y), `height` the north-south span
(world X) — the same denominators behind the
[`C_Map.GetAreaTriggerInfo`](#c_mapgetareatriggerinfotriggerid--c_mapgetareatriggersmapid)
map-percent transform, so `mapX/100 * width` and `mapY/100 * height` give a
point's offset in yards within the zone.
```lua
local w, h = C_Map.GetMapWorldSize(85) -- Tirisfal: ~4518.7, 3012.5
```
### `C_Map.GetAreaTriggerInfo(triggerID)` / `C_Map.GetAreaTriggers([mapID])`
ClassicAPI extension. Exposes `AreaTrigger.dbc` — the client's 517
static trigger volumes (subzone-entry, exploration, and teleport
triggers). Vanilla loads the DBC but exposes **none** of its geometry
to Lua, which is why map addons (pfQuest et al.) ship hand-scraped
trigger tables; this is a live DBC read, same "surface the hidden data"
pattern as [`C_Map.GetMapOverlays`](#c_mapgetmapoverlaysareaid).
- `GetAreaTriggerInfo(triggerID)` — one trigger's info table, or `nil`
for a missing / out-of-range id.
- `GetAreaTriggers([mapID])` — an array of every trigger's info table,
optionally filtered to a single `Map.dbc` id (`0` Eastern Kingdoms,
`1` Kalimdor, `30` Alterac Valley, `489` Warsong Gulch, …).
Each info table carries both the **authoritative raw geometry** and a
**derived, ready-to-draw form**:
| field | meaning |
|---|---|
| `id` | `AreaTrigger.dbc` row id |
| `mapID` | `Map.dbc` id (continent, or an instance map) |
| `x` / `y` / `z` | continent-space **world** coordinates of the center |
| `radius` | sphere-trigger radius (`0` for a box trigger) |
| `isBox` | `true` when the trigger is an oriented box |
| `boxLength` / `boxWidth` / `boxHeight` / `boxYaw` | box dims + yaw (all `0` for a sphere) |
| `areaID` | `AreaTable` zone the point falls in — **absent** if unresolved |
| `mapX` / `mapY` | position as a `0..100` zone-relative percent — **absent** alongside `areaID` |
The raw `x`/`y`/`z` come straight from the DBC. `areaID`/`mapX`/`mapY`
are derived from the `WorldMapArea.dbc` zone rect (WoW's world axes:
`+X` north, `+Y` west). `mapX` is the **horizontal** map axis (off
world Y), `mapY` the **vertical** (off world X) — the standard WoW /
pfQuest / Astrolabe convention:
```
mapX% = (locLeft - y) / (locLeft - locRight) * 100 -- horizontal
mapY% = (locTop - x) / (locTop - locBottom) * 100 -- vertical
```
The zone is chosen by which one's **drawn landmass** the point sits on:
`WorldMapArea` rects are loose overlapping boxes, so a point near a
border falls in several; the `WorldMapOverlay` hit rects trace the real
landmass, and the zone the point is deepest inside an overlay of wins
(the runtime stand-in for an ADT area lookup). When the point is on no
zone's overlay it falls back to the most-interior containing rect; when
no rect contains it at all (open sea, an instance with no `WorldMapArea`
row, a degenerate rect), the three derived fields are simply omitted —
the raw world coords are
always present.
```lua
-- One trigger, its position as a world-map percent point:
local t = C_Map.GetAreaTriggerInfo(2)
if t.areaID then
print(t.areaID, t.mapX, t.mapY) -- 85 21.89 67.87 (Tirisfal)
end
-- Every trigger in Warsong Gulch:
for _, t in ipairs(C_Map.GetAreaTriggers(489)) do
-- t.x/y/z world coords, t.radius or t.isBox geometry, t.mapX/mapY
end
```
> **Matches the pfQuest-turtle trigger table 1:1.** Keyed by trigger
> id, the resolved `areaID` + `mapX`/`mapY` line up with
> `pfQuest-turtle`'s scraped `areatrigger-turtle.lua` (id 2 →
> `{21.89, 67.87, 85}` Tirisfal; id 45 → `{68, 17, 796}`). One
> difference in kind: a scraped table may list a trigger under several
> maps at once (a subzone *and* its parent continent); this resolves to
> the single zone whose **drawn landmass** the point sits on (via the
> WorldMapOverlay hit rects — the runtime stand-in for an ADT area
> lookup). An addon can drop `GetAreaTriggers()` straight in place of a
> shipped table.
Backports the six `C_MerchantFrame.*` calls retail addons use when
interacting with a vendor. All entry points read the engine's
merchant/buyback storage directly — no Lua-roundtrip through
@@ -6947,6 +7418,50 @@ disable the sell-all-junk button; vanilla has no such setting, so
the feature is always on. Function exists so retail addons that
gate `SellAllJunkItems` on this don't no-op silently.
## MapExplorationInfo
`C_MapExplorationInfo.GetExploredMapTextures` (retail backport) and
`GetUnexploredMapTextures` (ClassicAPI extension) are the
exploration-filtered views of a zone's `WorldMapOverlay.dbc` overlays.
Together with [`C_Map.GetMapOverlays`](#c_mapgetmapoverlaysareaid) (which
returns **all** overlays) they form the three-way split:
| function | returns |
|---|---|
| `C_MapExplorationInfo.GetExploredMapTextures(areaID)` | only overlays the player has **discovered** |
| `C_MapExplorationInfo.GetUnexploredMapTextures(areaID)` | only overlays **not** yet discovered |
| `C_Map.GetMapOverlays(areaID)` | **all** overlays, explored or not |
All three return the identical per-overlay table shape (see
`GetMapOverlays`); these two just filter it. "Explored" is computed
per-areaID from the player's explored-areas bitfield (the same source the
engine's own `GetNumMapOverlays`/`GetMapOverlayInfo` gate on), so the
`areaID` argument works for any zone — not only the one currently on the
world map. With no argument, both default to the current map view.
### `C_MapExplorationInfo.GetExploredMapTextures([areaID])`
Backport of the retail call (retail added it in 8.0). Returns an array of
the overlay tables for overlays the local player **has discovered** in the
zone — same shape as [`C_Map.GetMapOverlays`](#c_mapgetmapoverlaysareaid),
including `fileDataIDs` (retail's field name for the tile list; here it
holds `SetTexture`-ready texture *paths*, since vanilla has no numeric
fileDataIDs). Empty before the player is resident (character select).
```lua
for _, tex in ipairs(C_MapExplorationInfo.GetExploredMapTextures(12)) do
-- tex.fileDataIDs[i] -> SetTexture path; tex.offsetX/offsetY placement
end
```
### `C_MapExplorationInfo.GetUnexploredMapTextures([areaID])`
ClassicAPI extension — the complement: overlays the player has **not**
discovered, the pieces a map-reveal addon draws over the fogged base map.
No retail equivalent (retail ships only the explored getter; its inverse,
`C_Map.GetMapOverlays`, returns *everything*). Same table shape and
`areaID` semantics as the explored getter.
## NamePlate
Modern `C_NamePlate.*` returns nameplate `Frame` objects keyed off
@@ -7445,6 +7960,55 @@ if C_PlayerCache.IsScanEnabled()
end
```
## NewItems
Backports the modern "new item glow" bookkeeping — the sparkle the retail
bag UI shows on items you've just picked up but haven't looked at yet.
Vanilla 1.12 never had this feature (no DBC, no server data, no engine
bit); it's pure client-side tracking over your bag contents, so ClassicAPI
adds it.
Newness is keyed on each item's **instance GUID** (read off the CGItem),
not on `(bagID, slotIndex)`. A flag therefore survives the player
rearranging a bag — exactly like retail, and impossible for a pure-Lua
addon (vanilla exposes no per-instance GUID to Lua). Only **bag** items are
tracked (bags 0–4); equipment and bank are out of scope, matching retail.
The feed is entirely client-side C++, the same as retail (no addon Lua), and
event-driven rather than polled: it hangs off the engine's own `BAG_UPDATE`
fire sites, so a bag rescan runs only on frames where a bag actually
changed. Each rescan diffs the resident item GUIDs against the previous one,
flags anything newly acquired, and prunes flags for items that leave.
`BAG_NEW_ITEMS_UPDATED` (no payload) fires whenever the new-item set
changes. A one-time baseline taken ~1.5s after login (re-armed on character
switch) means items already owned at login aren't flagged.
### `C_NewItems.IsNewItem(bagID, slotIndex)`
Returns `true` if the item currently in bag `bagID` slot `slotIndex` is
flagged new, `false` otherwise (empty slot, unflagged item, or non-numeric
args). `bagID` 0 is the backpack; 1–4 are the equipped bags. Slot indices
are 1-based, matching `GetContainerItemInfo`.
```lua
if C_NewItems.IsNewItem(0, 3) then
-- show the "new" glow on backpack slot 3
end
```
### `C_NewItems.RemoveNewItem(bagID, slotIndex)`
Clears the new flag for the item in that slot (e.g. once the player has
seen it). No-op if the slot is empty or the item wasn't flagged. Fires
`BAG_NEW_ITEMS_UPDATED` when it actually removes a flag. Because the flag is
GUID-keyed, the item stays un-flagged even if it's later moved to another
slot.
### `C_NewItems.ClearAll()`
Clears every new flag at once. Fires `BAG_NEW_ITEMS_UPDATED` if anything was
flagged.
## PlayerInfo
### `C_PlayerInfo.CanUseItem(itemID)`
@@ -7527,6 +8091,50 @@ Accepts either the 16-digit `"0xHHHHHHHHLLLLLLLL"` form or the
Malformed input returns `false` rather than raising — matching
modern's tolerance for stale GUIDs from addon-side caches.
### `C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`
The `PlayerLocation`-taking player accessors. A `PlayerLocation` (see
`PlayerLocation` / `PlayerLocationMixin`) wraps one way to identify a player;
these resolve it to a unit and return the requested attribute.
| Function | Returns |
|----------|---------|
| `C_PlayerInfo.GetName(playerLocation)` | `name` |
| `C_PlayerInfo.GetClass(playerLocation)` | `className, classFilename, classID` |
| `C_PlayerInfo.GetRace(playerLocation)` | `raceID` |
| `C_PlayerInfo.GetSex(playerLocation)` | `sex` (1 neutral / 2 male / 3 female) |
| `C_PlayerInfo.IsConnected([playerLocation])` | `isConnected` |
```lua
local loc = PlayerLocation:CreateFromUnit("target")
local className, classFile, classID = C_PlayerInfo.GetClass(loc)
local name = C_PlayerInfo.GetName(loc)
local raceID = C_PlayerInfo.GetRace(loc) -- feed into C_CreatureInfo.GetRaceInfo
local sex = C_PlayerInfo.GetSex(loc)
```
- **Supported location kinds: `unit` and `guid`.** Unit locations use their
token directly; GUID locations resolve only while the GUID maps to a
currently-visible unit (target, party/raid member, someone in range). The
chat-line / who / battlefield-score / community / voice kinds have no
client-side unit resolution in 1.12 and return nil.
- Everything is read straight from the engine's own data — no `Unit*` Lua
wrappers are called. `GetName` uses the polymorphic object name getter (the
same path as `UnitNameFromGUID`); `GetSex` reads the `UNIT_FIELD_BYTES_0`
gender byte and reports it as `UnitSex`'s 2/3; `IsConnected` treats a
currently-synced object as online and otherwise consults the group-member
roster's online bit, exactly as `UnitIsConnected` does. `IsConnected` with
no argument defaults to the local player.
- **`GetRace` / `GetClass`** return the numeric ids that vanilla's
`UnitRace` / `UnitClass` don't (those give only names). `classID` round-trips
with [`C_CreatureInfo.GetClassInfo`](#c_creatureinfogetclassinfoclassid) and
`raceID` with [`C_CreatureInfo.GetRaceInfo`](#c_creatureinfogetraceinforaceid);
`GetClass`'s `className` / `classFilename` are the same localized name +
non-localized token (`"WARRIOR"`) those return.
- An unresolvable or unsupported location returns nil. A malformed unit token
raises (the engine's token→GUID resolver rejects it), same as passing it to
`UnitName`/`UnitClass` directly.
## Quest
### `C_QuestLog.GetQuestIDForLogIndex(index)`
@@ -9213,6 +9821,51 @@ clean class-string→classID lookup. Returns an empty table at
character select / pre-login (no CGPlayer yet) and for levels
where no class/race spells match.
### `C_SpellBook.GetSkillLineName(skillLineID)`
Returns the localized `SkillLine.dbc` name for a skill-line id — the
id → name half of the bridge addons build by hand. A pure DBC read: it
answers for any valid skill line (`182` = Herbalism, `186` = Mining, the
weapon/defense lines, profession lines) whether or not the player has
learned it. `nil` for non-numeric / non-positive input or an id with no
`SkillLine.dbc` row.
Pairs with [`GetSkillLineRank`](#c_spellbookgetskilllinerankskilllineid) —
name from the DBC, rank from the player's live skills.
```lua
C_SpellBook.GetSkillLineName(182) -- "Herbalism"
C_SpellBook.GetSkillLineName(186) -- "Mining"
```
### `C_SpellBook.GetSkillLineRank(skillLineID)`
Returns `(curRank, maxRank, modifier)` — the local player's rank in a
skill line by `SkillLine.dbc` id — or `nil` if the player hasn't learned
it.
- `curRank` — current skill value (base, no temp bonus)
- `maxRank` — the line's cap at the player's level
- `modifier` — temporary bonus (weapon oils, buffs; `0` if none)
```lua
local cur, max = C_SpellBook.GetSkillLineRank(182) -- Herbalism: 285, 300 (nil if unlearned)
if C_SpellBook.GetSkillLineRank(186) then -- has Mining at all?
-- ...
end
```
Vanilla's `GetSkillLineInfo(index)` only walks the skill window by
position and returns skills by *name*, so an addon starting from a
SkillLine.dbc id (a quest's `requiredSkill`) has to map id → localized
name and then string-match every skill line to recover the rank. This
reads the player's live skill list keyed by id directly — the returned
ranks are the same values `GetSkillLineInfo` reports. Resolves the id to
the player's skill slot via `FUN_SKILL_LINE_TO_SLOT`, then reads the
CGPlayer skill table (the same storage `C_Item.IsEquippableItem`'s
require-skill gate uses). `nil` when the line isn't learned, for bad
input, or before the player object is resident (character select).
### `C_SpellBook.GetSpellSkillLine(spellID)`
Returns `(name, skillID)` — the SkillLine.dbc row that the given spell
@@ -9676,6 +10329,120 @@ a different field.
Equivalent to the talentID return slot of `GetTalentInfo` in modern
WoW (5.0+; not exposed at all in 1.12).
## TaxiMap
Backports of the retail taxi-map node enumerators, reading
`TaxiNodes.dbc` (static) and the open flight master's live session. Both
also register the `Enum.FlightPathFaction` (`Neutral=0`, `Horde=1`,
`Alliance=2`) and `Enum.FlightPathState` (`Current=0`, `Reachable=1`,
`Unreachable=2`) tables, so retail code comparing against those enums
ports unchanged.
Ship both because they answer different questions:
| function | source | use |
|---|---|---|
| `C_TaxiMap.GetTaxiNodesForMap([mapID])` | `TaxiNodes.dbc` (static) | every flight point (one continent, or all with no arg), faction-tagged, discovered or not — for a database / map overlay |
| `C_TaxiMap.GetAllTaxiNodes([uiMapID])` | live taxi session | the nodes reachable from the **open** flight master, with per-node state + the slot index to fly — for a flight-map UI |
### `C_TaxiMap.GetTaxiNodesForMap([mapID])`
Returns an array of flight masters. A numeric `mapID` (a `Map.dbc` id —
`0` Eastern Kingdoms, `1` Kalimdor, `30` Alterac Valley, … — the same
identity `C_Map.GetAreaTriggers([mapID])` uses; retail's `uiMapID` has no
vanilla analog) filters to that continent. **Omitted / non-number returns
every flight master on every continent** — a one-call flight database
(each entry carries its own `mapID`). This is a ClassicAPI extension to
retail's required-argument signature.
Only real flight masters are returned. `TaxiNodes.dbc` also contains
non-flight rows — transports/boats/zeppelins (no flight mount) and
non-selectable markers like Northshire Abbey or standalone instance
entries (no connecting flight path) — which are filtered out: a node is
included only if it has a flight mount **and** is a `TaxiPath.dbc`
endpoint (some flight path connects to it).
Each entry:
| field | meaning |
|---|---|
| `nodeID` | `TaxiNodes.dbc` row id |
| `name` | localized node name (`"Stormwind, Elwynn Forest"`) |
| `faction` | `Enum.FlightPathFaction` — Alliance-mount-only → `Alliance`, Horde-only → `Horde`, both → `Neutral` |
| `reachable` | *(ext)* a flight path leads *to* this node (in-degree > 0). `false` = departure-only, reachable only by other means (druid Teleport → "Nighthaven, Moonglade"; a boat dock → "Stormwind Harbor") |
| `position` | `{x, y}` 0–1 on the continent map (retail-accurate) |
| `mapID` | *(ext)* continent `Map.dbc` id |
| `x` / `y` / `z` | *(ext)* raw continent world coords |
| `areaID` | *(ext)* the `AreaTable` zone, from the node's own name — its **location** if that's a mapped city ("Orgrimmar" → Orgrimmar city), else the **zone** suffix ("…, Western Plaguelands" → WPL). Authoritative; absent if unresolved |
| `mapX` / `mapY` | *(ext)* 0–100 position within that zone (`mapX` horizontal, `mapY` vertical) |
`nodeID` / `name` / `faction` / `position` are the retail fields; the
rest are ClassicAPI extensions (marked *(ext)*). `position` matches
retail (continent 0–1), while `areaID` + `mapX`/`mapY` let a zone-map
addon pin directly. `areaID` comes from the node's **name**, matched
against `AreaTable` — the location part when it's a mapped city
("Orgrimmar, Durotar" → Orgrimmar city, so a capital's master pins on
the city map), otherwise the zone suffix ("Chillwind Camp, Western
Plaguelands" → WPL). Each candidate is accepted only if its map rect
contains the node, so this is right at borders a purely geometric guess
gets wrong (Chillwind → WPL, not the Alterac box it sits in). Nodes
whose name resolves to no containing zone fall back to the geometric
landmass resolver. `isUndiscovered` is not wired (the client's
known-node bitfield isn't read) — every node is returned regardless of
discovery.
```lua
for _, n in ipairs(C_TaxiMap.GetTaxiNodesForMap(0)) do
if n.faction == Enum.FlightPathFaction.Alliance and n.areaID then
Pin(n.areaID, n.mapX, n.mapY, n.name) -- zone-map pin
end
end
```
Replaces hand-scraped flight tables (pfQuest's `pfDB.meta.flight`):
draw a node per entry (filtered by faction, using `areaID`+`mapX`/`mapY`)
exactly like `SearchAreaTriggerID`. Auto-covers custom-server flight
points — no NPC-id curation.
### `C_TaxiMap.GetAllTaxiNodes([uiMapID])`
Returns the live flight-master destination list — the nodes reachable
from the **currently open** taxi map, with reachability state and the
slot index needed to take the flight. **Only meaningful while the taxi
map is open** (between `TAXIMAP_OPENED` and `TAXIMAP_CLOSED`); returns an
empty table otherwise. `uiMapID` is accepted for retail signature parity
but ignored (vanilla has a single active taxi map).
Each entry:
| field | meaning |
|---|---|
| `slotIndex` | the legacy `1..NumTaxiNodes()` index — pass to `TakeTaxiNode(slotIndex)` to fly |
| `name` | node name |
| `position` | `{x, y}` 0–1 on the flight-map image (matches retail) |
| `state` | `Enum.FlightPathState` — `CURRENT`→`Current`, `REACHABLE`→`Reachable`, `DISTANT`/none→`Unreachable` |
| `nodeID` | `TaxiNodes.dbc` id (resolved from the slot's node record; `0` if absent) |
No `faction` / `isUndiscovered` here — retail's `GetAllTaxiNodes` doesn't
carry them (reachability implies faction); that data lives on
`GetTaxiNodesForMap`. `textureKit` / `useSpecialIcon` (retail cosmetic
fields) have no vanilla equivalent and are omitted.
```lua
-- Fly to the first reachable destination:
for _, n in ipairs(C_TaxiMap.GetAllTaxiNodes()) do
if n.state == Enum.FlightPathState.Reachable then
TakeTaxiNode(n.slotIndex)
break
end
end
```
This is the interactive counterpart to `GetTaxiNodesForMap`: session-gated
and player-relative (its whole point is the open master's reachable set),
which is why a static database like pfQuest uses `GetTaxiNodesForMap`
instead.
## Time
### `GetServerTime()`
@@ -10103,6 +10870,47 @@ becomes visible to the client AND the server has sent its
the row may be briefly NULL — usually only one or two frames.
Subsequent calls succeed once the response lands.
### `UnitCreatureFamilyID(unit)`
Returns the numeric **CreatureFamily** id (the `CreatureFamily.dbc`
row — `1` = Wolf, `2` = Cat, `3` = Spider, …) for a unit, or `nil`
when it has no family (players, non-beast NPCs, unsynced NPCs).
```lua
/dump UnitCreatureFamilyID("pet") -- e.g. 1 for a wolf pet
/dump UnitCreatureFamilyID("target") -- 2 for a cat, nil for a humanoid
```
Stock `UnitCreatureFamily(unit)` returns only the *localized name*
(`"Wolf"`, `"Chat"`) — awkward to compare and locale-dependent. This
extension exposes the raw id addons actually want for stable checks
(tameable-pet filtering, family-specific ability tables). It's the same
id the engine's `UnitCreatureFamily` resolves internally
(`[unit + 0xB30] → [+0x1C]`) before mapping it through
`CreatureFamily.dbc` for the display name — this just stops at the id.
`nil` semantics match `UnitCreatureFamily` (both return nothing for a
family of 0), and it shares `UnitSubName`'s creature-cache coverage
caveat (a fresh NPC's row may be briefly NULL for a frame or two).
### `UnitCreatureTypeID(unit)`
Returns the numeric **CreatureType** id (the `CreatureType.dbc` row —
`1` = Beast, `3` = Demon, `7` = Humanoid, …) for a unit, or `nil` when
it has no resolvable type. The raw-id twin of `UnitCreatureFamilyID`.
```lua
/dump UnitCreatureTypeID("target") -- 7 for a humanoid, 1 for a beast
/dump UnitCreatureTypeID("player") -- 7 (Humanoid, via the player's race)
```
Stock `UnitCreatureType(unit)` returns only the *localized name*
(`"Humanoid"`, `"Humanoïde"`) — awkward to compare and locale-dependent.
This exposes the id addons want for stable checks. It calls the same inner
resolver the engine's `UnitCreatureType` uses, which handles every unit kind
(NPCs via the creature cache, players via their race → Humanoid). Pair with
[`C_CreatureInfo.GetCreatureTypeInfo`](#c_creatureinfogetcreaturetypeinfocreaturetypeid)
to get the localized name back from the id.
### `GetUnitSpeed(unit)`
Returns `currentSpeed, runSpeed, flightSpeed, swimSpeed` — all four
+186 -7
View File
@@ -530,14 +530,23 @@ enum Offsets {
OFF_UNIT_FIELD_CREATEDBY = 0x20,
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
// Returns 1 for a controllable **pet** (classified by creature family
// at descriptor +0x212 → family DBC, with a ChrRaces fallback), a
// non-1 value otherwise. The engine's unit-title builder `FUN_0052FD30`
// uses it as `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's
// Minion" — so it's the exact signal separating pet from minion, which
// neither the GUID (pets and guardians share the 0xF14… prefix) nor
// UNIT_FLAG_PLAYER_CONTROLLED reliably does.
// `0x00605570` is really the engine's **creature-type resolver** — it's
// `Script_UnitCreatureType`'s (`0x0051A280`) inner helper and returns the
// `CreatureType.dbc` id (1=Beast, 3=Demon, 7=Humanoid, …), via three
// paths in order: a display-override at descriptor `+0x212`, then the
// creature-cache type at `[unit+0xB30]+0x18`, then the player-race type
// at `ChrRaces[[unit+0x110]+0x78]+0x24`. `FUN_UNIT_CREATURE_TYPE` is the
// canonical name; `FUN_UNIT_PET_MINION_CLASS` is a historical alias for
// the same address.
//
// The pet/minion use: the unit-title builder `FUN_0052FD30` calls it as
// `(fn(unit) != 1) + 1` → 1 = "X's Pet" title, 2 = "X's Minion". That
// works because it's really testing "is this a Beast (type 1)": hunter
// pets are Beasts, warlock minions are Demons (type 3) — so `!= 1`
// cleanly separates them, which neither the GUID (pets and guardians
// share the 0xF14… prefix) nor UNIT_FLAG_PLAYER_CONTROLLED reliably does.
FUN_UNIT_PET_MINION_CLASS = 0x00605570,
FUN_UNIT_CREATURE_TYPE = 0x00605570,
// Party / raid roster counts and the party GUID array referenced
// in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by
@@ -706,11 +715,19 @@ enum Offsets {
// +0x00 ?
// +0x0C name C-string ptr (alias of UNIT_NAME for creatures)
// +0x10 subName C-string ptr ("Innkeeper", "<Master Trainer>", etc.)
// +0x1C CreatureFamily id (→ CreatureFamily.dbc), 0 = none
//
// Used by `Script_UnitName`'s creature path and by VanillaMinimapTracking's
// subname-based blip filter (`src/Blips.cpp::ExtractUnitSubName`).
OFF_UNIT_CREATURE_CACHE_ROW = 0xB30,
OFF_CREATURE_CACHE_SUB_NAME = 0x10,
// CreatureFamily id within the +0xB30 cache row — the value the engine's
// `Script_UnitCreatureFamily` (`0x0051A310`) reads via its family helper
// `FUN_006055e0` (`[unit+0xB30] ? [row+0x1C] : 0`) before mapping it
// through `CreatureFamily.dbc` for the localized name. `UnitCreatureFamilyID`
// returns the raw id. Same field as `OFF_CREATURE_FAMILY` in the creature
// cache data block, reached through the unit here.
OFF_CREATURE_CACHE_FAMILY = 0x1C,
// Pointer to the CGUnit's 8-byte GUID at `*(CGUnit + 0x08)`. Verified
// in `Script_GetInventoryItemLink` at `0x004C8CB0`-`0x004C8CB5`:
// `mov eax, [esi+8]; mov edi, [eax]; mov ecx, [eax+4]` reads the
@@ -1207,8 +1224,33 @@ enum Offsets {
// locLeft/locRight/locTop/locBottom floats}
VAR_WORLDMAP_AREA_RECORDS = 0x00C0D5BC,
VAR_WORLDMAP_AREA_COUNT = 0x00C0D5C0,
OFF_WMA_MAP_ID = 0x04,
OFF_WMA_AREA_ID = 0x08,
OFF_WMA_NAME = 0x0C,
OFF_WMA_LOC_LEFT = 0x10, // world Y at the map's left edge (max Y)
OFF_WMA_LOC_RIGHT = 0x14, // world Y at the map's right edge (min Y)
OFF_WMA_LOC_TOP = 0x18, // world X at the map's top edge (max X)
OFF_WMA_LOC_BOTTOM = 0x1C, // world X at the map's bottom edge (min X)
// AreaTrigger.dbc record (10 fields, 0x28 bytes) — instance cataloged
// in docs/DBCs.md (records slot 0x00C0E034, count 0x00C0E038). The
// client loads it but exposes NO geometry to Lua, which is why map
// addons (pfQuest et al.) ship hand-scraped trigger tables. Schema:
// {ID@+0, mapID@+4 (Map.dbc — continent or instance),
// x@+8, y@+0xC, z@+0x10 (continent-space world coords),
// radius@+0x14 (sphere trigger; 0 => box trigger),
// boxLength@+0x18, boxWidth@+0x1C, boxHeight@+0x20, boxYaw@+0x24}
VAR_AREATRIGGER_RECORDS = 0x00C0E034,
VAR_AREATRIGGER_COUNT = 0x00C0E038,
OFF_AT_MAP_ID = 0x04,
OFF_AT_X = 0x08,
OFF_AT_Y = 0x0C,
OFF_AT_Z = 0x10,
OFF_AT_RADIUS = 0x14,
OFF_AT_BOX_LENGTH = 0x18,
OFF_AT_BOX_WIDTH = 0x1C,
OFF_AT_BOX_HEIGHT = 0x20,
OFF_AT_BOX_YAW = 0x24,
// WorldMapOverlay.dbc record (17 fields, 0x44 bytes):
// {ID@+0, worldMapAreaID@+4 (-> WorldMapArea row),
@@ -1223,6 +1265,7 @@ enum Offsets {
VAR_WORLDMAP_OVERLAY_RECORDS = 0x00C0D594,
VAR_WORLDMAP_OVERLAY_COUNT = 0x00C0D598,
OFF_WMO_WORLDMAP_AREA = 0x04,
OFF_WMO_AREA_ID = 0x08, // areaID[4] @ +0x08..+0x14 (AreaTable ids revealed)
OFF_WMO_MAP_POINT_X = 0x18,
OFF_WMO_MAP_POINT_Y = 0x1C,
OFF_WMO_TEXTURE_NAME = 0x20,
@@ -1230,6 +1273,10 @@ enum Offsets {
OFF_WMO_TEXTURE_HEIGHT = 0x28,
OFF_WMO_OFFSET_X = 0x2C,
OFF_WMO_OFFSET_Y = 0x30,
OFF_WMO_HITRECT_TOP = 0x34, // hit rect in world-map canvas px (~1002x668):
OFF_WMO_HITRECT_LEFT = 0x38, // the tight clickable bounds of the landmass,
OFF_WMO_HITRECT_BOTTOM = 0x3C, // used for subzone-center placement (pfQuest's
OFF_WMO_HITRECT_RIGHT = 0x40, // pfDB["zones"] was scraped from these)
// Current world-map view selection — the globals the engine's
// current-map-name resolver (FUN_004a6cf0, behind GetMapInfo) reads:
@@ -1357,6 +1404,11 @@ enum Offsets {
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
// exists on the +0xE68 sub-struct here.
OFF_CGPLAYER_INFO = 0xE68,
// Explored-areas bitfield inside the CGPlayer info sub-struct
// (`[player+0xE68] + 0xE6C`): bit `AreaBit` set == that AreaTable area
// is explored. 0x100 bytes. Read by the discovered-overlay rebuild
// FUN_004a67a0 as `bits[AreaBit>>3] & (1 << (AreaBit&7))`.
OFF_PLAYER_EXPLORED_BITS = 0xE6C,
// PvP rank fields within the CGPlayer +0xE68 sub-struct (siblings of
// the combo-point byte at +0x1029). Read by the item tooltip's
@@ -1487,6 +1539,25 @@ enum Offsets {
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
// returns the inventory manager + linear slot ready to feed into GetItemBySlot.
FUN_PACK_BAG_SLOT = 0x004F9820,
// Equipped-bag container-GUID getter — `uint64 __fastcall(uint bagIndex0)`
// where `bagIndex0` is 0-based (Lua bagID 1..4 → 0..3; 4..9 are bank bags,
// gated on the bank-open globals). Returns the CGContainer GUID of the bag
// equipped in that slot, or 0 if none. This is the internal PackBagSlot
// uses to resolve bags 1..4: get the bag GUID here, resolve the container
// via FUN_OBJECT_RESOLVE_BY_GUID(OBJ_TYPE_CONTAINER, guid), then call the
// container's vtable[+OFF_CONTAINER_GET_INVENTORY] to get the inventory
// object GetItemBySlot indexes. Lets us enumerate bag contents in pure C++
// without PackBagSlot's Lua-stack coupling. (Reads GUID arrays at
// 0x00BDD060 lo / 0x00BDD064 hi, stride 8.)
FUN_GET_EQUIPPED_BAG_GUID = 0x004F93E0,
// CGContainer vtable slot (byte offset into the vtable) whose method
// returns the container's inventory object — `void* __thiscall(container)`.
// The inventory object's slot count lives at its +0x00 (OFF_INVMGR_SLOT_COUNT).
OFF_CONTAINER_GET_INVENTORY = 0x10,
// Backpack (bagID 0) linear-slot base in the player invMgr GUID/item
// arrays: a 1-based backpack slot S maps to linear (S - 1) + 23. Matches
// PackBagSlot's `*param_3 += 0x17` for the backpack branch.
BACKPACK_LINEAR_BASE = 23,
// Player inventory manager layout — used for direct GUID-array reads
// that bypass `GetItemBySlot`'s bank gate at `0x006228C1`.
// +0x00 uint32 max slot count
@@ -2177,6 +2248,56 @@ enum Offsets {
OFF_CHRRACES_FILENAME = 0x3C,
OFF_CHRRACES_NAMES = 0x44,
// CreatureFamily.dbc — pet/beast family metadata. Standard 5-DWORD
// class shape (instance 0x00C0DE74; records 0x00C0DE7C, count
// 0x00C0DE80 — the same globals `Script_UnitCreatureFamily`
// (`0x0051A310`) bounds-checks/indexes). 72-byte stride, familyID-keyed
// and sparse (10/13/14/22/… are unused). Localized `Name[]` at +0x20
// (the engine reads `record + 0x20 + locale*4`), `IconFile` texture-path
// string at +0x44 (last field; empty for warlock-pet families). Backs
// `C_CreatureInfo.GetCreatureFamilyInfo` / `GetCreatureFamilyIDs`.
VAR_CREATUREFAMILY_RECORDS = 0x00C0DE7C,
VAR_CREATUREFAMILY_COUNT = 0x00C0DE80,
OFF_CREATUREFAMILY_NAMES = 0x20,
OFF_CREATUREFAMILY_ICON = 0x44,
// CreatureType.dbc — pet/NPC classification (1=Beast … 11=Totem;
// contiguous ids, 11 rows). Records 0x00C0DE2C, count 0x00C0DE30 (the
// globals `Script_UnitCreatureType` (`0x0051A280`) bounds-checks/indexes),
// localized `Name[]` at +0x04 (engine reads `record + 0x04 + locale*4`).
// Backs `C_CreatureInfo.GetCreatureTypeInfo` / `GetCreatureTypeIDs`;
// `UnitCreatureTypeID` returns the id `FUN_UNIT_CREATURE_TYPE` resolves.
VAR_CREATURETYPE_RECORDS = 0x00C0DE2C,
VAR_CREATURETYPE_COUNT = 0x00C0DE30,
OFF_CREATURETYPE_NAMES = 0x04,
// Race → faction group, mirroring the player branch of
// `Script_UnitFactionGroup` (`0x00516630`) — backs
// `C_CreatureInfo.GetFactionInfo(raceID)`. Chain:
// ChrRaces[race] + 0x08 → FactionTemplate id
// FactionTemplate[id] + 0x0C → group mask (ourMask)
// scan FactionGroup for the row whose bit is set (and whose
// localized name is non-empty — that non-empty test is what
// skips the always-set "Player" bit and lands on Alliance/Horde)
// push { groupTag = row+0x08 (english), name = row+0x0C+locale*4 }
// FactionTemplate is a pointer-array DBC (records[id]); FactionGroup
// is stored CONTIGUOUSLY (records-base + i*stride, direct field reads,
// NOT a pointer array), so iterate it by hand rather than via DBC::Record.
// FactionTemplate globals match docs/DBCs.md; the FactionGroup
// records-base/count globals are the ones the engine actually
// dereferences per the disassembly (0x00C0DD5C / 0x00C0DD60), which
// differ from DBCs.md's standard-shape tabulation for this table.
OFF_CHRRACES_FACTION_TEMPLATE = 0x08,
VAR_FACTIONTEMPLATE_RECORDS = 0x00C0DD3C,
VAR_FACTIONTEMPLATE_COUNT = 0x00C0DD40,
OFF_FACTIONTEMPLATE_GROUP_MASK = 0x0C,
VAR_FACTIONGROUP_RECORDS = 0x00C0DD5C,
VAR_FACTIONGROUP_COUNT = 0x00C0DD60,
FACTIONGROUP_STRIDE = 0x30,
OFF_FACTIONGROUP_BIT = 0x04,
OFF_FACTIONGROUP_ENGLISH = 0x08,
OFF_FACTIONGROUP_NAMES = 0x0C,
// AreaTable.dbc — used by `Script_GetCharacterInfo` to resolve
// each character's last-known-area to a localized zone name.
// Same shape as the other DBCs (records pointer + count globals,
@@ -2184,6 +2305,55 @@ enum Offsets {
VAR_AREATABLE_RECORDS = 0x00C0E048,
VAR_AREATABLE_COUNT = 0x00C0E04C,
OFF_AREATABLE_NAMES = 0x2C,
// AreaBit @ +0x0C: index into the player's explored-areas bitfield
// (OFF_PLAYER_EXPLORED_BITS). Gate @ +0x28: when < 0 the area needs no
// exploration (always counts explored); when >= 0 the AreaBit check
// applies. Source: the discovered-overlay rebuild FUN_004a67a0.
OFF_AREATABLE_AREA_BIT = 0x0C,
OFF_AREATABLE_EXPLORE_GATE = 0x28,
// TaxiNodes.dbc (16 fields, 0x40 bytes) — flight points. records/count
// per docs/DBCs.md. mapID @ +0x04 is the Map.dbc continent; x/y/z floats;
// Name[8] localized @ +0x14; the two MountCreatureID columns are
// [Horde @ +0x38, Alliance @ +0x3C] (0 = that faction has no flight
// master at this node — both 0 = a transport/system waypoint).
VAR_TAXINODES_RECORDS = 0x00C0D6C0,
VAR_TAXINODES_COUNT = 0x00C0D6C4,
OFF_TAXINODE_MAP_ID = 0x04,
OFF_TAXINODE_X = 0x08,
OFF_TAXINODE_Y = 0x0C,
OFF_TAXINODE_Z = 0x10,
OFF_TAXINODE_NAME = 0x14,
OFF_TAXINODE_HORDE_MOUNT = 0x38,
OFF_TAXINODE_ALLIANCE_MOUNT = 0x3C,
// Live taxi session — the open flight master's reachable node list,
// populated between TAXIMAP_OPENED/CLOSED (count 0 otherwise). Backs the
// legacy NumTaxiNodes/TaxiNodeName/TaxiNodePosition/TaxiNodeGetType Lua
// globals. Node slots are 0x20-stride records at VAR_TAXI_NODE_ARRAY:
// +0x00 = pointer to the node's TaxiNodes.dbc record (id @ +0, name @
// +0x14), +0x04 = flight-map x (0-1 float), +0x08 = y. Count at
// VAR_TAXI_NODE_COUNT. Derived from Script_TaxiNode* (FUN_004dc380 name,
// FUN_004dc3b0 position); the reachability type comes from
// FUN_TAXI_NODE_TYPE.
VAR_TAXI_NODE_COUNT = 0x00BB4AA8,
VAR_TAXI_NODE_ARRAY = 0x00BB4F10,
TAXI_NODE_STRIDE = 0x20,
OFF_TAXI_SLOT_RECORD = 0x00,
OFF_TAXI_SLOT_X = 0x04,
OFF_TAXI_SLOT_Y = 0x08,
// __fastcall(uint slot0Based) -> const char* ("CURRENT" / "REACHABLE" /
// "DISTANT" / other) — the node's reachability for the open session.
FUN_TAXI_NODE_TYPE = 0x004DC4E0,
// TaxiPath.dbc (4 fields, 0x10 bytes): {id, fromNode@+0x04, toNode@+0x08,
// cost}. A real flight master is a node some flight path connects to;
// Northshire Abbey / spurious duplicate rows / standalone markers are not
// endpoints and so are filtered out of GetTaxiNodesForMap.
VAR_TAXIPATH_RECORDS = 0x00C0D698,
VAR_TAXIPATH_COUNT = 0x00C0D69C,
OFF_TAXIPATH_FROM = 0x04,
OFF_TAXIPATH_TO = 0x08,
// Engine player-info cache — populated by the
// SMSG_NAME_QUERY_RESPONSE handler (opcode 0x51) at 0x005551A0,
@@ -3426,6 +3596,15 @@ enum Offsets {
// shape from SMSG_PARTY_MEMBER_STATS_FULL).
FUN_GROUP_MEMBER_STATS_LOOKUP = 0x00496400,
OFF_GROUP_MEMBER_AREA_ID = 0x14,
// `+0x08` status-flags byte of the stats block (see field map above);
// bit 0 is the online flag `Script_UnitIsConnected` tests. Sibling
// `FUN_GROUP_MEMBER_SLOT_LOOKUP` (`0x00496420`, `__fastcall(u64 *guid)
// → member slot*`) is the presence fallback UnitIsConnected consults
// when no stats block exists — a non-null slot means the member is
// present (and thus connected). Both take a pointer to the 64-bit GUID.
OFF_GROUP_MEMBER_STATUS_FLAGS = 0x08,
GROUP_MEMBER_STATUS_ONLINE = 0x1,
FUN_GROUP_MEMBER_SLOT_LOOKUP = 0x00496420,
// Per-slot GUID tables — used to map party/raid token strings
// ("party1", "raid17") to GUIDs without going through
+11
View File
@@ -56,6 +56,7 @@
#include "Game.h"
#include "Offsets.h"
#include "bag/UpdateDelayed.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
@@ -71,8 +72,15 @@ const Event::Custom::AutoReserve _reserve{kEventName};
// Single bool, no atomics needed — the engine is single-threaded.
bool g_pending = false;
// C++ subscribers notified once per bag-change frame (see UpdateDelayed.h).
AutoSubscribe *g_subs = nullptr;
} // namespace
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_subs) {
g_subs = this;
}
// `FUN_004F91A0` — `__cdecl void()`. Bag-slot diff loop. Walks the
// 10 bag slots, fires BAG_UPDATE for each that changed.
using BagSlotDiff_t = void(__cdecl *)();
@@ -123,6 +131,9 @@ void OnWorldTick() {
const int slot = Event::Custom::Lookup(kEventName);
if (slot >= 0)
Event::Custom::Fire(slot, "");
// Notify C++ subscribers of the bag-change frame (see UpdateDelayed.h).
for (auto *node = g_subs; node != nullptr; node = node->next)
node->cb();
}
static const Game::HookAutoRegister _hookSlotDiff{
+39
View File
@@ -0,0 +1,39 @@
// 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/>.
#pragma once
// Authoritative "a bag changed this frame" subscription. `Bag::UpdateDelayed`
// co-hooks the three (and only three) engine `BAG_UPDATE` fire sites and
// coalesces them into a single per-frame signal — the same signal that backs
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
// per frame in which any bag actually changed, instead of polling every frame.
//
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
// frame has already fired (so bag contents are settled). No firing on frames
// where nothing changed. Order across subscribers is unspecified.
//
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
// file-scope `static const Bag::UpdateDelayed::AutoSubscribe _s{&Callback};`.
namespace Bag::UpdateDelayed {
using Callback = void (*)();
struct AutoSubscribe {
explicit AutoSubscribe(Callback cb);
Callback cb;
AutoSubscribe *next;
};
} // namespace Bag::UpdateDelayed
+28
View File
@@ -130,11 +130,39 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
return 2;
}
// `C_Container.CalculateTotalNumberOfFreeBagSlots()` — the total number of
// free slots across the player's *general-purpose* bags (backpack + equipped
// bags 0..4 whose BagFamily is 0). Specialty bags (quivers, soul bags, herb
// bags, …) are excluded, mirroring FrameXML's implementation exactly:
//
// for bag = BACKPACK .. lastBag:
// free, family = C_Container.GetContainerNumFreeSlots(bag)
// if family == 0 then total = total + free end
//
// Takes no arguments. Caveat inherited from `GetContainerNumFreeSlots`:
// vanilla-era server data leaves most bags' BagFamily at 0, so specialty
// bags that retail *would* exclude (Soul Pouch, Enchanting Bag, …) get
// counted here — the client just doesn't carry the family bit for them. The
// result stays consistent with what `GetContainerNumFreeSlots` reports for
// those same bags.
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
int total = 0;
for (int bagID = 0; bagID <= 4; bagID++) {
const BagInfo info = ResolveBagInfo(bagID);
if (info.slotCount > 0 && info.bagType == 0)
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
}
Game::Lua::PushNumber(L, static_cast<double>(total));
return 1;
}
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
&Script_C_Container_GetContainerNumFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+52
View File
@@ -0,0 +1,52 @@
// 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_Container.HasContainerItem(bagIndex, slotIndex)` — returns whether the
// given bag slot currently holds an item (a Classic Era addition). It's the
// occupancy sibling of `GetContainerItemID`: resolves the slot to its CGItem
// the same way (`Item::Location::ResolveBag` → engine `PackBagSlot` →
// `GetItemBySlot`), and a populated slot yields a non-null pointer while an
// empty slot yields null. Presence only — it does not depend on the item's
// static data being cached. Returns `false` for empty slots and invalid
// bag/slot indices.
#include "Game.h"
#include "item/Location.h"
namespace Container::HasItem {
namespace {
int __fastcall Script_C_Container_HasContainerItem(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: C_Container.HasContainerItem(bagIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
const bool hasItem = Item::Location::ResolveBag(L, bagID, slotIndex) != nullptr;
Game::Lua::PushBool(L, hasItem);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
&Script_C_Container_HasContainerItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::HasItem
+245
View File
@@ -36,10 +36,24 @@
// Field offsets verified against the binary (rank read by the engine's
// classification helper as `[block+0x20]`) and real creaturecache.wdb
// rows. See Offsets.h `OFF_CREATURE_*`.
//
// This file also hosts the two static-DBC lookups on the same table:
//
// C_CreatureInfo.GetRaceInfo(raceID) -> { raceName, clientFileString, raceID }
// C_CreatureInfo.GetClassInfo(classID) -> { className, classFile, classID }
//
// retail backports over `ChrRaces.dbc` / `ChrClasses.dbc`. Unlike the
// creature cache these DBCs are always loaded, localized, and synchronous —
// so a by-id lookup works for any race/class id at any time, with no WDB
// cold-cache problem. The file-string columns (`clientFileString` /
// `classFile`) are the same non-localized tokens `UnitRace`/`UnitClass`
// return as their 2nd value ("NightElf", "WARRIOR"), so they round-trip
// with the unit APIs. See Offsets.h `OFF_CHRRACES_*` / `OFF_CHRCLASSES_*`.
#include "Game.h"
#include "Offsets.h"
#include "cache/QueryLoad.h"
#include "dbc/Lookup.h"
#include "event/Custom.h"
#include <cstdint>
@@ -122,11 +136,242 @@ int __fastcall Script_RequestLoadCreatureByID(void *L) {
return 1;
}
// `C_CreatureInfo.GetRaceInfo(raceID)` — RaceInfo table, or nil for a
// non-numeric / non-positive id or one with no ChrRaces row.
int __fastcall Script_GetRaceInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *file =
(raceID > 0) ? DBC::StringField(Offsets::VAR_CHRRACES_RECORDS,
Offsets::VAR_CHRRACES_COUNT,
static_cast<uint32_t>(raceID),
Offsets::OFF_CHRRACES_FILENAME)
: nullptr;
if (file == nullptr)
return 0; // unknown id -> nil
const char *name = DBC::LocalizedField(Offsets::VAR_CHRRACES_RECORDS,
Offsets::VAR_CHRRACES_COUNT,
static_cast<uint32_t>(raceID),
Offsets::OFF_CHRRACES_NAMES);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "raceName", name); // NULL coerced to ""
Game::Lua::SetFieldString(L, "clientFileString", file);
Game::Lua::SetFieldNumber(L, "raceID", raceID);
return 1;
}
// `C_CreatureInfo.GetClassInfo(classID)` — ClassInfo table, or nil for a
// non-numeric / non-positive id or one with no ChrClasses row.
int __fastcall Script_GetClassInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int classID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *file =
(classID > 0) ? DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
Offsets::VAR_CHRCLASSES_COUNT,
static_cast<uint32_t>(classID),
Offsets::OFF_CHRCLASSES_FILENAME)
: nullptr;
if (file == nullptr)
return 0; // unknown id -> nil
const char *name = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
Offsets::VAR_CHRCLASSES_COUNT,
static_cast<uint32_t>(classID),
Offsets::OFF_CHRCLASSES_NAMES);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "className", name); // NULL coerced to ""
Game::Lua::SetFieldString(L, "classFile", file);
Game::Lua::SetFieldNumber(L, "classID", classID);
return 1;
}
// `C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)` — CreatureFamilyInfo
// table for a `CreatureFamily.dbc` row (1 = Wolf, 27 = Wind Serpent, …), or
// nil for a non-numeric / non-positive / unused id. `iconFile` is a texture
// path string here (retail exposes a numeric fileID; vanilla's DBC stores the
// path) — empty "" for families with no icon (e.g. warlock pets).
int __fastcall Script_GetCreatureFamilyInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int familyID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *name =
(familyID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
Offsets::VAR_CREATUREFAMILY_COUNT,
static_cast<uint32_t>(familyID),
Offsets::OFF_CREATUREFAMILY_NAMES)
: nullptr;
if (name == nullptr)
return 0; // unused / OOR id -> nil
const char *icon = DBC::StringField(Offsets::VAR_CREATUREFAMILY_RECORDS,
Offsets::VAR_CREATUREFAMILY_COUNT,
static_cast<uint32_t>(familyID),
Offsets::OFF_CREATUREFAMILY_ICON);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "id", familyID);
Game::Lua::SetFieldString(L, "name", name);
Game::Lua::SetFieldString(L, "iconFile", icon); // NULL/empty coerced to ""
return 1;
}
// `C_CreatureInfo.GetCreatureFamilyIDs()` — array of every populated
// `CreatureFamily.dbc` id (sparse: skips the unused rows). Round-trips with
// GetCreatureFamilyInfo for each element.
int __fastcall Script_GetCreatureFamilyIDs(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_CREATUREFAMILY_COUNT);
int n = 0;
for (int id = 1; id <= count; ++id) {
const char *name = DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
Offsets::VAR_CREATUREFAMILY_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_CREATUREFAMILY_NAMES);
if (name == nullptr)
continue; // unused row
++n;
Game::Lua::PushNumber(L, static_cast<double>(n));
Game::Lua::PushNumber(L, static_cast<double>(id));
Game::Lua::SetTable(L, -3);
}
return 1;
}
// `C_CreatureInfo.GetFactionInfo(raceID)` — faction-group info for a race:
// { name = localized ("Alliance"/"Horde"/…), groupTag = english }. Mirrors
// the player branch of `Script_UnitFactionGroup` (`0x00516630`): resolve the
// race's FactionTemplate group mask, then scan `FactionGroup.dbc` for the row
// whose bit the mask sets and whose localized name is non-empty (which is what
// skips the always-set "Player" / the nameless "Monster" rows and lands on
// Alliance / Horde). nil for a non-numeric / non-positive / unknown race or a
// race with no named group. See Offsets.h for the full chain.
int __fastcall Script_GetFactionInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (raceID <= 0)
return 0;
const uint8_t *crRec = DBC::Record(Offsets::VAR_CHRRACES_RECORDS,
Offsets::VAR_CHRRACES_COUNT,
static_cast<uint32_t>(raceID));
if (crRec == nullptr)
return 0; // unknown race
const int ftId = *reinterpret_cast<const int *>(
crRec + Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
if (ftId <= 0)
return 0;
const uint8_t *ftRec = DBC::Record(Offsets::VAR_FACTIONTEMPLATE_RECORDS,
Offsets::VAR_FACTIONTEMPLATE_COUNT,
static_cast<uint32_t>(ftId));
if (ftRec == nullptr)
return 0;
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
ftRec + Offsets::OFF_FACTIONTEMPLATE_GROUP_MASK);
// FactionGroup is stored contiguously (records-base + i*stride), not as a
// pointer array — iterate by hand rather than via DBC::Record.
auto *fgBase = *reinterpret_cast<const uint8_t *const *>(
Offsets::VAR_FACTIONGROUP_RECORDS);
const int fgCount =
*reinterpret_cast<const int *>(Offsets::VAR_FACTIONGROUP_COUNT);
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
if (fgBase == nullptr)
return 0;
for (int i = 0; i < fgCount; ++i) {
const uint8_t *row = fgBase + i * Offsets::FACTIONGROUP_STRIDE;
const int bit = *reinterpret_cast<const int *>(
row + Offsets::OFF_FACTIONGROUP_BIT);
if ((mask & (1u << (bit & 0x1F))) == 0)
continue;
const char *loc = *reinterpret_cast<const char *const *>(
row + Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
if (loc == nullptr || *loc == '\0')
continue; // "Player" / "Monster" have empty localized names
const char *eng = *reinterpret_cast<const char *const *>(
row + Offsets::OFF_FACTIONGROUP_ENGLISH);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "name", loc);
Game::Lua::SetFieldString(L, "groupTag", eng);
return 1;
}
return 0; // no named group -> nil
}
// `C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)` — CreatureTypeInfo
// table for a `CreatureType.dbc` row (1 = Beast … 11 = Totem), or nil for a
// non-numeric / non-positive / unused id.
int __fastcall Script_GetCreatureTypeInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int typeID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *name =
(typeID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
Offsets::VAR_CREATURETYPE_COUNT,
static_cast<uint32_t>(typeID),
Offsets::OFF_CREATURETYPE_NAMES)
: nullptr;
if (name == nullptr)
return 0; // unused / OOR id -> nil
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "id", typeID);
Game::Lua::SetFieldString(L, "name", name);
return 1;
}
// `C_CreatureInfo.GetCreatureTypeIDs()` — array of every populated
// `CreatureType.dbc` id (contiguous 1..N in vanilla). Round-trips with
// GetCreatureTypeInfo for each element.
int __fastcall Script_GetCreatureTypeIDs(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_CREATURETYPE_COUNT);
int n = 0;
for (int id = 1; id <= count; ++id) {
const char *name = DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
Offsets::VAR_CREATURETYPE_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_CREATURETYPE_NAMES);
if (name == nullptr)
continue;
++n;
Game::Lua::PushNumber(L, static_cast<double>(n));
Game::Lua::PushNumber(L, static_cast<double>(id));
Game::Lua::SetTable(L, -3);
}
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureInfoByID",
&Script_GetCreatureInfoByID);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "RequestLoadCreatureByID",
&Script_RequestLoadCreatureByID);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetRaceInfo",
&Script_GetRaceInfo);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetClassInfo",
&Script_GetClassInfo);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyInfo",
&Script_GetCreatureFamilyInfo);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyIDs",
&Script_GetCreatureFamilyIDs);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetFactionInfo",
&Script_GetFactionInfo);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeInfo",
&Script_GetCreatureTypeInfo);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeIDs",
&Script_GetCreatureTypeIDs);
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
}
+3 -11
View File
@@ -18,6 +18,7 @@
#include "event/Custom.h"
#include "item/ID.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include <cstdint>
#include <cstring>
@@ -31,24 +32,15 @@ std::string g_path;
bool g_loaded = false;
bool g_ignoredForSave[SLOT_COUNT] = {};
void *ResolvePlayerInvMgr() {
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(fn("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
// Reads the GUID currently in paperdoll slot `slot1Based` (1..19) by
// indexing the player invMgr's flat GUID array. Slot 1 (HEAD) lives
// at linear index 0; the equipment paperdoll occupies indices 0..18.
// Returns 0 for empty slots or unresolved player.
uint64_t ReadEquippedGUID(int slot1Based) {
auto *invMgr = static_cast<uint8_t *>(ResolvePlayerInvMgr());
const uint8_t *invMgr = Unit::Identity::PlayerInventoryManager();
if (invMgr == nullptr)
return 0;
auto *guids = *reinterpret_cast<uint64_t **>(
auto *guids = *reinterpret_cast<uint64_t *const *>(
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
if (guids == nullptr)
return 0;
+5 -30
View File
@@ -15,6 +15,7 @@
#include "Offsets.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -41,19 +42,6 @@ constexpr int LINEAR_BAG_SLOT_FIRST = 19; // bag1 → linear 19
constexpr int LINEAR_BACKPACK_FIRST = 23;
constexpr int LINEAR_BACKPACK_LAST = 38;
void *ResolvePlayer() {
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
return fn("player");
}
uint8_t *ResolvePlayerInvMgr() {
auto *player = static_cast<uint8_t *>(ResolvePlayer());
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
uint64_t *InvMgrGuidArray(const uint8_t *invMgr) {
if (invMgr == nullptr)
return nullptr;
@@ -79,20 +67,6 @@ uint8_t *ResolveByGUID(int type, uint64_t guid) {
0x172));
}
// CGContainer exposes its own `CContainerInventory *` via vtable
// `+0x10` — same dispatch `GetItemCount`'s bank walk uses. The
// returned invMgr has the standard shape: slot count at +0x00,
// flat GUID array at +0x04.
uint8_t *BagInvMgr(uint8_t *bag) {
if (bag == nullptr)
return nullptr;
auto *vtable = *reinterpret_cast<uint8_t **>(bag);
using GetInvMgr_t = void *(__thiscall *)(void *);
auto getInvMgr = reinterpret_cast<GetInvMgr_t>(
*reinterpret_cast<uintptr_t *>(vtable + 0x10));
return static_cast<uint8_t *>(getInvMgr(bag));
}
// Walks one bag's contents for a matching GUID. Returns the 1-based
// slot index of the match, or 0 if not found / bag unresolvable.
// Works for both player bags (linear 19..22 of player invMgr) and
@@ -102,7 +76,8 @@ int WalkBagContents(uint64_t bagGuid, uint64_t targetGuid) {
auto *bag = ResolveByGUID(Offsets::OBJ_TYPE_CONTAINER, bagGuid);
if (bag == nullptr)
return 0;
auto *bagInvMgr = BagInvMgr(bag);
auto *bagInvMgr =
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
if (bagInvMgr == nullptr)
return 0;
const int count = InvMgrSlotCount(bagInvMgr);
@@ -122,7 +97,7 @@ int FindGUID(uint64_t targetGuid) {
if (targetGuid == GUID_EMPTY || targetGuid == GUID_IGNORED)
return 0;
auto *invMgr = ResolvePlayerInvMgr();
auto *invMgr = Unit::Identity::PlayerInventoryManager();
auto *guids = InvMgrGuidArray(invMgr);
if (guids == nullptr)
return 0;
@@ -180,7 +155,7 @@ const uint8_t *ResolveItemByGUID(uint64_t guid) {
void SnapshotPaperdoll(uint64_t *out) {
for (int i = 0; i < SLOT_COUNT; ++i)
out[i] = 0;
auto *invMgr = ResolvePlayerInvMgr();
auto *invMgr = Unit::Identity::PlayerInventoryManager();
auto *guids = InvMgrGuidArray(invMgr);
if (guids == nullptr)
return;
+68 -19
View File
@@ -63,6 +63,14 @@ const char *const kKeyName[6] = {
"LSHIFT", "RSHIFT", "LCTRL", "RCTRL", "LALT", "RALT",
};
// L/R-specific virtual keys, one per bit, for the focus-regain resync.
// `GetAsyncKeyState` accepts the L/R-distinguishing VKs directly (unlike
// the keyboard-message path, where `VK_SHIFT` arrives merged and has to be
// disambiguated via the scancode).
const int kBitVk[6] = {
VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU,
};
constexpr const char *kModifierStateChanged = "MODIFIER_STATE_CHANGED";
// Static-init reservation — runs before DllMain. The
@@ -72,6 +80,7 @@ const Event::Custom::AutoReserve _reserveModifierStateChanged{kModifierStateChan
uint32_t g_modifierMask = 0;
HHOOK g_msgHook = nullptr;
HHOOK g_wndProcHook = nullptr;
void PushDown(void *L, bool down) {
if (down)
@@ -80,6 +89,28 @@ void PushDown(void *L, bool down) {
Game::Lua::PushNil(L);
}
// Apply a single modifier bit transition: update the cached mask and, on a
// genuine change, fire `MODIFIER_STATE_CHANGED`. Both the keyboard-message
// path and the focus-regain resync funnel through here so the "only
// transitions fire" invariant holds regardless of source.
void SetBit(int bitIdx, bool down) {
const uint32_t bit = 1u << bitIdx;
const bool wasDown = (g_modifierMask & bit) != 0;
if (down == wasDown)
return;
if (down) g_modifierMask |= bit;
else g_modifierMask &= ~bit;
// Slot was assigned when the engine populated its event table; our
// hook on `RebuildEventTable` recorded the index. Lookup at fire
// time rather than caching because `/reload` re-runs the rebuild
// and the index may shift.
const int slot = Event::Custom::Lookup(kModifierStateChanged);
if (slot >= 0)
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(down));
}
template <int BitIdx>
int __fastcall Script_IsKeyDown(void *L) {
const bool down = (g_modifierMask & (1u << BitIdx)) != 0;
@@ -131,23 +162,22 @@ void ProcessKeyMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
const int bitIdx = VkToBitIdx(wParam, lParam);
if (bitIdx < 0)
return;
const uint32_t bit = 1u << bitIdx;
const bool wasDown = (g_modifierMask & bit) != 0;
const bool nowDown = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
if (nowDown == wasDown)
return;
SetBit(bitIdx, nowDown);
}
if (nowDown) g_modifierMask |= bit;
else g_modifierMask &= ~bit;
// Slot was assigned when the engine populated its event table; our
// hook on `RebuildEventTable` recorded the index. Lookup at fire
// time rather than caching because `/reload` re-runs the rebuild
// and the index may shift.
const int slot = Event::Custom::Lookup(kModifierStateChanged);
if (slot >= 0)
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(nowDown));
// Reconcile the cached mask with the true physical key state. Needed on
// focus regain: keyboard messages only arrive while WoW has focus, so a
// modifier released while WoW was in the background is never seen by the
// `WH_GETMESSAGE` hook and the bit stays stuck down (and the release event
// never fires). `GetAsyncKeyState` reads global async keyboard state
// regardless of focus, so polling it on activation recovers the missed
// transitions — `SetBit` fires `MODIFIER_STATE_CHANGED` for each drift.
void ResyncFromPhysicalState() {
for (int b = 0; b < 6; ++b) {
const bool down = (GetAsyncKeyState(kBitVk[b]) & 0x8000) != 0;
SetBit(b, down);
}
}
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
@@ -164,11 +194,30 @@ LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
return CallNextHookEx(nullptr, code, wParam, lParam);
}
// `WM_ACTIVATEAPP` is *sent* (via the window manager), not *posted* to the
// queue, so `WH_GETMESSAGE` never sees it — a `WH_CALLWNDPROC` hook catches
// sent messages before the target WNDPROC runs. On app activation
// (`wParam != FALSE`) we resync from physical key state to recover any
// modifier released while WoW was in the background. Like the message hook,
// this is thread-level (not per-`HWND`), so it survives window recreation.
LRESULT CALLBACK CallWndProcHook(int code, WPARAM wParam, LPARAM lParam) {
if (code == HC_ACTION) {
const CWPSTRUCT *cwp = reinterpret_cast<const CWPSTRUCT *>(lParam);
if (cwp->message == WM_ACTIVATEAPP && cwp->wParam != FALSE)
ResyncFromPhysicalState();
}
return CallNextHookEx(nullptr, code, wParam, lParam);
}
void InstallHook() {
if (g_msgHook != nullptr)
return;
g_msgHook = SetWindowsHookExA(WH_GETMESSAGE, &GetMsgHook, nullptr,
GetCurrentThreadId());
if (g_msgHook == nullptr) {
g_msgHook = SetWindowsHookExA(WH_GETMESSAGE, &GetMsgHook, nullptr,
GetCurrentThreadId());
}
if (g_wndProcHook == nullptr) {
g_wndProcHook = SetWindowsHookExA(WH_CALLWNDPROC, &CallWndProcHook,
nullptr, GetCurrentThreadId());
}
}
void RegisterLuaFunctions() {
+5 -17
View File
@@ -55,6 +55,7 @@
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include <algorithm>
#include <cstdint>
@@ -153,21 +154,11 @@ ItemInfo ReadItemInfo(const uint8_t *cgItem) {
// Bank-walk helpers — bypass the bank-window gate on `GetItemBySlot`
// by reading the player's invMgr GUID array directly. Same shape as
// `C_Item.GetItemCount`'s bank path.
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
const char *debugName,
uint32_t guidLo,
uint32_t guidHi,
int priority);
using GetBagInvMgr_t = void *(__thiscall *)(void *bag);
const uint8_t *PlayerInvMgr() {
auto resolveUnit = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(resolveUnit("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
if (guid == 0)
@@ -229,7 +220,7 @@ void WalkGuidArrayRange(const uint8_t *invMgr, int firstSlot, int lastSlot,
// player invMgr hold the bank bags themselves; each bag has its own
// invMgr accessed via the bag CGContainer's vtable+0x10 method).
void WalkBankBags(std::vector<Candidate> &candidates) {
auto *playerInvMgr = PlayerInvMgr();
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
if (playerInvMgr == nullptr)
return;
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
@@ -242,11 +233,8 @@ void WalkBankBags(std::vector<Candidate> &candidates) {
playerGuidArray[slot]);
if (bag == nullptr)
continue;
auto *vtable = *reinterpret_cast<const uint8_t *const *>(bag);
auto getInvMgr = reinterpret_cast<GetBagInvMgr_t>(
*reinterpret_cast<const uintptr_t *>(vtable + 0x10));
auto *bagInvMgr = static_cast<const uint8_t *>(
getInvMgr(const_cast<uint8_t *>(bag)));
auto *bagInvMgr =
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
if (bagInvMgr == nullptr)
continue;
const int bagSlots =
@@ -295,7 +283,7 @@ static int __fastcall Script_GetAverageItemLevel(void *L) {
// session — GUIDs are populated from server data at login. Main
// bank first (linear slots 39..62 in the player invMgr), then
// each equipped bank bag's contents via the per-bag CGContainer.
const uint8_t *playerInvMgr = PlayerInvMgr();
const uint8_t *playerInvMgr = Unit::Identity::PlayerInventoryManager();
WalkGuidArrayRange(playerInvMgr,
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
+5 -19
View File
@@ -16,6 +16,7 @@
#include "item/Arg.h"
#include "item/ID.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -94,22 +95,12 @@ int CountInBag(void *L, int bagID, int targetItemID, bool includeUses) {
// object resolver `FUN_OBJECT_RESOLVE_BY_GUID` (the same function
// `GetItemBySlot` would call internally if the gate let us through).
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
const char *debugName,
uint32_t guidLo,
uint32_t guidHi,
int priority);
const uint8_t *PlayerInvMgr() {
auto ResolveUnitToken =
reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(ResolveUnitToken("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
if (guid == 0)
return nullptr;
@@ -155,7 +146,7 @@ int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
// Layout: bag invMgr+0x00 = max slot count (the bag's size); bag
// invMgr+0x04 = its GUID array. Same shape as the player invMgr.
int CountInBankBags(int targetItemID, bool includeUses) {
auto *playerInvMgr = PlayerInvMgr();
auto *playerInvMgr = Unit::Identity::PlayerInventoryManager();
if (playerInvMgr == nullptr)
return 0;
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
@@ -164,19 +155,14 @@ int CountInBankBags(int targetItemID, bool includeUses) {
return 0;
int total = 0;
using GetBagInvMgr_t = void *(__thiscall *)(void *bag);
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; slot++) {
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER,
playerGuidArray[slot]);
if (bag == nullptr)
continue;
// vtable +0x10 returns the bag's own invMgr pointer
auto *vtable = *reinterpret_cast<const uint8_t *const *>(bag);
auto getInvMgr = reinterpret_cast<GetBagInvMgr_t>(
*reinterpret_cast<const uintptr_t *>(vtable + 0x10));
auto *bagInvMgr = static_cast<const uint8_t *>(
getInvMgr(const_cast<uint8_t *>(bag)));
auto *bagInvMgr =
static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
if (bagInvMgr == nullptr)
continue;
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
@@ -251,7 +237,7 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
// Direct GUID-array reads — bypass the bank gate so this works
// without the bank window having ever been opened in the
// session.
total += CountInGuidArray(PlayerInvMgr(),
total += CountInGuidArray(Unit::Identity::PlayerInventoryManager(),
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
itemID, includeUses);
+30 -11
View File
@@ -16,6 +16,7 @@
#include "../Game.h"
#include "../Offsets.h"
#include "../guid/Guid.h"
#include "../unit/Identity.h"
#include "Arg.h"
#include "ID.h"
#include "Link.h"
@@ -29,20 +30,10 @@ namespace {
using GetItemBySlot_t = void *(__thiscall *)(void *thisInvMgr, int slot);
using PackBagSlot_t = int(__fastcall *)(void *L, void **outInvMgr, int *outLinearSlot,
int *outUnused);
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
void *ResolveActivePlayerInvMgr() {
auto ResolveUnitToken =
reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(ResolveUnitToken("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
// Reads `loc.fieldName` and returns it as an int. Returns false via the
// boolean result if the field is missing or non-numeric. Always leaves the
// Lua stack as it found it.
@@ -84,7 +75,7 @@ uint64_t ReadCGItemGUID(const uint8_t *item) {
} // namespace
const uint8_t *ResolveEquipmentSlot(int slot1Based) {
void *invMgr = ResolveActivePlayerInvMgr();
void *invMgr = const_cast<uint8_t *>(Unit::Identity::PlayerInventoryManager());
if (invMgr == nullptr)
return nullptr;
// GetItemBySlot expects 0-based linearized slot indices. The built-in
@@ -95,6 +86,34 @@ const uint8_t *ResolveEquipmentSlot(int slot1Based) {
return static_cast<const uint8_t *>(GetItemBySlot(invMgr, slot1Based - 1));
}
void *ContainerInventory(const uint8_t *container) {
if (container == nullptr)
return nullptr;
auto *vtable = *reinterpret_cast<const uint8_t *const *>(container);
using GetInventory_t = void *(__thiscall *)(void *container);
auto getInventory = *reinterpret_cast<const GetInventory_t *>(
vtable + Offsets::OFF_CONTAINER_GET_INVENTORY);
return getInventory(const_cast<uint8_t *>(container));
}
void *EquippedBagInventory(int bagID) {
if (bagID < 1 || bagID > 4)
return nullptr;
using GetBagGuid_t = uint64_t(__fastcall *)(uint32_t bagIndex0);
auto getBagGuid =
reinterpret_cast<GetBagGuid_t>(Offsets::FUN_GET_EQUIPPED_BAG_GUID);
const uint64_t bagGuid = getBagGuid(static_cast<uint32_t>(bagID - 1));
if (bagGuid == 0)
return nullptr; // no bag equipped in that slot
using ResolveByGUID_t = void *(__fastcall *)(int, const char *, uint32_t,
uint32_t, int);
auto resolve = reinterpret_cast<ResolveByGUID_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
auto *container = static_cast<const uint8_t *>(
resolve(Offsets::OBJ_TYPE_CONTAINER, "ItemMgr", static_cast<uint32_t>(bagGuid),
static_cast<uint32_t>(bagGuid >> 32), 0x172));
return ContainerInventory(container);
}
// Mirrors `Item::Bag::ResolveBagInfo`'s slot-count derivation: backpack
// is fixed at 16, equipped bags read `m_containerSlots` off the cache
// record. Returns 0 for empty bag slots or out-of-range bagIDs.
+22
View File
@@ -69,6 +69,28 @@ const uint8_t *ResolveEquipmentSlot(int slot1Based);
// guaranteed cached by the engine's bag-sync flow at login.
int GetBagSlotCount(int bagID);
// Given a `CGContainer*` (an equipped bag or bank bag, resolved by GUID via
// `FUN_OBJECT_RESOLVE_BY_GUID` with `OBJ_TYPE_CONTAINER`), returns its own
// inventory-manager object via the container's vtable method at
// `+OFF_CONTAINER_GET_INVENTORY`. The returned object has the same layout as
// the player inventory manager: slot count at `+0x00`, flat GUID array at
// `+OFF_INVMGR_GUID_ARRAY`. Null-safe (null container → null). Pure C — no
// Lua stack, safe from any context. Consolidates the vtable dispatch that was
// hand-rolled across the bag/bank walks in `item/Count`, `item/AverageLevel`,
// `equipmentset/Locations`, and `item/NewItems`.
void *ContainerInventory(const uint8_t *container);
// Returns the inventory object of the equipped bag in player bag slot `bagID`
// (1..4): its container GUID via `FUN_GET_EQUIPPED_BAG_GUID`, resolved to the
// CGContainer, then its inventory via `ContainerInventory`. The result has the
// player-invMgr shape (slot count at `+0x00`, GUID array at
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a 0-based
// slot. Returns null for `bagID` outside 1..4 or when no bag is equipped
// there. Pure C — no Lua stack. (The backpack, `bagID` 0, has no container
// object; index it directly off the player inventory manager at
// `BACKPACK_LINEAR_BASE + slot - 1`.)
void *EquippedBagInventory(int bagID);
// GUID-walk result. `equipmentSlotIndex != 0` means the item was
// found in a character-pane equipment slot; otherwise it's in
// `bagID`/`slotIndex`. `item` always points to the found CGItem
+299
View File
@@ -0,0 +1,299 @@
// 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_NewItems` — the modern "new item glow" bookkeeping system. Retail
// flags items freshly added to your bags as "new" (the sparkle/flash on
// the bag button) and clears the flag once you've acknowledged them.
// Vanilla 1.12 never had the feature — no DBC, no server data, no engine
// bit; it's pure client-side bookkeeping over bag contents. So we add it,
// entirely in C++ the same way retail's client owns it — no addon Lua.
//
// Public surface (matches retail exactly):
// C_NewItems.IsNewItem(bagID, slotIndex) -> bool
// C_NewItems.RemoveNewItem(bagID, slotIndex)
// C_NewItems.ClearAll()
//
// Newness is keyed on each item's **instance GUID** (read off the CGItem),
// not on (bagID, slotIndex). That's what makes a flag survive the player
// rearranging a bag — exactly like retail, and impossible for a pure-Lua
// addon (vanilla exposes no per-instance GUID to Lua).
//
// The feed is event-driven, not polled: it hangs off `Bag::UpdateDelayed`,
// which co-hooks the engine's three `BAG_UPDATE` fire sites and signals once
// per frame in which a bag actually changed. On that signal we scan bags
// 0..4, diff the resident item GUIDs against the previous scan, flag anything
// newly acquired, and prune flags for items that left. `BAG_NEW_ITEMS_UPDATED`
// (no payload — matching retail) fires whenever the new-item set changes. So
// nothing runs on quiet frames.
//
// The one exception is establishing the login baseline: a lightweight
// WorldTick handler waits ~1.5s after the player is first resolvable (long
// enough for the initial bag population to finish), takes a single scan as the
// baseline, and then goes idle — items already owned at login are therefore
// never flagged. It re-arms on player-GUID change (relog / character switch).
// Bank and equipment are out of scope, matching retail.
//
// Bag enumeration is pure C++: it replicates what `PackBagSlot` does
// internally (see Offsets.h). The backpack is indexed straight off the
// player inventory manager; each equipped bag 1..4 is resolved to its
// container object and inventory via the same GUID getter + vtable call the
// engine uses, then walked with `GetItemBySlot`. None of the Lua-stack-bound
// helpers are touched, so the scan is safe from the frame-loop tick.
#include "Game.h"
#include "Offsets.h"
#include "bag/UpdateDelayed.h"
#include "event/Custom.h"
#include "item/Location.h"
#include "tick/WorldTick.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Item::NewItems {
namespace {
constexpr const char *kEvtNewItems = "BAG_NEW_ITEMS_UPDATED";
const Event::Custom::AutoReserve _r{kEvtNewItems};
// Bags cap at backpack (16) + four equipped bags; even the largest Turtle
// bags stay well under this. Overflow silently drops — a flagged-new item
// past the cap just won't glow, never a crash.
constexpr int kMaxItems = 256;
// Delay after the player is first resolvable before the baseline is taken,
// giving the initial bag population time to finish. Wall-clock (via the
// engine ms counter) so it's independent of frame rate.
constexpr uint32_t kSettleMs = 1500;
uint64_t g_seen[kMaxItems]; // all bag-resident GUIDs at the last scan
int g_seenCount = 0;
uint64_t g_new[kMaxItems]; // subset currently flagged "new"
int g_newCount = 0;
bool g_seeded = false; // true once the login baseline has been taken
uint64_t g_playerGuid = 0; // re-arm the baseline when this changes (relog)
uint32_t g_loginMs = 0; // engine ms at which the current player resolved
uint32_t NowMs() {
using TickCount_t = uint32_t(__fastcall *)();
return reinterpret_cast<TickCount_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
}
bool Contains(const uint64_t *arr, int count, uint64_t guid) {
for (int i = 0; i < count; ++i)
if (arr[i] == guid)
return true;
return false;
}
bool AddUnique(uint64_t *arr, int *count, uint64_t guid) {
if (Contains(arr, *count, guid) || *count >= kMaxItems)
return false;
arr[(*count)++] = guid;
return true;
}
bool RemoveFrom(uint64_t *arr, int *count, uint64_t guid) {
for (int i = 0; i < *count; ++i) {
if (arr[i] == guid) {
arr[i] = arr[--(*count)];
return true;
}
}
return false;
}
uint64_t ItemGUID(const uint8_t *item) {
if (item == nullptr)
return 0;
auto *instance = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
if (instance == nullptr)
return 0;
return *reinterpret_cast<const uint64_t *>(
instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
}
// --- pure-C++ bag enumeration (no Lua stack) -----------------------------
using GetItemBySlot_t = void *(__thiscall *)(void *inventory, int slot0Based);
// Collects the instance GUIDs of every item in bags 0..4 into `out`.
// Returns the count. Pure C++ — safe to call off the frame-loop tick.
int ScanBags(const uint8_t *player, uint64_t *out) {
auto getItemBySlot =
reinterpret_cast<GetItemBySlot_t>(Offsets::FUN_ITEMMGR_GET_ITEM_BY_SLOT);
int count = 0;
// Backpack (bagID 0): indexed directly off the player inventory manager
// at linear slot (S-1) + 23. GetItemBySlot only reads the manager, so the
// const_cast to satisfy its `this` parameter is safe.
auto *invMgr = const_cast<uint8_t *>(player) + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
const int backpackSlots = Item::Location::GetBagSlotCount(0);
for (int slot = 1; slot <= backpackSlots; ++slot) {
auto *item = static_cast<const uint8_t *>(
getItemBySlot(invMgr, Offsets::BACKPACK_LINEAR_BASE + slot - 1));
const uint64_t guid = ItemGUID(item);
if (guid != 0)
AddUnique(out, &count, guid);
}
// Equipped bags 1..4: resolve each bag's inventory object, then walk it.
for (int bagID = 1; bagID <= 4; ++bagID) {
const int slots = Item::Location::GetBagSlotCount(bagID);
if (slots <= 0)
continue;
void *bagInv = Item::Location::EquippedBagInventory(bagID);
if (bagInv == nullptr)
continue;
for (int slot = 1; slot <= slots; ++slot) {
auto *item = static_cast<const uint8_t *>(getItemBySlot(bagInv, slot - 1));
const uint64_t guid = ItemGUID(item);
if (guid != 0)
AddUnique(out, &count, guid);
}
}
return count;
}
void FireChanged() {
Event::Custom::Fire(Event::Custom::Lookup(kEvtNewItems), "");
}
void CopyToSeen(const uint64_t *current, int count) {
for (int i = 0; i < count; ++i)
g_seen[i] = current[i];
g_seenCount = count;
}
// Login-baseline handler. Runs every frame but does no work once the
// baseline is taken — it only watches for (re)login and, after a short
// settle, takes the single authoritative baseline scan. Steady state is just
// a GUID read + `if (g_seeded) return`.
void OnTick() {
const uint64_t guid = Unit::Identity::PlayerGuid();
if (guid == 0)
return; // not in world yet
// Fresh login / character switch: drop all state and re-arm the settle.
if (guid != g_playerGuid) {
g_playerGuid = guid;
g_newCount = 0;
g_seenCount = 0;
g_seeded = false;
g_loginMs = NowMs();
}
if (g_seeded)
return; // baseline established; the bag-change signal drives the rest
if (NowMs() - g_loginMs < kSettleMs)
return; // let the initial bag population finish
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return; // not resolvable yet — retry next tick
// Everything currently in the bags becomes the baseline; nothing owned at
// login is "new". Any change up to this point is intentionally absorbed.
g_seenCount = ScanBags(player, g_seen);
g_newCount = 0;
g_seeded = true;
}
// Authoritative bag-change handler — fires only on frames where the engine
// actually changed a bag (via Bag::UpdateDelayed). Diffs the current bag set
// against the baseline, flags newly-acquired items, prunes departed ones.
void OnBagChanged() {
if (!g_seeded)
return; // pre-baseline changes are folded into the login baseline
const uint64_t guid = Unit::Identity::PlayerGuid();
if (guid == 0 || guid != g_playerGuid)
return; // mid-relog; OnTick re-arms the baseline
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return;
uint64_t current[kMaxItems];
const int currentCount = ScanBags(player, current);
bool changed = false;
// Drop flags for items no longer in the bags (used, sold, banked).
for (int i = 0; i < g_newCount;) {
if (!Contains(current, currentCount, g_new[i])) {
g_new[i] = g_new[--g_newCount];
changed = true;
} else {
++i;
}
}
// Flag anything not present at the previous scan.
for (int i = 0; i < currentCount; ++i) {
if (!Contains(g_seen, g_seenCount, current[i]))
changed |= AddUnique(g_new, &g_newCount, current[i]);
}
CopyToSeen(current, currentCount);
if (changed)
FireChanged();
}
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
const Bag::UpdateDelayed::AutoSubscribe _bagChange{&OnBagChanged};
// --- C_NewItems Lua surface ----------------------------------------------
// Resolves a `(bagID, slotIndex)` Lua-arg pair to the item's instance GUID,
// or 0 if the args are missing/non-numeric or the slot is empty. Uses the
// Lua-context bag resolver — fine here, these run as Lua C functions.
uint64_t SlotGUID(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2))
return 0;
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return ItemGUID(Item::Location::ResolveBag(L, bagID, slotIndex));
}
int __fastcall Script_IsNewItem(void *L) {
const uint64_t guid = SlotGUID(L);
Game::Lua::PushBool(L, guid != 0 && Contains(g_new, g_newCount, guid));
return 1;
}
int __fastcall Script_RemoveNewItem(void *L) {
const uint64_t guid = SlotGUID(L);
if (guid != 0 && RemoveFrom(g_new, &g_newCount, guid))
FireChanged();
return 0;
}
int __fastcall Script_ClearAll(void *L) {
(void)L;
if (g_newCount > 0) {
g_newCount = 0;
FireChanged();
}
return 0;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_NewItems", "IsNewItem", &Script_IsNewItem);
Game::Lua::RegisterTableFunction("C_NewItems", "RemoveNewItem", &Script_RemoveNewItem);
Game::Lua::RegisterTableFunction("C_NewItems", "ClearAll", &Script_ClearAll);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Item::NewItems
+8 -13
View File
@@ -16,6 +16,7 @@
#include "Game.h"
#include "Offsets.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -23,7 +24,6 @@ namespace Item::Swap {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using SwapFn_t = void(__thiscall *)(
void *player,
uint32_t srcItemLo, uint32_t srcItemHi,
@@ -33,11 +33,6 @@ using SwapFn_t = void(__thiscall *)(
uint32_t dstLinearSlot,
int flag);
void *ResolvePlayer() {
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
return fn("player");
}
// Reads the 64-bit GUID stored at instance-block offset 0 of any
// CGObject-derived pointer (CGItem, CGPlayer, CGContainer all use
// the same +0x8 → instance block → +0x00 GUID layout).
@@ -98,7 +93,7 @@ bool EncodeBagSlot(int bagID, int slotInBag,
if (slotInBag < 1)
return false;
if (bagID == 0) {
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
if (!ReadGuid(player, containerLo, containerHi))
@@ -119,7 +114,7 @@ bool EncodeBagSlot(int bagID, int slotInBag,
if (bagID == -1) {
if (slotInBag > BANK_MAIN_NUM_SLOTS)
return false;
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
if (!ReadGuid(player, containerLo, containerHi))
@@ -152,7 +147,7 @@ bool SendSwap(const void *srcItem,
if (srcItem == nullptr)
return false;
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
@@ -187,7 +182,7 @@ bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlo
if (srcPaperdollSlot < 1 || srcPaperdollSlot > 19)
return false;
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
uint32_t playerLo = 0, playerHi = 0;
@@ -205,7 +200,7 @@ bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotIn
if (cgItem == nullptr)
return false;
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
uint32_t playerLo = 0, playerHi = 0;
@@ -272,7 +267,7 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
if (count == srcStack)
return Containers(L, srcBag, srcSlot, dstBag, dstSlot);
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
@@ -308,7 +303,7 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot) {
if (srcItem == nullptr)
return false; // empty source slot
void *player = ResolvePlayer();
void *player = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
if (player == nullptr)
return false;
+251
View File
@@ -0,0 +1,251 @@
// 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/>.
#include "map/Area.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
namespace Map::Area {
int RowForAreaID(uint32_t areaID) {
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec != nullptr &&
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_WMA_AREA_ID) ==
areaID)
return id;
}
return -1;
}
int CurrentViewRow() {
const int continent =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
if (continent < 0)
return *reinterpret_cast<const int *>(
Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
const uint8_t *blob = *reinterpret_cast<const uint8_t *const *>(
Offsets::VAR_WORLDMAP_CONTINENT_DATA);
if (blob == nullptr)
return -1;
const uint8_t *entry = blob + continent * Offsets::WORLDMAP_CONTINENT_STRIDE;
const int zone =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
if (zone < 0)
return *reinterpret_cast<const int *>(entry + 0x04);
const int *zoneRows = *reinterpret_cast<const int *const *>(entry + 0x10);
return (zoneRows != nullptr) ? zoneRows[zone] : -1;
}
namespace {
double FloatField(const uint8_t *rec, int off) {
return *reinterpret_cast<const float *>(rec + off);
}
// Vanilla zone world-map canvas size (WorldMapDetailFrame); WorldMapOverlay
// hit rects live in this pixel space.
constexpr double kZoneCanvasW = 1002.0;
constexpr double kZoneCanvasH = 668.0;
// How deep inside its containing WorldMapOverlay hit rect the map-percent point
// (0..100) sits on WMA row `wmaRow` — the max, over that zone's overlays whose
// hit rect encloses the point, of the normalized margin to the nearest overlay
// edge (0 at the edge .. 0.5 at center). Returns -1 when the point is on none
// of the zone's overlays.
//
// This is the runtime analog of an ADT area-grid lookup. WorldMapArea rects are
// loose overlapping boxes; the overlay hit rects trace the real landmass, so
// "which zone is the point drawn on" disambiguates them (Un'Goro's crater vs
// Thousand Needles' empty overlap). But hit rects are themselves rectangles
// that overlap at borders, so the DEPTH matters: a border node deep inside its
// own zone's overlay beats one merely clipping a neighbor's overlay edge
// (Nijel's Point is deep in Desolace's "Nijel's Point" overlay but only nicks
// the edge of Stonetalon's Charred Vale box).
double OverlayMargin(int wmaRow, double mapXpct, double mapYpct) {
const double cx = mapXpct / 100.0 * kZoneCanvasW;
const double cy = mapYpct / 100.0 * kZoneCanvasH;
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
double best = -1.0;
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_OVERLAY_RECORDS,
Offsets::VAR_WORLDMAP_OVERLAY_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr ||
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
wmaRow)
continue;
const int hl = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_LEFT);
const int hr = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_RIGHT);
const int ht = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_TOP);
const int hb = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_HITRECT_BOTTOM);
const double sw = hr - hl, sh = hb - ht;
if (sw <= 0.0 || sh <= 0.0)
continue; // no / degenerate hit rect
if (cx < hl || cx > hr || cy < ht || cy > hb)
continue; // point outside this overlay
const double mX = (cx - hl < hr - cx ? cx - hl : hr - cx) / sw;
const double mY = (cy - ht < hb - cy ? cy - ht : hb - cy) / sh;
const double m = mX < mY ? mX : mY;
if (m > best)
best = m;
}
return best;
}
} // namespace
bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
double *outMapY) {
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
// Two candidate tracks over the zones whose rect contains the point:
// land* — the point is on the zone's drawn landmass (an overlay hit rect
// encloses it): authoritative, wins whenever any zone qualifies.
// Scored by how DEEP inside the overlay the point sits, so a
// border node deep in its own zone's overlay beats one nicking a
// neighbor's overlay edge.
// box* — bounding-box only: the fallback when the point is on no zone's
// overlay (map gaps / overlay-less custom zones), scored by the
// most-interior zone-rect margin.
bool haveLand = false, haveBox = false;
double landScore = -1.0, boxMargin = 0.0;
int landArea = 0, boxArea = 0;
double landX = 0.0, landY = 0.0, boxX = 0.0, boxY = 0.0;
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
continue;
const int areaID =
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
if (areaID == 0)
continue; // continent-spanning row, not a zone
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
const double spanX = top - bottom; // > 0
const double spanY = left - right; // > 0
if (spanX <= 0.0 || spanY <= 0.0)
continue; // degenerate / unmapped zone
if (x < bottom || x > top || y < right || y > left)
continue; // point outside this zone's rect
// mapX = horizontal (world Y), mapY = vertical (world X).
const double mapX = (left - y) / spanY * 100.0;
const double mapY = (top - x) / spanX * 100.0;
const double marginX = (x - bottom < top - x ? x - bottom : top - x) / spanX;
const double marginY = (y - right < left - y ? y - right : left - y) / spanY;
const double margin = marginX < marginY ? marginX : marginY;
if (!haveBox || margin > boxMargin) {
haveBox = true;
boxMargin = margin;
boxArea = areaID;
boxX = mapX;
boxY = mapY;
}
const double overlayMargin = OverlayMargin(id, mapX, mapY);
if (overlayMargin >= 0.0 && (!haveLand || overlayMargin > landScore)) {
haveLand = true;
landScore = overlayMargin;
landArea = areaID;
landX = mapX;
landY = mapY;
}
}
if (haveLand) {
*outAreaID = landArea;
*outMapX = landX;
*outMapY = landY;
return true;
}
if (haveBox) {
*outAreaID = boxArea;
*outMapX = boxX;
*outMapY = boxY;
return true;
}
return false;
}
bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMapY) {
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != areaID)
continue;
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
const double spanX = top - bottom;
const double spanY = left - right;
if (spanX <= 0.0 || spanY <= 0.0)
continue; // degenerate row — keep scanning for a usable one
if (x < bottom || x > top || y < right || y > left)
return false; // point outside this zone's rect — not in this zone
*outMapX = (left - y) / spanY * 100.0; // horizontal (world Y)
*outMapY = (top - x) / spanX * 100.0; // vertical (world X)
return true;
}
return false;
}
bool ContinentPercent(int mapID, float x, float y, double *outPx, double *outPy) {
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
continue;
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID) != 0)
continue; // want the continent-spanning row
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
const double spanX = top - bottom;
const double spanY = left - right;
if (spanX <= 0.0 || spanY <= 0.0)
continue; // stray zero-rect continent row (e.g. "World") — skip
*outPx = (left - y) / spanY; // horizontal (world Y)
*outPy = (top - x) / spanX; // vertical (world X)
return true;
}
return false;
}
} // namespace Map::Area
+79
View File
@@ -0,0 +1,79 @@
// 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/>.
#pragma once
#include <cstdint>
// `Map::Area` — shared `WorldMapArea.dbc` row resolution. A WorldMapArea
// row is the client's per-zone map descriptor (directory name + the
// world-coordinate rect used for world↔map transforms); every C_Map.*
// reader that keys off "which zone" resolves a row first. Both
// `Map::Overlays` (GetMapOverlays) and `Map::WorldSize` (GetMapWorldSize)
// go through here so the two ways of naming a zone — an AreaTable areaID
// argument, or the world map's current continent/zone view — live in one
// place.
namespace Map::Area {
// WorldMapArea row for an AreaTable `areaID` (the stable zone identity
// `C_Map.GetBestMapForUnit` returns). Linear walk of the ~175-row table.
// Returns -1 when no row carries that areaID.
int RowForAreaID(uint32_t areaID);
// WorldMapArea row for the world map's currently *viewed* zone — the same
// continent/zone selection the engine's map-name resolver (FUN_004a6cf0,
// behind GetMapInfo) performs: continent index → per-continent blob, zone
// index (-1 = whole continent) → that continent's zone-row array; no
// continent selected falls back to the default-row global. Returns -1 when
// the continent blob isn't resident.
int CurrentViewRow();
// Resolves world point (x, y) on continent `mapID` to a zone: its AreaTable
// areaID and a 0..100 zone-relative percent (`mapX` horizontal off world Y,
// `mapY` vertical off world X — the WoW/pfQuest convention).
//
// WorldMapArea rects are loose, overlapping bounding boxes, so a point near a
// zone edge falls inside several. Resolution therefore prefers the zone whose
// **drawn landmass** (WorldMapOverlay hit rect) the point actually sits on —
// the runtime analog of an ADT area-grid lookup (Un'Goro's crater vs Thousand
// Needles' empty overlap). Overlay hit rects are themselves rectangles that
// overlap at borders, so among landmass matches the winner is the zone the
// point is DEEPEST inside an overlay of (Nijel's Point is deep in Desolace's
// own overlay but only clips the edge of Stonetalon's Charred Vale box). If the
// point is on no zone's overlay, falls back to the most-interior containing
// rect. Returns false when no zone rect encloses the point (open sea,
// degenerate rect, an instance with no zone row). Shared by GetAreaTriggerInfo
// and the taxi-node zone resolve.
bool ZonePercent(int mapID, float x, float y, int *outAreaID, double *outMapX,
double *outMapY);
// Projects world (x, y) into the WorldMapArea rect of a SPECIFIC AreaTable
// zone (`areaID`), filling the 0..100 zone-relative percent (`mapX` horizontal
// off world Y, `mapY` vertical off world X). Returns false when that zone has
// no usable WorldMapArea row OR the point lies outside its rect. Used when the
// zone is already known by other means (a taxi node's name) and only the
// in-zone position is needed — bypassing the containment / landmass search of
// ZonePercent. The containment check lets a caller test candidate zones (a
// node's city vs its outdoor region) and take the first that actually holds
// the point.
bool PercentInZone(int areaID, float x, float y, double *outMapX, double *outMapY);
// Projects world (x, y) on `mapID` to 0..1 CONTINENT-map coordinates using
// the continent's WorldMapArea row (the areaID == 0 row with a non-degenerate
// rect — some maps carry a stray zero-rect continent row that is skipped).
// `outPx` is horizontal (off world Y), `outPy` vertical (off world X).
// Returns false when the map has no usable continent row. This is the
// projection retail's TaxiNodeInfo.position uses.
bool ContinentPercent(int mapID, float x, float y, double *outPx, double *outPy);
} // namespace Map::Area
+169
View File
@@ -0,0 +1,169 @@
// 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_Map.GetAreaTriggerInfo(triggerID)` and `C_Map.GetAreaTriggers([mapID])`
// — expose `AreaTrigger.dbc`, the client's static trigger-volume geometry
// (subzone-entry / exploration / teleport triggers). Vanilla loads the DBC
// but exposes NONE of its geometry to Lua, so map addons (pfQuest, …) ship
// hand-scraped trigger tables. Same value proposition as `GetMapOverlays`:
// live, correct client data, no per-server table drift.
//
// ## Return shape (one info table)
//
// id — AreaTrigger.dbc row id
// mapID — Map.dbc id (continent 0/1, or an instance map)
// x, y, z — continent-space world coordinates of the trigger center
// radius — sphere-trigger radius (0 for a box trigger)
// isBox — true when the trigger is an oriented box (see below)
// boxLength / boxWidth / boxHeight / boxYaw — box dimensions + yaw
// (all 0 for a pure sphere trigger)
// areaID — AreaTable zone id the point falls in (via WorldMapArea);
// absent when no zone rect on this map contains the point
// mapX / mapY — the trigger's position as a 0..100 map-relative percent
// within that zone (the coordinate form map addons draw
// with); absent alongside areaID when unresolved
//
// The raw world geometry is authoritative (straight from the DBC); the
// derived `areaID`/`mapX`/`mapY` is the consumable form. Both are provided
// so a consumer can draw immediately or run its own world→map transform.
//
// ## World → map-percent transform
//
// `WorldMapArea.dbc` gives each zone a world-coordinate rect
// (`locTop`/`locBottom` bound world X, `locLeft`/`locRight` bound world Y —
// WoW's world axes: +X north, +Y west). In map space the HORIZONTAL axis
// (mapX) runs off world Y and the VERTICAL axis (mapY) off world X — the
// standard WoW / pfQuest / Astrolabe convention:
// mapX% = (locLeft - y) / (locLeft - locRight) * 100 (horizontal)
// mapY% = (locTop - x) / (locTop - locBottom) * 100 (vertical)
// We pick the zone by containment: among WorldMapArea rows on the trigger's
// map with a real (non-continent) areaID whose rect encloses the point, the
// smallest-area rect wins (the tightest zone — a subzone beats its parent).
// The engine's own map-name resolver reads the same rect fields; see
// `Map::Overlays` for the shared WorldMapArea wiring.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "map/Area.h"
#include <cstdint>
namespace Map::AreaTriggers {
namespace {
float FloatField(const uint8_t *rec, int off) {
return *reinterpret_cast<const float *>(rec + off);
}
// Zone containment + map-percent projection lives in the shared
// `Map::Area::ZonePercent` (taxi nodes use the same transform).
// Builds one trigger's info table and leaves it on the Lua stack top.
// Returns false (pushing nothing) when `id` is out of range or the slot
// is empty.
bool PushTriggerInfo(void *L, int id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
Offsets::VAR_AREATRIGGER_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
return false;
const int mapID = *reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID);
const float x = FloatField(rec, Offsets::OFF_AT_X);
const float y = FloatField(rec, Offsets::OFF_AT_Y);
const float z = FloatField(rec, Offsets::OFF_AT_Z);
const float radius = FloatField(rec, Offsets::OFF_AT_RADIUS);
const float boxLength = FloatField(rec, Offsets::OFF_AT_BOX_LENGTH);
const float boxWidth = FloatField(rec, Offsets::OFF_AT_BOX_WIDTH);
const float boxHeight = FloatField(rec, Offsets::OFF_AT_BOX_HEIGHT);
const float boxYaw = FloatField(rec, Offsets::OFF_AT_BOX_YAW);
const bool isBox = (boxLength != 0.0f || boxWidth != 0.0f || boxHeight != 0.0f);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "id", id);
Game::Lua::SetFieldNumber(L, "mapID", mapID);
Game::Lua::SetFieldNumber(L, "x", x);
Game::Lua::SetFieldNumber(L, "y", y);
Game::Lua::SetFieldNumber(L, "z", z);
Game::Lua::SetFieldNumber(L, "radius", radius);
Game::Lua::SetFieldBool(L, "isBox", isBox);
Game::Lua::SetFieldNumber(L, "boxLength", boxLength);
Game::Lua::SetFieldNumber(L, "boxWidth", boxWidth);
Game::Lua::SetFieldNumber(L, "boxHeight", boxHeight);
Game::Lua::SetFieldNumber(L, "boxYaw", boxYaw);
int areaID = 0;
double mapX = 0.0, mapY = 0.0;
if (Map::Area::ZonePercent(mapID, x, y, &areaID, &mapX, &mapY)) {
Game::Lua::SetFieldNumber(L, "areaID", areaID);
Game::Lua::SetFieldNumber(L, "mapX", mapX);
Game::Lua::SetFieldNumber(L, "mapY", mapY);
}
return true;
}
// `C_Map.GetAreaTriggerInfo(triggerID)` — one trigger's info table, or nil
// for a missing / out-of-range id.
int __fastcall Script_GetAreaTriggerInfo(void *L) {
const bool hasID = Game::Lua::IsNumber(L, 1);
const int id = hasID ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
Game::Lua::SetTop(L, 0);
if (!hasID || !PushTriggerInfo(L, id))
Game::Lua::PushNil(L);
return 1;
}
// `C_Map.GetAreaTriggers([mapID])` — array of every trigger's info table,
// optionally filtered to a single Map.dbc id.
int __fastcall Script_GetAreaTriggers(void *L) {
const bool filter = Game::Lua::IsNumber(L, 1);
const int wantMap = filter ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_AREATRIGGER_COUNT);
int outIdx = 0;
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
Offsets::VAR_AREATRIGGER_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
if (filter &&
*reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID) != wantMap)
continue;
outIdx += 1;
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
PushTriggerInfo(L, id); // rec is valid, so this always pushes a table
Game::Lua::SetTable(L, -3);
}
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggerInfo",
&Script_GetAreaTriggerInfo);
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggers",
&Script_GetAreaTriggers);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Map::AreaTriggers
+133
View File
@@ -0,0 +1,133 @@
// 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_Map.GetAreaInfo(areaID)` and `C_Map.GetAreas()` — surface
// `AreaTable.dbc`'s localized name column to Lua. `AreaTable.dbc` is a
// client DBC (always loaded, synchronous, localized, ~1500 rows), so
// unlike creature/item names there's no WDB cold-cache problem — the name
// is always available for any id.
//
// - `GetAreaInfo(areaID)` is a retail backport: the localized name for one
// area id (zone or subzone), matching retail's signature.
// - `GetAreas()` is a ClassicAPI extension (retail uses the uiMapID tree,
// not raw AreaTable enumeration): the full id→name map, the load-bearing
// piece for name databases that need name→id / the full id set (reverse
// search, name resolution). Same "surface the hidden DBC" pattern as
// `GetAreaTriggers` / `GetMapOverlays`.
//
// Names are the client's active locale (same source as `GetRealZoneText` /
// `Script_GetCharacterInfo`'s zone-name resolve), so they match what the
// game shows everywhere — no locale/rename drift vs a shipped table.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Map::Areas {
namespace {
// `C_Map.GetAreaInfo(areaID)` — localized AreaTable name, or nil for
// non-numeric / non-positive input or an id with no row / empty name.
int __fastcall Script_GetAreaInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const int areaID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *name =
(areaID > 0) ? DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
Offsets::VAR_AREATABLE_COUNT,
static_cast<uint32_t>(areaID),
Offsets::OFF_AREATABLE_NAMES)
: nullptr;
if (name == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::PushString(L, name);
return 1;
}
// `C_Map.GetAreas()` — { [areaID] = name } for every row with a non-empty
// localized name. Empty-name rows (DBC::LocalizedField → nullptr) are
// omitted, so the map round-trips with GetAreaInfo for every key.
int __fastcall Script_GetAreas(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_AREATABLE_COUNT);
for (int id = 1; id <= count; ++id) {
const char *name = DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
Offsets::VAR_AREATABLE_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_AREATABLE_NAMES);
if (name == nullptr)
continue;
Game::Lua::PushNumber(L, static_cast<double>(id));
Game::Lua::PushString(L, name);
Game::Lua::SetTable(L, -3);
}
return 1;
}
// `C_Map.GetMapAreaIDs()` — { [mapName] = areaID } for every
// `WorldMapArea.dbc` row: the map's name (the dir the engine uses for its map
// textures) → its `AreaTable` areaID. Vanilla has no `GetCurrentMapAreaID`;
// this lets an addon resolve a *browsed* map to a zone id — including the
// city / instance maps `GetMapZones()` doesn't enumerate (Orgrimmar,
// Undercity, Alterac Valley), which otherwise can't be matched to zone content
// (e.g. a flight master pinned by areaID). Continent-spanning rows (areaID 0)
// and unnamed rows are omitted.
//
// Note: this is the WorldMapArea NAME, which matches `GetMapInfo()`'s returned
// dir for almost all maps but not a few misspelled Blizzard folders (the
// Orgrimmar map's texture folder is "Ogrimmar"), so `[GetMapInfo()]` can miss
// for those — resolve them by zone name instead.
int __fastcall Script_GetMapAreaIDs(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
const int areaID = *reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
if (areaID == 0)
continue; // continent-spanning row, no zone id
const char *dir = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_WMA_NAME);
if (dir == nullptr || dir[0] == '\0')
continue;
Game::Lua::SetFieldNumber(L, dir, static_cast<double>(areaID));
}
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetAreaInfo", &Script_GetAreaInfo);
Game::Lua::RegisterTableFunction("C_Map", "GetAreas", &Script_GetAreas);
Game::Lua::RegisterTableFunction("C_Map", "GetMapAreaIDs", &Script_GetMapAreaIDs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Map::Areas
+80
View File
@@ -0,0 +1,80 @@
// 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_MapExplorationInfo.GetExploredMapTextures(areaID)` and
// `GetUnexploredMapTextures(areaID)` — the exploration-filtered views of a
// zone's `WorldMapOverlay.dbc` entries.
//
// - `GetExploredMapTextures` is the retail-named backport: only the overlays
// the local player has **discovered**. Retail keys on `uiMapID` and returns
// numeric `fileDataIDs`; here it's an `AreaTable` areaID and the tiles are
// texture paths (`fileDataIDs` holds those paths, see Overlays.cpp).
// - `GetUnexploredMapTextures` is the ClassicAPI-extension complement: the
// overlays NOT yet discovered — what a map-reveal addon draws over the
// fogged base map. No retail equivalent (retail only ships the explored
// getter); its inverse, `C_Map.GetMapOverlays`, returns *all* overlays.
//
// All three share `Map::Overlays::PushZoneOverlays` — same per-overlay table
// shape (texture, resolved tiles, hit rect, areaIDs, fileDataIDs); these two
// just pass an exploration Filter. "Explored" is computed per-areaID from the
// player's explored-areas bitfield (see Overlays.cpp `IsOverlayExplored`), so
// the areaID argument works for any zone, not just the one on screen.
//
// With no argument, both default to the world map's currently viewed zone
// (like `GetMapOverlays()`).
#include "Game.h"
#include "map/Area.h"
#include "map/Overlays.h"
#include <cstdint>
namespace Map::Exploration {
namespace {
int ResolveRow(void *L) {
return Game::Lua::IsNumber(L, 1)
? Map::Area::RowForAreaID(
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)))
: Map::Area::CurrentViewRow();
}
int __fastcall Script_GetExploredMapTextures(void *L) {
const int row = ResolveRow(L);
Game::Lua::SetTop(L, 0);
Map::Overlays::PushZoneOverlays(L, row, Map::Overlays::Filter::Explored);
return 1;
}
int __fastcall Script_GetUnexploredMapTextures(void *L) {
const int row = ResolveRow(L);
Game::Lua::SetTop(L, 0);
Map::Overlays::PushZoneOverlays(L, row, Map::Overlays::Filter::Unexplored);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_MapExplorationInfo",
"GetExploredMapTextures",
&Script_GetExploredMapTextures);
Game::Lua::RegisterTableFunction("C_MapExplorationInfo",
"GetUnexploredMapTextures",
&Script_GetUnexploredMapTextures);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Map::Exploration
+181 -71
View File
@@ -80,23 +80,37 @@
// Array of overlay tables:
// textureName — bare DBC name ("GOLDSHIRE")
// texturePath — "Interface\WorldMap\<dir>\GOLDSHIRE" (engine format)
// areaID — primary AreaTable id revealed (first nonzero)
// areaIDs — all AreaTable ids revealed (WorldMapOverlay.dbc
// areaID[4]; usually one, some overlays span 2-3 subzones)
// textureWidth / textureHeight — the DBC placement rect (reliable for
// placement; its IMPLIED tile count is what lies)
// offsetX / offsetY — placement on the zone canvas
// mapPointX / mapPointY
// hitRectTop/Left/Bottom/Right — hit rectangle in world-map canvas px
// (~1002x668); the tight clickable bounds of the landmass,
// the subzone-center source map addons scrape
// tileCols / tileRows / upscaled — the resolved grid
// tiles — array of ready-to-draw tiles:
// { file — texture path incl. number (SetTexture-ready)
// width, height — draw size in map pixels
// texCoordX, texCoordY — right/bottom texcoords
// offsetX, offsetY } — absolute canvas position
// fileDataIDs — ordered tile texture paths (retail's field name; vanilla
// has no numeric fileDataIDs, so these are paths). Redundant
// with tiles[].file — for retail-ported exploration addons.
//
// A consumer draws an overlay with a dumb loop: per tile SetTexture,
// SetWidth/Height, SetTexCoord(0, texCoordX, 0, texCoordY), SetPoint.
// Texture-less rows (a subzone hit region with no distinct art) still appear,
// with textureName == "", zero size, and an empty `tiles` table — so
// geometry consumers see every subzone while `ipairs(ov.tiles)` draw loops
// stay safe. A consumer draws a textured overlay with a dumb loop: per tile
// SetTexture, SetWidth/Height, SetTexCoord(0, texCoordX, 0, texCoordY), SetPoint.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "map/Area.h"
#include "map/Overlays.h"
#include <cstdint>
#include <cstdio>
@@ -279,46 +293,8 @@ const ResolvedOverlay *CachedResolve(int overlayId, const char *basePath,
return g_cache[overlayId];
}
// WorldMapArea row id for the world map's current view — the same
// resolution the engine's current-map-name getter (FUN_004a6cf0, behind
// GetMapInfo) performs: continent/zone selection indexes into the
// per-continent data blob; no continent selected falls back to the
// default row global.
int CurrentWorldMapAreaRow() {
const int continent =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_CONTINENT_INDEX);
if (continent < 0)
return *reinterpret_cast<const int *>(
Offsets::VAR_WORLDMAP_DEFAULT_AREA_ROW);
const uint8_t *blob = *reinterpret_cast<const uint8_t *const *>(
Offsets::VAR_WORLDMAP_CONTINENT_DATA);
if (blob == nullptr)
return -1;
const uint8_t *entry = blob + continent * Offsets::WORLDMAP_CONTINENT_STRIDE;
const int zone =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_ZONE_INDEX);
if (zone < 0)
return *reinterpret_cast<const int *>(entry + 0x04);
const int *zoneRows = *reinterpret_cast<const int *const *>(entry + 0x10);
return (zoneRows != nullptr) ? zoneRows[zone] : -1;
}
// WorldMapArea row id for an AreaTable areaID. Linear walk — the table
// has ~175 rows; called once per Lua call.
int WorldMapAreaRowForAreaID(uint32_t areaID) {
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(id));
if (rec != nullptr &&
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_WMA_AREA_ID) ==
areaID)
return id;
}
return -1;
}
// WorldMapArea row resolution (areaID → row, current view → row) lives in
// the shared `Map::Area` helper — `GetMapWorldSize` keys off the same rows.
// Pushes the `tiles` array for one overlay (leaves it on the stack top).
void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
@@ -401,20 +377,78 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
}
}
// `C_Map.GetMapOverlays([areaID])` — see the module comment.
int __fastcall Script_GetMapOverlays(void *L) {
int wmaRow = -1;
if (Game::Lua::IsNumber(L, 1)) {
wmaRow = WorldMapAreaRowForAreaID(
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)));
} else {
wmaRow = CurrentWorldMapAreaRow();
}
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
// The local player's explored-areas bitfield (`[player+0xE68] + 0xE6C`), or
// null before the player is resident. Bit `AreaBit` set == that AreaTable
// area is explored. Same storage the engine's discovered-overlay rebuild
// (FUN_004a67a0) reads.
const uint8_t *ExplorationBits() {
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<const uint8_t *>(fn("player"));
if (player == nullptr)
return nullptr;
auto *sub = *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_CGPLAYER_INFO);
if (sub == nullptr)
return nullptr;
return sub + Offsets::OFF_PLAYER_EXPLORED_BITS;
}
// True if the local player has discovered `overlayRec` — mirrors the
// per-areaID test in FUN_004a67a0's discovered-overlay rebuild: an overlay
// is explored if ANY of its (up to 4) AreaTable ids is explored. An area
// with gate (+0x28) < 0 needs no exploration; otherwise its AreaBit (+0x0C)
// is looked up in `bits`. `bits == null` (no player) → not explored.
bool IsOverlayExplored(const uint8_t *overlayRec, const uint8_t *bits) {
if (bits == nullptr)
return false;
for (int k = 0; k < 4; ++k) {
const int areaID = *reinterpret_cast<const int *>(
overlayRec + Offsets::OFF_WMO_AREA_ID + k * 4);
if (areaID <= 0)
continue;
const uint8_t *areaRec =
DBC::Record(Offsets::VAR_AREATABLE_RECORDS,
Offsets::VAR_AREATABLE_COUNT, static_cast<uint32_t>(areaID));
if (areaRec == nullptr)
continue;
if (*reinterpret_cast<const int *>(areaRec + Offsets::OFF_AREATABLE_EXPLORE_GATE) < 0)
return true; // area needs no exploration → always counts
const int areaBit = *reinterpret_cast<const int *>(
areaRec + Offsets::OFF_AREATABLE_AREA_BIT);
if (areaBit < 0)
continue;
const int byteIdx = areaBit >> 3;
if (byteIdx >= 0x100)
return true; // out of bitfield range → engine treats as explored
if ((bits[byteIdx] & (1u << (areaBit & 7))) != 0)
return true;
}
return false;
}
// `C_Map.GetMapOverlays([areaID])` — see the module comment. Thin wrapper
// over the shared `PushZoneOverlays` with no exploration filter.
int __fastcall Script_GetMapOverlays(void *L) {
const int wmaRow = Game::Lua::IsNumber(L, 1)
? Map::Area::RowForAreaID(static_cast<uint32_t>(
Game::Lua::ToNumber(L, 1)))
: Map::Area::CurrentViewRow();
Game::Lua::SetTop(L, 0);
Map::Overlays::PushZoneOverlays(L, wmaRow, Map::Overlays::Filter::All);
return 1;
}
} // namespace
// Shared overlay walk — see Overlays.h. Filters by exploration and builds
// the rich per-overlay table (texture + resolved tiles + hit rect + areaIDs
// + fileDataIDs). Uses the anonymous-namespace helpers above (same TU).
void PushZoneOverlays(void *L, int wmaRow, Filter filter) {
Game::Lua::NewTable(L);
if (wmaRow <= 0)
return 1;
return;
const uint8_t *wmaRec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
@@ -425,6 +459,10 @@ int __fastcall Script_GetMapOverlays(void *L) {
Offsets::OFF_WMA_NAME)
: nullptr;
// Exploration bitfield only needed for the filtered variants.
const uint8_t *bits =
(filter == Filter::All) ? nullptr : ExplorationBits();
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_OVERLAY_COUNT);
int outIdx = 0;
@@ -436,35 +474,56 @@ int __fastcall Script_GetMapOverlays(void *L) {
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMO_WORLDMAP_AREA) !=
wmaRow)
continue;
if (filter != Filter::All) {
const bool explored = IsOverlayExplored(rec, bits);
if (filter == Filter::Explored && !explored)
continue;
if (filter == Filter::Unexplored && explored)
continue;
}
const char *texName = *reinterpret_cast<const char *const *>(
rec + Offsets::OFF_WMO_TEXTURE_NAME);
if (texName == nullptr || texName[0] == '\0')
continue; // hit-rect-only rows have no drawable texture
// Full path, mirroring the engine's own GetMapOverlayInfo format
// (unresolvable map dir falls back to the "World" folder).
char basePath[0x104];
if (mapDir != nullptr && mapDir[0] != '\0')
std::snprintf(basePath, sizeof(basePath),
"Interface\\WorldMap\\%s\\%s", mapDir, texName);
else
std::snprintf(basePath, sizeof(basePath),
"Interface\\WorldMap\\World\\%s", texName);
const bool hasTexture = (texName != nullptr && texName[0] != '\0');
const auto fieldInt = [rec](uintptr_t off) {
return *reinterpret_cast<const int32_t *>(rec + off);
};
const int dbcW = fieldInt(Offsets::OFF_WMO_TEXTURE_WIDTH);
const int dbcH = fieldInt(Offsets::OFF_WMO_TEXTURE_HEIGHT);
const int offX = fieldInt(Offsets::OFF_WMO_OFFSET_X);
const int offY = fieldInt(Offsets::OFF_WMO_OFFSET_Y);
const ResolvedOverlay *r = CachedResolve(id, basePath, dbcW, dbcH);
// Texture placement + tile grid only exist for rows that carry art.
// A hit-rect-only row (areaID + hit rect, no texture — a clickable
// subzone region with no distinct BLP) still gets a result row: empty
// textureName/path, zero size, and an empty `tiles` table (r == null →
// PushTiles emits `{}`), so geometry consumers see it while art-draw
// loops (`ipairs(ov.tiles)`) iterate zero times. Stock 1.12 and Octo
// both happen to give every overlay a texture, but the DBC schema
// allows texture-less rows, so we don't assume.
char basePath[0x104];
int dbcW = 0, dbcH = 0;
const ResolvedOverlay *r = nullptr;
if (hasTexture) {
// Full path, mirroring the engine's own GetMapOverlayInfo format
// (unresolvable map dir falls back to the "World" folder).
if (mapDir != nullptr && mapDir[0] != '\0')
std::snprintf(basePath, sizeof(basePath),
"Interface\\WorldMap\\%s\\%s", mapDir, texName);
else
std::snprintf(basePath, sizeof(basePath),
"Interface\\WorldMap\\World\\%s", texName);
dbcW = fieldInt(Offsets::OFF_WMO_TEXTURE_WIDTH);
dbcH = fieldInt(Offsets::OFF_WMO_TEXTURE_HEIGHT);
r = CachedResolve(id, basePath, dbcW, dbcH);
} else {
basePath[0] = '\0';
}
outIdx += 1;
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "textureName", texName);
Game::Lua::SetFieldString(L, "textureName", hasTexture ? texName : "");
Game::Lua::SetFieldString(L, "texturePath", basePath);
Game::Lua::SetFieldNumber(L, "textureWidth", dbcW);
Game::Lua::SetFieldNumber(L, "textureHeight", dbcH);
@@ -474,6 +533,41 @@ int __fastcall Script_GetMapOverlays(void *L) {
L, "mapPointX", fieldInt(Offsets::OFF_WMO_MAP_POINT_X));
Game::Lua::SetFieldNumber(
L, "mapPointY", fieldInt(Offsets::OFF_WMO_MAP_POINT_Y));
// Hit rectangle (world-map canvas px, ~1002x668) — the tight clickable
// bounds of the landmass, present for every row (texture or not). The
// subzone-center source: (left+right)/2 / 1002, (top+bottom)/2 / 668.
Game::Lua::SetFieldNumber(
L, "hitRectTop", fieldInt(Offsets::OFF_WMO_HITRECT_TOP));
Game::Lua::SetFieldNumber(
L, "hitRectLeft", fieldInt(Offsets::OFF_WMO_HITRECT_LEFT));
Game::Lua::SetFieldNumber(
L, "hitRectBottom", fieldInt(Offsets::OFF_WMO_HITRECT_BOTTOM));
Game::Lua::SetFieldNumber(
L, "hitRectRight", fieldInt(Offsets::OFF_WMO_HITRECT_RIGHT));
// AreaTable ids this overlay reveals (WorldMapOverlay.dbc areaID[4]).
// `areaID` is the primary (first nonzero — every overlay has one);
// `areaIDs` is the full nonzero list (52 of 707 overlays span 2-3
// subzones, e.g. a lake straddling two areas). Same table shape as
// `tiles` — build the array, then assign the scalar.
int firstArea = 0, aCount = 0;
Game::Lua::PushString(L, "areaIDs");
Game::Lua::NewTable(L);
for (int k = 0; k < 4; ++k) {
const int aid = fieldInt(Offsets::OFF_WMO_AREA_ID + k * 4);
if (aid == 0)
continue;
if (firstArea == 0)
firstArea = aid;
aCount += 1;
Game::Lua::PushNumber(L, static_cast<double>(aCount));
Game::Lua::PushNumber(L, static_cast<double>(aid));
Game::Lua::SetTable(L, -3);
}
Game::Lua::SetTable(L, -3); // overlay.areaIDs = { ... }
Game::Lua::SetFieldNumber(L, "areaID", firstArea);
if (r != nullptr) {
Game::Lua::SetFieldNumber(L, "tileCols", r->cols);
Game::Lua::SetFieldNumber(L, "tileRows", r->rows);
@@ -483,18 +577,34 @@ int __fastcall Script_GetMapOverlays(void *L) {
PushTiles(L, r, basePath, dbcW, dbcH, offX, offY);
Game::Lua::SetTable(L, -3);
// fileDataIDs — ordered tile texture paths (retail's field name for
// the tile list). Vanilla has no numeric fileDataIDs, so these are the
// SetTexture-ready paths; redundant with tiles[].file, provided so
// retail-ported exploration addons reading info.fileDataIDs work,
// while tiles[] carries the per-tile draw geometry Octo needs.
Game::Lua::PushString(L, "fileDataIDs");
Game::Lua::NewTable(L);
if (r != nullptr) {
char file[0x120];
for (int i = 0; i < r->count; ++i) {
std::snprintf(file, sizeof(file), "%s%d", basePath, i + 1);
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
Game::Lua::PushString(L, file);
Game::Lua::SetTable(L, -3);
}
}
Game::Lua::SetTable(L, -3); // overlay.fileDataIDs = { ... }
Game::Lua::SetTable(L, -3);
}
return 1;
}
namespace {
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetMapOverlays",
&Script_GetMapOverlays);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Map::Overlays
+36
View File
@@ -0,0 +1,36 @@
// 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/>.
#pragma once
namespace Map::Overlays {
// Which overlays of a zone to include, by the local player's exploration.
enum class Filter {
All, // every WorldMapOverlay.dbc row for the zone (C_Map.GetMapOverlays)
Explored, // only overlays the player has discovered
Unexplored, // only overlays the player has NOT discovered
};
// Pushes an array of overlay tables for `wmaRow` (a WorldMapArea.dbc row),
// filtered by exploration, onto the Lua stack as a single value. Shared by
// `C_Map.GetMapOverlays` (Filter::All) and `C_MapExplorationInfo`'s
// explored / unexplored texture getters. The caller resolves the row (via
// `Map::Area`) and clears the stack first; this leaves exactly one table.
// The per-overlay table shape is documented in Overlays.cpp's header.
//
// Explored/Unexplored read the player's explored-areas bitfield; before the
// player is resident (character select) every overlay reads as unexplored.
void PushZoneOverlays(void *L, int wmaRow, Filter filter);
} // namespace Map::Overlays
+82
View File
@@ -0,0 +1,82 @@
// 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_Map.GetMapWorldSize([areaID])` — the world size (in yards) of a zone,
// as `width, height`. This is the physical extent the map represents: the
// span of the `WorldMapArea.dbc` placement rect. Modern addons use it to
// convert map-relative percents to yard distances (e.g. tracking-range
// rings, "X yards away" readouts).
//
// With a numeric `areaID` (AreaTable id — the same identity
// `C_Map.GetBestMapForUnit` returns and `C_Map.GetMapOverlays` accepts),
// returns that zone's size. With no argument, returns the world map's
// currently viewed zone (same resolution `GetMapOverlays()` uses with no
// arg). Returns nothing (→ nil, nil) when the zone can't be resolved.
//
// WoW's world axes are +X north, +Y west; the WorldMapArea rect bounds
// them as locTop/locBottom (world X) and locLeft/locRight (world Y). Map
// width is the horizontal (east-west) extent and height the vertical
// (north-south):
// width = locLeft - locRight (world Y span)
// height = locTop - locBottom (world X span)
// The same spans are the denominators in the world→map percent transform
// (see `Map::AreaTriggers`), so a point's `mapX% * width / 100` is its
// offset in yards from the zone's east edge, and likewise for height.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "map/Area.h"
#include <cstdint>
namespace Map::WorldSize {
namespace {
int __fastcall Script_GetMapWorldSize(void *L) {
const int row = Game::Lua::IsNumber(L, 1)
? Map::Area::RowForAreaID(
static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)))
: Map::Area::CurrentViewRow();
Game::Lua::SetTop(L, 0);
if (row <= 0)
return 0; // no values -> nil, nil
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
Offsets::VAR_WORLDMAP_AREA_COUNT,
static_cast<uint32_t>(row));
if (rec == nullptr)
return 0;
const float left = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_LEFT);
const float right = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_RIGHT);
const float top = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_TOP);
const float bottom = *reinterpret_cast<const float *>(rec + Offsets::OFF_WMA_LOC_BOTTOM);
Game::Lua::PushNumber(L, static_cast<double>(left) - right); // width (world Y span)
Game::Lua::PushNumber(L, static_cast<double>(top) - bottom); // height (world X span)
return 2;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetMapWorldSize",
&Script_GetMapWorldSize);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Map::WorldSize
+2 -11
View File
@@ -33,6 +33,7 @@
#include "Game.h"
#include "Offsets.h"
#include "item/Location.h"
#include "unit/Identity.h"
#include "tick/WorldTick.h"
#include <cstdint>
@@ -42,7 +43,6 @@ namespace Merchant::Frame {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using GetItemRecord_t = const uint8_t *(__thiscall *)(
void *cache, uint32_t itemID, const uint64_t *guid,
void *callback, void *userData, bool requestIfMissing);
@@ -108,15 +108,6 @@ int ItemQuality(uint32_t itemID) {
// Player invMgr lives inline at player + 0x1D38. First uint32 is the
// max slot count; +0x04 is the GUID array pointer.
const uint8_t *PlayerInvMgr() {
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(resolve("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
// Walk a single bag, invoking `cb(cgItem)` for each non-empty slot.
// `cb` returns true to continue, false to stop iteration. Stomps
// the Lua stack — caller must own it (`ResolveBag` does the stomping).
@@ -161,7 +152,7 @@ int __fastcall Script_GetBuybackItemID(void *L) {
if (invSlot == 0)
return 0;
auto *invMgr = PlayerInvMgr();
auto *invMgr = Unit::Identity::PlayerInventoryManager();
if (invMgr == nullptr)
return 0;
// First u32 of the invMgr struct is the max slot count.
+2 -9
View File
@@ -55,6 +55,7 @@
#include "Offsets.h"
#include "item/Arg.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -65,7 +66,6 @@ namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
// `__thiscall(player /*ecx*/, skillLineID) -> slot` (-1 if the player
// doesn't have the skill line). See VAR docs on FUN_SKILL_LINE_TO_SLOT.
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
@@ -83,13 +83,6 @@ const uint8_t *PeekItemRecord(uint32_t itemID) {
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
// The local player's CGUnit/CGPlayer object (holds the descriptor at
// +OFF_UNIT_DESCRIPTOR and the CGPlayer sub-struct at +OFF_CGPLAYER_INFO).
const uint8_t *ResolvePlayer() {
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
return static_cast<const uint8_t *>(resolve("player"));
}
// True iff `mask` restricts a 1-based `index` (class/race) the player
// isn't part of. Mask 0 and 0xFFFFFFFF both mean "no restriction".
bool RestrictedOut(uint32_t mask, uint32_t index1Based) {
@@ -246,7 +239,7 @@ bool ComputeCanUse(int itemID) {
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr)
return false; // not cached → sync false, no load fired
auto *player = ResolvePlayer();
auto *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return false; // pre-login / no player
auto *desc = *reinterpret_cast<const uint8_t *const *>(
+244
View File
@@ -0,0 +1,244 @@
// 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/>.
// The `PlayerLocation`-taking `C_PlayerInfo` accessors:
//
// C_PlayerInfo.GetName(playerLocation) -> name
// C_PlayerInfo.GetClass(playerLocation) -> className, classFilename, classID
// C_PlayerInfo.GetRace(playerLocation) -> raceID
// C_PlayerInfo.GetSex(playerLocation) -> sex (1 neutral / 2 male / 3 female)
// C_PlayerInfo.IsConnected([playerLocation]) -> isConnected
//
// A `PlayerLocation` (see Util/PlayerLocation.lua) is a plain table carrying
// either a unit token (`.unit`) or a GUID string (`.guid`). We resolve it to
// a 64-bit GUID — unit tokens through the engine's token→GUID table
// (`FUN_TOKEN_TO_GUID`, which resolves group members in other zones too, not
// just synced CGUnits), GUID strings by parsing — and then read everything
// straight from the engine's own data structures. No `Script_*` Lua wrappers
// are called; each value comes from the same engine internals the codebase
// already uses elsewhere:
//
// - GetName -> resolve the GUID to a CGObject and call the polymorphic
// name getter `FUN_OBJECT_GET_NAME` (same path as
// `UnitNameFromGUID`).
// - GetRace / -> UNIT_FIELD_BYTES_0: the login-time mirror globals when the
// GetClass / location is the local player (valid before the descriptor
// GetSex spawns, matching `Script_UnitClass`'s own player path),
// otherwise the resolved unit's descriptor bytes. GetClass
// maps classID -> className/classFile via `ChrClasses.dbc`
// like `C_CreatureInfo.GetClassInfo`. GetSex reports the
// API's 2/3 (raw gender byte 0/1 + 2), matching
// `GetPlayerInfoByGUID`.
// - IsConnected -> a resolvable CGObject means connected; otherwise the
// group-member roster's online bit (then slot presence),
// exactly what `Script_UnitIsConnected` checks.
//
// Only the `unit` and `guid` PlayerLocation kinds resolve on 1.12 — the chat
// line / who / battlefield-score / community / voice kinds have no
// client-side unit resolution. Unsupported or unresolvable locations return
// nil.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "guid/Guid.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Player::LocationInfo {
namespace {
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
using ResolveObject_t = void *(__fastcall *)(uint32_t typeMask, const char *dbg,
uint32_t guidLo, uint32_t guidHi, int line);
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
using GroupLookup_t = void *(__fastcall *)(const uint64_t *guid);
// Reads a string field `t[field]` at table stack index `locIdx` into `out`.
// Returns true only for a non-empty string. Stack-neutral.
bool ReadStringField(void *L, int locIdx, const char *field, char *out, int size) {
Game::Lua::PushString(L, field);
Game::Lua::GetTable(L, locIdx); // pops key, pushes value
bool ok = false;
if (Game::Lua::IsString(L, -1)) {
const char *s = Game::Lua::ToString(L, -1);
if (s != nullptr && s[0] != '\0') {
int i = 0;
for (; s[i] != '\0' && i < size - 1; ++i)
out[i] = s[i];
out[i] = '\0';
ok = true;
}
}
Game::Lua::SetTop(L, -2); // pop value (or nil)
return ok;
}
// Resolves the PlayerLocation table at `locIdx` to a 64-bit GUID. Unit tokens
// go through the engine token→GUID table (handles out-of-zone group members);
// GUID strings are parsed. Returns false for unsupported kinds or a token
// that maps to nothing.
bool LocationGuid(void *L, int locIdx, uint64_t *out) {
char token[64];
if (ReadStringField(L, locIdx, "unit", token, sizeof(token))) {
auto toGuid = reinterpret_cast<TokenToGuid_t>(Offsets::FUN_TOKEN_TO_GUID);
*out = toGuid(token);
return *out != 0;
}
char guidStr[40];
if (ReadStringField(L, locIdx, "guid", guidStr, sizeof(guidStr)))
return Guid::Parse(guidStr, out) && *out != 0;
return false;
}
// Resolves a GUID to its currently-synced CGUnit, or null if not resident.
const uint8_t *ResolveUnit(uint64_t guid) {
auto resolve = reinterpret_cast<ResolveObject_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
return static_cast<const uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "ClassicAPI", static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32), 0));
}
// Reads a UNIT_FIELD_BYTES_0 byte: the login-time mirror global when `guid` is
// the local player (works before the descriptor spawns), otherwise the
// resolved unit's descriptor byte. Returns -1 when the unit isn't resident.
int UnitByte(uint64_t guid, uintptr_t playerMirror, uint32_t descByteOffset) {
if (guid == Unit::Identity::PlayerGuid())
return *reinterpret_cast<const uint8_t *>(playerMirror);
const uint8_t *unit = ResolveUnit(guid);
if (unit == nullptr)
return -1;
auto *desc = *reinterpret_cast<const uint8_t *const *>(
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
if (desc == nullptr)
return -1;
return *reinterpret_cast<const uint8_t *>(desc + descByteOffset);
}
int __fastcall Script_GetName(void *L) {
uint64_t guid = 0;
if (!LocationGuid(L, 1, &guid))
return 0;
const uint8_t *unit = ResolveUnit(guid);
if (unit == nullptr)
return 0;
auto getName = reinterpret_cast<GetName_t>(Offsets::FUN_OBJECT_GET_NAME);
const char *name = getName(const_cast<uint8_t *>(unit), nullptr);
if (name == nullptr || name[0] == '\0')
return 0;
Game::Lua::PushString(L, name);
return 1;
}
int __fastcall Script_GetRace(void *L) {
uint64_t guid = 0;
if (!LocationGuid(L, 1, &guid))
return 0;
const int raceID = UnitByte(guid, Offsets::VAR_PLAYER_RACE_BYTE,
Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
if (raceID <= 0)
return 0;
Game::Lua::PushNumber(L, static_cast<double>(raceID));
return 1;
}
int __fastcall Script_GetClass(void *L) {
uint64_t guid = 0;
if (!LocationGuid(L, 1, &guid))
return 0;
const int classID = UnitByte(guid, Offsets::VAR_PLAYER_CLASS_BYTE,
Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
if (classID <= 0)
return 0;
// className / classFile from ChrClasses.dbc — same reads as
// C_CreatureInfo.GetClassInfo(classID).
const char *classFile = DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
Offsets::VAR_CHRCLASSES_COUNT,
static_cast<uint32_t>(classID),
Offsets::OFF_CHRCLASSES_FILENAME);
if (classFile == nullptr)
return 0; // classID with no ChrClasses row
const char *className = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
Offsets::VAR_CHRCLASSES_COUNT,
static_cast<uint32_t>(classID),
Offsets::OFF_CHRCLASSES_NAMES);
Game::Lua::PushString(L, className); // NULL → nil via lua_pushstring
Game::Lua::PushString(L, classFile);
Game::Lua::PushNumber(L, static_cast<double>(classID));
return 3;
}
int __fastcall Script_GetSex(void *L) {
uint64_t guid = 0;
if (!LocationGuid(L, 1, &guid))
return 0;
const int gender = UnitByte(guid, Offsets::VAR_PLAYER_SEX_BYTE,
Offsets::OFF_UNIT_DESCRIPTOR_SEX_BYTE);
if (gender < 0)
return 0;
// UNIT_FIELD_BYTES_0 stores 0 = male / 1 = female; the API (and UnitSex)
// reports 2 = male / 3 = female. Same +2 remap as GetPlayerInfoByGUID.
Game::Lua::PushNumber(L, static_cast<double>(gender + 2));
return 1;
}
int __fastcall Script_IsConnected(void *L) {
// `playerLocation` is optional — with no table arg it defaults to the
// local player (always connected while playing).
uint64_t guid = 0;
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
if (!LocationGuid(L, 1, &guid))
return 0;
} else {
guid = Unit::Identity::PlayerGuid();
}
if (guid == 0)
return 0;
bool connected = ResolveUnit(guid) != nullptr; // a synced object is online
if (!connected) {
// Not resident — consult the group-member roster, mirroring
// Script_UnitIsConnected: the stats block's online bit, else slot
// presence (a member with no stats block yet still counts as present).
auto stats = reinterpret_cast<GroupLookup_t>(
Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP)(&guid);
if (stats != nullptr) {
connected = (*reinterpret_cast<const uint8_t *>(
static_cast<const uint8_t *>(stats) +
Offsets::OFF_GROUP_MEMBER_STATUS_FLAGS) &
Offsets::GROUP_MEMBER_STATUS_ONLINE) != 0;
} else {
connected = reinterpret_cast<GroupLookup_t>(
Offsets::FUN_GROUP_MEMBER_SLOT_LOOKUP)(&guid) != nullptr;
}
}
Game::Lua::PushBool(L, connected);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetName", &Script_GetName);
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetClass", &Script_GetClass);
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetRace", &Script_GetRace);
Game::Lua::RegisterTableFunction("C_PlayerInfo", "GetSex", &Script_GetSex);
Game::Lua::RegisterTableFunction("C_PlayerInfo", "IsConnected", &Script_IsConnected);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Player::LocationInfo
+2 -8
View File
@@ -29,6 +29,7 @@
#include "Offsets.h"
#include "aura/Data.h"
#include "dbc/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
#include <cstring>
@@ -37,15 +38,8 @@ namespace Spell::Cancel {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using CancelAuraSend_t = void(__fastcall *)(int spellID);
const uint8_t *ResolvePlayer() {
auto fn = reinterpret_cast<ResolveUnitToken_t>(
static_cast<uintptr_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN));
return static_cast<const uint8_t *>(fn("player"));
}
// Pre-validates `spellID` against Spell.dbc and ships the cancel
// packet. The engine's sender unconditionally derefs `[record+0x1C]`
// after a NULL-on-OOR record lookup, so skipping the
@@ -71,7 +65,7 @@ int __fastcall Script_CancelSpellByName(void *L) {
const char *targetName = Game::Lua::ToString(L, 1);
if (targetName == nullptr || *targetName == '\0')
return 0;
const uint8_t *player = ResolvePlayer();
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return 0;
// Walk the player's buff range only (slots 0..31). Modern
+101
View File
@@ -22,6 +22,7 @@ namespace Spell::SkillLine {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
// Reads UNIT_FIELD_BYTES_0 from the local player's descriptor. Same
// pattern Spell::Level::PlayerDescriptor uses. Returns (0, 0) when the
@@ -171,9 +172,109 @@ int __fastcall Script_GetSpellSkillLine(void *L) {
return 2;
}
// `C_SpellBook.GetSkillLineName(skillLineID) → name`
//
// Localized `SkillLine.dbc::Name` for a skill-line id — the id → name
// half of the bridge addons build by hand. Pure DBC read: returns the
// name for ANY valid skill line (Herbalism = 182, Mining = 186, Swords,
// Defense, …) whether or not the player has learned it. `nil` for
// non-numeric / non-positive input or an id with no SkillLine.dbc row.
//
// Pairs with `GetSkillLineRank` (the learned-rank lookup): name comes
// from the DBC, rank from the player's live skill list.
int __fastcall Script_GetSkillLineName(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const int id = static_cast<int>(Game::Lua::ToNumber(L, 1));
const char *name =
(id > 0) ? DBC::LocalizedField(Offsets::VAR_SKILL_LINE_RECORDS,
Offsets::VAR_SKILL_LINE_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_SKILL_LINE_NAME)
: nullptr;
if (name == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::PushString(L, name);
return 1;
}
// `C_SpellBook.GetSkillLineRank(skillLineID) → curRank, maxRank, modifier`
//
// The local player's rank in a skill line, keyed by `SkillLine.dbc` id.
// Vanilla's `GetSkillLineInfo(index)` only walks the skill window by
// position and returns skills by *name*, so an addon starting from a
// SkillLine.dbc id (a quest's `requiredSkill`, "Herbalism" = 182, …) has
// to map id → localized name and string-match every skill line to recover
// the rank. This reads the player's live skill list keyed by id directly.
//
// Returns the same values `GetSkillLineInfo` reports for the line:
// curRank — current skill value (base, no temp bonus)
// maxRank — the line's cap at the player's level
// modifier — temporary bonus (weapon oils, buffs; 0 if none)
//
// Returns `nil` (nothing) when the player hasn't learned the line
// (`FUN_SKILL_LINE_TO_SLOT` reports no slot), for bad input, or before
// the player object / skill sub-struct is resident (character select).
// Reads the same CGPlayer `+0xE68` skill table `Player::CanUseItem`'s
// RequiredSkill gate uses; ranks verified there (Tailoring 261/300, …).
int __fastcall Script_GetSkillLineRank(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const int skillLineID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (skillLineID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(resolve("player"));
if (player == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
auto toSlot = reinterpret_cast<SkillLineToSlot_t>(Offsets::FUN_SKILL_LINE_TO_SLOT);
const int slot = toSlot(player, static_cast<uint32_t>(skillLineID));
if (slot < 0) { // player doesn't have the skill line
Game::Lua::PushNil(L);
return 1;
}
auto *sub = *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_CGPLAYER_INFO);
if (sub == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const uint8_t *rec = sub + Offsets::OFF_SKILL_INFO_TABLE +
slot * Offsets::SKILL_INFO_STRIDE;
const uint32_t cur =
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_CUR);
const uint32_t max =
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_MAX);
const uint32_t modifier =
*reinterpret_cast<const uint16_t *>(rec + Offsets::OFF_SKILL_INFO_BONUS);
Game::Lua::PushNumber(L, static_cast<double>(cur));
Game::Lua::PushNumber(L, static_cast<double>(max));
Game::Lua::PushNumber(L, static_cast<double>(modifier));
return 3;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellSkillLine",
&Script_GetSpellSkillLine);
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSkillLineName",
&Script_GetSkillLineName);
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSkillLineRank",
&Script_GetSkillLineRank);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+394
View File
@@ -0,0 +1,394 @@
// 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_TaxiMap.GetTaxiNodesForMap(mapID)` (static) and
// `C_TaxiMap.GetAllTaxiNodes([uiMapID])` (live) — backports of the retail
// taxi-map node enumerators, plus the `Enum.FlightPathFaction` /
// `Enum.FlightPathState` tables so retail code ports unchanged.
//
// - **GetTaxiNodesForMap** reads `TaxiNodes.dbc` directly: every flight point
// on a continent (or all continents with no arg), faction-tagged,
// regardless of discovery — the static DB a zone-map addon (pfQuest's
// `pfDB.meta.flight`) wants. No flight master needed. Non-flight rows are filtered out — the DBC also holds transports/
// boats/zeppelins (no flight mount) and non-selectable markers like
// Northshire Abbey (no connecting flight path); a real flight master has a
// mount AND is a TaxiPath endpoint.
// - **GetAllTaxiNodes** wraps the open flight master's live reachable-node
// list (the legacy NumTaxiNodes/TaxiNode* globals' backing state): the
// nodes you can actually fly to right now, with per-node reachability and
// the slot index `TakeTaxiNode` takes. Empty when no taxi map is open.
//
// Ship both: static/DBC for databases + overlays, live/session for a custom
// flight-map UI. See docs/API.md for the field-by-field contract and which
// fields are retail vs ClassicAPI extensions.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include "map/Area.h"
#include <cstdint>
#include <cstring>
namespace Taxi::Map {
namespace {
// Enum.FlightPathFaction (retail values).
enum { FACTION_NEUTRAL = 0, FACTION_HORDE = 1, FACTION_ALLIANCE = 2 };
// Enum.FlightPathState (retail values).
enum { STATE_CURRENT = 0, STATE_REACHABLE = 1, STATE_UNREACHABLE = 2 };
float FloatField(const uint8_t *rec, int off) {
return *reinterpret_cast<const float *>(rec + off);
}
int IntField(const uint8_t *rec, int off) {
return *reinterpret_cast<const int *>(rec + off);
}
// --- name-based zone resolution -------------------------------------------
//
// A taxi node's name is "Location, Zone" (e.g. "Chillwind Camp, Western
// Plaguelands"), so the zone is authoritative in the data itself — which
// beats any geometric world->zone guess at genuine border interleaving that
// the WorldMapArea/overlay boxes can't separate (Chillwind sits by Strahnbrad,
// deep in Alterac's overlay box, but is a WPL node). We resolve the name
// suffix against AreaTable and fall back to the geometric resolver only when a
// node has no ", Zone" suffix or the zone name doesn't match.
char AsciiLower(char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }
bool IEqual(const char *a, const char *b) {
while (*a && *b) {
if (AsciiLower(*a) != AsciiLower(*b))
return false;
++a;
++b;
}
return *a == *b;
}
// True if `name` begins with `prefix` followed by a space — a word-prefix, so
// the abbreviated taxi suffixes ("Redridge", "Arathi") match the full
// AreaTable names ("Redridge Mountains", "Arathi Highlands").
bool IStartsWord(const char *name, const char *prefix) {
while (*prefix) {
if (!*name || AsciiLower(*name) != AsciiLower(*prefix))
return false;
++name;
++prefix;
}
return *name == ' ';
}
// Pointer into `name` at the zone part (after the last ", "), or null when the
// name has no ", " separator.
const char *ZoneSuffix(const char *name) {
const char *best = nullptr;
if (name != nullptr)
for (const char *p = name; p[0] != '\0'; ++p)
if (p[0] == ',' && p[1] == ' ')
best = p + 2;
return best;
}
// Copies the location part (before the FIRST ", ") of `name` into `out`,
// null-terminated. Returns `out`, or null when `name` has no ", " separator.
// This is the "Orgrimmar" in "Orgrimmar, Durotar" — a capital's own city map
// when it's a mapped area, distinct from its outdoor region.
const char *LocationPrefix(const char *name, char *out, int outSize) {
if (name == nullptr)
return nullptr;
for (int i = 0; name[i] != '\0'; ++i) {
if (name[i] == ',' && name[i + 1] == ' ') {
int n = (i < outSize - 1) ? i : outSize - 1;
for (int j = 0; j < n; ++j)
out[j] = name[j];
out[n] = '\0';
return out;
}
}
return nullptr;
}
// AreaTable areaID for a zone name: exact (case-insensitive) match preferred,
// else the first word-prefix match. 0 when nothing matches.
int ResolveZoneByName(const char *zone) {
if (zone == nullptr || *zone == '\0')
return 0;
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_AREATABLE_COUNT);
int prefixHit = 0;
for (int id = 1; id <= count; ++id) {
const char *nm = DBC::LocalizedField(Offsets::VAR_AREATABLE_RECORDS,
Offsets::VAR_AREATABLE_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_AREATABLE_NAMES);
if (nm == nullptr)
continue;
if (IEqual(nm, zone))
return id; // exact wins outright
if (prefixHit == 0 && IStartsWord(nm, zone))
prefixHit = id;
}
return prefixHit;
}
// Pushes { x = px, y = py } as a subtable field `key` of the table on top.
void PushPosition(void *L, const char *key, double px, double py) {
Game::Lua::PushString(L, key);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "x", px);
Game::Lua::SetFieldNumber(L, "y", py);
Game::Lua::SetTable(L, -3);
}
// Walks TaxiPath.dbc into two per-node-id flags:
// endpoint[] — the node is either end of some flight path. A real flight
// master always is; the fake rows (Northshire Abbey, spurious
// duplicate hub rows, standalone markers like "Eastern
// Plaguelands"/"Naxxramas") connect to nothing.
// dest[] — the node is a path DESTINATION (in-degree > 0), i.e. you can
// fly TO it. In-degree 0 marks a departure-only node reachable
// only by other means: druid Teleport (Nighthaven, Moonglade),
// a boat dock (Stormwind Harbor), etc.
// ids >= `size` are ignored (the vanilla table maxes ~122, so the fixed buffer
// is ample). Returns false when the path table isn't loaded.
constexpr int kMaxTaxiNodeID = 2048;
bool BuildTaxiGraph(bool *endpoint, bool *dest, int size) {
const int count = *reinterpret_cast<const int *>(Offsets::VAR_TAXIPATH_COUNT);
if (count <= 0)
return false;
for (int i = 1; i <= count; ++i) {
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXIPATH_RECORDS,
Offsets::VAR_TAXIPATH_COUNT,
static_cast<uint32_t>(i));
if (rec == nullptr)
continue;
const int from = IntField(rec, Offsets::OFF_TAXIPATH_FROM);
const int to = IntField(rec, Offsets::OFF_TAXIPATH_TO);
if (from >= 0 && from < size)
endpoint[from] = true;
if (to >= 0 && to < size) {
endpoint[to] = true;
dest[to] = true;
}
}
return true;
}
// `C_TaxiMap.GetTaxiNodesForMap([mapID])` — see module comment. A numeric
// `mapID` filters to that continent; omitted / non-number returns every
// flight master on every continent.
int __fastcall Script_GetTaxiNodesForMap(void *L) {
const bool filterMap = Game::Lua::IsNumber(L, 1);
const int wantMap =
filterMap ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : -1;
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
// Real flight masters are TaxiPath endpoints; `dest` marks the ones a
// path leads to (in-degree > 0) for the `reachable` field.
bool endpoint[kMaxTaxiNodeID] = {};
bool dest[kMaxTaxiNodeID] = {};
BuildTaxiGraph(endpoint, dest, kMaxTaxiNodeID);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_TAXINODES_COUNT);
int outIdx = 0;
for (int id = 1; id <= count; ++id) {
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXINODES_RECORDS,
Offsets::VAR_TAXINODES_COUNT,
static_cast<uint32_t>(id));
if (rec == nullptr)
continue;
const int mapID = IntField(rec, Offsets::OFF_TAXINODE_MAP_ID);
if (filterMap && mapID != wantMap)
continue;
// Two filters isolate real flight masters from the DBC's non-flight
// rows: (1) at least one flight mount — drops transports/boats/
// zeppelins/instance markers (both mounts 0); (2) the node is a
// TaxiPath endpoint — drops Northshire Abbey, spurious duplicate hub
// rows, and standalone markers that connect to no flight path.
const int horde = IntField(rec, Offsets::OFF_TAXINODE_HORDE_MOUNT);
const int alliance = IntField(rec, Offsets::OFF_TAXINODE_ALLIANCE_MOUNT);
if (horde == 0 && alliance == 0)
continue;
if (id >= kMaxTaxiNodeID || !endpoint[id])
continue;
const char *name = DBC::LocalizedField(Offsets::VAR_TAXINODES_RECORDS,
Offsets::VAR_TAXINODES_COUNT,
static_cast<uint32_t>(id),
Offsets::OFF_TAXINODE_NAME);
// The one path-connected, mounted row that still isn't a flight
// master: Blizzard's internal "Generic, World target" zeppelin-route
// marker. It's the sole such placeholder across all continents.
if (name != nullptr && std::strncmp(name, "Generic", 7) == 0)
continue;
const int faction = (horde != 0 && alliance != 0) ? FACTION_NEUTRAL
: (alliance != 0) ? FACTION_ALLIANCE
: FACTION_HORDE;
const float x = FloatField(rec, Offsets::OFF_TAXINODE_X);
const float y = FloatField(rec, Offsets::OFF_TAXINODE_Y);
const float z = FloatField(rec, Offsets::OFF_TAXINODE_Z);
outIdx += 1;
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "nodeID", id);
Game::Lua::SetFieldString(L, "name", name ? name : "");
Game::Lua::SetFieldNumber(L, "faction", faction);
// reachable: a flight path leads to this node (in-degree > 0). False
// marks a departure-only node reachable only by other means — druid
// Teleport (Nighthaven, Moonglade) or a boat dock (Stormwind Harbor);
// hide those from a non-class-specific "where can I fly" map.
Game::Lua::SetFieldBool(L, "reachable", id < kMaxTaxiNodeID && dest[id]);
Game::Lua::SetFieldNumber(L, "mapID", mapID);
Game::Lua::SetFieldNumber(L, "x", x);
Game::Lua::SetFieldNumber(L, "y", y);
Game::Lua::SetFieldNumber(L, "z", z);
// position: 0-1 on the continent map (retail-accurate). Falls back to
// {0,0} when the continent has no usable WorldMapArea row (instance
// maps); zone-map addons use areaID + mapX/mapY below instead.
double px = 0.0, py = 0.0;
::Map::Area::ContinentPercent(mapID, x, y, &px, &py);
PushPosition(L, "position", px, py);
// Derived zone anchor (areaID + 0-100 zone-relative percent), from the
// node's own name — authoritative, and right at borders geometry can't
// separate. Two candidate zones from the name, each accepted only if
// its WorldMapArea rect actually contains the point:
// 1. the LOCATION ("Orgrimmar" in "Orgrimmar, Durotar") — a capital's
// own city map, so the flight master pins on the city, not the
// surrounding region;
// 2. else the ZONE suffix ("Western Plaguelands") — the outdoor zone
// for a normal node (whose location is an unmapped subzone).
// Falls back to the geometric landmass resolver when neither name part
// resolves to a containing zone (no ", Zone" suffix, unknown name).
int areaID = 0;
double mapX = 0.0, mapY = 0.0;
bool haveZone = false;
char locBuf[128];
const int locArea = ResolveZoneByName(LocationPrefix(name, locBuf, sizeof(locBuf)));
const int zoneArea = ResolveZoneByName(ZoneSuffix(name));
if (locArea > 0 && ::Map::Area::PercentInZone(locArea, x, y, &mapX, &mapY)) {
areaID = locArea; // capital / mapped location -> its own city map
haveZone = true;
} else if (zoneArea > 0 &&
::Map::Area::PercentInZone(zoneArea, x, y, &mapX, &mapY)) {
areaID = zoneArea; // outdoor zone from the name suffix
haveZone = true;
} else {
haveZone = ::Map::Area::ZonePercent(mapID, x, y, &areaID, &mapX, &mapY);
}
if (haveZone) {
Game::Lua::SetFieldNumber(L, "areaID", areaID);
Game::Lua::SetFieldNumber(L, "mapX", mapX);
Game::Lua::SetFieldNumber(L, "mapY", mapY);
}
Game::Lua::SetTable(L, -3);
}
return 1;
}
using TaxiNodeType_t = const char *(__fastcall *)(uint32_t slot0Based);
int StateFromType(const char *type) {
if (type == nullptr)
return STATE_UNREACHABLE;
if (std::strcmp(type, "CURRENT") == 0)
return STATE_CURRENT;
if (std::strcmp(type, "REACHABLE") == 0)
return STATE_REACHABLE;
return STATE_UNREACHABLE; // DISTANT / NONE / other
}
// `C_TaxiMap.GetAllTaxiNodes([uiMapID])` — see module comment. The uiMapID
// arg is accepted for retail signature parity but ignored (vanilla has a
// single active taxi map — the open flight master's).
int __fastcall Script_GetAllTaxiNodes(void *L) {
Game::Lua::SetTop(L, 0);
Game::Lua::NewTable(L);
const int count =
*reinterpret_cast<const int *>(Offsets::VAR_TAXI_NODE_COUNT);
if (count <= 0)
return 1; // no taxi map open
auto typeFn = reinterpret_cast<TaxiNodeType_t>(Offsets::FUN_TAXI_NODE_TYPE);
const auto *arrayBase =
reinterpret_cast<const uint8_t *>(Offsets::VAR_TAXI_NODE_ARRAY);
for (int slot = 0; slot < count; ++slot) {
const uint8_t *slotRec = arrayBase + slot * Offsets::TAXI_NODE_STRIDE;
const uint8_t *dbcRec = *reinterpret_cast<const uint8_t *const *>(
slotRec + Offsets::OFF_TAXI_SLOT_RECORD);
const float x = FloatField(slotRec, Offsets::OFF_TAXI_SLOT_X);
const float y = FloatField(slotRec, Offsets::OFF_TAXI_SLOT_Y);
int nodeID = 0;
const char *name = nullptr;
if (dbcRec != nullptr) {
nodeID = IntField(dbcRec, 0); // TaxiNodes.dbc id @ +0
name = DBC::LocalizedField(Offsets::VAR_TAXINODES_RECORDS,
Offsets::VAR_TAXINODES_COUNT,
static_cast<uint32_t>(nodeID),
Offsets::OFF_TAXINODE_NAME);
}
const int state = StateFromType(typeFn(static_cast<uint32_t>(slot)));
Game::Lua::PushNumber(L, static_cast<double>(slot + 1));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "slotIndex", slot + 1); // TakeTaxiNode arg
Game::Lua::SetFieldString(L, "name", name ? name : "");
Game::Lua::SetFieldNumber(L, "nodeID", nodeID);
Game::Lua::SetFieldNumber(L, "state", state);
PushPosition(L, "position", x, y); // flight-map 0-1, matches retail
Game::Lua::SetTable(L, -3);
}
return 1;
}
void RegisterLuaFunctions() {
static const Game::Lua::EnumIntegerEntry kFaction[] = {
{"Neutral", FACTION_NEUTRAL},
{"Horde", FACTION_HORDE},
{"Alliance", FACTION_ALLIANCE},
};
Game::Lua::RegisterIntegerEnum("Enum", "FlightPathFaction", kFaction, 3);
static const Game::Lua::EnumIntegerEntry kState[] = {
{"Current", STATE_CURRENT},
{"Reachable", STATE_REACHABLE},
{"Unreachable", STATE_UNREACHABLE},
};
Game::Lua::RegisterIntegerEnum("Enum", "FlightPathState", kState, 3);
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetTaxiNodesForMap",
&Script_GetTaxiNodesForMap);
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetAllTaxiNodes",
&Script_GetAllTaxiNodes);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Taxi::Map
+87
View File
@@ -0,0 +1,87 @@
// 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/>.
// `UnitCreatureFamilyID(unit)` — the numeric CreatureFamily id (the
// `CreatureFamily.dbc` row: 1 = Wolf, 3 = Cat, 4 = Bear, …) for a unit,
// or `nil` when the unit has no family.
//
// The stock `UnitCreatureFamily(unit)` returns only the *localized name*
// ("Wolf", "Chat"), which is awkward to compare and locale-dependent. This
// extension exposes the raw id addons actually want for stable checks
// (tameable-pet logic, family-specific ability tables, etc.) — the same
// value that indexes `CreatureFamily.dbc`.
//
// This is exactly the id the engine's `Script_UnitCreatureFamily`
// (`0x0051A310`) reads before its DBC name lookup: its family helper
// `FUN_006055e0` returns `[unit + 0xB30] ? [row + 0x1C] : 0`, then the id is
// bounds-checked against `CreatureFamily.dbc` and used to push the localized
// name. We stop at the id. `nil` for:
// - Unresolvable unit token (`target` with nothing targeted, empty
// `partyN` slot, etc.).
// - Players and non-beast NPCs — family id is 0 (`UnitCreatureFamily`
// returns nil for these too).
// - NPCs whose creature data hasn't synced yet (no +0xB30 row).
//
// Raises a Lua error on a missing / non-string `unit` argument — same
// semantics as `UnitCreatureFamily` / `UnitSubName`.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Unit::CreatureFamily {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
int __fastcall Script_UnitCreatureFamilyID(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitCreatureFamilyID(\"unit\")");
return 0;
}
const char *token = Game::Lua::ToString(L, 1);
if (token == nullptr)
return 0;
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *unit = static_cast<const uint8_t *>(resolve(token));
if (unit == nullptr)
return 0;
auto *cacheRow = *reinterpret_cast<const uint8_t *const *>(
unit + Offsets::OFF_UNIT_CREATURE_CACHE_ROW);
if (cacheRow == nullptr)
return 0; // unsynced NPC / player unit
const int familyID = *reinterpret_cast<const int *>(
cacheRow + Offsets::OFF_CREATURE_CACHE_FAMILY);
if (familyID <= 0)
return 0; // no family (players, non-beast NPCs)
Game::Lua::PushNumber(L, static_cast<double>(familyID));
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitCreatureFamilyID",
&Script_UnitCreatureFamilyID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Unit::CreatureFamily
+81
View File
@@ -0,0 +1,81 @@
// 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/>.
// `UnitCreatureTypeID(unit)` — the numeric CreatureType id (the
// `CreatureType.dbc` row: 1 = Beast, 3 = Demon, 7 = Humanoid, …) for a
// unit, or `nil` when it has none.
//
// The stock `UnitCreatureType(unit)` returns only the *localized name*
// ("Humanoid", "Humanoïde"), which is awkward to compare and locale-
// dependent. This extension exposes the raw id — the twin of
// `UnitCreatureFamilyID`.
//
// It's exactly the value the engine's `Script_UnitCreatureType`
// (`0x0051A280`) resolves before its DBC name lookup, so we just call that
// same inner helper (`FUN_UNIT_CREATURE_TYPE`). The helper handles all three
// engine paths itself — a display-override, the creature-cache type, and the
// player-race type (players resolve to Humanoid via their race) — so a
// straight call matches `UnitCreatureType` for every unit kind. `nil` for:
// - Unresolvable unit token (`target` with nothing targeted, etc.).
// - A unit with no resolvable type (helper returns 0).
//
// Raises a Lua error on a missing / non-string `unit` argument — same
// semantics as `UnitCreatureType` / `UnitCreatureFamilyID`.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Unit::CreatureType {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using CreatureType_t = int(__fastcall *)(void *unit);
int __fastcall Script_UnitCreatureTypeID(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitCreatureTypeID(\"unit\")");
return 0;
}
const char *token = Game::Lua::ToString(L, 1);
if (token == nullptr)
return 0;
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
Offsets::FUN_RESOLVE_UNIT_TOKEN);
void *unit = resolve(token);
if (unit == nullptr)
return 0;
auto creatureType = reinterpret_cast<CreatureType_t>(
Offsets::FUN_UNIT_CREATURE_TYPE);
const int typeID = creatureType(unit);
if (typeID <= 0)
return 0; // no resolvable type
Game::Lua::PushNumber(L, static_cast<double>(typeID));
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitCreatureTypeID",
&Script_UnitCreatureTypeID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Unit::CreatureType
+7
View File
@@ -119,6 +119,13 @@ const uint8_t *PlayerObject() {
/*line*/ 0));
}
const uint8_t *PlayerInventoryManager() {
const uint8_t *player = PlayerObject();
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
const uint8_t *PlayerDescriptor() {
const uint8_t *player = PlayerObject();
if (player == nullptr)
+11
View File
@@ -44,6 +44,17 @@ const uint8_t *PlayerObject();
// re-deriving the player object + descriptor offset in each module.
const uint8_t *PlayerDescriptor();
// The local player's inventory manager (`PlayerObject() +
// OFF_PLAYER_INVENTORY_MANAGER`), or null. Layout: slot count at `+0x00`,
// flat GUID array at `+OFF_INVMGR_GUID_ARRAY`; it's the `this` that
// `GetItemBySlot` expects. This is the SAME pointer the throwing token
// resolver (`FUN_RESOLVE_UNIT_TOKEN("player")`) yields — both resolve the
// local player's GUID through the object manager (verified: the token
// resolver is `TokenToGUID → FUN_OBJECT_RESOLVE_BY_GUID`, differing only in
// the type-mask, which both the player object passes) — but non-throwing
// (null pre-world). Prefer this over re-deriving `player + 0x1D38` per module.
const uint8_t *PlayerInventoryManager();
// The 64-bit GUID of a resolved CGUnit/CGObject pointer, read through the
// pointer at `+0x08` to an instance block whose first 8 bytes are the GUID
// (verified in `Script_GetInventoryItemLink`; see `OFF_UNIT_GUID_PTR`).