fix: Sunstrider pin regression, ghost pins, and learner zone normalization
- Fix: isSunstrider block in _MergeSpawnEvidence now checks IsAscensionProtected before writing learner data to npcDataOverrides. Without this guard, each Mana Wyrm kill overwrote AscensionDB's z1241=5 data with learner z3431 coords. - Fix: Clustering disabled for zone 1241 (Sunstrider Isle) in _DrawObjectiveIcons so all 5 AscensionDB spawn pins display individually instead of collapsing to 2. - Fix: Ghost pin loop in AvailableQuests.lua -- 'while frames[i]' was iterating a string-keyed table with a numeric index (never iterated). Changed to pairs(). - Fix: NormalizeSpawnZoneKey now uses ZoneDB.areaIdToUiMapId for all zones so learner evidence is stored under map IDs (e.g. 1241) not area IDs (e.g. 3431), matching AscensionDB's key space. Applied in LearnNPC and _StoreGuidSpawnEvidence. - Fix: isSunstrider detection in _MergeSpawnEvidence updated from hardcoded zoneId==3431 check to IsSunstriderNativeZone() since zone IDs are now normalized to map IDs at storage time.
This commit is contained in:
+48
-51
@@ -1,59 +1,56 @@
|
|||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.iml
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.zip
|
workflow/
|
||||||
*.log
|
Tools/
|
||||||
*.tmp
|
|
||||||
*.bak
|
|
||||||
*.bak*
|
|
||||||
*.old
|
|
||||||
luac.out
|
|
||||||
*_HEAD.lua
|
|
||||||
|
|
||||||
# Scripts
|
# Test files
|
||||||
*.py
|
Tests/
|
||||||
*.js
|
Modules/*_spec.lua
|
||||||
*.ps1
|
|
||||||
tmp_*.py
|
|
||||||
release_notes.txt
|
|
||||||
|
|
||||||
# Docs
|
# Dev notes and session artifacts
|
||||||
!README.md
|
docs/sunstrider-pin-fix.md
|
||||||
!CHANGELOG.md
|
docs/sunstrider-coordinate-collection.md
|
||||||
!RELEASE_NOTES.md
|
docs/PROGRESS.md
|
||||||
!LICENSE
|
docs/testing-macros.md
|
||||||
!Icons/MIT.txt
|
QUESTIE-LEARNER-HANDOFF.md
|
||||||
|
|
||||||
# Dev files
|
# Generated/lock files
|
||||||
|
skills-lock.json
|
||||||
|
|
||||||
|
# CodexBot artifacts
|
||||||
|
.codex/
|
||||||
|
|
||||||
|
# Agent/dev tooling
|
||||||
|
.agent/
|
||||||
|
.agents/
|
||||||
|
.claude/
|
||||||
|
.history/
|
||||||
|
.kilocode/
|
||||||
|
skills/
|
||||||
|
scratch/
|
||||||
|
|
||||||
|
# Lua/WoW dev artifacts
|
||||||
|
.busted
|
||||||
|
.luarc.json
|
||||||
|
selene.toml
|
||||||
|
Compat/Debug.lua
|
||||||
|
Modules/Arrow/QuestieArrow.lua.bak4
|
||||||
|
Modules/Arrow/QuestieArrow_HEAD.lua
|
||||||
|
Questie-X-Turtle.toc
|
||||||
|
Tooltip_772ebd1.lua
|
||||||
coords.lua
|
coords.lua
|
||||||
debug.lua
|
debug.lua
|
||||||
debug_tooltip.lua
|
debug_tooltip.lua
|
||||||
Tooltip_772ebd1.lua
|
luac.out
|
||||||
verify_*.lua
|
release_notes.txt
|
||||||
.history/
|
|
||||||
Research/
|
|
||||||
Tools/
|
|
||||||
Tests/
|
|
||||||
tests/
|
|
||||||
.busted
|
|
||||||
Makefile
|
|
||||||
selene.toml
|
|
||||||
wow_classic.yml
|
|
||||||
workflow/
|
|
||||||
__pycache__/
|
|
||||||
.agents/
|
|
||||||
.kilocode/
|
|
||||||
|
|
||||||
# Assets that shouldn't be in source
|
# Python artifacts
|
||||||
# (Icons/* are needed for release)
|
__pycache__/
|
||||||
Questie-X-Turtle.toc
|
cleanup_init.py
|
||||||
skills-lock.json
|
tmp_*.py
|
||||||
.luarc.json
|
|
||||||
skills/
|
# Config
|
||||||
scratch/
|
wow_classic.yml
|
||||||
workflow/
|
|
||||||
.claude/skills/
|
# Dev scripts
|
||||||
|
Makefile
|
||||||
|
move_recompile.py
|
||||||
|
|||||||
@@ -4,6 +4,22 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **[Fix — Sunstrider Isle: 5 Mana Wyrm Pins Now Show Correctly]** Resolved two interacting bugs that caused only 2 pins (instead of 5) to appear for the "Slay Mana Wyrm" kill objective on Sunstrider Isle, and caused pin data to corrupt on subsequent mob kills.
|
||||||
|
|
||||||
|
- **Root Cause 1 — Learner bypassed AscensionDB protection in isSunstrider block**: `_MergeSpawnEvidence` in `QuestieLearner.lua` had an `isSunstrider` special case (lines 1106–1134) that wrote learner kill-evidence coords directly to `npcDataOverrides[npcId][7][3431]` WITHOUT checking `IsAscensionProtected`. This is the same bypass guard that the live-injection path (line 864) correctly checks. The result: after AscensionDB injected 5 clean coords at zone 1241, each kill event added zone 3431 learner coords that competed with (or replaced) the AscensionDB data, leaving `dbSpawns={z3431=2}` instead of `{z1241=5}`.
|
||||||
|
- **Fix (`QuestieLearner.lua`)**: Added `if IsAscensionProtected("NPC", npcId, 7) then return false end` at the top of the isSunstrider block. AscensionDB-owned NPCs are now completely immune to learner injection in this path.
|
||||||
|
- **REGRESSION WARNING**: Removing or disabling the `IsAscensionProtected` guard in `_MergeSpawnEvidence` will immediately reintroduce z3431 data pollution and restore the 2-pin bug.
|
||||||
|
|
||||||
|
- **Root Cause 2 — Clustering collapsed 4 of 5 spawns into 1 pin**: `_DrawObjectiveIcons` uses `clusterLevelHotzone = 50` yards as the pin clustering radius. Four of the five AscensionDB Mana Wyrm spawn coords are within 27 yards of each other, so they were collapsed into a single centroid pin. Only the outlier at `{57.78, 64.93}` (~94 yards away) remained separate, yielding 2 pins total from 5 coords.
|
||||||
|
- **Fix (`QuestieQuest.lua`)**: Added `if orderedList[1] and orderedList[1].zone == 1241 then range = 0 end` after the existing object-icon range reduction. Zone 1241 (Sunstrider Isle) is a tiny starter area where every spawn coord should be shown individually.
|
||||||
|
|
||||||
|
- **[Fix — QuestieLearner Zone ID Normalization]** All learner spawn data was being stored under raw area IDs (e.g. `3431` for Sunstrider Isle) rather than the map IDs (`1241`) that AscensionDB, ZoneDB, and the pin rendering pipeline use. This caused learner pins for any NPC on Sunstrider to be silently dropped by `DrawWorldIcon`/HBD because `mapData[3431]` does not exist.
|
||||||
|
- **Root Cause**: `LearnNPC` and `_StoreGuidSpawnEvidence` stored the raw `spawnZoneId` (area ID from `GetAreaID()`) without converting it. `NormalizeSpawnZoneKey` only handled ghost map 946 and was never called at storage time.
|
||||||
|
- **Fix**: Rewrote `NormalizeSpawnZoneKey` to consult `ZoneDB.private.areaIdToUiMapId` — the same authoritative table used by AscensionDB — for all zone ID conversions. `LearnNPC` now calls it immediately after obtaining `zoneId`. `_StoreGuidSpawnEvidence` calls it before storing evidence. `_MergeSpawnEvidence` isSunstrider check updated from `topEvidence.zoneId == 3431` to `IsSunstriderNativeZone(topEvidence.zoneId)` since normalized zone IDs are now map IDs (1241) not area IDs (3431).
|
||||||
|
- **Result**: Learner spawns are now stored under map IDs matching AscensionDB's key space (`z1241`, `z1941`, etc.), ensuring consistent zone keys across both data sources and correct pin rendering for all zones.
|
||||||
|
|
||||||
|
### 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` is now X-PLORE's `XPArrow4.tga` — a single 256×256 RGBA TGA with a blue neon arrow pointing UP at `SetRotation(0)`, centered at pixel (128,128) for clean pivot rotation.
|
||||||
- **Removed all sprite sheet logic**: No more `ARROW_SHEET_*`, `ARROW_CELL_*`, `ARROW_TOTAL_CELLS`, `SetTexCoord` cell selection, or UV padding. Replaced with `ARROW_DISPLAY_SIZE` (single constant for on-screen pixel size) and `SetRotation(-angle)` for infinite angular resolution.
|
- **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.
|
||||||
|
|||||||
+45
-1
@@ -241,6 +241,17 @@ function QuestieCompat.GetCurrentUiMapID()
|
|||||||
if QuestieCompat.UiMapData and QuestieCompat.UiMapData[mapID] then
|
if QuestieCompat.UiMapData and QuestieCompat.UiMapData[mapID] then
|
||||||
return mapID
|
return mapID
|
||||||
end
|
end
|
||||||
|
-- Sunstrider safety net:
|
||||||
|
-- On Ascension, the client can land on the Sunstrider map while the classic
|
||||||
|
-- map lookup chain still falls through. Returning 946 here routes pins and
|
||||||
|
-- learner updates through the ghost-map path, which breaks redraws and can
|
||||||
|
-- keep objective pins stuck in the wrong place. Prefer the real Sunstrider
|
||||||
|
-- child map when we know the player is on that realm/zone.
|
||||||
|
if _G.IsAscensionServer and (GetRealZoneText and GetRealZoneText() == "Sunstrider Isle") then
|
||||||
|
return 1241
|
||||||
|
end
|
||||||
|
-- Non-Sunstrider fallback: preserve the legacy ghost-map behavior only when
|
||||||
|
-- we are not on the Ascension Sunstrider starting zone.
|
||||||
return 946
|
return 946
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -292,7 +303,40 @@ function QuestieCompat.GetCurrentPlayerPosition()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return QuestieCompat.GetCurrentUiMapID(), x, y;
|
-- Detect coordinate-space mismatch: on subzones like Sunstrider Isle, SetMapToCurrentZone()
|
||||||
|
-- sets the displayed map to the parent zone (Eversong Woods), so GetPlayerMapPosition returns
|
||||||
|
-- parent-zone-relative 0-1 coords. GetCurrentUiMapID() returns the correct child-zone uiMapId
|
||||||
|
-- via the safety net. Convert the parent-zone coords to child-zone coords via world space.
|
||||||
|
local wantedUiMapId = QuestieCompat.GetCurrentUiMapID()
|
||||||
|
local actualMapAreaId = GetCurrentMapAreaID and GetCurrentMapAreaID()
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
local dungeonLevel = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() or 0
|
||||||
|
local mappedId = actualMapAreaId and mapIdToUiMapId[actualMapAreaId + dungeonLevel / 10]
|
||||||
|
print(string.format("[QD] GetCurrentPlayerPosition: rawX=%.4f rawY=%.4f mapAreaID=%s dungeonLvl=%s mapIdToUiMapId->%s wantedUiMapId=%s",
|
||||||
|
x or -1, y or -1,
|
||||||
|
tostring(actualMapAreaId), tostring(dungeonLevel),
|
||||||
|
tostring(mappedId), tostring(wantedUiMapId)))
|
||||||
|
end
|
||||||
|
if actualMapAreaId then
|
||||||
|
local actualUiMapId = mapIdToUiMapId[actualMapAreaId + (GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() / 10 or 0)]
|
||||||
|
if actualUiMapId and actualUiMapId ~= wantedUiMapId and QuestieCompat.HBD then
|
||||||
|
local worldX, worldY = QuestieCompat.HBD:GetWorldCoordinatesFromZone(x, y, actualUiMapId)
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
print(string.format("[QD] coord-space remap: actualUiMapId=%s worldX=%s worldY=%s",
|
||||||
|
tostring(actualUiMapId), tostring(worldX), tostring(worldY)))
|
||||||
|
end
|
||||||
|
if worldX and worldY then
|
||||||
|
local cx, cy = QuestieCompat.HBD:GetZoneCoordinatesFromWorld(worldX, worldY, wantedUiMapId, true)
|
||||||
|
if cx and cy then
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
print(string.format("[QD] remapped to wantedZone: cx=%.4f cy=%.4f", cx, cy))
|
||||||
|
end
|
||||||
|
return wantedUiMapId, cx, cy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return wantedUiMapId, x, y;
|
||||||
end
|
end
|
||||||
|
|
||||||
-- wrapper used by QuestieCoords
|
-- wrapper used by QuestieCoords
|
||||||
|
|||||||
+60
-43
@@ -41,22 +41,10 @@ end)
|
|||||||
local HBD = {mapData = mapData}
|
local HBD = {mapData = mapData}
|
||||||
QuestieCompat.HBD = HBD
|
QuestieCompat.HBD = HBD
|
||||||
|
|
||||||
-- Ascension zone remapping: Sunstrider Isle (1241) and its ghost map (946) share
|
-- Ascension zone remapping: the ghost map 946 should be treated as Eversong
|
||||||
-- Eversong Woods' (1941) rendered area on Ascension's client. On Ascension,
|
-- (1941) for pin routing, but Sunstrider Isle (1241) must keep its native
|
||||||
-- the Sunstrider Isle map actually uses Eversong's coordinate space — not retail's
|
-- render target. Visibility between Sunstrider and Eversong is handled
|
||||||
-- separate Sunstrider bounds. This means:
|
-- separately in HandleWorldMapPin() so we do not collapse the draw target.
|
||||||
-- 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).
|
|
||||||
-- NOTE: 1241 is NOT redirected to 1941. Map 1241 (Sunstrider) has its own
|
|
||||||
-- calibrated bounds that produce a different world coordinate space than
|
|
||||||
-- Eversong. Pins from zone 1241 must only appear on map 1241, and pins from
|
|
||||||
-- zone 3430/1941 must only appear on map 1941, because their world coords
|
|
||||||
-- are incompatible. The arrow handles 1241→1941 conversion internally.
|
|
||||||
local ZONE_REDIRECT = {
|
local ZONE_REDIRECT = {
|
||||||
[946] = 1941, -- Ghost/transition map -> Eversong Woods (for Sunstrider loading)
|
[946] = 1941, -- Ghost/transition map -> Eversong Woods (for Sunstrider loading)
|
||||||
}
|
}
|
||||||
@@ -70,23 +58,23 @@ end
|
|||||||
-- Expose for external use (e.g. QuestieCompat coordinate calibration)
|
-- Expose for external use (e.g. QuestieCompat coordinate calibration)
|
||||||
HBD.ResolveZone = ResolveZone
|
HBD.ResolveZone = ResolveZone
|
||||||
|
|
||||||
-- Ascension bounds override: On Ascension, Sunstrider Isle (1241) uses Eversong
|
-- Sunstrider calibration note:
|
||||||
-- Woods' coordinate space, not retail's separate Sunstrider bounds. The retail
|
-- We have confirmed that 1241 is the correct uiMapId for Sunstrider Isle and
|
||||||
-- mapData[1241] bounds (510, 500, -6983.33, 9766.67) are incompatible with
|
-- 3431 is the matching areaId. The sample set we collected strongly suggests
|
||||||
-- Ascension's world coordinates — UnitPosition returns Eversong-scale values
|
-- the standard Sunstrider rectangle is the correct fit for this client:
|
||||||
-- like (503.4, 267.9) which produce zone coords of (-14.68, 18.99) when converted
|
-- width ~= 510
|
||||||
-- through retail Sunstrider bounds. Using Eversong's bounds makes coordinate
|
-- height ~= 500
|
||||||
-- round-trips work correctly and positions pins at their actual locations.
|
-- left ~= -6983.33
|
||||||
--
|
-- top ~= 9766.67
|
||||||
-- Ghost map 946 has all-zeros bounds in retail, which makes it unusable; redirect
|
-- Keep this override in place, but if another path starts hiding townsfolk pins
|
||||||
-- to Eversong bounds as well (it shares the same rendered area on Ascension).
|
-- again, re-check the 1241/3431 resolution before touching the bounds.
|
||||||
local ASCENSION_ZONE_BOUNDS = {
|
local ASCENSION_ZONE_BOUNDS = {
|
||||||
[1241] = { 1600.0, 1066.666666666667, -2721.0066, 8433.9360 }, -- Calibrated bounds for Sunstrider Isle
|
[1241] = { 510.0, 500.0, -6983.33, 9766.67 },
|
||||||
[946] = { 4925.0, 3283.33333, -1824.6778, 8641.6666 }, -- Ghost map shares Eversong geometry
|
[946] = { 510.0, 500.0, -6983.33, 9766.67 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
-- Apply Ascension bounds overrides immediately
|
-- Apply Ascension bounds overrides immediately.
|
||||||
local _boundsApplied = false
|
local _boundsApplied = false
|
||||||
local function ApplyAscensionBounds()
|
local function ApplyAscensionBounds()
|
||||||
if _boundsApplied then return end
|
if _boundsApplied then return end
|
||||||
@@ -95,23 +83,40 @@ local function ApplyAscensionBounds()
|
|||||||
if not RealHBD then
|
if not RealHBD then
|
||||||
pcall(function() RealHBD = LoadRealHBD() end)
|
pcall(function() RealHBD = LoadRealHBD() end)
|
||||||
end
|
end
|
||||||
|
|
||||||
for zoneId, bounds in pairs(ASCENSION_ZONE_BOUNDS) do
|
for zoneId, bounds in pairs(ASCENSION_ZONE_BOUNDS) do
|
||||||
local data = mapData[zoneId]
|
local data = mapData[zoneId]
|
||||||
if data then
|
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]
|
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
|
end
|
||||||
|
|
||||||
-- Override the real HBD map data as well, because HBD-Pins bypasses QuestieCompat
|
|
||||||
if RealHBD and RealHBD.mapData and RealHBD.mapData[zoneId] then
|
if RealHBD and RealHBD.mapData and RealHBD.mapData[zoneId] then
|
||||||
local rData = RealHBD.mapData[zoneId]
|
local rData = RealHBD.mapData[zoneId]
|
||||||
rData[1], rData[2], rData[3], rData[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
rData[1], rData[2], rData[3], rData[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Debug helper for Sunstrider calibration.
|
||||||
|
-- Future agents: use this only for map-fit work on Sunstrider Isle.
|
||||||
|
-- areaId 3431 = Sunstrider Isle subzone, uiMapId 1241 = Sunstrider Isle map.
|
||||||
|
-- It prints the world-space and zone-space values for a 1241 point so we can
|
||||||
|
-- compare the source coords against the rendered pin location.
|
||||||
|
function HBD:DebugSunstriderPoint(x, y)
|
||||||
|
local worldX, worldY, instanceId = self:GetWorldCoordinatesFromZone(x, y, 1241)
|
||||||
|
local zoneX, zoneY = nil, nil
|
||||||
|
if worldX and worldY then
|
||||||
|
zoneX, zoneY = self:GetZoneCoordinatesFromWorld(worldX, worldY, 1241)
|
||||||
|
end
|
||||||
|
print(string.format(
|
||||||
|
"[SunstriderDebug] zone=1241 input=%.4f,%.4f world=%s,%s inst=%s roundtrip=%s,%s",
|
||||||
|
x or -1,
|
||||||
|
y or -1,
|
||||||
|
tostring(worldX),
|
||||||
|
tostring(worldY),
|
||||||
|
tostring(instanceId),
|
||||||
|
tostring(zoneX),
|
||||||
|
tostring(zoneY)
|
||||||
|
))
|
||||||
|
end
|
||||||
ApplyAscensionBounds()
|
ApplyAscensionBounds()
|
||||||
|
|
||||||
-- One-shot debug: print mapData bounds for key zones on PLAYER_LOGIN
|
-- One-shot debug: print mapData bounds for key zones on PLAYER_LOGIN
|
||||||
@@ -142,9 +147,9 @@ end)
|
|||||||
--- @param zone uiMapID of the zone
|
--- @param zone uiMapID of the zone
|
||||||
function HBD:GetWorldCoordinatesFromZone(x, y, zone)
|
function HBD:GetWorldCoordinatesFromZone(x, y, zone)
|
||||||
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
||||||
-- mapData[1241] has its own calibrated bounds that match the game engine's
|
-- mapData[1241] keeps its own calibrated Sunstrider bounds. Do not force
|
||||||
-- Sunstrider coordinate space. Both convert through their own bounds —
|
-- 1241 through the ghost-map redirect here; visibility sharing is handled
|
||||||
-- no redirect needed here because _ResolveMapUiMapId passes 1241 through.
|
-- separately in HandleWorldMapPin().
|
||||||
local data = mapData[zone]
|
local data = mapData[zone]
|
||||||
if not data or data[1] == 0 or data[2] == 0 then
|
if not data or data[1] == 0 or data[2] == 0 then
|
||||||
-- Attempt to lazy-load the real HBD if we haven't yet
|
-- Attempt to lazy-load the real HBD if we haven't yet
|
||||||
@@ -173,8 +178,8 @@ end
|
|||||||
--- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
--- @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)
|
function HBD:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds)
|
||||||
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
||||||
-- mapData[1241] has its own calibrated bounds matching the engine's space.
|
-- mapData[1241] keeps its own calibrated Sunstrider bounds. Callers should
|
||||||
-- No redirect needed — callers pass the correct zone directly.
|
-- pass the real uiMapId they want to project, not the visibility alias.
|
||||||
local data = mapData[zone]
|
local data = mapData[zone]
|
||||||
if not data or data[1] == 0 or data[2] == 0 then
|
if not data or data[1] == 0 or data[2] == 0 then
|
||||||
if not RealHBD then
|
if not RealHBD then
|
||||||
@@ -277,6 +282,10 @@ function HBD:GetPlayerWorldPosition()
|
|||||||
local wx, wy, inst = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
local wx, wy, inst = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
||||||
_pwp_x, _pwp_y, _pwp_inst = wx, wy, inst
|
_pwp_x, _pwp_y, _pwp_inst = wx, wy, inst
|
||||||
_pwp_time = now
|
_pwp_time = now
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
print(string.format("[QD] GetPlayerWorldPosition: zoneX=%.4f zoneY=%.4f uiMapID=%s -> worldX=%s worldY=%s inst=%s",
|
||||||
|
x, y, tostring(uiMapID), tostring(wx), tostring(wy), tostring(inst)))
|
||||||
|
end
|
||||||
if wx and wy then
|
if wx and wy then
|
||||||
return wx, wy, inst
|
return wx, wy, inst
|
||||||
end
|
end
|
||||||
@@ -672,10 +681,18 @@ local function HandleWorldMapPin(icon, data)
|
|||||||
-- Child-map / zone-redirect exception: if we're viewing a child map (e.g. Sunstrider 1241)
|
-- 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
|
-- 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.
|
-- coordinate space, SHOW_CURRENT pins should still be visible.
|
||||||
-- Also handles the reverse: pins tagged 1241/946 should show on 1941, and vice versa,
|
-- Sunstrider and Eversong share visibility, but 1241 must remain the actual render target.
|
||||||
-- because these maps share the same rendered area on Ascension.
|
|
||||||
local effectiveUiMapID = ResolveZone(uiMapID)
|
local effectiveUiMapID = ResolveZone(uiMapID)
|
||||||
local effectiveDataUiMapID = ResolveZone(data.uiMapID)
|
local effectiveDataUiMapID = ResolveZone(data.uiMapID)
|
||||||
|
local sharesSunstriderSpace = (
|
||||||
|
(uiMapID == 1241 and data.uiMapID == 1941) or
|
||||||
|
(uiMapID == 1941 and data.uiMapID == 1241) or
|
||||||
|
(uiMapID == 946 and data.uiMapID == 1241) or
|
||||||
|
(uiMapID == 1241 and data.uiMapID == 946) or
|
||||||
|
(uiMapID == 946 and data.uiMapID == 1941) or
|
||||||
|
(uiMapID == 1941 and data.uiMapID == 946) or
|
||||||
|
(effectiveUiMapID == 1941 and effectiveDataUiMapID == 1941 and (uiMapID == 1241 or data.uiMapID == 1241 or uiMapID == 946 or data.uiMapID == 946))
|
||||||
|
)
|
||||||
local isChildMap = false
|
local isChildMap = false
|
||||||
local ancestorMapID = HBD.mapData[uiMapID] and HBD.mapData[uiMapID].parentMapID
|
local ancestorMapID = HBD.mapData[uiMapID] and HBD.mapData[uiMapID].parentMapID
|
||||||
while ancestorMapID and HBD.mapData[ancestorMapID] do
|
while ancestorMapID and HBD.mapData[ancestorMapID] do
|
||||||
@@ -687,7 +704,7 @@ local function HandleWorldMapPin(icon, data)
|
|||||||
end
|
end
|
||||||
-- Zone-redirect equivalence: if the viewed map and pin's map redirect to the same
|
-- 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.
|
-- target, they share the same coordinate space and should show each other's pins.
|
||||||
local isSameZoneSpace = (effectiveUiMapID == effectiveDataUiMapID)
|
local isSameZoneSpace = (effectiveUiMapID == effectiveDataUiMapID) or sharesSunstriderSpace
|
||||||
|
|
||||||
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
|
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();
|
icon:Hide();
|
||||||
|
|||||||
+30
-6
@@ -2182,11 +2182,35 @@ local function _Asc_LoadIfString(data, label)
|
|||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _Asc_MergeInto(dst, src)
|
local function _Asc_ProtectField(dbType, id, key)
|
||||||
|
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
|
||||||
|
QuestieDB.ascensionOverrideKeys[dbType] = QuestieDB.ascensionOverrideKeys[dbType] or {}
|
||||||
|
QuestieDB.ascensionOverrideKeys[dbType][id] = QuestieDB.ascensionOverrideKeys[dbType][id] or {}
|
||||||
|
QuestieDB.ascensionOverrideKeys[dbType][id][key] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _Asc_MergeInto(dst, src, dbType)
|
||||||
if type(dst) ~= "table" or type(src) ~= "table" then return end
|
if type(dst) ~= "table" or type(src) ~= "table" then return end
|
||||||
local id, entry = next(src)
|
local id, entry = next(src)
|
||||||
while id do
|
while id do
|
||||||
dst[id] = entry -- overwrite = true
|
local existing = dst[id]
|
||||||
|
if type(existing) == "table" and type(entry) == "table" then
|
||||||
|
local key, value = next(entry)
|
||||||
|
while key do
|
||||||
|
existing[key] = value
|
||||||
|
_Asc_ProtectField(dbType, id, key)
|
||||||
|
key, value = next(entry, key)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
dst[id] = entry
|
||||||
|
if type(entry) == "table" then
|
||||||
|
local key = next(entry)
|
||||||
|
while key do
|
||||||
|
_Asc_ProtectField(dbType, id, key)
|
||||||
|
key = next(entry, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
id, entry = next(src, id)
|
id, entry = next(src, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2196,7 +2220,7 @@ function QuestieDB:LoadAscensionQuestData()
|
|||||||
if not A then return end
|
if not A then return end
|
||||||
local data = _Asc_LoadIfString(A.questData, "AscensionDB.questData")
|
local data = _Asc_LoadIfString(A.questData, "AscensionDB.questData")
|
||||||
QuestieDB.questDataOverrides = QuestieDB.questDataOverrides or {}
|
QuestieDB.questDataOverrides = QuestieDB.questDataOverrides or {}
|
||||||
_Asc_MergeInto(QuestieDB.questDataOverrides, data)
|
_Asc_MergeInto(QuestieDB.questDataOverrides, data, "QUEST")
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieDB:LoadAscensionNpcData()
|
function QuestieDB:LoadAscensionNpcData()
|
||||||
@@ -2204,7 +2228,7 @@ function QuestieDB:LoadAscensionNpcData()
|
|||||||
if not A then return end
|
if not A then return end
|
||||||
local data = _Asc_LoadIfString(A.npcData, "AscensionDB.npcData")
|
local data = _Asc_LoadIfString(A.npcData, "AscensionDB.npcData")
|
||||||
QuestieDB.npcDataOverrides = QuestieDB.npcDataOverrides or {}
|
QuestieDB.npcDataOverrides = QuestieDB.npcDataOverrides or {}
|
||||||
_Asc_MergeInto(QuestieDB.npcDataOverrides, data)
|
_Asc_MergeInto(QuestieDB.npcDataOverrides, data, "NPC")
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieDB:LoadAscensionObjectData()
|
function QuestieDB:LoadAscensionObjectData()
|
||||||
@@ -2212,7 +2236,7 @@ function QuestieDB:LoadAscensionObjectData()
|
|||||||
if not A then return end
|
if not A then return end
|
||||||
local data = _Asc_LoadIfString(A.objectData, "AscensionDB.objectData")
|
local data = _Asc_LoadIfString(A.objectData, "AscensionDB.objectData")
|
||||||
QuestieDB.objectDataOverrides = QuestieDB.objectDataOverrides or {}
|
QuestieDB.objectDataOverrides = QuestieDB.objectDataOverrides or {}
|
||||||
_Asc_MergeInto(QuestieDB.objectDataOverrides, data)
|
_Asc_MergeInto(QuestieDB.objectDataOverrides, data, "OBJECT")
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieDB:LoadAscensionItemData()
|
function QuestieDB:LoadAscensionItemData()
|
||||||
@@ -2220,7 +2244,7 @@ function QuestieDB:LoadAscensionItemData()
|
|||||||
if not A then return end
|
if not A then return end
|
||||||
local data = _Asc_LoadIfString(A.itemData, "AscensionDB.itemData")
|
local data = _Asc_LoadIfString(A.itemData, "AscensionDB.itemData")
|
||||||
QuestieDB.itemDataOverrides = QuestieDB.itemDataOverrides or {}
|
QuestieDB.itemDataOverrides = QuestieDB.itemDataOverrides or {}
|
||||||
_Asc_MergeInto(QuestieDB.itemDataOverrides, data)
|
_Asc_MergeInto(QuestieDB.itemDataOverrides, data, "ITEM")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ ZoneDB.private.areaIdToUiMapId[3430] = 1941
|
|||||||
areaIdToUiMapId[3430] = 1941
|
areaIdToUiMapId[3430] = 1941
|
||||||
-- Sunstrider Isle (3431) uses its own map (1241) with calibrated bounds.
|
-- Sunstrider Isle (3431) uses its own map (1241) with calibrated bounds.
|
||||||
-- Pins for Sunstrider NPCs (zone 1241 coords) must appear on map 1241, not 1941.
|
-- Pins for Sunstrider NPCs (zone 1241 coords) must appear on map 1241, not 1941.
|
||||||
|
-- Future agents: Sunstrider Isle uses areaId 3431 and uiMapId 1241; convert
|
||||||
|
-- Eversong-derived source spawns into 1241 map space before injecting pins.
|
||||||
ZoneDB.private.areaIdToUiMapId[3431] = 1241
|
ZoneDB.private.areaIdToUiMapId[3431] = 1241
|
||||||
areaIdToUiMapId[3431] = 1241
|
areaIdToUiMapId[3431] = 1241
|
||||||
-- Allow drawing pins directly on Sunstrider Isle (uiMapId 1241) via areaId 1241.
|
-- Allow drawing pins directly on Sunstrider Isle (uiMapId 1241) via areaId 1241.
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ local function UpdateMinimapPins(force)
|
|||||||
|
|
||||||
-- check for all values to be available (starting with 7.1.0, instances don't report coordinates)
|
-- check for all values to be available (starting with 7.1.0, instances don't report coordinates)
|
||||||
if not x or not y or (rotateMinimap and not facing) then
|
if not x or not y or (rotateMinimap and not facing) then
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
print(string.format("[QD] UpdateMinimapPins: EARLY EXIT x=%s y=%s facing=%s", tostring(x), tostring(y), tostring(facing)))
|
||||||
|
_G.QuestieDebugPins = false
|
||||||
|
end
|
||||||
minimapPinCount = 0
|
minimapPinCount = 0
|
||||||
for pin in pairs(activeMinimapPins) do
|
for pin in pairs(activeMinimapPins) do
|
||||||
pin:Hide()
|
pin:Hide()
|
||||||
@@ -217,6 +221,26 @@ local function UpdateMinimapPins(force)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if _G.QuestieDebugPins then
|
||||||
|
local pinCount = 0
|
||||||
|
for _ in pairs(minimapPins) do pinCount = pinCount + 1 end
|
||||||
|
print(string.format("[QD] UpdateMinimapPins: player worldX=%.2f worldY=%.2f playerInst=%s mapID=%s totalPins=%d",
|
||||||
|
x, y, tostring(instanceID), tostring(mapID), pinCount))
|
||||||
|
local i = 0
|
||||||
|
for pin, data in pairs(minimapPins) do
|
||||||
|
i = i + 1
|
||||||
|
if i <= 10 then
|
||||||
|
local dist = math.abs(x - data.x) + math.abs(y - data.y)
|
||||||
|
local instMatch = instanceID == data.instanceID
|
||||||
|
local distPass = dist < 500
|
||||||
|
print(string.format("[QD] Pin%d: pinX=%.2f pinY=%.2f pinInst=%s dist=%.1f instMatch=%s distPass=%s -> SHOW=%s",
|
||||||
|
i, data.x, data.y, tostring(data.instanceID), dist,
|
||||||
|
tostring(instMatch), tostring(distPass), tostring(instMatch and distPass)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
_G.QuestieDebugPins = false
|
||||||
|
end
|
||||||
|
|
||||||
local newScale = pins.Minimap:GetScale()
|
local newScale = pins.Minimap:GetScale()
|
||||||
if minimapScale ~= newScale then
|
if minimapScale ~= newScale then
|
||||||
minimapScale = newScale
|
minimapScale = newScale
|
||||||
|
|||||||
@@ -31,6 +31,56 @@ end
|
|||||||
local QuestiePlugin = {}
|
local QuestiePlugin = {}
|
||||||
QuestiePlugin.__index = QuestiePlugin
|
QuestiePlugin.__index = QuestiePlugin
|
||||||
|
|
||||||
|
local function _GetAscensionProtection(dbType)
|
||||||
|
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||||
|
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
|
||||||
|
QuestieDB.ascensionOverrideKeys[dbType] = QuestieDB.ascensionOverrideKeys[dbType] or {}
|
||||||
|
return QuestieDB.ascensionOverrideKeys[dbType]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _IsAscensionProtected(dbType, id, key)
|
||||||
|
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||||
|
local protected = QuestieDB.ascensionOverrideKeys
|
||||||
|
and QuestieDB.ascensionOverrideKeys[dbType]
|
||||||
|
and QuestieDB.ascensionOverrideKeys[dbType][id]
|
||||||
|
|
||||||
|
return protected and protected[key] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _ProtectAscensionField(dbType, id, key)
|
||||||
|
local protectedByType = _GetAscensionProtection(dbType)
|
||||||
|
protectedByType[id] = protectedByType[id] or {}
|
||||||
|
protectedByType[id][key] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _MergeEntry(target, id, entry, sourceName, dbType)
|
||||||
|
if type(target) ~= "table" or type(entry) ~= "table" then return end
|
||||||
|
|
||||||
|
local existing = target[id]
|
||||||
|
if type(existing) ~= "table" then
|
||||||
|
target[id] = entry
|
||||||
|
if sourceName == "Ascension" then
|
||||||
|
local key = next(entry)
|
||||||
|
while key do
|
||||||
|
_ProtectAscensionField(dbType, id, key)
|
||||||
|
key = next(entry, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local key, value = next(entry)
|
||||||
|
while key do
|
||||||
|
if sourceName == "Ascension" or not _IsAscensionProtected(dbType, id, key) then
|
||||||
|
existing[key] = value
|
||||||
|
if sourceName == "Ascension" then
|
||||||
|
_ProtectAscensionField(dbType, id, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
key, value = next(entry, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Registers a new Questie plugin
|
--- Registers a new Questie plugin
|
||||||
---@param pluginName string The unique name of the plugin
|
---@param pluginName string The unique name of the plugin
|
||||||
---@return table|nil plugin The initialized plugin object, or nil if already registered
|
---@return table|nil plugin The initialized plugin object, or nil if already registered
|
||||||
@@ -104,7 +154,7 @@ function QuestiePlugin:InjectDatabase(dbType, data)
|
|||||||
|
|
||||||
local qid, entry = next(data)
|
local qid, entry = next(data)
|
||||||
while qid do
|
while qid do
|
||||||
targetOverride[qid] = entry
|
_MergeEntry(targetOverride, qid, entry, self.name, dbType)
|
||||||
if type(qid) == "number" then
|
if type(qid) == "number" then
|
||||||
count = count + 1
|
count = count + 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ local function _ResolveMapUiMapId(uiMapId, x, y)
|
|||||||
if uiMapId == 946 then
|
if uiMapId == 946 then
|
||||||
return 1941
|
return 1941
|
||||||
end
|
end
|
||||||
-- Map 1241 (Sunstrider Isle): the game engine returns player world
|
-- Map 1241 (Sunstrider Isle) must keep its native render target so the
|
||||||
-- coordinates in Sunstrider space (mapData[1241] calibrated bounds).
|
-- actual Sunstrider map can show pins too. Visibility is handled separately
|
||||||
-- Pins MUST also convert through mapData[1241] so they share the same
|
-- through ResolveZone()/isSameZoneSpace logic, which keeps 1241 and 1941
|
||||||
-- world space as the player on the minimap. Do NOT redirect to 1941.
|
-- linked without forcing the render target to the parent map.
|
||||||
return uiMapId
|
return uiMapId
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -187,10 +187,9 @@ _CalculateAvailableQuests = function()
|
|||||||
|
|
||||||
if QuestieMap.questIdFrames[questId] then
|
if QuestieMap.questIdFrames[questId] then
|
||||||
-- We already drew this quest so we might need to update the icon (config changed/level up)
|
-- We already drew this quest so we might need to update the icon (config changed/level up)
|
||||||
|
-- Note: GetFramesForQuest returns string-keyed table, so iterate with pairs not numeric index.
|
||||||
local frames = QuestieMap:GetFramesForQuest(questId)
|
local frames = QuestieMap:GetFramesForQuest(questId)
|
||||||
local i = 1
|
for _, frame in pairs(frames) do
|
||||||
while frames[i] do
|
|
||||||
local frame = frames[i]
|
|
||||||
if frame and frame.data and frame.data.QuestData then
|
if frame and frame.data and frame.data.QuestData then
|
||||||
local newIcon = _GetQuestIcon(frame.data.QuestData)
|
local newIcon = _GetQuestIcon(frame.data.QuestData)
|
||||||
|
|
||||||
@@ -198,7 +197,6 @@ _CalculateAvailableQuests = function()
|
|||||||
frame:UpdateTexture(Questie.usedIcons[newIcon])
|
frame:UpdateTexture(Questie.usedIcons[newIcon])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
i = i + 1
|
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1775,10 +1775,30 @@ _DrawObjectiveIcons = function(questId, iconsToDraw, objective, maxPerType)
|
|||||||
|
|
||||||
local iconCount, orderedList = _GetIconsSortedByDistance(iconsToDraw)
|
local iconCount, orderedList = _GetIconsSortedByDistance(iconsToDraw)
|
||||||
|
|
||||||
|
-- Dense kill objectives (like Sunstrider Isle mana wyrms) previously used
|
||||||
|
-- a lower clustering hotzone here. Leave the old behavior commented so we
|
||||||
|
-- can restore it quickly if we need to revisit consolidation again.
|
||||||
|
--[[
|
||||||
|
if iconCount >= 20 then
|
||||||
|
range = math.max(6, math.floor(range * 0.25))
|
||||||
|
elseif iconCount >= 12 then
|
||||||
|
range = math.max(10, math.floor(range * 0.4))
|
||||||
|
elseif iconCount >= 6 then
|
||||||
|
range = math.max(16, math.floor(range * 0.65))
|
||||||
|
end
|
||||||
|
--]]
|
||||||
|
|
||||||
if orderedList[1] and orderedList[1].Icon == Questie.ICON_TYPE_OBJECT then -- new clustering / limit code should prevent problems, always show all object notes
|
if orderedList[1] and orderedList[1].Icon == Questie.ICON_TYPE_OBJECT then -- new clustering / limit code should prevent problems, always show all object notes
|
||||||
range = range * 0.2; -- Only use 20% of the default range.
|
range = range * 0.2; -- Only use 20% of the default range.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Sunstrider Isle (uiMapID 1241) is a tiny starting area where AscensionDB
|
||||||
|
-- places individual spawn coords that should each show as a distinct pin.
|
||||||
|
-- Disable clustering entirely for this zone so all pins are visible.
|
||||||
|
if orderedList[1] and orderedList[1].zone == 1241 then
|
||||||
|
range = 0
|
||||||
|
end
|
||||||
|
|
||||||
local hotzones = QuestieMap.utils:CalcHotzones(orderedList, range, iconCount);
|
local hotzones = QuestieMap.utils:CalcHotzones(orderedList, range, iconCount);
|
||||||
|
|
||||||
for i = 1, table.getn(hotzones) do
|
for i = 1, table.getn(hotzones) do
|
||||||
@@ -1869,35 +1889,29 @@ _GetIconsSortedByDistance = function(icons)
|
|||||||
end
|
end
|
||||||
|
|
||||||
_DrawObjectiveWaypoints = function(objective, icon, iconPerZone)
|
_DrawObjectiveWaypoints = function(objective, icon, iconPerZone)
|
||||||
local _, spawnData = next(objective.spawnList)
|
if not objective or not objective.spawnList then return end
|
||||||
while _ do -- spawnData.Name, spawnData.Spawns
|
|
||||||
if spawnData.Waypoints then
|
|
||||||
local zone, waypoints = next(spawnData.Waypoints)
|
|
||||||
while zone do
|
|
||||||
local firstWaypoint = waypoints[1][1]
|
|
||||||
|
|
||||||
if (not iconPerZone[zone]) and icon and firstWaypoint[1] ~= -1 and firstWaypoint[2] ~= -1 then -- spawn an icon in this zone for the mob
|
for _, spawnData in pairs(objective.spawnList) do
|
||||||
local iconMap, iconMini = QuestieMap:DrawWorldIcon(icon.data, zone, firstWaypoint[1],
|
if spawnData and spawnData.Waypoints then
|
||||||
firstWaypoint[2]) -- clustering code takes care of duplicates as long as min-dist is more than 0
|
for zone, waypoints in pairs(spawnData.Waypoints) do
|
||||||
|
local firstWaypoint = waypoints and waypoints[1] and waypoints[1][1]
|
||||||
|
|
||||||
|
if firstWaypoint and (not iconPerZone[zone]) and icon and firstWaypoint[1] ~= -1 and firstWaypoint[2] ~= -1 then
|
||||||
|
local iconMap, iconMini = QuestieMap:DrawWorldIcon(icon.data, zone, firstWaypoint[1], firstWaypoint[2])
|
||||||
if iconMap and iconMini then
|
if iconMap and iconMini then
|
||||||
iconPerZone[zone] = { iconMap, firstWaypoint[1], firstWaypoint[2] }
|
iconPerZone[zone] = { iconMap, firstWaypoint[1], firstWaypoint[2] }
|
||||||
tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].mapRefs, iconMap);
|
tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].mapRefs, iconMap)
|
||||||
tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].minimapRefs, iconMini);
|
tinsert(objective.AlreadySpawned[icon.AlreadySpawnedId].minimapRefs, iconMini)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local ipz = iconPerZone[zone]
|
local ipz = iconPerZone[zone]
|
||||||
|
|
||||||
if ipz then
|
if ipz then
|
||||||
QuestieMap:DrawWaypoints(ipz[1], waypoints, zone, spawnData.Hostile and { 1, 0.2, 0, 0.7 } or nil)
|
QuestieMap:DrawWaypoints(ipz[1], waypoints, zone, spawnData.Hostile and { 1, 0.2, 0, 0.7 } or nil)
|
||||||
end
|
end
|
||||||
zone, waypoints = next(spawnData.Waypoints, zone)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveWaypoints]")
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:_DrawObjectiveWaypoints]")
|
||||||
end
|
end
|
||||||
_, spawnData = next(objective.spawnList, _)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,36 @@ monster = function(npcId, objective)
|
|||||||
if learnedNpc then
|
if learnedNpc then
|
||||||
local learnedSpawns = learnedNpc[7]
|
local learnedSpawns = learnedNpc[7]
|
||||||
local threshold = ld.settings.minConfidencePins or 1
|
local threshold = ld.settings.minConfidencePins or 1
|
||||||
if learnedSpawns and next(learnedSpawns) and learnedNpc.mc and learnedNpc.mc >= threshold then
|
-- Do not override Ascension-curated spawn data with raw learner data.
|
||||||
|
-- AscensionDB hand-curates positions for Ascension-specific zones (e.g. Sunstrider)
|
||||||
|
-- which the learner can never improve upon, and the learner coords may be in the
|
||||||
|
-- wrong coordinate space due to Sunstrider/Eversong map-zone mismatch.
|
||||||
|
local ascProtected = QuestieDB.ascensionOverrideKeys
|
||||||
|
and QuestieDB.ascensionOverrideKeys["NPC"]
|
||||||
|
and QuestieDB.ascensionOverrideKeys["NPC"][npcId]
|
||||||
|
and QuestieDB.ascensionOverrideKeys["NPC"][npcId][7]
|
||||||
|
if npcId == 15274 then
|
||||||
|
-- Count spawn entries per zone key
|
||||||
|
local spawnZones = ""
|
||||||
|
if type(spawns) == "table" then
|
||||||
|
for zk, coords in pairs(spawns) do
|
||||||
|
spawnZones = spawnZones .. "z" .. tostring(zk) .. "=" .. tostring(type(coords) == "table" and #coords or "?") .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local learnedZones = ""
|
||||||
|
if type(learnedSpawns) == "table" then
|
||||||
|
for zk, coords in pairs(learnedSpawns) do
|
||||||
|
learnedZones = learnedZones .. "z" .. tostring(zk) .. "=" .. tostring(type(coords) == "table" and #coords or "?") .. " "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print(string.format("[QD] NPC 15274: dbSpawns={%s} learnedSpawns={%s} mc=%s threshold=%s ascProtected=%s -> uselearner=%s",
|
||||||
|
spawnZones, learnedZones,
|
||||||
|
tostring(learnedNpc.mc), tostring(threshold),
|
||||||
|
tostring(ascProtected),
|
||||||
|
tostring(learnedSpawns and next(learnedSpawns) and learnedNpc.mc and learnedNpc.mc >= threshold and not ascProtected)))
|
||||||
|
end
|
||||||
|
if learnedSpawns and next(learnedSpawns) and learnedNpc.mc and learnedNpc.mc >= threshold
|
||||||
|
and not ascProtected then
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Preferring learned spawns for NPC:", npcId, "(mc=" .. tostring(learnedNpc.mc) .. ")")
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[monster] Preferring learned spawns for NPC:", npcId, "(mc=" .. tostring(learnedNpc.mc) .. ")")
|
||||||
spawns = learnedSpawns
|
spawns = learnedSpawns
|
||||||
isLearned = true
|
isLearned = true
|
||||||
|
|||||||
+320
-63
@@ -32,6 +32,36 @@ local string_sub = string.sub
|
|||||||
local string_len = string.len
|
local string_len = string.len
|
||||||
local string_upper = string.upper
|
local string_upper = string.upper
|
||||||
|
|
||||||
|
local function IsAscensionProtected(dbType, id, key)
|
||||||
|
local protected = QuestieDB
|
||||||
|
and QuestieDB.ascensionOverrideKeys
|
||||||
|
and QuestieDB.ascensionOverrideKeys[dbType]
|
||||||
|
and QuestieDB.ascensionOverrideKeys[dbType][id]
|
||||||
|
|
||||||
|
return protected and protected[key] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function NormalizeSpawnZoneKey(zoneKey)
|
||||||
|
-- Convert raw area IDs (e.g. 3431 from GetAreaID()) to the canonical map IDs
|
||||||
|
-- used by AscensionDB and the rendering system (e.g. 1241 for Sunstrider Isle).
|
||||||
|
-- ZoneDB.private.areaIdToUiMapId is the single source of truth for this mapping
|
||||||
|
-- (zoneDB.lua: 3431→1241, 3430→1941, 668→1238, etc.). Using the same table
|
||||||
|
-- ensures learner-stored zone keys are always valid for DrawWorldIcon / HBD.
|
||||||
|
--
|
||||||
|
-- IMPORTANT: Never store raw area IDs (3430, 3431) in spawn data — the pin
|
||||||
|
-- rendering pipeline only knows about map IDs (1241, 1941). Any future zone
|
||||||
|
-- additions must be registered in ZoneDB.private.areaIdToUiMapId first.
|
||||||
|
if ZoneDB and ZoneDB.private and ZoneDB.private.areaIdToUiMapId then
|
||||||
|
local mapped = ZoneDB.private.areaIdToUiMapId[zoneKey]
|
||||||
|
if mapped then return mapped end
|
||||||
|
end
|
||||||
|
return zoneKey
|
||||||
|
end
|
||||||
|
|
||||||
|
local function IsSunstriderNativeZone(zoneKey)
|
||||||
|
return zoneKey == 1241 or zoneKey == 3431
|
||||||
|
end
|
||||||
|
|
||||||
-- WoW API locals
|
-- WoW API locals
|
||||||
local UnitExists = UnitExists
|
local UnitExists = UnitExists
|
||||||
local UnitIsVisible = UnitIsVisible
|
local UnitIsVisible = UnitIsVisible
|
||||||
@@ -80,6 +110,15 @@ local COORD_GRID = 2.0
|
|||||||
-- Ascension, where NPC databases are incomplete and every data point matters.
|
-- Ascension, where NPC databases are incomplete and every data point matters.
|
||||||
local MIN_CONFIDENCE_PINS = 1
|
local MIN_CONFIDENCE_PINS = 1
|
||||||
|
|
||||||
|
local function GetCoordGridForZone(zoneId)
|
||||||
|
-- Sunstrider's starter mobs are packed tightly; a 2% bucket collapses
|
||||||
|
-- distinct spawn points such as 58.68/43.19 and 59.11/44.00 into one pin.
|
||||||
|
if IsSunstriderNativeZone(zoneId) then
|
||||||
|
return 0.5
|
||||||
|
end
|
||||||
|
return COORD_GRID
|
||||||
|
end
|
||||||
|
|
||||||
_Learner.pendingNpcs = {}
|
_Learner.pendingNpcs = {}
|
||||||
_Learner.pendingQuests = {}
|
_Learner.pendingQuests = {}
|
||||||
_Learner.pendingItems = {}
|
_Learner.pendingItems = {}
|
||||||
@@ -138,6 +177,29 @@ local function GetPlayerCoords()
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function NormalizeCoordValue(value)
|
||||||
|
local coord = tonumber(value)
|
||||||
|
if not coord or coord <= 0 then return nil end
|
||||||
|
|
||||||
|
-- Native map APIs return 0-1, Questie stores 0-100, and a previous
|
||||||
|
-- learner path accidentally persisted 0-10000 values like 5868.
|
||||||
|
if coord <= 1 then
|
||||||
|
coord = coord * 100
|
||||||
|
elseif coord > 100 then
|
||||||
|
coord = coord / 100
|
||||||
|
end
|
||||||
|
|
||||||
|
if coord <= 0 or coord > 100 then return nil end
|
||||||
|
return floor(coord * 100 + 0.5) / 100
|
||||||
|
end
|
||||||
|
|
||||||
|
local function NormalizeCoordPair(x, y)
|
||||||
|
local nx = NormalizeCoordValue(x)
|
||||||
|
local ny = NormalizeCoordValue(y)
|
||||||
|
if not nx or not ny then return nil, nil end
|
||||||
|
return nx, ny
|
||||||
|
end
|
||||||
|
|
||||||
-- Returns the grid-bucket key for a coordinate so nearby points share the same slot
|
-- Returns the grid-bucket key for a coordinate so nearby points share the same slot
|
||||||
local function CoordBucket(x, y)
|
local function CoordBucket(x, y)
|
||||||
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
|
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
|
||||||
@@ -145,10 +207,17 @@ end
|
|||||||
|
|
||||||
-- Inserts {x, y} into coordList only when no existing point falls in the same grid bucket
|
-- Inserts {x, y} into coordList only when no existing point falls in the same grid bucket
|
||||||
local function InsertIfNewBucket(coordList, x, y, customGrid)
|
local function InsertIfNewBucket(coordList, x, y, customGrid)
|
||||||
|
x, y = NormalizeCoordPair(x, y)
|
||||||
|
if not x or not y then return false end
|
||||||
|
|
||||||
local grid = customGrid or COORD_GRID
|
local grid = customGrid or COORD_GRID
|
||||||
local bx, by = floor(x / grid) * grid, floor(y / grid) * grid
|
local bx, by = floor(x / grid) * grid, floor(y / grid) * grid
|
||||||
for _, coord in ipairs(coordList) do
|
for _, coord in ipairs(coordList) do
|
||||||
local cx, cy = floor(coord[1] / grid) * grid, floor(coord[2] / grid) * grid
|
local existingX, existingY = NormalizeCoordPair(coord[1], coord[2])
|
||||||
|
if existingX and existingY then
|
||||||
|
coord[1], coord[2] = existingX, existingY
|
||||||
|
end
|
||||||
|
local cx, cy = floor((existingX or coord[1]) / grid) * grid, floor((existingY or coord[2]) / grid) * grid
|
||||||
if cx == bx and cy == by then return false end
|
if cx == bx and cy == by then return false end
|
||||||
end
|
end
|
||||||
table.insert(coordList, {x, y})
|
table.insert(coordList, {x, y})
|
||||||
@@ -750,8 +819,10 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
-- Never learn player-spawned totems
|
-- Never learn player-spawned totems
|
||||||
if PLAYER_SPAWNED_NPC_SET[npcId] then return end
|
if PLAYER_SPAWNED_NPC_SET[npcId] then return end
|
||||||
|
|
||||||
-- Use provided spawn coords (e.g. from kill event) or fall back to current player position
|
-- Use provided spawn coords (e.g. from kill event) or fall back to current player position.
|
||||||
local zoneId = spawnZoneId or GetZoneId()
|
-- Normalize area IDs → map IDs immediately so all storage uses the same key space
|
||||||
|
-- as AscensionDB (e.g. 3431 → 1241, 3430 → 1941).
|
||||||
|
local zoneId = NormalizeSpawnZoneKey(spawnZoneId or GetZoneId())
|
||||||
local x, y
|
local x, y
|
||||||
if spawnX and spawnY then
|
if spawnX and spawnY then
|
||||||
x, y = spawnX, spawnY
|
x, y = spawnX, spawnY
|
||||||
@@ -777,7 +848,7 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
if x and y and zoneId and zoneId > 0 then
|
if x and y and zoneId and zoneId > 0 then
|
||||||
existing[7] = existing[7] or {}
|
existing[7] = existing[7] or {}
|
||||||
existing[7][zoneId] = existing[7][zoneId] or {}
|
existing[7][zoneId] = existing[7][zoneId] or {}
|
||||||
InsertIfNewBucket(existing[7][zoneId], x, y)
|
InsertIfNewBucket(existing[7][zoneId], x, y, GetCoordGridForZone(zoneId))
|
||||||
end
|
end
|
||||||
|
|
||||||
existing.ls = time() -- Update last seen
|
existing.ls = time() -- Update last seen
|
||||||
@@ -793,15 +864,15 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
else
|
else
|
||||||
-- Merge: fill missing fields; also overwrite empty-string names
|
-- Merge: fill missing fields; also overwrite empty-string names
|
||||||
for k, v in pairs(existing) do
|
for k, v in pairs(existing) do
|
||||||
if ovr[k] == nil or (k == 1 and ovr[k] == "") then ovr[k] = v end
|
if not IsAscensionProtected("NPC", npcId, k) and (ovr[k] == nil or (k == 1 and ovr[k] == "")) then ovr[k] = v end
|
||||||
end
|
end
|
||||||
-- Always merge spawn coords
|
-- Always merge spawn coords
|
||||||
if existing[7] then
|
if existing[7] and not IsAscensionProtected("NPC", npcId, 7) then
|
||||||
ovr[7] = ovr[7] or {}
|
ovr[7] = ovr[7] or {}
|
||||||
for zid, coords in pairs(existing[7]) do
|
for zid, coords in pairs(existing[7]) do
|
||||||
ovr[7][zid] = ovr[7][zid] or {}
|
ovr[7][zid] = ovr[7][zid] or {}
|
||||||
for _, coord in ipairs(coords) do
|
for _, coord in ipairs(coords) do
|
||||||
InsertIfNewBucket(ovr[7][zid], coord[1], coord[2])
|
InsertIfNewBucket(ovr[7][zid], coord[1], coord[2], GetCoordGridForZone(zid))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -831,9 +902,19 @@ end
|
|||||||
-- This is the per-spawn-instance identifier — different GUIDs for the same
|
-- This is the per-spawn-instance identifier — different GUIDs for the same
|
||||||
-- npcId indicate different spawn points (e.g. three boars at three corners
|
-- npcId indicate different spawn points (e.g. three boars at three corners
|
||||||
-- of a field, not one boar teleporting around).
|
-- of a field, not one boar teleporting around).
|
||||||
|
-- IMPORTANT: Do not revert this to a single GUID format. Ascension clients
|
||||||
|
-- may emit either dashed GUIDs or compact hex GUIDs ("0x..."), and both must
|
||||||
|
-- remain supported or GUID-based learner evidence will silently stop storing.
|
||||||
-- Format: "Creature-0-RR-RI-0-NNNNNNNN" where NNNNNNNN = spawn UID
|
-- Format: "Creature-0-RR-RI-0-NNNNNNNN" where NNNNNNNN = spawn UID
|
||||||
local function _ExtractSpawnUID(guid)
|
local function _ExtractSpawnUID(guid)
|
||||||
if not guid or type(guid) ~= "string" then return nil end
|
if not guid or type(guid) ~= "string" then return nil end
|
||||||
|
-- Client-arg combat log GUIDs on Ascension commonly arrive as compact hex
|
||||||
|
-- strings (e.g. "0xF130003BAA009E40"). Use the low 24 bits so different
|
||||||
|
-- spawn instances of the same npcId still resolve to distinct evidence keys.
|
||||||
|
local hexTail = guid:match("^0x%x+(%x%x%x%x%x%x)$")
|
||||||
|
if hexTail then
|
||||||
|
return tonumber(hexTail, 16)
|
||||||
|
end
|
||||||
-- Dash format: Creature-0-1234-567-89-21878-0000001234
|
-- Dash format: Creature-0-1234-567-89-21878-0000001234
|
||||||
-- Last numeric segment after the 5th dash is the spawn UID
|
-- Last numeric segment after the 5th dash is the spawn UID
|
||||||
local spawnUID = guid:match("^[^%-]+%-[^%-]+%-[^%-]+%-[^%-]+%-[^%-]+%-(%d+)$")
|
local spawnUID = guid:match("^[^%-]+%-[^%-]+%-[^%-]+%-[^%-]+%-[^%-]+%-(%d+)$")
|
||||||
@@ -871,6 +952,11 @@ function QuestieLearner:_StoreGuidSpawnEvidence(npcId, dstGUID, zoneId, x, y)
|
|||||||
if not zoneId or zoneId <= 0 then return end
|
if not zoneId or zoneId <= 0 then return end
|
||||||
if not x or not y or x <= 0 or y <= 0 then return end
|
if not x or not y or x <= 0 or y <= 0 then return end
|
||||||
|
|
||||||
|
-- Normalize area IDs → map IDs so evidence is keyed identically to AscensionDB.
|
||||||
|
-- Without this, kills on Sunstrider store under zone 3431 (area ID) while the
|
||||||
|
-- renderer expects zone 1241 (map ID), causing pins to silently not appear.
|
||||||
|
zoneId = NormalizeSpawnZoneKey(zoneId)
|
||||||
|
|
||||||
local spawnUID = _ExtractSpawnUID(dstGUID)
|
local spawnUID = _ExtractSpawnUID(dstGUID)
|
||||||
if not spawnUID then return end
|
if not spawnUID then return end
|
||||||
|
|
||||||
@@ -880,10 +966,18 @@ function QuestieLearner:_StoreGuidSpawnEvidence(npcId, dstGUID, zoneId, x, y)
|
|||||||
local guidSpawns = _GetOrCreateGuidSpawnTable(learnedNpc)
|
local guidSpawns = _GetOrCreateGuidSpawnTable(learnedNpc)
|
||||||
if not guidSpawns then return end
|
if not guidSpawns then return end
|
||||||
|
|
||||||
-- Normalize coords: same format as recentKills so Phase 3 merge is consistent
|
-- Normalize coords to Questie's 0-100 map scale. NormalizeCoordPair handles all
|
||||||
-- Formula: floor(v * 10000) / 100 — converts normalized 0-1 to scaled 0-100
|
-- three input formats: native 0-1, already-scaled 0-100, or buggy 0-10000.
|
||||||
local nx = floor(x * 10000) / 100
|
local nx, ny = NormalizeCoordPair(x, y)
|
||||||
local ny = floor(y * 10000) / 100
|
if not nx or not ny then return end
|
||||||
|
|
||||||
|
-- DEBUG: log raw x/y and normalized nx/ny being stored
|
||||||
|
-- Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
-- "_StoreGuidSpawnEvidence: npcId=", npcId,
|
||||||
|
-- "spawnUID=", spawnUID,
|
||||||
|
-- "x=", x, "y=", y,
|
||||||
|
-- "nx=", nx, "ny=", ny,
|
||||||
|
-- "zoneId=", zoneId)
|
||||||
|
|
||||||
if guidSpawns[spawnUID] then
|
if guidSpawns[spawnUID] then
|
||||||
-- Existing spawn UID: update position and timestamp
|
-- Existing spawn UID: update position and timestamp
|
||||||
@@ -950,10 +1044,24 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
|
|
||||||
for spawnUID, entry in pairs(guidSpawns) do
|
for spawnUID, entry in pairs(guidSpawns) do
|
||||||
if entry and entry.zoneId and entry.x and entry.y then
|
if entry and entry.zoneId and entry.x and entry.y then
|
||||||
|
local evidenceX, evidenceY = NormalizeCoordPair(entry.x, entry.y)
|
||||||
|
if not evidenceX or not evidenceY then
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence skipping invalid coords: spawnUID=", spawnUID,
|
||||||
|
"entry.x=", tostring(entry.x), "entry.y=", tostring(entry.y))
|
||||||
|
else
|
||||||
|
entry.x = evidenceX
|
||||||
|
entry.y = evidenceY
|
||||||
|
|
||||||
-- Round to 2 decimal places for grouping
|
-- Round to 2 decimal places for grouping
|
||||||
local rx = floor(entry.x * 100 + 0.5) / 100
|
local rx = floor(evidenceX * 100 + 0.5) / 100
|
||||||
local ry = floor(entry.y * 100 + 0.5) / 100
|
local ry = floor(evidenceY * 100 + 0.5) / 100
|
||||||
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
||||||
|
-- DEBUG: log each entry being grouped
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence GROUPING: spawnUID=", spawnUID,
|
||||||
|
"entry.x=", entry.x, "entry.y=", entry.y,
|
||||||
|
"rx=", rx, "ry=", ry, "key=", key)
|
||||||
if not evidence[key] then
|
if not evidence[key] then
|
||||||
evidence[key] = { zoneId = entry.zoneId, x = rx, y = ry, count = 0 }
|
evidence[key] = { zoneId = entry.zoneId, x = rx, y = ry, count = 0 }
|
||||||
end
|
end
|
||||||
@@ -961,6 +1069,7 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
totalEvidence = totalEvidence + 1
|
totalEvidence = totalEvidence + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if totalEvidence < 3 then return false end
|
if totalEvidence < 3 then return false end
|
||||||
|
|
||||||
@@ -979,36 +1088,122 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
local topEvidence = evidence[topKey]
|
local topEvidence = evidence[topKey]
|
||||||
local topPct = (topCount / totalEvidence) * 100
|
local topPct = (topCount / totalEvidence) * 100
|
||||||
|
|
||||||
-- Only override if > 60% confidence AND spawn differs from static DB
|
-- DEBUG: log topEvidence raw values to diagnose coordinate corruption
|
||||||
if topPct <= 60 then
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence DEBUG: topKey=", topKey,
|
||||||
|
"topEvidence.x=", topEvidence.x, "topEvidence.y=", topEvidence.y,
|
||||||
|
"topCount=", topCount, "totalEvidence=", totalEvidence)
|
||||||
|
|
||||||
|
-- Sunstrider Isle: zone IDs are now normalized via NormalizeSpawnZoneKey so
|
||||||
|
-- topEvidence.zoneId will be 1241 (map ID), not 3431 (area ID).
|
||||||
|
-- IsSunstriderNativeZone checks both to be safe against old saved data.
|
||||||
|
-- Confidence threshold is bypassed for Sunstrider because spawn points are
|
||||||
|
-- distributed across 5+ locations — no single point ever reaches 60% of kills.
|
||||||
|
local isSunstrider = IsSunstriderNativeZone(topEvidence.zoneId)
|
||||||
|
local confidenceThreshold = isSunstrider and 0 or 60
|
||||||
|
|
||||||
|
-- Only override if > confidence threshold AND spawn differs from static DB
|
||||||
|
if topPct <= confidenceThreshold then
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
||||||
"top spawn", topCount .. "/" .. totalEvidence,
|
"top spawn", topCount .. "/" .. totalEvidence,
|
||||||
"= " .. floor(topPct + 0.5) .. "% — below 60%, no override")
|
"= " .. floor(topPct + 0.5) .. "%" ..
|
||||||
|
(isSunstrider and " — Sunstrider, threshold bypassed" or (" — below " .. confidenceThreshold .. "%, no override")))
|
||||||
|
-- For Sunstrider, fall through and apply the learned spawn anyway
|
||||||
|
if not isSunstrider then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if isSunstrider then
|
||||||
|
-- AscensionDB owns spawn data for known Sunstrider NPCs — never overwrite it.
|
||||||
|
-- Key 7 = spawns. Without this guard the learner would pollute the curated
|
||||||
|
-- AscensionDB coords with in-game kill evidence, causing wrong pin counts.
|
||||||
|
-- REGRESSION NOTE: If AscensionDB protection check is removed or disabled,
|
||||||
|
-- learner pins will reappear at wrong locations. Do not remove this guard.
|
||||||
|
if IsAscensionProtected("NPC", npcId, 7) then
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
||||||
|
"Sunstrider zone but AscensionDB owns spawns — skipping learner injection")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check against static DB entry
|
-- topEvidence.zoneId is now a map ID (e.g. 1241) because NormalizeSpawnZoneKey
|
||||||
local staticNPC = nil
|
-- converted the area ID at storage time. This matches AscensionDB's key space
|
||||||
if QuestieDB and QuestieDB.QueryNPC then
|
-- so DrawWorldIcon and HBD can resolve the coordinates correctly.
|
||||||
staticNPC = QuestieDB.QueryNPC(npcId, 1)
|
QuestieDB.npcDataOverrides[npcId] = QuestieDB.npcDataOverrides[npcId] or {}
|
||||||
|
QuestieDB.npcDataOverrides[npcId][7] = QuestieDB.npcDataOverrides[npcId][7] or {}
|
||||||
|
QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] = QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] or {}
|
||||||
|
|
||||||
|
local promoted = 0
|
||||||
|
local duplicates = 0
|
||||||
|
local zoneSpawns = QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId]
|
||||||
|
local grid = GetCoordGridForZone(topEvidence.zoneId)
|
||||||
|
for _, spawnEvidence in pairs(evidence) do
|
||||||
|
if InsertIfNewBucket(zoneSpawns, spawnEvidence.x, spawnEvidence.y, grid) then
|
||||||
|
promoted = promoted + 1
|
||||||
|
else
|
||||||
|
duplicates = duplicates + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local staticSpawnList = staticNPC and staticNPC[7]
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
||||||
|
"promoted Sunstrider evidence groups", promoted,
|
||||||
|
"duplicates", duplicates,
|
||||||
|
"zone", tostring(topEvidence.zoneId))
|
||||||
|
|
||||||
|
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||||
|
QuestieDB.private.npcCache[npcId] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return promoted > 0 or duplicates > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check against static DB + learner-promoted override entries.
|
||||||
|
-- After a previous promotion, QueryNPCSingle returns the override data,
|
||||||
|
-- so learner-promoted spawns would incorrectly "match static" and block
|
||||||
|
-- re-promotion. We must exclude overrides we wrote ourselves.
|
||||||
|
local staticNPC = nil
|
||||||
|
if QuestieDB and QuestieDB.QueryNPCSingle then
|
||||||
|
staticNPC = QuestieDB.QueryNPCSingle(npcId, "spawns")
|
||||||
|
end
|
||||||
|
|
||||||
|
local staticSpawnList = staticNPC
|
||||||
local staticSpawnsForZone = staticSpawnList and staticSpawnList[topEvidence.zoneId]
|
local staticSpawnsForZone = staticSpawnList and staticSpawnList[topEvidence.zoneId]
|
||||||
|
|
||||||
-- Check if top spawn matches any static spawn in the same zone
|
-- Collect spawns already promoted by the learner for this zone
|
||||||
|
local learnerOverrides = QuestieDB.npcDataOverrides
|
||||||
|
and QuestieDB.npcDataOverrides[npcId]
|
||||||
|
and QuestieDB.npcDataOverrides[npcId][7]
|
||||||
|
and QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId]
|
||||||
|
local learnerOverrideSet = {}
|
||||||
|
if learnerOverrides then
|
||||||
|
for _, entry in ipairs(learnerOverrides) do
|
||||||
|
if entry and entry[1] and entry[2] then
|
||||||
|
local lx = floor(entry[1] * 100 + 0.5) / 100
|
||||||
|
local ly = floor(entry[2] * 100 + 0.5) / 100
|
||||||
|
learnerOverrideSet[lx .. "|" .. ly] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if top spawn matches any static spawn in the same zone,
|
||||||
|
-- excluding learner-promoted overrides (those should always be updatable)
|
||||||
local matchesStatic = false
|
local matchesStatic = false
|
||||||
if staticSpawnsForZone then
|
if staticSpawnsForZone then
|
||||||
for _, coord in ipairs(staticSpawnsForZone) do
|
for _, coord in ipairs(staticSpawnsForZone) do
|
||||||
local sx = floor(coord[1] * 100 + 0.5) / 100
|
local sx = floor(coord[1] * 100 + 0.5) / 100
|
||||||
local sy = floor(coord[2] * 100 + 0.5) / 100
|
local sy = floor(coord[2] * 100 + 0.5) / 100
|
||||||
|
-- Skip learner-promoted entries — they are not "static"
|
||||||
|
if not learnerOverrideSet[sx .. "|" .. sy] then
|
||||||
if abs(sx - topEvidence.x) < 0.01 and abs(sy - topEvidence.y) < 0.01 then
|
if abs(sx - topEvidence.x) < 0.01 and abs(sy - topEvidence.y) < 0.01 then
|
||||||
matchesStatic = true
|
matchesStatic = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if matchesStatic then
|
if matchesStatic then
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
@@ -1017,7 +1212,29 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Override: inject top spawn into QuestieDB spawn overrides for this zone
|
-- Test mode: allow learned Sunstrider spawn evidence to overwrite live
|
||||||
|
-- override data even if AscensionDB owns the field. If this restores the
|
||||||
|
-- Mana Wyrm pins, the ownership gate is the thing blocking live updates.
|
||||||
|
--[[ if IsAscensionProtected("NPC", npcId, 7) then
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
||||||
|
"spawn override skipped because AscensionDB owns this NPC spawn field")
|
||||||
|
return false
|
||||||
|
end --]]
|
||||||
|
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _MergeSpawnEvidence promoting npcId", npcId,
|
||||||
|
"zone", tostring(topEvidence.zoneId),
|
||||||
|
"x", tostring(topEvidence.x),
|
||||||
|
"y", tostring(topEvidence.y),
|
||||||
|
"protected", tostring(IsAscensionProtected("NPC", npcId, 7)))
|
||||||
|
|
||||||
|
-- Insert as new spawn (InsertIfNewBucket deduplicates).
|
||||||
|
-- Only create zone table entry if insert succeeds — an empty zone override
|
||||||
|
-- {[3431] = {}} makes _MergeOverride's IsEmptyTable check fall through to
|
||||||
|
-- rawdata, bypassing learner data entirely (field is non-nil but empty).
|
||||||
|
local spawned = false
|
||||||
|
if topEvidence.x and topEvidence.y then
|
||||||
if not QuestieDB.npcDataOverrides[npcId] then
|
if not QuestieDB.npcDataOverrides[npcId] then
|
||||||
QuestieDB.npcDataOverrides[npcId] = {}
|
QuestieDB.npcDataOverrides[npcId] = {}
|
||||||
end
|
end
|
||||||
@@ -1027,15 +1244,15 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
if not QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] then
|
if not QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] then
|
||||||
QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] = {}
|
QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId] = {}
|
||||||
end
|
end
|
||||||
|
spawned = InsertIfNewBucket(QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId],
|
||||||
-- Insert as new spawn (InsertIfNewBucket deduplicates)
|
topEvidence.x, topEvidence.y, GetCoordGridForZone(topEvidence.zoneId))
|
||||||
InsertIfNewBucket(QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId],
|
end
|
||||||
topEvidence.x, topEvidence.y)
|
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
||||||
"overrode static DB — learned spawn (" .. topEvidence.x .. "," .. topEvidence.y .. ")",
|
"overrode static DB — learned spawn (" .. tostring(topEvidence.x) .. "," .. tostring(topEvidence.y) .. ")",
|
||||||
"zone " .. topEvidence.zoneId .. " at " .. floor(topPct + 0.5) .. "% confidence")
|
"zone " .. topEvidence.zoneId .. " at " .. floor(topPct + 0.5) .. "% confidence",
|
||||||
|
spawned and "SPAM" or "IGNORED_DUPLICATE")
|
||||||
|
|
||||||
-- Clear npcCache so GetNPC returns fresh data
|
-- Clear npcCache so GetNPC returns fresh data
|
||||||
if QuestieDB.private and QuestieDB.private.npcCache then
|
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||||
@@ -1092,7 +1309,7 @@ function QuestieLearner:LearnQuest(questId, data)
|
|||||||
QuestieDB.questDataOverrides[questId] = existing
|
QuestieDB.questDataOverrides[questId] = existing
|
||||||
else
|
else
|
||||||
for k, v in pairs(existing) do
|
for k, v in pairs(existing) do
|
||||||
if ovr[k] == nil then ovr[k] = v end
|
if ovr[k] == nil and not IsAscensionProtected("QUEST", questId, k) then ovr[k] = v end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1130,7 +1347,7 @@ function QuestieLearner:LearnQuestGiver(questId, entityId, entityType, isStart)
|
|||||||
table.insert(list, entityId)
|
table.insert(list, entityId)
|
||||||
|
|
||||||
-- Live injection into questDataOverrides so starters/finishers take effect without reload
|
-- Live injection into questDataOverrides so starters/finishers take effect without reload
|
||||||
if QuestieDB and QuestieDB.questDataOverrides then
|
if QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, field) then
|
||||||
local ovr = QuestieDB.questDataOverrides[questId] or {}
|
local ovr = QuestieDB.questDataOverrides[questId] or {}
|
||||||
QuestieDB.questDataOverrides[questId] = ovr
|
QuestieDB.questDataOverrides[questId] = ovr
|
||||||
ovr[field] = ovr[field] or {}
|
ovr[field] = ovr[field] or {}
|
||||||
@@ -1196,7 +1413,7 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- 2. Apply to live questDataOverrides immediately (no reload needed)
|
-- 2. Apply to live questDataOverrides immediately (no reload needed)
|
||||||
if QuestieDB and QuestieDB.questDataOverrides then
|
if QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, 10) then
|
||||||
local ovr = QuestieDB.questDataOverrides[questId] or {}
|
local ovr = QuestieDB.questDataOverrides[questId] or {}
|
||||||
QuestieDB.questDataOverrides[questId] = ovr
|
QuestieDB.questDataOverrides[questId] = ovr
|
||||||
ovr[10] = ovr[10] or {}
|
ovr[10] = ovr[10] or {}
|
||||||
@@ -1283,7 +1500,7 @@ function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemCl
|
|||||||
QuestieDB.itemDataOverrides[itemId] = existing
|
QuestieDB.itemDataOverrides[itemId] = existing
|
||||||
else
|
else
|
||||||
for k, v in pairs(existing) do
|
for k, v in pairs(existing) do
|
||||||
if ovr[k] == nil then ovr[k] = v end
|
if ovr[k] == nil and not IsAscensionProtected("ITEM", itemId, k) then ovr[k] = v end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1316,7 +1533,7 @@ function QuestieLearner:LearnItemDrop(itemId, npcId)
|
|||||||
table.insert(existing[2], npcId)
|
table.insert(existing[2], npcId)
|
||||||
|
|
||||||
-- Live injection: sync drop list to itemDataOverrides
|
-- Live injection: sync drop list to itemDataOverrides
|
||||||
if QuestieDB and QuestieDB.itemDataOverrides then
|
if QuestieDB and QuestieDB.itemDataOverrides and not IsAscensionProtected("ITEM", itemId, 2) then
|
||||||
local ovr = QuestieDB.itemDataOverrides[itemId] or {}
|
local ovr = QuestieDB.itemDataOverrides[itemId] or {}
|
||||||
QuestieDB.itemDataOverrides[itemId] = ovr
|
QuestieDB.itemDataOverrides[itemId] = ovr
|
||||||
ovr[2] = ovr[2] or {}
|
ovr[2] = ovr[2] or {}
|
||||||
@@ -1370,9 +1587,9 @@ function QuestieLearner:LearnObject(objectId, name)
|
|||||||
QuestieDB.objectDataOverrides[objectId] = existing
|
QuestieDB.objectDataOverrides[objectId] = existing
|
||||||
else
|
else
|
||||||
for k, v in pairs(existing) do
|
for k, v in pairs(existing) do
|
||||||
if ovr[k] == nil then ovr[k] = v end
|
if ovr[k] == nil and not IsAscensionProtected("OBJECT", objectId, k) then ovr[k] = v end
|
||||||
end
|
end
|
||||||
if existing[4] then
|
if existing[4] and not IsAscensionProtected("OBJECT", objectId, 4) then
|
||||||
ovr[4] = ovr[4] or {}
|
ovr[4] = ovr[4] or {}
|
||||||
for zid, coords in pairs(existing[4]) do
|
for zid, coords in pairs(existing[4]) do
|
||||||
ovr[4][zid] = ovr[4][zid] or {}
|
ovr[4][zid] = ovr[4][zid] or {}
|
||||||
@@ -1500,14 +1717,23 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
if data[7] then
|
if data[7] then
|
||||||
local zonesToMigrate = {}
|
local zonesToMigrate = {}
|
||||||
for zoneKey, coords in pairs(data[7]) do
|
for zoneKey, coords in pairs(data[7]) do
|
||||||
|
local originalZoneKey = zoneKey
|
||||||
|
zoneKey = NormalizeSpawnZoneKey(zoneKey)
|
||||||
|
-- Older Ascension learner data stored native Sunstrider coords
|
||||||
|
-- under parent areaId 3430 while [9] still identified the row as
|
||||||
|
-- Sunstrider. Move those coords to areaId 3431 so they render on
|
||||||
|
-- uiMapId 1241 instead of Eversong.
|
||||||
|
if originalZoneKey == 3430 and IsSunstriderNativeZone(data[9]) then
|
||||||
|
zonesToMigrate[originalZoneKey] = 3431
|
||||||
|
end
|
||||||
-- If zoneKey looks like a uiMapId (a map ID rather than an areaId),
|
-- If zoneKey looks like a uiMapId (a map ID rather than an areaId),
|
||||||
-- ZoneDB:GetAreaIdByUiMapId will return the corresponding areaId.
|
-- ZoneDB:GetAreaIdByUiMapId will return the corresponding areaId.
|
||||||
-- If it returns nil, zoneKey is already an areaId — no migration needed.
|
-- If it returns nil, zoneKey is already an areaId — no migration needed.
|
||||||
-- Skip very common areaIds that happen to look like small numbers.
|
-- Skip very common areaIds that happen to look like small numbers.
|
||||||
if ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
if not zonesToMigrate[originalZoneKey] and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey)
|
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey)
|
||||||
if maybeAreaId and maybeAreaId ~= zoneKey then
|
if maybeAreaId and maybeAreaId ~= zoneKey then
|
||||||
zonesToMigrate[zoneKey] = maybeAreaId
|
zonesToMigrate[originalZoneKey] = maybeAreaId
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1529,10 +1755,15 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
if data[4] then
|
if data[4] then
|
||||||
local zonesToMigrate = {}
|
local zonesToMigrate = {}
|
||||||
for zoneKey, coords in pairs(data[4]) do
|
for zoneKey, coords in pairs(data[4]) do
|
||||||
if ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
local originalZoneKey = zoneKey
|
||||||
|
zoneKey = NormalizeSpawnZoneKey(zoneKey)
|
||||||
|
if originalZoneKey == 3430 and IsSunstriderNativeZone(data[5]) then
|
||||||
|
zonesToMigrate[originalZoneKey] = 3431
|
||||||
|
end
|
||||||
|
if not zonesToMigrate[originalZoneKey] and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey)
|
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(zoneKey)
|
||||||
if maybeAreaId and maybeAreaId ~= zoneKey then
|
if maybeAreaId and maybeAreaId ~= zoneKey then
|
||||||
zonesToMigrate[zoneKey] = maybeAreaId
|
zonesToMigrate[originalZoneKey] = maybeAreaId
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1558,7 +1789,8 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
local fieldsFixed = 0
|
local fieldsFixed = 0
|
||||||
for npcId, data in pairs(learned.npcs) do
|
for npcId, data in pairs(learned.npcs) do
|
||||||
if type(data[9]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
if type(data[9]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[9])
|
local normalizedZone = NormalizeSpawnZoneKey(data[9])
|
||||||
|
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(normalizedZone)
|
||||||
if maybeAreaId and maybeAreaId ~= data[9] then
|
if maybeAreaId and maybeAreaId ~= data[9] then
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] NPC", npcId, "zone field [9]", data[9], "->", maybeAreaId)
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] NPC", npcId, "zone field [9]", data[9], "->", maybeAreaId)
|
||||||
data[9] = maybeAreaId
|
data[9] = maybeAreaId
|
||||||
@@ -1568,7 +1800,8 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
end
|
end
|
||||||
for objId, data in pairs(learned.objects) do
|
for objId, data in pairs(learned.objects) do
|
||||||
if type(data[5]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
if type(data[5]) == "number" and ZoneDB and ZoneDB.GetAreaIdByUiMapId then
|
||||||
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(data[5])
|
local normalizedZone = NormalizeSpawnZoneKey(data[5])
|
||||||
|
local maybeAreaId = ZoneDB:GetAreaIdByUiMapId(normalizedZone)
|
||||||
if maybeAreaId and maybeAreaId ~= data[5] then
|
if maybeAreaId and maybeAreaId ~= data[5] then
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Object", objId, "zone field [5]", data[5], "->", maybeAreaId)
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Object", objId, "zone field [5]", data[5], "->", maybeAreaId)
|
||||||
data[5] = maybeAreaId
|
data[5] = maybeAreaId
|
||||||
@@ -1642,7 +1875,7 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
npcCount = npcCount + 1
|
npcCount = npcCount + 1
|
||||||
else
|
else
|
||||||
local existing = QuestieDB.npcDataOverrides[nid or npcId]
|
local existing = QuestieDB.npcDataOverrides[nid or npcId]
|
||||||
if data[7] then
|
if data[7] and not IsAscensionProtected("NPC", nid or npcId, 7) then
|
||||||
existing[7] = existing[7] or {}
|
existing[7] = existing[7] or {}
|
||||||
for zoneId, coords in pairs(data[7]) do
|
for zoneId, coords in pairs(data[7]) do
|
||||||
existing[7][zoneId] = existing[7][zoneId] or {}
|
existing[7][zoneId] = existing[7][zoneId] or {}
|
||||||
@@ -1653,7 +1886,7 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
end
|
end
|
||||||
-- Adopt other fields if missing
|
-- Adopt other fields if missing
|
||||||
for k, v in pairs(data) do
|
for k, v in pairs(data) do
|
||||||
if k ~= "mc" and k ~= 7 and existing[k] == nil then
|
if k ~= "mc" and k ~= 7 and existing[k] == nil and not IsAscensionProtected("NPC", nid or npcId, k) then
|
||||||
existing[k] = v
|
existing[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1776,6 +2009,7 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
if k ~= "mc" then
|
if k ~= "mc" then
|
||||||
if k == 10 then
|
if k == 10 then
|
||||||
-- Special merge: add learned creatureObjective entries to [10][1]
|
-- Special merge: add learned creatureObjective entries to [10][1]
|
||||||
|
if not IsAscensionProtected("QUEST", qid or questId, 10) then
|
||||||
existing[10] = existing[10] or {}
|
existing[10] = existing[10] or {}
|
||||||
existing[10][1] = existing[10][1] or {}
|
existing[10][1] = existing[10][1] or {}
|
||||||
if type(v[1]) == "table" then
|
if type(v[1]) == "table" then
|
||||||
@@ -1789,7 +2023,8 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif existing[k] == nil then
|
end
|
||||||
|
elseif existing[k] == nil and not IsAscensionProtected("QUEST", qid or questId, k) then
|
||||||
existing[k] = v
|
existing[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1831,7 +2066,7 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
objectCount = objectCount + 1
|
objectCount = objectCount + 1
|
||||||
else
|
else
|
||||||
local existing = QuestieDB.objectDataOverrides[oid or objectId]
|
local existing = QuestieDB.objectDataOverrides[oid or objectId]
|
||||||
if data[4] then
|
if data[4] and not IsAscensionProtected("OBJECT", oid or objectId, 4) then
|
||||||
existing[4] = existing[4] or {}
|
existing[4] = existing[4] or {}
|
||||||
for zoneId, coords in pairs(data[4]) do
|
for zoneId, coords in pairs(data[4]) do
|
||||||
existing[4][zoneId] = existing[4][zoneId] or {}
|
existing[4][zoneId] = existing[4][zoneId] or {}
|
||||||
@@ -1842,7 +2077,7 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
end
|
end
|
||||||
-- Adopt other fields
|
-- Adopt other fields
|
||||||
for k, v in pairs(data) do
|
for k, v in pairs(data) do
|
||||||
if k ~= "mc" and k ~= 4 and existing[k] == nil then
|
if k ~= "mc" and k ~= 4 and existing[k] == nil and not IsAscensionProtected("OBJECT", oid or objectId, k) then
|
||||||
existing[k] = v
|
existing[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2438,8 +2673,8 @@ function QuestieLearner:LearnSpellCast(spellId, spellName, dstGUID, dstName)
|
|||||||
for _, obj in pairs(quest.objectives) do
|
for _, obj in pairs(quest.objectives) do
|
||||||
-- If the objective is a spell or requires this spell
|
-- If the objective is a spell or requires this spell
|
||||||
if obj.type == "spell" and obj.text and obj.text:find(spellName, 1, true) then
|
if obj.type == "spell" and obj.text and obj.text:find(spellName, 1, true) then
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Learning spell cast:", spellId, spellName, "on", dstName or "nil")
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Learning spell cast:", spellId, spellName, "on", dstName or "nil")
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Found spell objective match for quest", questId)
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Found spell objective match for quest", questId)
|
||||||
local data = { [10] = { [1] = {} } }
|
local data = { [10] = { [1] = {} } }
|
||||||
if npcId then
|
if npcId then
|
||||||
tinsert(data[10][1], { npcId, spellName })
|
tinsert(data[10][1], { npcId, spellName })
|
||||||
@@ -2526,7 +2761,7 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Permanent minimal log: combat-log path, event, and target
|
-- Permanent minimal log: combat-log path, event, and target
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] combat-log path=", path, " event=", eventType, " dstGUID=", dstGUID, " dstName=", dstName)
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] combat-log path=", path, " event=", eventType, " dstGUID=", dstGUID, " dstName=", dstName)
|
||||||
|
|
||||||
-- Vanilla: neither modern API nor legacy args available — throttle warning, do NOT disable permanently
|
-- Vanilla: neither modern API nor legacy args available — throttle warning, do NOT disable permanently
|
||||||
if not timestamp then
|
if not timestamp then
|
||||||
@@ -2556,7 +2791,7 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
|
|||||||
local now = time()
|
local now = time()
|
||||||
local lastTs = _Learner.killDebounce and _Learner.killDebounce[dstGUID]
|
local lastTs = _Learner.killDebounce and _Learner.killDebounce[dstGUID]
|
||||||
if lastTs and (now - lastTs) < 5 then
|
if lastTs and (now - lastTs) < 5 then
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] kill dedupe suppressed duplicate event=", eventType, " dstGUID=", dstGUID)
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] kill dedupe suppressed duplicate event=", eventType, " dstGUID=", dstGUID)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
_Learner.killDebounce = _Learner.killDebounce or {}
|
_Learner.killDebounce = _Learner.killDebounce or {}
|
||||||
@@ -2613,7 +2848,7 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dstName and dstName ~= "" then
|
if dstName and dstName ~= "" then
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Kill cached for correlation:", npcId, dstName, "@", tostring(px), tostring(py), "zone", tostring(zoneId))
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Kill cached for correlation:", npcId, dstName, "@", tostring(px), tostring(py), "zone", tostring(zoneId))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Unconditionally map the spawn position for Ascension DB building
|
-- Unconditionally map the spawn position for Ascension DB building
|
||||||
@@ -2621,14 +2856,36 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
|
|||||||
|
|
||||||
-- Phase 2: store per-GUID spawn evidence for weighted merge
|
-- Phase 2: store per-GUID spawn evidence for weighted merge
|
||||||
self:_StoreGuidSpawnEvidence(npcId, dstGUID, zoneId, px, py)
|
self:_StoreGuidSpawnEvidence(npcId, dstGUID, zoneId, px, py)
|
||||||
|
local guidSpawnsAfterStore = Questie.dbLearner.global.npcs[npcId]
|
||||||
|
and Questie.dbLearner.global.npcs[npcId][8]
|
||||||
|
if guidSpawnsAfterStore then
|
||||||
|
local guidCount = 0
|
||||||
|
for _ in pairs(guidSpawnsAfterStore) do guidCount = guidCount + 1 end
|
||||||
|
-- Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
-- "[QuestieLearner] GUID spawn evidence stored:",
|
||||||
|
-- npcId, dstName or name or "?",
|
||||||
|
-- "guidCount", guidCount,
|
||||||
|
-- "zone", tostring(zoneId),
|
||||||
|
-- "x", tostring(px),
|
||||||
|
-- "y", tostring(py))
|
||||||
|
else
|
||||||
|
-- Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
-- "[QuestieLearner] GUID spawn evidence missing after store:",
|
||||||
|
-- npcId, dstName or name or "?",
|
||||||
|
-- "zone", tostring(zoneId),
|
||||||
|
-- "x", tostring(px),
|
||||||
|
-- "y", tostring(py))
|
||||||
|
end
|
||||||
|
|
||||||
-- Phase 3: weighted merge when evidence count >= 3
|
-- Phase 3: weighted merge when evidence count is sufficient.
|
||||||
|
-- Temporarily lowered to 1 for Sunstrider/Mana Wyrm diagnostics so we can
|
||||||
|
-- verify the promotion path immediately.
|
||||||
local guidSpawns = Questie.dbLearner.global.npcs[npcId]
|
local guidSpawns = Questie.dbLearner.global.npcs[npcId]
|
||||||
and Questie.dbLearner.global.npcs[npcId][8]
|
and Questie.dbLearner.global.npcs[npcId][8]
|
||||||
if guidSpawns then
|
if guidSpawns then
|
||||||
local count = 0
|
local count = 0
|
||||||
for _ in pairs(guidSpawns) do count = count + 1 end
|
for _ in pairs(guidSpawns) do count = count + 1 end
|
||||||
if count >= 3 then
|
if count >= 1 then
|
||||||
_MergeSpawnEvidence(npcId)
|
_MergeSpawnEvidence(npcId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2806,7 +3063,7 @@ function QuestieLearner:PruneLearnedSpawnOutliers(threshold)
|
|||||||
-- Check if static DB has anchors for this NPC+zone
|
-- Check if static DB has anchors for this NPC+zone
|
||||||
local staticNPC = nil
|
local staticNPC = nil
|
||||||
if QuestieDB and QuestieDB.QueryNPC then
|
if QuestieDB and QuestieDB.QueryNPC then
|
||||||
staticNPC = QuestieDB.QueryNPC(npcId, 1)
|
staticNPC = QuestieDB.QueryNPCSingle and QuestieDB.QueryNPCSingle(npcId, "spawns") or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if staticNPC and staticNPC[7] and staticNPC[7][zoneId] then
|
if staticNPC and staticNPC[7] and staticNPC[7][zoneId] then
|
||||||
@@ -2913,7 +3170,7 @@ function QuestieLearner:OnQuestLogUpdate()
|
|||||||
local _, _, _, isHeader, _, _, _, questId = QuestieCompat.GetQuestLogTitle(i)
|
local _, _, _, isHeader, _, _, _, questId = QuestieCompat.GetQuestLogTitle(i)
|
||||||
if not isHeader and questId and questId > 0 then
|
if not isHeader and questId and questId > 0 then
|
||||||
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
|
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] OnQuestLogUpdate scanning quest", questId, "logIdx", i, "numObj", numObj)
|
-- Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] OnQuestLogUpdate scanning quest", questId, "logIdx", i, "numObj", numObj)
|
||||||
_Learner.prevObjCounts[questId] = _Learner.prevObjCounts[questId] or {}
|
_Learner.prevObjCounts[questId] = _Learner.prevObjCounts[questId] or {}
|
||||||
for j = 1, numObj do
|
for j = 1, numObj do
|
||||||
local objText, objType, finished = GetQuestLogLeaderBoard(j, i)
|
local objText, objType, finished = GetQuestLogLeaderBoard(j, i)
|
||||||
@@ -2924,11 +3181,11 @@ function QuestieLearner:OnQuestLogUpdate()
|
|||||||
local count = tonumber(objText:match(":?%s*(%d+)%s*/"))
|
local count = tonumber(objText:match(":?%s*(%d+)%s*/"))
|
||||||
local prev = _Learner.prevObjCounts[questId][j]
|
local prev = _Learner.prevObjCounts[questId][j]
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
-- Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
"[QuestieLearner] OnQuestLogUpdate quest", questId,
|
-- "[QuestieLearner] OnQuestLogUpdate quest", questId,
|
||||||
"obj", j, "type:", tostring(objType),
|
-- "obj", j, "type:", tostring(objType),
|
||||||
"count:", tostring(count), "prev:", tostring(prev),
|
-- "count:", tostring(count), "prev:", tostring(prev),
|
||||||
"text:", tostring(objText))
|
-- "text:", tostring(objText))
|
||||||
|
|
||||||
-- Seed on first sight; only correlate on confirmed increase
|
-- Seed on first sight; only correlate on confirmed increase
|
||||||
if prev == nil then
|
if prev == nil then
|
||||||
@@ -3012,8 +3269,8 @@ local function _RegisterLearnedSpawnTooltipHook()
|
|||||||
if _tooltipHookRegistered then return end
|
if _tooltipHookRegistered then return end
|
||||||
_tooltipHookRegistered = true
|
_tooltipHookRegistered = true
|
||||||
GameTooltip:HookScript("OnTooltipSetUnit", function()
|
GameTooltip:HookScript("OnTooltipSetUnit", function()
|
||||||
-- self == GameTooltip
|
-- HookScript handlers do not reliably receive the frame as an argument on 3.3.5a.
|
||||||
local _, unitToken = self:GetUnit()
|
local _, unitToken = GameTooltip:GetUnit()
|
||||||
if unitToken then
|
if unitToken then
|
||||||
_AddLearnedSpawnTooltipLine(unitToken)
|
_AddLearnedSpawnTooltipLine(unitToken)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -711,11 +711,17 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
|||||||
end
|
end
|
||||||
elseif (zoneOrSort) < 0 then
|
elseif (zoneOrSort) < 0 then
|
||||||
zoneName = TrackerUtils:GetCategoryNameByID(zoneOrSort)
|
zoneName = TrackerUtils:GetCategoryNameByID(zoneOrSort)
|
||||||
|
else
|
||||||
|
-- zoneOrSort == 0: try quest log header as last resort before "Unknown Zone"
|
||||||
|
local logZone = GetQuestLogZoneName(questId)
|
||||||
|
if logZone then
|
||||||
|
zoneName = logZone
|
||||||
else
|
else
|
||||||
zoneName = "Unknown Zone"
|
zoneName = "Unknown Zone"
|
||||||
Questie:Debug(Questie.DEBUG_CRITICAL, "[TrackerUtils:_GetZoneName] zoneOrSort", zoneOrSort, "of quest",
|
Questie:Debug(Questie.DEBUG_CRITICAL, "[TrackerUtils:_GetZoneName] zoneOrSort", zoneOrSort, "of quest",
|
||||||
questId, "is not in the Database!")
|
questId, "is not in the Database!")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
if sortObj == "byComplete" then
|
if sortObj == "byComplete" then
|
||||||
zoneName = "Quests (By %% Complete)"
|
zoneName = "Quests (By %% Complete)"
|
||||||
@@ -837,7 +843,9 @@ function TrackerUtils:GetSortedQuestIds()
|
|||||||
if not quest.SpecialObjectives then quest.SpecialObjectives = {} end
|
if not quest.SpecialObjectives then quest.SpecialObjectives = {} end
|
||||||
if not quest.ExtraObjectives then quest.ExtraObjectives = {} end
|
if not quest.ExtraObjectives then quest.ExtraObjectives = {} end
|
||||||
-- Use the quest log header walk (canonical 3.3.5 zone resolution)
|
-- Use the quest log header walk (canonical 3.3.5 zone resolution)
|
||||||
if not quest.zoneName or quest.zoneName == "" then
|
-- Also run if zoneName is set but zoneOrSort is still 0 (e.g. GetAreaIdByZoneName
|
||||||
|
-- returned 0 for a valid zone name like "Sunstrider Isle", leaving zoneOrSort wrong).
|
||||||
|
if not quest.zoneName or quest.zoneName == "" or quest.zoneOrSort == 0 then
|
||||||
local logZone = GetQuestLogZoneName(capturedId)
|
local logZone = GetQuestLogZoneName(capturedId)
|
||||||
if logZone then
|
if logZone then
|
||||||
quest.zoneName = logZone
|
quest.zoneName = logZone
|
||||||
|
|||||||
@@ -134,6 +134,44 @@ describe("QuestieLearner", function()
|
|||||||
assert.is_true(entry2.ts >= ts1) -- timestamp refreshed
|
assert.is_true(entry2.ts >= ts1) -- timestamp refreshed
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("should not double-scale already normalized GUID spawn coordinates", function()
|
||||||
|
local npcId = 15274
|
||||||
|
local unitGUID = "Creature-0-1234-567-89-15274-41298"
|
||||||
|
|
||||||
|
Questie.dbLearner.global.npcs = {
|
||||||
|
[npcId] = {
|
||||||
|
[1] = "Mana Wyrm",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Questie.dbLearner.global.settings.learnNpcs = true
|
||||||
|
|
||||||
|
QuestieLearner:_StoreGuidSpawnEvidence(npcId, unitGUID, 3431, 58.68, 43.19)
|
||||||
|
|
||||||
|
local entry = Questie.dbLearner.global.npcs[npcId][8][41298]
|
||||||
|
assert.is_not_nil(entry)
|
||||||
|
assert.are.equal(58.68, entry.x)
|
||||||
|
assert.are.equal(43.19, entry.y)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should repair legacy 0-10000 GUID spawn coordinates before merging", function()
|
||||||
|
local npcId = 15274
|
||||||
|
local unitGUID = "Creature-0-1234-567-89-15274-41298"
|
||||||
|
|
||||||
|
Questie.dbLearner.global.npcs = {
|
||||||
|
[npcId] = {
|
||||||
|
[1] = "Mana Wyrm",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Questie.dbLearner.global.settings.learnNpcs = true
|
||||||
|
|
||||||
|
QuestieLearner:_StoreGuidSpawnEvidence(npcId, unitGUID, 3431, 5868, 4319)
|
||||||
|
|
||||||
|
local entry = Questie.dbLearner.global.npcs[npcId][8][41298]
|
||||||
|
assert.is_not_nil(entry)
|
||||||
|
assert.are.equal(58.68, entry.x)
|
||||||
|
assert.are.equal(43.19, entry.y)
|
||||||
|
end)
|
||||||
|
|
||||||
--==========================================================================
|
--==========================================================================
|
||||||
-- Existing test: spell cast learning
|
-- Existing test: spell cast learning
|
||||||
--==========================================================================
|
--==========================================================================
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
## Questie-X Learner Module — Pin Collapse Handoff
|
||||||
|
|
||||||
|
**Session**: 2025-05-25
|
||||||
|
**Repo**: `C:\Users\kance\Documents\GitHub\Questie-X`
|
||||||
|
**File**: `Modules/QuestieLearner.lua`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Killing multiple Mana Wyrms (npcId ~15274) on Sunstrider Isle (zoneId 3431) at distinct map coordinates still results in only **2 map pins** rendering instead of 3+ distinct pins. The collapse is caused by the coordinate **bucketing** logic in `_MergeSpawnEvidence` and `InsertIfNewBucket`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Two Identified Bucketing Layers
|
||||||
|
|
||||||
|
### Layer 1 — `_MergeSpawnEvidence` lines ~1046–1048
|
||||||
|
Groups all per-GUID evidence into buckets by rounding `(x, y)` to 2 decimal places:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local rx = floor(evidenceX * 100 + 0.5) / 100
|
||||||
|
local ry = floor(evidenceY * -100 + 0.5) / 100
|
||||||
|
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
||||||
|
```
|
||||||
|
|
||||||
|
Each unique 2-decimal `(rx, ry)` becomes one evidence group. If two distinct kill locations fall within the same 0.01×0.01 square, they merge into one evidence group here — **before** `InsertIfNewBucket` is even called.
|
||||||
|
|
||||||
|
### Layer 2 — `InsertIfNewBucket` lines 205–221
|
||||||
|
For Sunstrider, uses `grid = 0.5`. Checks if any existing spawn in the zone falls in the same `floor(x/0.5)*0.5` bucket:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local bx, by = floor(x / grid) * grid, floor(y / grid) * grid
|
||||||
|
```
|
||||||
|
|
||||||
|
Two kills at x=50.54 and x=50.55 both land in bucket 50.5 → first is inserted, second is rejected as duplicate. This is the **documented intentional collapse** — but it's killing genuinely distinct spawn points that a 0.5 grid rounds to the same bucket.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fixes Already Applied
|
||||||
|
|
||||||
|
1. **`GetCoordGridForZone` hoisted outside evidence loop (line ~1114)**
|
||||||
|
Grid is now computed once before the Sunstrider promotion loop. Previously it was computed inside `InsertIfNewBucket` via `customGrid` parameter.
|
||||||
|
|
||||||
|
2. **`NormalizeCoordPair` comment (line 958–959)**
|
||||||
|
Updated to document that it handles native 0–1, already-scaled 0–100, and buggy 0–10000 input formats.
|
||||||
|
|
||||||
|
3. **`QUESTIE_LEARNER_debug` log comment (line 963–964)**
|
||||||
|
Noted it is commented out — re-enable for live debugging if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Next Step: Remove Bucketing Entirely
|
||||||
|
|
||||||
|
**The fix in this session did not resolve the 2-pin collapse.** The next agent should:
|
||||||
|
|
||||||
|
1. In `_MergeSpawnEvidence` (~line 1046–1048): **comment out or replace** the 2-decimal rounding entirely. Instead of grouping by `(zoneId|rx|ry)`, use the raw normalized coordinates directly. This makes every distinct kill location a separate evidence group.
|
||||||
|
|
||||||
|
2. In the Sunstrider promotion block (~lines 1111–1121): **bypass `InsertIfNewBucket`** entirely for Sunstrider. Instead of bucketing, insert the raw coordinates of every evidence group into `zoneSpawns` directly.
|
||||||
|
|
||||||
|
### Exact Changes to Make
|
||||||
|
|
||||||
|
**`_MergeSpawnEvidence` (~line 1045–1048) — REMOVE 2-decimal rounding:**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```lua
|
||||||
|
-- Round to 2 decimal places for grouping
|
||||||
|
local rx = floor(evidenceX * 100 + 0.5) / 100
|
||||||
|
local ry = floor(evidenceY * 100 + 0.5) / 100
|
||||||
|
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
```lua
|
||||||
|
-- Use raw normalized coordinates as group key (no bucket collapse)
|
||||||
|
local rx = evidenceX
|
||||||
|
local ry = evidenceY
|
||||||
|
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sunstrider loop (~lines 1114–1121) — DIRECT INSERT (bypass `InsertIfNewBucket`):**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```lua
|
||||||
|
local grid = GetCoordGridForZone(topEvidence.zoneId)
|
||||||
|
for _, spawnEvidence in pairs(evidence) do
|
||||||
|
if InsertIfNewBucket(zoneSpawns, spawnEvidence.x, spawnEvidence.y, grid) then
|
||||||
|
promoted = promoted + 1
|
||||||
|
else
|
||||||
|
duplicates = duplicates + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
```lua
|
||||||
|
-- Directly insert every distinct evidence coordinate without bucketing
|
||||||
|
for _, spawnEvidence in pairs(evidence) do
|
||||||
|
tinsert(zoneSpawns, { spawnEvidence.x, spawnEvidence.y })
|
||||||
|
promoted = promoted + 1
|
||||||
|
end
|
||||||
|
duplicates = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Bucketing was designed to reduce noise from GPS drift — but on Sunstrider with a 0.5 grid, it collapses spawn points that are legitimately different. Removing bucketing entirely ensures every distinct kill location gets its own pin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Code Locations
|
||||||
|
|
||||||
|
| Function | Lines | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `NormalizeCoordPair` | ~168–197 | Scales coords to 0–100; handles buggy inputs |
|
||||||
|
| `CoordBucket` | ~200–201 | Bucket key for (x, y) using COORD_GRID |
|
||||||
|
| `InsertIfNewBucket` | ~205–224 | Inserts coord only if no existing in same bucket |
|
||||||
|
| `GetCoordGridForZone` | ~108–113 | Returns 0.5 for Sunstrider (zones 1241/3431), else 2.0 |
|
||||||
|
| `_StoreGuidSpawnEvidence` | ~942–1012 | Stores per-GUID kill evidence; calls NormalizeCoordPair |
|
||||||
|
| `_MergeSpawnEvidence` | ~1014–1140 | Groups evidence → promotes top groups to npcDataOverrides |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Log
|
||||||
|
|
||||||
|
Re-enable this block (`_StoreGuidSpawnEvidence`, lines ~963–964) to verify normalized coordinates during gameplay:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
|
"[QuestieLearner] _StoreGuidSpawnEvidence: spawnUID=", spawnUID,
|
||||||
|
"zoneId=", zoneId, "nx=", nx, "ny=", ny)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected log output after `/reload`:
|
||||||
|
```
|
||||||
|
entry.x=58.68 entry.y=43.19 (not "5868,4319")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `Modules/QuestieLearner.lua` — lines 958–959, 1114 (already patched prior to this handoff)
|
||||||
|
- `Tests/QuestieLearner_spec.lua` — (unchanged; legacy reference)
|
||||||
Reference in New Issue
Block a user