Skip to content

Commit 6be25d7

Browse files
committed
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.
1 parent 06e68d0 commit 6be25d7

4 files changed

Lines changed: 310 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
4747
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
4848
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
4949
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
50-
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit`, `C_Map.GetMapOverlays` |
50+
| [Map](docs/API.md#map) | `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapOverlays` |
5151
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
5252
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
5353
| [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` |

docs/API.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ build instructions.
282282
- [`GetInboxItemLink(messageIndex[, attachmentIndex])`](#getinboxitemlinkmessageindex-attachmentindex)
283283

284284
- [Map](#map)
285+
- [`C_Map.GetAreaTriggerInfo(triggerID)` / `C_Map.GetAreaTriggers([mapID])`](#c_mapgetareatriggerinfotriggerid--c_mapgetareatriggersmapid)
285286
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
286287
- [`C_Map.GetMapOverlays([areaID])`](#c_mapgetmapoverlaysareaid)
287288

@@ -6847,6 +6848,75 @@ end
68476848
> reliable for placement; it's only the tile count they imply that
68486849
> lies.
68496850
6851+
### `C_Map.GetAreaTriggerInfo(triggerID)` / `C_Map.GetAreaTriggers([mapID])`
6852+
6853+
ClassicAPI extension. Exposes `AreaTrigger.dbc` — the client's 517
6854+
static trigger volumes (subzone-entry, exploration, and teleport
6855+
triggers). Vanilla loads the DBC but exposes **none** of its geometry
6856+
to Lua, which is why map addons (pfQuest et al.) ship hand-scraped
6857+
trigger tables; this is a live DBC read, same "surface the hidden data"
6858+
pattern as [`C_Map.GetMapOverlays`](#c_mapgetmapoverlaysareaid).
6859+
6860+
- `GetAreaTriggerInfo(triggerID)` — one trigger's info table, or `nil`
6861+
for a missing / out-of-range id.
6862+
- `GetAreaTriggers([mapID])` — an array of every trigger's info table,
6863+
optionally filtered to a single `Map.dbc` id (`0` Eastern Kingdoms,
6864+
`1` Kalimdor, `30` Alterac Valley, `489` Warsong Gulch, …).
6865+
6866+
Each info table carries both the **authoritative raw geometry** and a
6867+
**derived, ready-to-draw form**:
6868+
6869+
| field | meaning |
6870+
|---|---|
6871+
| `id` | `AreaTrigger.dbc` row id |
6872+
| `mapID` | `Map.dbc` id (continent, or an instance map) |
6873+
| `x` / `y` / `z` | continent-space **world** coordinates of the center |
6874+
| `radius` | sphere-trigger radius (`0` for a box trigger) |
6875+
| `isBox` | `true` when the trigger is an oriented box |
6876+
| `boxLength` / `boxWidth` / `boxHeight` / `boxYaw` | box dims + yaw (all `0` for a sphere) |
6877+
| `areaID` | `AreaTable` zone the point falls in — **absent** if unresolved |
6878+
| `mapX` / `mapY` | position as a `0..100` zone-relative percent — **absent** alongside `areaID` |
6879+
6880+
The raw `x`/`y`/`z` come straight from the DBC. `areaID`/`mapX`/`mapY`
6881+
are derived from the `WorldMapArea.dbc` zone rect (WoW's world axes:
6882+
`+X` north, `+Y` west). `mapX` is the **horizontal** map axis (off
6883+
world Y), `mapY` the **vertical** (off world X) — the standard WoW /
6884+
pfQuest / Astrolabe convention:
6885+
6886+
```
6887+
mapX% = (locLeft - y) / (locLeft - locRight) * 100 -- horizontal
6888+
mapY% = (locTop - x) / (locTop - locBottom) * 100 -- vertical
6889+
```
6890+
6891+
The zone is chosen by containment — among the zone rects on the
6892+
trigger's map that enclose the point, the smallest-area one wins (so a
6893+
subzone beats its parent). When no zone rect contains the point (open
6894+
sea, an instance with no `WorldMapArea` row, a degenerate rect), the
6895+
three derived fields are simply omitted — the raw world coords are
6896+
always present.
6897+
6898+
```lua
6899+
-- One trigger, its position as a world-map percent point:
6900+
local t = C_Map.GetAreaTriggerInfo(2)
6901+
if t.areaID then
6902+
print(t.areaID, t.mapX, t.mapY) -- 85 21.89 67.87 (Tirisfal)
6903+
end
6904+
6905+
-- Every trigger in Warsong Gulch:
6906+
for _, t in ipairs(C_Map.GetAreaTriggers(489)) do
6907+
-- t.x/y/z world coords, t.radius or t.isBox geometry, t.mapX/mapY
6908+
end
6909+
```
6910+
6911+
> **Matches the pfQuest-turtle trigger table 1:1.** Keyed by trigger
6912+
> id, the resolved `areaID` + `mapX`/`mapY` line up with
6913+
> `pfQuest-turtle`'s scraped `areatrigger-turtle.lua` (id 2 →
6914+
> `{21.89, 67.87, 85}` Tirisfal; id 45 → `{68, 17, 796}`). One
6915+
> difference in kind: a scraped table may list a trigger under several
6916+
> maps at once (a subzone *and* its parent continent); this resolves to
6917+
> the single **tightest** zone whose rect contains the point. An addon
6918+
> can drop `GetAreaTriggers()` straight in place of a shipped table.
6919+
68506920
Backports the six `C_MerchantFrame.*` calls retail addons use when
68516921
interacting with a vendor. All entry points read the engine's
68526922
merchant/buyback storage directly — no Lua-roundtrip through

src/Offsets.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,8 +1207,33 @@ enum Offsets {
12071207
// locLeft/locRight/locTop/locBottom floats}
12081208
VAR_WORLDMAP_AREA_RECORDS = 0x00C0D5BC,
12091209
VAR_WORLDMAP_AREA_COUNT = 0x00C0D5C0,
1210+
OFF_WMA_MAP_ID = 0x04,
12101211
OFF_WMA_AREA_ID = 0x08,
12111212
OFF_WMA_NAME = 0x0C,
1213+
OFF_WMA_LOC_LEFT = 0x10, // world Y at the map's left edge (max Y)
1214+
OFF_WMA_LOC_RIGHT = 0x14, // world Y at the map's right edge (min Y)
1215+
OFF_WMA_LOC_TOP = 0x18, // world X at the map's top edge (max X)
1216+
OFF_WMA_LOC_BOTTOM = 0x1C, // world X at the map's bottom edge (min X)
1217+
1218+
// AreaTrigger.dbc record (10 fields, 0x28 bytes) — instance cataloged
1219+
// in docs/DBCs.md (records slot 0x00C0E034, count 0x00C0E038). The
1220+
// client loads it but exposes NO geometry to Lua, which is why map
1221+
// addons (pfQuest et al.) ship hand-scraped trigger tables. Schema:
1222+
// {ID@+0, mapID@+4 (Map.dbc — continent or instance),
1223+
// x@+8, y@+0xC, z@+0x10 (continent-space world coords),
1224+
// radius@+0x14 (sphere trigger; 0 => box trigger),
1225+
// boxLength@+0x18, boxWidth@+0x1C, boxHeight@+0x20, boxYaw@+0x24}
1226+
VAR_AREATRIGGER_RECORDS = 0x00C0E034,
1227+
VAR_AREATRIGGER_COUNT = 0x00C0E038,
1228+
OFF_AT_MAP_ID = 0x04,
1229+
OFF_AT_X = 0x08,
1230+
OFF_AT_Y = 0x0C,
1231+
OFF_AT_Z = 0x10,
1232+
OFF_AT_RADIUS = 0x14,
1233+
OFF_AT_BOX_LENGTH = 0x18,
1234+
OFF_AT_BOX_WIDTH = 0x1C,
1235+
OFF_AT_BOX_HEIGHT = 0x20,
1236+
OFF_AT_BOX_YAW = 0x24,
12121237

12131238
// WorldMapOverlay.dbc record (17 fields, 0x44 bytes):
12141239
// {ID@+0, worldMapAreaID@+4 (-> WorldMapArea row),

src/map/AreaTriggers.cpp

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_Map.GetAreaTriggerInfo(triggerID)` and `C_Map.GetAreaTriggers([mapID])`
15+
// — expose `AreaTrigger.dbc`, the client's static trigger-volume geometry
16+
// (subzone-entry / exploration / teleport triggers). Vanilla loads the DBC
17+
// but exposes NONE of its geometry to Lua, so map addons (pfQuest, …) ship
18+
// hand-scraped trigger tables. Same value proposition as `GetMapOverlays`:
19+
// live, correct client data, no per-server table drift.
20+
//
21+
// ## Return shape (one info table)
22+
//
23+
// id — AreaTrigger.dbc row id
24+
// mapID — Map.dbc id (continent 0/1, or an instance map)
25+
// x, y, z — continent-space world coordinates of the trigger center
26+
// radius — sphere-trigger radius (0 for a box trigger)
27+
// isBox — true when the trigger is an oriented box (see below)
28+
// boxLength / boxWidth / boxHeight / boxYaw — box dimensions + yaw
29+
// (all 0 for a pure sphere trigger)
30+
// areaID — AreaTable zone id the point falls in (via WorldMapArea);
31+
// absent when no zone rect on this map contains the point
32+
// mapX / mapY — the trigger's position as a 0..100 map-relative percent
33+
// within that zone (the coordinate form map addons draw
34+
// with); absent alongside areaID when unresolved
35+
//
36+
// The raw world geometry is authoritative (straight from the DBC); the
37+
// derived `areaID`/`mapX`/`mapY` is the consumable form. Both are provided
38+
// so a consumer can draw immediately or run its own world→map transform.
39+
//
40+
// ## World → map-percent transform
41+
//
42+
// `WorldMapArea.dbc` gives each zone a world-coordinate rect
43+
// (`locTop`/`locBottom` bound world X, `locLeft`/`locRight` bound world Y —
44+
// WoW's world axes: +X north, +Y west). In map space the HORIZONTAL axis
45+
// (mapX) runs off world Y and the VERTICAL axis (mapY) off world X — the
46+
// standard WoW / pfQuest / Astrolabe convention:
47+
// mapX% = (locLeft - y) / (locLeft - locRight) * 100 (horizontal)
48+
// mapY% = (locTop - x) / (locTop - locBottom) * 100 (vertical)
49+
// We pick the zone by containment: among WorldMapArea rows on the trigger's
50+
// map with a real (non-continent) areaID whose rect encloses the point, the
51+
// smallest-area rect wins (the tightest zone — a subzone beats its parent).
52+
// The engine's own map-name resolver reads the same rect fields; see
53+
// `Map::Overlays` for the shared WorldMapArea wiring.
54+
55+
#include "Game.h"
56+
#include "Offsets.h"
57+
#include "dbc/Lookup.h"
58+
59+
#include <cstdint>
60+
61+
namespace Map::AreaTriggers {
62+
63+
namespace {
64+
65+
float FloatField(const uint8_t *rec, int off) {
66+
return *reinterpret_cast<const float *>(rec + off);
67+
}
68+
69+
// Finds the WorldMapArea zone that best contains world point (x, y) on
70+
// `mapID` and fills the map-relative percent. Returns false when no zone
71+
// rect on that map encloses the point (open sea, an instance with no
72+
// WorldMapArea row, a degenerate rect, …).
73+
bool ResolveZonePercent(int mapID, float x, float y, int *outAreaID,
74+
double *outMapX, double *outMapY) {
75+
const int count =
76+
*reinterpret_cast<const int *>(Offsets::VAR_WORLDMAP_AREA_COUNT);
77+
bool found = false;
78+
double bestArea = 0.0;
79+
for (int id = 1; id <= count; ++id) {
80+
const uint8_t *rec = DBC::Record(Offsets::VAR_WORLDMAP_AREA_RECORDS,
81+
Offsets::VAR_WORLDMAP_AREA_COUNT,
82+
static_cast<uint32_t>(id));
83+
if (rec == nullptr)
84+
continue;
85+
if (*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_MAP_ID) != mapID)
86+
continue;
87+
const int areaID =
88+
*reinterpret_cast<const int *>(rec + Offsets::OFF_WMA_AREA_ID);
89+
if (areaID == 0)
90+
continue; // continent-spanning row, not a zone
91+
92+
const double left = FloatField(rec, Offsets::OFF_WMA_LOC_LEFT);
93+
const double right = FloatField(rec, Offsets::OFF_WMA_LOC_RIGHT);
94+
const double top = FloatField(rec, Offsets::OFF_WMA_LOC_TOP);
95+
const double bottom = FloatField(rec, Offsets::OFF_WMA_LOC_BOTTOM);
96+
const double spanX = top - bottom; // > 0
97+
const double spanY = left - right; // > 0
98+
if (spanX <= 0.0 || spanY <= 0.0)
99+
continue; // degenerate / unmapped zone
100+
101+
if (x < bottom || x > top || y < right || y > left)
102+
continue; // point outside this zone's rect
103+
104+
const double area = spanX * spanY;
105+
if (found && area >= bestArea)
106+
continue; // a tighter zone already wins
107+
108+
found = true;
109+
bestArea = area;
110+
*outAreaID = areaID;
111+
// mapX = horizontal (world Y), mapY = vertical (world X).
112+
*outMapX = (left - y) / spanY * 100.0;
113+
*outMapY = (top - x) / spanX * 100.0;
114+
}
115+
return found;
116+
}
117+
118+
// Builds one trigger's info table and leaves it on the Lua stack top.
119+
// Returns false (pushing nothing) when `id` is out of range or the slot
120+
// is empty.
121+
bool PushTriggerInfo(void *L, int id) {
122+
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
123+
Offsets::VAR_AREATRIGGER_COUNT,
124+
static_cast<uint32_t>(id));
125+
if (rec == nullptr)
126+
return false;
127+
128+
const int mapID = *reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID);
129+
const float x = FloatField(rec, Offsets::OFF_AT_X);
130+
const float y = FloatField(rec, Offsets::OFF_AT_Y);
131+
const float z = FloatField(rec, Offsets::OFF_AT_Z);
132+
const float radius = FloatField(rec, Offsets::OFF_AT_RADIUS);
133+
const float boxLength = FloatField(rec, Offsets::OFF_AT_BOX_LENGTH);
134+
const float boxWidth = FloatField(rec, Offsets::OFF_AT_BOX_WIDTH);
135+
const float boxHeight = FloatField(rec, Offsets::OFF_AT_BOX_HEIGHT);
136+
const float boxYaw = FloatField(rec, Offsets::OFF_AT_BOX_YAW);
137+
const bool isBox = (boxLength != 0.0f || boxWidth != 0.0f || boxHeight != 0.0f);
138+
139+
Game::Lua::NewTable(L);
140+
Game::Lua::SetFieldNumber(L, "id", id);
141+
Game::Lua::SetFieldNumber(L, "mapID", mapID);
142+
Game::Lua::SetFieldNumber(L, "x", x);
143+
Game::Lua::SetFieldNumber(L, "y", y);
144+
Game::Lua::SetFieldNumber(L, "z", z);
145+
Game::Lua::SetFieldNumber(L, "radius", radius);
146+
Game::Lua::SetFieldBool(L, "isBox", isBox);
147+
Game::Lua::SetFieldNumber(L, "boxLength", boxLength);
148+
Game::Lua::SetFieldNumber(L, "boxWidth", boxWidth);
149+
Game::Lua::SetFieldNumber(L, "boxHeight", boxHeight);
150+
Game::Lua::SetFieldNumber(L, "boxYaw", boxYaw);
151+
152+
int areaID = 0;
153+
double mapX = 0.0, mapY = 0.0;
154+
if (ResolveZonePercent(mapID, x, y, &areaID, &mapX, &mapY)) {
155+
Game::Lua::SetFieldNumber(L, "areaID", areaID);
156+
Game::Lua::SetFieldNumber(L, "mapX", mapX);
157+
Game::Lua::SetFieldNumber(L, "mapY", mapY);
158+
}
159+
return true;
160+
}
161+
162+
// `C_Map.GetAreaTriggerInfo(triggerID)` — one trigger's info table, or nil
163+
// for a missing / out-of-range id.
164+
int __fastcall Script_GetAreaTriggerInfo(void *L) {
165+
const bool hasID = Game::Lua::IsNumber(L, 1);
166+
const int id = hasID ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
167+
Game::Lua::SetTop(L, 0);
168+
if (!hasID || !PushTriggerInfo(L, id))
169+
Game::Lua::PushNil(L);
170+
return 1;
171+
}
172+
173+
// `C_Map.GetAreaTriggers([mapID])` — array of every trigger's info table,
174+
// optionally filtered to a single Map.dbc id.
175+
int __fastcall Script_GetAreaTriggers(void *L) {
176+
const bool filter = Game::Lua::IsNumber(L, 1);
177+
const int wantMap = filter ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
178+
179+
Game::Lua::SetTop(L, 0);
180+
Game::Lua::NewTable(L);
181+
182+
const int count =
183+
*reinterpret_cast<const int *>(Offsets::VAR_AREATRIGGER_COUNT);
184+
int outIdx = 0;
185+
for (int id = 1; id <= count; ++id) {
186+
const uint8_t *rec = DBC::Record(Offsets::VAR_AREATRIGGER_RECORDS,
187+
Offsets::VAR_AREATRIGGER_COUNT,
188+
static_cast<uint32_t>(id));
189+
if (rec == nullptr)
190+
continue;
191+
if (filter &&
192+
*reinterpret_cast<const int *>(rec + Offsets::OFF_AT_MAP_ID) != wantMap)
193+
continue;
194+
195+
outIdx += 1;
196+
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
197+
PushTriggerInfo(L, id); // rec is valid, so this always pushes a table
198+
Game::Lua::SetTable(L, -3);
199+
}
200+
return 1;
201+
}
202+
203+
void RegisterLuaFunctions() {
204+
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggerInfo",
205+
&Script_GetAreaTriggerInfo);
206+
Game::Lua::RegisterTableFunction("C_Map", "GetAreaTriggers",
207+
&Script_GetAreaTriggers);
208+
}
209+
210+
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
211+
212+
} // namespace
213+
214+
} // namespace Map::AreaTriggers

0 commit comments

Comments
 (0)