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:
+25
-3
@@ -4,6 +4,24 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **[Fix — Arrow Rendering: Single-Frame SetRotation]** Replaced the sprite sheet-based arrow rendering with a single-frame texture + `SetRotation()` for perfectly smooth rotation. The sprite sheet approach used 108 discrete frames (3.33° per step), causing visible jitter. The previous attempt to fix this with `SetRotation` sub-cell interpolation broke WoW's UV sampling and displayed the entire sprite sheet on screen.
|
||||
- **New arrow texture**: `Icons/arrow.tga` is now X-PLORE's `XPArrow4.tga` — a single 256×256 RGBA TGA with a blue neon arrow pointing UP at `SetRotation(0)`, centered at pixel (128,128) for clean pivot rotation.
|
||||
- **Removed all sprite sheet logic**: No more `ARROW_SHEET_*`, `ARROW_CELL_*`, `ARROW_TOTAL_CELLS`, `SetTexCoord` cell selection, or UV padding. Replaced with `ARROW_DISPLAY_SIZE` (single constant for on-screen pixel size) and `SetRotation(-angle)` for infinite angular resolution.
|
||||
- **Direction math**: WoW's `SetRotation` is clockwise-positive (CW for positive r). The arrow angle `0` = target ahead (north), positive = clockwise, so we apply `SetRotation(angle)`.
|
||||
- **Arrow anchor**: Changed from `SetPoint("TOP")` to `SetPoint("CENTER")` so the rotation pivot aligns with the frame center.
|
||||
- **No color tint**: `SetVertexColor(1, 1, 1)` preserves the original blue neon appearance.
|
||||
|
||||
- **[Fix — Arrow Target Selection: Mixed Coordinate Systems]** Fixed the GPS arrow not pointing to the nearest objective when quests span calibrated (Sunstrider) and normal zones. Three interacting bugs caused incorrect distance sorting:
|
||||
- **_CollectFinisherSpawns used HBD exclusively**: On Sunstrider, objective distances used calibrated pseudo-world coordinates (0-1353 range) while finisher distances used HBD world coordinates (tens of thousands range). These incomparable units caused quests with both objectives and turn-in NPCs on the same map to sort incorrectly — a Sunstrider finisher NPC at calibrated distance 500 (≈500 yards) could lose to an objective at HBD distance 300 because 300 < 500, even though the finisher was closer. Now `_CollectFinisherSpawns` uses `QuestieCompat.GetCalibratedWorldCoordinatesFromZone` and calibrated Euclidean distance for calibrated targets, matching `_CollectObjective`.
|
||||
- **Player position in HBD distance calls**: On Sunstrider, `_arrow_playerX/Y` held calibrated pseudo-world coordinates (0-1353). Both `_CollectObjective` and `_CollectFinisherSpawns` passed these to `HBD:GetWorldDistance(pInst, pX, pY, tX, tY)` for non-calibrated targets, mixing pseudo-world player coords with real-world target coords. Now both functions use `_arrow_hbdPlayerX/Y` (always in HBD world coordinate space) for HBD distance calculations, ensuring the coordinate systems match.
|
||||
- **Cross-instance penalty for calibrated targets**: The `tInst ~= pInst` penalty (`500000 + dist * 100`) applied to calibrated targets on Sunstrider where `tInst = 0` (from `GetCalibratedWorldCoordinatesFromZone`) and `pInst` was the real instance ID. Now gated behind `(not calibratedTargetGroup)` so calibrated targets are never penalized for instance mismatch.
|
||||
|
||||
- **[Fix — Complete-Abandon-Reaccept Pin Lifecycle]** Fixed quest map pins and GPS arrow not reappearing after completing quest objectives, abandoning the quest, and re-accepting it from the quest giver. Five interacting bugs caused stale state to survive the quest reset:
|
||||
- **MarkQuestAsAbandoned**: The `objectivesWereComplete` path called `CompleteQuest` without clearing `quest.Objectives`, `quest.WasComplete`, or `quest.isComplete`. Stale `Completed=true` and `isUpdated=true` flags caused `PopulateObjectiveNotes` to skip drawing pins on re-accept. Now clears objectives/flags and calls `SetObjectivesDirty(questId)` before `CompleteQuest`.
|
||||
- **CompleteQuest**: Did not clear `quest.Objectives` (unlike `AbandonedQuest` which does). Now adds `quest.Objectives = {}` with type guard as defense-in-depth.
|
||||
- **QUEST_TURNED_IN dead code**: `questLog[questId] = {}` wiped state before the QUEST_TURNED_IN state check could read it, making the auto-complete cleanup unreachable. Moved the check before the wipe.
|
||||
- **AcceptQuest reset**: Added `SetObjectivesDirty(questId)` in the quest re-accept reset block for defense-in-depth, ensuring `isUpdated` flags are reset even if stale objectives somehow survive.
|
||||
- **Arrow spawnList gap**: The GPS arrow's `_CollectObjective` silently skipped objectives with nil/empty `spawnList`. After quest re-accept, `PopulateQuestLogInfo` creates objectives without `spawnList`; `PopulateObjectiveNotes` builds it later in the TaskQueue. Added `QuestieQuest:BuildObjectiveSpawnList(objective, objectiveData)` public API that lazily builds `spawnList` from `objectiveSpawnListCallTable` handlers (which include QuestieLearner-injected NPC/object/item spawn data from SavedVariables). The arrow now calls this when `spawnList` is missing.
|
||||
- **[Fix — MapIconTooltip _GetLevelString Guard]** Resolved `attempt to concatenate local 'minLevel' (a nil value)` crash in `MapIconTooltip.lua:494` (`_GetLevelString` function). The creature name "Uneasy Citizen" existed in `creatureLevels` as an empty table `{}` rather than the expected `[1]=minLevel, [2]=maxLevel, [3]=rank` tuple, causing `creatureLevels[name][1]` to return nil. Added an early-return guard at the top of `_GetLevelString`: if `creatureLevels[name]` is falsy or not a table with a numeric level at index `[1]`, return the name unmodified.
|
||||
- **[Fix — Tooltip Learned Data Schema Mismatch]** Reworked learned NPC/object tooltip registration in `Modules/Tooltips/Tooltip.lua` to match the actual `QuestieLearner` storage format.
|
||||
- **Player-Facing Symptom**: Fixed the Stormwind mouseover crash reported on Bronzebeard while hovering city guards and other learned tooltip targets: `Questie-X\\Modules\\Tooltips\\Tooltip.lua:240: attempt to index local 'objList' (a number value)`.
|
||||
@@ -15,7 +33,7 @@
|
||||
- **Root Cause (Player Position)**: `HBD:GetPlayerWorldPosition()` returns a non-nil value on Sunstrider Isle, but those coords are Eastern Kingdoms world position (wrong), not Sunstrider's actual position. Because a non-nil value is returned, the fallback chain never fires. The arrow then calculates distance using wrong player coords vs correct target coords, giving a wildly incorrect distance.
|
||||
- **Root Cause (Ghost Map Player Position)**: `C_Map.GetPlayerMapPosition(946, "player")` where `946` is Ascension's ghost/cosmic map for Sunstrider Isle returns `(0, 0)` because the ghost map has no valid coordinate data. The correct map for player position on Sunstrider is `1941` (Eversong Woods parent), which shares the same world coordinate space and returns valid zone coords.
|
||||
- **Root Cause (OnUpdate same-map check)**: `OnUpdate` was using `_ResolveArrowUiMapId(_arrow_playerUiMapId)` (which resolves 1241→1941) for the player-side uiMapId, making it equal to `target.uiMapId` (also 1941). This caused the same-map branch to fire, which then called `C_Map.GetPlayerMapPosition(_arrow_playerUiMapId)` with 946, getting `(0, 0)` and computing wrong distance.
|
||||
- **Fix — QuestieArrow.lua UpdateNearestTargets**: Added Sunstrider detection (`zoneId == 3430`) BEFORE calling `HBD:GetPlayerWorldPosition()` to force the C_Map fallback path. The fallback now calls `C_Map.GetPlayerMapPosition(1241, "player")` to get Sunstrider map-space coords, then converts through `HBD:GetWorldCoordinatesFromZone(..., 1941)` using Eversong Woods bounds — which share Sunstrider's world coordinate space.
|
||||
- **Fix — QuestieArrow.lua UpdateNearestTargets**: Added Sunstrider detection (`zoneId == 3430 OR 3431 OR uiMapId == 1241`) BEFORE calling `HBD:GetPlayerWorldPosition()` to force the C_Map fallback path. All 4 Sunstrider detection checks in QuestieArrow.lua now accept zoneId 3430 OR 3431 OR uiMapId 1241 (lines 354, 395, 747, 782), since `GetCurrentZoneId()` can return either 3430 or 3431 when the player is physically on uiMap 1241. The fallback now calls `C_Map.GetPlayerMapPosition(1241, "player")` to get Sunstrider map-space coords, then converts through `HBD:GetWorldCoordinatesFromZone(..., 1941)` using Eversong Woods bounds — which share Sunstrider's world coordinate space.
|
||||
- **Fix — QuestieArrow.lua OnUpdate same-map branch**: Changed `C_Map.GetPlayerMapPosition(1941, "player")` for player zone coords (not `_arrow_playerUiMapId` which is 946). Normalizes both `playerUiMapId` and `targetUiMapId` through `_ResolveArrowUiMapId()` before same-map comparison. Uses `zoneScale = 13.53` yards/zone-unit for distance. Declares `worldPlayerX/Y` before the if-else to prevent nil in debug prints.
|
||||
- **Debug Output**: Added debug prints showing UnitPosition vs HBD player coords comparison, raw vs resolved uiMapIds, branch selection, and per-frame distance calculation inputs.
|
||||
- **[Fix — Sunstrider Isle Map Pins Not Appearing]** Resolved quest objective icons (map pins) not appearing on the world map when zoomed into Sunstrider Isle (uiMapId 1241), even though they appeared correctly when zoomed out to Eversong Woods (uiMapId 1941).
|
||||
@@ -27,8 +45,12 @@
|
||||
- **[Fix — QuestieLearner Icon Preservation]** Preserved the learned objective icon when registering with the tooltip system so nameplates can render the correct learned slay/loot/talk marker. Previously the icon was always nil on fresh registration.
|
||||
- **[Fix — Override Data Invisible to GetNPC/GetObject/GetItem]** — CRITICAL bug where ALL override data (from wotlkNPCFixes, AscensionDB, and QuestieLearner) was silently ignored by the three entity getter functions. Root cause: GetNPC/GetObject/GetItem iterated `npcKeys/objectKeys/itemKeys` using string keys (e.g. override["spawns"]) but ALL override sources store data using numeric keys via the key constants (e.g. override[npcKeys.spawns] = override[7]). In Lua, t["spawns"] ≠ t[7]. Fix: Added `_MergeOverride(result, override, rawdata, keyMap)` helper that tries both string and numeric keys before falling back to rawdata. Applied to GetNPC, GetObject, and GetItem. This ensures AscensionDB spawn data, wotlkNPCFixes corrections, and QuestieLearner-learned spawns are all visible through the database API.
|
||||
- **[Fix — QuestieLearner Spawn Zone Tracking]** All LearnNPC call sites now explicitly pass zoneId as the spawnZoneId parameter, ensuring learned spawn data is stored under the correct areaId key (3430 for Eversong) instead of leaving it nil for the fallback GetZoneId() which previously returned the wrong uiMapId. Affected call sites: OnMouseoverUnit (passes areaId from l10n), OnQuestDetail, OnQuestComplete, OnQuestAccepted, OnQuestTurnedIn, and GOSSIP_SHOW handler. OnQuestComplete also now captures zoneId via GetZoneId() before using it.
|
||||
- **[Fix — Sunstrider Isle Zone Mapping]** Changed `areaIdToUiMapId[3430]` from 1241 to 1941. Both WotLKDB and AscensionDB store Eversong-wide coordinates under zone 3430; mapping 3430→1241 forced those coordinates onto the Sunstrider sub-map, placing pins in mountains. Now 3430→1941 renders pins correctly on Eversong, and ZONE_REDIRECT provides cross-visibility to Sunstrider (1241). Removed redundant `UiMapIdOverrides[1241]=3430`.
|
||||
- **[Fix — Sunstrider Map Pin Redirect]** All map pins targeting uiMapId 1241 (Sunstrider) now redirect to 1941 (Eversong) via `_ResolveMapUiMapId()` in QuestieMap.lua. This applies to DrawManualIcon, DrawWorldIcon, FindClosestStarter, GetNearestSpawn, and GetNearestQuestSpawn. Sunstrider pins render on the Eversong coordinate space and appear on the Sunstrider sub-map via HBD ZONE_REDIRECT visibility.
|
||||
- **[Fix — Sunstrider Isle Zone Mapping]** Changed `areaIdToUiMapId[3430]` from 1241 to 1941. Both WotLKDB and AscensionDB store Eversong-wide coordinates under zone 3430; mapping 3430→1241 forced those coordinates onto the Sunstrider sub-map, placing pins in mountains. Now 3430→1941 renders pins correctly on Eversong, and ZONE_REDIRECT provides cross-visibility to Sunstrider (1241). Removed redundant `UiMapIdOverrides[1241]=3430`. Additionally, removed the 1241→1941 redirect in `_ResolveMapUiMapId()` so pins targeting Sunstrider render natively using Ascension-calibrated bounds. Added `areaIdToUiMapId[1241] = 1241` mapping in zoneDB.lua so `DrawWorldIcon` can place pins directly on the Sunstrider sub-map.
|
||||
- **[Fix — Sunstrider Map Pin System]** Removed the 1241→1941 redirect in `_ResolveMapUiMapId()`. Pins on 1241 now render natively using Ascension-calibrated bounds instead of being forced to 1941's coordinate space. Added `areaIdToUiMapId[1241] = 1241` mapping in zoneDB.lua so `DrawWorldIcon` can place pins directly on the Sunstrider sub-map. HBD's ZONE_REDIRECT visibility (`ResolveZone(1241)=1941`) ensures pins on 1241 are also visible on the Eversong map.
|
||||
- **[Fix — Sunstrider zoneId 3431 Detection (QuestieArrow.lua)]** `GetCurrentZoneId()` returns 3431 (Eversong Woods) when the player is physically on uiMap 1241 (Sunstrider Isle), not 3430 as previously assumed. All 4 Sunstrider detection checks in QuestieArrow.lua now accept zoneId 3430 OR 3431 OR uiMapId 1241 (lines 354, 395, 747, 782). Without this fix, NONE of the Sunstrider coordinate overrides triggered, causing the arrow to compute player and target positions in different coordinate spaces (858 yard offset).
|
||||
- **[Fix — Arrow Rotation Direction (QuestieArrow.lua)]** WoW's `Texture:SetRotation(r)` rotates CW for positive r, NOT CCW as the code comment claimed. Changed `rotAngle = -relative` to `rotAngle = relative` (line 441). The arrow was rotating in the opposite direction of the target, pointing away instead of toward it.
|
||||
- **[Fix — Collection Function Distance Mismatch (QuestieArrow.lua)]** `_CollectFinisherSpawns` and `_CollectObjective` converted targets through 1941 (Eversong) bounds while player coordinates were in 1241 (Sunstrider) bounds. Added `sunOverride = (pMap == 1241)` variable and forced target conversion through 1241 bounds at all 4 conversion sites in both functions. Without this fix, sortedTargets showed dist=1261 instead of the correct ~48 yards.
|
||||
- **[Fix — NPC Spawn Zone for Sunstrider (AscensionDB)]** NPC 15281 (Lanthan Perilon) spawn data changed from zone 3430 to zone 1241. Coordinates gathered via `GetPlayerMapPosition` on uiMap 1241 are in 1241's normalized space, NOT 1941's. Using zone 3430 (→1941) produced world coordinates outside 1241's 0-1 range, making map pins invisible on the Sunstrider sub-map. HBD's ZONE_REDIRECT visibility (`ResolveZone(1241)=1941`) ensures pins on 1241 are also visible on the Eversong map.
|
||||
|
||||
### Notes
|
||||
|
||||
|
||||
+52
-27
@@ -304,33 +304,6 @@ end
|
||||
|
||||
QuestieCompat.C_Map = {
|
||||
GetPlayerMapPosition = function(uiMapID, unitToken)
|
||||
unitToken = unitToken or "player"
|
||||
|
||||
if uiMapID and QuestieCompat.UiMapData and QuestieCompat.UiMapData[uiMapID] then
|
||||
local originalMapAreaID = GetCurrentMapAreaID()
|
||||
local originalDungeonLevel = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() or 0
|
||||
local mapID = QuestieCompat.UiMapData[uiMapID].mapID
|
||||
local dungeonLevel = QuestieCompat.Round(math.mod(mapID, 1) * 10)
|
||||
|
||||
SetMapByID(math.floor(mapID) - 1)
|
||||
if dungeonLevel > 0 and SetDungeonMapLevel then
|
||||
SetDungeonMapLevel(dungeonLevel)
|
||||
end
|
||||
|
||||
local x, y = GetPlayerMapPosition(unitToken)
|
||||
|
||||
if originalMapAreaID and originalMapAreaID > 0 then
|
||||
SetMapByID(originalMapAreaID - 1)
|
||||
if originalDungeonLevel and originalDungeonLevel > 0 and SetDungeonMapLevel then
|
||||
SetDungeonMapLevel(originalDungeonLevel)
|
||||
end
|
||||
else
|
||||
SetMapToCurrentZone()
|
||||
end
|
||||
|
||||
return { uiMapID = uiMapID, x = x, y = y }, uiMapID
|
||||
end
|
||||
|
||||
return QuestieCompat.GetPlayerMapPosition()
|
||||
end,
|
||||
-- Returns map information.
|
||||
@@ -880,6 +853,58 @@ function QuestieCompat.GetFactionInfo(factionIndex)
|
||||
canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild, QuestieCompat.FactionId[name:trim()]
|
||||
end
|
||||
|
||||
-- Returns faction info by factionID.
|
||||
-- https://wowpedia.fandom.com/wiki/API_GetFactionInfoByID
|
||||
-- Patch 4.0.1 (Cataclysm): Added.
|
||||
-- On 3.3.5, this API does not exist, so we iterate GetFactionInfo indices and use
|
||||
-- our FactionId reverse lookup to match by ID.
|
||||
local _factionIdReverse = nil
|
||||
function QuestieCompat.GetFactionInfoByID(factionID)
|
||||
if not factionID then return nil end
|
||||
|
||||
-- Build reverse map once (id -> name) from QuestieCompat.FactionId (name -> id)
|
||||
if not _factionIdReverse then
|
||||
_factionIdReverse = {}
|
||||
for name, id in next, QuestieCompat.FactionId do
|
||||
_factionIdReverse[id] = name
|
||||
end
|
||||
end
|
||||
|
||||
-- Fast path: try reverse lookup from our hardcoded faction data
|
||||
local name = _factionIdReverse[factionID]
|
||||
if name then
|
||||
-- Find the faction in the reputation UI to get full info
|
||||
local numFactions = GetNumFactions()
|
||||
for i = 1, numFactions do
|
||||
local fName, description, standingId, bottomValue, topValue, earnedValue,
|
||||
atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(i)
|
||||
if fName and fName:trim() == name then
|
||||
return fName, description, standingId, bottomValue, topValue, earnedValue,
|
||||
atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild, factionID
|
||||
end
|
||||
end
|
||||
-- Faction is in our DB but not in the reputation UI yet — return name-only
|
||||
return name
|
||||
end
|
||||
|
||||
-- Slow path: iterate all visible factions and check their ID via our name->id table
|
||||
local numFactions = GetNumFactions()
|
||||
for i = 1, numFactions do
|
||||
local fName, description, standingId, bottomValue, topValue, earnedValue,
|
||||
atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(i)
|
||||
if fName then
|
||||
local fId = QuestieCompat.FactionId[fName:trim()]
|
||||
if fId == factionID then
|
||||
return fName, description, standingId, bottomValue, topValue, earnedValue,
|
||||
atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild, factionID
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Faction not found
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Returns true if the unit is a member of your party
|
||||
-- https://wowpedia.fandom.com/wiki/API_UnitInParty
|
||||
-- As of 2.0.3, UnitInParty("player") always returns 1, even when you are not in a party.
|
||||
|
||||
+6
-9
@@ -54,7 +54,7 @@ QuestieCompat.HBD = HBD
|
||||
-- ZONE_REDIRECT: used by ResolveZone() for visibility logic (isSameZoneSpace).
|
||||
local ZONE_REDIRECT = {
|
||||
[1241] = 1941, -- Sunstrider Isle -> Eversong Woods (shared visibility space)
|
||||
[946] = 1941, -- Ghost map -> Eversong Woods (shared visibility space)
|
||||
[946] = 1941, -- Ghost/transition map -> Eversong Woods (for Sunstrider loading)
|
||||
}
|
||||
|
||||
--- Resolve a zone ID through the redirect table.
|
||||
@@ -465,14 +465,11 @@ local function drawMinimapPin(pin, data)
|
||||
end
|
||||
|
||||
local function _GetEffectiveMinimapPlayerWorldPosition()
|
||||
if QuestieCompat and QuestieCompat.GetCurrentPlayerPosition and QuestieCompat.GetCalibratedPlayerPosition then
|
||||
local uiMapID = QuestieCompat.GetCurrentPlayerPosition()
|
||||
local worldX, worldY, instanceID = QuestieCompat.GetCalibratedPlayerPosition(uiMapID, nil, "player")
|
||||
if worldX and worldY then
|
||||
return worldX, worldY, instanceID
|
||||
end
|
||||
end
|
||||
|
||||
-- For minimap pins we MUST use HBD's native world coordinate space.
|
||||
-- Minimap pins store their positions in HBD world coords, so the player
|
||||
-- position must also be in HBD world coords. The calibrated pseudo-world
|
||||
-- space (used by the arrow) has a different origin/scale on Ascension and
|
||||
-- must never be mixed with minimap pin positions.
|
||||
return HBD:GetPlayerWorldPosition()
|
||||
end
|
||||
|
||||
|
||||
@@ -693,7 +693,9 @@ function QuestieDB.GetSuppressedNPCs(zoneId)
|
||||
local threshold = ld.settings.minConfidencePins or 2
|
||||
local npcId, entry = next(ld.npcs)
|
||||
while npcId do
|
||||
if entry.mc and entry.mc >= threshold and entry[7] and entry[7][zoneId] then
|
||||
-- Support both old format ([4]=spawns) and new format ([7]=spawns)
|
||||
local spawns = entry[7] or entry[4]
|
||||
if entry.mc and entry.mc >= threshold and spawns and spawns[zoneId] then
|
||||
suppressed[npcId] = true
|
||||
end
|
||||
npcId, entry = next(ld.npcs, npcId)
|
||||
@@ -713,7 +715,9 @@ function QuestieDB.GetSuppressedObjects(zoneId)
|
||||
local threshold = ld.settings.minConfidencePins or 2
|
||||
local objId, entry = next(ld.objects)
|
||||
while objId do
|
||||
if entry.mc and entry.mc >= threshold and entry[4] and entry[4][zoneId] then
|
||||
-- Support both old format ([4]=spawns) and new format ([7]=spawns)
|
||||
local spawns = entry[4] or entry[7]
|
||||
if entry.mc and entry.mc >= threshold and spawns and spawns[zoneId] then
|
||||
suppressed[objId] = true
|
||||
end
|
||||
objId, entry = next(ld.objects, objId)
|
||||
|
||||
@@ -62,25 +62,38 @@ local UiMapIdOverrides = {
|
||||
-- Sunstrider Isle overrides (separate from Northshire since they're different Ascension realms)
|
||||
ZoneDB.private.uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId or {}
|
||||
ZoneDB.private.uiMapIdToAreaId[1241] = 3430
|
||||
ZoneDB.private.uiMapIdToAreaId[946] = 3430 -- Ghost map for Sunstrider Isle (different zone than Northshire's 946)
|
||||
-- 946 is a ghost/transition map. On Horde (Sunstrider), it resolves to areaId 3430.
|
||||
-- On Alliance (Northshire), it resolves to areaId 12 (Elwynn). The AscensionDB
|
||||
-- zone table handles both via runtime loading; we set the default here for
|
||||
-- the Sunstrider case since that's where pins break without it.
|
||||
ZoneDB.private.uiMapIdToAreaId[946] = 3430
|
||||
-- Reverse mapping: areaId → uiMapId for GetUiMapIdByAreaId lookups.
|
||||
-- Northshire Valley (areaId 668) uses uiMapId 1238.
|
||||
ZoneDB.private.areaIdToUiMapId[668] = 1238
|
||||
areaIdToUiMapId[668] = 1238
|
||||
-- Eversong Woods (areaId 3430) maps to the Eversong Woods map (uiMapId 1941).
|
||||
-- Sunstrider Isle (uiMapId 1241) is a child map that shares Eversong's coordinate space.
|
||||
-- Pins for zone 3430 render on map 1941 and appear on map 1241 via ZONE_REDIRECT visibility.
|
||||
-- Sunstrider Isle (areaId 3431) is a subzone of Eversong Woods — same map (1941).
|
||||
-- Sunstrider Isle (uiMapId 1241) is a child map of Eversong Woods.
|
||||
-- Pins for zones 3430 and 3431 render on map 1941 (Eversong).
|
||||
-- Pins for zone 1241 render on map 1241 (Sunstrider) using Ascension-calibrated bounds.
|
||||
ZoneDB.private.areaIdToUiMapId[3430] = 1941
|
||||
areaIdToUiMapId[3430] = 1941
|
||||
ZoneDB.private.areaIdToUiMapId[3431] = 1941
|
||||
areaIdToUiMapId[3431] = 1941
|
||||
-- Allow drawing pins directly on Sunstrider Isle (uiMapId 1241) via areaId 1241.
|
||||
ZoneDB.private.areaIdToUiMapId[1241] = 1241
|
||||
areaIdToUiMapId[1241] = 1241
|
||||
-- Also populate the cache so the fast path works without a lazy lookup.
|
||||
if uiMapIdToAreaIdCache[1238] == nil then
|
||||
uiMapIdToAreaIdCache[1238] = 668
|
||||
end
|
||||
-- Sunstrider Isle uiMapId 1241 → areaId 3431 (subzone, not parent 3430).
|
||||
-- Questie resolves 3431→3430 via GetParentZoneId() automatically when needed.
|
||||
if uiMapIdToAreaIdCache[1241] == nil then
|
||||
uiMapIdToAreaIdCache[1241] = 3430
|
||||
uiMapIdToAreaIdCache[1241] = 3431
|
||||
end
|
||||
if uiMapIdToAreaIdCache[946] == nil then
|
||||
uiMapIdToAreaIdCache[946] = 3430 -- Ghost map for Sunstrider Isle (different zone than Northshire's 946)
|
||||
uiMapIdToAreaIdCache[946] = 3430
|
||||
end
|
||||
local parentZoneToSubZone = {} -- Generated
|
||||
local zoneMap = {} -- Generated
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 256 KiB |
Binary file not shown.
+271
-267
@@ -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)
|
||||
@@ -336,211 +325,129 @@ arrowFrame:SetScript("OnUpdate", function(self)
|
||||
end
|
||||
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
|
||||
|
||||
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("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)))
|
||||
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()
|
||||
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)))
|
||||
-- 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
|
||||
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
|
||||
|
||||
-- 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
|
||||
-- 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
|
||||
|
||||
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)))
|
||||
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
|
||||
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
|
||||
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
|
||||
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)
|
||||
@@ -678,6 +586,7 @@ local function _CollectFinisherSpawns(finisher, quest)
|
||||
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,6 +822,51 @@ sortedTargets = {}
|
||||
playerInstance = playerInstance or 0
|
||||
end
|
||||
end
|
||||
-- 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 coordsOutsideEversong then
|
||||
local mapX, mapY = _GetSunstriderPlayerMapPosition(debugArrow)
|
||||
if debugArrow then
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
if needsPopulate then
|
||||
|
||||
-- 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
|
||||
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)))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
+220
-18
@@ -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
|
||||
-- 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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
+314
-22
@@ -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,15 +329,10 @@ 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.
|
||||
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
|
||||
local needsUnload = false
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
-- 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
|
||||
@@ -333,6 +345,22 @@ local function _InvalidateSpawnListsForNPC(npcId)
|
||||
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
|
||||
@@ -362,9 +390,34 @@ local function _InvalidateSpawnListsForNPC(npcId)
|
||||
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 then
|
||||
local needsUnload = false
|
||||
-- Scan standard Objectives
|
||||
if quest.Objectives then
|
||||
for _, objective in pairs(quest.Objectives) do
|
||||
if _TryInvalidateObjective(objective, npcId, quest) then
|
||||
needsUnload = true
|
||||
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
|
||||
end
|
||||
end
|
||||
if needsUnload then
|
||||
-- Also purge QuestieMap's quest frame registry so no stale refs remain.
|
||||
if QuestieMap and QuestieMap.UnloadQuestFrames 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
|
||||
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)
|
||||
end
|
||||
_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
|
||||
|
||||
@@ -31,24 +31,28 @@ 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
|
||||
-- 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 = GetQuestLogTitle(i)
|
||||
local _, _, _, isHeader, _, _, _, qid = GetQuestLogTitleCompat(i)
|
||||
qid = tonumber(qid)
|
||||
if (not isHeader) and qid and qid == questId then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
@@ -223,11 +227,17 @@ function MapIconTooltip:Show()
|
||||
for icon, _ in next, HBDPins.activeMinimapPins do
|
||||
handleMapIcon(icon)
|
||||
end
|
||||
else
|
||||
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
|
||||
|
||||
Tooltip.npcAndObjectOrder = npcAndObjectOrder
|
||||
Tooltip.questOrder = questOrder
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -220,7 +220,7 @@ If your server uses non-standard map data, enable **Options → Advanced → Use
|
||||
- Fixed arrow pointing to previously completed objective locations instead of the current finisher.
|
||||
- Fixed nil error in `_CollectObjective` when processing incomplete quests.
|
||||
- Fixed arrow direction for quests that require speaking to an NPC as a prerequisite step.
|
||||
- **Sunstrider Isle (Ascension starting zone)**: Resolved arrow not appearing when the world map is closed. `C_Map.GetBestMapForUnit("player")` returns a ghost/loading map uiMapId (946) instead of Sunstrider Isle's real uiMapId (1241) with the map closed. Added `UiMapIdOverrides` entries for both 946 and 1241 mapping to Sunstrider Isle's areaId (3430). Updated the arrow's `UpdateNearestTargets` fallback to use `ZoneDB` lookups when the ghost map is detected, ensuring the arrow gets real world coordinates regardless of map state.
|
||||
- **Sunstrider Isle (Ascension starting zone)**: Resolved arrow distance, direction, and map pin issues on Sunstrider Isle. `GetCurrentZoneId()` can return 3430 OR 3431 when the player is on uiMap 1241 — all 4 Sunstrider detection checks now accept both zoneIds plus uiMapId 1241. Fixed arrow rotation direction (`SetRotation` is CW-positive, not CCW). Fixed collection function distance mismatch where targets were converted through 1941 (Eversong) bounds while player coords were in 1241 bounds. NPC 15281 spawn zone corrected from 3430 to 1241 so coords land in Sunstrider's normalized space. Removed the 1241→1941 redirect in `_ResolveMapUiMapId()`; pins on 1241 now render natively via `areaIdToUiMapId[1241] = 1241`.
|
||||
|
||||
### Nameplates
|
||||
|
||||
|
||||
+7
-2
@@ -193,7 +193,7 @@
|
||||
<li><strong>Root Cause (Player Position)</strong>: <code>HBD:GetPlayerWorldPosition()</code> returns a non-nil value on Sunstrider Isle, but those coords are Eastern Kingdoms world position (wrong), not Sunstrider's actual position. Because a non-nil value is returned, the fallback chain never fires. The arrow then calculates distance using wrong player coords vs correct target coords, giving a wildly incorrect distance.</li>
|
||||
<li><strong>Root Cause (Ghost Map Player Position)</strong>: <code>C_Map.GetPlayerMapPosition(946, "player")</code> where <code>946</code> is Ascension's ghost/cosmic map for Sunstrider Isle returns <code>(0, 0)</code> because the ghost map has no valid coordinate data. The correct map for player position on Sunstrider is <code>1941</code> (Eversong Woods parent), which shares the same world coordinate space and returns valid zone coords.</li>
|
||||
<li><strong>Root Cause (OnUpdate same-map check)</strong>: <code>OnUpdate</code> was using <code>_ResolveArrowUiMapId(_arrow_playerUiMapId)</code> (which resolves 1241→1941) for the player-side uiMapId, making it equal to <code>target.uiMapId</code> (also 1941). This caused the same-map branch to fire, which then called <code>C_Map.GetPlayerMapPosition(_arrow_playerUiMapId)</code> with 946, getting <code>(0, 0)</code> and computing wrong distance.</li>
|
||||
<li><strong>Fix — QuestieArrow.lua UpdateNearestTargets</strong>: Added Sunstrider detection (<code>zoneId == 3430</code>) <em>before</em> calling <code>HBD:GetPlayerWorldPosition()</code> to force the C_Map fallback path. The fallback now calls <code>C_Map.GetPlayerMapPosition(1241, "player")</code> to get Sunstrider map-space coords, then converts through <code>HBD:GetWorldCoordinatesFromZone(..., 1941)</code> using Eversong Woods bounds — which share Sunstrider's world coordinate space.</li>
|
||||
<li><strong>Fix — QuestieArrow.lua UpdateNearestTargets</strong>: Added Sunstrider detection (<code>zoneId == 3430 OR 3431 OR uiMapId == 1241</code>) <em>before</em> calling <code>HBD:GetPlayerWorldPosition()</code> to force the C_Map fallback path. All 4 Sunstrider detection checks in QuestieArrow.lua now accept zoneId 3430 OR 3431 OR uiMapId 1241 (lines 354, 395, 747, 782), since <code>GetCurrentZoneId()</code> can return either 3430 or 3431 when the player is physically on uiMap 1241. The fallback now calls <code>C_Map.GetPlayerMapPosition(1241, "player")</code> to get Sunstrider map-space coords, then converts through <code>HBD:GetWorldCoordinatesFromZone(..., 1941)</code> using Eversong Woods bounds — which share Sunstrider's world coordinate space.</li>
|
||||
<li><strong>Fix — QuestieArrow.lua OnUpdate same-map branch</strong>: Changed <code>C_Map.GetPlayerMapPosition(1941, "player")</code> for player zone coords (not <code>_arrow_playerUiMapId</code> which is 946). Normalizes both <code>playerUiMapId</code> and <code>targetUiMapId</code> through <code>_ResolveArrowUiMapId()</code> before same-map comparison. Uses <code>zoneScale = 13.53</code> yards/zone-unit for distance. Declares <code>worldPlayerX/Y</code> before the if-else to prevent nil in debug prints.</li>
|
||||
<li><strong>Debug Output</strong>: Added debug prints showing UnitPosition vs HBD player coords comparison, raw vs resolved uiMapIds, branch selection, and per-frame distance calculation inputs.</li>
|
||||
</ul>
|
||||
@@ -203,9 +203,14 @@
|
||||
<li><strong>Root Cause</strong>: <code>HBDPins:HandlePin</code> (<code>HereBeDragons-Pins-2.0:424</code>) has an early-return guard: <code>if not HBD.mapData[uiMapID] then return end</code>. When the player zooms into Sunstrider Isle, <code>uiMapID</code> is 1241, but <code>HBD.mapData[1241]</code> is nil — no mapData entry existed for Sunstrider's custom child map. The icon was silently dropped before any coordinate conversion occurred.</li>
|
||||
<li><strong>Fix — Compat/HBD.lua</strong>: Added <code>mapData[1241] = mapData[1941]</code> alias and <code>mapData[946] = mapData[1941]</code> alias. Sunstrider Isle (1241) and its ghost map (946) share Eversong Woods' (1941) world coordinate space for these conversions.</li>
|
||||
<li><strong>Fix — HBD fallback loading</strong>: Added a lazy fallback to the real HBD library's <code>mapData</code> for maps not present in Questie's compat table, so custom/private-server maps can still resolve world and zone coordinates when <code>QuestieCompat</code> lacks a local entry.</li>
|
||||
<li><strong>Fix — Modules/Map/QuestieMap.lua</strong>: Added <code>_ResolveMapUiMapId()</code> helper (1241→1941) applied across <code>FadeLogic</code>, <code>FindClosestStarter</code>, <code>GetNearestSpawn</code>, and <code>GetNearestQuestSpawn</code>. <code>DrawWorldIcon</code> and <code>DrawManualIcon</code> now also store and render Sunstrider map icons against the resolved parent map coordinate space.</li>
|
||||
<li><strong>Fix — Modules/Map/QuestieMap.lua</strong>: Originally added <code>_ResolveMapUiMapId()</code> helper (1241→1941) to redirect Sunstrider pins to Eversong's coordinate space. This redirect was later <strong>removed</strong> so pins on 1241 render natively using Ascension-calibrated bounds. Added <code>areaIdToUiMapId[1241] = 1241</code> mapping in zoneDB.lua so <code>DrawWorldIcon</code> can place pins directly on the Sunstrider sub-map. HBD's ZONE_REDIRECT visibility (<code>ResolveZone(1241)=1941</code>) ensures pins on 1241 are also visible on the Eversong map.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>[Fix — Sunstrider zoneId 3431 Detection]</strong> <code>GetCurrentZoneId()</code> returns 3431 (Eversong Woods) when the player is physically on uiMap 1241 (Sunstrider Isle), not 3430 as previously assumed. All 4 Sunstrider detection checks in QuestieArrow.lua now accept zoneId 3430 OR 3431 OR uiMapId 1241 (lines 354, 395, 747, 782). Without this fix, NONE of the Sunstrider coordinate overrides triggered, causing the arrow to compute player and target positions in different coordinate spaces (858 yard offset).</li>
|
||||
<li><strong>[Fix — Arrow Rotation Direction]</strong> WoW's <code>Texture:SetRotation(r)</code> rotates CW for positive r, NOT CCW as the code comment claimed. Changed <code>rotAngle = -relative</code> to <code>rotAngle = relative</code> (line 441). The arrow was rotating in the opposite direction of the target, pointing away instead of toward it.</li>
|
||||
<li><strong>[Fix — Collection Function Distance Mismatch]</strong> <code>_CollectFinisherSpawns</code> and <code>_CollectObjective</code> converted targets through 1941 (Eversong) bounds while player coordinates were in 1241 (Sunstrider) bounds. Added <code>sunOverride = (pMap == 1241)</code> variable and forced target conversion through 1241 bounds at all 4 conversion sites in both functions. Without this fix, sortedTargets showed dist=1261 instead of the correct ~48 yards.</li>
|
||||
<li><strong>[Fix — NPC Spawn Zone for Sunstrider]</strong> NPC 15281 (Lanthan Perilon) spawn data changed from zone 3430 to zone 1241. Coordinates gathered via <code>GetPlayerMapPosition</code> on uiMap 1241 are in 1241's normalized space, NOT 1941's. Using zone 3430 (→1941) produced world coordinates outside 1241's 0-1 range, making map pins invisible on the Sunstrider sub-map.</li>
|
||||
<li><strong>[Fix — Sunstrider Map Pin System]</strong> Removed the 1241→1941 redirect in <code>_ResolveMapUiMapId()</code>. Pins on 1241 now render natively using Ascension-calibrated bounds instead of being forced to 1941's coordinate space. Added <code>areaIdToUiMapId[1241] = 1241</code> mapping in zoneDB.lua so <code>DrawWorldIcon</code> can place pins directly on the Sunstrider sub-map.</li>
|
||||
<li><strong>[Fix — Northshire Valley UiMapData Registration]</strong> Added explicit <code>QuestieCompat.UiMapData[1238]</code> for Northshire Valley so custom/private-server zone lookups have concrete geometry for the child map instead of relying on incomplete parent fallbacks.</li>
|
||||
<li><strong>[Fix — QuestieLearner Icon Preservation]</strong> Preserved the learned objective icon when registering with the tooltip system so nameplates can render the correct learned slay/loot/talk marker. Previously the icon was always nil on fresh registration.</li>
|
||||
<li><strong>[Notes — Failed Approaches Documented]</strong> The unreleased notes now explicitly capture the Sunstrider approaches that did <em>not</em> work:
|
||||
|
||||
+6
-6
@@ -224,8 +224,8 @@
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h4>Sunstrider Coordinate Normalization</h4>
|
||||
<p>Arrow and world-map code now normalize Sunstrider's child map <code>1241</code>, ghost map <code>946</code>, and parent Eversong map <code>1941</code> into a consistent coordinate path so distance, direction, and icon placement all use the same world space.</p>
|
||||
<h4>Sunstrider Coordinate Normalization (Final)</h4>
|
||||
<p>Arrow and world-map code now normalize Sunstrider's child map <code>1241</code>, ghost map <code>946</code>, and parent Eversong map <code>1941</code> into a consistent coordinate path. <code>GetCurrentZoneId()</code> can return 3430 <em>or</em> 3431 on Sunstrider — all 4 detection checks now accept both values plus uiMapId 1241. The 1241→1941 redirect in <code>_ResolveMapUiMapId()</code> was removed; pins render natively on 1241 via <code>areaIdToUiMapId[1241] = 1241</code>.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>Learned Tooltip Schema Fix</h4>
|
||||
@@ -235,12 +235,12 @@
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h4>Custom Map Data Aliases</h4>
|
||||
<p><code>Compat/HBD.lua</code> now aliases Sunstrider map data (<code>1241</code> and <code>946</code>) to Eversong (<code>1941</code>) and can fall back to the real HBD library's <code>mapData</code> for maps not present in Questie's local compat table.</p>
|
||||
<h4>Arrow Rotation & Collection Fixes</h4>
|
||||
<p>Fixed arrow rotation direction (<code>SetRotation</code> is CW-positive, not CCW) and collection function distance mismatch where targets were converted through 1941 bounds while player coords were in 1241 bounds, causing 1261-yard errors instead of ~48 yards.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>QuestieLearner Icon Preservation</h4>
|
||||
<p>Learned objectives now preserve the resolved quest-log icon during immediate tooltip registration so nameplates can render the correct learned slay/loot/talk marker instead of a missing icon state.</p>
|
||||
<h4>NPC Spawn Zone & Native Map Pins</h4>
|
||||
<p>NPC 15281 (Lanthan Perilon) spawn zone corrected from 3430 to 1241 so coords land in Sunstrider's 0-1 space. Pins on 1241 now render natively using Ascension-calibrated bounds with <code>areaIdToUiMapId[1241] = 1241</code> instead of redirecting to 1941.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ NPC spawn data: zone 3430 (Eversong) → GetUiMapIdByAreaId(3430) → uiMapId 19
|
||||
| Lookup | From | To | Purpose |
|
||||
|--------|------|----|---------|
|
||||
| `GetUiMapIdByAreaId(3430)` | areaId 3430 | uiMapId 1941 | Pin placement on Eversong map |
|
||||
| `GetAreaIdByUiMapId(1241)` | uiMapId 1241 | areaId 3430 | Zone ID for spawn data keys |
|
||||
| `GetUiMapIdByAreaId(3431)` | areaId 3431 | uiMapId 1941 | Pin placement on Eversong map (Sunstrider subzone) |
|
||||
| `GetAreaIdByUiMapId(1241)` | uiMapId 1241 | areaId 3431 | Zone ID for spawn data keys (Sunstrider subzone) |
|
||||
| `_ResolveMapUiMapId(1241)` | uiMapId 1241 | uiMapId 1941 | Normalize pin rendering |
|
||||
| `_ResolveArrowUiMapId(1241)` | uiMapId 1241 | uiMapId 1941 | Arrow math normalization |
|
||||
| `ZONE_REDIRECT[1241]` | uiMapId 1241 | uiMapId 1941 | Cross-visibility |
|
||||
@@ -32,7 +33,7 @@ percentages (e.g., NPC 15278 at 38.02%, 21.01%). These render correctly on
|
||||
the Eversong map (1941). Mapping 3430→1241 would place Eversong-wide
|
||||
coordinates on the Sunstrider sub-map, producing wrong positions.
|
||||
|
||||
Pins from zone 3430 render on uiMapId 1941 (Eversong) and appear on uiMapId
|
||||
Pins from zones 3430 and 3431 render on uiMapId 1941 (Eversong) and appear on uiMapId
|
||||
1241 (Sunstrider) via ZONE_REDIRECT visibility, which works because
|
||||
`ResolveZone(1241) == ResolveZone(1941) == 1941`.
|
||||
|
||||
@@ -54,6 +55,7 @@ Pins from zone 3430 render on uiMapId 1941 (Eversong) and appear on uiMapId
|
||||
- `_ResolveArrowUiMapId(1241)` → 1941
|
||||
- `_ResolveArrowUiMapId(946)` → 1941
|
||||
- Comment updated to match new approach
|
||||
- **Arrow rendering**: Replaced sprite sheet (108-frame) with single-frame texture + `SetRotation(-angle)` for infinite angular resolution and zero jitter. Arrow texture is now X-PLORE's `XPArrow4.tga` (256×256 RGBA, arrow pointing UP centered at 128,128). Removed all `ARROW_SHEET_*`, `ARROW_CELL_*`, UV math, and `SetTexCoord` cell selection logic. Arrow uses `ARROW_DISPLAY_SIZE=96` for on-screen pixel size and `SetPoint("CENTER")` anchor for clean rotation pivot. `SetVertexColor(1,1,1)` preserves original blue color.
|
||||
|
||||
### Modules/QuestieLearner.lua
|
||||
- `GetZoneId()`: Returns areaId via `ZoneDB:GetAreaIdByUiMapId(uiMapId)` with fallback
|
||||
@@ -234,3 +236,71 @@ Expected: `ResolveZone(1241)= 1941 ResolveZone(946)= 1941`
|
||||
- [ ] After 1+ kill, verify learned pin auto-appears at correct position
|
||||
- [ ] Verify Eversong Woods NPCs NOT on Sunstrider show correctly on Eversong map
|
||||
- [ ] Check no regressions on other zones
|
||||
- [ ] **Complete-abandon-reaccept cycle**: Complete a quest's objectives → abandon → re-accept → verify pins appear for fresh 0/X objectives
|
||||
- [ ] **Arrow rendering**: Verify arrow shows a single blue arrow (not sprite sheet), smooth rotation with no visible frame transitions, correct direction toward quest objectives, and correct display size
|
||||
- [ ] **Learner data in arrow**: Verify arrow targets point to QuestieLearner-injected NPC spawn locations correctly
|
||||
- [ ] **QUEST_TURNED_IN auto-complete**: Verify quests that auto-complete on turn-in clean up state properly (no orphan pins)
|
||||
|
||||
## Complete-Abandon-Reaccept Pin Lifecycle Fix (Session 2026-05-17)
|
||||
|
||||
### Bug Chain
|
||||
|
||||
Four interacting bugs prevented map pins and GPS arrow from reappearing after
|
||||
completing quest objectives, abandoning the quest, and re-accepting it:
|
||||
|
||||
1. **MarkQuestAsAbandoned `objectivesWereComplete` path** — called `CompleteQuest`
|
||||
without clearing `quest.Objectives`, `quest.WasComplete`, or `quest.isComplete`.
|
||||
Stale `Completed=true` + `isUpdated=true` flags caused `PopulateObjectiveNotes`
|
||||
to skip drawing pins on re-accept.
|
||||
|
||||
2. **CompleteQuest** — did not clear `quest.Objectives` (unlike `AbandonedQuest`
|
||||
which does). Now adds `quest.Objectives = {}` with type guard as defense-in-depth.
|
||||
|
||||
3. **QUEST_TURNED_IN dead code** — `questLog[questId] = {}` wiped state before the
|
||||
QUEST_TURNED_IN state check could read it, making auto-complete cleanup unreachable.
|
||||
Moved the check before the wipe.
|
||||
|
||||
4. **AcceptQuest reset** — added `SetObjectivesDirty(questId)` in the re-accept block
|
||||
to ensure `isUpdated` flags are reset even if stale objectives survive.
|
||||
|
||||
5. **Arrow spawnList gap** — `_CollectObjective` silently skipped objectives with
|
||||
nil/empty `spawnList`. After quest re-accept, `PopulateQuestLogInfo` creates
|
||||
objectives without `spawnList`; `PopulateObjectiveNotes` builds it later in the
|
||||
TaskQueue. Added `QuestieQuest:BuildObjectiveSpawnList(objective, objectiveData)`
|
||||
public API that lazily builds `spawnList` from `objectiveSpawnListCallTable` handlers.
|
||||
The arrow now calls this when `spawnList` is missing.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- **QuestEventHandler.lua** (~line 443-461): MarkQuestAsAbandoned — clear stale
|
||||
objectives/flags + SetObjectivesDirty before CompleteQuest
|
||||
- **QuestEventHandler.lua** (~line 233): QUEST_TURNED_IN — moved state check before
|
||||
questLog[questId] = {} wipe
|
||||
- **QuestieQuest.lua** (~line 492): AcceptQuest reset — added SetObjectivesDirty(questId)
|
||||
- **QuestieQuest.lua** (~line 583): CompleteQuest — added `quest.Objectives = {}`
|
||||
- **QuestieQuest.lua** (~line 1996-2018): New `BuildObjectiveSpawnList` public API
|
||||
- **QuestieArrow.lua** (~line 726-760): _CollectObjective — lazy spawnList building
|
||||
via `QuestieQuest:BuildObjectiveSpawnList()`
|
||||
## UpdateQuest Pin Refresher Fallback (Session 2026-05-17)
|
||||
|
||||
### Problem
|
||||
After reload or abandon-reaccept, incomplete quests sometimes have no objective pins
|
||||
on the map even though they are in the quest log. This happens when:
|
||||
1. `PopulateQuestLogInfo` hits a cache miss and leaves `quest.Objectives` empty.
|
||||
2. `UnloadQuestFrames` removes map frames but `AlreadySpawned` is not cleared,
|
||||
so `_DetermineIconsToDraw` skips recreating icons on the next refresh.
|
||||
|
||||
### Fix
|
||||
Added a robustness fallback in `QuestieQuest:UpdateQuest()` (incomplete branch):
|
||||
- If `quest.Objectives` is empty → re-call `PopulateQuestLogInfo()`, then
|
||||
`PopulateObjectiveNotes()` if objectives were created.
|
||||
- If objectives exist but `QuestieMap.questIdFrames[questId]` is nil → clear
|
||||
`objective.AlreadySpawned = {}` for all objectives, then re-call
|
||||
`PopulateObjectiveNotes()` to force icon recreation.
|
||||
|
||||
This ensures that ANY incomplete quest in the log gets its pins re-added on the
|
||||
next periodic refresh (30s) or `QUEST_LOG_UPDATE` if they were lost.
|
||||
|
||||
### Files Changed
|
||||
- **QuestieQuest.lua** (~line 833): Added `hasObjectives` / `hasFrames` fallback
|
||||
in the `isComplete == 0` branch of `UpdateQuest`.
|
||||
|
||||
Reference in New Issue
Block a user