-------------------------------------------------------------------------------- -- 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