v1.6.3-unreleased: Sunstrider zone fixes, QuestieLearner icon preservation, migration, and Busted test suite

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
This commit is contained in:
Xurkon
2026-05-16 07:32:47 -05:00
parent 306fb2ac89
commit bf6ecaedbe
22 changed files with 2218 additions and 179 deletions
+15
View File
@@ -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,
},
}
+4 -4
View File
@@ -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/
+28 -8
View File
@@ -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_<id>`, `o_<id>`) 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_<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.
- **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)
+27
View File
@@ -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.
+204 -17
View File
@@ -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
+24
View File
@@ -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,
+32 -30
View File
@@ -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)
+9 -5
View File
@@ -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
+26
View File
@@ -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
+256 -72
View File
@@ -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
+28 -12
View File
@@ -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
+103 -3
View File
@@ -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)
+28 -1
View File
@@ -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()
+115 -13
View File
@@ -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
+339
View File
@@ -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)
+161
View File
@@ -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
+33 -12
View File
@@ -169,32 +169,53 @@
<h1>Questie-X Documentation</h1>
<p class="subtitle">Complete history of changes, fixes, and additions.</p>
<div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.6.1</code>
<code>Version: v1.6.2 + Unreleased</code>
<a href="index.html"
style="background: var(--bg-tertiary); color: var(--accent-blue); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">&larr; Back to Documentation</a>
</div>
</div>
<div class="container">
<h2 id="unreleased">[Unreleased] &mdash; Sunstrider Isle Arrow, Tooltip Guard, QuestData String Safety</h2>
<h2 id="unreleased">[Unreleased] &mdash; Sunstrider Isle Arrow Distance, Map Pins, Tooltip Schema Fixes, QuestData String Safety</h2>
<ul>
<li><strong>[Fix &mdash; MapIconTooltip _GetLevelString Guard]</strong> Resolved <code>attempt to concatenate local 'minLevel' (a nil value)</code> crash in <code>MapIconTooltip.lua:494</code> (<code>_GetLevelString</code> function). The creature name &quot;Uneasy Citizen&quot; existed in <code>creatureLevels</code> as an empty table <code>{}</code> rather than the expected <code>[1]=minLevel, [2]=maxLevel, [3]=rank</code> tuple. Added an early-return guard at the top of <code>_GetLevelString</code>: if <code>creatureLevels[name]</code> is falsy, return the name unmodified.
</li>
<li><strong>[Fix &mdash; Tooltip NPC/Object Type Guard]</strong> Resolved a crash in <code>QuestieTooltips</code> when hovering over NPC or object tooltip keys (<code>m_&lt;id&gt;</code>, <code>o_&lt;id&gt;</code>) where <code>learnedNpc[10]</code> or <code>learnedObj[10]</code> was unexpectedly a string instead of a table.
<li><strong>[Fix &mdash; MapIconTooltip _GetLevelString Guard]</strong> Resolved <code>attempt to concatenate local 'minLevel' (a nil value)</code> crash in <code>MapIconTooltip.lua:494</code> (<code>_GetLevelString</code> function). The creature name &quot;Uneasy Citizen&quot; existed in <code>creatureLevels</code> as an empty table <code>{}</code> rather than the expected <code>[1]=minLevel, [2]=maxLevel, [3]=rank</code> tuple. Added an early-return guard at the top of <code>_GetLevelString</code>: if <code>creatureLevels[name]</code> is falsy or not a table with a numeric level at index <code>[1]</code>, return the name unmodified.</li>
<li><strong>[Fix &mdash; Tooltip Learned Data Schema Mismatch]</strong> Reworked learned NPC/object tooltip registration in <code>Modules/Tooltips/Tooltip.lua</code> to match the actual <code>QuestieLearner</code> storage format.
<ul>
<li><strong>Root Cause</strong>: <code>InsertMissingQuestIds</code> in the WotLKDB corrections files writes directly to <code>QuestieDB.questData[questId]</code> but <code>questData</code> is stored as a loadable Lua string on Ascension. When code later tried to index into that string as a table, it threw <code>attempt to index field 'questData' (a string value)</code>.</li>
<li><strong>Fix</strong>: Added <code>if type(objList) ~= "table" then break end</code> guard in both <code>m_/NPC</code> and <code>o_/object</code> iteration paths in <code>Tooltip.lua</code> before iterating <code>learnedNpc[10]</code> / <code>learnedObj[10]</code>.</li>
<li><strong>Player-Facing Symptom</strong>: Fixed the Stormwind mouseover crash reported on Bronzebeard while hovering city guards and other learned tooltip targets: <code>Questie-X\\Modules\\Tooltips\\Tooltip.lua:240: attempt to index local 'objList' (a number value)</code>.</li>
<li><strong>Root Cause</strong>: <code>QuestieLearner:_AddToArray</code> stores flat arrays of quest IDs (<code>learnedNpc[10] = { questId1, questId2, ... }</code>, <code>learnedObj[2] = { questId1, questId2, ... }</code>), but the pushed tooltip code still iterated them as <code>{ questId -&gt; objList }</code> maps. That caused crashes like <code>attempt to index local 'objList' (a number value)</code> when a quest ID number was treated like an objective-text array.</li>
<li><strong>Fix</strong>: Replaced the legacy <code>for questId, objList in next, ...</code> / <code>objList[oIndex]</code> traversal with schema-correct lookup: iterate learned quest IDs via <code>ipairs</code>, fetch <code>QuestieLearner.data.quests[questId]</code>, then walk <code>qData[10]</code> objective slots and entries (<code>objEntry[2]</code>) to reconstruct tooltip text safely.</li>
<li><strong>Scope</strong>: Applied to both learned NPC tooltips (<code>m_&lt;id&gt;</code>) and learned object tooltips (<code>o_&lt;id&gt;</code>). Object lookup now reads quest IDs from <code>learnedObj[2]</code> (quest starts) instead of the old <code>learnedObj[10]</code> path.</li>
</ul>
</li>
<li><strong>[Fix &mdash; InsertMissingQuestIds String Guard]</strong> Added <code>if type(QuestieDB.questData) ~= "table" then return end</code> guard at the start of <code>InsertMissingQuestIds()</code> in both <code>tbcQuestFixes.lua</code> and <code>wotlkQuestFixes.lua</code>. Prevents the function from writing to <code>questData</code> while it is still an uncompiled string during early loader initialization.</li>
<li><strong>[Fix &mdash; Sunstrider Isle Arrow / Zone Override]</strong> Resolved the quest arrow not appearing on Sunstrider Isle (Ascension's starting zone) when the world map is closed.
<li><strong>[Fix &mdash; Sunstrider Isle Arrow Distance (Map Closed)]</strong> 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.
<ul>
<li><strong>Root Cause</strong>: <code>C_Map.GetBestMapForUnit("player")</code> returns <code>946</code> (ghost/loading map uiMapId) instead of <code>1241</code> (Sunstrider Isle's real uiMapId) when the world map is closed. <code>ZoneDB:GetAreaIdByUiMapId(946)</code> had no override, causing <code>GetCurrentZoneId()</code> to return <code>946</code> instead of <code>3430</code> (Sunstrider Isle's areaId). This broke target zone filtering in <code>_CollectObjective</code> and caused <code>HBD:GetWorldCoordinatesFromZone</code> to return <code>0,0</code> (no world coord data for map 946).</li>
<li><strong>Fix &mdash; zoneDB.lua</strong>: Added <code>[946] = 3430</code> and <code>[1241] = 3430</code> to <code>UiMapIdOverrides</code> so <code>GetAreaIdByUiMapId</code> always resolves to the real Sunstrider Isle areaId regardless of which ghost or real uiMapId the game returns.</li>
<li><strong>Fix &mdash; QuestieArrow.lua</strong>: Updated <code>UpdateNearestTargets</code> fallback chain to use <code>QuestiePlayer:GetCurrentUiMapId()</code> for player position. When that returns an invalid/ghost map (946/947/0), it falls back to a <code>ZoneDB</code> lookup via the actual zoneId. Ensures the arrow gets real world coordinates via <code>C_Map.GetPlayerMapPosition</code> + <code>HBD:GetWorldCoordinatesFromZone</code> regardless of map open/closed state.</li>
<li><strong>Debug Output</strong>: Added per-frame debug output (respecting <code>debugArrow</code> profile setting) showing <code>frameShown</code>, <code>target.title</code>, player coordinates, and uiMapId values.</li>
<li><strong>Root Cause (Player Position)</strong>: <code>HBD:GetPlayerWorldPosition()</code> 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.</li>
<li><strong>Root Cause (Ghost Map Player Position)</strong>: <code>C_Map.GetPlayerMapPosition(946, "player")</code> where <code>946</code> is Ascension's ghost/cosmic map for Sunstrider Isle returns <code>(0, 0)</code> because the ghost map has no valid coordinate data. The correct map for player position on Sunstrider is <code>1941</code> (Eversong Woods parent), which shares the same world coordinate space and returns valid zone coords.</li>
<li><strong>Root Cause (OnUpdate same-map check)</strong>: <code>OnUpdate</code> was using <code>_ResolveArrowUiMapId(_arrow_playerUiMapId)</code> (which resolves 1241&rarr;1941) for the player-side uiMapId, making it equal to <code>target.uiMapId</code> (also 1941). This caused the same-map branch to fire, which then called <code>C_Map.GetPlayerMapPosition(_arrow_playerUiMapId)</code> with 946, getting <code>(0, 0)</code> and computing wrong distance.</li>
<li><strong>Fix &mdash; QuestieArrow.lua UpdateNearestTargets</strong>: Added Sunstrider detection (<code>zoneId == 3430</code>) <em>before</em> calling <code>HBD:GetPlayerWorldPosition()</code> to force the C_Map fallback path. The fallback now calls <code>C_Map.GetPlayerMapPosition(1241, "player")</code> to get Sunstrider map-space coords, then converts through <code>HBD:GetWorldCoordinatesFromZone(..., 1941)</code> using Eversong Woods bounds &mdash; which share Sunstrider's world coordinate space.</li>
<li><strong>Fix &mdash; QuestieArrow.lua OnUpdate same-map branch</strong>: Changed <code>C_Map.GetPlayerMapPosition(1941, "player")</code> for player zone coords (not <code>_arrow_playerUiMapId</code> which is 946). Normalizes both <code>playerUiMapId</code> and <code>targetUiMapId</code> through <code>_ResolveArrowUiMapId()</code> before same-map comparison. Uses <code>zoneScale = 13.53</code> yards/zone-unit for distance. Declares <code>worldPlayerX/Y</code> before the if-else to prevent nil in debug prints.</li>
<li><strong>Debug Output</strong>: Added debug prints showing UnitPosition vs HBD player coords comparison, raw vs resolved uiMapIds, branch selection, and per-frame distance calculation inputs.</li>
</ul>
</li>
<li><strong>[Fix &mdash; Sunstrider Isle Map Pins Not Appearing]</strong> 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).
<ul>
<li><strong>Root Cause</strong>: <code>HBDPins:HandlePin</code> (<code>HereBeDragons-Pins-2.0:424</code>) has an early-return guard: <code>if not HBD.mapData[uiMapID] then return end</code>. When the player zooms into Sunstrider Isle, <code>uiMapID</code> is 1241, but <code>HBD.mapData[1241]</code> is nil &mdash; no mapData entry existed for Sunstrider's custom child map. The icon was silently dropped before any coordinate conversion occurred.</li>
<li><strong>Fix &mdash; Compat/HBD.lua</strong>: Added <code>mapData[1241] = mapData[1941]</code> alias and <code>mapData[946] = mapData[1941]</code> alias. Sunstrider Isle (1241) and its ghost map (946) share Eversong Woods' (1941) world coordinate space for these conversions.</li>
<li><strong>Fix &mdash; HBD fallback loading</strong>: Added a lazy fallback to the real HBD library's <code>mapData</code> for maps not present in Questie's compat table, so custom/private-server maps can still resolve world and zone coordinates when <code>QuestieCompat</code> lacks a local entry.</li>
<li><strong>Fix &mdash; Modules/Map/QuestieMap.lua</strong>: Added <code>_ResolveMapUiMapId()</code> helper (1241&rarr;1941) applied across <code>FadeLogic</code>, <code>FindClosestStarter</code>, <code>GetNearestSpawn</code>, and <code>GetNearestQuestSpawn</code>. <code>DrawWorldIcon</code> and <code>DrawManualIcon</code> now also store and render Sunstrider map icons against the resolved parent map coordinate space.</li>
</ul>
</li>
<li><strong>[Fix &mdash; Northshire Valley UiMapData Registration]</strong> Added explicit <code>QuestieCompat.UiMapData[1238]</code> for Northshire Valley so custom/private-server zone lookups have concrete geometry for the child map instead of relying on incomplete parent fallbacks.</li>
<li><strong>[Fix &mdash; QuestieLearner Icon Preservation]</strong> 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.</li>
<li><strong>[Notes &mdash; Failed Approaches Documented]</strong> The unreleased notes now explicitly capture the Sunstrider approaches that did <em>not</em> work:
<ul>
<li>Relying on <code>HBD:GetPlayerWorldPosition()</code> while the world map is closed on Sunstrider, which returned the wrong world space.</li>
<li>Using ghost map <code>946</code> for <code>C_Map.GetPlayerMapPosition</code>, which returned invalid or misleading local coordinates.</li>
<li>Treating <code>QuestieLearner</code> tooltip data as legacy <code>{ questId -&gt; objList }</code> maps instead of the actual flat quest-id arrays.</li>
</ul>
</li>
<li><strong>[Note]</strong> The Unreleased section reflects the current working tree, including documentation for local in-progress fixes that are not yet part of a pushed release.</li>
</ul>
<hr>
+32 -1
View File
@@ -210,7 +210,7 @@
<img src="QuestieXlogo.png" alt="Questie-X Logo" width="400" />
<p class="subtitle">A universal WoW quest-helper with a plugin architecture for any private server.</p>
<div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.6.2</code>
<code>Version: v1.6.2 + Unreleased</code>
<a href="changelog.html"
style="background: var(--bg-tertiary); color: var(--accent-green); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">View
Changelog</a>
@@ -218,6 +218,37 @@
</div>
<div class="container">
<section id="unreleased-highlights">
<h2>Current Unreleased Work</h2>
<p>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.</p>
<div class="grid">
<div class="card">
<h4>Sunstrider Coordinate Normalization</h4>
<p>Arrow and world-map code now normalize Sunstrider's child map <code>1241</code>, ghost map <code>946</code>, and parent Eversong map <code>1941</code> into a consistent coordinate path so distance, direction, and icon placement all use the same world space.</p>
</div>
<div class="card">
<h4>Learned Tooltip Schema Fix</h4>
<p><code>QuestieTooltips</code> now reconstructs learned objective text from <code>QuestieLearner.data.quests[questId][10]</code> instead of treating learner arrays like legacy <code>{ questId -&gt; objList }</code> maps, preventing the Stormwind City Guard-style <code>objList</code>-as-number crash reported from Bronzebeard.</p>
</div>
</div>
<div class="grid">
<div class="card">
<h4>Custom Map Data Aliases</h4>
<p><code>Compat/HBD.lua</code> now aliases Sunstrider map data (<code>1241</code> and <code>946</code>) to Eversong (<code>1941</code>) and can fall back to the real HBD library's <code>mapData</code> for maps not present in Questie's local compat table.</p>
</div>
<div class="card">
<h4>QuestieLearner Icon Preservation</h4>
<p>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.</p>
</div>
</div>
<div class="important">
<strong>Documentation scope:</strong> 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.
</div>
</section>
<!-- QuestieLearner Engine -->
<section id="questielearner">
<h2>QuestieLearner: Autonomous Data Engine</h2>
+185
View File
@@ -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).
+236
View File
@@ -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[<npcId>] = {
-- 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 (01 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
+11
View File
@@ -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"
+321
View File
@@ -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