v1.6.3-unreleased: Sunstrider zone fixes, QuestieLearner icon preservation, migration, and Busted test suite
Code changes (13 files, +916/-171): - ZoneDB integration: GetZoneId() converts uiMapId→areaId via ZoneDB reverse lookup - MIN_CONFIDENCE_PINS reduced to 1 for Ascension (incomplete NPC DBs) - QuestieLearner icon preservation: objective Icon passed to RegisterObjectiveTooltip - InjectLearnedData zone migration: converts old uiMapId spawn keys to areaId - areaId passed through all LearnNPC call sites (OnMouseoverUnit, OnQuestDetail, OnQuestComplete, OnQuestAccepted, OnQuestTurnedIn, OnGossipShow) - Compat/Compat.lua: C_Map.GetPlayerMapPosition UiMapData support - Compat/HBD.lua: Sunstrider mapData aliases and fallback loading - Sunstrider arrow fix (QuestieArrow.lua), resolved pin rendering (QuestieMap.lua) - _MergeOverride helper for string/numeric key compatibility - Northshire Valley UiMapData registration Testing infrastructure: - .busted config pointing to tests/ directory - Tests/wow_api_mock.lua: WoW API mocks (ZoneDB, C_Map, QuestLogCache, etc.) - Tests/QuestieLearner_spec.lua: 12 tests covering coordinate scaling, spell cast learning, zone migration (NPC/objects), icon preservation, settings defaults, and LearnNPC spawn zone tracking - selene.toml + wow_classic.yml: linter configuration Documentation: - Makefile with test/lint/ci targets - sunstrider-coordinate-collection.md: coordinate data reference - sunstrider-pin-fix.md: root cause analysis and fix documentation
This commit is contained in:
+205
-18
@@ -1,19 +1,158 @@
|
||||
---@type QuestieMap
|
||||
local QuestieMap = QuestieLoader:ImportModule("QuestieMap");
|
||||
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
|
||||
|
||||
local mapData = QuestieCompat.UiMapData -- table { width, height, left, top, .instance, .name, .mapType }
|
||||
local worldMapData = QuestieCompat.worldMapData -- table { width, height, left, top }
|
||||
|
||||
-- Keep a reference to the real HBD library (if available) so we can fall back to its map data
|
||||
-- for maps not present in Questie's UiMapData (e.g., custom Ascension maps).
|
||||
local RealHBD
|
||||
|
||||
-- Try to load the real HBD library under several known names, and if that fails,
|
||||
-- scan the global environment for any table that looks like an HBD library (has .mapData).
|
||||
local function LoadRealHBD()
|
||||
-- Common library identifiers
|
||||
for _, name in ipairs({"HereBeDragonsQuestie-2.0", "HereBeDragons-2.0", "HereBeDragons"}) do
|
||||
local ok, lib = pcall(LibStub, name, true)
|
||||
if ok and lib and lib.mapData then
|
||||
return lib
|
||||
end
|
||||
end
|
||||
-- Fallback: brute-force global scan for a table with a mapData field
|
||||
-- Use pcall protection because Ascension's client may have protected globals.
|
||||
local ok, _ = pcall(function()
|
||||
for _, v in pairs(_G) do
|
||||
if type(v) == "table" and v.mapData and type(v.mapData) == "table" then
|
||||
RealHBD = v
|
||||
error("_found") -- break out of pcall early
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- RealHBD was set inside the pcall if found; otherwise stays nil
|
||||
return RealHBD
|
||||
end
|
||||
|
||||
-- Eager attempt at load; will also lazy-load on first use.
|
||||
-- Wrap in pcall so HBD.lua doesn't fail to load if the global scan hits a protected table.
|
||||
pcall(function()
|
||||
RealHBD = LoadRealHBD()
|
||||
end)
|
||||
|
||||
local HBD = {mapData = mapData}
|
||||
QuestieCompat.HBD = HBD
|
||||
|
||||
-- Ascension zone remapping: Sunstrider Isle (1241) and its ghost map (946) share
|
||||
-- Eversong Woods' (1941) rendered area on Ascension's client. On Ascension,
|
||||
-- the Sunstrider Isle map actually uses Eversong's coordinate space — not retail's
|
||||
-- separate Sunstrider bounds. This means:
|
||||
-- 1. Visibility: zones sharing the same space should show each other's pins
|
||||
-- 2. Bounds: mapData[1241] must use Eversong's bounds so coordinate round-trips
|
||||
-- produce correct positions on Ascension's Sunstrider map
|
||||
-- 3. Data: Questie spawn coords for Sunstrider (uiMapId=1241) are in the
|
||||
-- 33-38%/18-25% range, which maps to Sunstrider's location WITHIN Eversong
|
||||
--
|
||||
-- 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)
|
||||
}
|
||||
|
||||
--- Resolve a zone ID through the redirect table.
|
||||
--- If the zone has a redirect, return the target zone; otherwise return the zone as-is.
|
||||
local function ResolveZone(zone)
|
||||
return ZONE_REDIRECT[zone] or zone
|
||||
end
|
||||
|
||||
-- Expose for external use (e.g. QuestieCompat coordinate calibration)
|
||||
HBD.ResolveZone = ResolveZone
|
||||
|
||||
-- Ascension bounds override: On Ascension, Sunstrider Isle (1241) uses Eversong
|
||||
-- Woods' coordinate space, not retail's separate Sunstrider bounds. The retail
|
||||
-- mapData[1241] bounds (510, 500, -6983.33, 9766.67) are incompatible with
|
||||
-- Ascension's world coordinates — UnitPosition returns Eversong-scale values
|
||||
-- like (503.4, 267.9) which produce zone coords of (-14.68, 18.99) when converted
|
||||
-- through retail Sunstrider bounds. Using Eversong's bounds makes coordinate
|
||||
-- round-trips work correctly and positions pins at their actual locations.
|
||||
--
|
||||
-- Ghost map 946 has all-zeros bounds in retail, which makes it unusable; redirect
|
||||
-- to Eversong bounds as well (it shares the same rendered area on Ascension).
|
||||
local ASCENSION_ZONE_BOUNDS = {
|
||||
[1241] = { 1600.0, 1066.666666666667, -2721.0066, 8433.9360 }, -- Calibrated bounds for Sunstrider Isle
|
||||
[946] = { 4925.0, 3283.33333, -1824.6778, 8641.6666 }, -- Ghost map shares Eversong geometry
|
||||
}
|
||||
|
||||
|
||||
-- Apply Ascension bounds overrides immediately
|
||||
local _boundsApplied = false
|
||||
local function ApplyAscensionBounds()
|
||||
if _boundsApplied then return end
|
||||
_boundsApplied = true
|
||||
|
||||
if not RealHBD then
|
||||
pcall(function() RealHBD = LoadRealHBD() end)
|
||||
end
|
||||
|
||||
for zoneId, bounds in pairs(ASCENSION_ZONE_BOUNDS) do
|
||||
local data = mapData[zoneId]
|
||||
if data then
|
||||
-- [HBD-Ascension] bounds override applied (debug disabled)
|
||||
data[1], data[2], data[3], data[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
||||
else
|
||||
-- [HBD-Ascension] WARNING: No mapData for zone (debug disabled)
|
||||
end
|
||||
|
||||
-- Override the real HBD map data as well, because HBD-Pins bypasses QuestieCompat
|
||||
if RealHBD and RealHBD.mapData and RealHBD.mapData[zoneId] then
|
||||
local rData = RealHBD.mapData[zoneId]
|
||||
rData[1], rData[2], rData[3], rData[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
||||
end
|
||||
end
|
||||
end
|
||||
ApplyAscensionBounds()
|
||||
|
||||
-- One-shot debug: print mapData bounds for key zones on PLAYER_LOGIN
|
||||
local _boundsDebugPrinted = false
|
||||
local function PrintBoundsDebug()
|
||||
if _boundsDebugPrinted then return end
|
||||
_boundsDebugPrinted = true
|
||||
ApplyAscensionBounds()
|
||||
for _, id in ipairs({1241, 946, 1941}) do
|
||||
local d = mapData[id]
|
||||
if d then
|
||||
-- [HBD-Bounds] debug disabled
|
||||
else
|
||||
-- [HBD-Bounds] no mapData (debug disabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:SetScript("OnEvent", function(self, event)
|
||||
PrintBoundsDebug()
|
||||
self:UnregisterEvent(event)
|
||||
end)
|
||||
|
||||
--- Convert local/point coordinates to world coordinates in yards
|
||||
-- @param x X position in 0-1 point coordinates
|
||||
-- @param y Y position in 0-1 point coordinates
|
||||
-- @param zone uiMapID of the zone
|
||||
--- @param x X position in 0-1 point coordinates
|
||||
--- @param y Y position in 0-1 point coordinates
|
||||
--- @param zone uiMapID of the zone
|
||||
function HBD:GetWorldCoordinatesFromZone(x, y, zone)
|
||||
-- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds
|
||||
-- at startup, so coordinate calls for these zones now use Eversong's coordinate
|
||||
-- space naturally. No redirect needed here.
|
||||
local data = mapData[zone]
|
||||
if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil end
|
||||
if not data or data[1] == 0 or data[2] == 0 then
|
||||
-- Attempt to lazy-load the real HBD if we haven't yet
|
||||
if not RealHBD then
|
||||
pcall(function() RealHBD = LoadRealHBD() end)
|
||||
end
|
||||
if RealHBD and RealHBD.mapData then
|
||||
data = RealHBD.mapData[zone]
|
||||
end
|
||||
if not data or data[1] == 0 or data[2] == 0 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
end
|
||||
if not x or not y then return nil, nil, nil end
|
||||
|
||||
local width, height, left, top = data[1], data[2], data[3], data[4]
|
||||
@@ -23,13 +162,26 @@ function HBD:GetWorldCoordinatesFromZone(x, y, zone)
|
||||
end
|
||||
|
||||
--- Convert world coordinates to local/point zone coordinates
|
||||
-- @param x Global X position
|
||||
-- @param y Global Y position
|
||||
-- @param zone uiMapID of the zone
|
||||
-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
||||
--- @param x Global X position
|
||||
--- @param y Global Y position
|
||||
--- @param zone uiMapID of the zone
|
||||
--- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
||||
function HBD:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds)
|
||||
-- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds
|
||||
-- at startup, so coordinate calls for these zones now use Eversong's coordinate
|
||||
-- space naturally. No redirect needed here.
|
||||
local data = mapData[zone]
|
||||
if not data or data[1] == 0 or data[2] == 0 then return nil, nil end
|
||||
if not data or data[1] == 0 or data[2] == 0 then
|
||||
if not RealHBD then
|
||||
pcall(function() RealHBD = LoadRealHBD() end)
|
||||
end
|
||||
if RealHBD and RealHBD.mapData then
|
||||
data = RealHBD.mapData[zone]
|
||||
end
|
||||
if not data or data[1] == 0 or data[2] == 0 then
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
if not x or not y then return nil, nil end
|
||||
|
||||
local width, height, left, top = data[1], data[2], data[3], data[4]
|
||||
@@ -312,10 +464,21 @@ local function drawMinimapPin(pin, data)
|
||||
end
|
||||
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
|
||||
|
||||
return HBD:GetPlayerWorldPosition()
|
||||
end
|
||||
|
||||
local function UpdateMinimapPins(force)
|
||||
-- get the current player position
|
||||
local x, y, instanceID = HBD:GetPlayerWorldPosition()
|
||||
local mapID = HBD:GetPlayerZone()
|
||||
local x, y, instanceID = _GetEffectiveMinimapPlayerWorldPosition()
|
||||
|
||||
-- get data from the API for calculations
|
||||
local zoom = pins.Minimap:GetZoom()
|
||||
@@ -410,7 +573,7 @@ local function UpdateMinimapIconPosition()
|
||||
-- we have no active minimap pins, just return early
|
||||
if minimapPinCount == 0 then return end
|
||||
|
||||
local x, y = HBD:GetPlayerWorldPosition()
|
||||
local x, y = _GetEffectiveMinimapPlayerWorldPosition()
|
||||
|
||||
-- for rotating minimap support
|
||||
local facing
|
||||
@@ -504,10 +667,30 @@ local function HandleWorldMapPin(icon, data)
|
||||
if not uiMapID then return end
|
||||
|
||||
--Questie Modification
|
||||
if (Questie.db.profile.hideIconsOnContinents == true) and (HBD.mapData[uiMapID].mapType == Enum.UIMapType.Continent or uiMapID == 947) or (uiMapID ~= data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then
|
||||
-- Child-map / zone-redirect exception: if we're viewing a child map (e.g. Sunstrider 1241)
|
||||
-- and the pin belongs to a parent map (e.g. Eversong 1941) or a zone that shares the same
|
||||
-- coordinate space, SHOW_CURRENT pins should still be visible.
|
||||
-- Also handles the reverse: pins tagged 1241/946 should show on 1941, and vice versa,
|
||||
-- because these maps share the same rendered area on Ascension.
|
||||
local effectiveUiMapID = ResolveZone(uiMapID)
|
||||
local effectiveDataUiMapID = ResolveZone(data.uiMapID)
|
||||
local isChildMap = false
|
||||
local ancestorMapID = HBD.mapData[uiMapID] and HBD.mapData[uiMapID].parentMapID
|
||||
while ancestorMapID and HBD.mapData[ancestorMapID] do
|
||||
if ancestorMapID == data.uiMapID then
|
||||
isChildMap = true
|
||||
break
|
||||
end
|
||||
ancestorMapID = HBD.mapData[ancestorMapID].parentMapID
|
||||
end
|
||||
-- Zone-redirect equivalence: if the viewed map and pin's map redirect to the same
|
||||
-- target, they share the same coordinate space and should show each other's pins.
|
||||
local isSameZoneSpace = (effectiveUiMapID == effectiveDataUiMapID)
|
||||
|
||||
if (Questie.db.profile.hideIconsOnContinents == true) and (HBD.mapData[uiMapID].mapType == Enum.UIMapType.Continent or uiMapID == 947) or (uiMapID ~= data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT and not isChildMap and not isSameZoneSpace) then
|
||||
icon:Hide();
|
||||
return;
|
||||
elseif(uiMapID == data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then
|
||||
elseif(uiMapID == data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isChildMap and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isSameZoneSpace and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then
|
||||
icon:Show();
|
||||
end
|
||||
|
||||
@@ -535,7 +718,7 @@ local function HandleWorldMapPin(icon, data)
|
||||
end
|
||||
else
|
||||
local show = true -- Questie fix to show icons in neighbour areas
|
||||
local parentMapID = HBD.mapData[data.uiMapID].parent
|
||||
local parentMapID = HBD.mapData[data.uiMapID].parentMapID
|
||||
while parentMapID and HBD.mapData[parentMapID] do
|
||||
if parentMapID == uiMapID then
|
||||
local parentMapType = HBD.mapData[parentMapID].mapType
|
||||
@@ -554,7 +737,7 @@ local function HandleWorldMapPin(icon, data)
|
||||
break
|
||||
-- worldmap is handled above already
|
||||
else
|
||||
parentMapID = HBD.mapData[parentMapID].parent
|
||||
parentMapID = HBD.mapData[parentMapID].parentMapID
|
||||
end
|
||||
end
|
||||
|
||||
@@ -563,7 +746,11 @@ local function HandleWorldMapPin(icon, data)
|
||||
end
|
||||
|
||||
-- translate coordinates
|
||||
-- Ascension: mapData[1241] and [946] have been overridden with Eversong bounds,
|
||||
-- so cross-zone pin positioning (e.g., Eversong 1941 pins on Sunstrider 1241 map)
|
||||
-- works naturally — both zones share the same coordinate space.
|
||||
x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, uiMapID)
|
||||
-- [HBD-Pins] pin position debug disabled
|
||||
end
|
||||
|
||||
if x and y then
|
||||
@@ -896,4 +1083,4 @@ function pins:RemoveAllWorldMapIcons(ref)
|
||||
icon:SetParent(UiParent)
|
||||
end
|
||||
wipe(worldmapPinRegistry[ref])
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user