TradeSkillMaster/LibTSM/Service/Settings.lua

1631 lines
75 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- 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.<scope>.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