558 lines
17 KiB
Lua
558 lines
17 KiB
Lua
local useUiScale = tonumber(GetCVar("useUiScale"))
|
|
local uiScale = GetCVar("uiScale")
|
|
if useUiScale ~= 1 then uiScale = 1 end
|
|
frameWidth = UIParent:GetWidth() / uiScale
|
|
xMiddle = frameWidth / 2
|
|
frameHeight = UIParent:GetHeight() / uiScale
|
|
yMiddle = frameHeight / 2
|
|
frameScale = 0.75999999046326
|
|
local _G = getfenv(0)
|
|
local timeElapsed = 0
|
|
local updateTime = 1/60
|
|
CTextures = {}
|
|
CRotateTextures = {}
|
|
|
|
-- Default minimap dimensions (before any scaling)
|
|
local defaultMinimapWidth = 140
|
|
local defaultMinimapHeight = 140
|
|
|
|
-- Track hidden minimap buttons so we can restore them
|
|
local hiddenMinimapButtons = {}
|
|
|
|
|
|
-- Minimap cluster Show hook state (for minimap hide/show toggle)
|
|
local minimapClusterHooked = false
|
|
local minimapClusterOrigShow = nil
|
|
local minimapOrigShow = nil
|
|
|
|
-- Minimap button Show hook state
|
|
local minimapBtnHooked = false
|
|
|
|
-- Saved original Show functions for minimap buttons
|
|
local hiddenBtnOrigShows = {}
|
|
|
|
-- Saved original anchor points for minimap buttons (so positions are preserved across hide/show)
|
|
-- Format: hiddenBtnOrigPoints[frame] = { {point, relativeTo, relativePoint, xOfs, yOfs}, ... }
|
|
local hiddenBtnOrigPoints = {}
|
|
|
|
-- Addon version for SavedVariables migration
|
|
local ADDON_VERSION = 7
|
|
|
|
local function print(msg)
|
|
if msg and msg ~= "" then
|
|
DEFAULT_CHAT_FRAME:AddMessage(msg, 1, 1, 0)
|
|
end
|
|
end
|
|
|
|
function deepCopyHash(t)
|
|
if t == nil then return nil end
|
|
local nt = {}
|
|
for k, v in pairs(t) do
|
|
if type(v) == "table" then
|
|
nt[k] = deepCopyHash(v)
|
|
else
|
|
nt[k] = v
|
|
end
|
|
end
|
|
return nt
|
|
end
|
|
|
|
function destroyTable(t)
|
|
setmetatable(t, nil)
|
|
for k,v in pairs(t) do
|
|
t[k] = nil
|
|
end
|
|
end
|
|
|
|
-- ============================================
|
|
-- SavedVariables Migration
|
|
-- ============================================
|
|
-- Detects and fixes stale/invalid data from older addon versions.
|
|
-- Called during PLAYER_ENTERING_WORLD before any settings are applied.
|
|
local function MigrateSavedVariables()
|
|
if not SexyMap2DB then
|
|
SexyMap2DB = {}
|
|
end
|
|
|
|
-- Version-based migration: if the saved version is older than current,
|
|
-- apply fixes. This runs ONCE per version bump.
|
|
local savedVersion = SexyMap2DB.version or 0
|
|
|
|
-- Always check for invalid data regardless of version,
|
|
-- since the user might have stale data from before version tracking existed.
|
|
|
|
-- Fix empty borders array (from v1.4 when some presets had empty borders)
|
|
if SexyMap2DB.borders == nil or (type(SexyMap2DB.borders) == "table" and table.getn(SexyMap2DB.borders) == 0 and next(SexyMap2DB.borders) == nil) then
|
|
SexyMap2DB.borders = deepCopyHash(borderPresets["Diablo by Zork"].borders)
|
|
print("[SexyMap] Migrated: empty borders reset to default preset.")
|
|
end
|
|
|
|
-- Fix backdrop with invalid/missing data
|
|
if SexyMap2DB.backdrop == nil then
|
|
SexyMap2DB.backdrop = deepCopyHash(borderPresets["Diablo by Zork"].backdrop)
|
|
print("[SexyMap] Migrated: missing backdrop reset to default preset.")
|
|
else
|
|
-- Fix empty borderColor/textureColor that cause green rendering
|
|
if SexyMap2DB.backdrop.borderColor == nil or (type(SexyMap2DB.backdrop.borderColor) == "table" and next(SexyMap2DB.backdrop.borderColor) == nil) then
|
|
SexyMap2DB.backdrop.borderColor = { r = 1, g = 1, b = 1, a = 1 }
|
|
end
|
|
if SexyMap2DB.backdrop.textureColor == nil or (type(SexyMap2DB.backdrop.textureColor) == "table" and next(SexyMap2DB.backdrop.textureColor) == nil) then
|
|
SexyMap2DB.backdrop.textureColor = { r = 0, g = 0, b = 0, a = 1 }
|
|
end
|
|
-- Fix settings with .blp in edgeFile/bgFile (WoW 1.12 auto-appends .blp)
|
|
if SexyMap2DB.backdrop.settings then
|
|
local s = SexyMap2DB.backdrop.settings
|
|
if s.edgeFile and string.find(s.edgeFile, "%.blp$") then
|
|
s.edgeFile = string.gsub(s.edgeFile, "%.blp$", "")
|
|
print("[SexyMap] Migrated: removed .blp from backdrop edgeFile.")
|
|
end
|
|
if s.bgFile and string.find(s.bgFile, "%.blp$") then
|
|
s.bgFile = string.gsub(s.bgFile, "%.blp$", "")
|
|
print("[SexyMap] Migrated: removed .blp from backdrop bgFile.")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fix missing/empty shape
|
|
if SexyMap2DB.shape == nil or SexyMap2DB.shape == "" then
|
|
SexyMap2DB.shape = "Round"
|
|
end
|
|
|
|
-- Ensure all expected fields exist
|
|
if SexyMap2DB.showZoneText == nil then
|
|
SexyMap2DB.showZoneText = true
|
|
end
|
|
if SexyMap2DB.showMinimapButtons == nil then
|
|
SexyMap2DB.showMinimapButtons = true
|
|
end
|
|
if SexyMap2DB.mapScale == nil then
|
|
SexyMap2DB.mapScale = 1.0
|
|
end
|
|
if SexyMap2DB.showMinimap == nil then
|
|
SexyMap2DB.showMinimap = true
|
|
end
|
|
if not SexyMap2DB.userPresets then
|
|
SexyMap2DB.userPresets = {}
|
|
end
|
|
|
|
-- Mark the current version so migration doesn't re-run needlessly
|
|
-- (but the checks above still protect against manually-edited bad data)
|
|
SexyMap2DB.version = ADDON_VERSION
|
|
end
|
|
|
|
function CMap_OnLoad()
|
|
print("[SexyMap] v1.7 loaded for OctoWoW. Type '/sm' or '/sexymap' to open the menu.")
|
|
this:SetScript("OnEvent", CMap_OnEvent)
|
|
this:SetScript("OnUpdate", CMap_OnUpdate)
|
|
this:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
end
|
|
|
|
function CMap_OnEvent()
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
this:UnregisterEvent("PLAYER_ENTERING_WORLD")
|
|
|
|
-- Run migration FIRST, before any settings are applied
|
|
MigrateSavedVariables()
|
|
|
|
-- Capture the default minimap dimensions before we modify anything
|
|
defaultMinimapWidth = Minimap:GetWidth()
|
|
defaultMinimapHeight = Minimap:GetHeight()
|
|
|
|
CreateFrame("Frame", "CMinimapBackdrop", Minimap)
|
|
CMinimapBackdrop:SetFrameStrata("BACKGROUND")
|
|
CMinimapBackdrop:SetFrameLevel(CMinimapBackdrop:GetFrameLevel() - 1)
|
|
CMinimapBackdrop:SetPoint("CENTER", Minimap, "CENTER")
|
|
CMinimapBackdrop:SetWidth(Minimap:GetWidth())
|
|
CMinimapBackdrop:SetHeight(Minimap:GetHeight())
|
|
-- Set an explicit default backdrop so the frame never renders as green
|
|
-- (a frame with no backdrop set can render as solid green in WoW 1.12)
|
|
CMinimapBackdrop:SetBackdrop({
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false,
|
|
edgeSize = 16,
|
|
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
|
})
|
|
CMinimapBackdrop:SetBackdropColor(0, 0, 0, 1)
|
|
CMinimapBackdrop:SetBackdropBorderColor(1, 1, 1, 1)
|
|
|
|
CHideAnnoyingStuff()
|
|
CUpdateZoneText()
|
|
CUpdateMinimapButtonsVisibility()
|
|
CUpdateMinimapVisibility()
|
|
CApplyMapScale()
|
|
CLoadArtwork()
|
|
SlashCmdList["CMAP"] = CMap_SlashCmdHandler
|
|
SLASH_CMAP1 = "/cm"
|
|
SLASH_CMAP2 = "/sm"
|
|
SLASH_CMAP3 = "/sexymap"
|
|
end
|
|
end
|
|
|
|
|
|
function CMap_OnUpdate()
|
|
timeElapsed = timeElapsed + arg1
|
|
if timeElapsed >= updateTime then
|
|
for k,v in pairs(CRotateTextures) do
|
|
CRotateTexture(_G[k], v * timeElapsed)
|
|
end
|
|
timeElapsed = 0
|
|
end
|
|
end
|
|
|
|
|
|
function CMap_SlashCmdHandler(msg)
|
|
if not CMinimapOptionFrame.menuState then
|
|
CInitiateMenu()
|
|
else
|
|
CHideMenu()
|
|
end
|
|
end
|
|
|
|
|
|
function CHideAnnoyingStuff()
|
|
-- Hide the default minimap border art (we draw our own borders)
|
|
MinimapBorder:Hide()
|
|
MinimapBorderTop:Hide()
|
|
-- Hide the X button (minimap toggle) — we now control this from the GUI
|
|
MinimapToggleButton:Hide()
|
|
-- Hide zoom buttons ( SexyMap handles its own zoom or user doesn't need them )
|
|
MinimapZoomIn:Hide()
|
|
MinimapZoomOut:Hide()
|
|
-- Style the zone text font
|
|
MinimapZoneText:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
|
|
end
|
|
|
|
-- ============================================
|
|
-- Minimap visibility (replaces the X button)
|
|
-- ============================================
|
|
-- In vanilla WoW 1.12, MinimapToggleButton calls ToggleMinimap() which
|
|
-- hides/shows just the Minimap frame. We replace this with a full
|
|
-- MinimapCluster hide/show controlled from the GUI, and hook Show/Hide
|
|
-- to prevent anything from re-showing the minimap when it should be hidden.
|
|
|
|
local function MinimapClusterHookedShow(self)
|
|
if not SexyMap2DB.showMinimap then
|
|
return -- Block the Show, minimap stays hidden
|
|
end
|
|
if minimapClusterOrigShow then
|
|
minimapClusterOrigShow(self)
|
|
end
|
|
end
|
|
|
|
local function MinimapHookedShow(self)
|
|
if not SexyMap2DB.showMinimap then
|
|
return -- Block the Show, minimap stays hidden
|
|
end
|
|
if minimapOrigShow then
|
|
minimapOrigShow(self)
|
|
end
|
|
end
|
|
|
|
function CUpdateMinimapVisibility()
|
|
if SexyMap2DB.showMinimap then
|
|
-- Show the minimap cluster - unhook first
|
|
if minimapClusterHooked then
|
|
if MinimapCluster then
|
|
MinimapCluster.Show = minimapClusterOrigShow
|
|
end
|
|
if Minimap then
|
|
Minimap.Show = minimapOrigShow
|
|
end
|
|
minimapClusterHooked = false
|
|
minimapClusterOrigShow = nil
|
|
minimapOrigShow = nil
|
|
end
|
|
-- Show everything
|
|
if MinimapCluster then MinimapCluster:Show() end
|
|
if Minimap then Minimap:Show() end
|
|
-- Re-apply our hidden stuff (borders, zoom buttons, X button stay hidden)
|
|
CHideAnnoyingStuff()
|
|
-- Re-apply button visibility settings
|
|
CUpdateMinimapButtonsVisibility()
|
|
else
|
|
-- Hide the entire minimap cluster and hook Show to prevent re-appearance
|
|
if MinimapCluster then
|
|
if not minimapClusterHooked then
|
|
minimapClusterOrigShow = MinimapCluster.Show
|
|
MinimapCluster.Show = MinimapClusterHookedShow
|
|
minimapOrigShow = Minimap.Show
|
|
Minimap.Show = MinimapHookedShow
|
|
minimapClusterHooked = true
|
|
end
|
|
MinimapCluster:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================
|
|
-- Minimap buttons visibility
|
|
-- ============================================
|
|
|
|
local function MinimapBtnHookedShow(self)
|
|
if not SexyMap2DB.showMinimapButtons then
|
|
return -- Block Show while buttons should be hidden
|
|
end
|
|
-- When buttons are allowed, call the original Show
|
|
local orig = hiddenBtnOrigShows[self]
|
|
if orig then
|
|
orig(self)
|
|
end
|
|
end
|
|
|
|
-- Save a frame's current anchor points so we can restore them later
|
|
local function SaveButtonPoints(frame)
|
|
local points = {}
|
|
local numPoints = frame:GetNumPoints()
|
|
if numPoints and numPoints > 0 then
|
|
for i = 1, numPoints do
|
|
local point, relativeTo, relativePoint, xOfs, yOfs = frame:GetPoint(i)
|
|
if point then
|
|
table.insert(points, {
|
|
point = point,
|
|
relativeTo = relativeTo,
|
|
relativePoint = relativePoint,
|
|
xOfs = xOfs,
|
|
yOfs = yOfs,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
-- Only store if we actually got points; don't overwrite existing saved points with empty
|
|
if table.getn(points) > 0 then
|
|
hiddenBtnOrigPoints[frame] = points
|
|
end
|
|
end
|
|
|
|
-- Restore a frame's saved anchor points
|
|
local function RestoreButtonPoints(frame)
|
|
local points = hiddenBtnOrigPoints[frame]
|
|
if points and table.getn(points) > 0 then
|
|
frame:ClearAllPoints()
|
|
for _, pt in ipairs(points) do
|
|
frame:SetPoint(pt.point, pt.relativeTo, pt.relativePoint, pt.xOfs, pt.yOfs)
|
|
end
|
|
end
|
|
-- Clean up saved points for this frame
|
|
hiddenBtnOrigPoints[frame] = nil
|
|
end
|
|
|
|
function CUpdateMinimapButtonsVisibility()
|
|
if SexyMap2DB.showMinimapButtons then
|
|
-- Show only the buttons we previously hid (preserves original visibility)
|
|
for _, btn in ipairs(hiddenMinimapButtons) do
|
|
if btn then
|
|
-- Restore original Show
|
|
local orig = hiddenBtnOrigShows[btn]
|
|
if orig then
|
|
btn.Show = orig
|
|
hiddenBtnOrigShows[btn] = nil
|
|
end
|
|
-- Restore saved anchor points (preserves button positions across hide/show)
|
|
RestoreButtonPoints(btn)
|
|
btn:Show()
|
|
end
|
|
end
|
|
hiddenMinimapButtons = {}
|
|
else
|
|
-- Find and hide only VISIBLE minimap buttons, hook their Show
|
|
hiddenMinimapButtons = {}
|
|
local children = { Minimap:GetChildren() }
|
|
for idx, frame in ipairs(children) do
|
|
local objType = frame:GetObjectType()
|
|
if objType == "Button" or objType == "CheckButton" then
|
|
local name = frame:GetName() or ""
|
|
-- Skip our own frames and WoW native controls we already hide
|
|
if name ~= "MinimapZoomIn" and name ~= "MinimapZoomOut"
|
|
and name ~= "MinimapToggleButton" and name ~= "GameTimeFrame"
|
|
and name ~= "CMinimapBackdrop" then
|
|
-- Only hide buttons that are currently visible
|
|
-- (buttons already hidden by other addons should stay hidden)
|
|
if frame:IsVisible() then
|
|
-- Save anchor points before hiding so we can restore them later
|
|
SaveButtonPoints(frame)
|
|
-- Hook Show to prevent re-appearance
|
|
if not hiddenBtnOrigShows[frame] then
|
|
hiddenBtnOrigShows[frame] = frame.Show
|
|
frame.Show = MinimapBtnHookedShow
|
|
end
|
|
frame:Hide()
|
|
table.insert(hiddenMinimapButtons, frame)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Also hide known WoW minimap frames by name (only if visible)
|
|
local knownFrames = {
|
|
"MiniMapTracking",
|
|
"MiniMapTrackingButton",
|
|
"MiniMapTrackingFrame",
|
|
"MiniMapMailFrame",
|
|
"MiniMapMailBorder",
|
|
"MiniMapBattlefieldFrame",
|
|
"MiniMapMeetingStoneFrame",
|
|
"MiniMapWorldMapButton",
|
|
"MinimapVoiceChatFrame",
|
|
"MiniMapLFGFrame",
|
|
}
|
|
for _, frameName in ipairs(knownFrames) do
|
|
local frame = _G[frameName]
|
|
if frame and frame:IsVisible() then
|
|
-- Save anchor points before hiding so we can restore them later
|
|
SaveButtonPoints(frame)
|
|
if not hiddenBtnOrigShows[frame] then
|
|
hiddenBtnOrigShows[frame] = frame.Show
|
|
frame.Show = MinimapBtnHookedShow
|
|
end
|
|
frame:Hide()
|
|
table.insert(hiddenMinimapButtons, frame)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function CApplyMapScale()
|
|
local scale = SexyMap2DB.mapScale or 1.0
|
|
if scale < 0.3 then scale = 0.3 end
|
|
if scale > 1.0 then scale = 1.0 end
|
|
SexyMap2DB.mapScale = scale
|
|
|
|
-- Apply scale to the MinimapCluster which contains everything
|
|
MinimapCluster:SetScale(scale)
|
|
|
|
-- Re-anchor to TOPRIGHT - the anchor point is TOPRIGHT so when scaled down,
|
|
-- the right side and top stay put, while the left and bottom move inward
|
|
MinimapCluster:ClearAllPoints()
|
|
MinimapCluster:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -14, 10)
|
|
|
|
-- Update backdrop to match current minimap size
|
|
CMinimapBackdrop:SetWidth(Minimap:GetWidth())
|
|
CMinimapBackdrop:SetHeight(Minimap:GetHeight())
|
|
|
|
-- Update border textures to match new minimap size
|
|
CUpdateBackdrop()
|
|
end
|
|
|
|
function CUpdateZoneText()
|
|
MinimapCluster:ClearAllPoints()
|
|
-- Map position: always anchored TOPRIGHT so scale keeps right/top fixed
|
|
MinimapCluster:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -14, 10)
|
|
MinimapZoneText:ClearAllPoints()
|
|
if SexyMap2DB.showZoneText then
|
|
MinimapZoneText:Show()
|
|
-- Zone name positioned below the minimap
|
|
MinimapZoneText:SetPoint("TOP", Minimap, "BOTTOM", 0, -20)
|
|
else
|
|
-- Only hide the zone text; map and border do NOT move
|
|
MinimapZoneText:Hide()
|
|
end
|
|
end
|
|
|
|
-- ============================================
|
|
-- Set Defaults - resets all settings to factory
|
|
-- ============================================
|
|
function CSetDefaults()
|
|
-- Reset all SavedVariables to defaults
|
|
SexyMap2DB.borders = deepCopyHash(borderPresets["Diablo by Zork"].borders)
|
|
SexyMap2DB.backdrop = deepCopyHash(borderPresets["Diablo by Zork"].backdrop)
|
|
SexyMap2DB.shape = "Round"
|
|
SexyMap2DB.userPresets = {}
|
|
SexyMap2DB.showZoneText = true
|
|
SexyMap2DB.showMinimapButtons = true
|
|
SexyMap2DB.mapScale = 1.0
|
|
SexyMap2DB.showMinimap = true
|
|
SexyMap2DB.version = ADDON_VERSION
|
|
|
|
-- Reapply everything
|
|
CHideAnnoyingStuff()
|
|
CUpdateZoneText()
|
|
CUpdateMinimapButtonsVisibility()
|
|
CUpdateMinimapVisibility()
|
|
CApplyMapScale()
|
|
CClearArtwork()
|
|
CLoadArtwork()
|
|
|
|
-- Refresh the GUI if it's open
|
|
if CMinimapOptionFrame and CMinimapOptionFrame.menuState then
|
|
CUpdatePresetList()
|
|
CUpdateShapeList()
|
|
CTexture_Update()
|
|
CZoneTextToggle_Update()
|
|
CMinimapButtonsToggle_Update()
|
|
CMinimapToggle_Update()
|
|
CMapScaleSlider_Update()
|
|
CVariable_Update()
|
|
end
|
|
|
|
print("[SexyMap] All settings reset to defaults.")
|
|
end
|
|
|
|
function CClearArtwork()
|
|
for k,v in pairs(CTextures) do
|
|
_G[k]:SetTexCoord(0,1,0,1)
|
|
_G[k]:Hide()
|
|
end
|
|
destroyTable(CTextures)
|
|
CTextures = {}
|
|
destroyTable(CRotateTextures)
|
|
CRotateTextures = {}
|
|
end
|
|
|
|
|
|
function CLoadArtwork()
|
|
if SexyMap2DB.shape then
|
|
CApplyShape(SexyMap2DB.shape)
|
|
end
|
|
for k,v in ipairs(SexyMap2DB.borders) do
|
|
CCreateBorder(v)
|
|
end
|
|
CUpdateBackdrop()
|
|
end
|
|
|
|
|
|
function CUpdateBackdrop()
|
|
if not SexyMap2DB.backdrop or not SexyMap2DB.backdrop.show then
|
|
CMinimapBackdrop:Hide()
|
|
else
|
|
local backdrop = CMinimapBackdrop
|
|
backdrop:Show()
|
|
backdrop:SetWidth(Minimap:GetWidth() * (SexyMap2DB.backdrop.scale or 1))
|
|
backdrop:SetHeight(Minimap:GetHeight() * (SexyMap2DB.backdrop.scale or 1))
|
|
|
|
-- Validate the backdrop settings before applying.
|
|
-- If edgeFile or bgFile references are invalid, WoW 1.12 renders the frame
|
|
-- as solid green (missing texture fallback). We check that the paths exist
|
|
-- in the settings table before calling SetBackdrop.
|
|
local settings = SexyMap2DB.backdrop.settings
|
|
if settings then
|
|
-- Ensure edgeFile doesn't have .blp extension (WoW 1.12 auto-appends it)
|
|
if settings.edgeFile and string.find(settings.edgeFile, "%.blp$") then
|
|
settings.edgeFile = string.gsub(settings.edgeFile, "%.blp$", "")
|
|
end
|
|
if settings.bgFile and string.find(settings.bgFile, "%.blp$") then
|
|
settings.bgFile = string.gsub(settings.bgFile, "%.blp$", "")
|
|
end
|
|
backdrop:SetBackdrop(settings)
|
|
end
|
|
|
|
-- Set backdrop color with safe defaults for empty/missing color tables
|
|
local c = SexyMap2DB.backdrop.textureColor
|
|
if c and (c.r or c.g or c.b or c.a) then
|
|
backdrop:SetBackdropColor(c.r or 0, c.g or 0, c.b or 0, c.a or 1)
|
|
else
|
|
-- Default: black background, full opacity
|
|
backdrop:SetBackdropColor(0, 0, 0, 1)
|
|
end
|
|
|
|
-- Set border color with safe defaults for empty/missing color tables
|
|
c = SexyMap2DB.backdrop.borderColor
|
|
if c and (c.r or c.g or c.b or c.a) then
|
|
backdrop:SetBackdropBorderColor(c.r or 1, c.g or 1, c.b or 1, c.a or 1)
|
|
else
|
|
-- Default: white border, full opacity
|
|
backdrop:SetBackdropBorderColor(1, 1, 1, 1)
|
|
end
|
|
end
|
|
end
|