Files
Nanami-UI/MapReveal.lua
2026-04-09 09:46:47 +08:00

696 lines
23 KiB
Lua

--------------------------------------------------------------------------------
-- Nanami-UI: MapReveal -- Reveal unexplored world map areas
-- Adapted from ShaguTweaks-extras worldmap-reveal approach
-- Uses LibMapOverlayData (from !Libs) supplemented with Turtle WoW zones
-- Features persistent overlay discovery via GetMapOverlayInfo() API
--------------------------------------------------------------------------------
SFrames.MapReveal = SFrames.MapReveal or {}
local MapReveal = SFrames.MapReveal
local origWorldMapFrame_Update = nil
local overlayDBPatched = false
local errata = {
["Interface\\WorldMap\\Tirisfal\\BRIGHTWATERLAKE"] = { offsetX = { 587, 584 } },
["Interface\\WorldMap\\Silverpine\\BERENSPERIL"] = { offsetY = { 417, 415 } },
}
-- Zones only in TurtleWoW_Zones: not in patched WorldMapOverlay.dbc but needed for fog reveal
local TurtleWoW_Zones = {
["UpperKarazhan2f"] = {
"OUTLAND:1024:768:0:0",
},
}
-- Runtime-discovered overlay data (populated by scanning and passive collection)
local scannedOverlays = {}
local function IsTurtleWoW()
return TargetHPText and TargetHPPercText
end
local function GetOverlayDB()
return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
end
local function SafeSetMapToCurrentZone()
if not SetMapToCurrentZone then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
end
return pcall(SetMapToCurrentZone)
end
local function SafeSetMapZoom(continent, zone)
if not SetMapZoom then
return
end
if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
end
return pcall(SetMapZoom, continent, zone)
end
--------------------------------------------------------------------------------
-- Persistence: save/load discovered overlay data to SFramesDB
--------------------------------------------------------------------------------
local function GetGlobalDB()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
return SFramesGlobalDB
end
local function LoadPersistedData()
local gdb = GetGlobalDB()
local saved = gdb.MapRevealScanData
if type(saved) ~= "table" then return end
local count = 0
for zone, overlays in pairs(saved) do
if type(overlays) == "table" and table.getn(overlays) > 0 then
if not scannedOverlays[zone] or table.getn(scannedOverlays[zone]) < table.getn(overlays) then
scannedOverlays[zone] = overlays
count = count + 1
end
end
end
return count
end
local function SavePersistedData()
local gdb = GetGlobalDB()
if type(gdb.MapRevealScanData) ~= "table" then
gdb.MapRevealScanData = {}
end
for zone, overlays in pairs(scannedOverlays) do
local existing = gdb.MapRevealScanData[zone]
if not existing or table.getn(existing) < table.getn(overlays) then
gdb.MapRevealScanData[zone] = overlays
end
end
end
--------------------------------------------------------------------------------
-- Overlay name index for deduplication when merging
--------------------------------------------------------------------------------
local function BuildOverlayIndex(overlayList)
local idx = {}
if not overlayList then return idx end
for i = 1, table.getn(overlayList) do
local entry = overlayList[i]
local _, _, name = string.find(entry, "^([^:]+):")
if name then
idx[string.upper(name)] = i
end
end
return idx
end
local function MergeScannedDataIntoDB()
local db = GetOverlayDB()
if not db then return end
for zone, data in pairs(scannedOverlays) do
if not db[zone] then
db[zone] = data
else
local existingIdx = BuildOverlayIndex(db[zone])
for i = 1, table.getn(data) do
local _, _, newName = string.find(data[i], "^([^:]+):")
if newName and not existingIdx[string.upper(newName)] then
table.insert(db[zone], data[i])
existingIdx[string.upper(newName)] = table.getn(db[zone])
end
end
end
end
end
local function PatchOverlayDB()
if overlayDBPatched then return end
overlayDBPatched = true
if not IsTurtleWoW() then return end
local db = GetOverlayDB()
if not db then return end
for zone, data in pairs(TurtleWoW_Zones) do
if table.getn(data) > 0 then
db[zone] = data
end
end
MergeScannedDataIntoDB()
end
local function MergeOverlaysForZone(zone)
local db = GetOverlayDB()
if not db or not scannedOverlays[zone] then return end
if not db[zone] then
db[zone] = scannedOverlays[zone]
return
end
local existingIdx = BuildOverlayIndex(db[zone])
local data = scannedOverlays[zone]
for i = 1, table.getn(data) do
local _, _, newName = string.find(data[i], "^([^:]+):")
if newName and not existingIdx[string.upper(newName)] then
table.insert(db[zone], data[i])
existingIdx[string.upper(newName)] = table.getn(db[zone])
end
end
end
--------------------------------------------------------------------------------
-- Passive overlay discovery: captures explored overlay data from the API
-- every time the world map updates, discovering new overlays automatically
--------------------------------------------------------------------------------
local function PassiveCollectOverlays()
local mapFile = GetMapInfo and GetMapInfo()
if not mapFile or mapFile == "" or mapFile == "World" then return end
local numOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0
if numOverlays == 0 then return end
local currentOverlays = {}
local hasNew = false
for i = 1, numOverlays do
local texName, texW, texH, offX, offY = GetMapOverlayInfo(i)
if texName and texName ~= "" then
local _, _, name = string.find(texName, "\\([^\\]+)$")
if name then
name = string.upper(name)
table.insert(currentOverlays, name .. ":" .. texW .. ":" .. texH .. ":" .. offX .. ":" .. offY)
end
end
end
if table.getn(currentOverlays) == 0 then return end
if not scannedOverlays[mapFile] then
scannedOverlays[mapFile] = currentOverlays
hasNew = true
else
local existingIdx = BuildOverlayIndex(scannedOverlays[mapFile])
for i = 1, table.getn(currentOverlays) do
local _, _, newName = string.find(currentOverlays[i], "^([^:]+):")
if newName and not existingIdx[string.upper(newName)] then
table.insert(scannedOverlays[mapFile], currentOverlays[i])
existingIdx[string.upper(newName)] = table.getn(scannedOverlays[mapFile])
hasNew = true
end
end
end
if hasNew then
MergeOverlaysForZone(mapFile)
SavePersistedData()
end
end
local function GetConfig()
if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then
return { enabled = true, unexploredAlpha = 0.7 }
end
return SFramesDB.MapReveal
end
local function NextPowerOf2(n)
local p = 16
while p < n do
p = p * 2
end
return p
end
local function DoMapRevealUpdate()
local db = GetOverlayDB()
if not db then return end
local mapFileName = GetMapInfo and GetMapInfo()
if not mapFileName then mapFileName = "World" end
local zoneData = db[mapFileName]
if not zoneData then return end
local prefix = "Interface\\WorldMap\\" .. mapFileName .. "\\"
local numExploredOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0
local explored = {}
for i = 1, numExploredOverlays do
local textureName = GetMapOverlayInfo(i)
if textureName and textureName ~= "" then
explored[textureName] = true
end
end
local cfg = GetConfig()
local dimR, dimG, dimB = 0.4, 0.4, 0.4
if cfg.unexploredAlpha then
dimR = cfg.unexploredAlpha
dimG = cfg.unexploredAlpha
dimB = cfg.unexploredAlpha
end
local textureCount = 0
for idx = 1, table.getn(zoneData) do
local entry = zoneData[idx]
local _, _, name, sW, sH, sX, sY = string.find(entry, "^(%u+):(%d+):(%d+):(%d+):(%d+)$")
if not name then
_, _, name, sW, sH, sX, sY = string.find(entry, "^([^:]+):(%d+):(%d+):(%d+):(%d+)$")
end
if name then
local textureWidth = tonumber(sW)
local textureHeight = tonumber(sH)
local offsetX = tonumber(sX)
local offsetY = tonumber(sY)
local textureName = prefix .. name
local isExplored = explored[textureName]
if cfg.enabled or isExplored then
if errata[textureName] then
local e = errata[textureName]
if e.offsetX and e.offsetX[1] == offsetX then
offsetX = e.offsetX[2]
end
if e.offsetY and e.offsetY[1] == offsetY then
offsetY = e.offsetY[2]
end
end
local numTexturesHorz = math.ceil(textureWidth / 256)
local numTexturesVert = math.ceil(textureHeight / 256)
local neededTextures = textureCount + (numTexturesHorz * numTexturesVert)
if neededTextures > NUM_WORLDMAP_OVERLAYS then
for j = NUM_WORLDMAP_OVERLAYS + 1, neededTextures do
WorldMapDetailFrame:CreateTexture("WorldMapOverlay" .. j, "ARTWORK")
end
NUM_WORLDMAP_OVERLAYS = neededTextures
end
for row = 1, numTexturesVert do
local texturePixelHeight, textureFileHeight
if row < numTexturesVert then
texturePixelHeight = 256
textureFileHeight = 256
else
texturePixelHeight = math.mod(textureHeight, 256)
if texturePixelHeight == 0 then texturePixelHeight = 256 end
textureFileHeight = NextPowerOf2(texturePixelHeight)
end
for col = 1, numTexturesHorz do
if textureCount > NUM_WORLDMAP_OVERLAYS then return end
local texture = _G["WorldMapOverlay" .. (textureCount + 1)]
local texturePixelWidth, textureFileWidth
if col < numTexturesHorz then
texturePixelWidth = 256
textureFileWidth = 256
else
texturePixelWidth = math.mod(textureWidth, 256)
if texturePixelWidth == 0 then texturePixelWidth = 256 end
textureFileWidth = NextPowerOf2(texturePixelWidth)
end
texture:SetWidth(texturePixelWidth)
texture:SetHeight(texturePixelHeight)
texture:SetTexCoord(0, texturePixelWidth / textureFileWidth,
0, texturePixelHeight / textureFileHeight)
texture:ClearAllPoints()
texture:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT",
offsetX + (256 * (col - 1)),
-(offsetY + (256 * (row - 1))))
local tileIndex = ((row - 1) * numTexturesHorz) + col
texture:SetTexture(textureName .. tileIndex)
if not isExplored then
texture:SetVertexColor(dimR, dimG, dimB, 1)
else
texture:SetVertexColor(1, 1, 1, 1)
end
texture:Show()
textureCount = textureCount + 1
end
end
end
end
end
end
function MapReveal:Initialize()
local db = GetOverlayDB()
if not db then
SFrames:Print("MapReveal: LibMapOverlayData 未找到,地图揭示功能不可用。")
return
end
local loadedCount = LoadPersistedData()
PatchOverlayDB()
if loadedCount and loadedCount > 0 then
MergeScannedDataIntoDB()
end
if not origWorldMapFrame_Update and WorldMapFrame_Update then
origWorldMapFrame_Update = WorldMapFrame_Update
WorldMapFrame_Update = function()
for i = 1, NUM_WORLDMAP_OVERLAYS do
local tex = _G["WorldMapOverlay" .. i]
if tex then tex:Hide() end
end
origWorldMapFrame_Update()
PassiveCollectOverlays()
local cfg = GetConfig()
if cfg.enabled then
DoMapRevealUpdate()
end
end
end
end
function MapReveal:Toggle()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.MapReveal) ~= "table" then
SFramesDB.MapReveal = { enabled = true, unexploredAlpha = 0.7 }
end
SFramesDB.MapReveal.enabled = not SFramesDB.MapReveal.enabled
if SFramesDB.MapReveal.enabled then
SFrames:Print("地图迷雾揭示: |cff00ff00已开启|r")
if not origWorldMapFrame_Update and WorldMapFrame_Update then
self:Initialize()
end
else
SFrames:Print("地图迷雾揭示: |cffff0000已关闭|r")
end
if WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
function MapReveal:SetAlpha(alpha)
if not SFramesDB or type(SFramesDB.MapReveal) ~= "table" then return end
SFramesDB.MapReveal.unexploredAlpha = alpha
if SFramesDB.MapReveal.enabled and WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
function MapReveal:Refresh()
if WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
--------------------------------------------------------------------------------
-- Map Scanner: enumerate all continents/zones, collect overlay data from
-- explored areas, discover new zones, and merge into the overlay database.
-- Usage: /nui mapscan or SFrames.MapReveal:ScanAllMaps()
--------------------------------------------------------------------------------
local scanFrame = nil
local scanQueue = {}
local scanIndex = 0
local scanRunning = false
local scanResults = {}
local scanNewZones = {}
local scanUpdatedZones = {}
local savedMapC, savedMapZ = 0, 0
local function ExtractOverlayName(fullPath)
if not fullPath or fullPath == "" then return nil end
local _, _, name = string.find(fullPath, "\\([^\\]+)$")
return name and string.upper(name) or nil
end
local function ProcessScanZone()
if scanIndex > table.getn(scanQueue) then
MapReveal:FinishScan()
return
end
local entry = scanQueue[scanIndex]
SafeSetMapZoom(entry.cont, entry.zone)
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile == "" then
scanIndex = scanIndex + 1
return
end
local numOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0
if numOverlays > 0 then
local overlays = {}
for i = 1, numOverlays do
local texName, texW, texH, offX, offY = GetMapOverlayInfo(i)
if texName and texName ~= "" then
local name = ExtractOverlayName(texName)
if name then
table.insert(overlays, name .. ":" .. texW .. ":" .. texH .. ":" .. offX .. ":" .. offY)
end
end
end
if table.getn(overlays) > 0 then
local db = GetOverlayDB()
local existing = db and db[mapFile]
if not existing then
scanNewZones[mapFile] = overlays
scanResults[mapFile] = { overlays = overlays, status = "new", count = table.getn(overlays) }
else
local existingIdx = BuildOverlayIndex(existing)
local newEntries = 0
for i = 1, table.getn(overlays) do
local _, _, oName = string.find(overlays[i], "^([^:]+):")
if oName and not existingIdx[string.upper(oName)] then
newEntries = newEntries + 1
end
end
if newEntries > 0 then
scanUpdatedZones[mapFile] = overlays
scanResults[mapFile] = {
overlays = overlays, status = "updated",
count = table.getn(existing) + newEntries,
oldCount = table.getn(existing)
}
else
scanResults[mapFile] = { status = "ok", count = table.getn(existing) }
end
end
end
end
scanIndex = scanIndex + 1
end
function MapReveal:FinishScan()
scanRunning = false
if scanFrame then
scanFrame:SetScript("OnUpdate", nil)
end
if savedMapZ > 0 then
SafeSetMapZoom(savedMapC, savedMapZ)
elseif savedMapC > 0 then
SafeSetMapZoom(savedMapC, 0)
else
SafeSetMapToCurrentZone()
end
local cf = DEFAULT_CHAT_FRAME
local newCount = 0
local updCount = 0
local newOverlayCount = 0
for zone, overlays in pairs(scanNewZones) do
newCount = newCount + 1
scannedOverlays[zone] = overlays
end
for zone, overlays in pairs(scanUpdatedZones) do
updCount = updCount + 1
if not scannedOverlays[zone] then
scannedOverlays[zone] = overlays
else
local existingIdx = BuildOverlayIndex(scannedOverlays[zone])
for i = 1, table.getn(overlays) do
local _, _, oName = string.find(overlays[i], "^([^:]+):")
if oName and not existingIdx[string.upper(oName)] then
table.insert(scannedOverlays[zone], overlays[i])
existingIdx[string.upper(oName)] = table.getn(scannedOverlays[zone])
newOverlayCount = newOverlayCount + 1
end
end
end
end
MergeScannedDataIntoDB()
SavePersistedData()
cf:AddMessage("|cffffb3d9[Nanami-UI]|r 地图扫描完成!")
cf:AddMessage(string.format(" 扫描了 |cff00ff00%d|r 个区域", table.getn(scanQueue)))
if newCount > 0 then
cf:AddMessage(string.format(" 发现 |cff00ff00%d|r 个新区域 (已自动添加迷雾数据):", newCount))
for zone, overlays in pairs(scanNewZones) do
cf:AddMessage(" |cff00ffff" .. zone .. "|r (" .. table.getn(overlays) .. " 个覆盖层)")
end
end
if updCount > 0 then
cf:AddMessage(string.format(" 更新了 |cffffff00%d|r 个区域 (发现更多已探索覆盖层):", updCount))
for zone, info in pairs(scanResults) do
if info.status == "updated" then
cf:AddMessage(" |cffffff00" .. zone .. "|r (" .. (info.oldCount or 0) .. " -> " .. info.count .. ")")
end
end
end
if newCount == 0 and updCount == 0 then
cf:AddMessage(" 所有区域数据已是最新,未发现变动。")
end
cf:AddMessage(" 数据已自动保存,下次登录无需重新扫描。")
cf:AddMessage(" 提示: 新发现的区域仅记录已探索区域的覆盖层,完全探索后再次扫描可获取完整数据。")
if WorldMapFrame and WorldMapFrame:IsShown() then
WorldMapFrame_Update()
end
end
function MapReveal:ScanAllMaps()
if scanRunning then
SFrames:Print("地图扫描正在进行中...")
return
end
local db = GetOverlayDB()
if not db then
SFrames:Print("MapReveal: 未找到覆盖层数据库,无法扫描。")
return
end
scanRunning = true
scanQueue = {}
scanIndex = 1
scanResults = {}
scanNewZones = {}
scanUpdatedZones = {}
savedMapC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
savedMapZ = GetCurrentMapZone and GetCurrentMapZone() or 0
local numContinents = 0
if GetMapContinents then
local continents = { GetMapContinents() }
numContinents = table.getn(continents)
end
for c = 1, numContinents do
local zones = { GetMapZones(c) }
for z = 1, table.getn(zones) do
table.insert(scanQueue, { cont = c, zone = z, name = zones[z] or "" })
end
end
SFrames:Print("开始扫描所有地图... (共 " .. table.getn(scanQueue) .. " 个区域)")
if not scanFrame then
scanFrame = CreateFrame("Frame")
end
local scanElapsed = 0
scanFrame:SetScript("OnUpdate", function()
scanElapsed = scanElapsed + (arg1 or 0)
if scanElapsed < 0.02 then return end
scanElapsed = 0
if not scanRunning then return end
local batch = 0
while scanIndex <= table.getn(scanQueue) and batch < 5 do
ProcessScanZone()
batch = batch + 1
end
if scanIndex > table.getn(scanQueue) then
MapReveal:FinishScan()
end
end)
end
function MapReveal:ExportScannedData()
local cf = DEFAULT_CHAT_FRAME
local hasData = false
for zone, overlays in pairs(scannedOverlays) do
hasData = true
cf:AddMessage("|cffffb3d9[MapReveal Export]|r |cff00ffff" .. zone .. "|r = {")
for _, entry in ipairs(overlays) do
cf:AddMessage(' "' .. entry .. '",')
end
cf:AddMessage("}")
end
if not hasData then
cf:AddMessage("|cffffb3d9[MapReveal]|r 没有扫描到的新数据可导出。先运行 /nui mapscan")
end
end
function MapReveal:ShowStats()
local cf = DEFAULT_CHAT_FRAME
local db = GetOverlayDB()
cf:AddMessage("|cffffb3d9[MapReveal]|r 覆盖层数据统计:")
local dbZones, dbOverlays = 0, 0
if db then
for zone, data in pairs(db) do
dbZones = dbZones + 1
dbOverlays = dbOverlays + table.getn(data)
end
end
cf:AddMessage(string.format(" 数据库: |cff00ff00%d|r 个区域, |cff00ff00%d|r 个覆盖层", dbZones, dbOverlays))
local scanZones, scanOverlays = 0, 0
for zone, data in pairs(scannedOverlays) do
scanZones = scanZones + 1
scanOverlays = scanOverlays + table.getn(data)
end
cf:AddMessage(string.format(" 已发现: |cff00ff00%d|r 个区域, |cff00ff00%d|r 个覆盖层 (通过扫描/浏览)", scanZones, scanOverlays))
local savedZones = 0
local gdb = GetGlobalDB()
if type(gdb.MapRevealScanData) == "table" then
for _ in pairs(gdb.MapRevealScanData) do
savedZones = savedZones + 1
end
end
cf:AddMessage(string.format(" 已持久化: |cff00ff00%d|r 个区域", savedZones))
cf:AddMessage(" 提示: 打开世界地图浏览各区域可自动发现新覆盖层数据")
end
function MapReveal:ClearSavedData()
local gdb = GetGlobalDB()
gdb.MapRevealScanData = nil
scannedOverlays = {}
SFrames:Print("地图扫描数据已清除。重新加载UI后生效。")
end