-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local Groups = TSM:NewPackage("Groups") local Database = TSM.Include("Util.Database") local TempTable = TSM.Include("Util.TempTable") local Table = TSM.Include("Util.Table") local SmartMap = TSM.Include("Util.SmartMap") local String = TSM.Include("Util.String") local Log = TSM.Include("Util.Log") local ItemString = TSM.Include("Util.ItemString") local private = { db = nil, itemDB = nil, itemStringMap = nil, itemStringMapReader = nil, baseItemStringItemIteratorQuery = nil, groupListCache = {}, } -- ============================================================================ -- New Modules Functions -- ============================================================================ function Groups.OnInitialize() private.db = Database.NewSchema("GROUPS") :AddStringField("groupPath") :AddStringField("orderStr") :AddBooleanField("hasAuctioningOperation") :AddBooleanField("hasCraftingOperation") :AddBooleanField("hasMailingOperation") :AddBooleanField("hasShoppingOperation") :AddBooleanField("hasSniperOperation") :AddBooleanField("hasVendoringOperation") :AddBooleanField("hasWarehousingOperation") :AddIndex("groupPath") :Commit() private.itemDB = Database.NewSchema("GROUP_ITEMS") :AddUniqueStringField("itemString") :AddStringField("groupPath") :AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString") :AddIndex("groupPath") :AddIndex("baseItemString") :Commit() private.itemStringMapReader = private.itemStringMap:CreateReader() Groups.RebuildDatabase() private.baseItemStringItemIteratorQuery = private.itemDB:NewQuery() :Select("itemString", "groupPath") :Equal("baseItemString", Database.BoundQueryParam()) end function Groups.RebuildDatabase() wipe(private.groupListCache) -- convert ignoreRandomEnchants to ignoreItemVariations for _, info in pairs(TSM.db.profile.userData.groups) do if info.ignoreRandomEnchants ~= nil then info.ignoreItemVariations = info.ignoreRandomEnchants info.ignoreRandomEnchants = nil end end for groupPath, groupInfo in pairs(TSM.db.profile.userData.groups) do if type(groupPath) == "string" and not strmatch(groupPath, TSM.CONST.GROUP_SEP..TSM.CONST.GROUP_SEP) then -- check the contents of groupInfo for _, moduleName in TSM.Operations.ModuleIterator() do groupInfo[moduleName] = groupInfo[moduleName] or {} if groupPath == TSM.CONST.ROOT_GROUP_PATH then -- root group should have override flag set groupInfo[moduleName].override = true end end for key in pairs(groupInfo) do if TSM.Operations.ModuleExists(key) then -- this is a set of module operations local operations = groupInfo[key] while #operations > TSM.Operations.GetMaxNumber(key) do -- remove extra operations tremove(operations) end for key2 in pairs(operations) do if key2 == "override" then -- ensure the override field is either true or nil operations.override = operations.override and true or nil elseif type(key2) ~= "number" or key2 <= 0 or key2 > #operations then -- this is an invalid key Log.Err("Removing invalid operations key (%s, %s): %s", groupPath, key, tostring(key2)) operations[key2] = nil end end for i = #operations, 1, -1 do if type(operations[i]) ~= "string" or operations[i] == "" or not TSM.Operations.Exists(key, operations[i]) then -- remove operations which no longer exist -- we used to have a bunch of placeholder "" operations, so don't log for those if operations[i] ~= "" then Log.Err("Removing invalid operation from group (%s): %s, %s", groupPath, key, tostring(operations[i])) end tremove(operations, i) end end elseif key ~= "ignoreItemVariations" then -- invalid key Log.Err("Removing invalid groupInfo key (%s): %s", groupPath, tostring(key)) groupInfo[key] = nil end end else -- remove invalid group paths Log.Err("Removing invalid group path: %s", tostring(groupPath)) TSM.db.profile.userData.groups[groupPath] = nil end end if not TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH] then -- set the override flag for all top-level groups and then create it for groupPath, moduleOperations in pairs(TSM.db.profile.userData.groups) do if not strfind(groupPath, TSM.CONST.GROUP_SEP) then for _, moduleName in TSM.Operations.ModuleIterator() do moduleOperations[moduleName].override = true end end end -- create the root group manually with default operations TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH] = {} for _, moduleName in TSM.Operations.ModuleIterator() do assert(TSM.Operations.Exists(moduleName, "#Default")) TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH][moduleName] = { "#Default", override = true } end end for _, groupPath in Groups.GroupIterator() do local parentPath = TSM.Groups.Path.GetParent(groupPath) if not TSM.db.profile.userData.groups[parentPath] then -- the parent group doesn't exist, so remove this group Log.Err("Removing group with non-existent parent: %s", tostring(groupPath)) TSM.db.profile.userData.groups[groupPath] = nil else for _, moduleName in TSM.Operations.ModuleIterator() do if not Groups.HasOperationOverride(groupPath, moduleName) then private.InheritParentOperations(groupPath, moduleName) end end end end -- fix up any invalid items local newPaths = TempTable.Acquire() for itemString, groupPath in pairs(TSM.db.profile.userData.items) do local newItemString = ItemString.Get(itemString) if not newItemString then -- this itemstring is invalid Log.Err("Itemstring (%s) is invalid", tostring(itemString)) TSM.db.profile.userData.items[itemString] = nil elseif groupPath == TSM.CONST.ROOT_GROUP_PATH or not TSM.db.profile.userData.groups[groupPath] then -- this group doesn't exist Log.Err("Group (%s) doesn't exist, so removing item (%s)", groupPath, itemString) TSM.db.profile.userData.items[itemString] = nil elseif newItemString ~= itemString then -- remove this invalid itemstring from this group Log.Err("Itemstring changed (%s -> %s), so removing it from group (%s)", itemString, newItemString, groupPath) TSM.db.profile.userData.items[itemString] = nil -- add this new item to this group if it's not already in one if not TSM.db.profile.userData.items[newItemString] then newPaths[newItemString] = groupPath Log.Err("Adding to group instead (%s)", groupPath) end end end for itemString, groupPath in pairs(newPaths) do TSM.db.profile.userData.items[itemString] = groupPath end TempTable.Release(newPaths) -- populate our database private.itemDB:TruncateAndBulkInsertStart() for itemString, groupPath in pairs(TSM.db.profile.userData.items) do private.itemDB:BulkInsertNewRow(itemString, groupPath) end private.itemDB:BulkInsertEnd() private.itemStringMap:SetCallbacksPaused(true) for key in private.itemStringMap:Iterator() do private.itemStringMap:ValueChanged(key) end private.RebuildDB() private.itemStringMap:SetCallbacksPaused(false) end function Groups.TranslateItemString(itemString) return private.itemStringMapReader[itemString] end function Groups.GetAutoBaseItemStringSmartMap() return private.itemStringMap end function Groups.GetItemDBForJoin() return private.itemDB end function Groups.CreateQuery() return private.db:NewQuery() :OrderBy("orderStr", true) end function Groups.Create(groupPath) private.CreateGroup(groupPath) private.RebuildDB() end function Groups.Move(groupPath, newGroupPath) assert(not TSM.db.profile.userData.groups[newGroupPath], "Target group already exists") assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH, "Can't move root group") assert(TSM.db.profile.userData.groups[groupPath], "Group doesn't exist") local newParentPath = TSM.Groups.Path.GetParent(newGroupPath) assert(newParentPath and TSM.db.profile.userData.groups[newParentPath], "Parent of target is invalid") local changes = TempTable.Acquire() private.itemDB:SetQueryUpdatesPaused(true) -- get a list of group path changes for this group and all its subgroups local gsubEscapedNewGroupPath = gsub(newGroupPath, "%%", "%%%%") for path in pairs(TSM.db.profile.userData.groups) do if path == groupPath or TSM.Groups.Path.IsChild(path, groupPath) then changes[path] = gsub(path, "^"..String.Escape(groupPath), gsubEscapedNewGroupPath) end end for oldPath, newPath in pairs(changes) do -- move the group assert(TSM.db.profile.userData.groups[oldPath] and not TSM.db.profile.userData.groups[newPath]) TSM.db.profile.userData.groups[newPath] = TSM.db.profile.userData.groups[oldPath] TSM.db.profile.userData.groups[oldPath] = nil -- move the items local query = private.itemDB:NewQuery() :Equal("groupPath", oldPath) for _, row in query:Iterator() do local itemString = row:GetField("itemString") assert(TSM.db.profile.userData.items[itemString]) TSM.db.profile.userData.items[itemString] = newPath row:SetField("groupPath", newPath) :Update() end query:Release() end -- update the operations all groups which were moved for _, moduleName in TSM.Operations.ModuleIterator() do if not Groups.HasOperationOverride(newGroupPath, moduleName) then private.InheritParentOperations(newGroupPath, moduleName) private.UpdateChildGroupOperations(newGroupPath, moduleName) end end TempTable.Release(changes) private.RebuildDB() private.itemDB:SetQueryUpdatesPaused(false) end function Groups.Delete(groupPath) assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH and TSM.db.profile.userData.groups[groupPath]) local parentPath = TSM.Groups.Path.GetParent(groupPath) assert(parentPath) if parentPath == TSM.CONST.ROOT_GROUP_PATH then parentPath = nil end -- delete this group and all subgroups for path in pairs(TSM.db.profile.userData.groups) do if path == groupPath or TSM.Groups.Path.IsChild(path, groupPath) then -- delete this group TSM.db.profile.userData.groups[path] = nil end end -- remove all items from our DB private.itemDB:SetQueryUpdatesPaused(true) local query = private.itemDB:NewQuery() :Or() :Equal("groupPath", groupPath) :Matches("groupPath", "^"..String.Escape(groupPath)..TSM.CONST.GROUP_SEP) :End() local updateMapItems = TempTable.Acquire() for _, row in query:Iterator() do local itemString = row:GetField("itemString") assert(TSM.db.profile.userData.items[itemString]) TSM.db.profile.userData.items[itemString] = nil private.itemDB:DeleteRow(row) updateMapItems[itemString] = true end query:Release() private.itemStringMap:SetCallbacksPaused(true) for itemString in private.itemStringMap:Iterator() do if updateMapItems[itemString] or updateMapItems[ItemString.GetBaseFast(itemString)] then -- either this item itself was removed from a group, or the base item was - in either case trigger an update private.itemStringMap:ValueChanged(itemString) end end private.itemStringMap:SetCallbacksPaused(false) TempTable.Release(updateMapItems) private.RebuildDB() private.itemDB:SetQueryUpdatesPaused(false) end function Groups.Exists(groupPath) return TSM.db.profile.userData.groups[groupPath] and true or false end function Groups.SetItemGroup(itemString, groupPath) assert(not groupPath or (groupPath ~= TSM.CONST.ROOT_GROUP_PATH and TSM.db.profile.userData.groups[groupPath])) local row = private.itemDB:GetUniqueRow("itemString", itemString) local updateMap = false if row then if groupPath then row:SetField("groupPath", groupPath) :Update() row:Release() else private.itemDB:DeleteRow(row) row:Release() -- we just removed an item from a group, so update the map updateMap = true end else assert(groupPath) private.itemDB:NewRow() :SetField("itemString", itemString) :SetField("groupPath", groupPath) :Create() -- we just added a new item to a group, so update the map updateMap = true end TSM.db.profile.userData.items[itemString] = groupPath if updateMap then private.itemStringMap:SetCallbacksPaused(true) private.itemStringMap:ValueChanged(itemString) if itemString == ItemString.GetBaseFast(itemString) then -- this is a base item string, so need to also update all other items whose base item is equal to this item for mapItemString in private.itemStringMap:Iterator() do if ItemString.GetBaseFast(mapItemString) == itemString then private.itemStringMap:ValueChanged(mapItemString) end end end private.itemStringMap:SetCallbacksPaused(false) end end function Groups.BulkCreateFromImport(groupName, items, groups, groupOperations, moveExistingItems) -- create all the groups assert(not TSM.db.profile.userData.groups[groupName]) for relGroupPath in pairs(groups) do local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath) if not TSM.db.profile.userData.groups[groupPath] then private.CreateGroup(groupPath) end end for relGroupPath, moduleOperations in pairs(groupOperations) do local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath) for moduleName, operations in pairs(moduleOperations) do if operations.override then TSM.db.profile.userData.groups[groupPath][moduleName] = operations private.UpdateChildGroupOperations(groupPath, moduleName) end end end local numItems = 0 for itemString, relGroupPath in pairs(items) do if moveExistingItems or not Groups.IsItemInGroup(itemString) then local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath) Groups.SetItemGroup(itemString, groupPath) numItems = numItems + 1 end end private.RebuildDB() return numItems end function Groups.GetPathByItem(itemString) itemString = TSM.Groups.TranslateItemString(itemString) assert(itemString) local groupPath = private.itemDB:GetUniqueRowField("itemString", itemString, "groupPath") or TSM.CONST.ROOT_GROUP_PATH assert(TSM.db.profile.userData.groups[groupPath]) return groupPath end function Groups.IsItemInGroup(itemString) return private.itemDB:HasUniqueRow("itemString", itemString) end function Groups.ItemByBaseItemStringIterator(baseItemString) private.baseItemStringItemIteratorQuery:BindParams(baseItemString) return private.baseItemStringItemIteratorQuery:Iterator() end function Groups.ItemIterator(groupPathFilter, includeSubGroups) assert(groupPathFilter ~= TSM.CONST.ROOT_GROUP_PATH) local query = private.itemDB:NewQuery() :Select("itemString", "groupPath") if groupPathFilter then if includeSubGroups then query:StartsWith("groupPath", groupPathFilter) query:Custom(private.GroupPathQueryFilter, groupPathFilter) else query:Equal("groupPath", groupPathFilter) end end return query:IteratorAndRelease() end function private.GroupPathQueryFilter(row, groupPathFilter) return row:GetField("groupPath") == groupPathFilter or strmatch(row:GetField("groupPath"), "^"..String.Escape(groupPathFilter)..TSM.CONST.GROUP_SEP) end function Groups.GetNumItems(groupPathFilter) assert(groupPathFilter ~= TSM.CONST.ROOT_GROUP_PATH) return private.itemDB:NewQuery() :Equal("groupPath", groupPathFilter) :CountAndRelease() end function Groups.GroupIterator() if #private.groupListCache == 0 then for groupPath in pairs(TSM.db.profile.userData.groups) do if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then tinsert(private.groupListCache, groupPath) end end Groups.SortGroupList(private.groupListCache) end return ipairs(private.groupListCache) end function Groups.SortGroupList(list) Table.Sort(list, private.GroupSortFunction) end function Groups.SetOperationOverride(groupPath, moduleName, override) assert(TSM.db.profile.userData.groups[groupPath]) assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH) if override == (TSM.db.profile.userData.groups[groupPath][moduleName].override and true or false) then return end if not override then TSM.db.profile.userData.groups[groupPath][moduleName].override = nil private.InheritParentOperations(groupPath, moduleName) private.UpdateChildGroupOperations(groupPath, moduleName) else wipe(TSM.db.profile.userData.groups[groupPath][moduleName]) TSM.db.profile.userData.groups[groupPath][moduleName].override = true private.UpdateChildGroupOperations(groupPath, moduleName) end private.RebuildDB() end function Groups.HasOperationOverride(groupPath, moduleName) return TSM.db.profile.userData.groups[groupPath][moduleName].override end function Groups.OperationIterator(groupPath, moduleName) return ipairs(TSM.db.profile.userData.groups[groupPath][moduleName]) end function Groups.AppendOperation(groupPath, moduleName, operationName) assert(TSM.Operations.Exists(moduleName, operationName)) local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName] assert(groupOperations.override and #groupOperations < TSM.Operations.GetMaxNumber(moduleName)) tinsert(groupOperations, operationName) private.UpdateChildGroupOperations(groupPath, moduleName) private.RebuildDB() end function Groups.RemoveOperation(groupPath, moduleName, operationIndex) local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName] assert(groupOperations.override and groupOperations[operationIndex]) tremove(groupOperations, operationIndex) private.UpdateChildGroupOperations(groupPath, moduleName) private.RebuildDB() end function Groups.RemoveOperationByName(groupPath, moduleName, operationName) local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName] assert(groupOperations.override) assert(Table.RemoveByValue(groupOperations, operationName) > 0) private.UpdateChildGroupOperations(groupPath, moduleName) private.RebuildDB() end function Groups.RemoveOperationFromAllGroups(moduleName, operationName) -- just blindly remove from all groups - no need to check for override Table.RemoveByValue(TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH][moduleName], operationName) for _, groupPath in Groups.GroupIterator() do Table.RemoveByValue(TSM.db.profile.userData.groups[groupPath][moduleName], operationName) end private.RebuildDB() end function Groups.SwapOperation(groupPath, moduleName, fromIndex, toIndex) local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName] groupOperations[fromIndex], groupOperations[toIndex] = groupOperations[toIndex], groupOperations[fromIndex] private.UpdateChildGroupOperations(groupPath, moduleName) end function Groups.OperationRenamed(moduleName, oldName, newName) -- just blindly rename in all groups - no need to check for override for _, info in pairs(TSM.db.profile.userData.groups) do for i = 1, #info[moduleName] do if info[moduleName][i] == oldName then info[moduleName][i] = newName end end end end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.RebuildDB() private.db:TruncateAndBulkInsertStart() for groupPath in pairs(TSM.db.profile.userData.groups) do local orderStr = gsub(groupPath, TSM.CONST.GROUP_SEP, "\001") orderStr = strlower(orderStr) local hasAuctioningOperation = false for _ in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do hasAuctioningOperation = true end local hasCraftingOperation = false for _ in TSM.Operations.GroupOperationIterator("Crafting", groupPath) do hasCraftingOperation = true end local hasMailingOperation = false for _ in TSM.Operations.GroupOperationIterator("Mailing", groupPath) do hasMailingOperation = true end local hasShoppingOperation = false for _ in TSM.Operations.GroupOperationIterator("Shopping", groupPath) do hasShoppingOperation = true end local hasSniperOperation = false for _ in TSM.Operations.GroupOperationIterator("Sniper", groupPath) do hasSniperOperation = true end local hasVendoringOperation = false for _ in TSM.Operations.GroupOperationIterator("Vendoring", groupPath) do hasVendoringOperation = true end local hasWarehousingOperation = false for _ in TSM.Operations.GroupOperationIterator("Warehousing", groupPath) do hasWarehousingOperation = true end private.db:BulkInsertNewRow(groupPath, orderStr, hasAuctioningOperation, hasCraftingOperation, hasMailingOperation, hasShoppingOperation, hasSniperOperation, hasVendoringOperation, hasWarehousingOperation) end private.db:BulkInsertEnd() wipe(private.groupListCache) end function private.CreateGroup(groupPath) assert(not TSM.db.profile.userData.groups[groupPath]) local parentPath = TSM.Groups.Path.GetParent(groupPath) assert(parentPath) if parentPath ~= TSM.CONST.ROOT_GROUP_PATH and not TSM.db.profile.userData.groups[parentPath] then -- recursively create the parent group first private.CreateGroup(parentPath) end TSM.db.profile.userData.groups[groupPath] = {} for _, moduleName in TSM.Operations.ModuleIterator() do TSM.db.profile.userData.groups[groupPath][moduleName] = {} -- assign all parent operations to this group for _, operationName in ipairs(TSM.db.profile.userData.groups[parentPath][moduleName]) do tinsert(TSM.db.profile.userData.groups[groupPath][moduleName], operationName) end end end function private.GroupSortFunction(a, b) return strlower(gsub(a, TSM.CONST.GROUP_SEP, "\001")) < strlower(gsub(b, TSM.CONST.GROUP_SEP, "\001")) end function private.InheritParentOperations(groupPath, moduleName) local parentGroupPath = TSM.Groups.Path.GetParent(groupPath) local override = TSM.db.profile.userData.groups[groupPath][moduleName].override wipe(TSM.db.profile.userData.groups[groupPath][moduleName]) TSM.db.profile.userData.groups[groupPath][moduleName].override = override for _, operationName in ipairs(TSM.db.profile.userData.groups[parentGroupPath][moduleName]) do tinsert(TSM.db.profile.userData.groups[groupPath][moduleName], operationName) end end function private.UpdateChildGroupOperations(groupPath, moduleName) for _, childGroupPath in Groups.GroupIterator() do if TSM.Groups.Path.IsChild(childGroupPath, groupPath) and not Groups.HasOperationOverride(childGroupPath, moduleName) then private.InheritParentOperations(childGroupPath, moduleName) end end end -- ============================================================================ -- Item String Smart Map -- ============================================================================ do private.itemStringMap = SmartMap.New("string", "string", function(itemString) if Groups.IsItemInGroup(itemString) then -- this item is in a group, so just return it return itemString end local baseItemString = ItemString.GetBaseFast(itemString) -- return the base item if it's in a group; otherwise return the original item return Groups.IsItemInGroup(baseItemString) and baseItemString or itemString end) end