|
| 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