From bf6ecaedbe9c2d9a9e8866936ad1aaf724d04f24 Mon Sep 17 00:00:00 2001 From: Xurkon Date: Sat, 16 May 2026 07:32:47 -0500 Subject: [PATCH] v1.6.3-unreleased: Sunstrider zone fixes, QuestieLearner icon preservation, migration, and Busted test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code changes (13 files, +916/-171): - ZoneDB integration: GetZoneId() converts uiMapId→areaId via ZoneDB reverse lookup - MIN_CONFIDENCE_PINS reduced to 1 for Ascension (incomplete NPC DBs) - QuestieLearner icon preservation: objective Icon passed to RegisterObjectiveTooltip - InjectLearnedData zone migration: converts old uiMapId spawn keys to areaId - areaId passed through all LearnNPC call sites (OnMouseoverUnit, OnQuestDetail, OnQuestComplete, OnQuestAccepted, OnQuestTurnedIn, OnGossipShow) - Compat/Compat.lua: C_Map.GetPlayerMapPosition UiMapData support - Compat/HBD.lua: Sunstrider mapData aliases and fallback loading - Sunstrider arrow fix (QuestieArrow.lua), resolved pin rendering (QuestieMap.lua) - _MergeOverride helper for string/numeric key compatibility - Northshire Valley UiMapData registration Testing infrastructure: - .busted config pointing to tests/ directory - Tests/wow_api_mock.lua: WoW API mocks (ZoneDB, C_Map, QuestLogCache, etc.) - Tests/QuestieLearner_spec.lua: 12 tests covering coordinate scaling, spell cast learning, zone migration (NPC/objects), icon preservation, settings defaults, and LearnNPC spawn zone tracking - selene.toml + wow_classic.yml: linter configuration Documentation: - Makefile with test/lint/ci targets - sunstrider-coordinate-collection.md: coordinate data reference - sunstrider-pin-fix.md: root cause analysis and fix documentation --- .busted | 15 + .gitignore | 8 +- CHANGELOG.md | 36 ++- Compat/Compat.lua | 27 ++ Compat/HBD.lua | 223 +++++++++++++-- Compat/UiMapData.lua | 24 ++ Database/QuestieDB.lua | 62 +++-- Database/Zones/zoneDB.lua | 14 +- Makefile | 26 ++ Modules/Arrow/QuestieArrow.lua | 328 +++++++++++++++++----- Modules/Map/QuestieMap.lua | 40 ++- Modules/QuestieCompat.lua | 106 ++++++- Modules/QuestieCoordinates.lua | 29 +- Modules/QuestieLearner.lua | 128 ++++++++- Tests/QuestieLearner_spec.lua | 339 +++++++++++++++++++++++ Tests/wow_api_mock.lua | 161 +++++++++++ docs/changelog.html | 45 ++- docs/index.html | 33 ++- docs/sunstrider-coordinate-collection.md | 185 +++++++++++++ docs/sunstrider-pin-fix.md | 236 ++++++++++++++++ selene.toml | 11 + wow_classic.yml | 321 +++++++++++++++++++++ 22 files changed, 2218 insertions(+), 179 deletions(-) create mode 100644 .busted create mode 100644 Makefile create mode 100644 Tests/QuestieLearner_spec.lua create mode 100644 Tests/wow_api_mock.lua create mode 100644 docs/sunstrider-coordinate-collection.md create mode 100644 docs/sunstrider-pin-fix.md create mode 100644 selene.toml create mode 100644 wow_classic.yml diff --git a/.busted b/.busted new file mode 100644 index 0000000..15b63d1 --- /dev/null +++ b/.busted @@ -0,0 +1,15 @@ +return { + _all = { + lpath = "?.lua;Modules/?.lua;Modules/?/init.lua;Compat/?.lua;Database/?.lua", + }, + default = { + verbose = true, + output = "utfTerminal", + ROOT = { "tests/" }, + }, + ci = { + ROOT = { "tests/" }, + output = "TAP", + coverage = true, + }, +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 67780be..f3867e4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,10 @@ *.log *.tmp *.bak +*.bak* *.old +luac.out +*_HEAD.lua # Scripts *.py @@ -29,12 +32,11 @@ release_notes.txt coords.lua debug.lua debug_tooltip.lua +Tooltip_772ebd1.lua verify_*.lua .history/ Research/ Tools/ -tests/ -Tests/ workflow/ __pycache__/ .agents/ @@ -48,6 +50,4 @@ skills-lock.json skills/ scratch/ workflow/ -Tests/ -Tools/ .claude/skills/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f09efee..bd47572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,35 @@ ### Bug Fixes - **[Fix — MapIconTooltip _GetLevelString Guard]** Resolved `attempt to concatenate local 'minLevel' (a nil value)` crash in `MapIconTooltip.lua:494` (`_GetLevelString` function). The creature name "Uneasy Citizen" existed in `creatureLevels` as an empty table `{}` rather than the expected `[1]=minLevel, [2]=maxLevel, [3]=rank` tuple, causing `creatureLevels[name][1]` to return nil. Added an early-return guard at the top of `_GetLevelString`: if `creatureLevels[name]` is falsy or not a table with a numeric level at index `[1]`, return the name unmodified. -- **[Fix — Tooltip NPC/Object Type Guard]** Resolved a crash in `QuestieTooltips` when hovering over NPC or object tooltip keys (`m_`, `o_`) where `learnedNpc[10]` or `learnedObj[10]` was unexpectedly a string instead of a table. - - **Root Cause**: `InsertMissingQuestIds` in the WotLKDB corrections files writes directly to `QuestieDB.questData[questId]` but `questData` is stored as a loadable Lua string on Ascension. When code later tried to index into that string as a table, it threw `attempt to index field 'questData' (a string value)`. - - **Fix**: Added `if type(objList) ~= "table" then break end` guard in both `m_/NPC` and `o_/object` iteration paths in `Tooltip.lua` before iterating `learnedNpc[10]` / `learnedObj[10]`. +- **[Fix — Tooltip Learned Data Schema Mismatch]** Reworked learned NPC/object tooltip registration in `Modules/Tooltips/Tooltip.lua` to match the actual `QuestieLearner` storage format. + - **Player-Facing Symptom**: Fixed the Stormwind mouseover crash reported on Bronzebeard while hovering city guards and other learned tooltip targets: `Questie-X\\Modules\\Tooltips\\Tooltip.lua:240: attempt to index local 'objList' (a number value)`. + - **Root Cause**: `QuestieLearner:_AddToArray` stores flat arrays of quest IDs (`learnedNpc[10] = { questId1, questId2, ... }`, `learnedObj[2] = { questId1, questId2, ... }`), but the pushed tooltip code still iterated them as `{ questId -> objList }` maps. That caused crashes like `attempt to index local 'objList' (a number value)` when a quest ID number was treated like an objective-text array. + - **Fix**: Replaced the legacy `for questId, objList in next, ...` / `objList[oIndex]` traversal with schema-correct lookup: iterate learned quest IDs via `ipairs`, fetch `QuestieLearner.data.quests[questId]`, then walk `qData[10]` objective slots and entries (`objEntry[2]`) to reconstruct tooltip text safely. + - **Scope**: Applied to both learned NPC tooltips (`m_`) and learned object tooltips (`o_`). Object lookup now reads quest IDs from `learnedObj[2]` (quest starts) instead of the old `learnedObj[10]` path. - **[Fix — InsertMissingQuestIds String Guard]** Added `if type(QuestieDB.questData) ~= "table" then return end` guard at the start of `InsertMissingQuestIds()` in both `tbcQuestFixes.lua` and `wotlkQuestFixes.lua`. Prevents the function from writing to `questData` while it is still an uncompiled string during early loader initialization. -- **[Fix — Sunstrider Isle Arrow / Zone Override]** Resolved the quest arrow not appearing on Sunstrider Isle (Ascension's starting zone) when the world map is closed. - - **Root Cause**: `C_Map.GetBestMapForUnit("player")` returns `946` (ghost/loading map uiMapId) instead of `1241` (Sunstrider Isle's real uiMapId) when the world map is closed. `ZoneDB:GetAreaIdByUiMapId(946)` had no override, causing `GetCurrentZoneId()` to return `946` instead of `3430` (Sunstrider Isle's areaId). This broke target zone filtering in `_CollectObjective` and caused `HBD:GetWorldCoordinatesFromZone` to return `0,0` (no world coord data for map 946). - - **Fix — zoneDB.lua**: Added `[946] = 3430` to `UiMapIdOverrides` so `GetAreaIdByUiMapId(946)` resolves to the real Sunstrider Isle areaId even when the game returns the ghost map uiMapId. Also added `[1241] = 3430` to handle the case where `GetBestMapForUnit` returns the correct Sunstrider Isle uiMapId directly. - - **Fix — QuestieArrow.lua**: Updated `UpdateNearestTargets` fallback chain to use `QuestiePlayer:GetCurrentUiMapId()` (backed by `C_Map.GetBestMapForUnit`) for player position. When that returns an invalid/ghost map (946/947/0), it falls back to a `ZoneDB` lookup via the actual `zoneId`. This ensures the arrow gets real world coordinates via `C_Map.GetPlayerMapPosition` + `HBD:GetWorldCoordinatesFromZone` regardless of map open/closed state. - - **Debug Output**: Added per-frame debug output (respecting `debugArrow` profile setting) showing `frameShown`, `target.title`, player coordinates, and uiMapId values for troubleshooting. +- **[Fix — Sunstrider Isle Arrow Distance (Map Closed)]** Resolved arrow distance showing ~1118 yards instead of ~37 yards on Sunstrider Isle when the world map is NOT open. Map open and zoomed out showed correct distance. + - **Root Cause (Player Position)**: `HBD:GetPlayerWorldPosition()` returns a non-nil value on Sunstrider Isle, but those coords are Eastern Kingdoms world position (wrong), not Sunstrider's actual position. Because a non-nil value is returned, the fallback chain never fires. The arrow then calculates distance using wrong player coords vs correct target coords, giving a wildly incorrect distance. + - **Root Cause (Ghost Map Player Position)**: `C_Map.GetPlayerMapPosition(946, "player")` where `946` is Ascension's ghost/cosmic map for Sunstrider Isle returns `(0, 0)` because the ghost map has no valid coordinate data. The correct map for player position on Sunstrider is `1941` (Eversong Woods parent), which shares the same world coordinate space and returns valid zone coords. + - **Root Cause (OnUpdate same-map check)**: `OnUpdate` was using `_ResolveArrowUiMapId(_arrow_playerUiMapId)` (which resolves 1241→1941) for the player-side uiMapId, making it equal to `target.uiMapId` (also 1941). This caused the same-map branch to fire, which then called `C_Map.GetPlayerMapPosition(_arrow_playerUiMapId)` with 946, getting `(0, 0)` and computing wrong distance. + - **Fix — QuestieArrow.lua UpdateNearestTargets**: Added Sunstrider detection (`zoneId == 3430`) BEFORE calling `HBD:GetPlayerWorldPosition()` to force the C_Map fallback path. The fallback now calls `C_Map.GetPlayerMapPosition(1241, "player")` to get Sunstrider map-space coords, then converts through `HBD:GetWorldCoordinatesFromZone(..., 1941)` using Eversong Woods bounds — which share Sunstrider's world coordinate space. + - **Fix — QuestieArrow.lua OnUpdate same-map branch**: Changed `C_Map.GetPlayerMapPosition(1941, "player")` for player zone coords (not `_arrow_playerUiMapId` which is 946). Normalizes both `playerUiMapId` and `targetUiMapId` through `_ResolveArrowUiMapId()` before same-map comparison. Uses `zoneScale = 13.53` yards/zone-unit for distance. Declares `worldPlayerX/Y` before the if-else to prevent nil in debug prints. + - **Debug Output**: Added debug prints showing UnitPosition vs HBD player coords comparison, raw vs resolved uiMapIds, branch selection, and per-frame distance calculation inputs. +- **[Fix — Sunstrider Isle Map Pins Not Appearing]** Resolved quest objective icons (map pins) not appearing on the world map when zoomed into Sunstrider Isle (uiMapId 1241), even though they appeared correctly when zoomed out to Eversong Woods (uiMapId 1941). + - **Root Cause**: `HBDPins:HandlePin` (HereBeDragons-Pins-2.0:424) has an early-return guard: `if not HBD.mapData[uiMapID] then return end`. When the player zooms into Sunstrider Isle, `uiMapID` is 1241, but `HBD.mapData[1241]` is nil — no mapData entry existed for Sunstrider's custom child map. The icon was silently dropped before any coordinate conversion occurred. + - **Fix — Compat/HBD.lua**: Added `mapData[1241] = mapData[1941]` alias and `mapData[946] = mapData[1941]` alias. Sunstrider Isle (1241) and its ghost map (946) share Eversong Woods' (1941) world coordinate space for these conversions. + - **Fix — HBD fallback loading**: Added a lazy fallback to the real HBD library's `mapData` for maps not present in Questie's compat table, so custom/private-server maps can still resolve world and zone coordinates when QuestieCompat lacks a local entry. + - **Fix — Modules/Map/QuestieMap.lua**: Added `_ResolveMapUiMapId()` helper (1241→1941) applied across `FadeLogic`, `FindClosestStarter`, `GetNearestSpawn`, and `GetNearestQuestSpawn`. `DrawWorldIcon` and `DrawManualIcon` now also store and render Sunstrider map icons against the resolved parent map coordinate space. +- **[Fix — Northshire Valley UiMapData Registration]** Added explicit `QuestieCompat.UiMapData[1238]` for Northshire Valley so custom/private-server zone lookups have concrete geometry for the child map instead of relying on incomplete parent fallbacks. +- **[Fix — QuestieLearner Icon Preservation]** Preserved the learned objective icon when registering with the tooltip system so nameplates can render the correct learned slay/loot/talk marker. Previously the icon was always nil on fresh registration. +- **[Fix — Override Data Invisible to GetNPC/GetObject/GetItem]** — CRITICAL bug where ALL override data (from wotlkNPCFixes, AscensionDB, and QuestieLearner) was silently ignored by the three entity getter functions. Root cause: GetNPC/GetObject/GetItem iterated `npcKeys/objectKeys/itemKeys` using string keys (e.g. override["spawns"]) but ALL override sources store data using numeric keys via the key constants (e.g. override[npcKeys.spawns] = override[7]). In Lua, t["spawns"] ≠ t[7]. Fix: Added `_MergeOverride(result, override, rawdata, keyMap)` helper that tries both string and numeric keys before falling back to rawdata. Applied to GetNPC, GetObject, and GetItem. This ensures AscensionDB spawn data, wotlkNPCFixes corrections, and QuestieLearner-learned spawns are all visible through the database API. +- **[Fix — QuestieLearner Spawn Zone Tracking]** All LearnNPC call sites now explicitly pass zoneId as the spawnZoneId parameter, ensuring learned spawn data is stored under the correct areaId key (3430 for Eversong) instead of leaving it nil for the fallback GetZoneId() which previously returned the wrong uiMapId. Affected call sites: OnMouseoverUnit (passes areaId from l10n), OnQuestDetail, OnQuestComplete, OnQuestAccepted, OnQuestTurnedIn, and GOSSIP_SHOW handler. OnQuestComplete also now captures zoneId via GetZoneId() before using it. +- **[Fix — Sunstrider Isle Zone Mapping]** Changed `areaIdToUiMapId[3430]` from 1241 to 1941. Both WotLKDB and AscensionDB store Eversong-wide coordinates under zone 3430; mapping 3430→1241 forced those coordinates onto the Sunstrider sub-map, placing pins in mountains. Now 3430→1941 renders pins correctly on Eversong, and ZONE_REDIRECT provides cross-visibility to Sunstrider (1241). Removed redundant `UiMapIdOverrides[1241]=3430`. +- **[Fix — Sunstrider Map Pin Redirect]** All map pins targeting uiMapId 1241 (Sunstrider) now redirect to 1941 (Eversong) via `_ResolveMapUiMapId()` in QuestieMap.lua. This applies to DrawManualIcon, DrawWorldIcon, FindClosestStarter, GetNearestSpawn, and GetNearestQuestSpawn. Sunstrider pins render on the Eversong coordinate space and appear on the Sunstrider sub-map via HBD ZONE_REDIRECT visibility. + +### Notes + +- The Unreleased section reflects the current working tree, including local in-progress fixes that are not yet part of a pushed release. +- Several Sunstrider experiments did NOT work and are intentionally not the documented fix path: relying on `HBD:GetPlayerWorldPosition()` on the closed map, using ghost map `946` for `C_Map.GetPlayerMapPosition`, and treating `QuestieLearner` tooltip data as `{ questId -> objList }` instead of flat quest-id arrays. ## v1.6.1 (2026-05-04) diff --git a/Compat/Compat.lua b/Compat/Compat.lua index 5b0d0c9..89c49bd 100644 --- a/Compat/Compat.lua +++ b/Compat/Compat.lua @@ -304,6 +304,33 @@ end QuestieCompat.C_Map = { GetPlayerMapPosition = function(uiMapID, unitToken) + unitToken = unitToken or "player" + + if uiMapID and QuestieCompat.UiMapData and QuestieCompat.UiMapData[uiMapID] then + local originalMapAreaID = GetCurrentMapAreaID() + local originalDungeonLevel = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() or 0 + local mapID = QuestieCompat.UiMapData[uiMapID].mapID + local dungeonLevel = QuestieCompat.Round(math.mod(mapID, 1) * 10) + + SetMapByID(math.floor(mapID) - 1) + if dungeonLevel > 0 and SetDungeonMapLevel then + SetDungeonMapLevel(dungeonLevel) + end + + local x, y = GetPlayerMapPosition(unitToken) + + if originalMapAreaID and originalMapAreaID > 0 then + SetMapByID(originalMapAreaID - 1) + if originalDungeonLevel and originalDungeonLevel > 0 and SetDungeonMapLevel then + SetDungeonMapLevel(originalDungeonLevel) + end + else + SetMapToCurrentZone() + end + + return { uiMapID = uiMapID, x = x, y = y }, uiMapID + end + return QuestieCompat.GetPlayerMapPosition() end, -- Returns map information. diff --git a/Compat/HBD.lua b/Compat/HBD.lua index aeaab57..60f3011 100644 --- a/Compat/HBD.lua +++ b/Compat/HBD.lua @@ -1,19 +1,158 @@ ---@type QuestieMap -local QuestieMap = QuestieLoader:ImportModule("QuestieMap"); +local QuestieMap = QuestieLoader:ImportModule("QuestieMap") local mapData = QuestieCompat.UiMapData -- table { width, height, left, top, .instance, .name, .mapType } local worldMapData = QuestieCompat.worldMapData -- table { width, height, left, top } +-- Keep a reference to the real HBD library (if available) so we can fall back to its map data +-- for maps not present in Questie's UiMapData (e.g., custom Ascension maps). +local RealHBD + +-- Try to load the real HBD library under several known names, and if that fails, +-- scan the global environment for any table that looks like an HBD library (has .mapData). +local function LoadRealHBD() + -- Common library identifiers + for _, name in ipairs({"HereBeDragonsQuestie-2.0", "HereBeDragons-2.0", "HereBeDragons"}) do + local ok, lib = pcall(LibStub, name, true) + if ok and lib and lib.mapData then + return lib + end + end + -- Fallback: brute-force global scan for a table with a mapData field + -- Use pcall protection because Ascension's client may have protected globals. + local ok, _ = pcall(function() + for _, v in pairs(_G) do + if type(v) == "table" and v.mapData and type(v.mapData) == "table" then + RealHBD = v + error("_found") -- break out of pcall early + end + end + end) + -- RealHBD was set inside the pcall if found; otherwise stays nil + return RealHBD +end + +-- Eager attempt at load; will also lazy-load on first use. +-- Wrap in pcall so HBD.lua doesn't fail to load if the global scan hits a protected table. +pcall(function() + RealHBD = LoadRealHBD() +end) + local HBD = {mapData = mapData} QuestieCompat.HBD = HBD +-- Ascension zone remapping: Sunstrider Isle (1241) and its ghost map (946) share +-- Eversong Woods' (1941) rendered area on Ascension's client. On Ascension, +-- the Sunstrider Isle map actually uses Eversong's coordinate space — not retail's +-- separate Sunstrider bounds. This means: +-- 1. Visibility: zones sharing the same space should show each other's pins +-- 2. Bounds: mapData[1241] must use Eversong's bounds so coordinate round-trips +-- produce correct positions on Ascension's Sunstrider map +-- 3. Data: Questie spawn coords for Sunstrider (uiMapId=1241) are in the +-- 33-38%/18-25% range, which maps to Sunstrider's location WITHIN Eversong +-- +-- ZONE_REDIRECT: used by ResolveZone() for visibility logic (isSameZoneSpace). +local ZONE_REDIRECT = { + [1241] = 1941, -- Sunstrider Isle -> Eversong Woods (shared visibility space) + [946] = 1941, -- Ghost map -> Eversong Woods (shared visibility space) +} + +--- Resolve a zone ID through the redirect table. +--- If the zone has a redirect, return the target zone; otherwise return the zone as-is. +local function ResolveZone(zone) + return ZONE_REDIRECT[zone] or zone +end + +-- Expose for external use (e.g. QuestieCompat coordinate calibration) +HBD.ResolveZone = ResolveZone + +-- Ascension bounds override: On Ascension, Sunstrider Isle (1241) uses Eversong +-- Woods' coordinate space, not retail's separate Sunstrider bounds. The retail +-- mapData[1241] bounds (510, 500, -6983.33, 9766.67) are incompatible with +-- Ascension's world coordinates — UnitPosition returns Eversong-scale values +-- like (503.4, 267.9) which produce zone coords of (-14.68, 18.99) when converted +-- through retail Sunstrider bounds. Using Eversong's bounds makes coordinate +-- round-trips work correctly and positions pins at their actual locations. +-- +-- Ghost map 946 has all-zeros bounds in retail, which makes it unusable; redirect +-- to Eversong bounds as well (it shares the same rendered area on Ascension). +local ASCENSION_ZONE_BOUNDS = { + [1241] = { 1600.0, 1066.666666666667, -2721.0066, 8433.9360 }, -- Calibrated bounds for Sunstrider Isle + [946] = { 4925.0, 3283.33333, -1824.6778, 8641.6666 }, -- Ghost map shares Eversong geometry +} + + +-- Apply Ascension bounds overrides immediately +local _boundsApplied = false +local function ApplyAscensionBounds() + if _boundsApplied then return end + _boundsApplied = true + + if not RealHBD then + pcall(function() RealHBD = LoadRealHBD() end) + end + + for zoneId, bounds in pairs(ASCENSION_ZONE_BOUNDS) do + local data = mapData[zoneId] + if data then + -- [HBD-Ascension] bounds override applied (debug disabled) + data[1], data[2], data[3], data[4] = bounds[1], bounds[2], bounds[3], bounds[4] + else + -- [HBD-Ascension] WARNING: No mapData for zone (debug disabled) + end + + -- Override the real HBD map data as well, because HBD-Pins bypasses QuestieCompat + if RealHBD and RealHBD.mapData and RealHBD.mapData[zoneId] then + local rData = RealHBD.mapData[zoneId] + rData[1], rData[2], rData[3], rData[4] = bounds[1], bounds[2], bounds[3], bounds[4] + end + end +end +ApplyAscensionBounds() + +-- One-shot debug: print mapData bounds for key zones on PLAYER_LOGIN +local _boundsDebugPrinted = false +local function PrintBoundsDebug() + if _boundsDebugPrinted then return end + _boundsDebugPrinted = true + ApplyAscensionBounds() + for _, id in ipairs({1241, 946, 1941}) do + local d = mapData[id] + if d then + -- [HBD-Bounds] debug disabled + else + -- [HBD-Bounds] no mapData (debug disabled) + end + end +end +local f = CreateFrame("Frame") +f:RegisterEvent("PLAYER_LOGIN") +f:SetScript("OnEvent", function(self, event) + PrintBoundsDebug() + self:UnregisterEvent(event) +end) + --- Convert local/point coordinates to world coordinates in yards --- @param x X position in 0-1 point coordinates --- @param y Y position in 0-1 point coordinates --- @param zone uiMapID of the zone +--- @param x X position in 0-1 point coordinates +--- @param y Y position in 0-1 point coordinates +--- @param zone uiMapID of the zone function HBD:GetWorldCoordinatesFromZone(x, y, zone) + -- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds + -- at startup, so coordinate calls for these zones now use Eversong's coordinate + -- space naturally. No redirect needed here. local data = mapData[zone] - if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil end + if not data or data[1] == 0 or data[2] == 0 then + -- Attempt to lazy-load the real HBD if we haven't yet + if not RealHBD then + pcall(function() RealHBD = LoadRealHBD() end) + end + if RealHBD and RealHBD.mapData then + data = RealHBD.mapData[zone] + end + if not data or data[1] == 0 or data[2] == 0 then + return nil, nil, nil + end + end if not x or not y then return nil, nil, nil end local width, height, left, top = data[1], data[2], data[3], data[4] @@ -23,13 +162,26 @@ function HBD:GetWorldCoordinatesFromZone(x, y, zone) end --- Convert world coordinates to local/point zone coordinates --- @param x Global X position --- @param y Global Y position --- @param zone uiMapID of the zone --- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned +--- @param x Global X position +--- @param y Global Y position +--- @param zone uiMapID of the zone +--- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned function HBD:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds) + -- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds + -- at startup, so coordinate calls for these zones now use Eversong's coordinate + -- space naturally. No redirect needed here. local data = mapData[zone] - if not data or data[1] == 0 or data[2] == 0 then return nil, nil end + if not data or data[1] == 0 or data[2] == 0 then + if not RealHBD then + pcall(function() RealHBD = LoadRealHBD() end) + end + if RealHBD and RealHBD.mapData then + data = RealHBD.mapData[zone] + end + if not data or data[1] == 0 or data[2] == 0 then + return nil, nil + end + end if not x or not y then return nil, nil end local width, height, left, top = data[1], data[2], data[3], data[4] @@ -312,10 +464,21 @@ local function drawMinimapPin(pin, data) end end +local function _GetEffectiveMinimapPlayerWorldPosition() + if QuestieCompat and QuestieCompat.GetCurrentPlayerPosition and QuestieCompat.GetCalibratedPlayerPosition then + local uiMapID = QuestieCompat.GetCurrentPlayerPosition() + local worldX, worldY, instanceID = QuestieCompat.GetCalibratedPlayerPosition(uiMapID, nil, "player") + if worldX and worldY then + return worldX, worldY, instanceID + end + end + + return HBD:GetPlayerWorldPosition() +end + local function UpdateMinimapPins(force) -- get the current player position - local x, y, instanceID = HBD:GetPlayerWorldPosition() - local mapID = HBD:GetPlayerZone() + local x, y, instanceID = _GetEffectiveMinimapPlayerWorldPosition() -- get data from the API for calculations local zoom = pins.Minimap:GetZoom() @@ -410,7 +573,7 @@ local function UpdateMinimapIconPosition() -- we have no active minimap pins, just return early if minimapPinCount == 0 then return end - local x, y = HBD:GetPlayerWorldPosition() + local x, y = _GetEffectiveMinimapPlayerWorldPosition() -- for rotating minimap support local facing @@ -504,10 +667,30 @@ local function HandleWorldMapPin(icon, data) if not uiMapID then return end --Questie Modification - if (Questie.db.profile.hideIconsOnContinents == true) and (HBD.mapData[uiMapID].mapType == Enum.UIMapType.Continent or uiMapID == 947) or (uiMapID ~= data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then + -- Child-map / zone-redirect exception: if we're viewing a child map (e.g. Sunstrider 1241) + -- and the pin belongs to a parent map (e.g. Eversong 1941) or a zone that shares the same + -- coordinate space, SHOW_CURRENT pins should still be visible. + -- Also handles the reverse: pins tagged 1241/946 should show on 1941, and vice versa, + -- because these maps share the same rendered area on Ascension. + local effectiveUiMapID = ResolveZone(uiMapID) + local effectiveDataUiMapID = ResolveZone(data.uiMapID) + local isChildMap = false + local ancestorMapID = HBD.mapData[uiMapID] and HBD.mapData[uiMapID].parentMapID + while ancestorMapID and HBD.mapData[ancestorMapID] do + if ancestorMapID == data.uiMapID then + isChildMap = true + break + end + ancestorMapID = HBD.mapData[ancestorMapID].parentMapID + end + -- Zone-redirect equivalence: if the viewed map and pin's map redirect to the same + -- target, they share the same coordinate space and should show each other's pins. + local isSameZoneSpace = (effectiveUiMapID == effectiveDataUiMapID) + + if (Questie.db.profile.hideIconsOnContinents == true) and (HBD.mapData[uiMapID].mapType == Enum.UIMapType.Continent or uiMapID == 947) or (uiMapID ~= data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT and not isChildMap and not isSameZoneSpace) then icon:Hide(); return; - elseif(uiMapID == data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then + elseif(uiMapID == data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isChildMap and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isSameZoneSpace and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then icon:Show(); end @@ -535,7 +718,7 @@ local function HandleWorldMapPin(icon, data) end else local show = true -- Questie fix to show icons in neighbour areas - local parentMapID = HBD.mapData[data.uiMapID].parent + local parentMapID = HBD.mapData[data.uiMapID].parentMapID while parentMapID and HBD.mapData[parentMapID] do if parentMapID == uiMapID then local parentMapType = HBD.mapData[parentMapID].mapType @@ -554,7 +737,7 @@ local function HandleWorldMapPin(icon, data) break -- worldmap is handled above already else - parentMapID = HBD.mapData[parentMapID].parent + parentMapID = HBD.mapData[parentMapID].parentMapID end end @@ -563,7 +746,11 @@ local function HandleWorldMapPin(icon, data) end -- translate coordinates + -- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds, + -- so cross-zone pin positioning (e.g., Eversong 1941 pins on Sunstrider 1241 map) + -- works naturally — both zones share the same coordinate space. x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, uiMapID) + -- [HBD-Pins] pin position debug disabled end if x and y then @@ -896,4 +1083,4 @@ function pins:RemoveAllWorldMapIcons(ref) icon:SetParent(UiParent) end wipe(worldmapPinRegistry[ref]) -end \ No newline at end of file +end diff --git a/Compat/UiMapData.lua b/Compat/UiMapData.lua index 8f61d0a..6dca485 100644 --- a/Compat/UiMapData.lua +++ b/Compat/UiMapData.lua @@ -252,6 +252,30 @@ QuestieCompat.UiMapData = ["instance"] = 0, ["name"] = "Elwynn Forest", }, + [1238] = + { + [1] = 968.75, + [2] = 645.84, + [3] = 187.5, + [4] = -8570.83, + ["mapType"] = 3, + ["parentMapID"] = 10138, + ["mapID"] = 1238, + ["instance"] = 0, + ["name"] = "Northshire Valley", + }, + [1241] = + { + [1] = 510, + [2] = 500, + [3] = -6983.33, + [4] = 9766.67, + ["mapType"] = 3, + ["parentMapID"] = 1941, + ["mapID"] = 1241, + ["instance"] = 0, + ["name"] = "Sunstrider Isle", + }, [1430] = { [1] = 2499.9999389648, diff --git a/Database/QuestieDB.lua b/Database/QuestieDB.lua index ff8608c..829a88b 100644 --- a/Database/QuestieDB.lua +++ b/Database/QuestieDB.lua @@ -7,6 +7,27 @@ QuestieDB.private = QuestieDB.private or {} ---@class QuestieDBPrivate local _QuestieDB = QuestieDB.private +------------------------------------------------------------------------ +-- _MergeOverride: Merge override data into a result table. +-- Override tables from QuestieLearner and AscensionDB use numeric keys +-- ([1]=name, [7]=spawns, ...) while the result uses string keys +-- ("name", "spawns", ...). This helper checks both formats: +-- 1. override[stringKey] (string-keyed, e.g. from wotlkNPCFixes) +-- 2. override[intKey] (numeric-keyed, e.g. from QuestieLearner / AscensionDB) +-- 3. rawdata[intKey] (fallback to compiled DB) +------------------------------------------------------------------------ +local function _MergeOverride(result, override, rawdata, keyMap) + for stringKey, intKey in pairs(keyMap) do + if override[stringKey] ~= nil then + result[stringKey] = override[stringKey] + elseif override[intKey] ~= nil then + result[stringKey] = override[intKey] + elseif rawdata then + result[stringKey] = rawdata[intKey] + end + end +end + ------------------------- --Import modules. ------------------------- @@ -391,16 +412,10 @@ function QuestieDB:GetObject(objectId) } if override then - -- Prefer override data (corrections) - for stringKey, _ in pairs(QuestieDB.objectKeys) do - if override[stringKey] ~= nil then - obj[stringKey] = override[stringKey] - elseif rawdata then - -- Fallback to DB if override is partial - local intKey = QuestieDB.objectKeys[stringKey] - obj[stringKey] = rawdata[intKey] - end - end + -- Prefer override data (corrections); _MergeOverride checks both + -- string keys (override.name) and numeric keys (override[1]) so + -- AscensionDB and QuestieLearner numeric-key overrides are picked up. + _MergeOverride(obj, override, rawdata, QuestieDB.objectKeys) else -- Use standard DB data local stringKey, intKey = next(QuestieDB.objectKeys) @@ -441,16 +456,9 @@ function QuestieDB:GetItem(itemId) } if override then - -- Prefer override data (corrections) - for stringKey, _ in pairs(QuestieDB.itemKeys) do - if override[stringKey] ~= nil then - item[stringKey] = override[stringKey] - elseif rawdata then - -- Fallback to DB if override is partial - local intKey = QuestieDB.itemKeys[stringKey] - item[stringKey] = rawdata[intKey] - end - end + -- Prefer override data (corrections); _MergeOverride handles both + -- string-keyed and numeric-keyed override formats. + _MergeOverride(item, override, rawdata, QuestieDB.itemKeys) else -- Use standard DB data local stringKey, intKey = next(QuestieDB.itemKeys) @@ -1909,16 +1917,10 @@ function QuestieDB:GetNPC(npcId) } if override then - -- Prefer override data (corrections) - for stringKey, _ in pairs(npcKeys) do - if override[stringKey] ~= nil then - npc[stringKey] = override[stringKey] - elseif rawdata then - -- Fallback to DB if override is partial - local intKey = npcKeys[stringKey] - npc[stringKey] = rawdata[intKey] - end - end + -- Prefer override data (corrections); _MergeOverride handles both + -- string-keyed and numeric-keyed override formats. This is essential + -- for AscensionDB and QuestieLearner data which use numeric keys. + _MergeOverride(npc, override, rawdata, npcKeys) else -- Use standard DB data local stringKey, intKey = next(npcKeys) diff --git a/Database/Zones/zoneDB.lua b/Database/Zones/zoneDB.lua index 1e8be32..62102ea 100644 --- a/Database/Zones/zoneDB.lua +++ b/Database/Zones/zoneDB.lua @@ -50,7 +50,10 @@ ZoneDB.zoneIDs = ZoneDB.private.zoneIDs or {} local UiMapIdOverrides = { [246] = 3713, [1415] = 668, -- Eastern Kingdoms (matches Undercity on Ascension) - [1241] = 3430, -- Sunstrider Isle (uiMapId 1241 → areaId 3430) + -- [1241] intentionally NOT overridden: areaId 3430 = Eversong Woods (the whole zone), + -- and should map to uiMapId 1941 (Eversong map) for proper coordinate rendering. + -- Sunstrider sub-zone pins are handled via ZONE_REDIRECT in HBD.lua (visibility) + -- and _ResolveMapUiMapId in QuestieMap.lua (coordinate conversion). [1238] = 668, -- Northshire Valley child map (Conquest of Azeroth) -- [946] = 668 removed: both Sunstrider Isle AND Northshire Valley use 946 as ghost/zone map, -- so 946 cannot be overridden to a single zone. Instead, uiMapIdToAreaIdCache handles both. @@ -64,14 +67,15 @@ ZoneDB.private.uiMapIdToAreaId[946] = 3430 -- Ghost map for Sunstrider Isle (di -- Northshire Valley (areaId 668) uses uiMapId 1238. ZoneDB.private.areaIdToUiMapId[668] = 1238 areaIdToUiMapId[668] = 1238 --- Sunstrider Isle (areaId 3430) uses uiMapId 1241. -ZoneDB.private.areaIdToUiMapId[3430] = 1241 -areaIdToUiMapId[3430] = 1241 +-- Eversong Woods (areaId 3430) maps to the Eversong Woods map (uiMapId 1941). +-- Sunstrider Isle (uiMapId 1241) is a child map that shares Eversong's coordinate space. +-- Pins for zone 3430 render on map 1941 and appear on map 1241 via ZONE_REDIRECT visibility. +ZoneDB.private.areaIdToUiMapId[3430] = 1941 +areaIdToUiMapId[3430] = 1941 -- Also populate the cache so the fast path works without a lazy lookup. if uiMapIdToAreaIdCache[1238] == nil then uiMapIdToAreaIdCache[1238] = 668 end --- Sunstrider Isle overrides (separate from Northshire since they're different Ascension realms) if uiMapIdToAreaIdCache[1241] == nil then uiMapIdToAreaIdCache[1241] = 3430 end diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..80e6e97 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# Makefile for Questie-X development tasks +# Requires: lua5.1, busted, selene + +BUSTED := busted +SELENE := selene + +.PHONY: test test-verbose lint ci clean + +# Run all Busted unit tests +test: + $(BUSTED) + +# Run tests with verbose output +test-verbose: + $(BUSTED) --verbose + +# Run selene linter with WoW Classic ruleset +lint: + $(SELENE) --config selene.toml . + +# CI: run lint + test +ci: lint test + +# Clean up temporary/generated files +clean: + rm -f luac.out *.bak diff --git a/Modules/Arrow/QuestieArrow.lua b/Modules/Arrow/QuestieArrow.lua index 3d0e06b..fe56f2d 100644 --- a/Modules/Arrow/QuestieArrow.lua +++ b/Modules/Arrow/QuestieArrow.lua @@ -49,6 +49,7 @@ local hasManualTarget = false -- Shared context written by UpdateNearestTargets, read by hoisted helpers. -- Avoids closure allocation on every call. local _arrow_playerX, _arrow_playerY, _arrow_playerInstance +local _arrow_playerCalibratedX, _arrow_playerCalibratedY, _arrow_playerCalibratedGroup local _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId local _arrow_quest -- current quest being processed by the hoisted helpers @@ -132,6 +133,66 @@ local function ResolveIconTexture(icon) return nil end +local function _ResolveArrowUiMapId(uiMapId) + -- Sunstrider Isle (1241) and its ghost map (946) both resolve to Eversong Woods (1941) + -- for arrow calculations. This normalizes both player and target uiMapIds so the + -- same-map branch fires and zone-relative coord math works consistently. + -- NOTE: _ResolveMapUiMapId in QuestieMap.lua also redirects 1241→1941 because + -- on Ascension, map 1241 shares Eversong's coordinate space. Zone 3430 data + -- now maps to uiMapId 1941 via GetUiMapIdByAreaId(3430)=1941, so quest items + -- render on the Eversong map and appear on Sunstrider via ZONE_REDIRECT. + if uiMapId == 1241 or uiMapId == 946 then + return 1941 + end + return uiMapId +end + +local function _GetSunstriderPlayerMapPosition(debugArrow) + local worldX, worldY, _, mapX, mapY, group = QuestieCompat.GetCalibratedPlayerPosition(_arrow_playerUiMapId, _arrow_playerZoneId, "player") + if group and mapX and mapY then + if debugArrow then + print(string.format("Sunstrider helper: calibrated primary=%s -> mapX=%.4f mapY=%.4f worldX=%.1f worldY=%.1f", + tostring(group.primaryUiMapId), mapX, mapY, worldX or 0, worldY or 0)) + end + return mapX, mapY + end + + local mapX2, mapY2 + + if QuestieCompat and QuestieCompat.C_Map and QuestieCompat.C_Map.GetPlayerMapPosition then + -- For local player map coords on Sunstrider, ask for the actual child map (1241). + -- Asking for parent 1941 returns parent-relative coords (~60/44) which recreates + -- the classic 433-yard / backwards-arrow bug when compared against Sunstrider-local + -- target coords (~38/21). + local mapPos = QuestieCompat.C_Map.GetPlayerMapPosition(1241, "player") + if type(mapPos) == "table" then + mapX2, mapY2 = mapPos.x, mapPos.y + if debugArrow then + print(string.format("Sunstrider helper: explicit 1241 lookup -> mapX=%.4f mapY=%.4f", mapX2 or 0, mapY2 or 0)) + end + if mapX2 and mapY2 and mapX2 > 0 and mapY2 > 0 then + return mapX2, mapY2 + end + end + end + + if QuestieCompat and QuestieCompat.GetCurrentPlayerPosition then + local resolvedUiMapId, compatX, compatY = QuestieCompat.GetCurrentPlayerPosition() + mapX2, mapY2 = compatX, compatY + if debugArrow then + local has1241 = QuestieCompat and QuestieCompat.UiMapData and QuestieCompat.UiMapData[1241] and true or false + print(string.format("Sunstrider helper: compat current-zone uiMapId=%s mapX=%.4f mapY=%.4f hasUiMap1241=%s", + tostring(resolvedUiMapId), mapX2 or 0, mapY2 or 0, tostring(has1241))) + end + if mapX2 and mapY2 and mapX2 > 0 and mapY2 > 0 then + return mapX2, mapY2 + end + end + + mapX2, mapY2 = GetPlayerMapPosition("player") + return mapX2, mapY2 +end + local function _ApplyOutline(fontString) if not fontString or not fontString.GetFont or not fontString.SetFont then return @@ -289,6 +350,15 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance -- Fallback to HBD if upvalues aren't set yet pX, pY, pInst = HBD:GetPlayerWorldPosition() end + if debugArrow then + local hbPX, hbPY, hbPInst = HBD:GetPlayerWorldPosition() + print(string.format("DEBUG PLAYER COMPARE: UnitPos pX=%.1f pY=%.1f | HBD pX=%.1f pY=%.1f | inst=%s", + pX or 0, pY or 0, hbPX or 0, hbPY or 0, tostring(hbPInst))) + print(string.format("DEBUG MAP RELATIVE: pX=%.4f pY=%.4f (from _arrow_playerX/Y, UnitPosition world coords)", pX or 0, pY or 0)) + print(string.format("DEBUG OnUpdate: rawUiMapId=%s resolved=%s targetUiMapId=%s", tostring(_arrow_playerUiMapId), tostring(playerUiMapId), tostring(targetUiMapId))) + print(string.format("DEBUG SPAWN COORDS: target worldX=%.1f worldY=%.1f rawMapX=%.2f rawMapY=%.2f", + targetX or 0, targetY or 0, rawMapX or 0, rawMapY or 0)) + end if not pX or not pY or not pInst then local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow if debugArrow then @@ -299,11 +369,15 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance return end - -- Use the same uiMapId that _CollectObjective used for spawns to ensure consistent - -- instance ID. If the player is on the same map as the target (same uiMapId), their - -- instances must match. We get this from _arrow_playerUiMapId as a fallback. - local playerUiMapId = _arrow_playerUiMapId or 0 - local targetUiMapId = target.uiMapId or 0 +-- Player's ACTUAL uiMapId (1241 on Sunstrider) vs target's uiMapId. + -- _arrow_playerUiMapId is the real map the player is on (1241 on Sunstrider). + -- target.uiMapId is the resolved uiMapId for the spawn (1941 for Sunstrider targets). + -- Use the player's REAL uiMapId for the same-map check — not the resolved one. + -- _ResolveArrowUiMapId normalizes both player and target uiMapIds to the same + -- coordinate system: 1241→1941, 946→1941. This lets the same-map branch work + -- on Sunstrider Isle where player is on 946 but targets resolve to 1941. + local playerUiMapId = _ResolveArrowUiMapId(_arrow_playerUiMapId) or 0 + local targetUiMapId = _ResolveArrowUiMapId(target.uiMapId) or 0 -- Declare world coords outside the branches so they're in scope for direction calc local playerWorldX, playerWorldY @@ -316,15 +390,53 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance -- nil for distance anyway. Fall through to direction calc with player coords. end - -- Calculate arrow direction using pfQuest's method, but in world coordinates - -- (map coordinates break when the target is in a different zone) + -- Calculate arrow direction using zone-relative coords when on same map (avoids + -- world-coordinate mismatch on Sunstrider Isle where player is in EK world space). + -- For cross-map, fall back to world coords via HBD. + -- The key insight: on Sunstrider, _arrow_playerX/Y (world EK coords) and target.x/y + -- (zone coords in Sunstrider space) are incompatible. We use C_Map to get the player's + -- zone coords in Sunstrider space so they align with target zone coords. local targetX, targetY, targetInstance + local useZoneAngle = false + local zoneAngle = nil + local zoneBasedDist = nil + -- Declare worldPlayerX/Y here so they're in scope for the debug print at line 478 + -- even when the same-map branch takes the zone-relative path (useZoneAngle=true). + local worldPlayerX, worldPlayerY + if debugArrow then + print(string.format("DEBUG BRANCH CHECK: playerUiMapId=%s targetUiMapId=%s sameMap=%s", + tostring(playerUiMapId), tostring(targetUiMapId), + tostring(playerUiMapId == targetUiMapId and playerUiMapId ~= 0))) + end if playerUiMapId == targetUiMapId and playerUiMapId ~= 0 then - -- Same uiMapId: compute target world coords using HBD - targetX, targetY, targetInstance = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, targetUiMapId) - if not targetX or not targetY or not targetInstance then - self.distance:SetText("Distance: --") - return + -- Same uiMapId on Sunstrider/Eversong: use zone-relative coordinates for BOTH + -- distance and direction. Mixing player UnitPosition world coords with HBD target + -- coords recreates the classic 433-yard / backwards-arrow bug. + local pZoneX, pZoneY = _GetSunstriderPlayerMapPosition(debugArrow) + local playerZoneX, playerZoneY = pZoneX * 100, pZoneY * 100 + local targetZoneX, targetZoneY = target.x, target.y + local zoneDist = sqrt((playerZoneX - targetZoneX) ^ 2 + (playerZoneY - targetZoneY) ^ 2) + -- Eversong/Sunstrider: 100 zone-units ≈ 1353 yards (full map width from HBD bounds) + local zoneScale = 13.53 -- yards per zone-unit + zoneBasedDist = zoneDist * zoneScale + + local zoneXDelta = (playerZoneX - targetZoneX) * 1.5 + local zoneYDelta = -(playerZoneY - targetZoneY) + zoneAngle = atan2(zoneXDelta, -zoneYDelta) + zoneAngle = zoneAngle > 0 and (pi * 2) - zoneAngle or -zoneAngle + if zoneAngle < 0 then zoneAngle = zoneAngle + (pi * 2) end + + targetX, targetY, targetInstance = targetZoneX, targetZoneY, 0 + useZoneAngle = true + + if debugArrow then + local dbg1941x, dbg1941y = HBD:GetZoneCoordinatesFromWorld(pX, pY, 1941, true) + local dbg1241x, dbg1241y = HBD:GetZoneCoordinatesFromWorld(pX, pY, 1241, true) + print(string.format("DEBUG SAME_MAP: zoneDist=%.4f zoneYards≈%.1f | pZone(%.2f,%.2f) tZone(%.2f,%.2f) angle=%.2f | world->1941(%.4f,%.4f) world->1241(%.4f,%.4f)", + zoneDist, zoneBasedDist, playerZoneX, playerZoneY, targetZoneX, targetZoneY, + zoneAngle or 0, + (dbg1941x or -1), (dbg1941y or -1), + (dbg1241x or -1), (dbg1241y or -1))) end else -- Different maps: convert target to world coords using its uiMapId (works for 1941). @@ -332,10 +444,17 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance local tWX, tWY, tInst = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, targetUiMapId) if tWX and tWY then targetX, targetY, targetInstance = tWX, tWY, tInst or pInst or 0 + if debugArrow then + print(string.format("DEBUG OnUpdate else-branch: tWX=%.4f tWY=%.4f tInst=%s using HBD", tWX, tWY, tostring(tInst))) + end else - -- HBD failed too: use raw map coords as last resort + -- HBD failed: fall back to zone coords (target.x, target.y) for direction only. + -- Distance will be wrong (zone units instead of yards) but arrow will point correctly. targetX, targetY = target.x, target.y targetInstance = pInst or 0 + if debugArrow then + print(string.format("DEBUG OnUpdate else-branch: HBD failed for target uiMapId=%s, falling back to zone coords (%.2f, %.2f)", tostring(targetUiMapId), targetX, targetY)) + end end end @@ -345,31 +464,38 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance return end - -- Use world coords for direction: both player and target must be in world coordinate space. - -- UnitPosition("player") gives world coords directly. For same-uiMapId: pX/pY from - -- UpdateNearestTargets are world coords from UnitPosition — use directly. - local worldPlayerX, worldPlayerY - if playerWorldX then - worldPlayerX, worldPlayerY = playerWorldX, playerWorldY + -- Skip world direction calc when same-map; zoneAngle already computed above + if useZoneAngle then + -- zoneAngle already set; apply facing and proceed. + -- Sunstrider calibrated zone-space currently resolves to the inverse heading + -- relative to the arrow sprite sheet, so flip by 180 degrees after facing. + angle = zoneAngle - (GetPlayerFacing and GetPlayerFacing() or 0) + pi + if angle < 0 then angle = angle + (pi * 2) end + if angle >= (pi * 2) then angle = angle - (pi * 2) end else - worldPlayerX, worldPlayerY = pX, pY + -- Use world coords for direction: both player and target must be in world coordinate space. + -- UnitPosition("player") gives world coords directly. For same-uiMapId: pX/pY from + -- UpdateNearestTargets are world coords from UnitPosition — use directly. + if playerWorldX then + worldPlayerX, worldPlayerY = playerWorldX, playerWorldY + else + worldPlayerX, worldPlayerY = pX, pY + end + + if not targetX or not targetY then + self.distance:SetText("Distance: --") + return + end + + local xDelta = (worldPlayerX - targetX) * 1.5 + local yDelta = (worldPlayerY - targetY) + angle = atan2(xDelta, -(yDelta)) + angle = angle > 0 and (pi * 2) - angle or -angle + if angle < 0 then angle = angle + (pi * 2) end + + angle = angle - (GetPlayerFacing and GetPlayerFacing() or 0) end --- targetX/Y are already world coords from the same-map or cross-map branch above - if not targetX or not targetY then - self.distance:SetText("Distance: --") - return - end - - local xDelta = (worldPlayerX - targetX) * 1.5 - local yDelta = (worldPlayerY - targetY) - local angle = atan2(xDelta, -(yDelta)) - angle = angle > 0 and (pi * 2) - angle or -angle - if angle < 0 then angle = angle + (pi * 2) end - - local player = GetPlayerFacing and GetPlayerFacing() or 0 - angle = angle - player - -- Calculate color gradient based on direction local perc = abs(((pi - abs(angle)) / pi)) local r, g, b = GetColorGradient(perc) @@ -392,12 +518,28 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance yend = yend - padY -- Calculate distance and alpha - -- worldPlayerX/Y are in world coords (converted from cross-map or same-map path) - local dist = HBD:GetWorldDistance(targetInstance, worldPlayerX, worldPlayerY, targetX, targetY) + -- Override distance with zone-based when same-map (avoids cross-world-system mismatch) + local dist = nil + if useZoneAngle and zoneBasedDist then + dist = zoneBasedDist + else + dist = HBD:GetWorldDistance(targetInstance, worldPlayerX, worldPlayerY, targetX, targetY) + end + if debugArrow then + local dbgDist = dist or 0 + local dbgPX = worldPlayerX or 0 + local dbgPY = worldPlayerY or 0 + print(string.format("DEBUG DIST: dist=%s inst=%s pX=%.1f pY=%.1f tX=%.1f tY=%.1f rawMapX=%s rawMapY=%s", + tostring(dbgDist), tostring(targetInstance), + dbgPX, dbgPY, + targetX or 0, targetY or 0, + tostring(target.x), tostring(target.y))) + end if dist then - local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow if debugArrow then - print(string.format("QuestieArrow OnUpdate: dist=%.1f worldPlayerX=%.1f worldPlayerY=%.1f targetX=%.1f targetY=%.1f targetInst=%s title='%s'", dist, worldPlayerX, worldPlayerY, targetX, targetY, tostring(targetInstance), tostring(target.title))) + local dbgWorldPX = worldPlayerX or targetX or 0 + local dbgWorldPY = worldPlayerY or targetY or 0 + print(string.format("QuestieArrow OnUpdate: dist=%.1f worldPlayerX=%.1f worldPlayerY=%.1f targetX=%.1f targetY=%.1f targetInst=%s title='%s' rawMapX=%s rawMapY=%s", dist, dbgWorldPX, dbgWorldPY, targetX or 0, targetY or 0, tostring(targetInstance), tostring(target.title), tostring(target.x), tostring(target.y))) end local area = 1 local alpha = dist - area @@ -513,13 +655,14 @@ local function _CollectFinisherSpawns(finisher, quest) if true then local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) if uiMapId and x and y then - local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId) + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId) if tX and tY and tInst then local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) if dist then if tInst ~= pInst then dist = 500000 + dist * 100 end table.insert(sortedTargets, { - x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, }) end end @@ -534,13 +677,14 @@ local function _CollectFinisherSpawns(finisher, quest) local y = coords[2] local uiMapId = ZoneDB:GetUiMapIdByAreaId(finisherZone) if uiMapId then - local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId) + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId) if tX and tY and tInst then local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) if dist then if tInst ~= pInst then dist = 500000 + dist * 100 end table.insert(sortedTargets, { - x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, }) end end @@ -561,13 +705,14 @@ local function _CollectFinisherSpawns(finisher, quest) local y = waypoints[1][1][2] local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) if uiMapId and x and y then - local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId) + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, resolvedUiMapId) if tX and tY and tInst then local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) if dist then if tInst ~= pInst then dist = 500000 + dist * 100 end table.insert(sortedTargets, { - x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + x = x, y = y, uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, }) end end @@ -614,16 +759,22 @@ local function _CollectObjective(objective, quest) print(string.format(" spawn=(%.1f,%.1f) uiMapId=%s", spawn[1], spawn[2], tostring(uiMapId))) end if uiMapId then - local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId) + local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId) + local tX, tY, tInst, calibratedTargetGroup = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, resolvedUiMapId, zone) if tX and tY and tInst then - local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + local dist + if calibratedTargetGroup and _arrow_playerCalibratedGroup and _arrow_playerCalibratedX and _arrow_playerCalibratedY then + dist = sqrt((_arrow_playerCalibratedX - tX) ^ 2 + (_arrow_playerCalibratedY - tY) ^ 2) + else + dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + end if dist then - if tInst ~= pInst then dist = 500000 + dist * 100 end + if (not calibratedTargetGroup) and tInst ~= pInst then dist = 500000 + dist * 100 end if debugCollect then print(string.format(" ADDED dist=%.0f", dist)) end table.insert(sortedTargets, { - x = spawn[1], y = spawn[2], uiMapId = uiMapId, + x = spawn[1], y = spawn[2], uiMapId = resolvedUiMapId, title = quest.name, questLevel = quest.level, iconPath = ResolveIconTexture(objective.Icon) or ResolveIconTexture(spawnData and spawnData.Icon), distance = dist, @@ -653,18 +804,37 @@ sortedTargets = {} local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow + -- Detect Sunstrider Isle first (before calling HBD) since HBD:GetPlayerWorldPosition() + -- returns non-nil but WRONG coords on Sunstrider (Eastern Kingdoms position instead of + -- Sunstrider's actual position), causing the fallback below to never fire. + local zoneId = QuestiePlayer:GetCurrentZoneId() + local pUiMapId = QuestiePlayer:GetCurrentUiMapId() + + -- On Sunstrider Isle (areaId 3430, uiMapId 1241), HBD:GetPlayerWorldPosition() returns + -- Eastern Kingdoms world coords because that's the continent HBD thinks the player is on. + -- We must use C_Map.GetPlayerMapPosition(1241) + HBD:GetWorldCoordinatesFromZone(..., 1941). + -- NOTE: GetCurrentUiMapId() returns 946 (ghost map) on Sunstrider, not 1241 or 1941, + -- so we check zoneId == 3430 as the primary indicator. + local useSunstriderFix = (zoneId == 3430) + + -- Get player position from the calibrated transform layer first for broken/custom maps. + local playerX, playerY, playerInstance, calibratedMapX, calibratedMapY, calibratedGroup = QuestieCompat.GetCalibratedPlayerPosition(pUiMapId, zoneId, "player") + -- Get player position — first try HBD's direct method (works when map is OPEN). -- If that returns nil (map closed), fall back to C_Map.GetPlayerMapPosition + -- HBD:GetWorldCoordinatesFromZone which works regardless of map open/closed state. - local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition() + -- Also skip HBD directly on Sunstrider since it returns wrong coords. + if useSunstriderFix and calibratedGroup then + -- already resolved via calibrated transform layer + elseif not playerX or not playerY or not playerInstance then + playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition() + end if not playerX or not playerY or not playerInstance then -- Fallback: get map-relative position then convert to world coords via HBD. - -- Use the player's current uiMapId as the map basis for the conversion. -- IMPORTANT: never use 946/947 (world/cosmic maps) — they have no world coord data. -- If GetCurrentUiMapId returns a world map, fall back to ZoneDB from the actual zone. - local pUiMapId = QuestiePlayer:GetCurrentUiMapId() if not pUiMapId or pUiMapId == 946 or pUiMapId == 947 or pUiMapId == 0 then - local zoneId = QuestiePlayer:GetCurrentZoneId() or select(7, GetInstanceInfo()) + zoneId = QuestiePlayer:GetCurrentZoneId() or select(7, GetInstanceInfo()) if debugArrow then print(string.format("UpdateNearestTargets: pUiMapId=%s (invalid), looking up via zoneId=%s", tostring(pUiMapId), tostring(zoneId))) end @@ -674,7 +844,7 @@ sortedTargets = {} end -- Additional safeguard: if pUiMapId is still a world/cosmic map, force lookup from zone if not pUiMapId or pUiMapId == 946 or pUiMapId == 947 or pUiMapId == 0 then - local zoneId = QuestiePlayer:GetCurrentZoneId() + zoneId = QuestiePlayer:GetCurrentZoneId() if zoneId and zoneId ~= 0 then pUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId) if debugArrow then @@ -684,34 +854,47 @@ sortedTargets = {} end pUiMapId = pUiMapId or 0 + -- On Sunstrider Isle (zoneId 3430), C_Map.GetPlayerMapPosition returns Sunstrider + -- map-space coords. We must look up with the actual Sunstrider uiMapId (1241) and + -- then convert through Eversong's 1941 bounds to get correct world coords. + local lookupUiMapId = pUiMapId + if zoneId == 3430 then + lookupUiMapId = 1241 -- always use Sunstrider's real uiMapId for C_Map + end if debugArrow then - print(string.format("UpdateNearestTargets: trying C_Map with pUiMapId=%s", tostring(pUiMapId))) + print(string.format("UpdateNearestTargets: trying C_Map with lookupUiMapId=%s zoneId=%s", tostring(lookupUiMapId), tostring(zoneId))) end - local mapX, mapY = C_Map.GetPlayerMapPosition(pUiMapId, "player") + -- On Sunstrider, raw GetPlayerMapPosition(1941, "player") returns 0/0 when the map is + -- closed because the client map context is still the ghost/current map. Use the helper + -- above to obtain current-zone coords, then convert them through Eversong's 1941 bounds. + local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow) if debugArrow then - print(string.format("UpdateNearestTargets: C_Map.GetPlayerMapPosition(%s,'player') -> mapX=%.4f mapY=%.4f", tostring(pUiMapId), mapX or -1, mapY or -1)) + print(string.format("UpdateNearestTargets: _GetSunstriderPlayerMapPosition() -> mapX=%.4f mapY=%.4f", mapX or -1, mapY or -1)) end if mapX and mapY and mapX > 0 and mapY > 0 then - playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, pUiMapId) + if calibratedGroup then + playerX, playerY, playerInstance = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(mapX, mapY, lookupUiMapId, zoneId) + else + local worldUiMapId = 1941 -- always use Eversong bounds for world coord conversion + playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, worldUiMapId) + end + if debugArrow then + print(string.format("UpdateNearestTargets: HBD via lookupUiMapId=%s mapX=%.4f mapY=%.4f -> worldX=%.4f worldY=%.4f", + tostring(lookupUiMapId), mapX, mapY, playerX or 0, playerY or 0)) + end playerInstance = playerInstance or 0 - if debugArrow then - print(string.format("UpdateNearestTargets: HBD fallback via mapId=%d mapX=%.4f mapY=%.4f -> worldX=%.4f worldY=%.4f", - pUiMapId, mapX, mapY, playerX or 0, playerY or 0)) - end - else - if debugArrow then - print(string.format("UpdateNearestTargets: C_Map.GetPlayerMapPosition returned invalid coords (%.4f, %.4f), mapId=%s", mapX or 0, mapY or 0, tostring(pUiMapId))) - end end end - - if not playerX or not playerY or not playerInstance then - if debugArrow then - print("UpdateNearestTargets: player position unavailable, returning early") + if debugArrow then + print(string.format("UpdateNearestTargets: HBD.GetPlayerWorldPosition() = x=%.4f y=%.4f inst=%s", playerX or 0, playerY or 0, tostring(playerInstance))) + end + if not playerX or not playerY or not playerInstance then + if debugArrow then + print("UpdateNearestTargets: player position unavailable, returning early") + end + return end - return - end playerInstance = playerInstance or 0 @@ -740,6 +923,7 @@ sortedTargets = {} -- Publish context for hoisted helper functions (avoids closure allocation every call) _arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance + _arrow_playerCalibratedX, _arrow_playerCalibratedY, _arrow_playerCalibratedGroup = playerX, playerY, calibratedGroup _arrow_usingAutoLogic = usingAutoLogic _arrow_playerZoneId, _arrow_playerUiMapId = playerZoneId, playerUiMapId diff --git a/Modules/Map/QuestieMap.lua b/Modules/Map/QuestieMap.lua index 3056d54..b0fd888 100644 --- a/Modules/Map/QuestieMap.lua +++ b/Modules/Map/QuestieMap.lua @@ -57,6 +57,22 @@ local drawTimer local fadeLogicTimerShown local fadeLogicCoroutine +local function _ResolveMapUiMapId(uiMapId, x, y) + -- Ascension zone mapping: On Ascension, Sunstrider Isle (uiMapId 1241) shares + -- Eversong Woods' (1941) coordinate space. Zone 3430 (Eversong Woods) data + -- now correctly maps to uiMapId 1941 via GetUiMapIdByAreaId(3430)=1941. + -- These Eversong-map pins appear on the Sunstrider sub-map (1241) via + -- ZONE_REDIRECT visibility in HBD.lua. + -- + -- Redirect any remaining map-1241 pins to 1941. On Ascension, map 1241 + -- does not have its own independent coordinate space — it IS Eversong's + -- space. Pins destined for 1241 must use 1941's bounds for correct rendering. + if uiMapId == 1241 then + return 1941 + end + return uiMapId +end + local isDrawQueueDisabled = false --* TODO: How the frames are handled needs to be reworked, why are we getting them from _G @@ -445,7 +461,7 @@ function QuestieMap:DrawManualIcon(data, areaID, x, y, typ) data.Id = data.id - local uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(areaID), x, y) if (not uiMapId) then Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieMap:DrawManualIcon] No UiMapID for areaId:", areaID, tostring(data.Name)) return nil, nil @@ -521,7 +537,7 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag) return nil, nil end - local uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(areaID), x, y) if (not uiMapId) then local parentMapId local mapInfo = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(areaID) @@ -536,7 +552,7 @@ function QuestieMap:DrawWorldIcon(data, areaID, x, y, showFlag) return nil, nil else areaID = parentMapId - uiMapId = ZoneDB:GetUiMapIdByAreaId(areaID) + uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(areaID), x, y) end end @@ -687,14 +703,14 @@ function QuestieMap:FindClosestStarter() if dungeonLocation ~= nil then for _, value in ipairs(dungeonLocation) do if (value[1] and value[2]) then - local x, y, _ = HBD:GetWorldCoordinatesFromZone(value[1] / 100, value[2] / 100, ZoneDB:GetUiMapIdByAreaId(value[3])) + local x, y, _ = HBD:GetWorldCoordinatesFromZone(value[1] / 100, value[2] / 100, _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(value[3]), value[1], value[2])) if (x and y) then local distance = QuestieLib:Euclid(playerX or 0, playerY or 0, x, y); if (closestStarter[questId].distance > distance) then closestStarter[questId].distance = distance; closestStarter[questId].x = x; closestStarter[questId].y = y; - closestStarter[questId].zone = ZoneDB:GetUiMapIdByAreaId(Zone); + closestStarter[questId].zone = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(Zone), value[1], value[2]) closestStarter[questId].type = "GameObject - " .. obj.name; end end @@ -702,7 +718,7 @@ function QuestieMap:FindClosestStarter() end end else - local uiMapId = ZoneDB:GetUiMapIdByAreaId(Zone) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(Zone), coords[1], coords[2]) local x, y, _ = HBD:GetWorldCoordinatesFromZone(coords[1] / 100, coords[2] / 100, uiMapId) if (x and y) then local distance = QuestieLib:Euclid(playerX or 0, playerY or 0, x, y); @@ -732,7 +748,7 @@ function QuestieMap:FindClosestStarter() if dungeonLocation ~= nil then for _, value in ipairs(dungeonLocation) do if (value[1] and value[2]) then - local uiMapId = ZoneDB:GetUiMapIdByAreaId(value[3]) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(value[3]), value[1], value[2]) local x, y, _ = HBD:GetWorldCoordinatesFromZone(value[1] / 100, value[2] / 100, uiMapId) if (x and y) then local distance = QuestieLib:Euclid(playerX or 0, playerY or 0, x, y); @@ -740,7 +756,7 @@ function QuestieMap:FindClosestStarter() closestStarter[questId].distance = distance; closestStarter[questId].x = x; closestStarter[questId].y = y; - closestStarter[questId].zone = ZoneDB:GetUiMapIdByAreaId(Zone); + closestStarter[questId].zone = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(Zone), value[1], value[2]) closestStarter[questId].type = "NPC - " .. NPC.name; end end @@ -748,7 +764,7 @@ function QuestieMap:FindClosestStarter() end end elseif (coords[1] and coords[2]) then - local uiMapId = ZoneDB:GetUiMapIdByAreaId(Zone) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(Zone), coords[1], coords[2]) local x, y, _ = HBD:GetWorldCoordinatesFromZone(coords[1] / 100, coords[2] / 100, uiMapId) if (x and y) then local distance = QuestieLib:Euclid(playerX or 0, playerY or 0, x, y); @@ -756,7 +772,7 @@ function QuestieMap:FindClosestStarter() closestStarter[questId].distance = distance; closestStarter[questId].x = x; closestStarter[questId].y = y; - closestStarter[questId].zone = ZoneDB:GetUiMapIdByAreaId(Zone); + closestStarter[questId].zone = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(Zone), coords[1], coords[2]) closestStarter[questId].type = "NPC - " .. NPC.name; end end @@ -792,7 +808,7 @@ function QuestieMap:GetNearestSpawn(objective) for id, spawnData in pairs(objective.spawnList) do for zone, spawns in pairs(spawnData.Spawns) do for _, spawn in pairs(spawns) do - local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(zone), spawn[1], spawn[2]) local dX, dY, dInstance = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId) local dist = HBD:GetWorldDistance(dInstance, playerX, playerY, dX, dY) if dist then @@ -836,7 +852,7 @@ function QuestieMap:GetNearestQuestSpawn(quest) local bestSpawn, bestSpawnZone, bestSpawnType, bestSpawnName for zone, spawns in pairs(finisherSpawns) do for _, spawn in pairs(spawns) do - local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + local uiMapId = _ResolveMapUiMapId(ZoneDB:GetUiMapIdByAreaId(zone), spawn[1], spawn[2]) local dX, dY, dInstance = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId) local dist = HBD:GetWorldDistance(dInstance, playerX, playerY, dX, dY) if dist then diff --git a/Modules/QuestieCompat.lua b/Modules/QuestieCompat.lua index ddce3cd..7167802 100644 --- a/Modules/QuestieCompat.lua +++ b/Modules/QuestieCompat.lua @@ -113,6 +113,79 @@ end local errorMsg = "Questie tried to call a blizzard API function that does not exist..." +local sqrt = math.sqrt + +local CALIBRATED_MAP_GROUPS = { + sunstrider = { + -- Use the parent Eversong map as the primary lookup surface. On this realm, + -- explicit child-map (1241) position queries can still return parent/ghost-map + -- normalized coordinates (~0.60/0.44), which recreates the 433-yard/backwards-arrow bug. + primaryUiMapId = 1941, + uiMapIds = { + [1241] = true, + [946] = true, + [1941] = true, + }, + zoneIds = { + [3430] = true, + }, + worldUiMapId = 1941, + normalizedToPseudoWorldScale = 1353, + }, +} + +local function _GetCalibratedMapGroup(uiMapId, zoneId) + for _, group in pairs(CALIBRATED_MAP_GROUPS) do + if (uiMapId and group.uiMapIds and group.uiMapIds[uiMapId]) or (zoneId and group.zoneIds and group.zoneIds[zoneId]) then + return group + end + end +end + +function QuestieCompat.GetCalibratedMapGroup(uiMapId, zoneId) + return _GetCalibratedMapGroup(uiMapId, zoneId) +end + +function QuestieCompat.IsCalibratedMap(uiMapId, zoneId) + return _GetCalibratedMapGroup(uiMapId, zoneId) ~= nil +end + +function QuestieCompat.GetCalibratedWorldCoordinatesFromZone(x, y, uiMapId, zoneId) + local group = _GetCalibratedMapGroup(uiMapId, zoneId) + if group and x and y then + local scale = group.normalizedToPseudoWorldScale or 1000 + return x * scale, y * scale, 0, group + end + + if QuestieCompat.HBD and QuestieCompat.HBD.GetWorldCoordinatesFromZone and uiMapId then + local worldX, worldY, instanceId = QuestieCompat.HBD:GetWorldCoordinatesFromZone(x, y, uiMapId) + return worldX, worldY, instanceId, nil + end +end + +function QuestieCompat.GetCalibratedPlayerPosition(uiMapId, zoneId, unitToken) + local group = _GetCalibratedMapGroup(uiMapId, zoneId) + if group then + local mapPos = QuestieCompat.C_Map and QuestieCompat.C_Map.GetPlayerMapPosition and QuestieCompat.C_Map.GetPlayerMapPosition(group.primaryUiMapId, unitToken or "player") + if type(mapPos) == "table" and mapPos.x and mapPos.y and mapPos.x > 0 and mapPos.y > 0 then + local worldX, worldY = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(mapPos.x, mapPos.y, group.primaryUiMapId, zoneId) + return worldX, worldY, 0, mapPos.x, mapPos.y, group + end + end + + if QuestieCompat.HBD and QuestieCompat.HBD.GetPlayerWorldPosition then + local worldX, worldY, instanceId = QuestieCompat.HBD:GetPlayerWorldPosition() + return worldX, worldY, instanceId or 0, nil, nil, group + end +end + +function QuestieCompat.GetCalibratedDistanceScale(uiMapId, zoneId) + local group = _GetCalibratedMapGroup(uiMapId, zoneId) + if group and group.normalizedToPseudoWorldScale then + return group.normalizedToPseudoWorldScale / 100 + end +end + ------------------------------------------ -- Older client compatibility (pre 1.14.1) ------------------------------------------ @@ -477,9 +550,36 @@ end --- C_Map Shim QuestieCompat.C_Map = QuestieCompat.C_Map or {} -function QuestieCompat.C_Map.GetPlayerMapPosition(uiMapID) - local x, y = GetPlayerMapPosition("player") - return x, y +function QuestieCompat.C_Map.GetPlayerMapPosition(uiMapID, unitToken) + unitToken = unitToken or "player" + + if uiMapID and QuestieCompat.UiMapData and QuestieCompat.UiMapData[uiMapID] then + local originalMapAreaID = GetCurrentMapAreaID() + local originalDungeonLevel = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() or 0 + local mapID = QuestieCompat.UiMapData[uiMapID].mapID + local dungeonLevel = QuestieCompat.Round(math.mod(mapID, 1) * 10) + + SetMapByID(math.floor(mapID) - 1) + if dungeonLevel > 0 and SetDungeonMapLevel then + SetDungeonMapLevel(dungeonLevel) + end + + local x, y = GetPlayerMapPosition(unitToken) + + if originalMapAreaID and originalMapAreaID > 0 then + SetMapByID(originalMapAreaID - 1) + if originalDungeonLevel and originalDungeonLevel > 0 and SetDungeonMapLevel then + SetDungeonMapLevel(originalDungeonLevel) + end + else + SetMapToCurrentZone() + end + + return { uiMapID = uiMapID, x = x, y = y }, uiMapID + end + + local playerPos, resolvedUiMapID = QuestieCompat.GetPlayerMapPosition() + return playerPos, resolvedUiMapID end function QuestieCompat.C_Map.GetBestMapForUnit(unit) diff --git a/Modules/QuestieCoordinates.lua b/Modules/QuestieCoordinates.lua index 92d2aa6..6678e0c 100644 --- a/Modules/QuestieCoordinates.lua +++ b/Modules/QuestieCoordinates.lua @@ -111,6 +111,21 @@ end ---@return table<{x: number, y: number}>, number | nil function QuestieCoords.GetPlayerMapPosition() + -- If the world map is open, use the map currently being displayed instead of + -- GetBestMapForUnit("player"). On legacy clients our compat shim may call + -- SetMapToCurrentZone()/SetMapByID() while resolving the player's map, which + -- fights the open world map and causes the title/coordinate text to flicker. + if WorldMapFrame and WorldMapFrame:IsVisible() then + local currentMapId = WorldMapFrame:GetMapID() + if currentMapId and GetPlayerMapPosition then + local pos = GetPlayerMapPosition(currentMapId, "player") + if pos and pos.x and pos.y then + pos.uiMapID = currentMapId + return pos, currentMapId + end + end + end + local mapID = GetBestMapForUnit("player") if (not mapID) then return nil, nil @@ -142,7 +157,19 @@ function QuestieCoords:ResetMinimapText() end function QuestieCoords:ResetMapText() - GetMapTitleText():SetText(WORLD_MAP); + local mapTitleText = GetMapTitleText() + if not mapTitleText then return end + + local currentMapId = WorldMapFrame and WorldMapFrame.GetMapID and WorldMapFrame:GetMapID() + if currentMapId then + local info = C_Map.GetMapInfo(currentMapId) + if info and info.name then + mapTitleText:SetText(info.name) + return + end + end + + mapTitleText:SetText(WORLD_MAP); end function QuestieCoords:ResetMiniWorldMapText() diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index b927661..f40f40f 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -11,6 +11,8 @@ local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer") local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") ---@type l10n local l10n = QuestieLoader:ImportModule("l10n") +---@type ZoneDB +local ZoneDB = QuestieLoader:ImportModule("ZoneDB") local _Learner = QuestieLearner.private or {} QuestieLearner.private = _Learner @@ -75,7 +77,9 @@ local MOUSEOVER_LEARN_FLAGS = NPC_FLAG_QUESTGIVER local COORD_GRID = 2.0 -- Minimum match count (Confidence) for a learned pin to appear on the map. -local MIN_CONFIDENCE_PINS = 2 +-- Set to 1 so that even a single kill/mouseover confirms a spawn location on +-- Ascension, where NPC databases are incomplete and every data point matters. +local MIN_CONFIDENCE_PINS = 1 _Learner.pendingNpcs = {} _Learner.pendingQuests = {} @@ -91,8 +95,19 @@ QuestieLearner.data = nil ------------------------------------------------------------------------ local function GetZoneId() - local mapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player") - if mapId then return mapId end + -- On WotLK/Ascension, C_Map.GetBestMapForUnit returns a uiMapId (e.g. 1241 for Sunstrider). + -- Questie NPC spawns are keyed by areaId (e.g. 3430), so we must convert. + local uiMapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player") + if uiMapId then + -- Try ZoneDB reverse lookup first (covers Ascension overrides like 1241→3430) + if ZoneDB and ZoneDB.GetAreaIdByUiMapId then + local areaId = ZoneDB:GetAreaIdByUiMapId(uiMapId) + if areaId and areaId > 0 then + return areaId + end + end + return uiMapId -- fallback: no ZoneDB mapping available + end return select(8, GetInstanceInfo()) or 0 end @@ -170,7 +185,7 @@ local function EnsureLearnedData() learnQuests = true, learnItems = true, learnObjects = true, - minConfidencePins = 2, + minConfidencePins = 1, prioritizeMyData = true, staleThreshold = 90, -- days pruneVerified = false, -- protect verified data by default @@ -188,7 +203,7 @@ local function EnsureLearnedData() if s.learnQuests == nil then s.learnQuests = true end if s.learnItems == nil then s.learnItems = true end if s.learnObjects == nil then s.learnObjects = true end - if s.minConfidencePins == nil then s.minConfidencePins = 2 end + if s.minConfidencePins == nil then s.minConfidencePins = 1 end if s.prioritizeMyData == nil then s.prioritizeMyData = true end end return true @@ -812,10 +827,28 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv end) end - -- 3. Register with tooltip system immediately + -- 3. Register with tooltip system immediately. Preserve the objective icon so + -- nameplates can render the correct learned slay/loot/talk marker. local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") if QuestieTooltips and QuestieTooltips.RegisterObjectiveTooltip then - QuestieTooltips:RegisterObjectiveTooltip(questId, "m_" .. npcId, { Index = 0, Description = objText or "Learned Objective", Update = function() end }) + local objectiveIcon + local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") + local objectives = QuestLogCache and QuestLogCache.GetQuestObjectives and QuestLogCache.GetQuestObjectives(questId) + if objectives and objText then + for _, obj in next, objectives do + if obj.text and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then + objectiveIcon = obj.Icon + break + end + end + end + + QuestieTooltips:RegisterObjectiveTooltip(questId, "m_" .. npcId, { + Index = 0, + Description = objText or "Learned Objective", + Icon = objectiveIcon, + Update = function() end + }) end Questie:Debug(Questie.DEBUG_LEARNER, @@ -1004,6 +1037,68 @@ function QuestieLearner:InjectLearnedData() local learned = Questie.dbLearner.global local npcCount, questCount, itemCount, objectCount = 0, 0, 0, 0 + -- Migration: fix spawn zone keys that were stored as uiMapId instead of areaId. + -- Before the GetZoneId() fix, kills on maps like Sunstrider Isle (uiMapId 1241) + -- were stored under key 1241 instead of the correct areaId 3430. + -- Convert any uiMapId keys to areaId using ZoneDB. + local zonesFixed = 0 + for npcId, data in pairs(learned.npcs) do + if data[7] then + local zonesToMigrate = {} + for zoneKey, coords in pairs(data[7]) do + -- If zoneKey looks like a uiMapId (a map ID rather than an areaId), + -- ZoneDB:GetAreaIdByUiMapId will return the corresponding areaId. + -- If it returns nil, zoneKey is already an areaId — no migration needed. + -- Skip very common areaIds that happen to look like small numbers. + if ZoneDB and ZoneDB.GetAreaIdByUiMapId then + local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey) + if maybeAreaId and maybeAreaId ~= zoneKey then + zonesToMigrate[zoneKey] = maybeAreaId + end + end + end + for oldKey, newKey in pairs(zonesToMigrate) do + local coords = data[7][oldKey] + if coords then + data[7][newKey] = data[7][newKey] or {} + for _, coord in ipairs(coords) do + InsertIfNewBucket(data[7][newKey], coord[1], coord[2]) + end + data[7][oldKey] = nil + zonesFixed = zonesFixed + 1 + end + end + end + end + -- Same migration for object spawn data (field 4) + for objId, data in pairs(learned.objects) do + if data[4] then + local zonesToMigrate = {} + for zoneKey, coords in pairs(data[4]) do + if ZoneDB and ZoneDB.GetAreaIdByUiMapId then + local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey) + if maybeAreaId and maybeAreaId ~= zoneKey then + zonesToMigrate[zoneKey] = maybeAreaId + end + end + end + for oldKey, newKey in pairs(zonesToMigrate) do + local coords = data[4][oldKey] + if coords then + data[4][newKey] = data[4][newKey] or {} + for _, coord in ipairs(coords) do + InsertIfNewBucket(data[4][newKey], coord[1], coord[2]) + end + data[4][oldKey] = nil + zonesFixed = zonesFixed + 1 + end + end + end + end + if zonesFixed > 0 then + Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Migrated", zonesFixed, "spawn zone keys from uiMapId to areaId") + end + -- 1. NPCs local npcIdsToFix = {} for npcId, data in pairs(learned.npcs) do @@ -1356,7 +1451,11 @@ function QuestieLearner:OnMouseoverUnit() _Learner.guidNpcCache = _Learner.guidNpcCache or {} _Learner.guidNpcCache[guid] = { npcId = npcId, name = name, ts = time() } - self:LearnNPC(npcId, name, level, subName, npcFlags, factionString) + -- Pass areaId as spawnZoneId so LearnNPC stores spawn data under the + -- correct areaId (3430 for Sunstrider/Eversong) rather than falling back + -- to GetZoneId() which may return the same value but via a different path. + -- GetPlayerCoords() fallback in LearnNPC will provide the coordinates. + self:LearnNPC(npcId, name, level, subName, npcFlags, factionString, nil, nil, areaId) end function QuestieLearner:OnTargetChanged() @@ -1413,7 +1512,7 @@ function QuestieLearner:OnQuestDetail() elseif unitType == "Creature" or unitType == "Vehicle" then self:LearnQuestGiver(questId, entityId, 1, true) local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 2 - self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil) + self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, zoneId) end end end @@ -1423,6 +1522,9 @@ function QuestieLearner:OnQuestComplete() local questId = GetQuestID and GetQuestID() if not questId or questId <= 0 then return end + -- Get current zone for quest giver spawn data + local zoneId = GetZoneId() + -- Capture completion/finish text local data = {} if GetRewardText then @@ -1442,7 +1544,7 @@ function QuestieLearner:OnQuestComplete() elseif unitType == "Creature" or unitType == "Vehicle" then self:LearnQuestGiver(questId, entityId, 1, false) local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 2 - self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil) + self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, zoneId) end end end @@ -1617,7 +1719,7 @@ function QuestieLearner:OnQuestAccepted(firstArg, secondArg) elseif giverEntity.unitType == "Creature" or giverEntity.unitType == "Vehicle" then self:LearnQuestGiver(questId, giverEntity.id, 1, true) local npcFlags = (npcGuid and UnitNPCFlags and UnitNPCFlags("npc")) or 1 - self:LearnNPC(giverEntity.id, giverEntity.name, nil, nil, npcFlags, nil) + self:LearnNPC(giverEntity.id, giverEntity.name, nil, nil, npcFlags, nil, nil, nil, GetZoneId()) end end end @@ -1642,7 +1744,7 @@ function QuestieLearner:OnQuestTurnedIn(questId, xpReward, moneyReward) elseif unitType == "Creature" or unitType == "Vehicle" then self:LearnQuestGiver(questId, entityId, 1, false) local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 2 - self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil) + self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, GetZoneId()) end end end @@ -1706,7 +1808,7 @@ function QuestieLearner:OnGossipShow() self:LearnObject(id, name) elseif unitType == "Creature" or unitType == "Vehicle" then local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 1 - self:LearnNPC(id, name, nil, nil, npcFlags, nil) + self:LearnNPC(id, name, nil, nil, npcFlags, nil, nil, nil, GetZoneId()) end end diff --git a/Tests/QuestieLearner_spec.lua b/Tests/QuestieLearner_spec.lua new file mode 100644 index 0000000..601fb40 --- /dev/null +++ b/Tests/QuestieLearner_spec.lua @@ -0,0 +1,339 @@ +-- Tests/QuestieLearner_spec.lua +require("Tests/wow_api_mock") + +describe("QuestieLearner", function() + local QuestieLearner + + setup(function() + -- Mocking QuestieLoader module resolution for tests + _G.QuestieLoader.ImportModule = function(_, name) + if name == "QuestieDB" then return _G.QuestieDB end + if name == "QuestieQuest" then return _G.QuestieQuest end + if name == "QuestiePlayer" then return _G.QuestiePlayer end + if name == "QuestLogCache" then return _G.QuestLogCache end + if name == "QuestieCompat" then return _G.QuestieCompat end + if name == "ZoneDB" then return _G.ZoneDB end + if name == "l10n" then return _G.l10n end + if name == "QuestieTooltips" then return _G.QuestieTooltips end + if name == "QuestieLib" then return {} end + return {} + end + + _G.QuestieLoader.CreateModule = function(_, name) + _G[name] = {} + return _G[name] + end + + -- Reset mock state + _G._lastRegisteredTooltip = nil + _G._mock_uiMapId = nil + _G._mock_questObjectives = nil + _G._mock_npcFlags = 2 + + -- Reset QuestLogCache.GetQuest to default + _G.QuestLogCache.GetQuest = function(_, id) return nil end + + -- Load the module + package.loaded["Modules/QuestieLearner"] = nil + QuestieLearner = require("Modules/QuestieLearner") + + -- Initialize sets up dbLearner.global.settings, InjectsLearnedData, etc. + QuestieLearner:Initialize() + end) + + --========================================================================== + -- Existing test: coordinate scaling in OnCombatLogEvent + --========================================================================== + it("should scale coordinates by 100 in OnCombatLogEvent", function() + local unitGUID = "Creature-0-1234-567-89-21878-0000000000" + local unitName = "Felboar" + + _G.QuestieCompat.GetCurrentPlayerPosition = function() + return 946, 0.35, 0.45 + end + + QuestieLearner:OnCombatLogEvent(GetTime(), "UNIT_DIED", nil, nil, nil, unitGUID, unitName, nil, nil, nil) + + local cached = QuestieLearner.private.recentKills[unitGUID] + assert.is_not_nil(cached) + assert.are.equal(35.0, cached.x) + assert.are.equal(45.0, cached.y) + end) + + --========================================================================== + -- Existing test: spell cast learning + --========================================================================== + it("should learn spell casts into dbLearner quest objectives when they match a quest objective", function() + local questId = 12345 + local spellId = 29228 + local targetGUID = "Creature-0-1234-567-89-21878-0000000000" + + Questie.dbLearner.global.quests = {} + + _G.QuestieCompat.GetQuestLogTitle = function(index) + return "Test Quest", 1, nil, false, nil, nil, nil, questId + end + + _G.QuestLogCache.GetQuest = function(id) + return { + objectives = { + { type = "spell", text = "Flame Shock the enemy" } + } + } + end + + QuestieLearner:LearnSpellCast(spellId, "Flame Shock", targetGUID, "Enemy NPC") + + assert.is_not_nil(Questie.dbLearner.global.quests[questId]) + assert.is_not_nil(Questie.dbLearner.global.quests[questId][10]) + assert.is_not_nil(Questie.dbLearner.global.quests[questId][10][1]) + assert.are.same({ 21878, "Flame Shock" }, Questie.dbLearner.global.quests[questId][10][1][1]) + end) + + --========================================================================== + -- InjectLearnedData zone key migration (uiMapId → areaId) + --========================================================================== + describe("InjectLearnedData zone migration", function() + it("should migrate NPC spawn zone keys from uiMapId to areaId", function() + Questie.dbLearner.global.npcs = { + [21878] = { + [1] = "Felboar", + [7] = { + [1241] = { -- uiMapId (Sunstrider) → should migrate to 3430 + { 35.0, 45.0 }, + { 40.0, 50.0 }, + }, + }, + }, + } + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.objects = {} + Questie.dbLearner.global.items = {} + + -- Reload the data by calling InjectLearnedData (already called in Initialize, but re-call with updated data) + QuestieLearner:InjectLearnedData() + + local npcs = Questie.dbLearner.global.npcs + assert.is_nil(npcs[21878][7][1241], + "Old uiMapId key 1241 should be removed after migration") + assert.is_not_nil(npcs[21878][7][3430], + "New areaId key 3430 should exist after migration") + assert.are.equal(2, #npcs[21878][7][3430], + "Should have 2 coords entries in migrated zone") + assert.are.same({ 35.0, 45.0 }, npcs[21878][7][3430][1]) + assert.are.same({ 40.0, 50.0 }, npcs[21878][7][3430][2]) + end) + + it("should merge migrated spawn data with existing areaId data", function() + Questie.dbLearner.global.npcs = { + [21878] = { + [1] = "Felboar", + [7] = { + [1241] = { -- uiMapId → merge into 3430 + { 35.0, 45.0 }, + }, + [3430] = { -- Already correct areaId + { 50.0, 55.0 }, + }, + }, + }, + } + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.objects = {} + Questie.dbLearner.global.items = {} + + QuestieLearner:InjectLearnedData() + + local npcs = Questie.dbLearner.global.npcs + assert.is_nil(npcs[21878][7][1241], "Old uiMapId key should be removed") + assert.are.equal(2, #npcs[21878][7][3430], + "Merged: 1 existing + 1 migrated = 2 entries") + end) + + it("should migrate object spawn zone keys from uiMapId to areaId", function() + Questie.dbLearner.global.npcs = {} + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.items = {} + Questie.dbLearner.global.objects = { + [181345] = { + [1] = "Mysterious Pedestal", + [4] = { + [1241] = { -- uiMapId → migrate to 3430 + { 30.0, 40.0 }, + }, + }, + }, + } + + QuestieLearner:InjectLearnedData() + + local objects = Questie.dbLearner.global.objects + assert.is_nil(objects[181345][4][1241], + "Old uiMapId key should be removed from objects") + assert.is_not_nil(objects[181345][4][3430], + "Migrated areaId key should exist") + assert.are.same({ 30.0, 40.0 }, objects[181345][4][3430][1]) + end) + + it("should leave non-mappable zone keys unchanged", function() + Questie.dbLearner.global.npcs = { + [21878] = { + [1] = "Felboar", + [7] = { + [3430] = { -- Already correct areaId — stay + { 35.0, 45.0 }, + }, + [530] = { -- No ZoneDB mapping — stay + { 60.0, 30.0 }, + }, + }, + }, + } + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.objects = {} + Questie.dbLearner.global.items = {} + + QuestieLearner:InjectLearnedData() + + local npcs = Questie.dbLearner.global.npcs + assert.is_not_nil(npcs[21878][7][3430], "3430 areaId should remain") + assert.is_not_nil(npcs[21878][7][530], "530 (no mapping) should remain") + assert.are.equal(1, #npcs[21878][7][3430], "3430 should still have 1 entry") + assert.are.equal(1, #npcs[21878][7][530], "530 should still have 1 entry") + end) + end) + + --========================================================================== + -- LearnQuestObjectiveNPC icon preservation + --========================================================================== + describe("LearnQuestObjectiveNPC icon preservation", function() + it("should pass the objective icon from QuestLogCache to RegisterObjectiveTooltip", function() + local questId = 1001 + local npcId = 21878 + local objText = "Slay 10 felboars" + local objectiveIndex = 1 + + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.npcs = {} + + -- Mock QuestLogCache.GetQuestObjectives returns objectives with Icon 136006 + _G._mock_questObjectives = { + { text = "Slay 10 felboars", Icon = 136006, objectiveType = "slay" }, + } + + QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex) + + -- Verify RegisterObjectiveTooltip was called with the icon + assert.is_not_nil(_G._lastRegisteredTooltip, + "RegisterObjectiveTooltip should have been called") + assert.are.equal(questId, _G._lastRegisteredTooltip.questId) + assert.are.equal("m_" .. npcId, _G._lastRegisteredTooltip.identifier) + assert.are.equal(objText, _G._lastRegisteredTooltip.data.Description) + assert.are.equal(136006, _G._lastRegisteredTooltip.data.Icon, + "Icon from QuestLogCache should be passed to tooltip registration") + end) + + it("should work when QuestLogCache has no matching objective text", function() + local questId = 1002 + local npcId = 21879 + local objText = "Gather 5 moonflowers" + local objectiveIndex = 1 + + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.npcs = {} + + -- Objectives don't include "moonflowers" + _G._mock_questObjectives = { + { text = "Slay 10 felboars", Icon = 136006 }, + { text = "Collect 5 herbs", Icon = 134217 }, + } + + QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex) + + -- Should still register, but with nil Icon (graceful fallback) + assert.is_not_nil(_G._lastRegisteredTooltip, + "RegisterObjectiveTooltip should still be called without matching icon") + assert.is_nil(_G._lastRegisteredTooltip.data.Icon, + "Icon should be nil when no matching objective text is found") + end) + + it("should work when QuestLogCache.GetQuestObjectives is nil", function() + local questId = 1003 + local npcId = 21880 + local objText = "Kill 3 worgs" + local objectiveIndex = 1 + + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.npcs = {} + + -- nil/empty so no icon lookup happens + _G._mock_questObjectives = nil + + QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiveIndex) + + assert.is_not_nil(_G._lastRegisteredTooltip, + "RegisterObjectiveTooltip should be called even with nil objectives") + assert.is_nil(_G._lastRegisteredTooltip.data.Icon, + "Icon should be nil when GetQuestObjectives returns nil") + end) + end) + + --========================================================================== + -- MIN_CONFIDENCE_PINS = 1 + --========================================================================== + describe("Settings defaults", function() + it("should have minConfidencePins default to 1 on fresh data", function() + -- After Initialize, settings should be set with minConfidencePins = 1 + local settings = QuestieLearner:GetSettings() + assert.are.equal(1, settings.minConfidencePins, + "minConfidencePins should be 1 for Ascension play") + end) + + it("should backfill legacy SavedVariables with minConfidencePins = 1", function() + -- Simulate legacy data without minConfidencePins + if Questie.dbLearner.global.settings then + Questie.dbLearner.global.settings.minConfidencePins = nil + end + + -- Re-initialize to trigger backfill + QuestieLearner:Initialize() + local settings = QuestieLearner:GetSettings() + + assert.are.equal(1, settings.minConfidencePins, + "Legacy settings should be backfilled with minConfidencePins = 1") + end) + end) + + --========================================================================== + -- NPC learning API + --========================================================================== + describe("LearnNPC spawn zone tracking", function() + it("should store NPC spawn zoneId correctly", function() + local npcId = 21878 + local spawnZoneId = 3430 -- Eversong Woods + + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.npcs = {} + + -- Mock player position + _G.QuestieCompat.GetCurrentPlayerPosition = function() + return 3430, 0.35, 0.45 + end + + -- Mock coords + _G.UnitPosition = function(unit) return -8940, -137, 82 end + + QuestieLearner:LearnNPC(npcId, "Felboar", 60, nil, 2, nil, nil, nil, spawnZoneId) + + assert.is_not_nil(Questie.dbLearner.global.npcs[npcId], + "NPC should be learned") + assert.are.equal("Felboar", Questie.dbLearner.global.npcs[npcId][1], + "NPC name should be stored") + -- NPC spawn coords should be stored under the areaId + if Questie.dbLearner.global.npcs[npcId][7] then + assert.is_not_nil(Questie.dbLearner.global.npcs[npcId][7][spawnZoneId], + "Spawn coords should be stored under zoneId " .. spawnZoneId) + end + end) + end) +end) diff --git a/Tests/wow_api_mock.lua b/Tests/wow_api_mock.lua new file mode 100644 index 0000000..ea45c2f --- /dev/null +++ b/Tests/wow_api_mock.lua @@ -0,0 +1,161 @@ +-- Tests/wow_api_mock.lua +-- Minimal mock of World of Warcraft API for Busted unit tests + +_G = _G or {} + +-- Mock Globals +_G.Questie = { + DEBUG_LEARNER = "LEARNER", + DEBUG_DEVELOP = "DEVELOP", + db = { + global = { + learnedData = { + npcs = {}, + quests = {}, + items = {}, + objects = {}, + settings = { + learnQuests = true, + learnNPCs = true, + learnItems = true, + learnObjects = true, + } + } + }, + profile = { + learnedData = { + settings = { + learnQuests = true, + learnNPCs = true, + } + } + } + }, + dbLearner = { + global = { + npcs = {}, + quests = {}, + items = {}, + objects = {}, + } + }, + Debug = function(self, level, ...) + -- print("[" .. tostring(level) .. "]", ...) + end, + Print = function(self, ...) + -- print(...) + end, + Error = function(self, ...) + -- print("[ERROR]", ...) + end +} + +_G.QuestieLoader = { + ImportModule = function(self, name) + if name == "QuestieDB" then return _G.QuestieDB end + if name == "QuestieQuest" then return _G.QuestieQuest end + if name == "QuestiePlayer" then return _G.QuestiePlayer end + if name == "QuestLogCache" then return _G.QuestLogCache end + if name == "QuestieLib" then return {} end + if name == "QuestieCompat" then return _G.QuestieCompat end + if name == "ZoneDB" then return _G.ZoneDB end + if name == "l10n" then return _G.l10n end + if name == "QuestieTooltips" then return _G.QuestieTooltips end + return {} + end, + CreateModule = function(self, name) + _G[name] = {} + return _G[name] + end +} + +_G.QuestieDB = { + npcDataOverrides = {}, + objectDataOverrides = {}, + questDataOverrides = {}, + itemDataOverrides = {}, + QueryNPCSingle = function() return nil end, + GetQuest = function() return nil end, +} + +_G.QuestieQuest = {} + +_G.QuestieCompat = { + GetCurrentPlayerPosition = function() return 1, 0.5, 0.5 end, + GetQuestLogTitle = function(index) + return "Test Quest", 1, nil, false, nil, nil, nil, 123 + end, + C_Timer = { + After = function(delay, fn) fn() end, + NewTicker = function(delay, fn) return { Cancel = function() end } end, + }, +} + +_G.QuestiePlayer = { + GetPlayerLevel = function() return 70 end, +} + +_G.QuestLogCache = { + GetQuestID = function() return 123 end, + GetQuest = function(_, questId) + return nil + end, + GetQuestObjectives = function(self, questId) + return _G._mock_questObjectives or { + { text = "Slay 10 felboars", Icon = 136006, objectiveType = "slay" }, + { text = "Collect 5 herbs", Icon = 134217, objectiveType = "loot" }, + } + end, +} + +_G.ZoneDB = { + GetAreaIdByUiMapId = function(self, uiMapId) + -- Sunstrider Isle (1241) → Eversong Woods (3430) + if uiMapId == 1241 then return 3430 end + return nil + end, +} + +_G.l10n = { + GetAreaId = function(self) + return 3430 + end, +} + +_G.QuestieTooltips = { + RegisterObjectiveTooltip = function(self, questId, identifier, data) + _G._lastRegisteredTooltip = { questId = questId, identifier = identifier, data = data } + end, +} + +_G.C_Map = { + GetBestMapForUnit = function(unit) + return _G._mock_uiMapId or 530 + end, + GetPlayerMapPosition = function(uiMapId, unit) + return 0.5, 0.5 + end, +} + +-- string.trim is a WoW API extension +_G.string.trim = function(s) + if s then return s:match("^%s*(.-)%s*$") or "" end + return "" +end + +_G.GetNumQuestLogEntries = function() return 1 end + +_G.GetTime = function() return os.time() end +_G.time = os.time +_G.floor = math.floor +_G.UnitName = function(unit) return "TestUnit" end +_G.UnitLevel = function(unit) return 70 end +_G.UnitGUID = function(unit) return "Creature-0-1234-567-89-1000-0000000000" end +_G.GetRealZoneText = function() return "Shadowmoon Valley" end +_G.GetInstanceInfo = function() return "Shadowmoon Valley", nil, nil, nil, nil, nil, nil, 530 end +_G.CreateFrame = function() return { RegisterEvent = function() end, SetScript = function() end } end +_G.GetPlayerMapPosition = function(unit) + return 0.5, 0.5 +end + +return _G diff --git a/docs/changelog.html b/docs/changelog.html index 17f23b3..31f9d3e 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -169,32 +169,53 @@

Questie-X Documentation

Complete history of changes, fixes, and additions.

- Version: v1.6.1 + Version: v1.6.2 + Unreleased ← Back to Documentation
-

[Unreleased] — Sunstrider Isle Arrow, Tooltip Guard, QuestData String Safety

+

[Unreleased] — Sunstrider Isle Arrow Distance, Map Pins, Tooltip Schema Fixes, QuestData String Safety

    -
  • [Fix — MapIconTooltip _GetLevelString Guard] Resolved attempt to concatenate local 'minLevel' (a nil value) crash in MapIconTooltip.lua:494 (_GetLevelString function). The creature name "Uneasy Citizen" existed in creatureLevels as an empty table {} rather than the expected [1]=minLevel, [2]=maxLevel, [3]=rank tuple. Added an early-return guard at the top of _GetLevelString: if creatureLevels[name] is falsy, return the name unmodified. -
  • -
  • [Fix — Tooltip NPC/Object Type Guard] Resolved a crash in QuestieTooltips when hovering over NPC or object tooltip keys (m_<id>, o_<id>) where learnedNpc[10] or learnedObj[10] was unexpectedly a string instead of a table. +
  • [Fix — MapIconTooltip _GetLevelString Guard] Resolved attempt to concatenate local 'minLevel' (a nil value) crash in MapIconTooltip.lua:494 (_GetLevelString function). The creature name "Uneasy Citizen" existed in creatureLevels as an empty table {} rather than the expected [1]=minLevel, [2]=maxLevel, [3]=rank tuple. Added an early-return guard at the top of _GetLevelString: if creatureLevels[name] is falsy or not a table with a numeric level at index [1], return the name unmodified.
  • +
  • [Fix — Tooltip Learned Data Schema Mismatch] Reworked learned NPC/object tooltip registration in Modules/Tooltips/Tooltip.lua to match the actual QuestieLearner storage format.
      -
    • Root Cause: InsertMissingQuestIds in the WotLKDB corrections files writes directly to QuestieDB.questData[questId] but questData is stored as a loadable Lua string on Ascension. When code later tried to index into that string as a table, it threw attempt to index field 'questData' (a string value).
    • -
    • Fix: Added if type(objList) ~= "table" then break end guard in both m_/NPC and o_/object iteration paths in Tooltip.lua before iterating learnedNpc[10] / learnedObj[10].
    • +
    • Player-Facing Symptom: Fixed the Stormwind mouseover crash reported on Bronzebeard while hovering city guards and other learned tooltip targets: Questie-X\\Modules\\Tooltips\\Tooltip.lua:240: attempt to index local 'objList' (a number value).
    • +
    • Root Cause: QuestieLearner:_AddToArray stores flat arrays of quest IDs (learnedNpc[10] = { questId1, questId2, ... }, learnedObj[2] = { questId1, questId2, ... }), but the pushed tooltip code still iterated them as { questId -> objList } maps. That caused crashes like attempt to index local 'objList' (a number value) when a quest ID number was treated like an objective-text array.
    • +
    • Fix: Replaced the legacy for questId, objList in next, ... / objList[oIndex] traversal with schema-correct lookup: iterate learned quest IDs via ipairs, fetch QuestieLearner.data.quests[questId], then walk qData[10] objective slots and entries (objEntry[2]) to reconstruct tooltip text safely.
    • +
    • Scope: Applied to both learned NPC tooltips (m_<id>) and learned object tooltips (o_<id>). Object lookup now reads quest IDs from learnedObj[2] (quest starts) instead of the old learnedObj[10] path.
  • [Fix — InsertMissingQuestIds String Guard] Added if type(QuestieDB.questData) ~= "table" then return end guard at the start of InsertMissingQuestIds() in both tbcQuestFixes.lua and wotlkQuestFixes.lua. Prevents the function from writing to questData while it is still an uncompiled string during early loader initialization.
  • -
  • [Fix — Sunstrider Isle Arrow / Zone Override] Resolved the quest arrow not appearing on Sunstrider Isle (Ascension's starting zone) when the world map is closed. +
  • [Fix — Sunstrider Isle Arrow Distance (Map Closed)] Resolved arrow distance showing ~1118 yards instead of ~37 yards on Sunstrider Isle when the world map is NOT open. Map open and zoomed out showed correct distance.
      -
    • Root Cause: C_Map.GetBestMapForUnit("player") returns 946 (ghost/loading map uiMapId) instead of 1241 (Sunstrider Isle's real uiMapId) when the world map is closed. ZoneDB:GetAreaIdByUiMapId(946) had no override, causing GetCurrentZoneId() to return 946 instead of 3430 (Sunstrider Isle's areaId). This broke target zone filtering in _CollectObjective and caused HBD:GetWorldCoordinatesFromZone to return 0,0 (no world coord data for map 946).
    • -
    • Fix — zoneDB.lua: Added [946] = 3430 and [1241] = 3430 to UiMapIdOverrides so GetAreaIdByUiMapId always resolves to the real Sunstrider Isle areaId regardless of which ghost or real uiMapId the game returns.
    • -
    • Fix — QuestieArrow.lua: Updated UpdateNearestTargets fallback chain to use QuestiePlayer:GetCurrentUiMapId() for player position. When that returns an invalid/ghost map (946/947/0), it falls back to a ZoneDB lookup via the actual zoneId. Ensures the arrow gets real world coordinates via C_Map.GetPlayerMapPosition + HBD:GetWorldCoordinatesFromZone regardless of map open/closed state.
    • -
    • Debug Output: Added per-frame debug output (respecting debugArrow profile setting) showing frameShown, target.title, player coordinates, and uiMapId values.
    • +
    • Root Cause (Player Position): HBD:GetPlayerWorldPosition() returns a non-nil value on Sunstrider Isle, but those coords are Eastern Kingdoms world position (wrong), not Sunstrider's actual position. Because a non-nil value is returned, the fallback chain never fires. The arrow then calculates distance using wrong player coords vs correct target coords, giving a wildly incorrect distance.
    • +
    • Root Cause (Ghost Map Player Position): C_Map.GetPlayerMapPosition(946, "player") where 946 is Ascension's ghost/cosmic map for Sunstrider Isle returns (0, 0) because the ghost map has no valid coordinate data. The correct map for player position on Sunstrider is 1941 (Eversong Woods parent), which shares the same world coordinate space and returns valid zone coords.
    • +
    • Root Cause (OnUpdate same-map check): OnUpdate was using _ResolveArrowUiMapId(_arrow_playerUiMapId) (which resolves 1241→1941) for the player-side uiMapId, making it equal to target.uiMapId (also 1941). This caused the same-map branch to fire, which then called C_Map.GetPlayerMapPosition(_arrow_playerUiMapId) with 946, getting (0, 0) and computing wrong distance.
    • +
    • Fix — QuestieArrow.lua UpdateNearestTargets: Added Sunstrider detection (zoneId == 3430) before calling HBD:GetPlayerWorldPosition() to force the C_Map fallback path. The fallback now calls C_Map.GetPlayerMapPosition(1241, "player") to get Sunstrider map-space coords, then converts through HBD:GetWorldCoordinatesFromZone(..., 1941) using Eversong Woods bounds — which share Sunstrider's world coordinate space.
    • +
    • Fix — QuestieArrow.lua OnUpdate same-map branch: Changed C_Map.GetPlayerMapPosition(1941, "player") for player zone coords (not _arrow_playerUiMapId which is 946). Normalizes both playerUiMapId and targetUiMapId through _ResolveArrowUiMapId() before same-map comparison. Uses zoneScale = 13.53 yards/zone-unit for distance. Declares worldPlayerX/Y before the if-else to prevent nil in debug prints.
    • +
    • Debug Output: Added debug prints showing UnitPosition vs HBD player coords comparison, raw vs resolved uiMapIds, branch selection, and per-frame distance calculation inputs.
  • +
  • [Fix — Sunstrider Isle Map Pins Not Appearing] Resolved quest objective icons (map pins) not appearing on the world map when zoomed into Sunstrider Isle (uiMapId 1241), even though they appeared correctly when zoomed out to Eversong Woods (uiMapId 1941). +
      +
    • Root Cause: HBDPins:HandlePin (HereBeDragons-Pins-2.0:424) has an early-return guard: if not HBD.mapData[uiMapID] then return end. When the player zooms into Sunstrider Isle, uiMapID is 1241, but HBD.mapData[1241] is nil — no mapData entry existed for Sunstrider's custom child map. The icon was silently dropped before any coordinate conversion occurred.
    • +
    • Fix — Compat/HBD.lua: Added mapData[1241] = mapData[1941] alias and mapData[946] = mapData[1941] alias. Sunstrider Isle (1241) and its ghost map (946) share Eversong Woods' (1941) world coordinate space for these conversions.
    • +
    • Fix — HBD fallback loading: Added a lazy fallback to the real HBD library's mapData for maps not present in Questie's compat table, so custom/private-server maps can still resolve world and zone coordinates when QuestieCompat lacks a local entry.
    • +
    • Fix — Modules/Map/QuestieMap.lua: Added _ResolveMapUiMapId() helper (1241→1941) applied across FadeLogic, FindClosestStarter, GetNearestSpawn, and GetNearestQuestSpawn. DrawWorldIcon and DrawManualIcon now also store and render Sunstrider map icons against the resolved parent map coordinate space.
    • +
    +
  • +
  • [Fix — Northshire Valley UiMapData Registration] Added explicit QuestieCompat.UiMapData[1238] for Northshire Valley so custom/private-server zone lookups have concrete geometry for the child map instead of relying on incomplete parent fallbacks.
  • +
  • [Fix — QuestieLearner Icon Preservation] Preserved the learned objective icon when registering with the tooltip system so nameplates can render the correct learned slay/loot/talk marker. Previously the icon was always nil on fresh registration.
  • +
  • [Notes — Failed Approaches Documented] The unreleased notes now explicitly capture the Sunstrider approaches that did not work: +
      +
    • Relying on HBD:GetPlayerWorldPosition() while the world map is closed on Sunstrider, which returned the wrong world space.
    • +
    • Using ghost map 946 for C_Map.GetPlayerMapPosition, which returned invalid or misleading local coordinates.
    • +
    • Treating QuestieLearner tooltip data as legacy { questId -> objList } maps instead of the actual flat quest-id arrays.
    • +
    +
  • +
  • [Note] The Unreleased section reflects the current working tree, including documentation for local in-progress fixes that are not yet part of a pushed release.

diff --git a/docs/index.html b/docs/index.html index 587c875..5ab3b66 100644 --- a/docs/index.html +++ b/docs/index.html @@ -210,7 +210,7 @@ Questie-X Logo

A universal WoW quest-helper with a plugin architecture for any private server.

- Version: v1.6.2 + Version: v1.6.2 + Unreleased View Changelog @@ -218,6 +218,37 @@
+
+

Current Unreleased Work

+

The current working tree includes in-progress fixes for Ascension custom-zone rendering, learned tooltip reconstruction, and arrow/map behavior on Sunstrider Isle. These notes document local work that may not yet be part of the latest pushed release.

+ +
+
+

Sunstrider Coordinate Normalization

+

Arrow and world-map code now normalize Sunstrider's child map 1241, ghost map 946, and parent Eversong map 1941 into a consistent coordinate path so distance, direction, and icon placement all use the same world space.

+
+
+

Learned Tooltip Schema Fix

+

QuestieTooltips now reconstructs learned objective text from QuestieLearner.data.quests[questId][10] instead of treating learner arrays like legacy { questId -> objList } maps, preventing the Stormwind City Guard-style objList-as-number crash reported from Bronzebeard.

+
+
+ +
+
+

Custom Map Data Aliases

+

Compat/HBD.lua now aliases Sunstrider map data (1241 and 946) to Eversong (1941) and can fall back to the real HBD library's mapData for maps not present in Questie's local compat table.

+
+
+

QuestieLearner Icon Preservation

+

Learned objectives now preserve the resolved quest-log icon during immediate tooltip registration so nameplates can render the correct learned slay/loot/talk marker instead of a missing icon state.

+
+
+ +
+ Documentation scope: The landing page and changelog intentionally describe both the latest release and the current local unreleased work so collaborators can see what is being tested before the next push. +
+
+

QuestieLearner: Autonomous Data Engine

diff --git a/docs/sunstrider-coordinate-collection.md b/docs/sunstrider-coordinate-collection.md new file mode 100644 index 0000000..e81b625 --- /dev/null +++ b/docs/sunstrider-coordinate-collection.md @@ -0,0 +1,185 @@ +# Sunstrider Isle Pin Fix — Coordinate Collection Guide + +## Architecture + +On Ascension, Sunstrider Isle (uiMapId 1241) shares Eversong Woods' (1941) +coordinate space — it's a child map within Eversong. Pins for zone 3430 +(Eversong Woods) render on the Eversong map (uiMapId 1941) and appear on the +Sunstrider sub-map (1241) via ZONE_REDIRECT visibility in HBD.lua. + +### Key mappings +- **areaId 3430** (Eversong Woods) → uiMapId 1941 (Eversong map) +- **uiMapId 1241** (Sunstrider Isle) → areaId 3430 → pins redirected to 1941 via `_ResolveMapUiMapId` +- **ZONE_REDIRECT**: 1241→1941, 946→1941 (cross-visibility) +- **HBD bounds**: mapData[1241] uses Eversong's calibrated bounds for player position tracking +- **QuestieLearner**: GetZoneId() returns areaId 3430; HighConfidity set to 1 kill + +### Files modified +- `Database/Zones/zoneDB.lua` — areaIdToUiMapId[3430] = 1941 (was 1241) +- `Modules/Map/QuestieMap.lua` — _ResolveMapUiMapId redirects 1241→1941 +- `Modules/Arrow/QuestieArrow.lua` — _ResolveArrowUiMapId redirects 1241/946→1941 +- `Modules/QuestieLearner.lua` — GetZoneId() returns areaId via ZoneDB; InjectLearnedData migrates uiMapId keys; MIN_CONFIDENCE_PINS = 1 +- `Compat/HBD.lua` — ZONE_REDIRECT[1241]=1941, ASCENSION_ZONE_BOUNDS for mapData[1241] + +## Diagnostic /run Commands + +These must be run **in-game** after Questie has fully loaded (wait 5+ seconds +after login). If output is empty, the DB may not be initialized yet. + +### Check ZoneDB mappings +```lua +/run print("3430→uiMapId:", QuestieLoader:ImportModule("ZoneDB"):GetUiMapIdByAreaId(3430), " 1241→areaId:", QuestieLoader:ImportModule("ZoneDB"):GetAreaIdByUiMapId(1241)) +``` +Expected: `3430→uiMapId: 1941 1241→areaId: 3430` + +### Check known NPC spawns for Sunstrider (zone 3430) +```lua +/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local ids={15271,15273,15274,15278,15279,15280,15281,15283,15284,15285,15287,15289,15291,15292,15294,15295,15297,15298,15301,15366,15367,15371,15372}; for _,id in ipairs(ids) do local n=QuestieDB:GetNPC(id); if n and n.spawns then for z,c in pairs(n.spawns) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print(id..":"..(n.name or "?").." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end +``` + +### Check QuestieLearner overrides for zone 3430 +```lua +/run local ov=QuestieDB and QuestieDB.npcDataOverrides; if ov then for id,d in pairs(ov) do if d[7] then for z,c in pairs(d[7]) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print("override npc="..id.." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end else print("npcDataOverrides not loaded") end +``` + +### Check HBD ZONE_REDIRECT +```lua +/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); print("ResolveZone(1241)=", HBD.ResolveZone and HBD.ResolveZone(1241) or "N/A", "ResolveZone(946)=", HBD.ResolveZone and HBD.ResolveZone(946) or "N/A") +``` +Expected: `ResolveZone(1241)= 1941 ResolveZone(946)= 1941` + +### Check HBD bounds for map 1241 +```lua +/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); local d=HBD.mapData[1241]; if d then print("1241 bounds: left="..d.left.." right="..d.right.." top="..d.top.." bottom="..d.bottom.." parentMapID="..(d.parentMapID or "nil")) else print("No mapData for 1241") end +``` +Expected: `left=-2721.0066 right=-1120.9934 top=8433.9360 bottom=7367.2693 parentMapID=1941` + +### Check LearnNPC zone tracking (run after killing a mob on Sunstrider) +```lua +/run local ld=Questie.dbLearner; if ld and ld.global and ld.global.npcs then local count=0; for id,d in pairs(ld.global.npcs) do if d[7] and (d[7][3430] or d[7]["3430"]) then count=count+1; print("learned npc="..id.." mc="..(d.mc or 0).." zone=3430") end end; if count==0 then print("No learned NPCs in zone 3430 yet") end else print("Learner data not available") end +``` + +### Verify pin rendering pipeline +```lua +/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local uiMapId=ZoneDB:GetUiMapIdByAreaId(3430); print("Zone 3430 → uiMapId "..tostring(uiMapId).." (expected 1941)"); local HBD=LibStub("HereBeDragonsQuestie-2.0"); local wx,wy=HBD:GetWorldCoordinatesFromZone(0.38,0.21,uiMapId); print("World coords for (38%,21%) on map "..uiMapId..": "..string.format("%.1f, %.1f",wx or 0,wy or 0)) +``` +Expected: uiMapId 1941, world coords around (-2156, 8209) + +## Testing checklist +- [ ] Load addon on Ascension server +- [ ] Create a Blood Elf character on Sunstrider Isle +- [ ] Verify quest giver pins appear on both the Sunstrider minimap AND the Eversong world map +- [ ] Verify clicking a quest giver pin shows quest info +- [ ] Verify arrow (distance/direction) points correctly to quest targets +- [ ] After visiting/killing NPCs, verify QuestieLearner creates pin overrides (mc≥1) +- [ ] Verify pins do NOT appear in mountains or off-map +- [ ] Verify Eversong Woods (zone 1941) quest givers NOT on Sunstrider show correctly on Eversong map + +## Adding Townsfolk / NPC Data + +There are three ways to add NPC spawn data so that pins appear for +Sunstrider Isle NPCs. Choose the one that matches your data source. + +### 1. AscensionDB plugin (numeric-key array) + +The AscensionDB companion addon ships NPC data as a numeric-key array. +Each entry is keyed by NPC ID and uses numeric indices matching +`QuestieDB.npcKeys`. The spawns field is index **7** (see `npcKeys.spawns = 7`) +and is itself a dict keyed by **areaId**. + +```lua +-- AscensionDB.npcData example for a Sunstrider NPC +A.npcData = { + -- [npcId] = { [1]=name, [4]=minLevel, [5]=maxLevel, [6]=rank, [7]=spawns, ... } + [15273] = { + "Arcane Wraith", -- [1] name + nil, nil, -- [2] minLevelHealth, [3] maxLevelHealth + 1, 2, -- [4] minLevel, [5] maxLevel + 0, -- [6] rank + { -- [7] spawns ← keyed by areaId, NOT uiMapId + [3430] = { -- 3430 = Eversong Woods areaId + {38.4, 21.6}, + {39.2, 20.8}, + {40.0, 22.4}, + }, + }, + }, +} +``` + +This data is loaded by `QuestieDB:LoadAscensionNpcData()` which calls +`_Asc_MergeInto(QuestieDB.npcDataOverrides, data)` — it writes each NPC +entry directly into `npcDataOverrides[npcId]`. + +### 2. WotLKDB / TBC corrections (string-key dict) + +The built-in correction files (`wotlkNPCFixes.lua`, `tbcNPCFixes.lua`, +`classicNPCFixes.lua`) use the **named-key** format via the `npcKeys` +constants. This is the format you should use for patches submitted to +Questie-X itself. + +```lua +-- In Database/Corrections/wotlkNPCFixes.lua or tbcNPCFixes.lua +local npcKeys = QuestieDB.npcKeys + +return { + -- [npcId] = { [npcKeys.field] = value, ... } + [15273] = { + [npcKeys.spawns] = { + [3430] = { -- areaId 3430 (Eversong Woods), NOT uiMapId 1241 + {38.4, 21.6}, + {39.2, 20.8}, + {40.0, 22.4}, + }, + }, + }, +} +``` + +These corrections are merged into `QuestieDB.npcDataOverrides` by the +correction loader before overrides are applied. + +### 3. QuestieLearner runtime (automatic) + +QuestieLearner learns NPC positions automatically as you play. When you +kill or interact with an NPC on Sunstrider Isle, `LearnNPC` stores the +spawn under **areaId 3430** (the return value of `GetZoneId()`, which +uses `ZoneDB:GetAreaIdByUiMapId(1241) → 3430`). Learned data is written +to `Questie.dbLearner.global.npcs[npcId]` as a numeric-key array +(identical structure to AscensionDB) and injected into +`npcDataOverrides` once the confidence threshold (`mc >= MIN_CONFIDENCE_PINS`) +is met. + +### CRITICAL RULE: spawns are keyed by areaId, NOT uiMapId + +This bears repeating because it is the #1 source of Sunstrider bugs: + +- **CORRECT**: `[3430] = { {38.4, 21.6}, ... }` — areaId for Eversong Woods +- **WRONG**: `[1241] = { {38.4, 21.6}, ... }` — uiMapId for Sunstrider Isle + +Questie's internal spawn tables use `areaId` as the key. The ZoneDB +redirect (`uiMapId 1241 → areaId 3430`) ensures that even when the +player is on the Sunstrider sub-map, the correct areaId is used. If you +accidentally key spawns by uiMapId (1241), they will never be found by +`GetNPC` and no pins will render. + +### _MergeOverride fix (historical note) + +Prior to the `_MergeOverride` helper (added as part of the Sunstrider +pin fix), the `GetNPC` function only checked **string-keyed** override +entries (`override["spawns"]`). AscensionDB and QuestieLearner store +overrides with **numeric keys** (`override[7]`), so their spawn data +was silently ignored. The `_MergeOverride` function now checks both +formats: + +```lua +-- _MergeOverride checks both override formats: +-- 1. override[stringKey] (string-keyed, e.g. from wotlkNPCFixes) +-- 2. override[intKey] (numeric-keyed, e.g. from QuestieLearner / AscensionDB) +-- 3. rawdata[intKey] (fallback to compiled DB) +``` + +This means override data from **all three sources** is now visible to +`GetNPC` for the first time. If you are debugging and overrides seem +ignored, confirm `_MergeOverride` is being called (line 1923 in +QuestieDB.lua as of this writing). \ No newline at end of file diff --git a/docs/sunstrider-pin-fix.md b/docs/sunstrider-pin-fix.md new file mode 100644 index 0000000..861b5d7 --- /dev/null +++ b/docs/sunstrider-pin-fix.md @@ -0,0 +1,236 @@ +# Sunstrider Isle Pin Fix — Questie-X on Ascension + +## Architecture (Current) + +On Ascension, Sunstrider Isle (uiMapId 1241) shares Eversong Woods' (1941) +coordinate space. The fix ensures correct cross-map pin visibility. + +### Coordinate Flow +``` +NPC spawn data: zone 3430 (Eversong) → GetUiMapIdByAreaId(3430) → uiMapId 1941 + → pin rendered on Eversong map (1941) with Eversong coordinates + → ZONE_REDIRECT makes pin visible on Sunstrider (1241) too + → _ResolveMapUiMapId redirects 1241→1941 for consistency +``` + +### Key Mappings +| Lookup | From | To | Purpose | +|--------|------|----|---------| +| `GetUiMapIdByAreaId(3430)` | areaId 3430 | uiMapId 1941 | Pin placement on Eversong map | +| `GetAreaIdByUiMapId(1241)` | uiMapId 1241 | areaId 3430 | Zone ID for spawn data keys | +| `_ResolveMapUiMapId(1241)` | uiMapId 1241 | uiMapId 1941 | Normalize pin rendering | +| `_ResolveArrowUiMapId(1241)` | uiMapId 1241 | uiMapId 1941 | Arrow math normalization | +| `ZONE_REDIRECT[1241]` | uiMapId 1241 | uiMapId 1941 | Cross-visibility | +| `ZONE_REDIRECT[946]` | uiMapId 946 | uiMapId 1941 | Cross-visibility (ghost map) | +| HBD bounds `mapData[1241]` | — | Eversong's bounds | Player position tracking on Sunstrider | + +### Why zone 3430 → uiMapId 1941 (not 1241) + +Zone 3430 = Eversong Woods (the whole zone, not just Sunstrider). +In the WotLKDB, NPC spawn coordinates under zone 3430 are Eversong-wide +percentages (e.g., NPC 15278 at 38.02%, 21.01%). These render correctly on +the Eversong map (1941). Mapping 3430→1241 would place Eversong-wide +coordinates on the Sunstrider sub-map, producing wrong positions. + +Pins from zone 3430 render on uiMapId 1941 (Eversong) and appear on uiMapId +1241 (Sunstrider) via ZONE_REDIRECT visibility, which works because +`ResolveZone(1241) == ResolveZone(1941) == 1941`. + +--- + +## Files Modified + +### Database/Zones/zoneDB.lua +- `areaIdToUiMapId[3430] = 1941` (was 1241) +- `uiMapIdToAreaIdCache[1241] = 3430` (unchanged — Sunstrider map IS in Eversong zone) +- `UiMapIdOverrides[1241] = 3430` (unchanged — reverse lookup) + +### Modules/Map/QuestieMap.lua +- `_ResolveMapUiMapId(1241, x, y)` → redirects to 1941 +- `_ResolveMapUiMapId(946, x, y)` → redirects to 1941 +- Pins from zone 3430 naturally go to uiMapId 1941 (no redirect needed for them) + +### Modules/Arrow/QuestieArrow.lua +- `_ResolveArrowUiMapId(1241)` → 1941 +- `_ResolveArrowUiMapId(946)` → 1941 +- Comment updated to match new approach + +### Modules/QuestieLearner.lua +- `GetZoneId()`: Returns areaId via `ZoneDB:GetAreaIdByUiMapId(uiMapId)` with fallback +- `MIN_CONFIDENCE_PINS = 1` (was 2) — Ascension needs every data point +- `InjectLearnedData()`: Migration converts uiMapId spawn keys to areaId (1241→3430) +- All `LearnNPC` call sites now pass zoneId: + - `OnMouseoverUnit`: passes areaId from `l10n:GetAreaIdByLocalName()` + - `OnQuestDetail`: passes zoneId from `GetZoneId()` + - `OnQuestComplete`: passes zoneId from `GetZoneId()` + - `OnQuestAccepted`: passes `GetZoneId()` + - `OnQuestTurnedIn`: passes `GetZoneId()` + - `GOSSIP_SHOW` handler: passes `GetZoneId()` + - Kill handler: passes `bestKill.zoneId` (already correct) + +### Compat/HBD.lua +- `ASCENSION_ZONE_BOUNDS[1241]` = Eversong's calibrated bounds for player position tracking +- `ASCENSION_ZONE_BOUNDS[946]` = same +- `ZONE_REDIRECT[1241]=1941`, `ZONE_REDIRECT[946]=1941` (visibility) +- `ResolveZone()` for `isSameZoneSpace` checks + +### Database/QuestieDB.lua +- `_MergeOverride(data, key, override)`: Fixed numeric-vs-string key mismatch + - **Bug**: Override sources (wotlkNPCFixes, AscensionDB, QuestieLearner) could store spawn + zone keys as either numbers (`3430`) or strings (`"3430"`). When `_MergeOverride` merged + spawns into the base NPC data, a string key like `"3430"` would create a *new* table entry + alongside the existing numeric `3430` key, producing duplicate spawn entries that rendered + pins twice or confused zone lookups. + - **Fix**: `_MergeOverride` now normalises all zone keys to numeric before merging. Any + string-keyed spawn entry (e.g. `{["3430"] = {{0.38,0.21}}}`) is converted to its numeric + equivalent (`{3430 = {{0.38,0.21}}}`) before the merge loop runs, so both formats resolve + to the same table slot. + - This fix is applied **once** inside `_MergeOverride` — no changes needed in individual + override sources. + +### Modules/QuestieLearner.lua (zone tracking additions) +- `OnQuestComplete`: Now captures `zoneId` via `GetZoneId()` and passes it as `spawnZoneId` + to every `LearnNPC` call inside this handler. +- All `LearnNPC` call sites now pass `spawnZoneId` — the area ID of the zone the player + was in when the event fired. Previously only some handlers included zone data; now every + path supplies it, giving `npcDataOverrides` consistent spawn-zone keys for learned NPCs. + +--- + +## NPC Data Format & Override Pipeline + +### Override Sources +Three systems feed into `QuestieDB.npcDataOverrides`, each producing spawn data that +Questie merges at load time: + +| Source | When it runs | Key format | Typical content | +|--------|-------------|------------|-----------------| +| `wotlkNPCFixes` (Database/NPCs) | Addon load | numeric | Corrections for vanilla→WotLK data changes | +| AscensionDB plugin | Addon load | numeric | Ascension-specific NPC additions & tweaks | +| QuestieLearner | Runtime events | **was string** (now numeric via `_MergeOverride`) | Player-observed NPC spawns | + +### Numeric-vs-String Key Issue +Lua tables can have both `3430` (number) and `"3430"` (string) as separate keys. +The base NPC data in `QuestieDB.npcs` uses **numeric** zone keys exclusively. +If an override source stored spawns under `"3430"`, the merge would produce: + +```lua +spawns = { + [3430] = {{0.38, 0.21}}, -- original + ["3430"]= {{0.38, 0.21}}, -- duplicate from string key +} +``` + +This caused double pins and zone-lookup failures. The `_MergeOverride` fix normalises +all keys to numeric *before* merging, collapsing both entries into one. + +### How _MergeOverride Resolves Both Formats +```lua +-- Inside _MergeOverride, before merging spawns (field index 7): +if override[7] then + local normalised = {} + for zoneKey, coords in pairs(override[7]) do + normalised[tonumber(zoneKey) or zoneKey] = coords + end + override[7] = normalised +end +-- Then proceed with the standard deep-merge loop +``` + +This ensures every string key like `"3430"` is converted to `3430`, matching the +numeric keys in the base data. The fix is centralised — each override source can +store keys in whatever format is convenient. + +### Adding Townsfolk Data to AscensionDB Plugin +To add a townsfolk (non-combat NPC) to the AscensionDB plugin's override data: + +```lua +-- In AscensionDB/NPCs.lua (or equivalent), npcDataOverrides section: +npcDataOverrides[] = { + -- Field layout follows QuestieDB NPC format: + -- [1] name, [2] minLevel, [3] maxLevel, [4] friendly (0=hostile, 1=friendly) + -- [5] spawnByZone or nil, [6] waypoints or nil, + -- [7] spawns keyed by areaId + [7] = { + [3430] = { -- areaId for Eversong Woods (covers Sunstrider Isle) + {0.38, 0.21}, -- {x%, y%} on the Eversong map + }, + }, +} +``` + +Key points: +- Use **numeric** areaId keys (`3430`, not `"3430"`). Even though `_MergeOverride` + now handles both formats, numeric is canonical and avoids ambiguity. +- Spawn coordinates are percentages (0–1 range) relative to the Eversong Woods map + (uiMapId 1941), **not** the Sunstrider sub-map. +- Townsfolk typically set field `[4] = 1` (friendly). +- areaId `3430` covers both Eversong Woods and Sunstrider Isle — no separate entry + for the sub-zone is needed because `ZONE_REDIRECT` handles cross-visibility. + +--- + +## Diagnostic /run Commands + +Must be run **in-game** after Questie has fully loaded (5+ seconds after login). + +### Check ZoneDB mappings +```lua +/run print("3430→uiMapId:", QuestieLoader:ImportModule("ZoneDB"):GetUiMapIdByAreaId(3430), " 1241→areaId:", QuestieLoader:ImportModule("ZoneDB"):GetAreaIdByUiMapId(1241)) +``` +Expected: `3430→uiMapId: 1941 1241→areaId: 3430` + +### Check HBD ZONE_REDIRECT +```lua +/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); print("ResolveZone(1241)=", HBD.ResolveZone and HBD.ResolveZone(1241) or "N/A", "ResolveZone(946)=", HBD.ResolveZone and HBD.ResolveZone(946) or "N/A") +``` +Expected: `ResolveZone(1241)= 1941 ResolveZone(946)= 1941` + +### Check known NPC spawns for Sunstrider zone (3430) +```lua +/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local ids={15271,15273,15274,15278,15279,15280,15281,15283,15284,15285,15287,15289,15291,15292,15294,15295,15297,15298,15301,15366,15367,15371,15372}; for _,id in ipairs(ids) do local n=QuestieDB:GetNPC(id); if n and n.spawns then for z,c in pairs(n.spawns) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print(id..":"..(n.name or "?").." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end +``` + +### Check QuestieLearner overrides for zone 3430 +```lua +/run local ov=QuestieDB and QuestieDB.npcDataOverrides; if ov then for id,d in pairs(ov) do if d[7] then for z,c in pairs(d[7]) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print("override npc="..id.." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end else print("npcDataOverrides not loaded") end +``` + +### Check HBD bounds for map 1241 +```lua +/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); local d=HBD.mapData[1241]; if d then print("1241: left="..d.left.." right="..d.right.." top="..d.top.." bottom="..d.bottom.." parentMapID="..(d.parentMapID or "nil")) else print("No mapData for 1241") end +``` + +### Check learned data (after visiting Sunstrider) +```lua +/run local ld=Questie.dbLearner; if ld and ld.global and ld.global.npcs then local count=0; for id,d in pairs(ld.global.npcs) do if d[7] and (d[7][3430] or d[7]["3430"]) then count=count+1; print("learned npc="..id.." mc="..(d.mc or 0).." zone=3430") end end; if count==0 then print("No learned NPCs in zone 3430 yet") end else print("Learner data not available") end +``` + +### Verify pin rendering +```lua +/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local uiMapId=ZoneDB:GetUiMapIdByAreaId(3430); print("Zone 3430 → uiMapId "..tostring(uiMapId).." (expected 1941)"); local HBD=LibStub("HereBeDragonsQuestie-2.0"); local wx,wy=HBD:GetWorldCoordinatesFromZone(0.38,0.21,uiMapId); print("World coords for (38%,21%) on map "..uiMapId..": "..string.format("%.1f, %.1f",wx or 0,wy or 0)) +``` + +### Verify MIN_CONFIDENCE_PINS +```lua +/run print("minConfidencePins:", Questie.dbLearner.global.settings.minConfidencePins or "default(1)") +``` + +### Reset all learned data (WARNING: deletes everything!) +```lua +/run Questie.dbLearner.global.npcs = {}; Questie.dbLearner.global.quests = {}; Questie.dbLearner.global.items = {}; Questie.dbLearner.global.objects = {}; ReloadUI() +``` + +--- + +## Testing Checklist +- [ ] Load addon on Ascension server +- [ ] Create a Blood Elf character on Sunstrider Isle +- [ ] Verify diagnostic: `GetUiMapIdByAreaId(3430)` returns 1941 +- [ ] Verify quest giver pins appear on BOTH Sunstrider minimap AND Eversong world map +- [ ] Verify pins do NOT appear in mountains or off-map +- [ ] Verify arrow (distance/direction) points correctly to quest targets +- [ ] Kill 1 NPC on Sunstrider, check learned data shows zone=3430 (not 1241) +- [ ] After 1+ kill, verify learned pin auto-appears at correct position +- [ ] Verify Eversong Woods NPCs NOT on Sunstrider show correctly on Eversong map +- [ ] Check no regressions on other zones \ No newline at end of file diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..176443b --- /dev/null +++ b/selene.toml @@ -0,0 +1,11 @@ +std = "wow_classic" + +[lints] +global_usage = "allow" +unused_variable = "allow" +duplicate_key = "allow" +ambiguous_if = "allow" +incorrect_standard_library_use = "allow" +undefined_variable = "allow" +if_same_then_else = "allow" +constant_table_comparison = "allow" \ No newline at end of file diff --git a/wow_classic.yml b/wow_classic.yml new file mode 100644 index 0000000..d75efbf --- /dev/null +++ b/wow_classic.yml @@ -0,0 +1,321 @@ +name: wow_classic + +globals: + UIParent: + property: new-fields + WorldFrame: + property: new-fields + GameTooltip: + property: new-fields + Minimap: + property: new-fields + WorldMapDetailFrame: + property: new-fields + StaticPopupDialogs: + property: new-fields + QuestLogFrame: + property: new-fields + DEFAULT_CHAT_FRAME: + property: new-fields + ChatFrame1: + property: new-fields + CreateFrame: + property: new-fields + GetTime: + property: new-fields + UnitName: + property: new-fields + UnitGUID: + property: new-fields + UnitLevel: + property: new-fields + UnitFactionGroup: + property: new-fields + GetMinimapZoneText: + property: new-fields + GetZoneText: + property: new-fields + GetSubZoneText: + property: new-fields + SetMapToCurrentZone: + property: new-fields + SetMapByID: + property: new-fields + GetPlayerMapPosition: + property: new-fields + GetCurrentMapAreaID: + property: new-fields + GetCurrentMapZone: + property: new-fields + C_Map: + property: new-fields + C_Timer: + property: new-fields + LibStub: + property: new-fields + HBD: + property: new-fields + HBDp: + property: new-fields + RegisterEvent: + property: new-fields + UnregisterEvent: + property: new-fields + UnregisterAllEvents: + property: new-fields + SetScript: + property: new-fields + hooksecurefunc: + property: new-fields + InCombatLockdown: + property: new-fields + RegisterStateDriver: + property: new-fields + UnregisterStateDriver: + property: new-fields + geterrorhandler: + property: new-fields + debugstack: + property: new-fields + print: + property: new-fields + wipe: + property: new-fields + select: + property: new-fields + format: + property: new-fields + strsplit: + property: new-fields + pcall: + property: new-fields + error: + property: new-fields + next: + property: new-fields + pairs: + property: new-fields + ipairs: + property: new-fields + Ambiguate: + property: new-fields + PlaySound: + property: new-fields + GetAddOnInfo: + property: new-fields + IsAddOnLoaded: + property: new-fields + GetLocale: + property: new-fields + GetRealmName: + property: new-fields + GetScreenWidth: + property: new-fields + GetScreenHeight: + property: new-fields + GetItemInfo: + property: new-fields + GetItemCount: + property: new-fields + GetQuestLogTitle: + property: new-fields + GetQuestID: + property: new-fields + SelectQuestLogEntry: + property: new-fields + AcceptQuest: + property: new-fields + CompleteQuest: + property: new-fields + GetQuestReward: + property: new-fields + CloseQuest: + property: new-fields + GetTitleText: + property: new-fields + GetGreetingText: + property: new-fields + SendChatMessage: + property: new-fields + RAID_CLASS_COLORS: + property: new-fields + Enum: + property: new-fields + arg: + property: new-fields + this: + property: new-fields + tinsert: + property: new-fields + tremove: + property: new-fields + abs: + property: new-fields + max: + property: new-fields + min: + property: new-fields + Questie: + property: new-fields + QuestieLoader: + property: new-fields + QuestieCompat: + property: new-fields + QuestieDB: + property: new-fields + Questie_SV: + property: new-fields + QuestieCompartment: + property: new-fields + QuestieArrow: + property: new-fields + AceAddon-3.0: + property: new-fields + AceConsole-3.0: + property: new-fields + AceConfig-3.0: + property: new-fields + AceDB-3.0: + property: new-fields + AceDBOptions-3.0: + property: new-fields + AceEvent-3.0: + property: new-fields + AceHook-3.0: + property: new-fields + AceLocale-3.0: + property: new-fields + AceSerializer-3.0: + property: new-fields + CallbackHandler-1.0: + property: new-fields + string: + property: new-fields + table: + property: new-fields + math: + property: new-fields + unpack: + property: new-fields + tonumber: + property: new-fields + tostring: + property: new-fields + type: + property: new-fields + setmetatable: + property: new-fields + getmetatable: + property: new-fields + rawget: + property: new-fields + rawset: + property: new-fields + rawequal: + property: new-fields + loadstring: + property: new-fields + time: + property: new-fields + date: + property: new-fields + floor: + property: new-fields + ceil: + property: new-fields + mod: + property: new-fields + gsub: + property: new-fields + gmatch: + property: new-fields + match: + property: new-fields + find: + property: new-fields + format: + property: new-fields + sub: + property: new-fields + lower: + property: new-fields + upper: + property: new-fields + len: + property: new-fields + reverse: + property: new-fields + concat: + property: new-fields + insert: + property: new-fields + remove: + property: new-fields + sort: + property: new-fields + IsShiftKeyDown: + property: new-fields + IsControlKeyDown: + property: new-fields + IsAltKeyDown: + property: new-fields + GossipFrame: + property: new-fields + GossipFrameGreetingPanel: + property: new-fields + QuestFrameGreetingPanel: + property: new-fields + QuestFrameDetailPanel: + property: new-fields + QuestFrameProgressPanel: + property: new-fields + QuestFrameRewardPanel: + property: new-fields + QuestFrameGoodbyeButton: + property: new-fields + IsQuestCompletable: + property: new-fields + SelectAvailableQuest: + property: new-fields + ConfirmAcceptQuest: + property: new-fields + GetNumActiveQuests: + property: new-fields + GetActiveTitle: + property: new-fields + SelectActiveQuest: + property: new-fields + GetNumAvailableQuests: + property: new-fields + GetNumQuestChoices: + property: new-fields + _G: + property: new-fields + GameFontHighlightLarge: + property: new-fields + GameFontHighlightSmall: + property: new-fields + GameFontHighlight: + property: new-fields + GameFontNormal: + property: new-fields + UISpecialFrames: + property: new-fields + QuestFrameCloseButton: + property: new-fields + WorldMapTooltip: + property: new-fields + IsModifierKeyDown: + property: new-fields + ChatEdit_GetActiveWindow: + property: new-fields + StaticPopup_Show: + property: new-fields + ChatEdit_InsertLink: + property: new-fields + TomTom: + property: new-fields + MBB_Ignore: + property: new-fields + coroutine: + property: new-fields \ No newline at end of file