--[[ LibDualSpec-1.0 - Adds dual spec support to individual AceDB-3.0 databases Copyright (C) 2009-2022 Adirelle All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Redistribution of a stand alone version is strictly prohibited without prior written authorization from the LibDualSpec project manager. * Neither the name of the LibDualSpec authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --]] -- Don't load unless we are Retail or Wrath Classic if WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE and WOW_PROJECT_ID ~= WOW_PROJECT_WRATH_CLASSIC then return end local MAJOR, MINOR = "LibDualSpec-1.0", 22 assert(LibStub, MAJOR.." requires LibStub") local lib, minor = LibStub:NewLibrary(MAJOR, MINOR) if not lib then return end -- ---------------------------------------------------------------------------- -- Library data -- ---------------------------------------------------------------------------- lib.eventFrame = lib.eventFrame or CreateFrame("Frame") lib.registry = lib.registry or {} lib.options = lib.options or {} lib.mixin = lib.mixin or {} lib.upgrades = lib.upgrades or {} lib.currentSpec = tonumber(lib.currentSpec) or 0 if minor and minor < 15 then lib.talentsLoaded, lib.talentGroup = nil, nil lib.specLoaded, lib.specGroup = nil, nil lib.eventFrame:UnregisterAllEvents() wipe(lib.options) end -- ---------------------------------------------------------------------------- -- Locals -- ---------------------------------------------------------------------------- local registry = lib.registry local options = lib.options local mixin = lib.mixin local upgrades = lib.upgrades -- "Externals" local AceDB3 = LibStub('AceDB-3.0', true) local AceDBOptions3 = LibStub('AceDBOptions-3.0', true) local AceConfigRegistry3 = LibStub('AceConfigRegistry-3.0', true) local isRetail = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE local numSpecs = 2 local specNames = {TALENT_SPEC_PRIMARY, TALENT_SPEC_SECONDARY} if isRetail then -- class id specialization functions don't require player data to be loaded local _, classId = UnitClassBase("player") numSpecs = GetNumSpecializationsForClassID(classId) for i = 1, numSpecs do local _, name = GetSpecializationInfoForClassID(classId, i) specNames[i] = name end end local GetSpecialization = isRetail and GetSpecialization or GetActiveTalentGroup local CanPlayerUseTalentSpecUI = isRetail and C_SpecializationInfo.CanPlayerUseTalentSpecUI or function() return true, HELPFRAME_CHARACTER_BULLET5 end -- ---------------------------------------------------------------------------- -- Localization -- ---------------------------------------------------------------------------- local L_ENABLED = "Enable spec profiles" local L_ENABLED_DESC = "When enabled, your profile will be set to the specified profile when you change specialization." local L_CURRENT = "%s - Active" do local locale = GetLocale() if locale == "deDE" then L_ENABLED = "Spezialisierungsprofile aktivieren" L_ENABLED_DESC = "Falls diese Option aktiviert ist, wird dein Profil auf das angegebene Profil gesetzt, wenn du die Spezialisierung wechselst." L_CURRENT = "%s - Aktiv" elseif locale == "esES" or locale == "esMX" then L_ENABLED = "Activar perfiles de especialización" L_ENABLED_DESC = "Cuando está habilitado, su perfil se establecerá en el perfil especificado cuando cambie de especialización." L_CURRENT = "%s - Activo" elseif locale == "frFR" then L_ENABLED = "Activer les profils de spécialisation" L_ENABLED_DESC = "Lorsque cette option est activée, votre profil sera défini sur le profil spécifié lorsque vous changerez de spécialisation." L_CURRENT = "%s - Actifs" elseif locale == "itIT" then L_ENABLED = "Abilita i profili per la specializzazione" L_ENABLED_DESC = "Quando abilitato, il tuo profilo verrà impostato in base alla specializzazione usata." L_CURRENT = "%s - Attivi" elseif locale == "koKR" then L_ENABLED = "전문화 프로필 활성화" L_ENABLED_DESC = "활성화하면 전문화를 변경할 때 프로필이 지정된 프로필로 설정됩니다." L_CURRENT = "%s - 활성화" elseif locale == "ptBR" then L_ENABLED = "Ativar perfis de especialização" L_ENABLED_DESC = "Quando ativado, seu perfil será definido para o perfil especificado quando você alterar a especialização." L_CURRENT = "%s – ativo" elseif locale == "ruRU" then L_ENABLED = "Включить профили специализации" L_ENABLED_DESC = "Если включено, ваш профиль будет зависеть от выбранной специализации." L_CURRENT = "%s - активен" elseif locale == "zhCN" then L_ENABLED = "启用专精配置文件" L_ENABLED_DESC = "当启用后,当切换专精时配置文件将设置为专精配置文件。" L_CURRENT = "%s - 开启" elseif locale == "zhTW" then L_ENABLED = "啟用專精設定檔" L_ENABLED_DESC = "當啟用後,當你切換專精時設定檔會設定為專精設定檔。" L_CURRENT = "%s - 啟動" end end -- ---------------------------------------------------------------------------- -- Mixin -- ---------------------------------------------------------------------------- --- Get dual spec feature status. -- @return (boolean) true is dual spec feature enabled. -- @name enhancedDB:IsDualSpecEnabled function mixin:IsDualSpecEnabled() return lib.currentSpec > 0 and registry[self].db.char.enabled end --- Enable/disabled dual spec feature. -- @param enabled (boolean) true to enable dual spec feature, false to disable it. -- @name enhancedDB:SetDualSpecEnabled function mixin:SetDualSpecEnabled(enabled) local db = registry[self].db.char db.enabled = not not enabled local currentProfile = self:GetCurrentProfile() for i = 1, numSpecs do -- nil out entries on disable, set nil entries to the current profile on enable db[i] = enabled and (db[i] or currentProfile) or nil end self:CheckDualSpecState() end --- Get the profile assigned to a specialization. -- Defaults to the current profile. -- @param spec (number) the specialization index. -- @return (string) the profile name. -- @name enhancedDB:GetDualSpecProfile function mixin:GetDualSpecProfile(spec) return registry[self].db.char[spec or lib.currentSpec] or self:GetCurrentProfile() end --- Set the profile assigned to a specialization. -- No validation are done to ensure the profile is valid. -- @param profileName (string) the profile name to use. -- @param spec (number) the specialization index. -- @name enhancedDB:SetDualSpecProfile function mixin:SetDualSpecProfile(profileName, spec) spec = spec or lib.currentSpec if spec < 1 or spec > numSpecs then return end registry[self].db.char[spec] = profileName self:CheckDualSpecState() end --- Check if a profile swap should occur. -- There is normally no reason to call this method directly as LibDualSpec -- takes care of calling it at the appropriate time. -- @name enhancedDB:CheckDualSpecState function mixin:CheckDualSpecState() if not registry[self].db.char.enabled then return end if lib.currentSpec == 0 then return end local profileName = self:GetDualSpecProfile() if profileName ~= self:GetCurrentProfile() then self:SetProfile(profileName) end end -- ---------------------------------------------------------------------------- -- AceDB-3.0 support -- ---------------------------------------------------------------------------- local function EmbedMixin(target) for k,v in next, mixin do rawset(target, k, v) end end -- Upgrade settings from current/alternate system. -- This sets the current profile as the profile for your current spec and your -- swapped profile as the profile for the rest of your specs. local function UpgradeDatabase(target) if lib.currentSpec == 0 then upgrades[target] = true return end local db = target:GetNamespace(MAJOR, true) if db and db.char.profile then for i = 1, numSpecs do if i == lib.currentSpec then db.char[i] = target:GetCurrentProfile() else db.char[i] = db.char.profile end end db.char.profile = nil db.char.specGroup = nil end end -- Reset a spec profile to the current one if its profile is deleted. function lib:OnProfileDeleted(event, target, profileName) local db = registry[target].db.char if not db.enabled then return end for i = 1, numSpecs do if db[i] == profileName then db[i] = target:GetCurrentProfile() end end end -- Actually enhance the database -- This is used on first initialization and everytime the database is reset using :ResetDB function lib:_EnhanceDatabase(event, target) registry[target].db = target:GetNamespace(MAJOR, true) or target:RegisterNamespace(MAJOR) EmbedMixin(target) target:CheckDualSpecState() end --- Embed dual spec feature into an existing AceDB-3.0 database. -- LibDualSpec specific methods are added to the instance. -- @name LibDualSpec:EnhanceDatabase -- @param target (table) the AceDB-3.0 instance. -- @param name (string) a user-friendly name of the database (best bet is the addon name). function lib:EnhanceDatabase(target, name) AceDB3 = AceDB3 or LibStub('AceDB-3.0', true) if type(target) ~= "table" then error("Usage: LibDualSpec:EnhanceDatabase(target, name): target should be a table.", 2) elseif type(name) ~= "string" then error("Usage: LibDualSpec:EnhanceDatabase(target, name): name should be a string.", 2) elseif not AceDB3 or not AceDB3.db_registry[target] then error("Usage: LibDualSpec:EnhanceDatabase(target, name): target should be an AceDB-3.0 database.", 2) elseif target.parent then error("Usage: LibDualSpec:EnhanceDatabase(target, name): cannot enhance a namespace.", 2) elseif registry[target] then return end registry[target] = { name = name } UpgradeDatabase(target) lib:_EnhanceDatabase("EnhanceDatabase", target) target.RegisterCallback(lib, "OnDatabaseReset", "_EnhanceDatabase") target.RegisterCallback(lib, "OnProfileDeleted") end -- ---------------------------------------------------------------------------- -- AceDBOptions-3.0 support -- ---------------------------------------------------------------------------- options.new = { name = "New", type = "input", order = 30, get = false, set = function(info, value) local db = info.handler.db if db:IsDualSpecEnabled() then db:SetDualSpecProfile(value, lib.currentSpec) else db:SetProfile(value) end end, } options.choose = { name = "Existing Profiles", type = "select", order = 40, get = "GetCurrentProfile", set = "SetProfile", values = "ListProfiles", arg = "common", disabled = function(info) return info.handler.db:IsDualSpecEnabled() end } options.enabled = { type = "toggle", name = "|cffffd200"..L_ENABLED.."|r", desc = function() local desc = L_ENABLED_DESC if lib.currentSpec == 0 then local _, reason = CanPlayerUseTalentSpecUI() if not reason or reason == "" then reason = TALENT_MICRO_BUTTON_NO_SPEC end desc = desc .. "\n\n" .. RED_FONT_COLOR:WrapTextInColorCode(reason) end return desc end, descStyle = "inline", order = 41, width = "full", get = function(info) return info.handler.db:IsDualSpecEnabled() end, set = function(info, value) info.handler.db:SetDualSpecEnabled(value) end, disabled = function() return lib.currentSpec == 0 end, } local points = {} for i = 1, numSpecs do options["specProfile" .. i] = { type = "select", name = function(info) local specIndex = tonumber(info[#info]:sub(-1)) return lib.currentSpec == specIndex and L_CURRENT:format(specNames[specIndex]) or specNames[specIndex] end, desc = not isRetail and function(info) local specIndex = tonumber(info[#info]:sub(-1)) local highPointsSpentIndex = nil for treeIndex = 1, 3 do local name, _, pointsSpent, _, previewPointsSpent = GetTalentTabInfo(treeIndex, false, false, specIndex) if name then local displayPointsSpent = pointsSpent + previewPointsSpent points[treeIndex] = displayPointsSpent if displayPointsSpent > 0 and (not highPointsSpentIndex or displayPointsSpent > points[highPointsSpentIndex]) then highPointsSpentIndex = treeIndex end else points[treeIndex] = 0 end end if highPointsSpentIndex then points[highPointsSpentIndex] = GREEN_FONT_COLOR:WrapTextInColorCode(points[highPointsSpentIndex]) end return ("|cffffffff%s / %s / %s|r"):format(unpack(points)) end or nil, order = 42 + i, get = function(info) local specIndex = tonumber(info[#info]:sub(-1)) return info.handler.db:GetDualSpecProfile(specIndex) end, set = function(info, value) local specIndex = tonumber(info[#info]:sub(-1)) info.handler.db:SetDualSpecProfile(value, specIndex) end, values = "ListProfiles", arg = "common", disabled = function(info) return not info.handler.db:IsDualSpecEnabled() end, } end --- Embed dual spec options into an existing AceDBOptions-3.0 option table. -- @name LibDualSpec:EnhanceOptions -- @param optionTable (table) The option table returned by AceDBOptions-3.0. -- @param target (table) The AceDB-3.0 the options operate on. function lib:EnhanceOptions(optionTable, target) AceDBOptions3 = AceDBOptions3 or LibStub('AceDBOptions-3.0', true) AceConfigRegistry3 = AceConfigRegistry3 or LibStub('AceConfigRegistry-3.0', true) if type(optionTable) ~= "table" then error("Usage: LibDualSpec:EnhanceOptions(optionTable, target): optionTable should be a table.", 2) elseif type(target) ~= "table" then error("Usage: LibDualSpec:EnhanceOptions(optionTable, target): target should be a table.", 2) elseif not AceDBOptions3 or not AceDBOptions3.optionTables[target] then error("Usage: LibDualSpec:EnhanceOptions(optionTable, target): optionTable is not an AceDBOptions-3.0 table.", 2) elseif optionTable.handler.db ~= target then error("Usage: LibDualSpec:EnhanceOptions(optionTable, target): optionTable must be the option table of target.", 2) elseif not registry[target] then error("Usage: LibDualSpec:EnhanceOptions(optionTable, target): EnhanceDatabase should be called before EnhanceOptions(optionTable, target).", 2) end -- localize our replacements options.new.name = optionTable.args.new.name options.new.desc = optionTable.args.new.desc options.choose.name = optionTable.args.choose.name options.choose.desc = optionTable.args.choose.desc -- add our new options if not optionTable.plugins then optionTable.plugins = {} end optionTable.plugins[MAJOR] = options end -- ---------------------------------------------------------------------------- -- Upgrade existing -- ---------------------------------------------------------------------------- for target in next, registry do UpgradeDatabase(target) EmbedMixin(target) target:CheckDualSpecState() local optionTable = AceDBOptions3 and AceDBOptions3.optionTables[target] if optionTable then lib:EnhanceOptions(optionTable, target) end end -- ---------------------------------------------------------------------------- -- Inspection -- ---------------------------------------------------------------------------- local function iterator(registry, key) local data key, data = next(registry, key) if key then return key, data.name end end --- Iterate through enhanced AceDB3.0 instances. -- The iterator returns (instance, name) pairs where instance and name are the -- arguments that were provided to lib:EnhanceDatabase. -- @name LibDualSpec:IterateDatabases -- @return Values to be used in a for .. in .. do statement. function lib:IterateDatabases() return iterator, lib.registry end -- ---------------------------------------------------------------------------- -- Switching logic -- ---------------------------------------------------------------------------- local function eventHandler(self, event) local spec = GetSpecialization() or 0 -- Newly created characters start at 5 instead of 1 in 9.0.1. if spec == 5 or not CanPlayerUseTalentSpecUI() then spec = 0 end lib.currentSpec = spec if event == "PLAYER_LOGIN" then self:UnregisterEvent(event) self:RegisterEvent("PLAYER_ENTERING_WORLD") if isRetail then self:RegisterUnitEvent("PLAYER_SPECIALIZATION_CHANGED", "player") self:RegisterEvent("PLAYER_LEVEL_CHANGED") else self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED") end end if spec > 0 and next(upgrades) then for target in next, upgrades do UpgradeDatabase(target) end wipe(upgrades) end for target in next, registry do target:CheckDualSpecState() end if AceConfigRegistry3 and next(registry) then -- Update the "Current" text in options -- We don't get the key for the actual registered options table, and we can't -- really check for our enhanced options without walking every options table, -- so just refresh anything. for appName in AceConfigRegistry3:IterateOptionsTables() do AceConfigRegistry3:NotifyChange(appName) end end end lib.eventFrame:SetScript("OnEvent", eventHandler) if IsLoggedIn() then eventHandler(lib.eventFrame, "PLAYER_LOGIN") else lib.eventFrame:RegisterEvent("PLAYER_LOGIN") end