fix(arrow): Sunstrider zoneId 3431, rotation CW, collection distance, native map pins

- zoneId detection: accept 3430 OR 3431 OR uiMapId 1241 (4 locations)
- SetRotation is CW-positive: rotAngle=relative (was -relative)
- Collection functions: override target->1241 when player on Sunstrider
- NPC 15281: spawn zone 1241 (not 3430) for correct coord space
- _ResolveMapUiMapId: removed 1241->1941 redirect
- zoneDB: added areaIdToUiMapId[1241]=1241
- Updated CHANGELOG, README, docs
This commit is contained in:
Xurkon
2026-05-21 19:22:40 -05:00
parent 7f99e7228d
commit 24ed0f48b3
19 changed files with 1170 additions and 469 deletions
+286 -282
View File
@@ -26,12 +26,12 @@ local abs = math.abs
local max = math.max
local min = math.min
local ARROW_SHEET_SIZE = 512
local ARROW_CELL_W = 56
local ARROW_CELL_H = 42
local ARROW_SHEET_COLS = 9
local ARROW_SHEET_ROWS = 12
local ARROW_TOTAL_CELLS = ARROW_SHEET_COLS * ARROW_SHEET_ROWS
-- Single-frame arrow with SetRotation for perfectly smooth rotation.
-- Texture is a 256x256 SQUARE TGA with arrow 2x horizontally stretched to fill
-- ~70% of canvas. SQUARE is critical: SetRotation rotates UVs inside the display
-- rect, so non-square textures distort at every diagonal angle. No SetTexCoord.
local ARROW_DISPLAY_WIDTH = 160
local ARROW_DISPLAY_HEIGHT = 160
local UPDATE_THROTTLE_SECONDS = 0.05
local RECALC_NEAREST_SECONDS = 1.0
@@ -49,10 +49,10 @@ 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
local lastPopulateByQuestId = {}
local function _IsArrowEnabled()
@@ -134,36 +134,24 @@ local function ResolveIconTexture(icon)
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
-- Ghost map 946 has no real coordinate data; redirect to Eversong (1941).
-- Ascension custom map 1241 (Sunstrider Isle) must also redirect to 1941
-- so target world coords match player world coords (both in Eversong space).
if uiMapId == 946 or uiMapId == 1241 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
-- NEVER use the calibrated branch here. It returns Eversong-wide normalized coords
-- (~0.60, 0.44) which are in a completely different coordinate space from
-- Sunstrider-local target coords (~0.38, 0.21). Mixing them causes bogus distances.
-- Always obtain Sunstrider-local coords via the 1241 map lookup, then convert
-- through HBD using Eversong bounds (1941) to get real comparable world coords.
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
@@ -193,6 +181,8 @@ local function _GetSunstriderPlayerMapPosition(debugArrow)
return mapX2, mapY2
end
local function _ApplyOutline(fontString)
if not fontString or not fontString.GetFont or not fontString.SetFont then
return
@@ -235,9 +225,9 @@ local function EnsureArrowFrame()
-- Store whether we should use saved position or default
arrowFrame._useDefaultPosition = not (pos and pos.point)
-- Make room for the objective icon below the arrow (no overlap)
arrowFrame:SetWidth(56)
arrowFrame:SetHeight(64)
-- Make room for arrow (square) plus icon and text below
arrowFrame:SetWidth(ARROW_DISPLAY_WIDTH)
arrowFrame:SetHeight(ARROW_DISPLAY_HEIGHT + 60)
arrowFrame:SetScale(_GetArrowScale())
arrowFrame:SetClampedToScreen(true)
arrowFrame:SetMovable(true)
@@ -278,20 +268,19 @@ local function EnsureArrowFrame()
self:SetScale(scale)
end)
-- Arrow sprite sheet texture (108 cells: 9 columns, 12 rows)
-- Single arrow texture with SetRotation for smooth rotation
arrowFrame.arrow = arrowFrame:CreateTexture(nil, "MEDIUM")
arrowFrame.arrow:SetTexture(QuestieLib.AddonPath .. "Icons\\arrow.tga")
-- Render at native cell size; use frame scaling if you want it larger.
arrowFrame.arrow:SetWidth(ARROW_CELL_W)
arrowFrame.arrow:SetHeight(ARROW_CELL_H)
arrowFrame.arrow:SetPoint("TOP", arrowFrame, "TOP", 0, 0)
arrowFrame.arrow:SetTexCoord(0, 0.109375, 0, 0.08203125) -- First cell
arrowFrame.arrow:SetWidth(ARROW_DISPLAY_WIDTH)
arrowFrame.arrow:SetHeight(ARROW_DISPLAY_HEIGHT)
arrowFrame.arrow:SetPoint("CENTER", arrowFrame, "CENTER", 0, 0)
arrowFrame.arrow:SetRotation(0) -- 0 = pointing up (north)
-- Quest icon texture at bottom (pfQuest style)
arrowFrame.icon = arrowFrame:CreateTexture(nil, "OVERLAY")
arrowFrame.icon:SetWidth(28)
arrowFrame.icon:SetHeight(28)
arrowFrame.icon:SetPoint("BOTTOM", arrowFrame.arrow, "BOTTOM", 0, -20)
arrowFrame.icon:SetPoint("BOTTOM", arrowFrame.arrow, "BOTTOM", 0, 0)
arrowFrame.title = arrowFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
arrowFrame.title:SetPoint("TOP", arrowFrame.icon, "BOTTOM", 0, -2)
@@ -334,213 +323,131 @@ arrowFrame:SetScript("OnUpdate", function(self)
if (self._lastUpdate or 0) + UPDATE_THROTTLE_SECONDS > now then
return
end
self._lastUpdate = now
self._lastUpdate = now
-- Persistent debug: print every frame so we can see what OnUpdate sees
local debugArrow = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
if debugArrow then
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)))
local target = sortedTargets[1]
-- -----------------------------------------------------------------
-- Get fresh player world position every frame for smooth arrow
-- rotation. UnitPosition updates every frame via HBD, which is
-- essential — cached values (updated only every 1s) make the
-- arrow rotate with the character since only GetPlayerFacing()
-- changes per frame when position is stale.
-- On Sunstrider, HBD may return Eastern Kingdoms coords. We
-- detect this and override with the cached corrected position.
-- -----------------------------------------------------------------
local playerX, playerY, playerInstance = HBD:GetPlayerWorldPosition()
if not playerX or not playerY or not playerInstance then
self.distance:SetText("Distance: --")
return
end
-- On Sunstrider, HBD returns Eastern Kingdoms continent coords
-- which are outside Eversong bounds. Instead of replacing fresh
-- per-frame coords with stale 1-second cache, compute fresh
-- Sunstrider-local coords via C_Map on EVERY frame so that
-- both position AND facing update per-frame.
-- PITFALL: zoneId can be 3431 (Eversong) while player is on uiMap 1241 (Sunstrider).
-- Check both zoneId and cached uiMapId for robust detection.
local _curZoneId = QuestiePlayer:GetCurrentZoneId()
local _sunOnUpdate = (_curZoneId == 3430 or _curZoneId == 3431
or _arrow_playerUiMapId == 1241)
if _sunOnUpdate then
local EXMIN, EXMAX, EYMIN, EYMAX = -2000, 3200, 5300, 8700
if playerX < EXMIN or playerX > EXMAX or playerY < EYMIN or playerY > EYMAX then
-- Fresh per-frame computation instead of stale cache
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
if mapX and mapY and mapX > 0 and mapY > 0 then
local wX, wY, wInst = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241) -- use Ascension-calibrated 1241 bounds
if wX and wY then
if debugArrow then
print(string.format("OnUpdate: Sunstrider fresh HBD(%.0f,%.0f) -> computed(%.0f,%.0f)",
playerX, playerY, wX, wY))
end
playerX, playerY, playerInstance = wX, wY, wInst or playerInstance
elseif _arrow_playerX and _arrow_playerY then
-- Last resort: stale cache only if fresh calc fails
if debugArrow then
print(string.format("OnUpdate: Sunstrider fresh calc FAILED, using stale cache(%.0f,%.0f)",
_arrow_playerX, _arrow_playerY))
end
playerX, playerY, playerInstance = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
end
elseif _arrow_playerX and _arrow_playerY then
-- _GetSunstriderPlayerMapPosition failed, stale cache fallback
if debugArrow then
print(string.format("OnUpdate: Sunstrider map pos failed, using stale cache(%.0f,%.0f)",
_arrow_playerX, _arrow_playerY))
end
playerX, playerY, playerInstance = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
end
end
end
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
if not pX or not pY or not pInst then
-- Fallback to HBD if upvalues aren't set yet
pX, pY, pInst = HBD:GetPlayerWorldPosition()
-- Convert target spawn coords to world coordinates.
-- Resolve uiMapId again here as a safety net for targets that may
-- have raw custom map IDs (1241) bypassing PopulateTargets resolution.
local targetUiMapId = _ResolveArrowUiMapId(target.uiMapId)
-- On Sunstrider, convert target through Ascension-calibrated 1241 bounds
-- so target world coords match player world coords (both through 1241).
-- PITFALL: zoneId can be 3431 while player is on uiMap 1241.
if _sunOnUpdate and targetUiMapId == 1941 then
targetUiMapId = 1241
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
print(string.format("QuestieArrow OnUpdate: player position nil (x=%s y=%s inst=%s)", tostring(pX), tostring(pY), tostring(pInst)))
end
local 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
if targetInstance ~= playerInstance then
self:Hide()
return
end
-- 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
-- Arrow direction from pure world-coordinate math.
-- HBD world coords: X decreases going EAST (more negative = more west).
-- Y increases going NORTH (larger = more north).
-- GetPlayerFacing: 0=North, π/2=East, π=South, 3π/2=West (CW from N).
-- SetRotation(r): rotates texture CW (positive = clockwise).
-- Arrow image tip is at TOP of file → points UP at SetRotation(0).
--
-- dx = targetX - playerX:
-- targetX > playerX (numerically) → target LESS negative → target EAST
-- So dx>0 = target EAST of player
-- dy = targetY - playerY:
-- targetY > playerY → target MORE north
-- So dy>0 = target NORTH of player
--
-- Bearing CW from North: atan2(dx, dy)
-- N: dx=0, dy>0 → atan2(0,+) = 0
-- E: dx>0, dy=0 → atan2(+,0) = π/2
-- S: dx=0, dy<0 → atan2(0,-) = π
-- W: dx<0, dy=0 → atan2(-,0) = -π/2 → 3π/2
--
-- Screen direction relative to facing:
-- relative = bearing - facing (0 = target ahead)
--
-- SetRotation wants CW; relative is CW: use it directly.
-- SetRotation(relative) → arrow points at target on screen.
local dx = targetX - playerX
local dy = targetY - playerY
local bearing = atan2(dx, dy)
if bearing < 0 then bearing = bearing + (pi * 2) end
local facing = GetPlayerFacing and GetPlayerFacing() or 0
local relative = bearing - facing
if relative < 0 then relative = relative + (pi * 2) end
local rotAngle = relative -- CW rotation for SetRotation
-- 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 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 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).
-- 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
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: 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
print(string.format("QuestieArrow OnUpdate: target=%s pX=%.1f pY=%.1f tX=%.1f tY=%.1f dx=%.1f dy=%.1f bearing=%.2f facing=%.2f relative=%.2f rotAng=%.2f inst=%s",
tostring(target.title), playerX, playerY, targetX, targetY, dx, dy, bearing, facing, relative, rotAngle, tostring(playerInstance)))
end
-- If targetX is still nil at this point, bail out
if not targetX or not targetY then
self.distance:SetText("Distance: --")
return
end
-- 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
-- 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
-- Calculate color gradient based on direction
local perc = abs(((pi - abs(angle)) / pi))
local r, g, b = GetColorGradient(perc)
-- Select sprite sheet cell
local cell = modulo(floor(angle / (pi * 2) * ARROW_TOTAL_CELLS + 0.5), ARROW_TOTAL_CELLS)
local column = modulo(cell, ARROW_SHEET_COLS)
local row = floor(cell / ARROW_SHEET_COLS)
local xstart = (column * ARROW_CELL_W) / ARROW_SHEET_SIZE
local ystart = (row * ARROW_CELL_H) / ARROW_SHEET_SIZE
local xend = ((column + 1) * ARROW_CELL_W) / ARROW_SHEET_SIZE
local yend = ((row + 1) * ARROW_CELL_H) / ARROW_SHEET_SIZE
-- Avoid bleeding from neighboring cells when texture filtering is enabled.
local padX = 0.5 / ARROW_SHEET_SIZE
local padY = 0.5 / ARROW_SHEET_SIZE
xstart = xstart + padX
ystart = ystart + padY
xend = xend - padX
yend = yend - padY
-- Calculate distance and alpha
-- 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
-- Calculate distance and alpha
local dist = HBD:GetWorldDistance(targetInstance, playerX, playerY, targetX, targetY)
if dist then
if debugArrow then
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
alpha = alpha > 1 and 1 or alpha
@@ -550,10 +457,8 @@ local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
texalpha = texalpha > 1 and 1 or texalpha
texalpha = texalpha < 0 and 0 or texalpha
r, g, b = r + texalpha, g + texalpha, b + texalpha
self.arrow:SetTexCoord(xstart, xend, ystart, yend)
self.arrow:SetVertexColor(r, g, b)
self.arrow:SetRotation(rotAngle)
self.arrow:SetVertexColor(1, 1, 1)
self.arrow:SetAlpha(alpha)
local distText = string.format("%.1f", dist)
@@ -638,6 +543,8 @@ local function _CollectFinisherSpawns(finisher, quest)
if not finisher then return end
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
-- On Sunstrider, force target conversion through 1241 bounds to match player coords
local sunOverride = (pMap == 1241)
local iconPath = ResolveIconTexture(_GetCompleteIconType(quest))
if finisher.spawns then
for finisherZone, spawns in pairs(finisher.spawns) do
@@ -656,6 +563,7 @@ local function _CollectFinisherSpawns(finisher, quest)
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
if uiMapId and x and y then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
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)
@@ -672,12 +580,13 @@ local function _CollectFinisherSpawns(finisher, quest)
end
else
-- Zone filtering disabled (same zone ID vs area ID mismatch issue)
if true then
local x = coords[1]
local y = coords[2]
local uiMapId = ZoneDB:GetUiMapIdByAreaId(finisherZone)
if uiMapId then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if true then
local x = coords[1]
local y = coords[2]
local uiMapId = ZoneDB:GetUiMapIdByAreaId(finisherZone)
if uiMapId then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
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)
@@ -706,6 +615,7 @@ local function _CollectFinisherSpawns(finisher, quest)
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
if uiMapId and x and y then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
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)
@@ -724,9 +634,38 @@ local function _CollectFinisherSpawns(finisher, quest)
end
local function _CollectObjective(objective, quest)
if not objective or not objective.spawnList then return end
if not objective or not objective.spawnList then
-- spawnList can be nil if _TryInvalidateObjective cleared it (QuestieLearner
-- learned new data) but the rebuild via UpdateQuest hasn't happened yet, or if
-- stale quest.isComplete prevented PopulateObjective from rebuilding it.
-- Proactively trigger UpdateQuest to rebuild the spawnList for this quest.
if quest and quest.Id and QuestieQuest and QuestieQuest.UpdateQuest then
local dbComplete = QuestieDB.IsComplete(quest.Id)
-- Only request rebuild if quest is NOT complete in the DB
if dbComplete ~= 1 and not quest.isComplete then
local now = GetTime()
local last = lastPopulateByQuestId[quest.Id] or 0
-- Throttle rebuilds to every 5 seconds per quest
if (last + 5.0) < now then
lastPopulateByQuestId[quest.Id] = now
Questie:Debug(Questie.DEBUG_DEVELOP, "[Arrow] _CollectObjective: spawnList nil for quest", quest.Id, "- triggering UpdateQuest rebuild")
QuestieQuest:UpdateQuest(quest.Id)
end
end
end
if debugCollect then
print(string.format(" _CollectObjective SKIP: obj=%s spawnList=%s (quest=%s)",
tostring(objective), objective and tostring(objective.spawnList) or "nil", quest and tostring(quest.name) or "?"))
end
return
end
if QuestieQuest.ShouldHideObjective(objective) then return end
if objective.Completed == true or objective.Completed == 1 then return end
if objective.Completed == true or objective.Completed == 1 then
if debugCollect then
print(string.format(" _CollectObjective SKIP: Completed=%s (quest=%s)", tostring(objective.Completed), quest and tostring(quest.name) or "?"))
end
return
end
if objective.Needed and objective.Collected
and type(objective.Needed) == "number" and type(objective.Collected) == "number"
and objective.Collected >= objective.Needed then
@@ -734,6 +673,8 @@ local function _CollectObjective(objective, quest)
end
local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance
local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId
-- On Sunstrider, force target conversion through 1241 bounds to match player coords
local sunOverride = (pMap == 1241)
local debugCollect = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
if debugCollect then
print(string.format(" _CollectObjective: spawnList=%s", objective.spawnList and "yes" or "nil"))
@@ -760,16 +701,12 @@ local function _CollectObjective(objective, quest)
end
if uiMapId then
local resolvedUiMapId = _ResolveArrowUiMapId(uiMapId)
local tX, tY, tInst, calibratedTargetGroup = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, resolvedUiMapId, zone)
if sunOverride and resolvedUiMapId == 1941 then resolvedUiMapId = 1241 end
local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, resolvedUiMapId)
if tX and tY and tInst then
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
local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY)
if dist then
if (not calibratedTargetGroup) and tInst ~= pInst then dist = 500000 + dist * 100 end
if tInst ~= pInst then dist = 500000 + dist * 100 end
if debugCollect then
print(string.format(" ADDED dist=%.0f", dist))
end
@@ -813,22 +750,14 @@ sortedTargets = {}
-- 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)
-- NOTE: GetCurrentUiMapId() returns 1241 on Sunstrider Isle (not 946 or 1941).
-- PITFALL: zoneId can be 3431 (Eversong) while player is on uiMap 1241 (Sunstrider).
local useSunstriderFix = (zoneId == 3430 or zoneId == 3431 or pUiMapId == 1241)
-- 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 +
-- Get player position — always try HBD's direct method first (works when map is OPEN).
-- If that returns nil (map closed or Sunstrider), fall back to C_Map.GetPlayerMapPosition +
-- HBD:GetWorldCoordinatesFromZone which works regardless of map open/closed state.
-- 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
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.
-- IMPORTANT: never use 946/947 (world/cosmic maps) — they have no world coord data.
@@ -858,7 +787,7 @@ sortedTargets = {}
-- 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
if zoneId == 3430 or zoneId == 3431 or pUiMapId == 1241 then
lookupUiMapId = 1241 -- always use Sunstrider's real uiMapId for C_Map
end
if debugArrow then
@@ -873,11 +802,18 @@ sortedTargets = {}
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
if calibratedGroup then
playerX, playerY, playerInstance = QuestieCompat.GetCalibratedWorldCoordinatesFromZone(mapX, mapY, lookupUiMapId, zoneId)
if useSunstriderFix then
-- Sunstrider: use 1941 (Eversong) for coordinate conversion so that
-- player world coords are in the same space as target world coords.
-- Targets always use 1941 (via ZoneDB:GetUiMapIdByAreaId(3430)→1941),
-- so the player must also use 1941 for consistent distance/direction.
-- Ascension-calibrated 1241 bounds now match Eversong world space.
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241)
else
local worldUiMapId = 1941 -- always use Eversong bounds for world coord conversion
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, worldUiMapId)
-- Normal zone: convert the player's map coords through the ACTUAL zone's
-- uiMapId bounds. This was hardcoded to 1941 in the original Sunstrider fix,
-- which broke every zone except Eversong.
playerX, playerY, playerInstance = HBD:GetWorldCoordinatesFromZone(mapX, mapY, lookupUiMapId)
end
if debugArrow then
print(string.format("UpdateNearestTargets: HBD via lookupUiMapId=%s mapX=%.4f mapY=%.4f -> worldX=%.4f worldY=%.4f",
@@ -886,15 +822,60 @@ sortedTargets = {}
playerInstance = playerInstance or 0
end
end
if debugArrow then
print(string.format("UpdateNearestTargets: HBD.GetPlayerWorldPosition() = x=%.4f y=%.4f inst=%s", playerX or 0, playerY or 0, tostring(playerInstance)))
-- Sunstrider override: HBD:GetPlayerWorldPosition() may return incorrect
-- (Eastern Kingdoms offset) coords on Sunstrider Isle. Only override if
-- the HBD coords appear wrong — specifically, if they fall outside the
-- Eversong bounding box. Eversong world bounds: X ∈ [-1825, 3100],
-- Y ∈ [5358, 8642]. If HBD coords are outside this range, they're EK coords.
-- NOTE: _GetSunstriderPlayerMapPosition returns 1241-local normalized coords.
-- We convert through 1941 (Eversong) so that player world coords share the
-- same coordinate space as targets (which always use uiMapId 1941 via ZoneDB).
if useSunstriderFix and playerX and playerY then
-- Eversong bounding box in world coordinates (with some margin).
-- A player on Sunstrider/Eversong should be within these bounds.
local EVERSENG_XMIN, EVERSENG_XMAX = -2000, 3200
local EVERSENG_YMIN, EVERSENG_YMAX = 5300, 8700
local coordsOutsideEversong = (playerX < EVERSENG_XMIN or playerX > EVERSENG_XMAX
or playerY < EVERSENG_YMIN or playerY > EVERSENG_YMAX)
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider check outsideEv=%s px=%.0f py=%.0f bounds=[%d..%d,%d..%d]",
tostring(coordsOutsideEversong), playerX, playerY,
EVERSENG_XMIN, EVERSENG_XMAX, EVERSENG_YMIN, EVERSENG_YMAX))
end
if not playerX or not playerY or not playerInstance then
if coordsOutsideEversong then
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
if debugArrow then
print("UpdateNearestTargets: player position unavailable, returning early")
print(string.format("UpdateNearestTargets: Sunstrider mapPos mapX=%.4f mapY=%.4f", mapX or -1, mapY or -1))
end
if mapX and mapY and mapX > 0 and mapY > 0 then
-- Use Ascension-calibrated 1241 bounds for consistent world space.
local wX, wY, wInst = HBD:GetWorldCoordinatesFromZone(mapX, mapY, 1241)
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider HBD1941 wX=%s wY=%s wInst=%s", tostring(wX), tostring(wY), tostring(wInst)))
end
if wX and wY then
if debugArrow then
print(string.format("UpdateNearestTargets: Sunstrider override HBD(%.4f,%.4f) -> HBD1941(%.4f,%.4f)", playerX, playerY, wX, wY))
end
playerX, playerY, playerInstance = wX, wY, wInst or 0
else
-- HBD 1941 conversion failed — no fallback available.
if debugArrow then
print("UpdateNearestTargets: Sunstrider HBD1941 conversion FAILED")
end
end
end
return
end
end
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
playerInstance = playerInstance or 0
@@ -923,17 +904,22 @@ 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
local function _CollectQuestTargets(quest)
if not quest then return end
local debugCollect = Questie and Questie.db and Questie.db.profile and Questie.db.profile.debugArrow
-- Avoid spamming QuestieQuest:PopulateQuestLogInfo (it can trigger marker rebuilds and flicker).
-- Only populate when objective completion flags are missing, and throttle per quest id.
-- Periodic quest state verification: call PopulateQuestLogInfo when
-- (a) objectives/completion flags are missing (original logic), OR
-- (b) the quest hasn't been populated in the last 30 seconds
-- (c) quest.isComplete is stale (does not match DB)
-- This ensures the arrow always has accurate completion data, catching
-- cases where events didn't fire or stale flags survived a reload.
if QuestieQuest and QuestieQuest.PopulateQuestLogInfo and quest.Id then
local needsPopulate = false
if not quest.Objectives and not quest.SpecialObjectives then
@@ -941,9 +927,17 @@ sortedTargets = {}
elseif _HasMissingCompletedFlag(quest.Objectives) or _HasMissingCompletedFlag(quest.SpecialObjectives) then
needsPopulate = true
end
-- Periodic force-refresh: if more than 30 seconds since last populate, re-sync
-- quest state from live quest log. This catches stale isComplete/WasComplete
-- that survived event-driven updates (e.g., Ascension missing QUEST_LOG_UPDATE).
local now = GetTime()
local last = lastPopulateByQuestId[quest.Id] or 0
if (last + 30.0) < now then
needsPopulate = true
end
if needsPopulate then
local now = GetTime()
local last = lastPopulateByQuestId[quest.Id] or 0
if (last + 2.0) < now then
lastPopulateByQuestId[quest.Id] = now
QuestieQuest:PopulateQuestLogInfo(quest)
@@ -951,12 +945,22 @@ sortedTargets = {}
end
end
local isComplete = quest.isComplete or (QuestieDB.IsComplete(quest.Id) == 1)
if isComplete then quest.isComplete = true end
local dbComplete = QuestieDB.IsComplete(quest.Id)
-- Defensive: if quest.isComplete is stale from a prior complete-then-abandon, and
-- QuestLogCache says the quest is NOT complete (0 or nil), clear the stale flag so
-- objective pins are drawn. AcceptQuest normally resets this, but this guards
-- against edge cases where AcceptQuest's reset didn't fire.
if quest.isComplete and dbComplete ~= 1 then
quest.isComplete = nil
end
local isComplete = quest.isComplete or (dbComplete == 1)
if debugCollect then
print(string.format(" _CollectQuestTargets: %s isComplete=%s hasObjectives=%s hasSpecialObjectives=%s hasFinisher=%s",
local objCount = quest.Objectives and #quest.Objectives or 0
print(string.format(" _CollectQuestTargets: %s isComplete=%s (quest.isComplete=%s dbComplete=%s) objCount=%d hasObjectives=%s hasSpecialObjectives=%s hasFinisher=%s",
tostring(quest.name), tostring(isComplete),
tostring(quest.isComplete), tostring(dbComplete),
objCount,
tostring(quest.Objectives ~= nil),
tostring(quest.SpecialObjectives ~= nil),
tostring(quest.Finisher ~= nil)))
+5 -11
View File
@@ -58,18 +58,12 @@ 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
-- Ghost map 946 has no real coordinate data; redirect to Eversong (1941).
if uiMapId == 946 then
return 1941
end
-- Map 1241 (Sunstrider Isle) now has its own Ascension-calibrated bounds
-- and pins on 1241 render correctly on the Sunstrider sub-map. No redirect.
return uiMapId
end
@@ -678,7 +672,7 @@ end
local closestStarter = {}
function QuestieMap:FindClosestStarter()
local playerX, playerY, _ = HBD:GetPlayerWorldPosition();
local playerZone = HBD:GetPlayerWorldPosition();
local playerZone = QuestiePlayer:GetCurrentUiMapId();
for questId in pairs(QuestiePlayer.currentQuestlog) do
if (not closestStarter[questId]) then
local quest = QuestieDB.GetQuest(questId);
+48 -15
View File
@@ -58,6 +58,14 @@ local deletedQuestItem = false
local _bagUpdateDebounceTimer = nil
local _bagUpdateFollowUpTimer = nil
-- Periodic quest state verification timer.
-- Ascension server events (QUEST_LOG_UPDATE, UNIT_QUEST_LOG_CHANGED) can be
-- unreliable or delayed, causing stale quest.isComplete / quest.WasComplete flags
-- to persist across reload/reaccept cycles. This timer forces a full quest log
-- reconciliation every 30 seconds so pins and arrows stay accurate.
local _periodicRefreshTimer = nil
local PERIODIC_REFRESH_SECONDS = 30
--- Registers all events that are required for questing (accepting, removing, objective updates, ...)
function QuestEventHandler:RegisterEvents()
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] RegisterEvents")
@@ -77,6 +85,18 @@ function QuestEventHandler:RegisterEvents()
eventFrame:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
eventFrame:SetScript("OnEvent", _QuestEventHandler.OnEvent)
-- Start periodic quest state verification timer.
-- On Ascension, QUEST_LOG_UPDATE events can be unreliable. This timer forces
-- a full reconciliation every 30 seconds, catching stale isComplete/WasComplete
-- flags and ensuring objective spawnLists stay populated for active quests.
if not _periodicRefreshTimer then
_periodicRefreshTimer = C_Timer.NewTicker(PERIODIC_REFRESH_SECONDS, function()
Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] Periodic refresh: forcing full quest log scan")
doFullQuestLogScan = true
_QuestEventHandler:QuestLogUpdate()
end)
end
-- StaticPopup dialog hooks. Deleteing Quest items do not always trigger a Quest Log Update.
hooksecurefunc("StaticPopup_Show", function(...)
-- Hook StaticPopup_Show. If we find the "DELETE_ITEM" dialog, check for Quest Items and notify the player.
@@ -230,6 +250,20 @@ function _QuestEventHandler:QuestAccepted(questLogIndex, questId)
end)
end
-- If the quest was already in questLog as QUEST_TURNED_IN (e.g. Ebonhold Call Board repeatable
-- quests that vanish without a proper QUEST_REMOVED event), clean up before re-accepting so the
-- tracker doesn't show it as already complete.
-- NOTE: Must check BEFORE wiping questLog[questId] below, otherwise the state check is always false.
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
if wasTurnedIn then
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "re-accepted after auto-complete, clearing stale state")
QuestLogCache.RemoveQuest(questId)
QuestieQuest:CompleteQuest(questId) -- clears per-quest data
QuestieJourney:CompleteQuest(questId)
QuestieAnnounce:CompletedQuest(questId)
QuestieTracker:RemoveQuest(questId)
end
questLog[questId] = {}
-- Timed quests do not need a full Quest Log Update.
@@ -241,19 +275,6 @@ function _QuestEventHandler:QuestAccepted(questLogIndex, questId)
skipNextUQLCEvent = true
end
-- If the quest was already in questLog as QUEST_TURNED_IN (e.g. Ebonhold Call Board repeatable
-- quests that vanish without a proper QUEST_REMOVED event), clean up before re-accepting so the
-- tracker doesn't show it as already complete.
if questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN then
Questie:Debug(Questie.DEBUG_INFO, "Quest:", questId, "re-accepted after auto-complete, clearing stale state")
QuestLogCache.RemoveQuest(questId)
QuestieQuest:CompleteQuest(questId) -- clears per-quest data
QuestieJourney:CompleteQuest(questId)
QuestieAnnounce:CompletedQuest(questId)
QuestieTracker:RemoveQuest(questId)
questLog[questId] = nil
end
QuestieCombatQueue:Queue(function()
QuestieLib:CacheItemNames(questId)
_QuestEventHandler:HandleQuestAccepted(questId)
@@ -266,8 +287,10 @@ end
---@param questId number
---@return boolean true @if the function was successful, false otherwise
function _QuestEventHandler:HandleQuestAccepted(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - questId:", questId)
local idx = QuestieCompat.GetQuestLogIndexByID(questId)
if not idx then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - NO quest log index yet, retrying for questId:", questId)
_QuestLogUpdateQueue:Insert(function()
return _QuestEventHandler:HandleQuestAccepted(questId)
end)
@@ -277,7 +300,7 @@ function _QuestEventHandler:HandleQuestAccepted(questId)
local cacheMiss, changes = QuestLogCache.CheckForChanges({ [questId] = true })
if cacheMiss then
-- if cacheMiss, no need to check changes as only 1 questId
Questie:Debug(Questie.DEBUG_INFO, "Objectives are not cached yet")
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - CACHE MISS for questId:", questId, "- retrying later")
_QuestLogUpdateQueue:Insert(function()
return _QuestEventHandler:HandleQuestAccepted(questId)
end)
@@ -285,7 +308,7 @@ function _QuestEventHandler:HandleQuestAccepted(questId)
return false
end
Questie:Debug(Questie.DEBUG_INFO, "Objectives are correct. Calling accept logic. quest:", questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestEventHandler] HandleQuestAccepted - cache ready, calling AcceptQuest for questId:", questId)
questLog[questId].state = QUEST_LOG_STATES.QUEST_ACCEPTED
QuestieQuest:SetObjectivesDirty(questId)
@@ -447,6 +470,16 @@ function _QuestEventHandler:MarkQuestAsAbandoned(questId)
"objectives were complete - treating as completed (auto-complete quest)")
questEntry.state = QUEST_LOG_STATES.QUEST_TURNED_IN
-- Clear stale objective data so re-accepting this quest later doesn't
-- inherit Cached "Completed = true" / "isUpdated = true" flags that
-- would cause PopulateObjectiveNotes to skip drawing map pins.
if quest then
quest.Objectives = {}
quest.WasComplete = nil
quest.isComplete = nil
end
QuestieQuest:SetObjectivesDirty(questId)
QuestLogCache.RemoveQuest(questId)
QuestieQuest:CompleteQuest(questId)
QuestieJourney:CompleteQuest(questId)
+221 -19
View File
@@ -472,28 +472,63 @@ function QuestieQuest:AcceptQuest(questId)
local quest = QuestieDB.GetQuest(questId)
if quest then
local complete = QuestieDB.IsComplete(questId)
-- If any of these flags exsist then this quest has already once been accepted and is probobly in a failed state
if (quest.WasComplete or quest.isComplete or complete == 0 or complete == -1) and (QuestiePlayer.currentQuestlog[questId]) then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId,
" Warning: This quest was once accepted and needs to be reset.")
-- If any of these flags exist, this quest was previously accepted and may
-- have stale completion state (e.g. complete-then-abandon-then-reaccept leaves
-- quest.isComplete=true, WasComplete=true). Only check quest-object flags
-- (WasComplete, isComplete), NOT QuestieDB.IsComplete, because IsComplete
-- returns 0 for any incomplete quest — including brand new acceptances.
-- The original code also checked complete==0 and complete==-1, but those
-- combined with removing the currentQuestlog guard caused every quest
-- acceptance to enter this block and unload freshly-drawn pins.
-- DEBUG: Log stale flag state on accept
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest state check - questId:", questId,
" WasComplete:", tostring(quest.WasComplete),
" isComplete:", tostring(quest.isComplete),
" currentQuestlog:", tostring(QuestiePlayer.currentQuestlog[questId] and "present" or "nil"))
-- Reset quest log
if quest.WasComplete or quest.isComplete then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest RESET BLOCK firing for questId:", questId)
-- Reset quest log (may already be nil after QUEST_REMOVED)
QuestiePlayer.currentQuestlog[questId] = nil
-- Reset quest objectives
quest.Objectives = {}
-- Reset quest objectives (clear stale Completed/isUpdated flags that
-- would prevent map pins from being drawn on re-accept)
if type(quest.Objectives) == "table" then
for k in pairs(quest.Objectives) do
quest.Objectives[k] = nil
end
else
quest.Objectives = {}
end
-- Reset SpecialObjectives too
if type(quest.SpecialObjectives) == "table" then
for k in pairs(quest.SpecialObjectives) do
quest.SpecialObjectives[k] = nil
end
end
-- Reset quest flags
quest.WasComplete = nil
quest.isComplete = nil
-- Ensure isUpdated flags are reset so ObjectiveUpdate will refresh
-- objective state from the quest log instead of short-circuiting
QuestieQuest:SetObjectivesDirty(questId)
-- Reset tooltips
QuestieTooltips:RemoveQuest(questId)
-- Unload map pins from previous acceptance so they don't linger
QuestieMap:UnloadQuestFrames(questId)
end
if not QuestiePlayer.currentQuestlog[questId] then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest NORMAL path - questId:", questId,
" quest.isComplete:", tostring(quest.isComplete),
" quest.WasComplete:", tostring(quest.WasComplete),
" quest.Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
QuestiePlayer.currentQuestlog[questId] = quest
@@ -537,8 +572,8 @@ function QuestieQuest:AcceptQuest(questId)
end
)
else
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest] Accepted Quest:", questId,
" Warning: Quest already exists, not adding")
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AcceptQuest DUPLICATE - questId:", questId,
" Warning: Quest already exists in currentQuestlog, not processing!")
end
end
end
@@ -575,6 +610,14 @@ function QuestieQuest:CompleteQuest(questId)
Questie.db.char.complete[13701] = true -- Horde Champion Marker
Questie.db.char.complete[13687] = nil -- Horde Tournament Eligibility Marker
end
-- Clear stale objective data so a subsequent re-accept doesn't inherit
-- Cached Completed=true / isUpdated=true flags that would cause
-- PopulateObjectiveNotes to skip drawing map pins (bug: complete-abandon-reaccept).
local quest = QuestieDB.GetQuest(questId)
if quest and type(quest.Objectives) == "table" then
quest.Objectives = {}
end
QuestieMap:UnloadQuestFrames(questId)
-- Clear the pending-complete guard now that frames are unloaded
@@ -620,6 +663,11 @@ function QuestieQuest:AbandonedQuest(questId)
local quest = QuestieDB.GetQuest(questId)
if quest then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] AbandonedQuest - questId:", questId,
" WasComplete:", tostring(quest.WasComplete),
" isComplete:", tostring(quest.isComplete),
" Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
-- Reset quest objectives
quest.Objectives = {}
@@ -686,9 +734,31 @@ function QuestieQuest:UpdateQuest(questId)
end
QuestieQuest:PopulateQuestLogInfo(quest)
if QuestieQuest:ShouldShowQuestNotes(questId) then
-- Defensive: clear stale isComplete/WasComplete after PopulateQuestLogInfo
-- syncs quest log state. This catches cases where PopulateQuestLogInfo ran
-- but the quest log cache didn't have the isComplete field (nil), in which
-- case the else branch above wouldn't clear the flags.
if quest.isComplete then
local dbComplete = QuestieDB.IsComplete(questId)
if dbComplete ~= 1 then
quest.isComplete = nil
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Cleared stale isComplete for quest:", questId)
end
end
if quest.WasComplete then
local dbComplete = QuestieDB.IsComplete(questId)
if dbComplete ~= 1 then
quest.WasComplete = nil
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] Cleared stale WasComplete for quest:", questId)
end
end
local showNotes = QuestieQuest:ShouldShowQuestNotes(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] ShouldShowQuestNotes:", tostring(showNotes), "questId:", questId)
if showNotes then
QuestieQuest:UpdateObjectiveNotes(quest)
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:UpdateQuest] ShouldShowQuestNotes=false, removing tooltips for questId:", questId)
QuestieTooltips:RemoveQuest(questId)
end
@@ -760,6 +830,34 @@ function QuestieQuest:UpdateQuest(questId)
QuestieQuest:PopulateObjectiveNotes(quest)
AvailableQuests.CalculateAndDrawAll()
else
-- Robustness: ensure objective pins are present for incomplete quests.
-- On Ascension, quest log cache can be stale after reload/abandon-reaccept,
-- leaving objectives empty or AlreadySpawned pointing to dead frames.
if showNotes then
local hasObjectives = quest.Objectives and table.getn(quest.Objectives) > 0
local hasFrames = QuestieMap.questIdFrames[questId] ~= nil
if not hasObjectives then
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieQuest:UpdateQuest] Objectives missing for incomplete quest, re-populating:", questId)
QuestieQuest:PopulateQuestLogInfo(quest)
hasObjectives = quest.Objectives and table.getn(quest.Objectives) > 0
if hasObjectives then
QuestieQuest:PopulateObjectiveNotes(quest)
end
elseif not hasFrames then
-- Objectives exist but no frames on map: AlreadySpawned may be stale
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieQuest:UpdateQuest] Incomplete quest has objectives but no map frames, clearing AlreadySpawned:", questId)
for _, objective in pairs(quest.Objectives) do
if objective.AlreadySpawned then
objective.AlreadySpawned = {}
end
end
QuestieQuest:PopulateObjectiveNotes(quest)
end
end
-- Sometimes objective(s) are all complete but the quest doesn't get flagged as "1". So far the only
-- quests I've found that does this are quests involving an item(s). Checks all objective(s) and if they
-- are all complete, simulate a "Complete Quest" so the quest finisher appears on the map.
@@ -1048,7 +1146,12 @@ end
-- iterate all notes, update / remove as needed
---@param quest Quest
function QuestieQuest:UpdateObjectiveNotes(quest)
-- DEBUG: Log currentQuestlog state
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] UpdateObjectiveNotes - questId:", quest.Id,
" currentQuestlog present:", tostring(QuestiePlayer.currentQuestlog and QuestiePlayer.currentQuestlog[quest.Id] and "yes" or "no"))
if (not QuestiePlayer.currentQuestlog) or (not QuestiePlayer.currentQuestlog[quest.Id]) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] UpdateObjectiveNotes - EARLY RETURN: quest not in currentQuestlog, questId:", quest.Id)
return
end
@@ -1342,14 +1445,17 @@ end
---@param objective QuestObjective
---@param blockItemTooltips any
function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockItemTooltips) -- must be p-called
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjective]", objective.Description)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] questId:", quest.Id,
" objIdx:", objectiveIndex, " Desc:", objective.Description,
" Completed:", tostring(objective.Completed),
" quest.isComplete:", tostring(quest.isComplete))
if (not objective.Update) then
-- No Update function means static/pre-populated objective.
-- Still check completion state so icons are removed if this objective was
-- already marked complete from a previous update cycle.
if objective.Completed or quest.isComplete then
Questie:Debug(Questie.DEBUG_INFO,
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieQuest:PopulateObjective] - No Update fn but objective is complete, unloading icons.")
_UnloadAlreadySpawnedIcons(objective)
end
@@ -1358,7 +1464,24 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
objective:Update()
-- Defensive: clear stale quest.isComplete if quest DB says the quest is NOT
-- complete. The DB cache persists quest objects across acceptance cycles
-- (complete → abandon → reaccept), and PopulateQuestLogInfo may not have
-- run yet for this specific path (e.g., Learner invalidation).
if quest.isComplete then
local dbComplete = QuestieDB.IsComplete(quest.Id)
if dbComplete ~= 1 then
quest.isComplete = nil
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] Cleared stale isComplete for quest:", quest.Id)
end
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] POST-Update questId:", quest.Id,
" objIdx:", objectiveIndex, " Completed:", tostring(objective.Completed),
" isComplete:", tostring(quest.isComplete), " HasUpdate:", tostring(objective.Update ~= nil))
if QuestieQuest.ShouldHideObjective(objective) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] HIDDEN by ShouldHideObjective, unloading icons for objIdx:", objectiveIndex)
_UnloadAlreadySpawnedIcons(objective)
return
end
@@ -1377,6 +1500,8 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
_RegisterObjectiveTooltips(objective, quest.Id, blockItemTooltips)
if completed or quest.isComplete then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] SKIPPING objective (completed):",
objective.Description, "completed:", tostring(completed), "quest.isComplete:", tostring(quest.isComplete))
_UnloadAlreadySpawnedIcons(objective)
return
end
@@ -1386,6 +1511,8 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
end
if objective.spawnList and next(objective.spawnList) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] spawnList present for quest:", quest.Id,
"objIdx:", objectiveIndex, "spawnList entries:", (function() local n=0 for _ in pairs(objective.spawnList) do n=n+1 end return n end)())
local maxPerType = 300
if Questie.db.profile.enableIconLimit and Questie.db.profile.iconLimit < maxPerType then
@@ -1448,6 +1575,9 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
local iconsToDraw, _ = _DetermineIconsToDraw(quest, objective, objectiveIndex, objectiveCenter)
local icon, iconPerZone = _DrawObjectiveIcons(quest.Id, iconsToDraw, objective, maxPerType)
_DrawObjectiveWaypoints(objective, icon, iconPerZone)
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateObjective] NO spawnList for quest:", quest.Id,
"objIdx:", objectiveIndex, "spawnList:", tostring(objective.spawnList))
end
end
@@ -1519,7 +1649,10 @@ end
---@param objectiveIndex ObjectiveIndex
---@param objectiveCenter {x:X, y:Y}
_DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCenter)
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DetermineIconsToDraw]")
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DetermineIconsToDraw] quest:", quest.Id,
"objective:", objective.Description, "spawnList:", objective.spawnList and next(objective.spawnList) and "has entries" or "nil/empty",
"AlreadySpawned:", objective.AlreadySpawned and next(objective.AlreadySpawned) and "has entries" or "empty",
"Completed:", tostring(objective.Completed), "enableObjectives:", tostring(Questie.db.profile.enableObjectives))
local iconsToDraw = {}
local spawnItemId
@@ -1535,6 +1668,7 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
end
if (not objective.AlreadySpawned[id]) and (not objective.Completed) and Questie.db.profile.enableObjectives then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DetermineIconsToDraw] CREATING icon entry for id:", id, "quest:", quest.Id)
local data = {
Id = quest.Id,
ObjectiveIndex = objectiveIndex,
@@ -1605,7 +1739,15 @@ _DetermineIconsToDraw = function(quest, objective, objectiveIndex, objectiveCent
end
_DrawObjectiveIcons = function(questId, iconsToDraw, objective, maxPerType)
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveIcons] Adding Icons for quest:", questId)
local iconCount = 0
local _, _ = next(iconsToDraw)
if _ then
local n = 0
for _ in pairs(iconsToDraw) do n = n + 1 end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DrawObjectiveIcons] Drawing", n, "icon groups for quest:", questId)
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:_DrawObjectiveIcons] NO icons to draw for quest:", questId)
end
local spawnedIconCount = 0
local icon
@@ -1747,7 +1889,23 @@ function QuestieQuest:PopulateObjectiveNotes(quest) -- this should be renamed to
return
end
if QuestieDB.IsComplete(quest.Id) == 1 then
local dbComplete = QuestieDB.IsComplete(quest.Id)
-- DEBUG: Log PopulateObjectiveNotes state
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateObjectiveNotes - questId:", quest.Id,
" quest.isComplete:", tostring(quest.isComplete),
" dbComplete:", tostring(dbComplete),
" Objectives count:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty")
-- Defensive: clear stale quest.isComplete flag if the quest log doesn't confirm
-- completion. Without this, a re-accepted quest that was previously completed can
-- have isComplete=true while the quest log shows it as incomplete.
if quest.isComplete and dbComplete ~= 1 then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateObjectiveNotes - clearing stale isComplete for questId:", quest.Id)
quest.isComplete = nil
end
if dbComplete == 1 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateObjectiveNotes] Quest Complete! Adding Finisher for:",
quest.Id)
@@ -1778,14 +1936,33 @@ function QuestieQuest:PopulateQuestLogInfo(quest)
return nil
end
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:PopulateQuestLogInfo] ", quest.Id)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateQuestLogInfo - questId:", quest.Id,
" Objectives:", quest.Objectives and next(quest.Objectives) and "has entries" or "empty",
" isComplete:", tostring(quest.isComplete),
" WasComplete:", tostring(quest.WasComplete))
local questLogEngtry = QuestLogCache.GetQuest(quest.Id) -- DO NOT MODIFY THE RETURNED TABLE
if (not questLogEngtry) then return end
if (not questLogEngtry) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest] PopulateQuestLogInfo - CACHE MISS for questId:", quest.Id, "- returning early!")
return
end
-- Sync quest.isComplete with quest log state. The cache in QuestieDB persists
-- quest objects across acceptance cycles (complete → abandon → reaccept), so
-- stale isComplete/WasComplete flags from a previous acceptance must be cleared
-- when the quest log says the quest is no longer complete.
if questLogEngtry.isComplete ~= nil and questLogEngtry.isComplete == 1 then
quest.isComplete = true
else
quest.isComplete = nil
-- Also clear WasComplete when quest log confirms the quest is NOT complete.
-- Without this, the isComplete==0 branch in UpdateQuest sees stale
-- WasComplete=true and triggers an unnecessary reset sequence.
if quest.WasComplete then
quest.WasComplete = nil
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieQuest:PopulateQuestLogInfo] Cleared stale WasComplete for quest:", quest.Id)
end
end
-- Live fallback quests (no static DB entry) manage their own Objectives.
@@ -1980,6 +2157,31 @@ function _QuestieQuest.ObjectiveUpdate(self)
end
end
--- Lazily build an objective's spawnList from the spawn call table (includes
--- QuestieLearner-injected data). Used by QuestieArrow to resolve targets when
--- the spawnList hasn't been populated yet (e.g. after a quest re-accept cycle).
---@param objective table @An objective entry from quest.Objectives or quest.SpecialObjectives
---@param objectiveData table? @Corresponding ObjectiveData entry (falls back to the objective itself for SpecialObjectives)
---@return table|nil @The built spawnList, or nil if no handler matched
function QuestieQuest:BuildObjectiveSpawnList(objective, objectiveData)
if not objective then return nil end
-- Already populated — nothing to do
if objective.spawnList and next(objective.spawnList) then
return objective.spawnList
end
-- Try the explicit ObjectiveData first (normal objectives)
if objectiveData and objectiveData.Type and _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type] then
objective.spawnList = _QuestieQuest.objectiveSpawnListCallTable[objectiveData.Type](objective.Id, objective, objectiveData)
return objective.spawnList
end
-- SpecialObjectives and fallback: use the objective's own Type
if objective.Type and _QuestieQuest.objectiveSpawnListCallTable[objective.Type] then
objective.spawnList = _QuestieQuest.objectiveSpawnListCallTable[objective.Type](objective.Id, objective, objective)
return objective.spawnList
end
return nil
end
---@param questId number
---@return table<ObjectiveIndex, QuestLogCacheObjectiveData>|nil @DO NOT EDIT RETURNED TABLE
function QuestieQuest:GetAllLeaderBoardDetails(questId)
+36 -1
View File
@@ -120,7 +120,23 @@ monster = function(npcId, objective)
end
local name = QuestieDB.QueryNPCSingle(npcId, "name")
if (not name) then
if not name or name == "" then
-- Last resort: extract NPC name from objective description text.
-- This mirrors the name-parsing logic in the killcredit function.
if objective then
local desc = objective.Description or objective.text
if desc then
name = desc:match("^%d+/%d+%s+(.+)$") or desc:match("^(.-):%s*%d+/%d+$")
if not name then
name = desc:gsub("%d+/%d+", ""):gsub("[:!?,.%(%)%[%]]", ""):gsub("^%s+", ""):gsub("%s+$", "")
end
if name and name ~= "" then
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Using objective description as name fallback for NPC:", npcId, name)
end
end
end
end
if not name or name == "" then
Questie:Debug(Questie.DEBUG_CRITICAL, "Name missing for NPC:", npcId)
return nil
end
@@ -131,6 +147,25 @@ monster = function(npcId, objective)
spawns = {}
end
-- Learner safety net: when prioritizeMyData is enabled and the Learner has
-- verified spawn data for this NPC, prefer it over compiled DB spawns.
-- This catches edge cases where the npcDataOverrides chain doesn't fully
-- replace retail positions (e.g. format migration gaps, timing issues).
if Questie.IsAscension and Questie.dbLearner and Questie.dbLearner.global then
local ld = Questie.dbLearner.global
if ld.settings and ld.settings.prioritizeMyData then
local learnedNpc = ld.npcs and ld.npcs[npcId]
if learnedNpc then
local learnedSpawns = learnedNpc[7]
local threshold = ld.settings.minConfidencePins or 1
if learnedSpawns and next(learnedSpawns) and learnedNpc.mc and learnedNpc.mc >= threshold then
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Preferring learned spawns for NPC:", npcId, "(mc=" .. tostring(learnedNpc.mc) .. ")")
spawns = learnedSpawns
end
end
end
end
local rank = QuestieDB.QueryNPCSingle(npcId, "rank")
local enableSpawns = not QuestieCorrections.questNPCBlacklist[npcId]
+349 -57
View File
@@ -95,11 +95,28 @@ QuestieLearner.data = nil
------------------------------------------------------------------------
local function GetZoneId()
-- 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.
-- Prefer the most specific zone available: GetRealZoneText() returns the
-- sub-zone name when the player is on a child map (e.g. "Sunstrider Isle"
-- on map 1241 → areaId 3431), and the parent zone name otherwise (e.g.
-- "Eversong Woods" → areaId 3430). Using the sub-zone is correct because
-- Questie resolves subzones to parents via GetParentZoneId() automatically.
local zoneText = GetRealZoneText and GetRealZoneText() or ""
if zoneText ~= "" then
local areaId = _Learner.zoneCache[zoneText]
if not areaId and l10n and l10n.GetAreaIdByLocalName then
areaId = l10n:GetAreaIdByLocalName(zoneText)
if areaId and areaId > 0 then
_Learner.zoneCache[zoneText] = areaId
end
end
if areaId and areaId > 0 then
return areaId
end
end
-- Fallback: uiMapId-based conversion (e.g. 1241→3431 for Sunstrider).
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
@@ -312,57 +329,93 @@ end
-- reference the given npcId. Unloads existing world/minimap icons, resets
-- tooltip registration, and forces the map system to rebuild spawn lists from
-- QuestieDB on the next update — picking up newly learned coordinates in real time.
-- Helper: check if a single objective references the given npcId and, if so,
-- unload its icons and reset its cached spawnList so the map rebuilds it.
-- Returns true if the objective was invalidated.
local function _TryInvalidateObjective(objective, npcId, quest)
local shouldInvalidate = false
-- Monster objectives reference NPCs directly in spawnList keys
if objective.spawnList then
if objective.spawnList[npcId] then
shouldInvalidate = true
end
-- Also check killcredit IdList
if not shouldInvalidate and objective.IdList then
for _, id in ipairs(objective.IdList) do
if id == npcId then shouldInvalidate = true; break end
end
end
else
-- spawnList is nil (first-ever encounter, never populated).
-- Check the quest's ObjectiveData for NPC references so killcredit
-- and item objectives are still invalidated on the first kill.
if quest and quest.ObjectiveData and objective.Index then
local objData = quest.ObjectiveData[objective.Index]
if objData and objData.IdList then
for _, id in ipairs(objData.IdList) do
if id == npcId then shouldInvalidate = true; break end
end
end
-- Also match the primary objective Id (e.g. single-target monster objectives)
if not shouldInvalidate and objData and objData.Id == npcId then
shouldInvalidate = true
end
end
end
-- Fallback: if objective Id matches the NPC (some objectives use NPC as their primary Id)
if not shouldInvalidate and objective.Id == npcId then
shouldInvalidate = true
end
if shouldInvalidate then
-- Unload existing icons manually so frames are removed from map/minimap.
-- We can't call QuestieQuest's local _UnloadAlreadySpawnedIcons from here,
-- so we iterate the refs directly.
if objective.AlreadySpawned then
for _, spawn in pairs(objective.AlreadySpawned) do
if spawn then
if spawn.mapRefs then
for _, mapIcon in ipairs(spawn.mapRefs) do
if mapIcon and mapIcon.Unload then mapIcon:Unload() end
end
end
if spawn.minimapRefs then
for _, minimapIcon in ipairs(spawn.minimapRefs) do
if minimapIcon and minimapIcon.Unload then minimapIcon:Unload() end
end
end
end
end
end
objective.spawnList = nil
objective.AlreadySpawned = {} -- empty table, NOT nil (_DetermineIconsToDraw indexes this)
objective.hasRegisteredTooltips = false
objective.registeredItemTooltips = false
end
return shouldInvalidate
end
local function _InvalidateSpawnListsForNPC(npcId)
if not QuestieQuest or not QuestiePlayer or not QuestiePlayer.currentQuestlog then return end
local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer)
local questsToRefresh = {}
for questId, _ in pairs(QuestiePlayer.currentQuestlog) do
local quest = QuestieDB.GetQuest and QuestieDB.GetQuest(questId)
if quest and quest.Objectives then
if quest then
local needsUnload = false
for _, objective in pairs(quest.Objectives) do
local shouldInvalidate = false
-- Monster objectives reference NPCs directly in spawnList keys
if objective.spawnList then
if objective.spawnList[npcId] then
shouldInvalidate = true
end
-- Also check killcredit IdList
if not shouldInvalidate and objective.IdList then
for _, id in ipairs(objective.IdList) do
if id == npcId then shouldInvalidate = true; break end
end
-- Scan standard Objectives
if quest.Objectives then
for _, objective in pairs(quest.Objectives) do
if _TryInvalidateObjective(objective, npcId, quest) then
needsUnload = true
end
end
-- Fallback: if objective Id matches the NPC (some objectives use NPC as their primary Id)
if not shouldInvalidate and objective.Id == npcId then
shouldInvalidate = true
end
if shouldInvalidate then
-- Unload existing icons manually so frames are removed from map/minimap.
-- We can't call QuestieQuest's local _UnloadAlreadySpawnedIcons from here,
-- so we iterate the refs directly.
if objective.AlreadySpawned then
for _, spawn in pairs(objective.AlreadySpawned) do
if spawn then
if spawn.mapRefs then
for _, mapIcon in ipairs(spawn.mapRefs) do
if mapIcon and mapIcon.Unload then mapIcon:Unload() end
end
end
if spawn.minimapRefs then
for _, minimapIcon in ipairs(spawn.minimapRefs) do
if minimapIcon and minimapIcon.Unload then minimapIcon:Unload() end
end
end
end
end
end
-- Scan SpecialObjectives (demonic runestones, custom Ascension objectives, etc.)
if quest.SpecialObjectives then
for _, objective in pairs(quest.SpecialObjectives) do
if _TryInvalidateObjective(objective, npcId, quest) then
needsUnload = true
end
objective.spawnList = nil
objective.AlreadySpawned = {} -- empty table, NOT nil (_DetermineIconsToDraw indexes this)
objective.hasRegisteredTooltips = false
objective.registeredItemTooltips = false
needsUnload = true
end
end
if needsUnload then
@@ -648,10 +701,21 @@ end
-- NPC learning
------------------------------------------------------------------------
-- Player-spawned NPCs that should never be learned (totems, guardians, etc.)
local PLAYER_SPAWNED_NPC_SET = {
[2523] = true, -- Searing Totem
[2630] = true, -- Earthbind Totem
[10183] = true, -- Moonflare Totem
[1103907] = true, -- Healing Stream Totem III
[1107398] = true, -- Stoneclaw Totem V
}
function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionString, spawnX, spawnY, spawnZoneId)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnNpcs then return end
if not npcId or npcId <= 0 then return end
-- Never learn player-spawned totems
if PLAYER_SPAWNED_NPC_SET[npcId] then return end
-- Use provided spawn coords (e.g. from kill event) or fall back to current player position
local zoneId = spawnZoneId or GetZoneId()
@@ -669,7 +733,7 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
Questie.dbLearner.global.npcs[npcId] = existing
end
if name and not existing[1] then existing[1] = name end
if name and name ~= "" and (not existing[1] or existing[1] == "") then existing[1] = name end
if level then
if not existing[4] or level < existing[4] then existing[4] = level end
if not existing[5] or level > existing[5] then existing[5] = level end
@@ -694,9 +758,9 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
if not ovr then
QuestieDB.npcDataOverrides[npcId] = existing
else
-- Merge: fill missing fields only
-- Merge: fill missing fields; also overwrite empty-string names
for k, v in pairs(existing) do
if ovr[k] == nil then ovr[k] = v end
if ovr[k] == nil or (k == 1 and ovr[k] == "") then ovr[k] = v end
end
-- Always merge spawn coords
if existing[7] then
@@ -718,12 +782,11 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
if isNew then
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] New NPC learned:", npcId, name or "?")
CrossLinkAfterNPC(npcId)
else
-- Existing NPC got new spawn data: invalidate cached spawnLists
-- for any active quest objective that references this NPC so the
-- map system rebuilds them with fresh data on next update.
_InvalidateSpawnListsForNPC(npcId)
end
-- Existing NPC got new spawn data: invalidate cached spawnLists
-- for any active quest objective that references this NPC so the
-- map system rebuilds them with fresh data on next update.
_InvalidateSpawnListsForNPC(npcId)
_Learner:BroadcastIfCommsAvailable("NPC", npcId, existing)
end
@@ -1117,11 +1180,65 @@ function QuestieLearner:InjectLearnedData()
if not EnsureLearnedData() then return end
local learned = Questie.dbLearner.global
-- Migrate old-format NPC data ([4]=spawns, [5]=zoneId) to new format ([7]=spawns, [9]=zoneId)
-- Always merge [4] into [7], even when [7] already has partial data from a recent session.
for npcId, data in pairs(learned.npcs) do
if type(data[4]) == "table" then
if data[7] == nil then
-- Simple move: no [7] exists yet
data[7] = data[4]
else
-- Merge: [7] has partial data, consolidate [4] coordinates into it
for zoneId, coords in pairs(data[4]) do
data[7][zoneId] = data[7][zoneId] or {}
for _, coord in ipairs(coords) do
InsertIfNewBucket(data[7][zoneId], coord[1], coord[2])
end
end
end
data[4] = nil
end
if type(data[5]) == "number" and data[9] == nil then
data[9] = data[5]
data[5] = nil
end
end
-- Merge character-specific learned NPC data into global pool
if Questie.db and Questie.db.char and Questie.db.char.npcs then
for npcId, data in pairs(Questie.db.char.npcs) do
local globalData = learned.npcs[npcId]
if not globalData then
learned.npcs[npcId] = data
else
-- Merge spawns: char data may be old ([4]) or new ([7]) format
local charSpawns = data[7] or data[4]
local globalSpawns = globalData[7] or globalData[4]
if charSpawns then
if not globalSpawns then
globalData[7] = {}
globalSpawns = globalData[7]
end
for zoneId, coords in pairs(charSpawns) do
globalSpawns[zoneId] = globalSpawns[zoneId] or {}
for _, coord in ipairs(coords) do
InsertIfNewBucket(globalSpawns[zoneId], coord[1], coord[2])
end
end
end
-- Adopt zoneId if missing
if not globalData[9] and data[9] then globalData[9] = data[9] end
if not globalData[5] and data[5] then globalData[5] = data[5] end
end
end
Questie.db.char.npcs = nil
end
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.
-- were stored under key 1241 instead of the correct areaId 3431.
-- Convert any uiMapId keys to areaId using ZoneDB.
local zonesFixed = 0
for npcId, data in pairs(learned.npcs) do
@@ -1181,6 +1298,82 @@ function QuestieLearner:InjectLearnedData()
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Migrated", zonesFixed, "spawn zone keys from uiMapId to areaId")
end
-- Also fix [9] zone field for NPCs and [5] zone field for Objects
-- that were stored as uiMapId instead of areaId (e.g. 1241 → 3431).
local fieldsFixed = 0
for npcId, data in pairs(learned.npcs) do
if type(data[9]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[9])
if maybeAreaId and maybeAreaId ~= data[9] then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] NPC", npcId, "zone field [9]", data[9], "->", maybeAreaId)
data[9] = maybeAreaId
fieldsFixed = fieldsFixed + 1
end
end
end
for objId, data in pairs(learned.objects) do
if type(data[5]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[5])
if maybeAreaId and maybeAreaId ~= data[5] then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Object", objId, "zone field [5]", data[5], "->", maybeAreaId)
data[5] = maybeAreaId
fieldsFixed = fieldsFixed + 1
end
end
end
if fieldsFixed > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Fixed", fieldsFixed, "zone fields from uiMapId to areaId")
end
-- Purge player-spawned totems from learned NPCs (they are not real world spawns)
-- Also purge NPCs with entirely empty spawn data (stale learner artifacts)
local purgedNpcs = 0
local PLAYER_SPAWNED_NPCS = {
[2523] = true, -- Searing Totem
[2630] = true, -- Earthbind Totem
[10183] = true, -- Moonflare Totem
[1103907] = true, -- Healing Stream Totem III
[1107398] = true, -- Stoneclaw Totem V
}
for npcId, data in pairs(learned.npcs) do
if PLAYER_SPAWNED_NPCS[npcId] then
learned.npcs[npcId] = nil
purgedNpcs = purgedNpcs + 1
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged player-spawned NPC", npcId, data[1] or "?")
elseif data[7] then
-- Check for empty spawn table (no coords at all = stale learner artifact)
local hasCoords = false
for zoneKey, coords in pairs(data[7]) do
if type(coords) == "table" and #coords > 0 then
hasCoords = true
break
end
end
if not hasCoords then
learned.npcs[npcId] = nil
purgedNpcs = purgedNpcs + 1
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged NPC with empty spawns", npcId, data[1] or "?")
end
end
end
if purgedNpcs > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedNpcs, "invalid NPCs from learned data")
end
-- Purge Object entries that duplicate NPC entries (mobs learned as both NPC and Object).
-- NPC data is richer (has names, quest IDs), so keep the NPC version and remove the Object.
local dupObjectsRemoved = 0
for objId, _ in pairs(learned.objects) do
if learned.npcs[objId] then
learned.objects[objId] = nil
dupObjectsRemoved = dupObjectsRemoved + 1
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Removed duplicate Object", objId, "(NPC version exists)")
end
end
if dupObjectsRemoved > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Removed", dupObjectsRemoved, "Object entries that duplicate NPC entries")
end
-- 1. NPCs
local npcIdsToFix = {}
for npcId, data in pairs(learned.npcs) do
@@ -1216,6 +1409,87 @@ function QuestieLearner:InjectLearnedData()
learned.npcs[old] = nil
end
-- Diagnostic: log how many NPCs were injected with spawn overrides
local spawnOverrideCount = 0
for nid, ovr in pairs(QuestieDB.npcDataOverrides) do
if ovr[7] and next(ovr[7]) then
spawnOverrideCount = spawnOverrideCount + 1
end
end
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieLearner] InjectLearnedData: injected", npcCount, "NPCs (", spawnOverrideCount, "with spawn overrides)")
-- Purge garbage quest entries: quests with no name [1] and only mc/ls metadata.
-- These are Ascension internal tracking artifacts (hash-like IDs) with no real quest data.
-- Also purge quest 788 ("Mottled Boar slain") which is an objective text, not a quest name.
local purgedQuests = 0
local OBJECTIVE_TEXT_QUEST_IDS = {
[788] = true, -- "Mottled Boar slain" — objective text, not a quest
}
for questId, data in pairs(learned.quests) do
local hasRealData = false
-- Check if quest has any meaningful data beyond mc/ls metadata
if type(data[1]) == "string" and data[1] ~= "" then
hasRealData = true
end
-- Also check for objective data, level, zone, etc.
if not hasRealData then
for k, v in pairs(data) do
if k ~= "mc" and k ~= "ls" then
hasRealData = true
break
end
end
end
local qid = tonumber(questId)
if not hasRealData or (qid and OBJECTIVE_TEXT_QUEST_IDS[qid]) then
local reason = not hasRealData and "garbage" or "objective-text"
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Purged", reason, "quest", questId, data[1] or "?")
learned.quests[questId] = nil
purgedQuests = purgedQuests + 1
end
end
if purgedQuests > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedQuests, "invalid quests (garbage/objective-text)")
end
-- Infer sortKey [17] from zone data [3] for quests that have a name but no sortKey.
-- Field [3] contains zone/area info (e.g. {{470}} for Ghostlands, {{414}} for Zul'Drak).
-- The sortKey should be the areaId from [3] so the quest appears in the correct zone in the UI.
local sortKeysInferred = 0
for questId, data in pairs(learned.quests) do
if data[1] and not data[17] and data[3] then
-- [3] can be a table of tables: {{414}} or nested {{414, ...}, ...}
-- Extract the first numeric areaId from it
local sortKey = nil
if type(data[3]) == "table" then
-- Walk into nested tables to find the first numeric value
local function findFirstNumber(t)
if type(t) ~= "table" then return nil end
for i = 1, #t do
if type(t[i]) == "number" then
return t[i]
elseif type(t[i]) == "table" then
local result = findFirstNumber(t[i])
if result then return result end
end
end
return nil
end
sortKey = findFirstNumber(data[3])
elseif type(data[3]) == "number" then
sortKey = data[3]
end
if sortKey then
data[17] = sortKey
sortKeysInferred = sortKeysInferred + 1
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Inferred sortKey", sortKey, "for quest", questId, data[1])
end
end
end
if sortKeysInferred > 0 then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Inferred sortKey for", sortKeysInferred, "quests from zone data")
end
-- 2. Quests
local questIdsToFix = {}
for questId, data in pairs(learned.quests) do
@@ -1512,7 +1786,7 @@ function QuestieLearner:OnMouseoverUnit()
local level = UnitLevel("mouseover")
local zoneText = GetRealZoneText()
local areaId = _Learner.zoneCache[zoneText]
if not areaId then
if not areaId and l10n and l10n.GetAreaIdByLocalName then
areaId = l10n:GetAreaIdByLocalName(zoneText)
if areaId then
_Learner.zoneCache[zoneText] = areaId
@@ -1707,7 +1981,7 @@ function QuestieLearner:OnQuestAccepted(firstArg, secondArg)
-- Zone: reverse-lookup from GetRealZoneText() which is always accurate on 3.3.5.
local zoneText = GetRealZoneText()
if zoneText and zoneText ~= "" then
if zoneText and zoneText ~= "" and l10n and l10n.GetAreaIdByLocalName then
local areaId = l10n:GetAreaIdByLocalName(zoneText)
if areaId and areaId > 0 then
data[17] = areaId
@@ -1973,12 +2247,27 @@ function QuestieLearner:OnCombatLogEvent(...)
if not dstGUID then return end
local npcId = GetNpcIdFromGUID(dstGUID)
local name = dstName
-- Fallback chain for mob name: combat-log dstName → cached target/mouseover → current target unit
if not name or name == "" then
if _Learner.guidNpcCache then
local cached = _Learner.guidNpcCache[dstGUID]
if cached and cached.name and cached.name ~= "" then
name = cached.name
end
end
end
if not name or name == "" then
if UnitGUID("target") == dstGUID then
name = UnitName("target")
end
end
if not npcId and _Learner.guidNpcCache then
local cached = _Learner.guidNpcCache[dstGUID]
if cached then
npcId = cached.npcId
if not dstName then dstName = cached.name end
end
end
@@ -1993,7 +2282,7 @@ function QuestieLearner:OnCombatLogEvent(...)
local zoneText = GetRealZoneText and GetRealZoneText() or ""
_Learner.recentKills[dstGUID] = {
npcId = npcId,
name = dstName or "",
name = name or "",
x = px,
y = py,
zoneId = zoneId,
@@ -2005,6 +2294,9 @@ function QuestieLearner:OnCombatLogEvent(...)
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Kill cached for correlation:", npcId, dstName, "@", tostring(px), tostring(py), "zone", tostring(zoneId))
end
-- Unconditionally map the spawn position for Ascension DB building
self:LearnNPC(npcId, name, nil, nil, nil, nil, px, py, zoneId)
-- TTL cleanup: drop entries older than 10 minutes
local now = time()
for g, entry in pairs(_Learner.recentKills) do
+29 -18
View File
@@ -31,22 +31,26 @@ local GetClassColor = QuestieCompat.GetClassColor
-- Silent quest log index lookup (prevents warning spam for auto-complete/achievement "quests")
-- Uses QuestieCompat wrapper which normalizes return values across API versions.
local function _Questie_SilentGetQuestLogIndexByID(questId)
questId = tonumber(questId)
if not questId then return 0 end
if _G.GetQuestLogIndexByID then
return _G.GetQuestLogIndexByID(questId) or 0
-- QuestieCompat.GetQuestLogIndexByID returns nil when not found,
-- coerce to 0 for callers that check > 0.
local idx = QuestieCompat.GetQuestLogIndexByID(questId)
if idx then
return idx
end
local n = (GetNumQuestLogEntries and select(1, GetNumQuestLogEntries())) or 0
if n > 0 and GetQuestLogTitle then
for i = 1, n do
local _, _, _, isHeader, _, _, _, qid = GetQuestLogTitle(i)
qid = tonumber(qid)
if (not isHeader) and qid and qid == questId then
return i
end
-- Fallback: use the compat-normalized GetQuestLogTitle (always 8 returns)
local GetQuestLogTitleCompat = QuestieCompat.GetQuestLogTitle
local n = select(1, GetNumQuestLogEntries()) or 0
for i = 1, n do
local _, _, _, isHeader, _, _, _, qid = GetQuestLogTitleCompat(i)
qid = tonumber(qid)
if (not isHeader) and qid and qid == questId then
return i
end
end
return 0
@@ -224,8 +228,14 @@ function MapIconTooltip:Show()
handleMapIcon(icon)
end
else
for pin in HBDPins.worldmapProvider:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplateQuestie") do
handleMapIcon(pin.icon)
if QuestieCompat.Is335 then
for icon, _ in next, HBDPins.worldmapPins do
handleMapIcon(icon)
end
else
for pin in HBDPins.worldmapProvider:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplateQuestie") do
handleMapIcon(pin.icon)
end
end
end
@@ -402,7 +412,7 @@ function MapIconTooltip:Show()
break
end
factionName = select(1, GetFactionInfoByID(factionId))
factionName = select(1, QuestieCompat.GetFactionInfoByID(factionId))
if factionName then
rewardValue = rewardPair[2]
@@ -423,10 +433,10 @@ function MapIconTooltip:Show()
end
if aldorPenalty then
factionName = select(1, GetFactionInfoByID(932))
factionName = select(1, QuestieCompat.GetFactionInfoByID(932))
tinsert(rewardTable, aldorPenalty .. " " .. factionName)
elseif scryersPenalty then
factionName = select(1, GetFactionInfoByID(934))
factionName = select(1, QuestieCompat.GetFactionInfoByID(934))
tinsert(rewardTable, scryersPenalty .. " " .. factionName)
end
@@ -513,6 +523,7 @@ function MapIconTooltip:Show()
end
return levelString
end
-- Used to get the white color for the quests which don't have anything to collect
local defaultQuestColor = QuestieLib:GetRGBForObjective({})
local creatureLevels = QuestieDB:GetCreatureLevels(quest) -- Data for min and max level
@@ -585,7 +596,7 @@ function MapIconTooltip:Show()
break
end
factionName = select(1, GetFactionInfoByID(factionId))
factionName = select(1, QuestieCompat.GetFactionInfoByID(factionId))
if factionName then
rewardValue = rewardPair[2]
@@ -606,10 +617,10 @@ function MapIconTooltip:Show()
end
if aldorPenalty then
factionName = select(1, GetFactionInfoByID(932))
factionName = select(1, QuestieCompat.GetFactionInfoByID(932))
tinsert(rewardTable, aldorPenalty .. " " .. factionName)
elseif scryersPenalty then
factionName = select(1, GetFactionInfoByID(934))
factionName = select(1, QuestieCompat.GetFactionInfoByID(934))
tinsert(rewardTable, scryersPenalty .. " " .. factionName)
end
+2 -8
View File
@@ -232,7 +232,7 @@ function QuestieTooltips:GetTooltip(key)
-- Try to find in learned NPCs or objects
local id = tonumber(key:sub(3))
if id then
if key:sub(1,2) == "m_" then
if key:sub(1,2) == "m_" then
local learnedNpc = QuestieLearner.data.npcs[id]
-- npc[10] from _AddToArray is an array of questIds, not {questId -> objList}
if learnedNpc and learnedNpc[10] then
@@ -248,13 +248,11 @@ function QuestieTooltips:GetTooltip(key)
local objText = objEntry[2]
local needed, collected
local objectives = QuestLogCache.GetQuestObjectives(questId)
local objectiveIcon
if objectives then
for _, obj in next, objectives 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
needed = obj.numRequired
collected = obj.numFulfilled
objectiveIcon = obj.Icon
break
end
end
@@ -264,7 +262,6 @@ function QuestieTooltips:GetTooltip(key)
Description = objText,
Needed = needed,
Collected = collected,
Icon = objectiveIcon,
Update = function(self)
local objs = QuestLogCache.GetQuestObjectives(questId)
if objs then
@@ -288,7 +285,7 @@ function QuestieTooltips:GetTooltip(key)
tinsert(tooltipLines, "|cFF5EBAF3(Learned - Confidence: " .. tostring(learnedNpc.mc) .. ")|r")
end
end
elseif key:sub(1,2) == "o_" then
elseif key:sub(1,2) == "o_" then
local learnedObj = QuestieLearner.data.objects[id]
-- obj[2] from _AddToArray is an array of questIds (questStarts), not {questId -> objList}
if learnedObj and learnedObj[2] then
@@ -304,13 +301,11 @@ function QuestieTooltips:GetTooltip(key)
local objText = objEntry[2]
local needed, collected
local objectives = QuestLogCache.GetQuestObjectives(questId)
local objectiveIcon
if objectives then
for _, obj in next, objectives 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
needed = obj.numRequired
collected = obj.numFulfilled
objectiveIcon = obj.Icon
break
end
end
@@ -320,7 +315,6 @@ function QuestieTooltips:GetTooltip(key)
Description = objText,
Needed = needed,
Collected = collected,
Icon = objectiveIcon,
Update = function(self)
local objs = QuestLogCache.GetQuestObjectives(questId)
if objs then