--------------------------------------------------------------------------- local L = AceLibrary("AceLocale-2.2"):new("Aloft") local SML = AceLibrary("SharedMedia-1.0") ----------------------------------------------------------------------------- L:RegisterTranslations("enUS", function() return { ["Aloft"] = true, ["Show Nameplates"] = true, ["Show nameplates for neutral or hostile units"] = true, ["Show Friendly Nameplates"] = true, ["Show nameplates for friendly units"] = true, ["Data Options"] = true, ["Data options"] = true, ["Load Options"] = true, ["Load options for Aloft modules"] = true, ["Unable to determine module providing data: "] = true, ["Internal error: Dependency list not resolved - cyclic dependency?"] = true, } end) L:RegisterTranslations("koKR", function() return { ["Aloft"] = "Aloft", ["Show Nameplates"] = "이름표 표시", ["Show nameplates for neutral or hostile units"] = "중립적 혹은 적대적 대상에 대한 이름표를 표시합니다.", ["Show Friendly Nameplates"] = "우호적 이름표 표시", ["Show nameplates for friendly units"] = "우호적 대상에 대한 이름표를 표시합니다.", ["Data Options"] = "데이터 설정", ["Data options"] = "데이터에 대한 설정입니다.", ["Load Options"] = "불러오기 설정", ["Load options for Aloft modules"] = "Aloft 모듈들에 대한 불러오기 설정입니다.", ["Unable to determine module providing data: "] = "데이터를 위한 모듈을 결정할 수 없습니다: ", ["Internal error: Dependency list not resolved - cyclic dependency?"] = "내부적 오류: 목록 의존성이 해결되지 않았습니다 - 순환적인 의존성?", } end) L:RegisterTranslations("zhTW", function() return { ["Aloft"] = "Aloft", ["Show Nameplates"] = "顯示名牌", ["Show nameplates for neutral or hostile units"] = "顯示中立或敵對單位的名牌", ["Show Friendly Nameplates"] = "顯示友好名牌", ["Show nameplates for friendly units"] = "顯示友好單位的名牌", ["Data Options"] = "資料選項", ["Data options"] = "資料選項", ["Load Options"] = "載入選項", ["Load options for Aloft modules"] = "Aloft 模組的載入選項", ["Unable to determine module providing data: "] = "不能測定模組。資料: ", ["Internal error: Dependency list not resolved - cyclic dependency?"] = "內部錯誤: 循環倚靠?", } end) L:RegisterTranslations("zhCN", function() return { ["Aloft"] = "Aloft", ["Show Nameplates"] = "显示姓名板", ["Show nameplates for neutral or hostile units"] = "显示中立方或敌对方单位的姓名板", ["Show Friendly Nameplates"] = "显示友好", ["Show nameplates for friendly units"] = "显示友好方单位的姓名板", ["Data Options"] = "信息设置", ["Data options"] = "信息设置", ["Load Options"] = "读入设置", ["Load options for Aloft modules"] = "Aloft模块的加载设置", ["Unable to determine module providing data: "] = "不能测定模块的资料", ["Internal error: Dependency list not resolved - cyclic dependency?"] = "内部错误:无法确定需求设置,需求循环?", } end) ----------------------------------------------------------------------------- Aloft = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceConsole-2.0", "AceDB-2.0", "AceModuleCore-2.0", "AceHook-2.1") Aloft.version = "r"..("$Revision: 45867 $"):match("%d+") Aloft.Debug = (UnitName("player") == "Roartindon") and function(_, text) ChatFrame3:AddMessage(text) end or function() end Aloft:RegisterDB("AloftDB") ----------------------------------------------------------------------------- Aloft.Options = { handler = Aloft, type = 'group', args = { show = { type = 'toggle', name = L["Show Nameplates"], desc = L["Show nameplates for neutral or hostile units"], get = function() return NAMEPLATES_ON end, set = function(v) if v then ShowNameplates() else HideNameplates() end NAMEPLATES_ON = v end, order = 1 }, showFriendly = { type = 'toggle', name = L["Show Friendly Nameplates"], desc = L["Show nameplates for friendly units"], get = function() return FRIENDNAMEPLATES_ON end, set = function(v) if v then ShowFriendNameplates() else HideFriendNameplates() end FRIENDNAMEPLATES_ON = v end, order = 2 }, moduleSpacer = { type = 'header', order = 4, }, dataSpacer = { type = 'header', order = 500, }, data = { type = 'group', name = L["Data Options"], desc = L["Data options"], order = 600, disabled = function() return not next(Aloft.Options.args.data.args) end, args = { }, } }, } Aloft:RegisterChatCommand("/aloft", Aloft.Options) ----------------------------------------------------------------------------- Aloft.DataAvailableMethods = { } ----------------------------------------------------------------------------- local numberOfWorldChildren = 0 local nameplateList = { } local noTargetNameplate = false local nameToNameplateMap = { } local visibleNameplateList = { } local dataSourceList = { } -- Since pairs and select are used often and in loops, make them upvalues rather than a global lookup local pairs, select = pairs, select ----------------------------------------------------------------------------- local function UpdateAloftData(aloftData) for i = 1, #dataSourceList do dataSourceList[i]:UpdateData(aloftData) end end local function UpdateAloftTypeData(aloftData, healthBar) aloftData.originalHealthBarR, aloftData.originalHealthBarG, aloftData.originalHealthBarB, a = healthBar:GetStatusBarColor() local r, g, b = floor(aloftData.originalHealthBarR * 256), floor(aloftData.originalHealthBarG*256), floor(aloftData.originalHealthBarB*256) if r == 255 and g == 0 and b == 0 then aloftData.type = "hostile" elseif r == 0 and g == 0 and b == 255 then aloftData.type = "friendlyPlayer" elseif r == 0 and g == 255 and b == 0 then aloftData.type = "friendlyNPC" elseif r == 255 and g == 255 and b == 0 then aloftData.type = "neutral" else -- Aloft:Debug("rgb = "..r.."."..g.."."..b) end end ----------------------------------------------------------------------------- function Aloft:LoadOptions() if Aloft.Options.args.loadOptions then for i = 1,GetNumAddOns() do if GetAddOnMetadata(i, "X-Aloft-Options") then LoadAddOn(i) end end Aloft.Options.args.loadOptions = nil self:TriggerEvent("Aloft:LoadOptions") collectgarbage('collect') end end ----------------------------------------------------------------------------- function Aloft:OnInitialize() for i = 1,GetNumAddOns() do if GetAddOnMetadata(i, "X-Aloft-Options") then Aloft.Options.args.loadOptions = { type = 'execute', name = L["Load Options"], desc = L["Load options for Aloft modules"], func = function() Aloft:LoadOptions() end, order = 5 } break end end end function Aloft:OnEnable() for _, module in self:IterateModules() do self:ToggleModuleActive(module, true) end self:ScheduleRepeatingEvent("AloftUpdate", self.Update, 0.2, self) self:RegisterEvent("PLAYER_TARGET_CHANGED", function() noTargetNameplate = false end) self:DetermineDataSources() -- Reinstate hooks and update data - this is for when Aloft is disbaled, and later reenabled for aloftData in self:IterateNameplates() do self:HookNameplate(aloftData) if aloftData:IsShown() then visibleNameplateList[aloftData] = true self:OnNameplateShow(aloftData) end end end function Aloft:OnDisable() self:CancelScheduledEvent("AloftUpdate") for aloftData in self:IterateVisibleNameplates() do self:TriggerEvent("Aloft:OnNameplateHide", aloftData) end for _, module in self:IterateModules() do self:ToggleModuleActive(module, false) module:DisableDataSource() end self:RedisplayNameplates() self:RedisplayFriendlyNameplates() end function Aloft:OnProfileEnable() for aloftData in self:IterateVisibleNameplates() do self:TriggerEvent("Aloft:OnNameplateHide", aloftData) end for _, module in self:IterateModules() do if module.OnInitialize then module:OnInitialize() end end for _, module in self:IterateModules() do if module.OnEnable then module:OnEnable() end end self:DetermineDataSources() for aloftData in self:IterateVisibleNameplates() do UpdateAloftData(aloftData) self:TriggerEvent("Aloft:OnNameplateShow", aloftData) end end ----------------------------------------------------------------------------- local function IsNameplateFrame(frame) if frame:GetName() then return false end local overlayRegion = frame:GetRegions() if not overlayRegion or overlayRegion:GetObjectType() ~= "Texture" or overlayRegion:GetTexture() ~= "Interface\\Tooltips\\Nameplate-Border" then return false end return true end function Aloft:ProcessChildFrames(...) for i = 1, select('#', ...) do local frame = select(i, ...) if not frame.aloftData and IsNameplateFrame(frame) then self:SetupNameplate(frame) end end end function Aloft:Update() local newNumberOfWorldChildren = WorldFrame:GetNumChildren() if newNumberOfWorldChildren ~= numberOfWorldChildren then numberOfWorldChildren = newNumberOfWorldChildren self:ProcessChildFrames(WorldFrame:GetChildren()) end for aloftData in pairs(visibleNameplateList) do local healthBar = aloftData.healthBar local r, g, b, a = healthBar:GetStatusBarColor() -- An awful awful update routine to stop the game overwriting our colors sporadically if r ~= aloftData.healthBarR or g ~= aloftData.healthBarG or b ~= aloftData.healthBarB or a ~= aloftData.healthBarA then UpdateAloftTypeData(aloftData, healthBar) self:TriggerEvent("Aloft:OnHealthBarColorChanged", aloftData) end end end ----------------------------------------------------------------------------- function Aloft:IterateNameplates() return pairs(nameplateList) end function Aloft:IterateVisibleNameplates() return pairs(visibleNameplateList) end ----------------------------------------------------------------------------- local dataNameToModuleMap = { } local moduleToModuleListMap = { } local unavailableModuleList = { } function Aloft:UpdateDataNameToModuleMap(module, ...) if not ... then return end for i = 1,select('#', ...) do -- Aloft:Debug(string.format("Added data module: %s (%s)", tostring(select(i, ...)), tostring(module))) dataNameToModuleMap[select(i, ...)] = module end if not module:IsDataAvailable() then -- Aloft:Debug(string.format("Flagging %s as unavailable\n", tostring(module))) unavailableModuleList[module] = true end end function Aloft:UpdateModuleToModuleListMap(module, ...) if not ... then return end local moduleList = { } moduleToModuleListMap[module] = moduleList for i = 1,select('#', ...) do local dataName = select(i, ...) local requiredModule = dataNameToModuleMap[dataName] -- Aloft:Debug(string.format("%s requires %s", tostring(module), dataName)) if requiredModule then moduleList[requiredModule] = true else self:Print(L["Unable to determine module providing data: "]..dataName) end end end function Aloft:RemoveRequiredModuleFromModuleToModuleListMap(requiredModule) for module,moduleList in pairs(moduleToModuleListMap) do moduleList[requiredModule] = nil end end function Aloft:DetermineDataSources() -- 1. Iterate all modules, flag them to be disabled and gather provided data for _,module in self:IterateModules() do module.needToDisable = true self:UpdateDataNameToModuleMap(module, module:ProvidesData()) end -- Iterate all of the modules for data that they require, resolve them to the modules that provide them for name,module in self:IterateModules() do self:UpdateModuleToModuleListMap(module, module:RequiresData()) end -- Clear our old data sources for i = 1, #dataSourceList do dataSourceList[i] = nil end -- Iterate to remove purely data modules local iterationCount = 0 -- A safety factor local requiredModules = { } while iterationCount < 100 and next(moduleToModuleListMap) do for module, moduleList in pairs(moduleToModuleListMap) do for requiredModule in pairs(moduleList) do requiredModules[requiredModule] = true end end local moduleRemoved = false for module, _ in pairs(moduleToModuleListMap) do if module:IsPurelyData() and not requiredModules[module] then -- Aloft:Debug("Not required: "..tostring(module)) moduleToModuleListMap[module] = nil self:RemoveRequiredModuleFromModuleToModuleListMap(module) moduleRemoved = true end end if not moduleRemoved then break end for k in pairs(requiredModules) do requiredModules[k] = nil end iterationCount = iterationCount + 1 end -- Iterate our moduleToModuleListMap iterationCount = 0 -- A safety factor while iterationCount < 100 and next(moduleToModuleListMap) do for module, moduleList in pairs(moduleToModuleListMap) do for requiredModule in pairs(moduleList) do if unavailableModuleList[requiredModule] then -- Aloft:Debug(string.format("%s is unavailable. Flagging %s as unavailable", tostring(requiredModule), tostring(module))) unavailableModuleList[module] = true moduleList[requiredModule] = nil elseif not moduleToModuleListMap[requiredModule] then -- The required module isn't dependent on anything! Put it into our dataSourceList and remove from all modules that request it table.insert(dataSourceList, requiredModule) requiredModule:EnableDataSource() requiredModule.needToDisable = nil -- Aloft:Debug(#dataSourceList..": "..tostring(requiredModule)) self:RemoveRequiredModuleFromModuleToModuleListMap(requiredModule) end end if not next(moduleList) or unavailableModuleList[module] then moduleToModuleListMap[module] = nil end end iterationCount = iterationCount + 1 end if next(moduleToModuleListMap) then self:Print(L["Internal error: Dependency list not resolved - cyclic dependency?"]) for k in pairs(moduleToModuleListMap) do moduleToModuleListMap[k] = nil end end -- Empty our maps for k in pairs(dataNameToModuleMap) do dataNameToModuleMap[k] = nil end for k in pairs(unavailableModuleList) do unavailableModuleList[k] = nil end for _,module in self:IterateModules() do if module.needToDisable then module:DisableDataSource() end end for aloftData in self:IterateVisibleNameplates() do UpdateAloftData(aloftData) end end ----------------------------------------------------------------------------- function Aloft:GetTargetNameplate() if noTargetNameplate then return end -- Do quick lookup first local unitName = UnitName("target") if not unitName then return end local aloftData = nameToNameplateMap[unitName] if aloftData and aloftData.nameplateFrame:GetAlpha() == 1 then return aloftData end -- Do nameplate search for aloftData in pairs(visibleNameplateList) do if aloftData.nameplateFrame:GetAlpha() == 1 then -- And store it as our future reference nameToNameplateMap[unitName] = aloftData return aloftData end end noTargetNameplate = true return nil end function Aloft:GetNameplate(unitName) return nameToNameplateMap[unitName] end function Aloft:GetUnitNameplate(unitid) local unitName = UnitName(unitid) if unitName then return self:GetNameplate(unitName) end end ----------------------------------------------------------------------------- function Aloft:OnHealthBarValueChanged(aloftData, value, maxValue) if aloftData.nameplateFrame:IsShown() then aloftData.healthBarValue = value aloftData.healthBarMaxValue = maxValue self:TriggerEvent("Aloft:OnHealthBarValueChanged", aloftData) end end function Aloft:OnNameplateShow(aloftData) -- When a nameplate is first shown, grab some data about it local nameTextRegion = aloftData.nameTextRegion local newNameText = self.hooks[nameTextRegion].GetText(nameTextRegion) if aloftData.nameTextRegionText ~= newNameText then nameToNameplateMap[aloftData.name] = nil aloftData.name = newNameText nameToNameplateMap[aloftData.name] = aloftData aloftData.nameTextRegionText = name end local healthBar = aloftData.healthBar aloftData.healthBarValue = healthBar:GetValue() _, aloftData.healthBarMaxValue = healthBar:GetMinMaxValues() aloftData.isBoss = aloftData.bossIconRegion:IsShown() local levelTextRegion = aloftData.levelTextRegion if levelTextRegion:IsShown() then aloftData.level = tonumber(levelTextRegion:GetText()) or nil aloftData.levelTextR, aloftData.levelTextG, aloftData.levelTextB = levelTextRegion:GetTextColor() else aloftData.level = nil aloftData.levelTextR, aloftData.levelTextG, aloftData.levelTextB = 1, 1, 1 end UpdateAloftTypeData(aloftData, healthBar) UpdateAloftData(aloftData) self:TriggerEvent("Aloft:OnNameplateShow", aloftData) end function Aloft:OnNameplateHide(aloftData) self:TriggerEvent("Aloft:OnNameplateHide", aloftData) nameToNameplateMap[aloftData.name] = nil aloftData.name = "[invalid]" aloftData.nameTextRegionText = nil aloftData.level = nil aloftData.type = "unknown" end function Aloft:OnCastBarValueChanged(aloftData, castBar) if castBar:IsShown() then self:TriggerEvent("Aloft:OnCastBarValueChanged", aloftData) end end function Aloft:OnCastBarShow(aloftData, castBar) self:TriggerEvent("Aloft:OnCastBarShow", aloftData) end function Aloft:OnCastBarHide(aloftData) self:TriggerEvent("Aloft:OnCastBarHide", aloftData) end ----------------------------------------------------------------------------- function Aloft:OnNameplateShowScript(this) local aloftData = this.aloftData noTargetNameplate = false visibleNameplateList[aloftData] = true self:OnNameplateShow(aloftData) self.hooks[this]["OnShow"](this) end function Aloft:OnNameplateHideScript(this) local aloftData = this.aloftData self:OnNameplateHide(aloftData) visibleNameplateList[aloftData] = nil self.hooks[this]["OnHide"](this) end function Aloft:OnHealthBarValueChangedScript(this, value) local _, maxValue = this:GetMinMaxValues() self:OnHealthBarValueChanged(this.aloftData, value, maxValue) self.hooks[this]["OnValueChanged"](this, value) end function Aloft:OnCastBarValueChangedScript(this, value) self:OnCastBarValueChanged(this.aloftData, this) self.hooks[this]["OnValueChanged"](this, value) end function Aloft:OnCastBarShowScript(this) self:OnCastBarShow(this.aloftData, this) self.hooks[this]["OnShow"](this) end function Aloft:OnCastBarHideScript(this) self:OnCastBarHide(this.aloftData) self.hooks[this]["OnHide"](this) end ----------------------------------------------------------------------------- function Aloft:SetHealthBarColor(this, r, g, b, a) local aloftData = this.aloftData self.hooks[this].SetStatusBarColor(this, r, g, b, a) -- We read it back, because rounding seems to kick in and prevent them from being exactly what we sent.. aloftData.healthBarR, aloftData.healthBarG, aloftData.healthBarB, aloftData.healthBarA = this:GetStatusBarColor() end function Aloft:SetNameTextRegionText(this, text) local aloftData = this.aloftData aloftData.nameTextRegionText = text self.hooks[this].SetText(this, text) end function Aloft:GetLevelTextFrameText(this) local level = this.aloftData.level if level then return level end return self.hooks[this].GetText(this) end function Aloft:GetNameTextRegionText(this) local name = this.aloftData.name if name ~= "[invalid]" then return name end return self.hooks[this].GetText(this) end ----------------------------------------------------------------------------- local function AloftDataIsTarget(aloftData) return aloftData.nameplateFrame:GetAlpha() == 1 and aloftData.name == UnitName("target") end local function AloftDataIsShown(aloftData) return aloftData.nameplateFrame:IsShown() end local function AloftDataCreateFontString(aloftData) return aloftData.nameplateFrame:CreateFontString(nil, "OVERLAY") end local function AloftDataCreateTexture(aloftData) return aloftData.nameplateFrame:CreateTexture(nil, "OVERLAY") end function Aloft:SetupNameplate(frame) local aloftData = { type = "unknown", name = "[invalid]", IsTarget = AloftDataIsTarget, IsShown = AloftDataIsShown, CreateFontString = AloftDataCreateFontString, CreateTexture = AloftDataCreateTexture } frame.aloftData = aloftData noTargetNameplate = false nameplateList[aloftData] = true visibleNameplateList[aloftData] = true local overlayRegion, castBarOverlayRegion, spellIconRegion, highlightRegion, nameTextRegion, levelTextRegion, bossIconRegion, raidIconRegion = frame:GetRegions() local healthBar, castBar = frame:GetChildren() -- Reference all of these now aloftData.healthBar = healthBar aloftData.castBar = castBar aloftData.overlayRegion = overlayRegion aloftData.castBarOverlayRegion = castBarOverlayRegion aloftData.spellIconRegion = spellIconRegion aloftData.highlightRegion = highlightRegion aloftData.nameTextRegion = nameTextRegion aloftData.levelTextRegion = levelTextRegion aloftData.bossIconRegion = bossIconRegion aloftData.raidIconRegion = raidIconRegion aloftData.nameplateFrame = frame healthBar.aloftData = aloftData castBar.aloftData = aloftData nameTextRegion.aloftData = aloftData levelTextRegion.aloftData = aloftData self:HookNameplate(aloftData) self:TriggerEvent("Aloft:SetupFrame", aloftData) self:OnNameplateShow(aloftData) if castBar:IsShown() then self:OnCastBarShow(aloftData, castBar) end end function Aloft:HookNameplate(aloftData) local nameplateFrame = aloftData.nameplateFrame self:HookScript(nameplateFrame, "OnShow", "OnNameplateShowScript") self:HookScript(nameplateFrame, "OnHide", "OnNameplateHideScript") local healthBar = aloftData.healthBar self:HookScript(healthBar, "OnValueChanged", "OnHealthBarValueChangedScript") self:Hook(healthBar, "SetStatusBarColor", "SetHealthBarColor", true) local castBar = aloftData.castBar self:HookScript(castBar, "OnShow", "OnCastBarShowScript") self:HookScript(castBar, "OnHide", "OnCastBarHideScript") self:HookScript(castBar, "OnValueChanged", "OnCastBarValueChangedScript") local nameTextRegion = aloftData.nameTextRegion self:Hook(nameTextRegion, "SetText", "SetNameTextRegionText", true) self:Hook(nameTextRegion, "GetText", "GetNameTextRegionText", true) local levelTextRegion = aloftData.levelTextRegion self:Hook(levelTextRegion, "GetText", "GetLevelTextFrameText", true) end ----------------------------------------------------------------------------- function Aloft.modulePrototype:PrepareText(textFrame, data) textFrame:SetFont(SML:Fetch("font", data.font), data.fontSize, data.outline) if data.shadow then textFrame:SetShadowOffset(1, -1) else textFrame:SetShadowOffset(0, 0) end if data.alignment then textFrame:SetJustifyH(data.alignment) end if data.color then textFrame:SetTextColor(unpack(data.color)) end end function Aloft.modulePrototype:PlaceFrame(frame, relativeFrame, data) if data.enable then frame:ClearAllPoints() frame:SetPoint(data.point, relativeFrame, data.relativeToPoint, data.offsetX, data.offsetY) if data.size then frame:SetWidth(data.size) frame:SetHeight(data.size) else if data.width then frame:SetWidth(data.width) end if data.height then frame:SetWidth(data.width) end end frame:SetAlpha(data.alpha or 1.0) else frame:SetAlpha(0) end end ----------------------------------------------------------------------------- -- Provide these functions mainly for Visibiltiy module. ReportStatusChanges can hook them so that the log doesn't fill up function Aloft:RedisplayNameplates() HideNameplates() self:ScheduleEvent("AloftRedisplayNameplates", ShowNameplates, 0.1) end function Aloft:RedisplayFriendlyNameplates() HideFriendNameplates() self:ScheduleEvent("AloftRedisplayFriendNameplates", ShowFriendNameplates, 0.1) end ----------------------------------------------------------------------------- function Aloft.modulePrototype:RequiresData() -- eg. return "class", "isPetName", "isGuildMember" end function Aloft.modulePrototype:ProvidesData() end function Aloft.modulePrototype:EnableDataSource() end function Aloft:IsDataAvailable(name) local dataAvailableMethod = Aloft.DataAvailableMethods[name] if dataAvailableMethod then -- Aloft:Debug(string.format("%s: %s", name, dataAvailableMethod() and "true" or "false")) return dataAvailableMethod() end return true end function Aloft.modulePrototype:IsPurelyData() return self:ProvidesData() ~= nil end function Aloft.modulePrototype:IsOneOfDataListAvailable(...) for i=1,select('#', ...) do if Aloft:IsDataAvailable(select(i, ...)) then return true end end return false end function Aloft.modulePrototype:IsDataAvailable() return self:IsOneOfDataListAvailable(self:ProvidesData()) end function Aloft.modulePrototype:RemoveAloftData(name) for aloftData in Aloft:IterateNameplates() do aloftData[name] = nil end end function Aloft.modulePrototype:RemoveProvidedData(...) for i=1,select('#', ...) do self:RemoveAloftData(select(i, ...)) end end function Aloft.modulePrototype:DisableDataSource() local dataName = self:ProvidesData() if dataName then self:UnregisterAllEvents() self:CancelAllScheduledEvents() if self.UnhookAll then self:UnhookAll() end self:RemoveProvidedData(self:ProvidesData()) end end function Aloft.modulePrototype:UpdateData(aloftData) DEFAULT_CHAT_FRAME:AddMessage("Internal error: DataSource's UpdateData not overriden.") end function Aloft.modulePrototype:CancelEvent(eventName) if self:IsEventRegistered(eventName) then self:UnregisterEvent(eventName) end end function Aloft.modulePrototype:IsArrayEqual(a, b) if #a ~= #b then return false end for i=1,#a do if a[i] ~= b[i] then return false end end return true end -----------------------------------------------------------------------------