chore: release 1.6.3

This commit is contained in:
Xurkon
2026-06-02 11:15:45 -05:00
parent d3c59e0ce8
commit fe1f79cd2b
26 changed files with 1085 additions and 824 deletions
+13
View File
@@ -4,6 +4,14 @@
# Exclude dev/meta files from release archives # Exclude dev/meta files from release archives
.gitattributes export-ignore .gitattributes export-ignore
.gitignore export-ignore .gitignore export-ignore
.busted export-ignore
.luarc.json export-ignore
selene.toml export-ignore
.codex/ export-ignore
.codex/skills/ export-ignore
.codex-plugin/ export-ignore
handoff.md export-ignore
QUESTIE-LEARNER-HANDOFF.md export-ignore
.kilocode/ export-ignore .kilocode/ export-ignore
.idea/ export-ignore .idea/ export-ignore
.vscode/ export-ignore .vscode/ export-ignore
@@ -11,6 +19,11 @@
*.zip export-ignore *.zip export-ignore
*.log export-ignore *.log export-ignore
*.tmp export-ignore *.tmp export-ignore
temp_*.png export-ignore
docs/arrow-options.png export-ignore
docs/PROGRESS.md export-ignore
docs/sunstrider*.md export-ignore
docs/testing*.md export-ignore
*.bak export-ignore *.bak export-ignore
*.old export-ignore *.old export-ignore
*.py export-ignore *.py export-ignore
+5
View File
@@ -8,15 +8,19 @@ tests/
Modules/*_spec.lua Modules/*_spec.lua
# Dev notes and session artifacts # Dev notes and session artifacts
docs/arrow-options.png
docs/sunstrider-pin-fix.md docs/sunstrider-pin-fix.md
docs/sunstrider-coordinate-collection.md docs/sunstrider-coordinate-collection.md
docs/PROGRESS.md docs/PROGRESS.md
docs/testing-macros.md docs/testing-macros.md
docs/sunstrider*.md
docs/testing*.md
handoff.md handoff.md
QUESTIE-LEARNER-HANDOFF.md QUESTIE-LEARNER-HANDOFF.md
# Generated/lock files # Generated/lock files
skills-lock.json skills-lock.json
*.zip
# CodexBot artifacts # CodexBot artifacts
.codex/ .codex/
@@ -34,6 +38,7 @@ scratch/
.busted .busted
.luarc.json .luarc.json
selene.toml selene.toml
temp_*.png
Compat/Debug.lua Compat/Debug.lua
Modules/Arrow/QuestieArrow.lua.bak4 Modules/Arrow/QuestieArrow.lua.bak4
Modules/Arrow/QuestieArrow_HEAD.lua Modules/Arrow/QuestieArrow_HEAD.lua
+9 -2
View File
@@ -4,7 +4,14 @@
### Bug Fixes ### Bug Fixes
- **[Fix — Minimap Pin Drift: Live View-Radius API + Corrected Pixel Math]** Resolved the long-running minimap pin drift bug where quest pins appeared to "follow" the player or jump on every frame. Pins now stay anchored to their world positions across all minimap zoom levels (0-5+) and across both Stock UI and ElvUI. - **[Docs - Release Metadata Sync]** Updated the README badge, documentation version badges, and in-game addon version to `v1.6.3`, then tightened the release filters so handoff notes, local dev settings, and other workspace-only artifacts stay out of the exported release archive.
- **[Fix - Arrow Asset Regression]** Restored the default `Arrow1` asset from the original `XPArrow4.png` source, renamed the bundled image arrows to `Arrow1` through `Arrow4`, regenerated the bundled arrow manifest from the actual image data, and added a busted regression test so `arrowold` remains the only bundled sprite sheet.
- **[Fix - Arrow UI / Attachment Redesign]** Reworked the Arrow tab so the arrow and objective text can be detached, reattached, locked independently, and reset independently. Added an attached-gap slider, objective transparency slider, distance-unit selector, larger font sizing, and drop-in preview support for the generated arrow swatches.
- **[Fix - Arrow Asset Size Reduction]** Rewrote the bundled image arrows to their visible bounds and saved them with TGA RLE compression, which keeps the same in-game appearance while reducing the arrow asset footprint dramatically.
- **[Fix - Arrow Texture Cache Bust]** Moved the default Arrow style onto the fresh `Icons\\Arrows\\Arrow1.tga` texture path so WoW stops reusing the old sheet data under the original filename.
- **[Fix - Arrow Live Texture Fallback]** Routed bundled image arrow rendering through the generated preview textures at runtime so image-based styles stay stable even if a source TGA has odd client-side rendering behavior. The sheet arrow remains on `arrowold` only.
- **[Fix - Arrow Sheet Guardrail]** Hardened the runtime arrow renderer so only `arrowold` and explicitly custom sheet uploads can enter sprite-sheet mode. All bundled image arrows are now forced down the image/rotation path even if an asset manifest entry gets out of sync.
- **[Fix — Minimap Pin Drift: Live View-Radius API + Corrected Pixel Math]** Resolved the long-running minimap pin drift bug where quest pins appeared to "follow" the player or jump on every frame. This one took three weeks of iteration, including three false starts and one regression, before the live API path and pixel math finally lined up. Pins now stay anchored to their world positions across all minimap zoom levels (0-5+) and across both Stock UI and ElvUI.
- **Root Cause 1 — Hardcoded lookup table used on 3.3.5a**: `Compat/HBD.lua` was reading `mapRadius` from a hardcoded `minimap_size` lookup table calibrated for stock WoW zoom levels. The API check `C_Minimap and C_Minimap.GetViewRadius` evaluated to `nil` on 3.3.5a (and Ascension), so the broken lookup table was always used. The lookup value `minimap_size.outdoor[5] = 250` produced `mapRadius = 125`, but the actual live minimap view radius at zoom 5 is `116.67` yards (from `Minimap:GetViewRadius()`). This 6.7% error compounded across all pin offsets. - **Root Cause 1 — Hardcoded lookup table used on 3.3.5a**: `Compat/HBD.lua` was reading `mapRadius` from a hardcoded `minimap_size` lookup table calibrated for stock WoW zoom levels. The API check `C_Minimap and C_Minimap.GetViewRadius` evaluated to `nil` on 3.3.5a (and Ascension), so the broken lookup table was always used. The lookup value `minimap_size.outdoor[5] = 250` produced `mapRadius = 125`, but the actual live minimap view radius at zoom 5 is `116.67` yards (from `Minimap:GetViewRadius()`). This 6.7% error compounded across all pin offsets.
- **Root Cause 2 — Factor-of-2 in pixel math**: `minimapWidth` was computed as `(GetWidth() * mapRadius / 155.52) / 2`, mixing pixel-half-width with a yards-based scale factor. The `/ 155.52` constant was a hardcoded normalization that did not match the live API value. The math was self-inconsistent: ratio `minimapWidth / mapRadius` was `0.56` (off by ~7% from the correct `0.6`). - **Root Cause 2 — Factor-of-2 in pixel math**: `minimapWidth` was computed as `(GetWidth() * mapRadius / 155.52) / 2`, mixing pixel-half-width with a yards-based scale factor. The `/ 155.52` constant was a hardcoded normalization that did not match the live API value. The math was self-inconsistent: ratio `minimapWidth / mapRadius` was `0.56` (off by ~7% from the correct `0.6`).
- **Root Cause 3 — Scale not applied to pixel dimensions**: `minimapWidth` was based on `GetWidth()` alone, ignoring `GetScale()`. When UI scale changed, the pixel dimensions reported by `GetWidth()` would diverge from the actual on-screen size, while `mapRadius` (in yards) stayed fixed. This caused drift to worsen at higher zoom levels where the ratio was most sensitive. - **Root Cause 3 — Scale not applied to pixel dimensions**: `minimapWidth` was based on `GetWidth()` alone, ignoring `GetScale()`. When UI scale changed, the pixel dimensions reported by `GetWidth()` would diverge from the actual on-screen size, while `mapRadius` (in yards) stayed fixed. This caused drift to worsen at higher zoom levels where the ratio was most sensitive.
@@ -34,7 +41,7 @@
### Bug Fixes ### 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. - **[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. - **New arrow texture**: `Icons/arrow.tga` was set to 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. - **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)`. - **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. - **Arrow anchor**: Changed from `SetPoint("TOP")` to `SetPoint("CENTER")` so the rotation pivot aligns with the frame center.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
---@class QuestieArrowAssets
local QuestieArrowAssets = QuestieLoader:CreateModule("QuestieArrowAssets")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
QuestieArrowAssets.styles = {
["arrow1"] = {
label = "Arrow 1",
texture = "Icons\\Arrows\\Arrow1.tga",
preview = "Icons\\Arrows\\Arrow1_preview.tga",
mode = "image",
displayWidth = 77,
displayHeight = 96,
visualBottomInset = 0,
},
["arrow2"] = {
label = "Arrow 2",
texture = "Icons\\Arrows\\Arrow2.tga",
preview = "Icons\\Arrows\\Arrow2_preview.tga",
mode = "image",
displayWidth = 75,
displayHeight = 96,
visualBottomInset = 0,
},
["arrow3"] = {
label = "Arrow 3",
texture = "Icons\\Arrows\\Arrow3.tga",
preview = "Icons\\Arrows\\Arrow3_preview.tga",
mode = "image",
displayWidth = 64,
displayHeight = 96,
visualBottomInset = 0,
},
["arrow4"] = {
label = "Arrow 4",
texture = "Icons\\Arrows\\Arrow4.tga",
preview = "Icons\\Arrows\\Arrow4_preview.tga",
mode = "image",
displayWidth = 67,
displayHeight = 96,
visualBottomInset = 0,
},
["arrowold"] = {
label = "Arrow Old",
texture = "Icons\\Arrows\\arrowold.tga",
preview = "Icons\\Arrows\\arrowold_preview.tga",
mode = "sheet",
displayWidth = 56,
displayHeight = 42,
visualBottomInset = 0,
},
}
QuestieArrowAssets.order = {
"arrow1",
"arrow2",
"arrow3",
"arrow4",
"arrowold",
}
function QuestieArrowAssets:GetStyleData(key)
return self.styles[key]
end
function QuestieArrowAssets:GetStyleOptions()
local values = {}
local addonPath = (QuestieLib and QuestieLib.AddonPath) or ""
for _, key in ipairs(self.order) do
local style = self.styles[key]
if style then
values[key] = string.format('|T%s:32:32:0:0|t %s', addonPath .. style.preview, style.label)
end
end
return values
end
function QuestieArrowAssets:GetStyleOrder()
return self.order
end
function QuestieArrowAssets:GetStyles()
return self.styles
end
+226 -11
View File
@@ -3,6 +3,8 @@
------------------------- -------------------------
---@type QuestieOptions ---@type QuestieOptions
local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions") local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions")
---@type QuestieOptionsDefaults
local QuestieOptionsDefaults = QuestieLoader:ImportModule("QuestieOptionsDefaults")
---@type QuestieOptionsUtils ---@type QuestieOptionsUtils
local QuestieOptionsUtils = QuestieLoader:ImportModule("QuestieOptionsUtils") local QuestieOptionsUtils = QuestieLoader:ImportModule("QuestieOptionsUtils")
---@type QuestieArrow ---@type QuestieArrow
@@ -14,6 +16,14 @@ local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
local l10n = QuestieLoader:ImportModule("l10n") local l10n = QuestieLoader:ImportModule("l10n")
local SharedMedia = LibStub and LibStub("LibSharedMedia-3.0", true) local SharedMedia = LibStub and LibStub("LibSharedMedia-3.0", true)
local AceConfigRegistry = LibStub and LibStub("AceConfigRegistry-3.0", true)
local optionsDefaults = QuestieOptionsDefaults:Load()
local function RefreshOptions()
if AceConfigRegistry and AceConfigRegistry.NotifyChange then
AceConfigRegistry:NotifyChange("Questie")
end
end
-- Build expanded font list from SharedMedia + common WoW fonts -- Build expanded font list from SharedMedia + common WoW fonts
local function GetExpandedFontList() local function GetExpandedFontList()
@@ -82,6 +92,84 @@ function QuestieOptions.tabs.arrow:Initialize()
end, end,
}, },
arrow_spacer_1 = QuestieOptionsUtils:Spacer(3), arrow_spacer_1 = QuestieOptionsUtils:Spacer(3),
arrowStyle = {
type = "select",
order = 3.1,
width = 1.5,
values = function()
if QuestieArrow and QuestieArrow.GetArrowStyleOptions then
return QuestieArrow:GetArrowStyleOptions()
end
return {
arrow1 = "Arrow 1",
}
end,
sorting = function()
if QuestieArrow and QuestieArrow.GetArrowStyleOrder then
return QuestieArrow:GetArrowStyleOrder()
end
return { "arrow1", "custom" }
end,
style = 'dropdown',
name = function() return l10n("Arrow Style") end,
desc = function()
return l10n("Choose which arrow artwork Questie uses. Sprite sheets animate through directional frames; regular images rotate as a single texture.")
end,
get = function() return Questie.db.profile.arrowStyle or optionsDefaults.profile.arrowStyle end,
set = function(_, value)
Questie.db.profile.arrowStyle = value
if QuestieArrow and QuestieArrow.UpdateSettings then
QuestieArrow:UpdateSettings()
elseif QuestieArrow and QuestieArrow.Refresh then
QuestieArrow:Refresh()
end
RefreshOptions()
end,
},
arrowCustomTexture = {
type = "input",
order = 3.2,
width = 1.8,
hidden = function()
return Questie.db.profile.arrowStyle ~= "custom"
end,
name = function() return l10n("Custom Arrow File") end,
desc = function()
return l10n("Enter the filename of a .tga file placed in Icons\\Arrows. Only the file name is used, not the full path.")
end,
get = function()
return Questie.db.profile.arrowCustomTexture or ""
end,
set = function(_, value)
Questie.db.profile.arrowCustomTexture = value
if QuestieArrow and QuestieArrow.UpdateSettings then
QuestieArrow:UpdateSettings()
end
RefreshOptions()
end,
},
arrowCustomIsSheet = {
type = "toggle",
order = 3.3,
width = 1.4,
hidden = function()
return Questie.db.profile.arrowStyle ~= "custom"
end,
name = function() return l10n("Custom Is Sprite Sheet") end,
desc = function()
return l10n("Enable this only if your custom TGA is a directional sprite sheet.")
end,
get = function()
return Questie.db.profile.arrowCustomIsSheet == true
end,
set = function(_, value)
Questie.db.profile.arrowCustomIsSheet = value
if QuestieArrow and QuestieArrow.UpdateSettings then
QuestieArrow:UpdateSettings()
end
RefreshOptions()
end,
},
arrow_scale = { arrow_scale = {
type = "range", type = "range",
order = 4, order = 4,
@@ -89,7 +177,7 @@ function QuestieOptions.tabs.arrow:Initialize()
name = function() return l10n("Arrow Scale") end, name = function() return l10n("Arrow Scale") end,
desc = function() return l10n("Change the size of the arrow") end, desc = function() return l10n("Change the size of the arrow") end,
min = 0.5, min = 0.5,
max = 2.0, max = 4.0,
step = 0.05, step = 0.05,
get = function() return Questie.db.profile.arrowScale or 1 end, get = function() return Questie.db.profile.arrowScale or 1 end,
set = function(_, value) set = function(_, value)
@@ -99,6 +187,66 @@ function QuestieOptions.tabs.arrow:Initialize()
end end
end, end,
}, },
arrowLocked = {
type = "toggle",
order = 4.1,
width = 1.7,
name = function() return l10n("Lock Arrow Position") end,
desc = function() return l10n("Prevent the arrow frame from being moved independently.") end,
get = function() return Questie.db.profile.arrowLocked == true end,
set = function(_, value)
Questie.db.profile.arrowLocked = value
end,
},
arrowObjectiveLocked = {
type = "toggle",
order = 4.2,
width = 1.9,
name = function() return l10n("Lock Objective Position") end,
desc = function() return l10n("Prevent the objective text block from being moved independently.") end,
get = function() return Questie.db.profile.arrowObjectiveLocked == true end,
set = function(_, value)
Questie.db.profile.arrowObjectiveLocked = value
end,
},
arrowObjectiveAttached = {
type = "toggle",
order = 4.3,
width = 1.9,
name = function() return l10n("Attach Objective To Arrow") end,
desc = function() return l10n("Keep the objective block anchored to the arrow instead of moving it separately.") end,
get = function() return Questie.db.profile.arrowObjectiveAttached == true end,
set = function(_, value)
Questie.db.profile.arrowObjectiveAttached = value
if QuestieArrow then
if value and QuestieArrow.AttachObjectiveToArrow then
QuestieArrow:AttachObjectiveToArrow()
elseif not value and QuestieArrow.DetachObjectiveFromArrow then
QuestieArrow:DetachObjectiveFromArrow()
end
end
RefreshOptions()
end,
},
arrowObjectiveGap = {
type = "range",
order = 4.4,
width = 1.8,
name = function() return l10n("Attached Gap") end,
desc = function() return l10n("Control the spacing between the arrow and the attached objective block.") end,
min = 0,
max = 40,
step = 1,
get = function() return Questie.db.profile.arrowObjectiveGap or 10 end,
set = function(_, value)
Questie.db.profile.arrowObjectiveGap = value
if QuestieArrow and QuestieArrow.SetObjectiveGap then
QuestieArrow:SetObjectiveGap(value)
elseif QuestieArrow and QuestieArrow.Refresh then
QuestieArrow:Refresh()
end
end,
},
arrow_alpha = { arrow_alpha = {
type = "range", type = "range",
order = 5, order = 5,
@@ -116,6 +264,44 @@ function QuestieOptions.tabs.arrow:Initialize()
end end
end, end,
}, },
arrowObjectiveAlpha = {
type = "range",
order = 5.1,
width = 1.5,
name = function() return l10n("Objective Transparency") end,
desc = function() return l10n("Change the transparency of the objective text block") end,
min = 0.1,
max = 1.0,
step = 0.05,
get = function() return Questie.db.profile.arrowObjectiveAlpha or 1.0 end,
set = function(_, value)
Questie.db.profile.arrowObjectiveAlpha = value
if QuestieArrow and QuestieArrow.Refresh then
QuestieArrow:Refresh()
end
end,
},
arrowDistanceUnit = {
type = "select",
order = 5.2,
width = 1.5,
name = function() return l10n("Distance Units") end,
desc = function() return l10n("Choose the unit used for the distance label.") end,
values = function()
return {
yards = "Yards",
meters = "Meters",
feet = "Feet",
}
end,
get = function() return Questie.db.profile.arrowDistanceUnit or "yards" end,
set = function(_, value)
Questie.db.profile.arrowDistanceUnit = value
if QuestieArrow and QuestieArrow.Refresh then
QuestieArrow:Refresh()
end
end,
},
arrow_spacer_2 = QuestieOptionsUtils:Spacer(6), arrow_spacer_2 = QuestieOptionsUtils:Spacer(6),
arrowFont = { arrowFont = {
type = "select", type = "select",
@@ -140,7 +326,7 @@ function QuestieOptions.tabs.arrow:Initialize()
desc = function() return l10n("The font size used for the arrow distance and title text.") end, desc = function() return l10n("The font size used for the arrow distance and title text.") end,
width = "double", width = "double",
min = 8, min = 8,
max = 18, max = 30,
step = 1, step = 1,
get = function() return Questie.db.profile.arrowFontSize or 10 end, get = function() return Questie.db.profile.arrowFontSize or 10 end,
set = function(_, value) set = function(_, value)
@@ -150,10 +336,10 @@ function QuestieOptions.tabs.arrow:Initialize()
end end
end, end,
}, },
arrow_spacer_3 = QuestieOptionsUtils:Spacer(8), arrow_spacer_3 = QuestieOptionsUtils:Spacer(8.5),
autoTrackQuests = { autoTrackQuests = {
type = "toggle", type = "toggle",
order = 7, order = 9,
width = 1.5, width = 1.5,
name = function() return l10n("Auto-track Quests") end, name = function() return l10n("Auto-track Quests") end,
desc = function() return l10n("Automatically track all quests in your quest log. If disabled, only manually tracked quests will show on the arrow.") end, desc = function() return l10n("Automatically track all quests in your quest log. If disabled, only manually tracked quests will show on the arrow.") end,
@@ -168,10 +354,10 @@ function QuestieOptions.tabs.arrow:Initialize()
end end
end, end,
}, },
arrow_spacer_3 = QuestieOptionsUtils:Spacer(8), arrow_spacer_4 = QuestieOptionsUtils:Spacer(9.5),
resetArrowPosition = { resetArrowPosition = {
type = "execute", type = "execute",
order = 9, order = 10,
width = 1.0, width = 1.0,
name = function() return l10n("Reset Arrow Position") end, name = function() return l10n("Reset Arrow Position") end,
desc = function() return l10n("Reset the arrow position to the center of the screen") end, desc = function() return l10n("Reset the arrow position to the center of the screen") end,
@@ -182,10 +368,39 @@ function QuestieOptions.tabs.arrow:Initialize()
end end
end, end,
}, },
arrow_spacer_4 = QuestieOptionsUtils:Spacer(10), resetObjectivePosition = {
type = "execute",
order = 10.1,
width = 1.3,
name = function() return l10n("Reset Objective Position") end,
desc = function() return l10n("Reset the objective text block position to its default") end,
func = function()
Questie.db.profile.arrowObjectivePosition = nil
if QuestieArrow and QuestieArrow.ResetObjectivePosition then
QuestieArrow:ResetObjectivePosition()
end
end,
},
resetAndAttachObjective = {
type = "execute",
order = 10.2,
width = 1.6,
name = function() return l10n("Reset & Reattach") end,
desc = function() return l10n("Reset both arrow layouts and reattach the objective block to the arrow.") end,
func = function()
Questie.db.profile.arrowPosition = nil
Questie.db.profile.arrowObjectivePosition = nil
Questie.db.profile.arrowObjectiveAttached = true
if QuestieArrow and QuestieArrow.ResetAndAttachObjective then
QuestieArrow:ResetAndAttachObjective()
end
RefreshOptions()
end,
},
arrow_spacer_5 = QuestieOptionsUtils:Spacer(11),
debugArrow = { debugArrow = {
type = "toggle", type = "toggle",
order = 11, order = 12,
width = 1.5, width = 1.5,
name = function() return l10n("Debug Arrow") end, name = function() return l10n("Debug Arrow") end,
desc = function() return l10n("Show debug information about the arrow target in chat") end, desc = function() return l10n("Show debug information about the arrow target in chat") end,
@@ -196,7 +411,7 @@ function QuestieOptions.tabs.arrow:Initialize()
}, },
printArrowTarget = { printArrowTarget = {
type = "execute", type = "execute",
order = 12, order = 13,
width = 1.0, width = 1.0,
name = function() return l10n("Print Current Target") end, name = function() return l10n("Print Current Target") end,
desc = function() return l10n("Print the current arrow target coordinates to chat") end, desc = function() return l10n("Print the current arrow target coordinates to chat") end,
@@ -208,7 +423,7 @@ function QuestieOptions.tabs.arrow:Initialize()
}, },
debugPrintArrow = { debugPrintArrow = {
type = "execute", type = "execute",
order = 12.5, order = 13.5,
width = 1.0, width = 1.0,
name = function() return l10n("Debug Print Arrow State") end, name = function() return l10n("Debug Print Arrow State") end,
desc = function() return l10n("Print detailed debug info about arrow state to chat") end, desc = function() return l10n("Print detailed debug info about arrow state to chat") end,
@@ -220,7 +435,7 @@ function QuestieOptions.tabs.arrow:Initialize()
}, },
clearArrowTarget = { clearArrowTarget = {
type = "execute", type = "execute",
order = 13, order = 14,
width = 1.0, width = 1.0,
name = function() return l10n("Clear Target") end, name = function() return l10n("Clear Target") end,
desc = function() return l10n("Clear the current arrow target and resume auto-tracking") end, desc = function() return l10n("Clear the current arrow target and resume auto-tracking") end,
@@ -52,6 +52,15 @@ function QuestieOptionsDefaults:Load()
arrowEnabled = true, arrowEnabled = true,
arrowScale = 1, arrowScale = 1,
arrowAlpha = 1.0, arrowAlpha = 1.0,
arrowObjectiveAlpha = 1.0,
arrowLocked = false,
arrowObjectiveLocked = false,
arrowObjectiveAttached = false,
arrowObjectiveGap = 10,
arrowDistanceUnit = "yards",
arrowStyle = "arrow1",
arrowCustomTexture = "",
arrowCustomIsSheet = false,
arrowFontSize = 10, arrowFontSize = 10,
arrowFont = 'Friz Quadrata TT', arrowFont = 'Friz Quadrata TT',
debugArrow = false, debugArrow = false,
+2 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.6.1 ## Version: 1.6.3
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-AscensionDB, Questie-X-EbonholdDB ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB, QuestieJourneyDB ## SavedVariables: QuestieConfig, QuestieLearnerDB, QuestieCacheDB, QuestieJourneyDB
## SavedVariablesPerCharacter: QuestieConfigCharacter ## SavedVariablesPerCharacter: QuestieConfigCharacter
@@ -91,6 +91,7 @@ Modules\Libs\QuestieCombatQueue.lua
Modules\Libs\RamerDouglasPeucker.lua Modules\Libs\RamerDouglasPeucker.lua
# Modules # Modules
Modules\QuestieValidateGameCache.lua Modules\QuestieValidateGameCache.lua
Modules\Arrow\QuestieArrowAssets.lua
Modules\Arrow\QuestieArrow.lua Modules\Arrow\QuestieArrow.lua
Modules\QuestieInit.lua Modules\QuestieInit.lua
Modules\MinimapIcon.lua Modules\MinimapIcon.lua
+18 -1
View File
@@ -2,7 +2,7 @@
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" /> <img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
![Version](https://img.shields.io/badge/Questie--X-v1.6.2-blue.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/Questie--X-v1.6.3-blue.svg?style=for-the-badge)
[![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases) [![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases)
[![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/) [![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/)
[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon) [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon)
@@ -188,6 +188,23 @@ If your server uses non-standard map data, enable **Options → Advanced → Use
--- ---
## Arrow Styles
Questie-X includes a configurable arrow style picker with preview swatches, plus independent controls for arrow scale, transparency, font size, and objective attachment.
Bundled arrow assets are detected automatically as either sprite sheets or regular textures, and custom `.tga` files can be dropped into `Icons/Arrows` for use in the dropdown.
### Arrow Redesign
- `Arrow1` through `Arrow4` are the bundled image styles, while `arrowold` remains the only bundled sprite sheet.
- The dropdown now uses generated preview swatches from `Icons/Arrows`, so each bundled style shows a live thumbnail instead of a text-only entry.
- Image arrows rotate as a single texture and keep their native art, while sprite sheets only use sheet-cell logic when the style is explicitly `arrowold` or a custom sheet is marked as such.
- The arrow and objective text can be detached or reattached independently, locked separately, and reset with one-click actions.
- The attached gap, objective transparency, arrow transparency, font size, and distance unit are all configurable directly from the Arrow tab.
- Custom `.tga` files can still be dropped into `Icons/Arrows` and picked from the same dropdown without editing core files.
---
## Fixes & Compatibility ## Fixes & Compatibility
### Quest Log & Tracker ### Quest Log & Tracker
-140
View File
@@ -1,140 +0,0 @@
# QuestieLearner Phases — Progress Log
## Overview
QuestieLearner is being hardened in phases. Each phase is committed and pushed separately. This log tracks status, file changes, commits, and revert notes.
---
## Phase 1: Lua 5.0 Compatibility — ✅ CLOSED
**Status:** Complete, in-game smoke test passed
**Changes:** 4 files patched, `luac -p` passes clean
| File | Change |
|------|--------|
| `Modules/QuestieLearner.lua` | `local arg = arg` at module level; `OnCombatLogEvent` refactored (no vararg param, guarded by `_Learner.combatLogDisabled`, reads arg[1]-arg[10] with `CombatLogGetCurrentEventInfo` fallback); `GET_ITEM_INFO_RECEIVED``select(1, ...)`; `QUEST_REMOVED``select(1, ...)` |
| `Modules/Quest/AvailableQuests.lua:98` | `UnloadUndoable()` guard: `and not QuestiePlayer.currentQuestlog[questId]` |
| `Modules/Quest/QuestieQuest.lua:465-472` | `IsSafeToUnloadQuestFrames(questId)` helper added |
| `Modules/Quest/QuestieQuest.lua:458-461` | `HideQuest()` guard: `and not QuestiePlayer.currentQuestlog[questId]` |
| `Modules/Quest/DailyQuests.lua:114` | `HandleDailyQuests()` guard added |
| `Modules/Tracker/TrackerUtils.lua:791` | Fallback `IsComplete` gated by `QuestiePlayer.currentQuestlog[questId]` |
| `Modules/Tooltips/TooltipHandler.lua:11,117` | Questie import + `currentQuestlog` guard |
**Commits:** `77746a7` (Arrow debug), `3b31ba5` (Felendren fix), `11a3b47` (Sunstrider map pins), `f394a3b` (quest spawns), `096b4cc` (learned spawns)
**Revert per file:** `git checkout HEAD~5 -- <file>` to undo Phase 1 compat patches
**Smoke test criteria met:** One unload/rebuild flicker = architecture confirmed, not a bug.
---
## Phase 2: GUID-Based Kill Learning — ⚠️ IMPLEMENTED, NOT FULLY TEST-VERIFIED
**Status:** Committed and pushed. 2 known test failures in spec (quarantined). Awaiting in-game smoke test before claiming full verification.
**Commit:** `41f968b` — GUID-based spawn evidence and outlier pruning
**Test commit:** `b9aa0be` — Phase 2 unit tests for spawn evidence and pruning
**Note:** Spec tests in busted show 2 failures (quarantined). Not xfail'd — awaiting in-game smoke test.
**Revert:** `git revert HEAD~1 --no-edit && git push` to undo Phase 2 features
**Changes:**
|| File | Change |
||------|--------|
|| `Modules/QuestieLearner.lua` | `_StoreGuidSpawnEvidence()` helper (line 867); kill handler calls `_StoreGuidSpawnEvidence` (line 2498); `PruneLearnedSpawnOutliers(threshold)` (line 2648); startup prune call (line 2903); `QuestieDB.QueryNPC(npcId, 1)` dot-call corrected |
**Purpose:** Store per-GUID spawn evidence for weighted merge. Each kill caches GUID+coords keyed by npcId. Outlier pruning runs once at startup via `PruneLearnedSpawnOutliers()`.
**Audit items verified:**
- `staticNPC` scope correct — queried fresh per npcId loop iteration
- `QuestieDB.QueryNPC(npcId, 1)` uses dot-call (not colon)
- Object spawns use `[4]`, NPCs use `[7]`
- Pruning uses two-pass (collect to `toRemove[]`, then delete — no delete-during-iterate)
- `InjectLearnedData()` called after pruning when `anyChanged == true`
- Startup prune runs ONCE after `InjectLearnedData()`, no timer
**Revert:** `git revert HEAD~1 --no-edit && git push` to undo Phase 2 features; `git revert HEAD~2 --no-edit && git push` to also undo test commit
---
## Phase 3: Self-Healing Spawn Merge — ⚠️ IMPLEMENTED, NOT FULLY TEST-VERIFIED
**Status:** Committed and pushed. Awaiting in-game smoke test. No spec test failures reported, but no in-game validation yet.
**Commit:** `dc96782` — weighted spawn merge
**Test commit:** `2da18a8` — Phase 3 unit tests for spawn merge
**Changes:**
|| File | Change |
||------|--------|
|| `Modules/QuestieLearner.lua` | `_MergeSpawnEvidence(npcId)` (line ~928); kill handler integration (line ~2628) |
**Purpose:** Collate all learned spawn evidence for an NPC, score by frequency, override static DB only when top spawn appears in >60% of evidence AND differs from static entry. Below 60%, both sources coexist.
**Revert:** `git revert HEAD~1 --no-edit && git push` to undo Phase 3 features; `git revert HEAD~2 --no-edit && git push` to also undo test commit
---
## Phase 4: Real-Time Tooltip Population — ⚠️ IMPLEMENTED, NOT FULLY TEST-VERIFIED
**Status:** Committed. Awaiting in-game smoke test.
**Commit:** `c467538` — real-time tooltip for learned spawns
**Features added:**
- `_AddLearnedSpawnTooltipLine(unitToken)` — checks if the hovered unit is a learned NPC and adds "Learned spawn: (x, y) from N kills" via `GameTooltip:AddDoubleLine`
- `_RegisterLearnedSpawnTooltipHook()` — registers the `GameTooltip:HookScript("OnTooltipSetUnit", ...)` hook once (guard prevents double-hook)
- Hook called during `QuestieLearner:Initialize()` after all other setup
**Spec tests:** `Modules/QuestieLearner_spec.lua` — 6 tests covering coordinate formatting, grammar (singular/plural), early-return guards, and data extraction path
**Revert:** `git revert c467538 --no-edit && git push` undoes the feature commit; then `git revert c56f2c5 --no-edit && git push` also undoes the test commit
---
## Phase 5: Comms Hardening — ⚠️ IMPLEMENTED, NOT FULLY TEST-VERIFIED
**Status:** Committed. Awaiting in-game smoke test.
**Commit:** `3f8c9f5` — comms data validation
**Features added:**
- `_ValidateLearnedSpawnData(data)` — validates external learned spawn data before merge, checks: data is a table, spawns[zoneId] zoneId keys are numbers, each zone's coord list is a table of {x, y} pairs where x and y are numbers in the 0100 range. Rejects strings, nil, out-of-range coords, and malformed nested structures silently (no crash).
- Validation gate added at the start of `HandleNetworkData` before any merge
**Spec tests:** `Modules/QuestieLearner_spec.lua` Phase 5 section — 15 cases covering valid data, missing spawn data, type failures (string/nil/number), string zoneId, string zoneSpawns, malformed coords, out-of-range coords (negative, > 100), boundary edge cases
**Revert:** `git revert 3f8c9f5 --no-edit && git push` undoes feature; `git revert 2f5312f --no-edit && git push` also undoes test commit
---
## AscensionDB — ✅ CLEAN
**Last push:** `0ab4b56` — Sunstrider trainer NPC spawns (15280, 15285, 15513)
**Prior push:** `7505215` — clear-quest-race-class-gates-sunstrider-isle (13 quest race/class gates cleared)
**Revert:** `git revert HEAD --no-edit` for latest; `git revert <hash> --no-edit` for specific commit
---
## WotLKDB — ✅ CLEAN
**Last push:** `f3e91aa` — Remove race restriction from quest 9392; `09322cc` — Remove race restriction from quest 8328
**Revert:** `git revert HEAD --no-edit` per commit
---
## Overnight Rules
1. Each logical change = one commit. Never mix rollback domains.
2. Push after each commit. Visible progress = pushed commits.
3. Phase advances only after prior phase smoke test confirmed.
4. If conflict arises: pause, report state, wait for direction.
+24 -3
View File
@@ -169,17 +169,38 @@
<h1>Questie-X Documentation</h1> <h1>Questie-X Documentation</h1>
<p class="subtitle">Complete history of changes, fixes, and additions.</p> <p class="subtitle">Complete history of changes, fixes, and additions.</p>
<div style="display: flex; justify-content: center; gap: 10px;"> <div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.6.2 + Unreleased</code> <code>Version: v1.6.3 + Unreleased</code>
<a href="index.html" <a href="index.html"
style="background: var(--bg-tertiary); color: var(--accent-blue); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">&larr; Back to Documentation</a> style="background: var(--bg-tertiary); color: var(--accent-blue); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">&larr; Back to Documentation</a>
</div> </div>
</div> </div>
<div class="container"> <div class="container">
<h2 id="unreleased">[Unreleased] &mdash; Minimap Pin Drift Fix (Live API + Corrected Pixel Math), Sunstrider Isle Arrow Distance, Map Pins, Tooltip Schema Fixes, QuestData String Safety</h2> <h2 id="unreleased">[Unreleased] &mdash; Arrow Redesign, Minimap Pin Drift Fix (Live API + Corrected Pixel Math), Sunstrider Isle Arrow Distance, Map Pins, Tooltip Schema Fixes, QuestData String Safety</h2>
<ul> <ul>
<li><strong>[Docs &mdash; Release Metadata Sync]</strong> Updated the README badge, documentation version badges, and in-game addon version to <code>v1.6.3</code>, then tightened the release filters so handoff notes, local dev settings, and other workspace-only artifacts stay out of the exported release archive.</li>
<li><strong>[Fix &mdash; Arrow Asset Regression &amp; Redesign]</strong> Rebuilt the bundled arrow set so the live dropdown is backed by four image arrows (<code>Arrow1</code> through <code>Arrow4</code>) plus one legacy sheet arrow (<code>arrowold</code>). The old default <code>arrow.tga</code> cache path was retired in favor of the new image-specific asset names, and a regression test now asserts that only <code>arrowold</code> remains a bundled sprite sheet.
<ul>
<li><strong>Bundled asset layout</strong>: The new arrow images live under <code>Icons\\Arrows</code>, with matching generated preview TGAs so the dropdown can show a visible swatch for each style.</li>
<li><strong>Runtime split</strong>: Image styles rotate as single textures; sprite-sheet logic is reserved for <code>arrowold</code> and explicitly custom sheet uploads only.</li>
<li><strong>Regression proofing</strong>: <code>Tests\\QuestieArrowAssets_spec.lua</code> now checks the manifest labels, texture paths, sheet guard, and folder contents so a future asset rename cannot silently reintroduce sheet mode for every style.</li>
</ul>
</li>
<li><strong>[Fix &mdash; Arrow UI/Attachment Redesign]</strong> The Arrow tab now exposes independent arrow/objective positioning, individual lock toggles, attach/detach behavior, a gap slider, objective transparency, distance-unit selection, and larger font sizing. The objective text can be attached to the arrow again with a one-click reset, but it can also be moved and locked independently when detached.
<ul>
<li><strong>Attached mode</strong>: When attached, the arrow and objective frame move together as a pair, and the gap slider controls the distance between the visible arrow and the objective block.</li>
<li><strong>Detached mode</strong>: When detached, the arrow and objective text each remember their own saved position instead of snapping back to a shared default anchor.</li>
<li><strong>Preview handling</strong>: The dropdown uses preview swatches from the generated <code>Icons\\Arrows\\*_preview.tga</code> files so users can see the artwork at a glance instead of guessing from the name.</li>
</ul>
</li>
<li><strong>[Fix &mdash; Arrow Runtime Texture Fallback]</strong> Bundled image arrows now use the generated preview texture path at runtime as the live render source, while the raw source TGA remains available for packaging and regeneration. This keeps image arrows stable even when the client behaves oddly with a specific source TGA, while the sheet arrow stays isolated to <code>arrowold</code>.</li>
<li><strong>[Fix &mdash; Minimap Pin Drift: Live View-Radius API + Corrected Pixel Math]</strong> Resolved the long-running minimap pin drift bug where quest pins appeared to &quot;follow&quot; the player or jump on every frame. Pins now stay anchored to their world positions across all minimap zoom levels (0-5+) and across both Stock UI and ElvUI. <li><strong>[Fix &mdash; Minimap Pin Drift: Live View-Radius API + Corrected Pixel Math]</strong> Resolved the long-running minimap pin drift bug where quest pins appeared to &quot;follow&quot; the player or jump on every frame. Pins now stay anchored to their world positions across all minimap zoom levels (0-5+) and across both Stock UI and ElvUI.
<ul> <ul>
<li><strong>Time-to-fix note</strong>: This took roughly three weeks of iteration. The first three attempts either crashed, partially fixed only some zoom levels, or regressed zoom 0. The final patch was the first one that held across the full zoom range without the pin math drifting again.</li>
<li><strong>Iteration 1 &mdash; mapRadius reuse (broken)</strong>: The first attempt reused <code>mapRadius</code> for both the radius and the pixel multiplier, which crashed on 3.3.5a because the live API fallback was not wired in yet.</li>
<li><strong>Iteration 2 &mdash; direct half-width (partial)</strong>: The second attempt used <code>GetWidth() * GetScale() / 2</code> directly and fixed low zoom levels, but higher zooms still drifted because the map radius source was still wrong.</li>
<li><strong>Iteration 3 &mdash; scaleFactor normalization (regressed)</strong>: The third attempt added a normalization factor based on a reference radius and inverted the ratio, which made zoom 0 worse and had to be backed out.</li>
<li><strong>Iteration 4 &mdash; final fix</strong>: The final patch combined the native 3.3.5a <code>Minimap:GetViewRadius()</code> API with the correct pixel half-width formula, which aligned the world-yard math with the on-screen pixel math.</li>
<li><strong>Root Cause 1 &mdash; Hardcoded lookup table used on 3.3.5a</strong>: <code>Compat/HBD.lua</code> was reading <code>mapRadius</code> from a hardcoded <code>minimap_size</code> lookup table calibrated for stock WoW zoom levels. The API check <code>C_Minimap and C_Minimap.GetViewRadius</code> evaluated to <code>nil</code> on 3.3.5a (and Ascension), so the broken lookup table was always used. The lookup value <code>minimap_size.outdoor[5] = 250</code> produced <code>mapRadius = 125</code>, but the actual live minimap view radius at zoom 5 is <code>116.67</code> yards (from <code>Minimap:GetViewRadius()</code>). This 6.7% error compounded across all pin offsets.</li> <li><strong>Root Cause 1 &mdash; Hardcoded lookup table used on 3.3.5a</strong>: <code>Compat/HBD.lua</code> was reading <code>mapRadius</code> from a hardcoded <code>minimap_size</code> lookup table calibrated for stock WoW zoom levels. The API check <code>C_Minimap and C_Minimap.GetViewRadius</code> evaluated to <code>nil</code> on 3.3.5a (and Ascension), so the broken lookup table was always used. The lookup value <code>minimap_size.outdoor[5] = 250</code> produced <code>mapRadius = 125</code>, but the actual live minimap view radius at zoom 5 is <code>116.67</code> yards (from <code>Minimap:GetViewRadius()</code>). This 6.7% error compounded across all pin offsets.</li>
<li><strong>Root Cause 2 &mdash; Factor-of-2 in pixel math</strong>: <code>minimapWidth</code> was computed as <code>(GetWidth() * mapRadius / 155.52) / 2</code>, mixing pixel-half-width with a yards-based scale factor. The <code>/ 155.52</code> constant was a hardcoded normalization that did not match the live API value. The math was self-inconsistent: ratio <code>minimapWidth / mapRadius</code> was <code>0.56</code> (off by ~7% from the correct <code>0.6</code>).</li> <li><strong>Root Cause 2 &mdash; Factor-of-2 in pixel math</strong>: <code>minimapWidth</code> was computed as <code>(GetWidth() * mapRadius / 155.52) / 2</code>, mixing pixel-half-width with a yards-based scale factor. The <code>/ 155.52</code> constant was a hardcoded normalization that did not match the live API value. The math was self-inconsistent: ratio <code>minimapWidth / mapRadius</code> was <code>0.56</code> (off by ~7% from the correct <code>0.6</code>).</li>
<li><strong>Root Cause 3 &mdash; Scale not applied to pixel dimensions</strong>: <code>minimapWidth</code> was based on <code>GetWidth()</code> alone, ignoring <code>GetScale()</code>. When UI scale changed, the pixel dimensions reported by <code>GetWidth()</code> would diverge from the actual on-screen size, while <code>mapRadius</code> (in yards) stayed fixed. This caused drift to worsen at higher zoom levels where the ratio was most sensitive.</li> <li><strong>Root Cause 3 &mdash; Scale not applied to pixel dimensions</strong>: <code>minimapWidth</code> was based on <code>GetWidth()</code> alone, ignoring <code>GetScale()</code>. When UI scale changed, the pixel dimensions reported by <code>GetWidth()</code> would diverge from the actual on-screen size, while <code>mapRadius</code> (in yards) stayed fixed. This caused drift to worsen at higher zoom levels where the ratio was most sensitive.</li>
@@ -1211,4 +1232,4 @@
</footer> </footer>
</body> </body>
</html> </html>
+10 -6
View File
@@ -210,7 +210,7 @@
<img src="QuestieXlogo.png" alt="Questie-X Logo" width="400" /> <img src="QuestieXlogo.png" alt="Questie-X Logo" width="400" />
<p class="subtitle">A universal WoW quest-helper with a plugin architecture for any private server.</p> <p class="subtitle">A universal WoW quest-helper with a plugin architecture for any private server.</p>
<div style="display: flex; justify-content: center; gap: 10px;"> <div style="display: flex; justify-content: center; gap: 10px;">
<code>Version: v1.6.2 + Unreleased</code> <code>Version: v1.6.3 + Unreleased</code>
<a href="changelog.html" <a href="changelog.html"
style="background: var(--bg-tertiary); color: var(--accent-green); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">View style="background: var(--bg-tertiary); color: var(--accent-green); text-decoration: none; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color);">View
Changelog</a> Changelog</a>
@@ -224,16 +224,20 @@
<div class="grid"> <div class="grid">
<div class="card"> <div class="card">
<h4>Sunstrider Coordinate Normalization (Final)</h4> <h4>Arrow Redesign</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 &mdash; all 4 detection checks now accept both values plus uiMapId 1241. The 1241&rarr;1941 redirect in <code>_ResolveMapUiMapId()</code> was removed; pins render natively on 1241 via <code>areaIdToUiMapId[1241] = 1241</code>.</p> <p>The Arrow tab now ships with `Arrow1`-`Arrow4` as bundled image styles, keeps `arrowold` as the only sprite sheet, and lets users detach, lock, reattach, and preview custom arrow textures from `Icons/Arrows` without touching core files.</p>
</div> </div>
<div class="card"> <div class="card">
<h4>Learned Tooltip Schema Fix</h4> <h4>Sunstrider Coordinate Normalization (Final)</h4>
<p><code>QuestieTooltips</code> now reconstructs learned objective text from <code>QuestieLearner.data.quests[questId][10]</code> instead of treating learner arrays like legacy <code>{ questId -&gt; objList }</code> maps, preventing the Stormwind City Guard-style <code>objList</code>-as-number crash reported from Bronzebeard.</p> <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 &mdash; all 4 detection checks now accept both values plus uiMapId 1241. The 1241&rarr;1941 redirect in <code>_ResolveMapUiMapId()</code> was removed; pins render natively on 1241 via <code>areaIdToUiMapId[1241] = 1241</code>.</p>
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="card">
<h4>Learned Tooltip Schema Fix</h4>
<p><code>QuestieTooltips</code> now reconstructs learned objective text from <code>QuestieLearner.data.quests[questId][10]</code> instead of treating learner arrays like legacy <code>{ questId -&gt; objList }</code> maps, preventing the Stormwind City Guard-style <code>objList</code>-as-number crash reported from Bronzebeard.</p>
</div>
<div class="card"> <div class="card">
<h4>Arrow Rotation &amp; Collection Fixes</h4> <h4>Arrow Rotation &amp; 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> <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>
@@ -423,4 +427,4 @@ end</code></pre>
</footer> </footer>
</body> </body>
</html> </html>
-185
View File
@@ -1,185 +0,0 @@
# Sunstrider Isle Pin Fix — Coordinate Collection Guide
## Architecture
On Ascension, Sunstrider Isle (uiMapId 1241) shares Eversong Woods' (1941)
coordinate space — it's a child map within Eversong. Pins for zone 3430
(Eversong Woods) render on the Eversong map (uiMapId 1941) and appear on the
Sunstrider sub-map (1241) via ZONE_REDIRECT visibility in HBD.lua.
### Key mappings
- **areaId 3430** (Eversong Woods) → uiMapId 1941 (Eversong map)
- **uiMapId 1241** (Sunstrider Isle) → areaId 3430 → pins redirected to 1941 via `_ResolveMapUiMapId`
- **ZONE_REDIRECT**: 1241→1941, 946→1941 (cross-visibility)
- **HBD bounds**: mapData[1241] uses Eversong's calibrated bounds for player position tracking
- **QuestieLearner**: GetZoneId() returns areaId 3430; HighConfidity set to 1 kill
### Files modified
- `Database/Zones/zoneDB.lua` — areaIdToUiMapId[3430] = 1941 (was 1241)
- `Modules/Map/QuestieMap.lua` — _ResolveMapUiMapId redirects 1241→1941
- `Modules/Arrow/QuestieArrow.lua` — _ResolveArrowUiMapId redirects 1241/946→1941
- `Modules/QuestieLearner.lua` — GetZoneId() returns areaId via ZoneDB; InjectLearnedData migrates uiMapId keys; MIN_CONFIDENCE_PINS = 1
- `Compat/HBD.lua` — ZONE_REDIRECT[1241]=1941, ASCENSION_ZONE_BOUNDS for mapData[1241]
## Diagnostic /run Commands
These must be run **in-game** after Questie has fully loaded (wait 5+ seconds
after login). If output is empty, the DB may not be initialized yet.
### Check ZoneDB mappings
```lua
/run print("3430→uiMapId:", QuestieLoader:ImportModule("ZoneDB"):GetUiMapIdByAreaId(3430), " 1241→areaId:", QuestieLoader:ImportModule("ZoneDB"):GetAreaIdByUiMapId(1241))
```
Expected: `3430→uiMapId: 1941 1241→areaId: 3430`
### Check known NPC spawns for Sunstrider (zone 3430)
```lua
/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local ids={15271,15273,15274,15278,15279,15280,15281,15283,15284,15285,15287,15289,15291,15292,15294,15295,15297,15298,15301,15366,15367,15371,15372}; for _,id in ipairs(ids) do local n=QuestieDB:GetNPC(id); if n and n.spawns then for z,c in pairs(n.spawns) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print(id..":"..(n.name or "?").." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end
```
### Check QuestieLearner overrides for zone 3430
```lua
/run local ov=QuestieDB and QuestieDB.npcDataOverrides; if ov then for id,d in pairs(ov) do if d[7] then for z,c in pairs(d[7]) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print("override npc="..id.." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end else print("npcDataOverrides not loaded") end
```
### Check HBD ZONE_REDIRECT
```lua
/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); print("ResolveZone(1241)=", HBD.ResolveZone and HBD.ResolveZone(1241) or "N/A", "ResolveZone(946)=", HBD.ResolveZone and HBD.ResolveZone(946) or "N/A")
```
Expected: `ResolveZone(1241)= 1941 ResolveZone(946)= 1941`
### Check HBD bounds for map 1241
```lua
/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); local d=HBD.mapData[1241]; if d then print("1241 bounds: left="..d.left.." right="..d.right.." top="..d.top.." bottom="..d.bottom.." parentMapID="..(d.parentMapID or "nil")) else print("No mapData for 1241") end
```
Expected: `left=-2721.0066 right=-1120.9934 top=8433.9360 bottom=7367.2693 parentMapID=1941`
### Check LearnNPC zone tracking (run after killing a mob on Sunstrider)
```lua
/run local ld=Questie.dbLearner; if ld and ld.global and ld.global.npcs then local count=0; for id,d in pairs(ld.global.npcs) do if d[7] and (d[7][3430] or d[7]["3430"]) then count=count+1; print("learned npc="..id.." mc="..(d.mc or 0).." zone=3430") end end; if count==0 then print("No learned NPCs in zone 3430 yet") end else print("Learner data not available") end
```
### Verify pin rendering pipeline
```lua
/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local uiMapId=ZoneDB:GetUiMapIdByAreaId(3430); print("Zone 3430 → uiMapId "..tostring(uiMapId).." (expected 1941)"); local HBD=LibStub("HereBeDragonsQuestie-2.0"); local wx,wy=HBD:GetWorldCoordinatesFromZone(0.38,0.21,uiMapId); print("World coords for (38%,21%) on map "..uiMapId..": "..string.format("%.1f, %.1f",wx or 0,wy or 0))
```
Expected: uiMapId 1941, world coords around (-2156, 8209)
## Testing checklist
- [ ] Load addon on Ascension server
- [ ] Create a Blood Elf character on Sunstrider Isle
- [ ] Verify quest giver pins appear on both the Sunstrider minimap AND the Eversong world map
- [ ] Verify clicking a quest giver pin shows quest info
- [ ] Verify arrow (distance/direction) points correctly to quest targets
- [ ] After visiting/killing NPCs, verify QuestieLearner creates pin overrides (mc≥1)
- [ ] Verify pins do NOT appear in mountains or off-map
- [ ] Verify Eversong Woods (zone 1941) quest givers NOT on Sunstrider show correctly on Eversong map
## Adding Townsfolk / NPC Data
There are three ways to add NPC spawn data so that pins appear for
Sunstrider Isle NPCs. Choose the one that matches your data source.
### 1. AscensionDB plugin (numeric-key array)
The AscensionDB companion addon ships NPC data as a numeric-key array.
Each entry is keyed by NPC ID and uses numeric indices matching
`QuestieDB.npcKeys`. The spawns field is index **7** (see `npcKeys.spawns = 7`)
and is itself a dict keyed by **areaId**.
```lua
-- AscensionDB.npcData example for a Sunstrider NPC
A.npcData = {
-- [npcId] = { [1]=name, [4]=minLevel, [5]=maxLevel, [6]=rank, [7]=spawns, ... }
[15273] = {
"Arcane Wraith", -- [1] name
nil, nil, -- [2] minLevelHealth, [3] maxLevelHealth
1, 2, -- [4] minLevel, [5] maxLevel
0, -- [6] rank
{ -- [7] spawns ← keyed by areaId, NOT uiMapId
[3430] = { -- 3430 = Eversong Woods areaId
{38.4, 21.6},
{39.2, 20.8},
{40.0, 22.4},
},
},
},
}
```
This data is loaded by `QuestieDB:LoadAscensionNpcData()` which calls
`_Asc_MergeInto(QuestieDB.npcDataOverrides, data)` — it writes each NPC
entry directly into `npcDataOverrides[npcId]`.
### 2. WotLKDB / TBC corrections (string-key dict)
The built-in correction files (`wotlkNPCFixes.lua`, `tbcNPCFixes.lua`,
`classicNPCFixes.lua`) use the **named-key** format via the `npcKeys`
constants. This is the format you should use for patches submitted to
Questie-X itself.
```lua
-- In Database/Corrections/wotlkNPCFixes.lua or tbcNPCFixes.lua
local npcKeys = QuestieDB.npcKeys
return {
-- [npcId] = { [npcKeys.field] = value, ... }
[15273] = {
[npcKeys.spawns] = {
[3430] = { -- areaId 3430 (Eversong Woods), NOT uiMapId 1241
{38.4, 21.6},
{39.2, 20.8},
{40.0, 22.4},
},
},
},
}
```
These corrections are merged into `QuestieDB.npcDataOverrides` by the
correction loader before overrides are applied.
### 3. QuestieLearner runtime (automatic)
QuestieLearner learns NPC positions automatically as you play. When you
kill or interact with an NPC on Sunstrider Isle, `LearnNPC` stores the
spawn under **areaId 3430** (the return value of `GetZoneId()`, which
uses `ZoneDB:GetAreaIdByUiMapId(1241) → 3430`). Learned data is written
to `Questie.dbLearner.global.npcs[npcId]` as a numeric-key array
(identical structure to AscensionDB) and injected into
`npcDataOverrides` once the confidence threshold (`mc >= MIN_CONFIDENCE_PINS`)
is met.
### CRITICAL RULE: spawns are keyed by areaId, NOT uiMapId
This bears repeating because it is the #1 source of Sunstrider bugs:
- **CORRECT**: `[3430] = { {38.4, 21.6}, ... }` — areaId for Eversong Woods
- **WRONG**: `[1241] = { {38.4, 21.6}, ... }` — uiMapId for Sunstrider Isle
Questie's internal spawn tables use `areaId` as the key. The ZoneDB
redirect (`uiMapId 1241 → areaId 3430`) ensures that even when the
player is on the Sunstrider sub-map, the correct areaId is used. If you
accidentally key spawns by uiMapId (1241), they will never be found by
`GetNPC` and no pins will render.
### _MergeOverride fix (historical note)
Prior to the `_MergeOverride` helper (added as part of the Sunstrider
pin fix), the `GetNPC` function only checked **string-keyed** override
entries (`override["spawns"]`). AscensionDB and QuestieLearner store
overrides with **numeric keys** (`override[7]`), so their spawn data
was silently ignored. The `_MergeOverride` function now checks both
formats:
```lua
-- _MergeOverride checks both override formats:
-- 1. override[stringKey] (string-keyed, e.g. from wotlkNPCFixes)
-- 2. override[intKey] (numeric-keyed, e.g. from QuestieLearner / AscensionDB)
-- 3. rawdata[intKey] (fallback to compiled DB)
```
This means override data from **all three sources** is now visible to
`GetNPC` for the first time. If you are debugging and overrides seem
ignored, confirm `_MergeOverride` is being called (line 1923 in
QuestieDB.lua as of this writing).
-306
View File
@@ -1,306 +0,0 @@
# Sunstrider Isle Pin Fix — Questie-X on Ascension
## Architecture (Current)
On Ascension, Sunstrider Isle (uiMapId 1241) shares Eversong Woods' (1941)
coordinate space. The fix ensures correct cross-map pin visibility.
### Coordinate Flow
```
NPC spawn data: zone 3430 (Eversong) → GetUiMapIdByAreaId(3430) → uiMapId 1941
→ pin rendered on Eversong map (1941) with Eversong coordinates
→ ZONE_REDIRECT makes pin visible on Sunstrider (1241) too
→ _ResolveMapUiMapId redirects 1241→1941 for consistency
```
### Key Mappings
| Lookup | From | To | Purpose |
|--------|------|----|---------|
| `GetUiMapIdByAreaId(3430)` | areaId 3430 | uiMapId 1941 | Pin placement on Eversong map |
| `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 |
| `ZONE_REDIRECT[946]` | uiMapId 946 | uiMapId 1941 | Cross-visibility (ghost map) |
| HBD bounds `mapData[1241]` | — | Eversong's bounds | Player position tracking on Sunstrider |
### Why zone 3430 → uiMapId 1941 (not 1241)
Zone 3430 = Eversong Woods (the whole zone, not just Sunstrider).
In the WotLKDB, NPC spawn coordinates under zone 3430 are Eversong-wide
percentages (e.g., NPC 15278 at 38.02%, 21.01%). These render correctly on
the Eversong map (1941). Mapping 3430→1241 would place Eversong-wide
coordinates on the Sunstrider sub-map, producing wrong positions.
Pins from 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`.
---
## Files Modified
### Database/Zones/zoneDB.lua
- `areaIdToUiMapId[3430] = 1941` (was 1241)
- `uiMapIdToAreaIdCache[1241] = 3430` (unchanged — Sunstrider map IS in Eversong zone)
- `UiMapIdOverrides[1241] = 3430` (unchanged — reverse lookup)
### Modules/Map/QuestieMap.lua
- `_ResolveMapUiMapId(1241, x, y)` → redirects to 1941
- `_ResolveMapUiMapId(946, x, y)` → redirects to 1941
- Pins from zone 3430 naturally go to uiMapId 1941 (no redirect needed for them)
### Modules/Arrow/QuestieArrow.lua
- `_ResolveArrowUiMapId(1241)` → 1941
- `_ResolveArrowUiMapId(946)` → 1941
- Comment updated to match new approach
- **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
- `MIN_CONFIDENCE_PINS = 1` (was 2) — Ascension needs every data point
- `InjectLearnedData()`: Migration converts uiMapId spawn keys to areaId (1241→3430)
- All `LearnNPC` call sites now pass zoneId:
- `OnMouseoverUnit`: passes areaId from `l10n:GetAreaIdByLocalName()`
- `OnQuestDetail`: passes zoneId from `GetZoneId()`
- `OnQuestComplete`: passes zoneId from `GetZoneId()`
- `OnQuestAccepted`: passes `GetZoneId()`
- `OnQuestTurnedIn`: passes `GetZoneId()`
- `GOSSIP_SHOW` handler: passes `GetZoneId()`
- Kill handler: passes `bestKill.zoneId` (already correct)
### Compat/HBD.lua
- `ASCENSION_ZONE_BOUNDS[1241]` = Eversong's calibrated bounds for player position tracking
- `ASCENSION_ZONE_BOUNDS[946]` = same
- `ZONE_REDIRECT[1241]=1941`, `ZONE_REDIRECT[946]=1941` (visibility)
- `ResolveZone()` for `isSameZoneSpace` checks
### Database/QuestieDB.lua
- `_MergeOverride(data, key, override)`: Fixed numeric-vs-string key mismatch
- **Bug**: Override sources (wotlkNPCFixes, AscensionDB, QuestieLearner) could store spawn
zone keys as either numbers (`3430`) or strings (`"3430"`). When `_MergeOverride` merged
spawns into the base NPC data, a string key like `"3430"` would create a *new* table entry
alongside the existing numeric `3430` key, producing duplicate spawn entries that rendered
pins twice or confused zone lookups.
- **Fix**: `_MergeOverride` now normalises all zone keys to numeric before merging. Any
string-keyed spawn entry (e.g. `{["3430"] = {{0.38,0.21}}}`) is converted to its numeric
equivalent (`{3430 = {{0.38,0.21}}}`) before the merge loop runs, so both formats resolve
to the same table slot.
- This fix is applied **once** inside `_MergeOverride` — no changes needed in individual
override sources.
### Modules/QuestieLearner.lua (zone tracking additions)
- `OnQuestComplete`: Now captures `zoneId` via `GetZoneId()` and passes it as `spawnZoneId`
to every `LearnNPC` call inside this handler.
- All `LearnNPC` call sites now pass `spawnZoneId` — the area ID of the zone the player
was in when the event fired. Previously only some handlers included zone data; now every
path supplies it, giving `npcDataOverrides` consistent spawn-zone keys for learned NPCs.
---
## NPC Data Format & Override Pipeline
### Override Sources
Three systems feed into `QuestieDB.npcDataOverrides`, each producing spawn data that
Questie merges at load time:
| Source | When it runs | Key format | Typical content |
|--------|-------------|------------|-----------------|
| `wotlkNPCFixes` (Database/NPCs) | Addon load | numeric | Corrections for vanilla→WotLK data changes |
| AscensionDB plugin | Addon load | numeric | Ascension-specific NPC additions & tweaks |
| QuestieLearner | Runtime events | **was string** (now numeric via `_MergeOverride`) | Player-observed NPC spawns |
### Numeric-vs-String Key Issue
Lua tables can have both `3430` (number) and `"3430"` (string) as separate keys.
The base NPC data in `QuestieDB.npcs` uses **numeric** zone keys exclusively.
If an override source stored spawns under `"3430"`, the merge would produce:
```lua
spawns = {
[3430] = {{0.38, 0.21}}, -- original
["3430"]= {{0.38, 0.21}}, -- duplicate from string key
}
```
This caused double pins and zone-lookup failures. The `_MergeOverride` fix normalises
all keys to numeric *before* merging, collapsing both entries into one.
### How _MergeOverride Resolves Both Formats
```lua
-- Inside _MergeOverride, before merging spawns (field index 7):
if override[7] then
local normalised = {}
for zoneKey, coords in pairs(override[7]) do
normalised[tonumber(zoneKey) or zoneKey] = coords
end
override[7] = normalised
end
-- Then proceed with the standard deep-merge loop
```
This ensures every string key like `"3430"` is converted to `3430`, matching the
numeric keys in the base data. The fix is centralised — each override source can
store keys in whatever format is convenient.
### Adding Townsfolk Data to AscensionDB Plugin
To add a townsfolk (non-combat NPC) to the AscensionDB plugin's override data:
```lua
-- In AscensionDB/NPCs.lua (or equivalent), npcDataOverrides section:
npcDataOverrides[<npcId>] = {
-- Field layout follows QuestieDB NPC format:
-- [1] name, [2] minLevel, [3] maxLevel, [4] friendly (0=hostile, 1=friendly)
-- [5] spawnByZone or nil, [6] waypoints or nil,
-- [7] spawns keyed by areaId
[7] = {
[3430] = { -- areaId for Eversong Woods (covers Sunstrider Isle)
{0.38, 0.21}, -- {x%, y%} on the Eversong map
},
},
}
```
Key points:
- Use **numeric** areaId keys (`3430`, not `"3430"`). Even though `_MergeOverride`
now handles both formats, numeric is canonical and avoids ambiguity.
- Spawn coordinates are percentages (01 range) relative to the Eversong Woods map
(uiMapId 1941), **not** the Sunstrider sub-map.
- Townsfolk typically set field `[4] = 1` (friendly).
- areaId `3430` covers both Eversong Woods and Sunstrider Isle — no separate entry
for the sub-zone is needed because `ZONE_REDIRECT` handles cross-visibility.
---
## Diagnostic /run Commands
Must be run **in-game** after Questie has fully loaded (5+ seconds after login).
### Check ZoneDB mappings
```lua
/run print("3430→uiMapId:", QuestieLoader:ImportModule("ZoneDB"):GetUiMapIdByAreaId(3430), " 1241→areaId:", QuestieLoader:ImportModule("ZoneDB"):GetAreaIdByUiMapId(1241))
```
Expected: `3430→uiMapId: 1941 1241→areaId: 3430`
### Check HBD ZONE_REDIRECT
```lua
/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); print("ResolveZone(1241)=", HBD.ResolveZone and HBD.ResolveZone(1241) or "N/A", "ResolveZone(946)=", HBD.ResolveZone and HBD.ResolveZone(946) or "N/A")
```
Expected: `ResolveZone(1241)= 1941 ResolveZone(946)= 1941`
### Check known NPC spawns for Sunstrider zone (3430)
```lua
/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local ids={15271,15273,15274,15278,15279,15280,15281,15283,15284,15285,15287,15289,15291,15292,15294,15295,15297,15298,15301,15366,15367,15371,15372}; for _,id in ipairs(ids) do local n=QuestieDB:GetNPC(id); if n and n.spawns then for z,c in pairs(n.spawns) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print(id..":"..(n.name or "?").." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end
```
### Check QuestieLearner overrides for zone 3430
```lua
/run local ov=QuestieDB and QuestieDB.npcDataOverrides; if ov then for id,d in pairs(ov) do if d[7] then for z,c in pairs(d[7]) do if z==3430 or z=="3430" then for i,pt in ipairs(c) do print("override npc="..id.." zone="..z.." ["..i.."]="..string.format("%.2f,%.2f",pt[1],pt[2])) end end end end end else print("npcDataOverrides not loaded") end
```
### Check HBD bounds for map 1241
```lua
/run local HBD=LibStub("HereBeDragonsQuestie-2.0"); local d=HBD.mapData[1241]; if d then print("1241: left="..d.left.." right="..d.right.." top="..d.top.." bottom="..d.bottom.." parentMapID="..(d.parentMapID or "nil")) else print("No mapData for 1241") end
```
### Check learned data (after visiting Sunstrider)
```lua
/run local ld=Questie.dbLearner; if ld and ld.global and ld.global.npcs then local count=0; for id,d in pairs(ld.global.npcs) do if d[7] and (d[7][3430] or d[7]["3430"]) then count=count+1; print("learned npc="..id.." mc="..(d.mc or 0).." zone=3430") end end; if count==0 then print("No learned NPCs in zone 3430 yet") end else print("Learner data not available") end
```
### Verify pin rendering
```lua
/run local ZoneDB=QuestieLoader:ImportModule("ZoneDB"); local uiMapId=ZoneDB:GetUiMapIdByAreaId(3430); print("Zone 3430 → uiMapId "..tostring(uiMapId).." (expected 1941)"); local HBD=LibStub("HereBeDragonsQuestie-2.0"); local wx,wy=HBD:GetWorldCoordinatesFromZone(0.38,0.21,uiMapId); print("World coords for (38%,21%) on map "..uiMapId..": "..string.format("%.1f, %.1f",wx or 0,wy or 0))
```
### Verify MIN_CONFIDENCE_PINS
```lua
/run print("minConfidencePins:", Questie.dbLearner.global.settings.minConfidencePins or "default(1)")
```
### Reset all learned data (WARNING: deletes everything!)
```lua
/run Questie.dbLearner.global.npcs = {}; Questie.dbLearner.global.quests = {}; Questie.dbLearner.global.items = {}; Questie.dbLearner.global.objects = {}; ReloadUI()
```
---
## Testing Checklist
- [ ] Load addon on Ascension server
- [ ] Create a Blood Elf character on Sunstrider Isle
- [ ] Verify diagnostic: `GetUiMapIdByAreaId(3430)` returns 1941
- [ ] Verify quest giver pins appear on BOTH Sunstrider minimap AND Eversong world map
- [ ] Verify pins do NOT appear in mountains or off-map
- [ ] Verify arrow (distance/direction) points correctly to quest targets
- [ ] Kill 1 NPC on Sunstrider, check learned data shows zone=3430 (not 1241)
- [ ] After 1+ kill, verify learned pin auto-appears at correct position
- [ ] Verify Eversong Woods NPCs NOT on Sunstrider show correctly on Eversong map
- [ ] Check no regressions on other zones
- [ ] **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`.
-77
View File
@@ -1,77 +0,0 @@
# QuestieLearner In-Game Testing Macros
Copy these into WoW's `/macro` interface (one per macro button).
Use `/run` to execute raw Lua from a macro.
---
## Macro 1: Learner Stats
Shows how much data QuestieLearner has collected.
```
/run local QL=QuestieLoader:ImportModule("QuestieLearner")local n,q,i,o=QL:GetStats()print("Learner: "..n.." NPCs, "..q.." quests, "..i.." items, "..o.." objects")
```
---
## Macro 2: Dump Current Target
Prints what QuestieLearner knows about your current target.
```
/run local t="target"local g=UnitGUID(t)if not g then print("No target")return end local nid=tonumber(g:match("%-(%d+)%-[^-]+$"))if not nid then print("Can't parse NPC ID")return end local d=QuestieLoader:ImportModule("QuestieLearner").dbLearner.global.npcs[nid]if d then print("NPC "..nid..": "..tostring(d[1]))for z,c in pairs(d[7] or {})do print(" Zone "..z..": "..#c.." coords")for _,v in ipairs(c)do print(" ("..v[1]..", "..v[2]..")")end end else print("No learned data for NPC "..nid)end
```
---
## Macro 3: Dump Mouseover
Same as above, but reads your mouseover unit.
```
/run local t="mouseover"local g=UnitGUID(t)if not g then print("No mouseover")return end local nid=tonumber(g:match("%-(%d+)%-[^-]+$"))if not nid then print("Can't parse NPC ID")return end local d=QuestieLoader:ImportModule("QuestieLearner").dbLearner.global.npcs[nid]if d then print("NPC "..nid..": "..tostring(d[1]))for z,c in pairs(d[7] or {})do print(" Zone "..z..": "..#c.." coords")for _,v in ipairs(c)do print(" ("..v[1]..", "..v[2]..")")end end else print("No learned data for NPC "..nid)end
```
---
## Macro 4: Zone Info
Shows current zone's areaId, uiMapId, and name — useful for verifying zone mapping.
```
/run local n=GetRealZoneText()local u=C_Map.GetBestMapForUnit("player")local a=select(8,GetInstanceInfo())local zd=QuestieLoader:ImportModule("ZoneDB")if zd and zd.GetAreaIdByUiMapId then local aid=zd:GetAreaIdByUiMapId(u)if aid and aid>0 then a=aid end end print("Zone: "..n.." | uiMapId: "..tostring(u).." | areaId: "..tostring(a))
```
---
## Macro 5: Debug Toggle
Toggles Questie's DEVELOP debug level to see [QL-DEV] learner prints.
```
/run local q=Questie if q.db.profile.debugEnabled then q.db.profile.debugEnabled=not q.db.profile.debugEnabled;q.db.profile.debugLevel=0;print("Debug OFF")else q.db.profile.debugEnabled=true;q.db.profile.debugLevel=16384;print("Debug ON (DEVELOP level)")end
```
---
## Macro 6: Force Learn Mouseover
Manually triggers QuestieLearner to record the mouseover NPC's location.
```
/run local mo="mouseover"if UnitExists(mo)and not UnitIsPlayer(mo)then local g=UnitGUID(mo)local nid=tonumber(g:match("%-(%d+)%-[^-]+$"))if nid then local QL=QuestieLoader:ImportModule("QuestieLearner")local name=UnitName(mo)local level=UnitLevel(mo)local flags=UnitNPCFlags and UnitNPCFlags(mo)or 0 local a=l10n and l10n.GetAreaId and l10n:GetAreaId()QL:LearnNPC(nid,name,level,nil,flags,nil,nil,nil,a)print("Learned "..name.." ("..nid..") in zone "..tostring(a))end else print("No valid mouseover target")end
```
---
## Macro 7: Re-inject Data
Forces QuestieLearner to re-process all learned data (zone migration, override injection). Use after importing data or if pins aren't showing.
```
/run QuestieLoader:ImportModule("QuestieLearner"):InjectLearnedData()print("Data re-injected")
```
---
## Usage Notes
- These use WoW's 255-character macro limit; they've been compressed to fit.
- Run each macro once after creating it to verify it works.
- Enable Debug (Macro 5) first to see `[QL-DEV]` prints for learning events.
- Use Macro 4 to check which zone/areaId you're in when testing.
- If a macro doesn't fit, split it into two macros.