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:
+256
-72
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user