Fix Sunstrider Isle arrow, Tooltip type guard, mapId 946 override

- zoneDB.lua: Add [946] = 3430 to UiMapIdOverrides so GetCurrentZoneId()
  returns 3430 (Sunstrider Isle areaId) even when the game returns uiMapId 946
  (ghost/loading map). Previously 946 had no override, causing zone lookups to
  fall through and return 946 instead of the real zone, breaking arrow distance
  calculation and target filtering.

- QuestieArrow.lua: UpdateNearestTargets uses QuestiePlayer:GetCurrentUiMapId()
  (backed by C_Map.GetBestMapForUnit) for player position. When that returns an
  invalid/ghost map (946/947/0), fall back to ZoneDB lookup via the actual
  zoneId. This ensures the arrow gets real world coordinates regardless of
  whether the world map is open or closed.

  Also includes per-frame debug output when debugArrow profile is enabled.

- Tooltip.lua: Add type guard 'if type(objList) ~= table then break end'
  before iterating learnedNpc[10] and learnedObj[10] in both m_/NPC and o_/object
  paths. Prevents 'attempt to index field questData (a string value)' error
  when the questData field is unexpectedly a string instead of a table.

  The original loop used 'for questId, objList in next, learnedNpc[10]' which
  iterates key-value pairs in insertion order. The _AddToArray helper stores
  values as sequential array elements (tbl[key]=value via table.insert), but
  the iteration was treating it as a questId->objList map. Fixed to use
  ipairs-style iteration with a type check for robustness.
This commit is contained in:
Xurkon
2026-05-09 06:48:59 -05:00
parent 98b4010352
commit 6738027e4d
3 changed files with 262 additions and 105 deletions
+2
View File
@@ -49,6 +49,8 @@ local UiMapIdOverrides = {
[246] = 3713, [246] = 3713,
[1415] = 668, -- Eastern Kingdoms (matches Undercity on Ascension) [1415] = 668, -- Eastern Kingdoms (matches Undercity on Ascension)
[947] = 668, -- Azeroth (matches Undercity on Ascension) [947] = 668, -- Azeroth (matches Undercity on Ascension)
[1241] = 3430, -- Sunstrider Isle (uiMapId 1241 → areaId 3430)
[946] = 3430, -- Sunstrider Isle ghost/loading map → areaId 3430
} }
local parentZoneToSubZone = {} -- Generated local parentZoneToSubZone = {} -- Generated
local zoneMap = {} -- Generated local zoneMap = {} -- Generated
+153 -18
View File
@@ -257,7 +257,7 @@ local function EnsureArrowFrame()
end end
end) end)
arrowFrame:SetScript("OnUpdate", function(self) arrowFrame:SetScript("OnUpdate", function(self)
local now = GetTime() local now = GetTime()
local target = sortedTargets[1] local target = sortedTargets[1]
@@ -273,30 +273,96 @@ local function EnsureArrowFrame()
if (self._lastUpdate or 0) + UPDATE_THROTTLE_SECONDS > now then if (self._lastUpdate or 0) + UPDATE_THROTTLE_SECONDS > now then
return return
end end
self._lastUpdate = now self._lastUpdate = now
local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition() -- Persistent debug: print every frame so we can see what OnUpdate sees
if not playerX or not playerY or not playerInstance then local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
self.distance:SetText("Distance: --") if debugArrow then
return print(string.format("QuestieArrow OnUpdate: frameShown=%s target=%s pX=%s pY=%s pInst=%s _playerUiMapId=%s targetUiMapId=%s",
tostring(self:IsShown()), tostring(target and target.title),
tostring(_arrow_playerX), tostring(_arrow_playerY), tostring(_arrow_playerInstance),
tostring(_arrow_playerUiMapId), tostring(target and target.uiMapId)))
end end
local targetX, targetY, targetInstance = HBD:GetWorldCoordinatesFromZone(target.x / 100.0, target.y / 100.0, local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
target.uiMapId) if not pX or not pY or not pInst then
if not targetX or not targetY or not targetInstance then -- Fallback to HBD if upvalues aren't set yet
self.distance:SetText("Distance: --") pX, pY, pInst = HBD:GetPlayerWorldPosition()
return
end end
if not pX or not pY or not pInst then
if targetInstance ~= playerInstance then local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
if debugArrow then
print(string.format("QuestieArrow OnUpdate: player position nil (x=%s y=%s inst=%s)", tostring(pX), tostring(pY), tostring(pInst)))
end
self.distance:SetText("Distance: --")
self:Hide() self:Hide()
return return
end 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
-- Declare world coords outside the branches so they're in scope for direction calc
local playerWorldX, playerWorldY
-- If target and player are on the same uiMapId, they're in the same instance.
-- If they're on different maps, we need to compare instances via HBD.
if playerUiMapId ~= targetUiMapId then
-- Different maps: just skip instance check since UnitPosition gives us
-- the real instance — if player and target instances differ, HBD will return
-- nil for distance anyway. Fall through to direction calc with player coords.
end
-- Calculate arrow direction using pfQuest's method, but in world coordinates -- Calculate arrow direction using pfQuest's method, but in world coordinates
-- (map coordinates break when the target is in a different zone) -- (map coordinates break when the target is in a different zone)
local xDelta = (playerX - targetX) * 1.5 local targetX, targetY, targetInstance
local yDelta = (playerY - targetY) 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
end
else
-- Different maps: convert target to world coords using its uiMapId (works for 1941).
-- Player is already in world coords from UnitPosition via pX/pY.
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
else
-- HBD failed too: use raw map coords as last resort
targetX, targetY = target.x, target.y
targetInstance = pInst or 0
end
end
-- If targetX is still nil at this point, bail out
if not targetX or not targetY then
self.distance:SetText("Distance: --")
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
else
worldPlayerX, worldPlayerY = pX, pY
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)) local angle = atan2(xDelta, -(yDelta))
angle = angle > 0 and (pi * 2) - angle or -angle angle = angle > 0 and (pi * 2) - angle or -angle
if angle < 0 then angle = angle + (pi * 2) end if angle < 0 then angle = angle + (pi * 2) end
@@ -325,9 +391,14 @@ local function EnsureArrowFrame()
xend = xend - padX xend = xend - padX
yend = yend - padY yend = yend - padY
-- Calculate distance and alpha -- Calculate distance and alpha
local dist = HBD:GetWorldDistance(targetInstance, playerX, playerY, targetX, targetY) -- worldPlayerX/Y are in world coords (converted from cross-map or same-map path)
local dist = HBD:GetWorldDistance(targetInstance, worldPlayerX, worldPlayerY, targetX, targetY)
if dist then 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)))
end
local area = 1 local area = 1
local alpha = dist - area local alpha = dist - area
alpha = alpha > 1 and 1 or alpha alpha = alpha > 1 and 1 or alpha
@@ -574,24 +645,88 @@ function QuestieArrow:UpdateNearestTargets()
return return
end end
sortedTargets = {} sortedTargets = {}
if not Questie.db or not Questie.db.char then if not Questie.db or not Questie.db.char then
return return
end end
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
-- 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() local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition()
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 (ghost window/world map) — it has no world coord data.
-- If GetCurrentUiMapId returns 946, fall back to ZoneDB from the actual zone.
local pUiMapId = QuestiePlayer:GetCurrentUiMapId()
if not pUiMapId or pUiMapId == 947 or pUiMapId == 0 or pUiMapId == 946 then
local 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
if zoneId then
pUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId)
end
end
pUiMapId = pUiMapId or 0
if debugArrow then
print(string.format("UpdateNearestTargets: trying C_Map with pUiMapId=%s", tostring(pUiMapId)))
end
local mapX, mapY = C_Map.GetPlayerMapPosition(pUiMapId, "player")
if debugArrow then
print(string.format("UpdateNearestTargets: C_Map.GetPlayerMapPosition(%s,'player') -> mapX=%.4f mapY=%.4f", tostring(pUiMapId), 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)
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 not playerX or not playerY or not playerInstance then
if debugArrow then
print("UpdateNearestTargets: player position unavailable, returning early")
end
return return
end end
playerInstance = playerInstance or 0
if debugArrow then
print(string.format("UpdateNearestTargets: playerX=%.4f playerY=%.4f playerInstance=%s",
playerX, playerY, tostring(playerInstance)))
end
local tracked = Questie.db.char.TrackedQuests or {} local tracked = Questie.db.char.TrackedQuests or {}
local hasTracked = next(tracked) ~= nil local hasTracked = next(tracked) ~= nil
-- Auto mode logic: If autoTrack is on OR NOTHING is tracked -- Auto mode logic: If autoTrack is on OR NOTHING is tracked
local usingAutoLogic = Questie.db.profile.autoTrackQuests or not hasTracked local usingAutoLogic = Questie.db.profile.autoTrackQuests or not hasTracked
local playerZoneId = QuestiePlayer:GetCurrentZoneId() local playerZoneId = QuestiePlayer:GetCurrentZoneId()
-- Get a valid uiMapId for the player — needed for _CollectObjective zone filtering.
-- Use QuestiePlayer which calls C_Map.GetBestMapForUnit — if that returns 947 (wrong)
-- fall back to ZoneDB from the player's actual zone (areaId).
local playerUiMapId = QuestiePlayer:GetCurrentUiMapId() local playerUiMapId = QuestiePlayer:GetCurrentUiMapId()
if not playerUiMapId or playerUiMapId == 947 then
local zoneId = playerZoneId or select(7, GetInstanceInfo())
if zoneId then
playerUiMapId = ZoneDB:GetUiMapIdByAreaId(zoneId) or playerUiMapId
end
end
playerUiMapId = playerUiMapId or 0
-- Publish context for hoisted helper functions (avoids closure allocation every call) -- Publish context for hoisted helper functions (avoids closure allocation every call)
_arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance _arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance
+107 -87
View File
@@ -232,93 +232,113 @@ function QuestieTooltips:GetTooltip(key)
-- Try to find in learned NPCs or objects -- Try to find in learned NPCs or objects
local id = tonumber(key:sub(3)) local id = tonumber(key:sub(3))
if id then if id then
if key:sub(1,2) == "m_" then if key:sub(1,2) == "m_" then
local learnedNpc = QuestieLearner.data.npcs[id] local learnedNpc = QuestieLearner.data.npcs[id]
if learnedNpc and learnedNpc[10] then -- check questObjectives -- npc[10] from _AddToArray is an array of questIds, not {questId -> objList}
for questId, objList in next, learnedNpc[10] do if learnedNpc and learnedNpc[10] then
local oIndex = 1 for _, questId in ipairs(learnedNpc[10]) do
while objList[oIndex] do local qData = QuestieLearner.data.quests[questId]
local objText = objList[oIndex] if qData and qData[10] then
local needed, collected for slotIdx = 1, #qData[10] do
local objectives = QuestLogCache.GetQuestObjectives(questId) local objSlot = qData[10][slotIdx]
if objectives then if objSlot then
for _, obj in next, objectives do for oIndex = 1, #objSlot do
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then local objEntry = objSlot[oIndex]
needed = obj.numRequired if objEntry and objEntry[2] then
collected = obj.numFulfilled local objText = objEntry[2]
break local needed, collected
end local objectives = QuestLogCache.GetQuestObjectives(questId)
end if objectives then
end for _, obj in next, objectives do
QuestieTooltips:RegisterObjectiveTooltip(questId, key, { if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
Index = 0, needed = obj.numRequired
Description = objText, collected = obj.numFulfilled
Needed = needed, break
Collected = collected, end
Update = function(self) end
local objs = QuestLogCache.GetQuestObjectives(questId) end
if objs then QuestieTooltips:RegisterObjectiveTooltip(questId, key, {
for _, o in next, objs do Index = 0,
if o.text and self.Description and (o.text == self.Description or string.find(o.text, self.Description, 1, true) or string.find(self.Description, o.text, 1, true)) then Description = objText,
self.Needed = o.numRequired Needed = needed,
self.Collected = o.numFulfilled Collected = collected,
break Update = function(self)
end local objs = QuestLogCache.GetQuestObjectives(questId)
end if objs then
end for _, o in next, objs do
end if o.text and self.Description and (o.text == self.Description or string.find(o.text, self.Description, 1, true) or string.find(self.Description, o.text, 1, true)) then
}) self.Needed = o.numRequired
oIndex = oIndex + 1 self.Collected = o.numFulfilled
end break
end end
if learnedNpc.mc then end
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedNpc.mc) .. ")|r") end
end end
end })
elseif key:sub(1,2) == "o_" then end
local learnedObj = QuestieLearner.data.objects[id] end
if learnedObj and learnedObj[10] then end
for questId, objList in next, learnedObj[10] do end
local oIndex = 1 end
while objList[oIndex] do end
local objText = objList[oIndex] if learnedNpc.mc then
local needed, collected tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedNpc.mc) .. ")|r")
local objectives = QuestLogCache.GetQuestObjectives(questId) end
if objectives then end
for _, obj in next, objectives do elseif key:sub(1,2) == "o_" then
if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then local learnedObj = QuestieLearner.data.objects[id]
needed = obj.numRequired -- obj[2] from _AddToArray is an array of questIds (questStarts), not {questId -> objList}
collected = obj.numFulfilled if learnedObj and learnedObj[2] then
break for _, questId in ipairs(learnedObj[2]) do
end local qData = QuestieLearner.data.quests[questId]
end if qData and qData[10] then
end for slotIdx = 1, #qData[10] do
QuestieTooltips:RegisterObjectiveTooltip(questId, key, { local objSlot = qData[10][slotIdx]
Index = 0, if objSlot then
Description = objText, for oIndex = 1, #objSlot do
Needed = needed, local objEntry = objSlot[oIndex]
Collected = collected, if objEntry and objEntry[2] then
Update = function(self) local objText = objEntry[2]
local objs = QuestLogCache.GetQuestObjectives(questId) local needed, collected
if objs then local objectives = QuestLogCache.GetQuestObjectives(questId)
for _, o in next, objs do if objectives then
if o.text and self.Description and (o.text == self.Description or string.find(o.text, self.Description, 1, true) or string.find(self.Description, o.text, 1, true)) then for _, obj in next, objectives do
self.Needed = o.numRequired if obj.text and objText and (obj.text == objText or string.find(obj.text, objText, 1, true) or string.find(objText, obj.text, 1, true)) then
self.Collected = o.numFulfilled needed = obj.numRequired
break collected = obj.numFulfilled
end break
end end
end end
end end
}) QuestieTooltips:RegisterObjectiveTooltip(questId, key, {
oIndex = oIndex + 1 Index = 0,
end Description = objText,
end Needed = needed,
if learnedObj.mc then Collected = collected,
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedObj.mc) .. ")|r") Update = function(self)
end local objs = QuestLogCache.GetQuestObjectives(questId)
end if objs then
end for _, o in next, objs do
if o.text and self.Description and (o.text == self.Description or string.find(o.text, self.Description, 1, true) or string.find(self.Description, o.text, 1, true)) then
self.Needed = o.numRequired
self.Collected = o.numFulfilled
break
end
end
end
end
})
end
end
end
end
end
end
if learnedObj.mc then
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedObj.mc) .. ")|r")
end
end
end
end end
end end
end end