-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Settings = TSM.Init("Service.Settings") local L = TSM.Include("Locale").GetTable() local TempTable = TSM.Include("Util.TempTable") local Table = TSM.Include("Util.Table") local Math = TSM.Include("Util.Math") local String = TSM.Include("Util.String") local Log = TSM.Include("Util.Log") local Sound = TSM.Include("Util.Sound") local CSV = TSM.Include("Util.CSV") local Wow = TSM.Include("Util.Wow") local private = { context = {}, proxies = {}, views = {}, protectedAccessAllowed = {}, cachedConnectedRealms = nil, upgradeContext = nil, svCopyErrorTime = 0, db = nil, } local LibRealmInfo = LibStub("LibRealmInfo") local KEY_SEP = "@" local SCOPE_KEY_SEP = " - " local GLOBAL_SCOPE_KEY = " " local DEFAULT_PROFILE_NAME = "Default" local PLAYER = UnitName("player") local FACTION = UnitFactionGroup("player") local REALM = GetRealmName() local VALID_TYPES = { boolean = true, string = true, table = true, number = true, } local SCOPE_TYPES = { global = "g", profile = "p", realm = "r", factionrealm = "f", char = "c", sync = "s", } local SCOPE_KEYS = { global = " ", profile = nil, -- set per-DB realm = REALM, factionrealm = strjoin(SCOPE_KEY_SEP, FACTION, REALM), char = strjoin(SCOPE_KEY_SEP, PLAYER, REALM), sync = strjoin(SCOPE_KEY_SEP, PLAYER, FACTION, REALM), } local DEFAULT_DB = { _version = -math.huge, -- DB version _currentProfile = {}, -- lookup table of the current profile name by character _syncAccountKey = {}, -- lookup table of the sync account key by factionrealm _syncOwner = {}, -- lookup table of the owner sync account key by character _hash = 0, _scopeKeys = { profile = {}, realm = {}, factionrealm = {}, char = {}, sync = {}, }, _lastModifiedVersion = {}, } -- Changelog: -- [6] added 'global.locale' key -- [7] changed default value of 'tsmItemTweetEnabled' to false -- [8] added 'global.itemCacheVersion' key -- [9] removed 'global.itemCacheVersion' key, added 'global.clientVersion' key -- [10] first TSM4 version - combined all module settings into a single DB -- [11] added profile.internalData.createdDefaultOperations -- [12] added global.shoppingOptions.pctSource -- [13] added profile.internalData.{managementGroupTreeContext,auctioningGroupTreeContext,shoppingGroupTreeContext} -- [14] added global.userData.savedAuctioningSearches -- [15] added global.coreOptions.bankUITab, profile.coreOptions.{bankUIBankFramePosition,bankUIGBankFramePosition} -- [16] moved profile.coreOptions.{bankUIBankFramePosition,bankUIGBankFramePosition} to profile.internalData.{bankUIBankFramePosition,bankUIGBankFramePosition} -- [17] added global.internalData.{mainUIFrameContext,auctionUIFrameContext,craftingUIFrameContext} -- [18] removed global.internalData.itemStringLookup -- [19] added sync scope (initially with internalData.{classKey,bagQuantity,bankQuantity,reagentBankQuantity,auctionQuantity,mailQuantity}), removed factionrealm.internalData.{syncMetadata,accountKey,inventory,characters} and factionrealm.coreOptions.syncAccounts, added global.debug.chatLoggingEnabled -- [20] added global.tooltipOptions.enabled -- [21] added global.craftingOptions.{profitPercent,questSmartCrafting,queueSort} -- [22] added global.coreOptions.cleanGuildBank -- [23] changed global.shoppingOptions.maxDeSearchPercent default to 100 -- [24] added global.auctioningOptions.{showAuctionDBTab,openAllBags,ahRowDisplay} -- [25] split realm.internalData.goldLog into sync.internalData.goldLog and factionrealm.internalData.guildGoldLog -- [26] added profile.internalData.{shoppingTabGroupContext,auctioningTabGroupContext} -- [27] added char.internalData.craftingCooldowns -- [28] added global.internalData.mailingUIFrameContext -- [29] added global.internalData.vendoringUIFrameContext -- [30] added global.internalData.bankingUIFrameContext -- [31] changed global.internalData.bankingUIFrameContext default (isOpen = true), added profile.internalData.{bankingWarehousingGroupTreeContext,bankingAuctioningGroupTreeContext,bankingMailingGroupTreeContext} -- [32] removed factionrealm.internalData.gathering, added factionrealm.internalData.gatheringContext.{crafter,professions}, added profile.gatheringOptions.sources -- [33] added global.internalData.taskListUIFrameContext -- [34] removed realm.internalData.{lastAuctionDBCompleteScan,lastAuctionDBSaveTime,auctionDBScanData} -- [35] added factionrealm.userData.craftingCooldownIgnore -- [36] removed factionrealm.internalData.playerProfessions and added sync.internalData.playerProfessions -- [37] removed global.auctioningOptions.showAuctionDBTab -- [38] removed global.mailingOptions.{defaultMailTab,autoCheck,displayMoneyCollected,deleteEmptyNPCMail,showReloadBtn,sendDelay,defaultPage}, added global.mailingOptions.recentlyMailedList -- [39] added profile.internalData.{craftingGroupTreeContext,mailingGroupTreeContext,vendoringGroupTreeContext,importGroupTreeContext} -- [40] removed global.accountingOptions.{timeFormat,mvSource} -- [41] removed global.coreOptions.groupPriceSource -- [42] removed global.vendoringOptions.defaultMerchantTab -- [43] removed global.coreOptions.{moveDelay,bankUITab}, removed global.auctioningOptions.{openAllBags,ahRowDisplay}, removed global.craftingOptions.{profitPercent,questSmartCrafting,queueSort}, removed global.destroyingOptions.{logDays,timeFormat}, removed global.vendoringOptions.{autoSellTrash,qsHideGrouped,qsHideSoulbound,qsBatchSize,defaultPage,qsMaxMarketValue,qsDestroyValue}, removed profile.coreOptions.{cleanBags,cleanBank,cleanReagentBank,cleanGuildBank} -- [44] changed global.internalData.{mainUIFrameContext,auctionUIFrameContext,craftingUIFrameContext,destroyingUIFrameContext,mailingUIFrameContext,vendoringUIFrameContext,bankingUIFrameContext} default (added "scale = 1") -- [45] added char.internalData.auctionSaleHints -- [46] added global.shoppingOptions.{buyoutConfirm,buyoutAlertSource} -- [47] added factionrealm.internalData.expiringMail and factionrealm.internalData.expiringAuction -- [48] added profile.internalData.exportGroupTreeContext -- [49] added factionrealm.internalData.{mailDisenchantablesChar,mailExcessGoldChar,mailExcessGoldLimit} -- [50] added factionrealm.internalData.{csvAuctionDBScan,auctionDBScanTime,auctionDBScanHash} -- [51-53] resetting factionrealm.internalData.crafts -- [54] removed global.coreOptions.{tsmItemTweetEnabled,auctionSaleEnabled,auctionBuyEnabled} -- [55] added global.auctionUIContext.{auctioningSelectionDividedContainer,auctioningBagScrollingTable,auctioningLogScrollingTable,auctioningAuctionScrollingTable,myAuctionsScrollingTable,shoppingSelectionDividedContainer,shoppingAuctionScrollingTable,sniperScrollingTable,frame,showDefault,shoppingSearchesTabGroup} -- added global.bankingUIContext.{frame,isOpen,tab} -- added global.craftingUIContext.{craftsScrollingTable,matsScrollingTable,gatheringDividedContainer,gatheringScrollingTable,professionScrollingTable,frame,showDefault,professionDividedContainer} -- added global.destroyingUIContext.itemsScrollingTable -- added global.mailingUIContext.{mailsScrollingTable,frame,showDefault} -- added global.mainUIContext.{ledgerDetailScrollingTable,ledgerInventoryScrollingTable,ledgerAuctionsScrollingTable,ledgerOtherScrollingTable,ledgerTransactionsScrollingTable,ledgerResaleScrollingTable,frame,dashboardDividedContainer,groupsDividedContainer,operationsDividedContainer,importExportDividedContainer} -- added global.taskListUIContext.{frame,isOpen} -- added global.vendoringUIContext.{buyScrollingTable,buybackScrollingTable,sellScrollingTable,frame,showDefault} -- added profile.mainUIContext.{groupsManagementGroupTree,importGroupTree,exportGroupTree} -- added profile.auctionUIContext.{auctioningTabGroup,auctioningGroupTree,shoppingGroupTree} -- added profile.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree} -- added profile.craftingUIContext.groupTree -- added profile.mailingUIContext.groupTree -- added profile.vendoringUIContext.groupTree -- removed profile.internalData.{auctioningTabGroupContext,auctioningGroupTreeContext,managementGroupTreeContext,shoppingGroupTreeContext,importGroupTreeContext,exportGroupTreeContext,bankingUIFrameContext,craftingUIFrameContext,auctionUIFrameContext,mailingUIFrameContext,vendoringUIFrameContext,destroyingUIFrameContext,mainUIFrameContext,taskListUIFrameContext} -- [56] added factionrealm.internalData.isCraftFavorite -- [57] updated global.auctionUIContext.auctioningAuctionScrollingTable -- [58] updated global.auctionUIContext.sniperScrollingTable -- [59] updated global.mainUIContext.{frame,dashboardDividedContainer,ledgerDetailScrollingTable,ledgerInventoryScrollingTable,ledgerAuctionsScrollingTable,ledgerOtherScrollingTable,ledgerTransactionsScrollingTable,ledgerResaleScrollingTable} -- [60] updated global.auctionUIContext.{auctioningAuctionScrollingTable,shoppingAuctionScrollingTable,sniperScrollingTable}, global.craftingUIContext.professionScrollingTable -- [61] updated global.auctionUIContext.sniperScrollingTable -- [62] updated global.mainUIContext.{ledgerTransactionsScrollingTable,ledgerResaleScrollingTable} -- [63] removed global.auctioningOptions.roundNormalPrice -- [64] removed global.accountingOptions.smartBuyPrice -- [65] added global.appearanceOptions.colorSet -- [66] added global.auctionUIContext.auctioningSelectionVerticalDividedContainer -- [67] updated global.mailingUIContext.mailsScrollingTable -- [68] removed profile.internalData.{bankUIBankFramePosition,bankUIGBankFramePosition,shoppingTabGroupContext,bankingWarehousingGroupTreeContext,bankingAuctioningGroupTreeContext,bankingMailingGroupTreeContext,craftingGroupTreeContext,mailingGroupTreeContext,vendoringGroupTreeContext} -- [69] updated global.mainUIContext.ledgerInventoryScrollingTable -- [70] updated global.auctionUIContext.{auctioningAuctionScrollingTable,shoppingAuctionScrollingTable} -- [71] moved profile.auctionUIContext.{auctioningGroupTree,shoppingGroupTree},profile.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree},profile.craftingUIContext.groupTree,profile.mailingUIContext.groupTree,profile.mainUIContext.{groupsManagementGroupTree,importGroupTree,exportGroupTree} to char.* -- [72] updated global.auctionUIContext.sniperScrollingTable -- [73] added profile.vendoringUIContext.groupTree -- [74] added sync.internalData.money -- [75] updated global.appearanceOptions.colorSet -- [76] updated global.mainUIContext.operationsSummaryScrollingTable -- [77] added global.coreOptions.protectAuctionHouse -- [78] added global.mainUIContext.{dashboardUnselectedCharacters,dashboardTimeRange} -- [79] updated global.shoppingOptions.maxDeSearchLvl -- [80] updated char.auctionUIContext.{auctioningGroupTree,shoppingGroupTree},char.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree},char.craftingUIContext.groupTree,char.mailingUIContext.groupTree,char.vendoringUIContext.groupTree,char.mainUIContext.{importGroupTree,exportGroupTree} -- [81] updated global.mailingUIContext.mailsScrollingTable -- [82] updated global.craftingUIContext.professionScrollingTable -- [83] added sync.internalData.goldLogLastUpdate, factionrealm.internalData.guildGoldLogLastUpdate -- [84] added global.auctionUIContext.myAuctionsScrollingTable -- [85] removed global.craftingOptions.ignoreCDCraftCost -- [86] updated global.craftingUIContext.craftsScrollingTable -- [87] added global.craftingUIContext.craftsScrollingTable -- [88] added global.shoppingOptions.searchAutoFocus -- [89] updated global.craftingOptions.defaultCraftPriceMethod -- [90] added global.internalData.lastCharacter -- [91] updated global.craftingUIContext.professionScrollingTable -- [92] updated global.vendoringUIContext.buyScrollingTable -- [93] moved profile.auctionUIContext.auctioningTabGroup to global.auctionUIContext.auctioningTabGroup -- [94] added global.internalData.whatsNewVersion -- [95] added global.appearanceOptions.showTotalMoney -- [96] updated global.userData.{savedShoppingSearches,savedAuctioningSearches} -- [97] added global.internalData.{optionalMatBonusIdLookup,optionalMatTextLookup} local SETTINGS_INFO = { version = 97, global = { debug = { chatLoggingEnabled = { type = "boolean", default = false, lastModifiedVersion = 19 }, }, internalData = { lastCharacter = { type = "string", default = "???", lastModifiedVersion = 90 }, vendorItems = { type = "table", default = {}, lastModifiedVersion = 10 }, appMessageId = { type = "number", default = 0, lastModifiedVersion = 10 }, destroyingHistory = { type = "table", default = {}, lastModifiedVersion = 10 }, whatsNewVersion = { type = "number", default = 0, lastModifiedVersion = 94 }, optionalMatBonusIdLookup = { type = "table", default = {}, lastModifiedVersion = 97 }, optionalMatTextLookup = { type = "table", default = {}, lastModifiedVersion = 97 }, }, appearanceOptions = { taskListBackgroundLock = { type = "boolean", default = false, lastModifiedVersion = 87 }, showTotalMoney = { type = "boolean", default = false, lastModifiedVersion = 95 }, colorSet = { type = "string", default = "midnight", lastModifiedVersion = 75 }, }, auctionUIContext = { frame = { type = "table", default = { width = 830, height = 587, centerX = -300, centerY = 100, scale = 1, page = 1 }, lastModifiedVersion = 55 }, showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 }, auctioningSelectionDividedContainer = { type = "table", default = { leftWidth = 272 }, lastModifiedVersion = 55 }, auctioningSelectionVerticalDividedContainer = { type = "table", default = { leftWidth = 220 }, lastModifiedVersion = 66 }, auctioningBagScrollingTable = { type = "table", default = { colWidth = { selected = 16, item = 246, operation = 206 }, colHidden = {} }, lastModifiedVersion = 55 }, auctioningLogScrollingTable = { type = "table", default = { colWidth = { index = 14, item = 190, buyout = 110, operation = 108, seller = 90, info = 234 }, colHidden = {} }, lastModifiedVersion = 55 }, auctioningAuctionScrollingTable = { type = "table", default = { colWidth = { item = 226, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, timeLeft = 26, seller = TSM.IsWowClassic() and 88 or 136, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115, bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 70 }, myAuctionsScrollingTable = { type = "table", default = { colWidth = { item = 248, stackSize = 30, timeLeft = 40, highbidder = TSM.IsWowClassic() and 110 or nil, group = TSM.IsWowClassic() and 110 or 228, currentBid = 100, buyout = 100 }, colHidden = {} }, lastModifiedVersion = 84 }, shoppingSelectionDividedContainer = { type = "table", default = { leftWidth = 272 }, lastModifiedVersion = 55 }, shoppingAuctionScrollingTable = { type = "table", default = { colWidth = { item = 226, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, timeLeft = 26, seller = TSM.IsWowClassic() and 88 or 136, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115, bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 70 }, sniperScrollingTable = { type = "table", default = { colWidth = { icon = 24, item = 230, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, seller = TSM.IsWowClassic() and 86 or 134, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115, bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 72 }, shoppingSearchesTabGroup = { type = "table", default = { pathIndex = 1 }, lastModifiedVersion = 55 }, auctioningTabGroup = { type = "table", default = { pathIndex = 1 }, lastModifiedVersion = 93 }, }, bankingUIContext = { frame = { type = "table", default = { width = 325, height = 600, centerX = 500, centerY = 0, scale = 1 }, lastModifiedVersion = 55 }, isOpen = { type = "boolean", default = true, lastModifiedVersion = 55 }, tab = { type = "string", default = "Warehousing", lastModifiedVersion = 55 }, }, craftingUIContext = { frame = { type = "table", default = { width = 820, height = 587, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 }, showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 }, craftsScrollingTable = { type = "table", default = { colWidth = { queued = 30, craftName = 218, operation = 80, bags = 28, ah = 24, craftingCost = 100, itemValue = 100, profit = 100, profitPct = 50, saleRate = 32 }, colHidden = { profitPct = true } }, lastModifiedVersion = 86 }, matsScrollingTable = { type = "table", default = { colWidth = { name = 242, price = 100, professions = 310, num = 100 }, colHidden = {} }, lastModifiedVersion = 55 }, gatheringDividedContainer = { type = "table", default = { leftWidth = 284 }, lastModifiedVersion = 55 }, gatheringScrollingTable = { type = "table", default = { colWidth = { name = 206, sources = 160, have = 50, need = 50 }, colHidden = {} }, lastModifiedVersion = 55 }, professionScrollingTable = { type = "table", default = { colWidth = { name = not TSM.IsWowClassic() and 240 or 288, qty = 54, rank = not TSM.IsWowClassic() and 40 or nil, craftingCost = 100, itemValue = 100, profit = 100, profitPct = 50, saleRate = 30 }, colHidden = { craftingCost = true, itemValue = true, profitPct = true }, collapsed = {} }, lastModifiedVersion = 91 }, professionDividedContainer = { type = "table", default = { leftWidth = 520 }, lastModifiedVersion = 55 }, }, destroyingUIContext = { frame = { type = "table", default = { width = 296, height = 442, centerX = 0, centerY = 0, scale = 1 }, lastModifiedVersion = 55 }, itemsScrollingTable = { type = "table", default = { colWidth = { item = 214, num = 30 }, colHidden = {} }, lastModifiedVersion = 55 }, }, mailingUIContext = { frame = { type = "table", default = { width = 620, height = 516, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 }, showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 }, mailsScrollingTable = { type = "table", default = { colWidth = { items = 380, sender = 100, expires = 65, money = 115 }, colHidden = { sender = true } }, lastModifiedVersion = 81 }, }, mainUIContext = { frame = { type = "table", default = { width = 900, height = 700, centerX = 0, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 59 }, ledgerDetailScrollingTable = { type = "table", default = { colWidth = { activityType = 91, source = 60, buyerSeller = 100, qty = 45, perItem = 120, totalPrice = 120, time = 110 }, colHidden = {} }, lastModifiedVersion = 59 }, ledgerInventoryScrollingTable = { type = "table", default = { colWidth = { item = 160, totalItems = 50, bags = 50, banks = 50, mail = 50, alts = 50, guildVault = 50, auctionHouse = 50, totalValue = 120 }, colHidden = {} }, lastModifiedVersion = 69 }, ledgerAuctionsScrollingTable = { type = "table", default = { colWidth = { item = 305, player = 110, stackSize = 55, quantity = 72, time = 120 }, colHidden = {} }, lastModifiedVersion = 59 }, ledgerOtherScrollingTable = { type = "table", default = { colWidth = { type = 200, character = 110, otherCharacter = 122, amount = 120, time = 110 }, colHidden = {} }, lastModifiedVersion = 59 }, ledgerTransactionsScrollingTable = { type = "table", default = { colWidth = { item = 156, player = 95, type = 50, stack = 55, auctions = 60, perItem = 120, total = 120, time = 110 }, colHidden = { total = true } }, lastModifiedVersion = 62 }, ledgerResaleScrollingTable = { type = "table", default = { colWidth = { item = 194, bought = 50, avgBuyPrice = 120, sold = 50, avgSellPrice = 120, avgProfit = 120, totalProfit = 120, profitPct = 80 }, colHidden = { totalProfit = true, profitPct = true } }, lastModifiedVersion = 62 }, dashboardDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 59 }, dashboardUnselectedCharacters = { type = "table", default = {}, lastModifiedVersion = 78 }, dashboardTimeRange = { type = "number", default = -1, lastModifiedVersion = 78 }, groupsDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 55 }, operationsDividedContainer = { type = "table", default = { leftWidth = 306 }, lastModifiedVersion = 55 }, importExportDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 55 }, operationsSummaryScrollingTable = { type = "table", default = { colWidth = { selected = 16, name = 248, groups = 130, items = 130 }, colHidden = {} }, lastModifiedVersion = 76 }, }, taskListUIContext = { frame = { type = "table", default = { topRightX = -220, topRightY = -10, minimized = false, isOpen = true }, lastModifiedVersion = 55 }, isOpen = { type = "boolean", default = true, lastModifiedVersion = 55 }, }, vendoringUIContext = { frame = { type = "table", default = { width = 560, height = 500, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 }, showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 }, buyScrollingTable = { type = "table", default = { colWidth = { qty = 40, item = 310, ilvl = 32, cost = 150 }, colHidden = { ilvl = true } }, lastModifiedVersion = 92 }, buybackScrollingTable = { type = "table", default = { colWidth = { qty = 40, item = 360, cost = 100 }, colHidden = {} }, lastModifiedVersion = 55 }, sellScrollingTable = { type = "table", default = { colWidth = { item = 300, vendorSell = 100, potential = 100 }, colHidden = {} }, lastModifiedVersion = 55 }, }, coreOptions = { globalOperations = { type = "boolean", default = false, lastModifiedVersion = 10 }, protectAuctionHouse = { type = "boolean", default = false, lastModifiedVersion = 77 }, chatFrame = { type = "string", default = "", lastModifiedVersion = 10 }, auctionSaleSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 }, minimapIcon = { type = "table", default = { hide = false, minimapPos = 220, radius = 80 }, lastModifiedVersion = 10 }, destroyValueSource = { type = "string", default = "dbmarket", lastModifiedVersion = 10 }, groupPriceSource = { type = "string", default = "dbmarket", lastModifiedVersion = 41 }, }, accountingOptions = { trackTrades = { type = "boolean", default = true, lastModifiedVersion = 10 }, autoTrackTrades = { type = "boolean", default = false, lastModifiedVersion = 10 }, }, auctioningOptions = { cancelWithBid = { type = "boolean", default = false, lastModifiedVersion = 10 }, disableInvalidMsg = { type = "boolean", default = false, lastModifiedVersion = 10 }, matchWhitelist = { type = "boolean", default = true, lastModifiedVersion = 10 }, scanCompleteSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 }, confirmCompleteSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 }, }, craftingOptions = { defaultMatCostMethod = { type = "string", default = "min(dbmarket, crafting, vendorbuy, convert(dbmarket))", lastModifiedVersion = 10 }, defaultCraftPriceMethod = { type = "string", default = "first(dbminbuyout, dbmarket)*0.95", lastModifiedVersion = 89 }, ignoreCharacters = { type = "table", default = {}, lastModifiedVersion = 10 }, ignoreGuilds = { type = "table", default = {}, lastModifiedVersion = 10 }, }, destroyingOptions = { autoStack = { type = "boolean", default = true, lastModifiedVersion = 10 }, includeSoulbound = { type = "boolean", default = false, lastModifiedVersion = 10 }, autoShow = { type = "boolean", default = true, lastModifiedVersion = 10 }, deMaxQuality = { type = "number", default = 3, lastModifiedVersion = 10 }, deAbovePrice = { type = "string", default = "0c", lastModifiedVersion = 10 }, }, mailingOptions = { sendItemsIndividually = { type = "boolean", default = false, lastModifiedVersion = 10 }, inboxMessages = { type = "boolean", default = true, lastModifiedVersion = 10 }, sendMessages = { type = "boolean", default = true, lastModifiedVersion = 10 }, resendDelay = { type = "number", default = 1, lastModifiedVersion = 10 }, keepMailSpace = { type = "number", default = 0, lastModifiedVersion = 10 }, deMaxQuality = { type = "number", default = 2, lastModifiedVersion = 10 }, openMailSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 }, recentlyMailedList = { type = "table", default = {}, lastModifiedVersion = 38 }, }, shoppingOptions = { minDeSearchLvl = { type = "number", default = 1, lastModifiedVersion = 10 }, maxDeSearchLvl = { type = "number", default = 500, lastModifiedVersion = 79 }, maxDeSearchPercent = { type = "number", default = 100, lastModifiedVersion = 23 }, pctSource = { type = "string", default = "dbmarket", lastModifiedVersion = 12 }, buyoutConfirm = { type = "boolean", default = false, lastModifiedVersion = 46 }, buyoutAlertSource = { type = "string", default = "min(100000g, 200% dbmarket)", lastModifiedVersion = 46 }, searchAutoFocus = { type = "boolean", default = true, lastModifiedVersion = 88 }, }, sniperOptions = { sniperSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 }, }, vendoringOptions = { displayMoneyCollected = { type = "boolean", default = false, lastModifiedVersion = 10 }, qsMarketValue = { type = "string", default = "dbmarket", lastModifiedVersion = 10 }, }, tooltipOptions = { enabled = { type = "boolean", default = true, lastModifiedVersion = 20 }, embeddedTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, customPriceTooltips = { type = "table", default = {}, lastModifiedVersion = 10 }, moduleTooltips = { type = "table", default = {}, lastModifiedVersion = 10 }, vendorBuyTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, vendorSellTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, groupNameTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, detailedDestroyTooltip = { type = "boolean", default = false, lastModifiedVersion = 10 }, millTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, prospectTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, deTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, transformTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 }, operationTooltips = { type = "table", default = {}, lastModifiedVersion = 10 }, tooltipShowModifier = { type = "string", default = "none", lastModifiedVersion = 10 }, inventoryTooltipFormat = { type = "string", default = "full", lastModifiedVersion = 10 }, tooltipPriceFormat = { type = "string", default = "text", lastModifiedVersion = 10 }, }, userData = { operations = { type = "table", default = {}, lastModifiedVersion = 10 }, customPriceSources = { type = "table", default = {}, lastModifiedVersion = 10 }, destroyingIgnore = { type = "table", default = {}, lastModifiedVersion = 10 }, savedShoppingSearches = { type = "table", default = { filters = {}, name = {}, isFavorite = {} }, lastModifiedVersion = 96 }, vendoringIgnore = { type = "table", default = {}, lastModifiedVersion = 10 }, savedAuctioningSearches = { type = "table", default = { filters = {}, searchTypes = {}, name = {}, isFavorite = {} }, lastModifiedVersion = 96 }, }, }, profile = { internalData = { createdDefaultOperations = { type = "boolean", default = false, lastModifiedVersion = 11 }, }, userData = { groups = { type = "table", default = {}, lastModifiedVersion = 10 }, items = { type = "table", default = {}, lastModifiedVersion = 10 }, operations = { type = "table", default = {}, lastModifiedVersion = 10 }, }, gatheringOptions = { sources = { type = "table", default = { "vendor", "guildBank", "alt", "altGuildBank", "craftProfit", "auction", "craftNoProfit" }, lastModifiedVersion = 32 }, }, }, factionrealm = { internalData = { characterGuilds = { type = "table", default = {}, lastModifiedVersion = 10 }, guildVaults = { type = "table", default = {}, lastModifiedVersion = 10 }, pendingMail = { type = "table", default = {}, lastModifiedVersion = 10 }, expiringMail = { type = "table", default = {}, lastModifiedVersion = 47 }, expiringAuction = { type = "table", default = {}, lastModifiedVersion = 47 }, mailDisenchantablesChar = { type = "string", default = "", lastModifiedVersion = 49 }, mailExcessGoldChar = { type = "string", default = "", lastModifiedVersion = 49 }, mailExcessGoldLimit = { type = "number", default = 10000000000, lastModifiedVersion = 49 }, crafts = { type = "table", default = {}, lastModifiedVersion = 53 }, mats = { type = "table", default = {}, lastModifiedVersion = 10 }, guildGoldLog = { type = "table", default = {}, lastModifiedVersion = 25 }, guildGoldLogLastUpdate = { type = "table", default = {}, lastModifiedVersion = 83 }, csvAuctionDBScan = { type = "string", default = "", lastModifiedVersion = 50 }, auctionDBScanTime = { type = "number", default = 0, lastModifiedVersion = 50 }, auctionDBScanHash = { type = "number", default = 0, lastModifiedVersion = 50 }, isCraftFavorite = { type = "table", default = {}, lastModifiedVersion = 56 }, }, coreOptions = { ignoreGuilds = { type = "table", default = {}, lastModifiedVersion = 10 }, }, auctioningOptions = { whitelist = { type = "table", default = {}, lastModifiedVersion = 10 }, }, gatheringContext = { crafter = { type = "string", default = "", lastModifiedVersion = 32 }, professions = { type = "table", default = {}, lastModifiedVersion = 32 }, }, userData = { craftingCooldownIgnore = { type = "table", default = {}, lastModifiedVersion = 35 }, }, }, realm = { internalData = { csvSales = { type = "string", default = "", lastModifiedVersion = 10 }, csvBuys = { type = "string", default = "", lastModifiedVersion = 10 }, csvIncome = { type = "string", default = "", lastModifiedVersion = 10 }, csvExpense = { type = "string", default = "", lastModifiedVersion = 10 }, csvExpired = { type = "string", default = "", lastModifiedVersion = 10 }, csvCancelled = { type = "string", default = "", lastModifiedVersion = 10 }, saveTimeSales = { type = "string", default = "", lastModifiedVersion = 10 }, saveTimeBuys = { type = "string", default = "", lastModifiedVersion = 10 }, saveTimeExpires = { type = "string", default = "", lastModifiedVersion = 10 }, saveTimeCancels = { type = "string", default = "", lastModifiedVersion = 10 }, accountingTrimmed = { type = "table", default = {}, lastModifiedVersion = 10 }, }, }, char = { internalData = { auctionPrices = { type = "table", default = {}, lastModifiedVersion = 10 }, auctionMessages = { type = "table", default = {}, lastModifiedVersion = 10 }, craftingCooldowns = { type = "table", default = {}, lastModifiedVersion = 27 }, auctionSaleHints = { type = "table", default = {}, lastModifiedVersion = 45 }, }, auctionUIContext = { auctioningGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, shoppingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, bankingUIContext = { warehousingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, auctioningGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, mailingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, craftingUIContext = { groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, mailingUIContext = { groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, vendoringUIContext = { groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, mainUIContext = { groupsManagementGroupTree = { type = "table", default = { collapsed = {} }, lastModifiedVersion = 71 }, importGroupTree = { type = "table", default = { collapsed = {}, selected = {} }, lastModifiedVersion = 80 }, exportGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 }, }, }, sync = { -- NOTE: whenever these are changed, the sync version needs to be increased in LibTSM/Services/SyncClasses/Constants.lua internalData = { money = { type = "number", default = 0, lastModifiedVersion = 74 }, classKey = { type = "string", default = "", lastModifiedVersion = 19 }, bagQuantity = { type = "table", default = {}, lastModifiedVersion = 19 }, bankQuantity = { type = "table", default = {}, lastModifiedVersion = 19 }, reagentBankQuantity = { type = "table", default = {}, lastModifiedVersion = 19 }, auctionQuantity = { type = "table", default = {}, lastModifiedVersion = 19 }, mailQuantity = { type = "table", default = {}, lastModifiedVersion = 19 }, goldLog = { type = "string", default = "", lastModifiedVersion = 25 }, goldLogLastUpdate = { type = "number", default = 0, lastModifiedVersion = 83 }, playerProfessions = { type = "table", default = {}, lastModifiedVersion = 36 }, }, }, } -- ============================================================================ -- Module Loading -- ============================================================================ Settings:OnSettingsLoad(function() local db, upgradeObj = private.Constructor("TradeSkillMasterDB", SETTINGS_INFO) private.db = db if not upgradeObj then return end -- process DB upgrades local prevVersion = upgradeObj:GetPrevVersion() if prevVersion < 19 then -- migrate inventory data to the sync scope local oldInventoryData = TempTable.Acquire() local oldSyncMetadata = TempTable.Acquire() local oldAccountKey = TempTable.Acquire() local oldCharacters = TempTable.Acquire() for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, scopeKey, _, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "factionrealm" then if settingKey == "inventory" then oldInventoryData[scopeKey] = value elseif settingKey == "syncMetadata" then oldSyncMetadata[scopeKey] = value elseif settingKey == "accountKey" then oldAccountKey[scopeKey] = value elseif settingKey == "characters" then oldCharacters[scopeKey] = value end end end for factionrealm, characters in pairs(oldInventoryData) do local syncMetadata = oldSyncMetadata[factionrealm] and oldSyncMetadata[factionrealm].TSM_CHARACTERS for character, inventoryData in pairs(characters) do if not syncMetadata or not syncMetadata[character] or syncMetadata[character].owner == oldAccountKey[factionrealm] then db:NewSyncCharacter(character, db:GetSyncAccountKey(factionrealm), factionrealm) local syncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm) local class = oldCharacters[factionrealm] and oldCharacters[factionrealm][character] if type(class) == "string" then db:Set("sync", syncScopeKey, "internalData", "classKey", class) end db:Set("sync", syncScopeKey, "internalData", "bagQuantity", inventoryData.bag) db:Set("sync", syncScopeKey, "internalData", "bankQuantity", inventoryData.bank) db:Set("sync", syncScopeKey, "internalData", "reagentBankQuantity", inventoryData.reagentBank) db:Set("sync", syncScopeKey, "internalData", "auctionQuantity", inventoryData.auction) db:Set("sync", syncScopeKey, "internalData", "mailQuantity", inventoryData.mail) end end end TempTable.Release(oldInventoryData) TempTable.Release(oldSyncMetadata) TempTable.Release(oldAccountKey) TempTable.Release(oldCharacters) end if prevVersion < 25 then -- migrate gold log info local NEW_CSV_COLS = { "minute", "copper" } local function ConvertGoldLogFormat(data) local decodedData = select(2, CSV.Decode(data)) if not decodedData then return end for _, entry in ipairs(decodedData) do local minute = entry.startMinute local copper = entry.copper wipe(entry) entry.minute = minute entry.copper = copper end return CSV.Encode(NEW_CSV_COLS, decodedData) end local function ProcessGoldLogData(character, data, scopeKey) if type(data) ~= "string" then return end -- check if we know about this character and under what faction local syncScopeKey = nil for factionrealm in db:FactionrealmByRealmIterator(scopeKey) do local testSyncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm) if db:Get("sync", testSyncScopeKey, "internalData", "classKey") then syncScopeKey = testSyncScopeKey end end if syncScopeKey then db:Set("sync", syncScopeKey, "internalData", "goldLog", ConvertGoldLogFormat(data)) else -- check if this is a known guild local found = false for factionrealm in db:FactionrealmByRealmIterator(scopeKey) do local characterGuilds = db:Get("factionrealm", factionrealm, "internalData", "characterGuilds") if not found and characterGuilds and Table.KeyByValue(characterGuilds, character) then local guildGoldLog = db:Get("factionrealm", factionrealm, "internalData", "guildGoldLog") or {} guildGoldLog[character] = ConvertGoldLogFormat(data) db:Set("factionrealm", factionrealm, "internalData", "guildGoldLog", guildGoldLog) found = true end end end end if prevVersion >= 10 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, scopeKey, _, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "realm" and settingKey == "goldLog" then for character, data in pairs(value) do ProcessGoldLogData(character, data, scopeKey) end end end end end if prevVersion < 36 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, factionrealm, _, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "factionrealm" and settingKey == "playerProfessions" then for character, data in pairs(value) do -- check if we know about this character local syncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm) if db:Get("sync", syncScopeKey, "internalData", "classKey") then db:Set("sync", syncScopeKey, "internalData", "playerProfessions", data) end end end end end if prevVersion < 53 and not TSM.IsWowClassic() then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, factionrealm, namespace, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "factionrealm" and namespace == "internalData" and settingKey == "crafts" then db:Set("factionrealm", factionrealm, "internalData", "crafts", value) end end end if prevVersion < 64 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, _, namespace, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "global" and namespace == "accountingOptions" and settingKey == "smartBuyPrice" and value then -- show a dialog to inform the user that this was removed StaticPopupDialogs["TSM_ACCOUNTING_SMART_AVG_REMOVED"] = { text = L["The 'use smart average for purchase price' setting has been removed from TSM and replaced with a new 'SmartAvgBuy' price source. Please update your custom prices appropriately."], button1 = OKAY, timeout = 0, whileDead = true, } Wow.ShowStaticPopupDialog("TSM_ACCOUNTING_SMART_AVG_REMOVED") end end end if prevVersion < 82 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "global" and namespace == "craftingUIContext" and settingKey == "professionScrollingTable" then -- preserve the previous values local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey) for col, width in pairs(value.colWidth) do newTbl.colWidth[col] = width end for col, hidden in pairs(value.colHidden) do newTbl.colHidden[col] = hidden end if value.collapsed then for col, collapsed in pairs(value.collapsed) do newTbl.collapsed[col] = collapsed end end end end end if prevVersion < 89 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "global" and namespace == "craftingOptions" and settingKey == "defaultCraftPriceMethod" then -- preserve the previous value db:Set(scopeType, scopeKey, namespace, settingKey, value) end end end if prevVersion < 96 then for key, value in upgradeObj:RemovedSettingIterator() do local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key) if scopeType == "global" and namespace == "userData" and settingKey == "savedShoppingSearches" then -- convert how they are stored local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey) for i, searchInfo in ipairs(value) do local filter = searchInfo.filter if searchInfo.name ~= filter then newTbl.name[filter] = searchInfo.name end if searchInfo.isFavorite then newTbl.isFavorite[filter] = true end newTbl.filters[i] = filter end elseif scopeType == "global" and namespace == "userData" and settingKey == "savedAuctioningSearches" then -- convert how they are stored local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey) for i, searchInfo in ipairs(value) do local filter = searchInfo.filter if searchInfo.name ~= filter then newTbl.name[filter] = searchInfo.name end if searchInfo.isFavorite then newTbl.isFavorite[filter] = true end newTbl.filters[i] = filter newTbl.searchTypes[i] = searchInfo.searchType end end end end end) -- ============================================================================ -- Module Functions -- ============================================================================ -- TODO: get rid of this function Settings.GetDB() assert(private.db) return private.db end function Settings.NewView() assert(private.db) return private.CreateView(private.db) end function Settings.Get(scope, scopeKey, namespace, key) return private.db:Get(scope, scopeKey, namespace, key) end function Settings.Set(scope, scopeKey, namespace, key, value) return private.db:Set(scope, scopeKey, namespace, key, value) end function Settings.GetCurrentSyncAccountKey() return private.db:GetSyncAccountKey() end function Settings.GetSyncScopeKeyByCharacter(character, factionrealm) return private.db:GetSyncScopeKeyByCharacter(character, factionrealm) end function Settings.GetCharacterSyncAccountKey(character) return private.context[private.db].db._syncOwner[private.db:GetSyncScopeKeyByCharacter(character)] end function Settings.ShowSyncSVCopyError() if time() - private.svCopyErrorTime < 60 then return end private.svCopyErrorTime = time() Log.PrintfUser(L["It appears that you've manually copied your saved variables between accounts which will cause TSM's automatic sync'ing to not work. You'll need to undo this, and/or delete the TradeSkillMaster saved variables files on both accounts (with WoW closed) in order to fix this."]) end function Settings.CharacterByAccountFactionrealmIterator(account, factionrealm) factionrealm = factionrealm or SCOPE_KEYS.factionrealm account = account or private.db:GetSyncAccountKey(factionrealm) local result = TempTable.Acquire() for scopeKey, ownerAccount in pairs(private.context[private.db].db._syncOwner) do if ownerAccount == account then local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm)) if character then tinsert(result, character) end end end return TempTable.Iterator(result) end function Settings.CharacterByFactionrealmIterator(factionrealm) factionrealm = factionrealm or SCOPE_KEYS.factionrealm local result = TempTable.Acquire() for scopeKey in pairs(private.context[private.db].db._syncOwner) do local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm)) if character then tinsert(result, character) end end return TempTable.Iterator(result) end function Settings.IsCurrentAccountOwner(character) return Settings.GetCharacterSyncAccountKey(character) == Settings.GetCurrentSyncAccountKey() end function Settings.ConnectedFactionrealmAltCharacterIterator() local result = TempTable.Acquire() for factionrealm in private.db:GetConnectedRealmIterator("factionrealm") do for scopeKey in pairs(private.context[private.db].db._syncOwner) do local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm)) if character and (factionrealm ~= SCOPE_KEYS.factionrealm or character ~= PLAYER) then tinsert(result, factionrealm) tinsert(result, character) tinsert(result, character..SCOPE_KEY_SEP..factionrealm) end end end return TempTable.Iterator(result, 3) end function Settings.SyncAccountIterator() local result = TempTable.Acquire() local used = TempTable.Acquire() for _, syncOwner in pairs(private.context[private.db].db._syncOwner) do if strmatch(syncOwner, "^"..String.Escape(SCOPE_KEYS.factionrealm..SCOPE_KEY_SEP).."(%d+)$") and not used[syncOwner] and syncOwner ~= Settings.GetCurrentSyncAccountKey() then used[syncOwner] = true tinsert(result, syncOwner) end end TempTable.Release(used) return TempTable.Iterator(result) end function Settings.NewSyncCharacter(accountKey, character) local factionrealm = SCOPE_KEYS.factionrealm assert(strmatch(accountKey, "^"..String.Escape(factionrealm..SCOPE_KEY_SEP).."(%d+)$"), "Invalid account key") local scopeKey = private.db:GetSyncScopeKeyByCharacter(character, factionrealm) local context = private.context[private.db] context.db._syncOwner[scopeKey] = accountKey if not tContains(context.db._scopeKeys.sync, scopeKey) then tinsert(context.db._scopeKeys.sync, scopeKey) end private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.sync, String.Escape(scopeKey), ".+", ".+")) end function Settings.RemoveSyncAccount(accountKey) local settingsDB = private.context[private.db].db assert(accountKey ~= private.db:GetSyncAccountKey()) local scopeKeysToRemove = TempTable.Acquire() for scopeKey, ownerAccountKey in pairs(settingsDB._syncOwner) do if ownerAccountKey == accountKey then tinsert(scopeKeysToRemove, scopeKey) end end for _, scopeKey in ipairs(scopeKeysToRemove) do private.db:DeleteScope("sync", scopeKey) settingsDB._syncOwner[scopeKey] = nil end TempTable.Release(scopeKeysToRemove) end function Settings.RemoveSyncCharacter(character) local settingsDB = private.context[private.db].db local scopeKey = private.db:GetSyncScopeKeyByCharacter(character) private.db:DeleteScope("sync", scopeKey) settingsDB._syncOwner[scopeKey] = nil end function Settings.SyncSettingIterator() local result = TempTable.Acquire() for namespace, settings in pairs(private.context[private.db].settingsInfo.sync) do for settingKey in pairs(settings) do tinsert(result, namespace) tinsert(result, settingKey) end end return TempTable.Iterator(result, 2) end function Settings.FactionrealmCharacterIterator() return private.db:FactionrealmCharacterIterator() end -- ============================================================================ -- Main SettingsDB Class -- ============================================================================ local PROTECTED_TABLE_MT = { __newindex = function(self, key, value) assert(private.protectedAccessAllowed[self], "Attempting to modify a protected table") rawset(self, key, value) end, __metatable = false } local SETTINGS_MT = { -- getter __index = function(self, key) if private.SettingsDBMethods[key] then return private.SettingsDBMethods[key] elseif SCOPE_TYPES[key] then return private.context[self].scopeProxies[key] else error("Invalid scope: "..tostring(key)) end end, -- setter __newindex = function(self, key, value) error("You cannot set values in this table! You're probably missing a scope.") end, __metatable = false, } function private.Constructor(name, rawSettingsInfo) assert(type(name) == "string") assert(type(rawSettingsInfo) == "table") local version = rawSettingsInfo.version assert(type(version) == "number" and version >= 1) -- get (and create if necessary) the global table local db = _G[name] if not db then db = {} _G[name] = db end -- flatten and validate rawSettingsInfo and generate hash data local settingsInfo = CopyTable(rawSettingsInfo) local hashDataParts = TempTable.Acquire() local newLastModifiedVersion = TempTable.Acquire() for scope, scopeSettingsInfo in pairs(rawSettingsInfo) do if scope ~= "version" then assert(SCOPE_TYPES[scope], "Invalid scope: "..tostring(scope)) for namespace, namespaceSettingsInfo in pairs(scopeSettingsInfo) do assert(type(namespace) == "string" and type(namespaceSettingsInfo) == "table") assert(not strfind(namespace, KEY_SEP)) for key, info in pairs(namespaceSettingsInfo) do assert(type(key) == "string" and type(info) == "table", "Invalid type for key: "..tostring(key)) assert(not strfind(key, KEY_SEP)) for k, v in pairs(info) do if k == "type" then assert(VALID_TYPES[info.type], "Invalid type for key: "..key) elseif k == "default" then assert(type(v) == info.type, "Invalid default for key: "..key) if type(v) == "table" then private.CheckDefaultTable(v) end elseif k == "lastModifiedVersion" then assert(type(v) == "number" and v <= version, "Invalid lastModifiedVersion for key: "..key) newLastModifiedVersion[strjoin(KEY_SEP, SCOPE_TYPES[scope], namespace, key)] = v else error("Unexpected key in settingsInfo for key: "..key) end end tinsert(hashDataParts, strjoin(",", key, scope, namespace, info.type, type(info.default) == "table" and "table" or tostring(info.default))) end end end end sort(hashDataParts) local hash = Math.CalculateHash(table.concat(hashDataParts, ";")) TempTable.Release(hashDataParts) -- reset the DB if it's not valid local isValid = true if not next(db) then -- new DB isValid = false elseif not private.ValidateDB(db) then -- corrupted DB assert(not TSM.IsDevVersion(), "DB is not valid!") isValid = false elseif db._version == version and db._hash ~= hash then -- the hash didn't match assert(not TSM.IsDevVersion(), "Invalid settings hash! Did you forget to increase the version?") isValid = false elseif db._syncOwner and db._syncOwner[SCOPE_KEYS.sync] and db._syncOwner[SCOPE_KEYS.sync] ~= db._syncAccountKey[SCOPE_KEYS.factionrealm] then -- we aren't the owner of this character, so wipe the DB and show a manual error Settings.ShowSyncSVCopyError() assert(not TSM.IsDevVersion(), "Settings are corrupted due to manual copying of saved variables file") isValid = false end if not isValid then -- wipe the DB and start over wipe(db) for key, value in pairs(DEFAULT_DB) do db[key] = private.CopyData(value) end end db._hash = hash if not db._syncOwner then -- we just upgraded to the first version with the sync scope db._syncOwner = {} db._syncAccountKey = {} db._scopeKeys.sync = {} end -- make sure we have sync account keys for every factionrealm for _, factionrealm in ipairs(db._scopeKeys.factionrealm) do db._syncAccountKey[factionrealm] = db._syncAccountKey[factionrealm] or strjoin(SCOPE_KEY_SEP, factionrealm, random(time())) end -- create the sync account key for this factionrealm if necessary db._syncAccountKey[SCOPE_KEYS.factionrealm] = db._syncAccountKey[SCOPE_KEYS.factionrealm] or strjoin(SCOPE_KEY_SEP, SCOPE_KEYS.factionrealm, random(time())) -- set the sync owner of the current sync scope key to this account db._syncOwner[SCOPE_KEYS.sync] = db._syncOwner[SCOPE_KEYS.sync] or db._syncAccountKey[SCOPE_KEYS.factionrealm] -- setup current scope keys and set defaults for new keys db._currentProfile[SCOPE_KEYS.char] = db._currentProfile[SCOPE_KEYS.char] or DEFAULT_PROFILE_NAME local currentScopeKeys = CopyTable(SCOPE_KEYS) currentScopeKeys.profile = db._currentProfile[SCOPE_KEYS.char] for scopeType, scopeKey in pairs(currentScopeKeys) do if scopeType ~= "global" and not tContains(db._scopeKeys[scopeType], scopeKey) then tinsert(db._scopeKeys[scopeType], scopeKey) private.SetScopeDefaults(db, settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES[scopeType], String.Escape(scopeKey), ".+", ".+")) end end -- set any values which are nil to their default value db._scopeKeys = db._scopeKeys or { profile = {}, realm = {}, factionrealm = {}, char = {}, sync = {}, } for scopeType, scopeKeys in pairs(db._scopeKeys) do for _, scopeKey in ipairs(scopeKeys) do for namespace, namespaceInfo in pairs(settingsInfo[scopeType]) do for settingKey, info in pairs(namespaceInfo) do local key = strjoin(KEY_SEP, SCOPE_TYPES[scopeType], scopeKey, namespace, settingKey) if db[key] == nil then private.SetDBKeyValue(db, key, private.CopyData(info.default)) end end end end end for namespace, namespaceInfo in pairs(settingsInfo.global) do for settingKey, info in pairs(namespaceInfo) do local key = strjoin(KEY_SEP, SCOPE_TYPES.global, GLOBAL_SCOPE_KEY, namespace, settingKey) if db[key] == nil then private.SetDBKeyValue(db, key, private.CopyData(info.default)) end end end -- do any necessary upgrading or downgrading if the version changed db._lastModifiedVersion = db._lastModifiedVersion or {} local removedSettings, prevVersion = nil, nil if version ~= db._version then -- clear any settings which no longer exist, and set new/updated settings to their default values removedSettings = {} for key in pairs(db) do -- ignore metadata (keys starting with "_") if strsub(key, 1, 1) ~= "_" then local scopeTypeShort, namespace, settingKey = strmatch(key, "^(.+)"..KEY_SEP..".+"..KEY_SEP.."(.+)"..KEY_SEP.."(.+)$") local settingLastModifiedVersion = scopeTypeShort and db._lastModifiedVersion[strjoin(KEY_SEP, scopeTypeShort, namespace, settingKey)] local scopeType = scopeTypeShort and private.ScopeReverseLookup(scopeTypeShort) local info = settingKey and settingsInfo[scopeType] and settingsInfo[scopeType][namespace] and settingsInfo[scopeType][namespace][settingKey] if not info then -- this setting was removed so remove it from the db removedSettings[key] = db[key] db[key] = nil elseif info.lastModifiedVersion > db._version then -- this setting was updated, so we'll reset it to the default value removedSettings[key] = db[key] elseif not settingLastModifiedVersion and version < db._version then -- we don't have lastModifiedVersion info for this setting and the DB is getting downgraded, so we'll reset it to the default value removedSettings[key] = db[key] elseif (settingLastModifiedVersion or 0) > version then -- this setting is being downgraded, so we'll reset it to the default value removedSettings[key] = db[key] end end end for scope, scopeInfo in pairs(settingsInfo) do if scope ~= "version" then for namespace, namespaceInfo in pairs(scopeInfo) do for settingKey, info in pairs(namespaceInfo) do local settingLastModifiedVersion = db._lastModifiedVersion[strjoin(KEY_SEP, SCOPE_TYPES[scope], namespace, settingKey)] if info.lastModifiedVersion > db._version or (not settingLastModifiedVersion and version < db._version) or (settingLastModifiedVersion or 0) > version then -- this is either a new setting or was changed or this is a downgrade - either way set it to the default value private.SetScopeDefaults(db, settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES[scope], ".+", namespace, settingKey)) end end end end end if version > db._version then prevVersion = db._version else removedSettings = nil end db._version = version end -- populate the new lastModifiedVersion info wipe(db._lastModifiedVersion) for k, v in pairs(newLastModifiedVersion) do db._lastModifiedVersion[k] = v end TempTable.Release(newLastModifiedVersion) -- make the db table protected setmetatable(db, PROTECTED_TABLE_MT) -- create the new object and return it local new = setmetatable({}, SETTINGS_MT) private.context[new] = { db = db, settingsInfo = settingsInfo, currentScopeKeys = currentScopeKeys, callbacks = {}, scopeProxies = {}, namespaceProxies = {}, } for scopeType, scopeInfo in pairs(rawSettingsInfo) do if scopeType ~= "version" then for namespace in pairs(scopeInfo) do private.context[new].namespaceProxies[scopeType..KEY_SEP..namespace] = private.CreateNamespace(new, namespace, scopeType) end private.context[new].scopeProxies[scopeType] = private.CreateScope(new, scopeType) end end local upgradeObj = nil if removedSettings then upgradeObj = setmetatable({}, private.SettingsDBUpgradeObjMT) assert(prevVersion) private.upgradeContext = { removedSettings = removedSettings, prevVersion = prevVersion, } end return new, upgradeObj end -- ============================================================================ -- Class for upgrade object -- ============================================================================ private.SettingsDBUpgradeObjMT = { -- getter __index = { GetPrevVersion = function(self) return private.upgradeContext.prevVersion end, RemovedSettingIterator = function(self) return next, private.upgradeContext.removedSettings, nil end, GetKeyInfo = function(self, key) local scopeType, scopeKey, namespace, settingKey = nil, nil, nil, nil local parts = TempTable.Acquire(strsplit(KEY_SEP, key)) if #parts == 4 then scopeType, scopeKey, namespace, settingKey = TempTable.UnpackAndRelease(parts) scopeType = private.ScopeReverseLookup(scopeType) elseif #parts == 3 then scopeType, scopeKey, settingKey = TempTable.UnpackAndRelease(parts) scopeType = private.ScopeReverseLookup(scopeType) else error("Unknown key: "..tostring(key)) end return scopeType, scopeKey, namespace, settingKey end, }, -- setter __newindex = function(self) error("You cannot set values in this table!") end, __metatable = false, } -- ============================================================================ -- SettingsDB Object Methods -- ============================================================================ private.SettingsDBMethods = { Get = function(self, scope, scopeKey, namespace, key) assert(SCOPE_TYPES[scope] and type(namespace) == "string" and type(key) == "string", "Invalid parameters!") local context = private.context[self] assert(context.settingsInfo[scope][namespace][key], "Setting does not exist!") scopeKey = scopeKey or context.currentScopeKeys[scope] return context.db[strjoin(KEY_SEP, SCOPE_TYPES[scope], scopeKey, namespace, key)] end, Set = function(self, scope, scopeKey, namespace, key, value) assert(SCOPE_TYPES[scope] and type(namespace) == "string" and type(key) == "string", "Invalid parameters!") local context = private.context[self] local info = context.settingsInfo[scope][namespace][key] assert(info, "Setting does not exist!") assert(value == nil or type(value) == info.type, "Value is of wrong type.") scopeKey = scopeKey or context.currentScopeKeys[scope] private.SetDBKeyValue(context.db, strjoin(KEY_SEP, SCOPE_TYPES[scope], scopeKey, namespace, key), value) end, GetDefaultReadOnly = function(self, scope, namespace, key) local context = private.context[self] return context.settingsInfo[scope][namespace][key].default end, GetDefault = function(self, scope, namespace, key) return private.CopyData(self:GetDefaultReadOnly(scope, namespace, key)) end, RegisterCallback = function(self, event, callback) assert(event == "OnProfileUpdated") assert(type(callback) == "function") private.context[self].callbacks[event] = callback end, IsValidProfileName = function(self, name) return name ~= "" and not strfind(name, KEY_SEP) end, ProfileExists = function(self, name) return tContains(private.context[self].db._scopeKeys.profile, name) and true or false end, GetCurrentProfile = function(self) return private.context[self].currentScopeKeys.profile end, GetScopeKeys = function(self, scope) return CopyTable(private.context[self].db._scopeKeys[scope]) end, GetProfiles = function(self) return self:GetScopeKeys("profile") end, ProfileIterator = function(self) return ipairs(private.context[self].db._scopeKeys.profile) end, SetProfile = function(self, profileName, noCallback) assert(type(profileName) == "string", tostring(profileName)) assert(not strfind(profileName, KEY_SEP)) local context = private.context[self] -- change the current profile for this character context.db._currentProfile[SCOPE_KEYS.char] = profileName context.currentScopeKeys.profile = context.db._currentProfile[SCOPE_KEYS.char] local isNew = false if not tContains(context.db._scopeKeys.profile, profileName) then tinsert(context.db._scopeKeys.profile, profileName) -- this is a new profile, so set all the settings to their default values private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.profile, String.Escape(profileName), ".+", ".+")) isNew = true end if context.callbacks.OnProfileUpdated and not noCallback then context.callbacks.OnProfileUpdated(isNew) end end, ResetProfile = function(self) local context = private.context[self] private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.profile, String.Escape(context.currentScopeKeys.profile), ".+", ".+")) if context.callbacks.OnProfileUpdated then context.callbacks.OnProfileUpdated(true) end end, CopyProfile = function(self, sourceProfileName) assert(type(sourceProfileName) == "string") assert(not strfind(sourceProfileName, KEY_SEP)) local context = private.context[self] assert(sourceProfileName ~= context.currentScopeKeys.profile) -- copy all the settings from the source profile to the current one for namespace, namespaceInfo in pairs(context.settingsInfo.profile) do for settingKey in pairs(namespaceInfo) do local srcKey = strjoin(KEY_SEP, SCOPE_TYPES.profile, sourceProfileName, namespace, settingKey) local destKey = strjoin(KEY_SEP, SCOPE_TYPES.profile, context.currentScopeKeys.profile, namespace, settingKey) private.SetDBKeyValue(context.db, destKey, private.CopyData(context.db[srcKey])) end end if context.callbacks.OnProfileUpdated then context.callbacks.OnProfileUpdated(false) end end, DeleteScope = function(self, scopeType, scopeKey) assert(SCOPE_TYPES[scopeType]) assert(type(scopeKey) == "string") local context = private.context[self] assert(scopeKey ~= context.currentScopeKeys[scopeType]) -- remove all settings for the specified profile local searchPattern = strjoin(KEY_SEP, SCOPE_TYPES[scopeType], String.Escape(scopeKey), ".+", ".+") for key in pairs(context.db) do if strmatch(key, searchPattern) then private.SetDBKeyValue(context.db, key, nil) end end -- remove the scope key from the list Table.RemoveByValue(context.db._scopeKeys[scopeType], scopeKey) end, DeleteProfile = function(self, profileName, defaultNewProfileName) self:DeleteScope("profile", profileName) -- move other characters which were on this profile to another one local context = private.context[self] if not defaultNewProfileName then defaultNewProfileName = context.db._scopeKeys.profile[1] end assert(defaultNewProfileName and tContains(context.db._scopeKeys.profile, defaultNewProfileName)) for character, currentProfileName in pairs(context.db._currentProfile) do if currentProfileName == profileName then assert(character ~= SCOPE_KEYS.char) context.db._currentProfile[character] = defaultNewProfileName end end end, GetConnectedRealmIterator = function(self, scope) assert(scope == "factionrealm" or scope == "realm") return private.ConnectedRealmIterator, self, scope end, GetSyncAccountKey = function(self, factionrealm) factionrealm = factionrealm or SCOPE_KEYS.factionrealm return private.context[self].db._syncAccountKey[factionrealm] end, SyncAccountIterator = function(self) local result = TempTable.Acquire() local used = TempTable.Acquire() for _, syncOwner in pairs(private.context[self].db._syncOwner) do if strmatch(syncOwner, "^"..String.Escape(SCOPE_KEYS.factionrealm..SCOPE_KEY_SEP).."(%d+)$") and not used[syncOwner] and syncOwner ~= self:GetSyncAccountKey() then used[syncOwner] = true tinsert(result, syncOwner) end end TempTable.Release(used) return TempTable.Iterator(result) end, NewSyncCharacter = function(self, character, accountKey, factionrealm) factionrealm = factionrealm or SCOPE_KEYS.factionrealm assert(strmatch(accountKey, "^"..String.Escape(factionrealm..SCOPE_KEY_SEP).."(%d+)$"), "Invalid account key") local scopeKey = self:GetSyncScopeKeyByCharacter(character, factionrealm) local context = private.context[self] context.db._syncOwner[scopeKey] = accountKey if not tContains(context.db._scopeKeys.sync, scopeKey) then tinsert(context.db._scopeKeys.sync, scopeKey) end private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.sync, String.Escape(scopeKey), ".+", ".+")) end, RemoveSyncAccount = function(self, accountKey) local settingsDB = private.context[self].db assert(accountKey ~= self:GetSyncAccountKey()) local scopeKeysToRemove = TempTable.Acquire() for scopeKey, ownerAccountKey in pairs(settingsDB._syncOwner) do if ownerAccountKey == accountKey then tinsert(scopeKeysToRemove, scopeKey) end end for _, scopeKey in ipairs(scopeKeysToRemove) do self:DeleteScope("sync", scopeKey) settingsDB._syncOwner[scopeKey] = nil end TempTable.Release(scopeKeysToRemove) end, RemoveSyncCharacter = function(self, character) local settingsDB = private.context[self].db local scopeKey = self:GetSyncScopeKeyByCharacter(character) self:DeleteScope("sync", scopeKey) settingsDB._syncOwner[scopeKey] = nil end, GetSyncOwnerAccountKey = function(self, character) return private.context[self].db._syncOwner[self:GetSyncScopeKeyByCharacter(character)] end, FactionrealmCharacterIterator = function(self, factionrealm) factionrealm = factionrealm or SCOPE_KEYS.factionrealm local result = TempTable.Acquire() for scopeKey in pairs(private.context[self].db._syncOwner) do local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm)) if character then tinsert(result, character) end end return TempTable.Iterator(result) end, GetSyncScopeKeyByCharacter = function(self, character, factionrealm) return character..SCOPE_KEY_SEP..(factionrealm or SCOPE_KEYS.factionrealm) end, FactionrealmByRealmIterator = function(self, realm) return private.FactionrealmByRealmIteratorHelper, realm end, } -- ============================================================================ -- Proxy Class for Scopes (TSM.db.XXXXX) -- ============================================================================ local SCOPE_MT = { -- getter __index = function(self, namespace) assert(type(namespace) == "string", "Invalid namespace type!") local proxyInfo = private.proxies[self] local context = private.context[proxyInfo.settingsDB] assert(context.settingsInfo[proxyInfo.scope][namespace], "Namespace does not exist!") local namespaceProxy = context.namespaceProxies[proxyInfo.scope..KEY_SEP..namespace] assert(namespaceProxy) return namespaceProxy end, -- setter __newindex = function(self, key, value) error("You cannot set values in this table! You're probably missing a namespace.") end, __metatable = false, } -- ============================================================================ -- Proxy Class for Namespaces (TSM.db..XXXXX) -- ============================================================================ local NAMESPACE_MT = { -- getter __index = function(self, key) assert(type(key) == "string", "Invalid setting key type!") local proxyInfo = private.proxies[self] return proxyInfo.settingsDB:Get(proxyInfo.scope, nil, proxyInfo.namespace, key) end, -- setter __newindex = function(self, key, value) local proxyInfo = private.proxies[self] proxyInfo.settingsDB:Set(proxyInfo.scope, nil, proxyInfo.namespace, key, value) end, __metatable = false, } -- ============================================================================ -- Setting View Class (see Settings.CreateView(...)) -- ============================================================================ local VIEW_METHODS = { AddKey = function(self, scopeType, namespace, key) local viewInfo = private.views[self] assert(viewInfo and not viewInfo.keyProxies[key]) viewInfo.scopeNamespace[key] = scopeType..KEY_SEP..namespace viewInfo.keyProxies[key] = private.context[viewInfo.settingsDB].namespaceProxies[viewInfo.scopeNamespace[key]] return self end, RegisterCallback = function(self, key, callback) local viewInfo = private.views[self] assert(callback and not viewInfo.callbacks[key]) viewInfo.callbacks[key] = callback return self end, GetDefaultReadOnly = function(self, key) local viewInfo = private.views[self] local scope, namespace = strsplit(KEY_SEP, viewInfo.scopeNamespace[key]) assert(scope and namespace) return viewInfo.settingsDB:GetDefaultReadOnly(scope, namespace, key) end, } local VIEW_MT = { __index = function(self, key) if VIEW_METHODS[key] then return VIEW_METHODS[key] end return private.views[self].keyProxies[key][key] end, __newindex = function(self, key, value) private.views[self].keyProxies[key][key] = value end, __metatable = false, } -- ============================================================================ -- Helper Functions -- ============================================================================ function private.CheckDefaultTable(tbl) for k, v in pairs(tbl) do assert(type(k) == "string" or type(k) == "number") if type(v) == "table" then private.CheckDefaultTable(v) end end end function private.CreateScope(settingsDB, scope) assert(private.context[settingsDB]) local new = setmetatable({}, SCOPE_MT) private.proxies[new] = { settingsDB = settingsDB, scope = scope, } return new end function private.CreateNamespace(settingsDB, namespace, scope) assert(private.context[settingsDB]) local new = setmetatable({}, NAMESPACE_MT) private.proxies[new] = { settingsDB = settingsDB, namespace = namespace, scope = scope, } return new end function private.CreateView(settingsDB) assert(private.context[settingsDB]) local view = setmetatable({}, VIEW_MT) private.views[view] = { settingsDB = settingsDB, keyProxies = {}, scopeNamespace = {}, callbacks = {}, } return view end function private.SetDBKeyValue(db, key, value) private.protectedAccessAllowed[db] = true db[key] = value private.protectedAccessAllowed[db] = nil local scopeType, _, namespace, settingKey = strsplit(KEY_SEP, key) if not settingKey then return end scopeType = private.ScopeReverseLookup(scopeType) for _, info in pairs(private.views) do if info.callbacks[settingKey] and info.scopeNamespace[settingKey] == scopeType..KEY_SEP..namespace then info.callbacks[settingKey]() end end end function private.CopyData(data) if type(data) == "table" then return CopyTable(data) elseif VALID_TYPES[type(data)] or type(data) == nil then return data end end function private.ScopeReverseLookup(scopeTypeShort) for key, value in pairs(SCOPE_TYPES) do if value == scopeTypeShort then return key end end end function private.ValidateDB(db) -- make sure the DB we are loading from is valid if #db > 0 then return end if type(db._version) ~= "number" then return end if type(db._hash) ~= "number" then return end if db._lastModifiedVersion ~= nil and type(db._lastModifiedVersion) ~= "table" then return end if type(db._scopeKeys) ~= "table" then return end for scopeType, keys in pairs(db._scopeKeys) do if not SCOPE_TYPES[scopeType] then return end for i, name in pairs(keys) do if type(i) ~= "number" or i > #keys or i <= 0 or type(name) ~= "string" then return end end end if type(db._currentProfile) ~= "table" then return end for key, value in pairs(db._currentProfile) do if type(key) ~= "string" or type(value) ~= "string" then return end end return true end function private.SetScopeDefaults(db, settingsInfo, searchPattern) -- remove any existing entries for matching keys for key in pairs(db) do if strmatch(key, searchPattern) then private.SetDBKeyValue(db, key, nil) end end local scopeTypeShort = strsub(searchPattern, 1, 1) local scopeType = private.ScopeReverseLookup(scopeTypeShort) assert(scopeType, "Couldn't find scopeType: "..tostring(scopeTypeShort)) local scopeKeys = nil if scopeTypeShort == SCOPE_TYPES.global then scopeKeys = {GLOBAL_SCOPE_KEY} else scopeKeys = db._scopeKeys[scopeType] assert(scopeKeys, "Couldn't find scopeKeys for type: "..tostring(scopeTypeShort)) end -- set any matching keys to their default values if not settingsInfo[scopeType] then return end for namespace, namespaceInfo in pairs(settingsInfo[scopeType]) do for settingKey, info in pairs(namespaceInfo) do for _, scopeKey in ipairs(scopeKeys) do local key = strjoin(KEY_SEP, scopeTypeShort, scopeKey, namespace, settingKey) if strmatch(key, searchPattern) then private.SetDBKeyValue(db, key, private.CopyData(info.default)) end end end end end function private.ConnectedRealmIterator(self, prevScopeKey) if not private.cachedConnectedRealms then local connectedRealms = {} if not TSM.IsWowClassic() then local realmId, _, _, _, _, _, _, _, connectedRealmIds = LibRealmInfo:GetRealmInfo(REALM) if connectedRealmIds then for _, id in ipairs(connectedRealmIds) do if id ~= realmId then local _, connectedRealmName = LibRealmInfo:GetRealmInfoByID(id) tinsert(connectedRealms, connectedRealmName) end end end end private.cachedConnectedRealms = connectedRealms end local scope = nil if prevScopeKey == "factionrealm" or prevScopeKey == "realm" then -- this is the first time scope = prevScopeKey prevScopeKey = nil else scope = strmatch(prevScopeKey, String.Escape(FACTION.." - ")) and "factionrealm" or "realm" end local foundPrev = prevScopeKey == nil local index = 0 while true do local realm = index == 0 and SCOPE_KEYS.realm or private.cachedConnectedRealms[index] if not realm then return end index = index + 1 local scopeKey = (scope == "factionrealm") and (FACTION..SCOPE_KEY_SEP..realm) or realm if scopeKey == prevScopeKey then foundPrev = true elseif foundPrev and tContains(private.context[self].db._scopeKeys[scope], scopeKey) then return scopeKey end end end function private.FactionrealmByRealmIteratorHelper(realm, prevValue) if not prevValue then return strjoin(SCOPE_KEY_SEP, "Horde", realm) elseif strmatch(prevValue, "^Horde") then return strjoin(SCOPE_KEY_SEP, "Alliance", realm) elseif strmatch(prevValue, "^Alliance") then return strjoin(SCOPE_KEY_SEP, "Neutral", realm) end end