local ADDON_NAME = "HailMaryGuildTools" local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME) if not HMGT then return end local MapOverlay = HMGT:NewModule("MapOverlay", "AceEvent-3.0") HMGT.MapOverlay = MapOverlay local MEDIA_PATH = "Interface\\AddOns\\HailMaryGuildTools\\Modules\\MapOverlay\\Media\\" local DEFAULT_ICON = MEDIA_PATH .. "DefaultIcon.png" local DEFAULT_CATEGORY = "default" local OBJECT_ICONS_TEXTURE = "Interface\\Minimap\\ObjectIcons" local OBJECT_ICONS_ATLAS_TEXTURE = "Interface\\Minimap\\ObjectIconsAtlas" local BLIP_TEXTURE_WIDTH = 256 local BLIP_TEXTURE_HEIGHT = 256 local BLIP_CELL_SIZE = 32 local ATLAS_TEXTURE_DEFAULTS = { [OBJECT_ICONS_TEXTURE] = { width = 256, height = 256 }, [OBJECT_ICONS_ATLAS_TEXTURE] = { width = 256, height = 256 }, } local ICON_CONFIG = HMGT.MapOverlayIconConfig or {} local RAW_CATEGORY_DEFS = type(ICON_CONFIG.icons) == "table" and ICON_CONFIG.icons or { { key = DEFAULT_CATEGORY, label = "Default", texture = DEFAULT_ICON }, } DEFAULT_CATEGORY = tostring(ICON_CONFIG.defaultKey or DEFAULT_CATEGORY):lower() local CATEGORY_DEFS = {} local CATEGORY_BY_KEY = {} for _, rawDef in ipairs(RAW_CATEGORY_DEFS) do if type(rawDef) == "table" then local key = tostring(rawDef.key or ""):lower() if key ~= "" and not CATEGORY_BY_KEY[key] then local def = { key = key, label = tostring(rawDef.label or key), texture = tostring(rawDef.texture or DEFAULT_ICON), atlasIndex = rawDef.atlasIndex, atlasSize = rawDef.atlasSize, atlasColumns = rawDef.atlasColumns, textureWidth = rawDef.textureWidth, textureHeight = rawDef.textureHeight, cell = rawDef.cell, iconCoords = rawDef.iconCoords, } CATEGORY_DEFS[#CATEGORY_DEFS + 1] = def CATEGORY_BY_KEY[key] = def if type(rawDef.aliases) == "table" then for _, alias in ipairs(rawDef.aliases) do local aliasKey = tostring(alias or ""):lower() if aliasKey ~= "" and not CATEGORY_BY_KEY[aliasKey] then CATEGORY_BY_KEY[aliasKey] = def end end end end end end if not CATEGORY_BY_KEY[DEFAULT_CATEGORY] then local fallbackDef = { key = DEFAULT_CATEGORY, label = "Default", texture = DEFAULT_ICON, } table.insert(CATEGORY_DEFS, 1, fallbackDef) CATEGORY_BY_KEY[DEFAULT_CATEGORY] = fallbackDef end local ROUND_ICON_MASK = "Interface\\CharacterFrame\\TempPortraitAlphaMask" local PIN_RING_TEXTURE = "Interface\\Minimap\\POIIcons" local PIN_RING_COLOR = { 0.96, 0.82, 0.22, 1.00 } local PIN_SHADOW_COLOR = { 0.00, 0.00, 0.00, 0.22 } local function EnsureRoundMask(owner, fieldName, texture) if not owner or not fieldName or not texture then return nil end if not owner.CreateMaskTexture or not texture.AddMaskTexture then return nil end local mask = owner[fieldName] if not mask then mask = owner:CreateMaskTexture(nil, "ARTWORK") mask:SetTexture(ROUND_ICON_MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") owner[fieldName] = mask texture:AddMaskTexture(mask) owner._textureMaskApplied = true end return mask end local function SetTextureMaskEnabled(owner, texture, enabled) if not owner or not texture then return end local mask = owner.textureMask if not mask then return end if enabled then if texture.AddMaskTexture and not owner._textureMaskApplied then texture:AddMaskTexture(mask) owner._textureMaskApplied = true end mask:Show() else if texture.RemoveMaskTexture and owner._textureMaskApplied then texture:RemoveMaskTexture(mask) owner._textureMaskApplied = false end mask:Hide() end end local function ApplyRingVisual(texture, r, g, b, a) if not texture then return end texture:SetTexture(PIN_RING_TEXTURE) texture:SetTexCoord(0, 0.125, 0.625, 0.75) texture:SetVertexColor(r or 1, g or 1, b or 1, a or 1) end local function GetIconInset(pinSize) local size = tonumber(pinSize) or 16 local iconInset = math.max(2, math.floor((size * 0.20) + 0.5)) local maxIconInset = math.max(2, math.floor((size - 6) / 2)) if iconInset > maxIconInset then iconInset = maxIconInset end return iconInset end local function clamp(v, minv, maxv, fallback) v = tonumber(v) if not v then return fallback end if v < minv then return minv end if v > maxv then return maxv end return v end local function GetFallbackObjectIconTexCoord(atlasIndex) local index = tonumber(atlasIndex) if not index or index < 0 then return nil end local col = math.fmod(index, 8) local row = math.floor(index / 8) local left = (col * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH local right = ((col + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH local top = (row * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT local bottom = ((row + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT return { left, right, top, bottom } end local function GetObjectIconTexCoord(iconIndex) local index = tonumber(iconIndex) if not index or index < 0 then return nil end if type(GetObjectIconTextureCoords) == "function" then local left, right, top, bottom = GetObjectIconTextureCoords(index) if left and right and top and bottom then return { left, right, top, bottom } end end return GetFallbackObjectIconTexCoord(index) end local function ParseAtlasSize(atlasSize) if type(atlasSize) == "string" then local width, height = string.match(atlasSize, "^(%d+)%s*[xX]%s*(%d+)$") return tonumber(width), tonumber(height) end if type(atlasSize) == "table" then return tonumber(atlasSize[1]), tonumber(atlasSize[2]) end return nil, nil end local function IsShimmedMinimapAtlas(def) if type(def) ~= "table" then return false end if tostring(def.texture or "") ~= OBJECT_ICONS_ATLAS_TEXTURE then return false end local width, height = ParseAtlasSize(def.atlasSize) local index = tonumber(def.atlasIndex) return width == 32 and height == 32 and index and index >= 0 and index < 40 or false end local function GetDefinitionResolvedTexture(def) if type(def) ~= "table" then return DEFAULT_ICON end if IsShimmedMinimapAtlas(def) then return OBJECT_ICONS_TEXTURE end return tostring(def.texture or DEFAULT_ICON) end local function GetDefinitionTextureDimensions(def) if type(def) ~= "table" then return BLIP_TEXTURE_WIDTH, BLIP_TEXTURE_HEIGHT end local texture = GetDefinitionResolvedTexture(def) local defaults = ATLAS_TEXTURE_DEFAULTS[texture] local width = tonumber(def.textureWidth) or (defaults and defaults.width) or BLIP_TEXTURE_WIDTH local height = tonumber(def.textureHeight) or (defaults and defaults.height) or BLIP_TEXTURE_HEIGHT return width, height end local function GetAtlasTexCoord(def) if type(def) ~= "table" then return nil end local index = tonumber(def.atlasIndex) local cellWidth, cellHeight = ParseAtlasSize(def.atlasSize) if not index or not cellWidth or not cellHeight or cellWidth <= 0 or cellHeight <= 0 then return nil end local textureWidth, textureHeight = GetDefinitionTextureDimensions(def) local columns = tonumber(def.atlasColumns) if not columns or columns < 1 then columns = math.max(1, math.floor(textureWidth / cellWidth)) end local col = math.fmod(index, columns) local row = math.floor(index / columns) local left = (col * cellWidth) / textureWidth local right = ((col + 1) * cellWidth) / textureWidth local top = (row * cellHeight) / textureHeight local bottom = ((row + 1) * cellHeight) / textureHeight return { left, right, top, bottom } end local function GetObjectIconCellTexCoord(col, row) col = tonumber(col) row = tonumber(row) if not col or not row or col < 0 or row < 0 then return nil end local left = (col * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH local right = ((col + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_WIDTH local top = (row * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT local bottom = ((row + 1) * BLIP_CELL_SIZE) / BLIP_TEXTURE_HEIGHT return { left, right, top, bottom } end local function GetDefinitionIconCoords(def) if type(def) ~= "table" then return nil end if type(def.iconCoords) == "table" and #def.iconCoords == 4 then return { def.iconCoords[1], def.iconCoords[2], def.iconCoords[3], def.iconCoords[4] } end if type(def.cell) == "table" then return GetObjectIconCellTexCoord(def.cell[1], def.cell[2]) end if IsShimmedMinimapAtlas(def) then return GetObjectIconTexCoord(def.atlasIndex) end if def.atlasIndex ~= nil and def.atlasSize ~= nil then return GetAtlasTexCoord(def) end if def.atlasIndex ~= nil then if GetDefinitionResolvedTexture(def) == OBJECT_ICONS_TEXTURE then return GetObjectIconTexCoord(def.atlasIndex) end return GetAtlasTexCoord(def) end return nil end local function GetTextureEscape(def, iconCoords, size) local iconTexture = type(def) == "table" and GetDefinitionResolvedTexture(def) or tostring(def or "") local iconSize = tonumber(size) or 14 if iconTexture == "" then return "" end if type(iconCoords) == "table" and #iconCoords == 4 then local textureWidth, textureHeight = GetDefinitionTextureDimensions(type(def) == "table" and def or nil) local left = math.floor((tonumber(iconCoords[1]) or 0) * textureWidth + 0.5) local right = math.floor((tonumber(iconCoords[2]) or 0) * textureWidth + 0.5) local top = math.floor((tonumber(iconCoords[3]) or 0) * textureHeight + 0.5) local bottom = math.floor((tonumber(iconCoords[4]) or 0) * textureHeight + 0.5) return string.format( "|T%s:%d:%d:0:0:%d:%d:%d:%d:%d:%d|t", iconTexture, iconSize, iconSize, textureWidth, textureHeight, left, right, top, bottom ) end return string.format("|T%s:%d:%d:0:0|t", iconTexture, iconSize, iconSize) end local function BuildCategoryDropdownLabel(def) if not def then return "Unknown" end local label = tostring(def.label or def.key or "Icon") local iconTag = GetTextureEscape(def, GetDefinitionIconCoords(def), 14) if iconTag ~= "" then return string.format("%s %s", iconTag, label) end return label end function MapOverlay:GetCategoryDefinitions() return CATEGORY_DEFS end function MapOverlay:GetCategoryDropdownValues() local values = {} for _, def in ipairs(CATEGORY_DEFS) do values[def.key] = BuildCategoryDropdownLabel(def) end return values end function MapOverlay:GetCategoryIcon(category) local key = tostring(category or DEFAULT_CATEGORY):lower() local def = CATEGORY_BY_KEY[key] if def then local texture = GetDefinitionResolvedTexture(def) if texture and texture ~= "" then return texture end end return DEFAULT_ICON end function MapOverlay:GetCategoryIconCoords(category) local key = tostring(category or DEFAULT_CATEGORY):lower() local def = CATEGORY_BY_KEY[key] if def then return GetDefinitionIconCoords(def) end return nil end function MapOverlay:GetCategoryLabel(category) local key = tostring(category or DEFAULT_CATEGORY):lower() local def = CATEGORY_BY_KEY[key] if def and def.label and def.label ~= "" then return def.label end return tostring(category or DEFAULT_CATEGORY) end function MapOverlay:GetNormalizedCategory(category) local key = tostring(category or DEFAULT_CATEGORY):lower() if CATEGORY_BY_KEY[key] then return key end return DEFAULT_CATEGORY end function MapOverlay:GetCategoryVisual(category) local normalizedCategory = self:GetNormalizedCategory(category) return self:GetCategoryIcon(normalizedCategory), self:GetCategoryIconCoords(normalizedCategory), self:GetCategoryLabel(normalizedCategory), normalizedCategory end function MapOverlay:UsesAtlasIcon(category) local normalizedCategory = self:GetNormalizedCategory(category) local def = CATEGORY_BY_KEY[normalizedCategory] return def and (def.atlasIndex ~= nil or type(def.cell) == "table" or type(def.iconCoords) == "table") or false end function MapOverlay:IsPOIUserWaypoint(poi) if not poi or not C_Map or type(C_Map.GetUserWaypoint) ~= "function" then return false end local wp = C_Map.GetUserWaypoint() if not wp then return false end if tonumber(wp.uiMapID) ~= tonumber(poi.mapID) then return false end local px = (tonumber(poi.x) or 0) / 100 local py = (tonumber(poi.y) or 0) / 100 local wx = tonumber(wp.position and wp.position.x) or -1 local wy = tonumber(wp.position and wp.position.y) or -1 return math.abs(px - wx) <= 0.0005 and math.abs(py - wy) <= 0.0005 end function MapOverlay:ToggleWaypointForPOI(poi) if not poi or not C_Map or not UiMapPoint then return end if type(C_Map.ClearUserWaypoint) ~= "function" or type(C_Map.SetUserWaypoint) ~= "function" then return end if self:IsPOIUserWaypoint(poi) then C_Map.ClearUserWaypoint() if C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then C_SuperTrack.SetSuperTrackedUserWaypoint(false) end return end local mapID = tonumber(poi.mapID) local x = (tonumber(poi.x) or 0) / 100 local y = (tonumber(poi.y) or 0) / 100 local mapPoint = UiMapPoint.CreateFromCoordinates(mapID, x, y) if not mapPoint then return end C_Map.SetUserWaypoint(mapPoint) if C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then C_SuperTrack.SetSuperTrackedUserWaypoint(true) end end function MapOverlay:GetSettings() local p = HMGT.db and HMGT.db.profile if not p then return nil end p.mapOverlay = p.mapOverlay or {} p.mapOverlay.pois = p.mapOverlay.pois or {} if p.mapOverlay.enabled == nil then p.mapOverlay.enabled = true end p.mapOverlay.iconSize = clamp(p.mapOverlay.iconSize, 8, 48, 16) p.mapOverlay.alpha = clamp(p.mapOverlay.alpha, 0.1, 1, 1) if p.mapOverlay.showLabels == nil then p.mapOverlay.showLabels = true end for _, poi in ipairs(p.mapOverlay.pois) do if type(poi) == "table" then local icon, iconCoords, _, normalizedCategory = self:GetCategoryVisual(poi.category) poi.category = normalizedCategory poi.icon = icon poi.iconCoords = iconCoords end end return p.mapOverlay end function MapOverlay:GetActiveMapID() if WorldMapFrame and WorldMapFrame.GetMapID then local mapID = WorldMapFrame:GetMapID() if mapID then return mapID end end if C_Map and C_Map.GetBestMapForUnit then return C_Map.GetBestMapForUnit("player") end return nil end function MapOverlay:GetCurrentPlayerPosition() if not C_Map or type(C_Map.GetPlayerMapPosition) ~= "function" then return nil, nil, nil, "api unavailable" end local mapID = tonumber(self:GetActiveMapID()) if not mapID and C_Map.GetBestMapForUnit then mapID = tonumber(C_Map.GetBestMapForUnit("player")) end if not mapID then return nil, nil, nil, "map unavailable" end local pos = C_Map.GetPlayerMapPosition(mapID, "player") if (not pos or pos.x == nil or pos.y == nil) and C_Map.GetBestMapForUnit then local bestMapID = tonumber(C_Map.GetBestMapForUnit("player")) if bestMapID and bestMapID ~= mapID then mapID = bestMapID pos = C_Map.GetPlayerMapPosition(mapID, "player") end end if not pos then return mapID, nil, nil, "position unavailable" end local x, y if type(pos.GetXY) == "function" then x, y = pos:GetXY() else x, y = pos.x, pos.y end x = tonumber(x) y = tonumber(y) if not x or not y then return mapID, nil, nil, "position unavailable" end return mapID, clamp(x * 100, 0, 100, 0), clamp(y * 100, 0, 100, 0) end function MapOverlay:AddPOI(mapID, x, y, label, icon, category) local s = self:GetSettings() if not s then return false, "no settings" end mapID = tonumber(mapID) x = tonumber(x) y = tonumber(y) if not mapID or not x or not y then return false, "invalid coordinates" end x = clamp(x, 0, 100, 0) y = clamp(y, 0, 100, 0) label = tostring(label or ""):gsub("^%s+", ""):gsub("%s+$", "") if label == "" then label = string.format("POI %.1f, %.1f", x, y) end local iconCoords icon, iconCoords, _, category = self:GetCategoryVisual(category) s.pois[#s.pois + 1] = { mapID = mapID, x = x, y = y, label = label, icon = icon, iconCoords = iconCoords, category = category, } self:Refresh() self:UpdatePOIManagerFrame() return true, #s.pois end function MapOverlay:UpdatePOI(index, mapID, x, y, label, icon, category) local s = self:GetSettings() if not s then return false, "no settings" end index = tonumber(index) if not index or index < 1 or index > #s.pois then return false, "invalid index" end mapID = tonumber(mapID) x = tonumber(x) y = tonumber(y) if not mapID or not x or not y then return false, "invalid coordinates" end x = clamp(x, 0, 100, 0) y = clamp(y, 0, 100, 0) label = tostring(label or ""):gsub("^%s+", ""):gsub("%s+$", "") if label == "" then label = string.format("POI %.1f, %.1f", x, y) end local iconCoords icon, iconCoords, _, category = self:GetCategoryVisual(category) local poi = s.pois[index] poi.mapID = mapID poi.x = x poi.y = y poi.label = label poi.icon = icon poi.iconCoords = iconCoords poi.category = category self:Refresh() self:UpdatePOIManagerFrame() return true end function MapOverlay:RemovePOI(index) local s = self:GetSettings() if not s then return false end index = tonumber(index) if not index or index < 1 or index > #s.pois then return false end table.remove(s.pois, index) if self._poiManagerSelected and self._poiManagerSelected > #s.pois then self._poiManagerSelected = #s.pois end if self._poiManagerSelected and self._poiManagerSelected < 1 then self._poiManagerSelected = nil end self:Refresh() self:UpdatePOIManagerFrame() return true end function MapOverlay:BuildPOIListText(mapID) local s = self:GetSettings() if not s or type(s.pois) ~= "table" or #s.pois == 0 then return "No POIs configured." end local filterMap = tonumber(mapID) local out = {} for i, poi in ipairs(s.pois) do if (not filterMap) or poi.mapID == filterMap then out[#out + 1] = string.format( "%d) map=%d x=%.2f y=%.2f [%s] %s", i, tonumber(poi.mapID) or 0, tonumber(poi.x) or 0, tonumber(poi.y) or 0, tostring(self:GetCategoryLabel(poi.category)), tostring(poi.label or "POI") ) end end if #out == 0 then return "No POIs for this map." end return table.concat(out, "\n") end function MapOverlay:CreatePOIManagerFrame() if self.poiManagerFrame then return self.poiManagerFrame end local frame = CreateFrame("Frame", "HMGTMapPOIManagerFrame", UIParent, "BasicFrameTemplateWithInset") frame:SetSize(470, 500) frame:SetPoint("CENTER") frame:SetFrameStrata("DIALOG") frame:Hide() frame:SetMovable(true) frame:EnableMouse(true) frame:RegisterForDrag("LeftButton") frame:SetScript("OnDragStart", frame.StartMoving) frame:SetScript("OnDragStop", frame.StopMovingOrSizing) frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight") frame.title:SetPoint("LEFT", frame.TitleBg, "LEFT", 8, 0) frame.title:SetText("HMGT - POIs") frame.subtitle = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") frame.subtitle:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -34) frame.subtitle:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -34) frame.subtitle:SetJustifyH("LEFT") frame.subtitle:SetText("Klicke auf einen Eintrag, um Details unten anzuzeigen.") local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate") scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -54) scroll:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -32, -54) scroll:SetHeight(250) local content = CreateFrame("Frame", nil, scroll) content:SetSize(1, 1) scroll:SetScrollChild(content) frame.scroll = scroll frame.content = content frame.rows = {} frame.emptyText = content:CreateFontString(nil, "OVERLAY", "GameFontDisable") frame.emptyText:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -4) frame.emptyText:SetText("Keine POIs vorhanden.") frame.detailHeader = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") frame.detailHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -320) frame.detailHeader:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -320) frame.detailHeader:SetJustifyH("LEFT") frame.detailHeader:SetText("Details") frame.detailText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") frame.detailText:SetPoint("TOPLEFT", frame.detailHeader, "BOTTOMLEFT", 0, -8) frame.detailText:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -336) frame.detailText:SetJustifyH("LEFT") frame.detailText:SetJustifyV("TOP") frame.detailText:SetText("Waehle einen POI aus der Liste.") frame.waypointButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") frame.waypointButton:SetSize(130, 24) frame.waypointButton:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 14, 14) frame.waypointButton:SetText("Waypoint") frame.waypointButton:SetScript("OnClick", function() local s = self:GetSettings() local idx = tonumber(self._poiManagerSelected) local poi = s and s.pois and idx and s.pois[idx] if not poi then return end self:ToggleWaypointForPOI(poi) end) frame.removeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") frame.removeButton:SetSize(130, 24) frame.removeButton:SetPoint("LEFT", frame.waypointButton, "RIGHT", 8, 0) frame.removeButton:SetText("Entfernen") frame.removeButton:SetScript("OnClick", function() local idx = tonumber(self._poiManagerSelected) if not idx then return end self:RemovePOI(idx) end) frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") frame.closeButton:SetSize(90, 24) frame.closeButton:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14) frame.closeButton:SetText(CLOSE or "Close") frame.closeButton:SetScript("OnClick", function() frame:Hide() end) self.poiManagerFrame = frame return frame end function MapOverlay:AcquirePOIManagerRow(index) local frame = self.poiManagerFrame if not frame then return nil end local row = frame.rows[index] if row then return row end row = CreateFrame("Button", nil, frame.content) row:SetHeight(22) row.bg = row:CreateTexture(nil, "BACKGROUND") row.bg:SetAllPoints(row) row.bg:SetColorTexture(1, 1, 1, 0.05) row.highlight = row:CreateTexture(nil, "HIGHLIGHT") row.highlight:SetAllPoints(row) row.highlight:SetColorTexture(1, 1, 1, 0.12) row.expandText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") row.expandText:SetPoint("LEFT", row, "LEFT", 6, 0) row.expandText:SetText("+") row.labelText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight") row.labelText:SetPoint("LEFT", row.expandText, "RIGHT", 8, 0) row.labelText:SetPoint("RIGHT", row, "RIGHT", -8, 0) row.labelText:SetJustifyH("LEFT") row:SetScript("OnClick", function(btn) local idx = btn._index if not idx then return end if self._poiManagerSelected == idx then self._poiManagerSelected = nil else self._poiManagerSelected = idx end self:UpdatePOIManagerFrame() end) frame.rows[index] = row return row end function MapOverlay:UpdatePOIManagerFrame() local frame = self.poiManagerFrame if not frame or not frame:IsShown() then return end local s = self:GetSettings() local pois = (s and s.pois) or {} local rowHeight = 22 local offsetY = -2 for i, poi in ipairs(pois) do local row = self:AcquirePOIManagerRow(i) row._index = i row:ClearAllPoints() row:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 0, offsetY) row:SetPoint("TOPRIGHT", frame.content, "TOPRIGHT", 0, offsetY) local selected = (self._poiManagerSelected == i) row.expandText:SetText(selected and "-" or "+") row.labelText:SetText(string.format("(%d) %s", i, tostring(poi.label or ("POI " .. i)))) if selected then row.bg:SetColorTexture(0.20, 0.45, 0.80, 0.22) else local alpha = (i % 2 == 0) and 0.03 or 0.07 row.bg:SetColorTexture(1, 1, 1, alpha) end row:Show() offsetY = offsetY - rowHeight end for i = #pois + 1, #frame.rows do frame.rows[i]:Hide() frame.rows[i]._index = nil end frame.content:SetHeight(math.max(1, #pois * rowHeight + 6)) frame.emptyText:SetShown(#pois == 0) local selected = tonumber(self._poiManagerSelected) local poi = selected and pois[selected] or nil if poi then frame.detailHeader:SetText(string.format("%s (%d)", tostring(poi.label or "POI"), selected)) frame.detailText:SetText(string.format( "Map ID: %d\nX: %.2f\nY: %.2f\nIcon: %s", tonumber(poi.mapID) or 0, tonumber(poi.x) or 0, tonumber(poi.y) or 0, self:GetCategoryLabel(poi.category) )) frame.waypointButton:Enable() frame.removeButton:Enable() else frame.detailHeader:SetText("Details") frame.detailText:SetText("Waehle einen POI aus der Liste.") frame.waypointButton:Disable() frame.removeButton:Disable() end end function MapOverlay:ShowPOIManager() local frame = self:CreatePOIManagerFrame() frame:Show() frame:Raise() self:UpdatePOIManagerFrame() end function MapOverlay:HidePOIManager() if self.poiManagerFrame then self.poiManagerFrame:Hide() end end function MapOverlay:TogglePOIManager() local frame = self:CreatePOIManagerFrame() if frame:IsShown() then frame:Hide() return end frame:Show() frame:Raise() self:UpdatePOIManagerFrame() end local WORLD_MAP_PIN_TEMPLATE = "HMGTMapOverlayPinTemplate" local WorldMapPinMixin = CreateFromMixins(MapCanvasPinMixin) local WorldMapDataProviderMixin = CreateFromMixins(MapCanvasDataProviderMixin) _G.HMGTMapOverlayPinMixin = WorldMapPinMixin function WorldMapPinMixin:EnsureVisuals() if not self.shadow then self.shadow = self:CreateTexture(nil, "BACKGROUND") self.shadow:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1) self.shadow:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 1, -1) end ApplyRingVisual(self.shadow, unpack(PIN_SHADOW_COLOR)) if not self.ring then self.ring = self:CreateTexture(nil, "ARTWORK", nil, -2) self.ring:SetAllPoints(self) end ApplyRingVisual(self.ring, unpack(PIN_RING_COLOR)) if self.innerStroke then self.innerStroke:Hide() end if self.fill then self.fill:Hide() end if not self.texture then self.texture = self:CreateTexture(nil, "OVERLAY") self.texture:SetTexCoord(0.07, 0.93, 0.07, 0.93) end if not self.textureMask then EnsureRoundMask(self, "textureMask", self.texture) end if not self.label then self.label = self:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") self.label:SetPoint("LEFT", self, "RIGHT", 3, 0) self.label:SetJustifyH("LEFT") self.label:SetText("") end if self.RegisterForClicks then self:RegisterForClicks("LeftButtonUp", "RightButtonUp") end end function WorldMapPinMixin:UpdateVisualLayout(pinSize, fullSizeTexture) self:EnsureVisuals() self.texture:ClearAllPoints() if fullSizeTexture then self.texture:SetAllPoints(self) else local iconInset = GetIconInset(pinSize) self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", iconInset, -iconInset) self.texture:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -iconInset, iconInset) end if self.textureMask then self.textureMask:ClearAllPoints() self.textureMask:SetAllPoints(self.texture) end end function WorldMapPinMixin:OnLoad() self:EnsureVisuals() self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI") if self.SetScalingLimits then self:SetScalingLimits(1, 1.0, 1.2) end if self.SetPassThroughButtons then self:SetPassThroughButtons("") end end function WorldMapPinMixin:OnAcquired(poi, settings, owner) self:EnsureVisuals() self._poi = poi self._owner = owner self._settings = settings local pinSize = tonumber(settings and settings.iconSize) or 16 local usesAtlasIcon = owner and owner.UsesAtlasIcon and owner:UsesAtlasIcon(poi.category) or false self:SetPosition((tonumber(poi.x) or 0) / 100, (tonumber(poi.y) or 0) / 100) self:SetSize(pinSize, pinSize) self:SetAlpha(tonumber(settings and settings.alpha) or 1) self:UpdateVisualLayout(pinSize, usesAtlasIcon) if usesAtlasIcon then SetTextureMaskEnabled(self, self.texture, false) self.shadow:Hide() self.ring:Hide() else SetTextureMaskEnabled(self, self.texture, true) self.shadow:Show() self.ring:Show() end local texture, iconCoords = nil, nil if owner and owner.GetCategoryVisual then texture, iconCoords = owner:GetCategoryVisual(poi.category) end self.texture:SetTexture(texture or poi.icon or DEFAULT_ICON) if type(iconCoords) == "table" then self.texture:SetTexCoord(unpack(iconCoords)) elseif type(poi.iconCoords) == "table" then self.texture:SetTexCoord(unpack(poi.iconCoords)) else self.texture:SetTexCoord(0.07, 0.93, 0.07, 0.93) end self.texture:SetVertexColor(1, 1, 1, 1) self.texture:Show() if settings and settings.showLabels then self.label:SetText(tostring(poi.label or "POI")) self.label:Show() else self.label:SetText("") self.label:Hide() end end function WorldMapPinMixin:OnReleased() self._poi = nil self._owner = nil self._settings = nil if self.texture then self.texture:SetTexture(nil) end if self.label then self.label:SetText("") self.label:Hide() end GameTooltip:Hide() end function WorldMapPinMixin:OnMouseEnter() local poi = self._poi local owner = self._owner if not poi then return end GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:AddLine(tostring(poi.label or "POI"), 1, 1, 1) GameTooltip:AddLine(string.format("Map: %d", tonumber(poi.mapID) or 0), 0.8, 0.8, 0.8) GameTooltip:AddLine(string.format("X: %.2f Y: %.2f", tonumber(poi.x) or 0, tonumber(poi.y) or 0), 0.8, 0.8, 0.8) if poi.category then local label = owner and owner.GetCategoryLabel and owner:GetCategoryLabel(poi.category) or tostring(poi.category) GameTooltip:AddLine("Icon: " .. tostring(label), 0.8, 0.8, 0.8) end GameTooltip:AddLine("Left click: toggle waypoint", 0.8, 0.8, 0.8) HMGT:SafeShowTooltip(GameTooltip) end function WorldMapPinMixin:OnMouseLeave() GameTooltip:Hide() end function WorldMapPinMixin:OnClick(mouseButton) if mouseButton ~= "LeftButton" then return end local owner = self._owner local poi = self._poi if not owner or not poi then return end owner:ToggleWaypointForPOI(poi) end function WorldMapPinMixin:SetPassThroughButtons() end function WorldMapDataProviderMixin:RemoveAllData() local map = self:GetMap() if map then map:RemoveAllPinsByTemplate(WORLD_MAP_PIN_TEMPLATE) end end function WorldMapDataProviderMixin:RefreshAllData() self:RemoveAllData() local map = self:GetMap() local owner = self.owner if not map or not owner then return end local settings = owner:GetSettings() if not settings or settings.enabled ~= true then return end local mapID = map:GetMapID() if not mapID then return end for _, poi in ipairs(settings.pois or {}) do if tonumber(poi.mapID) == tonumber(mapID) then map:AcquirePin(WORLD_MAP_PIN_TEMPLATE, poi, settings, owner) end end end function MapOverlay:EnsureWorldMapPinPool() if not WorldMapFrame then return false end WorldMapFrame.pinPools = WorldMapFrame.pinPools or {} if WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE] then self._worldMapPinPool = WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE] return true end local canvas = WorldMapFrame.GetCanvas and WorldMapFrame:GetCanvas() if not canvas then canvas = WorldMapFrame.ScrollContainer and WorldMapFrame.ScrollContainer.Child end if not canvas then return false end local pool if type(CreateUnsecuredRegionPoolInstance) == "function" then pool = CreateUnsecuredRegionPoolInstance(WORLD_MAP_PIN_TEMPLATE) elseif type(CreateFramePool) == "function" then pool = CreateFramePool("BUTTON") end if not pool then return false end pool.parent = canvas pool.createFunc = function() local frame = CreateFrame("Button", nil, canvas) frame:SetSize(1, 1) if type(Mixin) == "function" then Mixin(frame, WorldMapPinMixin) end if frame.OnLoad then frame:OnLoad() end return frame end pool.resetFunc = function(_, pin) pin:Hide() pin:ClearAllPoints() if pin.OnReleased then pin:OnReleased() end pin.pinTemplate = nil pin.owningMap = nil end -- Compatibility with older pool field names. pool.creationFunc = pool.createFunc pool.resetterFunc = pool.resetFunc WorldMapFrame.pinPools[WORLD_MAP_PIN_TEMPLATE] = pool self._worldMapPinPool = pool return true end function MapOverlay:EnsureWorldMapProvider() if not WorldMapFrame or type(WorldMapFrame.AddDataProvider) ~= "function" then return false end if not self:EnsureWorldMapPinPool() then return false end if not self._worldMapProvider then self._worldMapProvider = CreateFromMixins(WorldMapDataProviderMixin) self._worldMapProvider.owner = self end if not self._worldMapProviderRegistered then WorldMapFrame:AddDataProvider(self._worldMapProvider) self._worldMapProviderRegistered = true end return true end function MapOverlay:Refresh() if not self:EnsureWorldMapProvider() then return end self._worldMapProvider:RefreshAllData() end function MapOverlay:HookWorldMap() if self._mapHooked then return end if not WorldMapFrame then return end self._mapHooked = true self:EnsureWorldMapProvider() WorldMapFrame:HookScript("OnShow", function() self:Refresh() end) WorldMapFrame:HookScript("OnHide", function() if self._worldMapProvider then self._worldMapProvider:RemoveAllData() end end) hooksecurefunc(WorldMapFrame, "SetMapID", function() self:Refresh() end) end function MapOverlay:OnEnable() self:GetSettings() self:RegisterEvent("ADDON_LOADED", "OnAddonLoaded") self:RegisterEvent("ZONE_CHANGED_NEW_AREA", "Refresh") self:RegisterEvent("ZONE_CHANGED", "Refresh") self:RegisterEvent("ZONE_CHANGED_INDOORS", "Refresh") self:RegisterEvent("PLAYER_ENTERING_WORLD", "Refresh") self:HookWorldMap() self:Refresh() end function MapOverlay:OnAddonLoaded(_, addonName) if addonName == "Blizzard_WorldMap" then self:HookWorldMap() self:Refresh() end end function MapOverlay:OnDisable() self:UnregisterAllEvents() if self._worldMapProvider then self._worldMapProvider:RemoveAllData() end self:HidePOIManager() end local function GetMapModule(self) if self and self.MapOverlay then return self.MapOverlay end if self and self.GetModule then return self:GetModule("MapOverlay", true) end return nil end function HMGT:AddMapPOI(mapID, x, y, label, icon, category) local mod = GetMapModule(self) if not mod then return false, "module missing" end return mod:AddPOI(mapID, x, y, label, icon, category) end function HMGT:RemoveMapPOI(index) local mod = GetMapModule(self) if not mod then return false end return mod:RemovePOI(index) end function HMGT:UpdateMapPOI(index, mapID, x, y, label, icon, category) local mod = GetMapModule(self) if not mod then return false, "module missing" end return mod:UpdatePOI(index, mapID, x, y, label, icon, category) end function HMGT:BuildMapPOIListText(mapID) local mod = GetMapModule(self) if not mod then return "Map module missing." end return mod:BuildPOIListText(mapID) end function HMGT:GetMapPOICategoryValues() local mod = GetMapModule(self) if not mod then return { [DEFAULT_CATEGORY] = "Default" } end return mod:GetCategoryDropdownValues() end function HMGT:GetCurrentMapPOIData() local mod = GetMapModule(self) if not mod then return nil, nil, nil, "module missing" end return mod:GetCurrentPlayerPosition() end function HMGT:ToggleMapPOIManager() local mod = GetMapModule(self) if not mod then return false end mod:TogglePOIManager() return true end