-------------------------------------------------------------------------------- -- 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 -------------------------------------------------------------------------------- -- 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] SetMapZoom(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 SetMapZoom(savedMapC, savedMapZ) elseif savedMapC > 0 then SetMapZoom(savedMapC, 0) else if SetMapToCurrentZone then SetMapToCurrentZone() end 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