-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Operations = TSM:NewPackage("Operations") local TempTable = TSM.Include("Util.TempTable") local Log = TSM.Include("Util.Log") local Database = TSM.Include("Util.Database") local private = { db = nil, operations = nil, operationInfo = {}, operationModules = {}, shouldCreateDefaultOperations = false, ignoreProfileUpdate = false, } local COMMON_OPERATION_INFO = { ignorePlayer = { type = "table", default = {} }, ignoreFactionrealm = { type = "table", default = {} }, relationships = { type = "table", default = {} }, } local FACTION_REALM = UnitFactionGroup("player").." - "..GetRealmName() local PLAYER_KEY = UnitName("player").." - "..FACTION_REALM -- ============================================================================ -- Modules Functions -- ============================================================================ function Operations.OnInitialize() private.db = Database.NewSchema("OPERATIONS") :AddStringField("moduleName") :AddStringField("operationName") :AddIndex("moduleName") :Commit() if TSM.db.global.coreOptions.globalOperations then private.operations = TSM.db.global.userData.operations else private.operations = TSM.db.profile.userData.operations end private.RebuildDB() private.shouldCreateDefaultOperations = not TSM.db.profile.internalData.createdDefaultOperations TSM.db.profile.internalData.createdDefaultOperations = true TSM.db:RegisterCallback("OnProfileUpdated", private.OnProfileUpdated) end function Operations.Register(moduleName, localizedName, operationInfo, maxOperations, infoCallback, customSanitizeFunction) for key, info in pairs(operationInfo) do assert(type(key) == "string" and type(info) == "table") assert(info.type == type(info.default)) end for key, info in pairs(COMMON_OPERATION_INFO) do assert(not operationInfo[key]) operationInfo[key] = info end tinsert(private.operationModules, moduleName) private.operationInfo[moduleName] = { info = operationInfo, localizedName = localizedName, maxOperations = maxOperations, infoCallback = infoCallback, customSanitizeFunction = customSanitizeFunction, } local shouldCreateDefaultOperations = private.shouldCreateDefaultOperations or not private.operations[moduleName] private.operations[moduleName] = private.operations[moduleName] or {} if shouldCreateDefaultOperations and not private.operations[moduleName]["#Default"] then -- create default operation Operations.Create(moduleName, "#Default") end private.ValidateOperations(moduleName) private.RebuildDB() end function Operations.IsCommonKey(key) return COMMON_OPERATION_INFO[key] and true or false end function Operations.IsValidName(operationName) return operationName == strtrim(operationName) and operationName ~= "" and not strmatch(operationName, TSM.CONST.OPERATION_SEP) end function Operations.ModuleIterator() return ipairs(private.operationModules) end function Operations.ModuleExists(moduleName) return private.operationInfo[moduleName] and true or false end function Operations.GetLocalizedName(moduleName) return private.operationInfo[moduleName].localizedName end function Operations.GetMaxNumber(moduleName) return private.operationInfo[moduleName].maxOperations end function Operations.GetSettingDefault(moduleName, key) local info = private.operationInfo[moduleName].info[key] return info.type == "table" and CopyTable(info.default) or info.default end function Operations.OperationIterator(moduleName) local operations = TempTable.Acquire() for operationName in pairs(private.operations[moduleName]) do tinsert(operations, operationName) end sort(operations) return TempTable.Iterator(operations) end function Operations.Exists(moduleName, operationName) return private.operations[moduleName][operationName] and true or false end function Operations.GetSettings(moduleName, operationName) return private.operations[moduleName][operationName] end function Operations.Create(moduleName, operationName) assert(not private.operations[moduleName][operationName]) private.operations[moduleName][operationName] = {} Operations.Reset(moduleName, operationName) private.RebuildDB() end function Operations.BulkCreateFromImport(operations, replaceExisting) for moduleName, moduleOperations in pairs(operations) do for operationName, operationSettings in pairs(moduleOperations) do assert(replaceExisting or not private.operations[moduleName][operationName]) private.operations[moduleName][operationName] = operationSettings end end private.RebuildDB() end function Operations.Rename(moduleName, oldName, newName) assert(private.operations[moduleName][oldName]) private.operations[moduleName][newName] = private.operations[moduleName][oldName] private.operations[moduleName][oldName] = nil -- redirect relationships for _, operation in pairs(private.operations[moduleName]) do for key, target in pairs(operation.relationships) do if target == oldName then operation.relationships[key] = newName end end end TSM.Groups.OperationRenamed(moduleName, oldName, newName) private.RebuildDB() end function Operations.Copy(moduleName, operationName, sourceOperationName) assert(private.operations[moduleName][operationName] and private.operations[moduleName][sourceOperationName]) for key, info in pairs(private.operationInfo[moduleName].info) do local sourceValue = private.operations[moduleName][sourceOperationName][key] private.operations[moduleName][operationName][key] = info.type == "table" and CopyTable(sourceValue) or sourceValue end private.RemoveDeadRelationships(moduleName) private.RebuildDB() end function Operations.Delete(moduleName, operationName) assert(private.operations[moduleName][operationName]) private.operations[moduleName][operationName] = nil private.RemoveDeadRelationships(moduleName) TSM.Groups.RemoveOperationFromAllGroups(moduleName, operationName) private.RebuildDB() end function Operations.DeleteList(moduleName, operationNames) for _, operationName in ipairs(operationNames) do assert(private.operations[moduleName][operationName]) private.operations[moduleName][operationName] = nil private.RemoveDeadRelationships(moduleName) TSM.Groups.RemoveOperationFromAllGroups(moduleName, operationName) end private.RebuildDB() end function Operations.Reset(moduleName, operationName) for key in pairs(private.operationInfo[moduleName].info) do private.operations[moduleName][operationName][key] = Operations.GetSettingDefault(moduleName, key) end end function Operations.Update(moduleName, operationName) for key in pairs(private.operations[moduleName][operationName].relationships) do local operation = private.operations[moduleName][operationName] while operation.relationships[key] do local newOperation = private.operations[moduleName][operation.relationships[key]] if not newOperation then break end operation = newOperation end private.operations[moduleName][operationName][key] = operation[key] end end function Operations.IsCircularRelationship(moduleName, operationName, key) local visited = TempTable.Acquire() while operationName do if visited[operationName] then TempTable.Release(visited) return true end visited[operationName] = true operationName = private.operations[moduleName][operationName].relationships[key] end TempTable.Release(visited) return false end function Operations.GetFirstOperationByItem(moduleName, itemString) local groupPath = TSM.Groups.GetPathByItem(itemString) for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do Operations.Update(moduleName, operationName) if not private.IsIgnored(moduleName, operationName) then return operationName, private.operations[moduleName][operationName] end end end function Operations.GroupOperationIterator(moduleName, groupPath) local operations = TempTable.Acquire() operations.moduleName = moduleName for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do Operations.Update(moduleName, operationName) if not private.IsIgnored(moduleName, operationName) then tinsert(operations, operationName) end end return private.GroupOperationIteratorHelper, operations, 0 end function Operations.GroupHasOperation(moduleName, groupPath, targetOperationName) for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do if operationName == targetOperationName then return true end end return false end function Operations.GetDescription(moduleName, operationName) local operationSettings = private.operations[moduleName][operationName] assert(operationSettings) Operations.Update(moduleName, operationName) return private.operationInfo[moduleName].infoCallback(operationSettings) end function Operations.SanitizeSettings(moduleName, operationName, operationSettings, silentMissingCommonKeys, noRelationshipCheck) local didReset = false local operationInfo = private.operationInfo[moduleName].info if private.operationInfo[moduleName].customSanitizeFunction then private.operationInfo[moduleName].customSanitizeFunction(operationSettings) end for key, value in pairs(operationSettings) do if not noRelationshipCheck and Operations.IsCircularRelationship(moduleName, operationName, key) then Log.Err("Removing circular relationship (%s, %s, %s)", moduleName, operationName, key) operationSettings.relationships[key] = nil end if not operationInfo[key] then operationSettings[key] = nil elseif type(value) ~= operationInfo[key].type then if operationInfo[key].type == "string" and type(value) == "number" then -- some custom price settings were potentially stored as numbers previously, so just convert them operationSettings[key] = tostring(value) else didReset = true Log.Err("Resetting operation setting %s,%s,%s (%s)", moduleName, operationName, tostring(key), tostring(value)) operationSettings[key] = operationInfo[key].type == "table" and CopyTable(operationInfo[key].default) or operationInfo[key].default end elseif operationInfo[key].customSanitizeFunction then operationSettings[key] = operationInfo[key].customSanitizeFunction(value) end end for key in pairs(operationInfo) do if operationSettings[key] == nil then -- this key was missing if operationInfo[key].type == "boolean" then -- we previously stored booleans as nil instead of false operationSettings[key] = false else if not silentMissingCommonKeys or not Operations.IsCommonKey(key) then didReset = true Log.Err("Resetting missing operation setting %s,%s,%s", moduleName, operationName, tostring(key)) end operationSettings[key] = operationInfo[key].type == "table" and CopyTable(operationInfo[key].default) or operationInfo[key].default end end end return didReset end function Operations.HasRelationship(moduleName, operationName, settingKey) return Operations.GetRelationship(moduleName, operationName, settingKey) and true or false end function Operations.GetRelationship(moduleName, operationName, settingKey) assert(private.operationInfo[moduleName].info[settingKey]) return private.operations[moduleName][operationName].relationships[settingKey] end function Operations.SetRelationship(moduleName, operationName, settingKey, targetOperationName) assert(targetOperationName == nil or private.operations[moduleName][targetOperationName]) assert(private.operationInfo[moduleName].info[settingKey]) private.operations[moduleName][operationName].relationships[settingKey] = targetOperationName end function Operations.GetRelationshipColors(operationType, operationName, settingKey, value) local relationshipSet = Operations.HasRelationship(operationType, operationName, settingKey) local linkColor = nil if not value and relationshipSet then linkColor = "INDICATOR_DISABLED" elseif not value then linkColor = "TEXT_DISABLED" elseif relationshipSet then linkColor = "INDICATOR" else linkColor = "TEXT" end local linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", linkColor) return relationshipSet, linkTexture, value and not relationshipSet and "TEXT" or "TEXT_DISABLED" end function Operations.IsStoredGlobally() return TSM.db.global.coreOptions.globalOperations end function Operations.SetStoredGlobally(storeGlobally) TSM.db.global.coreOptions.globalOperations = storeGlobally -- we shouldn't be running the OnProfileUpdated callback while switching profiles private.ignoreProfileUpdate = true if storeGlobally then -- move current profile to global TSM.db.global.userData.operations = CopyTable(TSM.db.profile.userData.operations) -- clear out old operations for _ in TSM.GetTSMProfileIterator() do TSM.db.profile.userData.operations = nil end else -- move global to all profiles for _ in TSM.GetTSMProfileIterator() do TSM.db.profile.userData.operations = CopyTable(TSM.db.global.userData.operations) end -- clear out old operations TSM.db.global.userData.operations = nil end private.ignoreProfileUpdate = false private.OnProfileUpdated() end function Operations.ReplaceProfileOperations(newOperations) for k, v in pairs(newOperations) do TSM.db.profile.userData.operations[k] = v end end function Operations.CreateQuery() return private.db:NewQuery() end function Operations.GroupIterator(moduleName, filterOperationName, overrideOnly) local result = TempTable.Acquire() -- check the base group if Operations.GroupHasOperation(moduleName, TSM.CONST.ROOT_GROUP_PATH, filterOperationName) then tinsert(result, TSM.CONST.ROOT_GROUP_PATH) end -- need to filter out the groups without operations for _, groupPath in TSM.Groups.GroupIterator() do if (not overrideOnly or TSM.Groups.HasOperationOverride(groupPath, moduleName)) and Operations.GroupHasOperation(moduleName, groupPath, filterOperationName) then tinsert(result, groupPath) end end return TempTable.Iterator(result) end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.OnProfileUpdated() if private.ignoreProfileUpdate then return end if TSM.db.global.coreOptions.globalOperations then private.operations = TSM.db.global.userData.operations else private.operations = TSM.db.profile.userData.operations end for _, moduleName in Operations.ModuleIterator() do private.ValidateOperations(moduleName) end private.RebuildDB() TSM.Groups.RebuildDatabase() end function private.ValidateOperations(moduleName) if not private.operations[moduleName] then -- this is a new profile private.operations[moduleName] = {} Operations.Create(moduleName, "#Default") return end for operationName, operationSettings in pairs(private.operations[moduleName]) do if type(operationName) ~= "string" or not Operations.IsValidName(operationName) then Log.Err("Removing %s operation with invalid name: ", moduleName, tostring(operationName)) private.operations[moduleName][operationName] = nil else Operations.SanitizeSettings(moduleName, operationName, operationSettings) for key, target in pairs(operationSettings.relationships) do if not private.operations[moduleName][target] then Log.Err("Removing invalid relationship %s,%s,%s -> %s", moduleName, operationName, tostring(key), tostring(target)) operationSettings.relationships[key] = nil end end end end end function private.IsIgnored(moduleName, operationName) local operationSettings = private.operations[moduleName][operationName] assert(operationSettings) return operationSettings.ignorePlayer[PLAYER_KEY] or operationSettings.ignoreFactionrealm[FACTION_REALM] end function private.GroupOperationIteratorHelper(operations, index) index = index + 1 if index > #operations then TempTable.Release(operations) return end local operationName = operations[index] return index, operationName, private.operations[operations.moduleName][operationName] end function private.RemoveDeadRelationships(moduleName) for _, operation in pairs(private.operations[moduleName]) do for key, target in pairs(operation.relationships) do if not private.operations[moduleName][target] then operation.relationships[key] = nil end end end end function private.RebuildDB() private.db:TruncateAndBulkInsertStart() for moduleName, operations in pairs(private.operations) do for operationName in pairs(operations) do private.db:BulkInsertNewRow(moduleName, operationName) end end private.db:BulkInsertEnd() end