-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local ImportExport = TSM.Groups:NewPackage("ImportExport") local L = TSM.Include("Locale").GetTable() local TempTable = TSM.Include("Util.TempTable") local Table = TSM.Include("Util.Table") local Log = TSM.Include("Util.Log") local String = TSM.Include("Util.String") local ItemString = TSM.Include("Util.ItemString") local CustomPrice = TSM.Include("Service.CustomPrice") local AceSerializer = LibStub("AceSerializer-3.0") local LibDeflate = LibStub("LibDeflate") local LibSerialize = LibStub("LibSerialize") local private = { isOperationSettingsTable = {}, importContext = { groupName = nil, items = nil, groups = nil, groupOperations = nil, operations = nil, customSources = nil, numChangedOperations = nil, filteredGroups = {}, }, } local MAGIC_STR = "TSM_EXPORT" local VERSION = 1 local EXPORT_OPERATION_MODULES = { Auctioning = true, Crafting = true, Shopping = true, Sniper = true, Vendoring = true, Warehousing = true, } local EXPORT_CUSTOM_STRINGS = { Auctioning = { postCap = true, keepQuantity = true, maxExpires = true, undercut = true, minPrice = true, maxPrice = true, normalPrice = true, cancelRepostThreshold = true, stackSize = TSM.IsWowClassic() or nil, }, Crafting = { minRestock = true, maxRestock = true, minProfit = true, craftPriceMethod = true, }, Shopping = { restockQuantity = true, maxPrice = true, }, Sniper = { belowPrice = true, }, Vendoring = { vsMarketValue = true, vsMaxMarketValue = true, vsDestroyValue = true, vsMaxDestroyValue = true, }, Warehousing = {}, } local SERIALIZE_OPTIONS = { stable = true, filter = function(tbl, key, value) return not private.isOperationSettingsTable[tbl] or not TSM.Operations.IsCommonKey(key) end, } -- ============================================================================ -- Module Functions -- ============================================================================ function ImportExport.GenerateExport(exportGroupPath, includeSubGroups, excludeOperations, excludeCustomSources) assert(exportGroupPath ~= TSM.CONST.ROOT_GROUP_PATH) local exportGroupName = TSM.Groups.Path.GetName(exportGroupPath) -- collect the items and sub groups local items = TempTable.Acquire() local groups = TempTable.Acquire() local groupOperations = TempTable.Acquire() local operations = TempTable.Acquire() local customSources = TempTable.Acquire() for moduleName in pairs(EXPORT_OPERATION_MODULES) do operations[moduleName] = {} end assert(not next(private.isOperationSettingsTable)) for _, groupPath in TSM.Groups.GroupIterator() do local relGroupPath = nil if TSM.Groups.Path.IsChild(groupPath, exportGroupPath) then relGroupPath = TSM.Groups.Path.GetRelative(groupPath, exportGroupPath) if not includeSubGroups[relGroupPath] then relGroupPath = nil end elseif groupPath == exportGroupPath then relGroupPath = TSM.CONST.ROOT_GROUP_PATH end if relGroupPath then groups[relGroupPath] = true for _, itemString in TSM.Groups.ItemIterator(groupPath) do items[itemString] = relGroupPath end if not excludeOperations then groupOperations[relGroupPath] = {} for moduleName in pairs(EXPORT_OPERATION_MODULES) do groupOperations[relGroupPath][moduleName] = { -- always override at the top-level override = TSM.Groups.HasOperationOverride(groupPath, moduleName) or groupPath == exportGroupPath or nil, } for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator(moduleName, groupPath) do tinsert(groupOperations[relGroupPath][moduleName], operationName) operations[moduleName][operationName] = operationSettings private.isOperationSettingsTable[operationSettings] = true if not excludeCustomSources then for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do private.GetCustomSources(operationSettings[key], customSources) end end end end end end end local serialized = LibSerialize:SerializeEx(SERIALIZE_OPTIONS, MAGIC_STR, VERSION, exportGroupName, items, groups, groupOperations, operations, customSources) local compressed = LibDeflate:EncodeForPrint(LibDeflate:CompressDeflate(serialized)) local numItems = Table.Count(items) local numSubGroups = Table.Count(groups) - 1 local numOperations = 0 for _, moduleOperations in pairs(operations) do numOperations = numOperations + Table.Count(moduleOperations) end local numCustomSources = Table.Count(customSources) wipe(private.isOperationSettingsTable) TempTable.Release(customSources) TempTable.Release(operations) TempTable.Release(groupOperations) TempTable.Release(groups) TempTable.Release(items) return compressed, numItems, numSubGroups, numOperations, numCustomSources end function ImportExport.ProcessImport(str) return private.DecodeNewImport(str) or private.DecodeOldImport(str) or private.DecodeOldGroupOrItemListImport(str) end function ImportExport.GetImportTotals() local numExistingItems = 0 for itemString, groupPath in pairs(private.importContext.items) do if not private.importContext.filteredGroups[groupPath] then if TSM.Groups.IsItemInGroup(itemString) then numExistingItems = numExistingItems + 1 end end end wipe(private.importContext.customSources) local numOperations, numExistingOperations = 0, 0 for moduleName, moduleOperations in pairs(private.importContext.operations) do local usedOperations = TempTable.Acquire() for groupPath, operations in pairs(private.importContext.groupOperations) do if not private.importContext.filteredGroups[groupPath] then for _, operationName in ipairs(operations[moduleName]) do usedOperations[operationName] = true end end end for operationName, operationSettings in pairs(moduleOperations) do if usedOperations[operationName] then numOperations = numOperations + 1 if TSM.Operations.Exists(moduleName, operationName) then numExistingOperations = numExistingOperations + 1 end for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do private.GetCustomSources(operationSettings[key], private.importContext.customSources) end end end TempTable.Release(usedOperations) end local numExistingCustomSources = 0 for name in pairs(private.importContext.customSources) do if TSM.db.global.userData.customPriceSources[name] then numExistingCustomSources = numExistingCustomSources + 1 end end local numItems = 0 for _, groupPath in pairs(private.importContext.items) do if not private.importContext.filteredGroups[groupPath] then numItems = numItems + 1 end end local numGroups = 0 for groupPath in pairs(private.importContext.groups) do if not private.importContext.filteredGroups[groupPath] then numGroups = numGroups + 1 end end return numItems, numGroups, numExistingItems, numOperations, numExistingOperations, numExistingCustomSources end function ImportExport.PendingImportGroupIterator() assert(private.importContext.groupName) return pairs(private.importContext.groups) end function ImportExport.GetPendingImportGroupName() assert(private.importContext.groupName) return private.importContext.groupName end function ImportExport.SetGroupFiltered(groupPath, isFiltered) private.importContext.filteredGroups[groupPath] = isFiltered or nil end function ImportExport.CommitImport(moveExistingItems, includeOperations, replaceOperations) assert(private.importContext.groupName) local numOperations, numCustomSources = 0, 0 if includeOperations and next(private.importContext.operations) then -- remove filtered operations for moduleName, moduleOperations in pairs(private.importContext.operations) do local usedOperations = TempTable.Acquire() for groupPath, operations in pairs(private.importContext.groupOperations) do if not private.importContext.filteredGroups[groupPath] then for _, operationName in ipairs(operations[moduleName]) do usedOperations[operationName] = true end end end for operationName in pairs(moduleOperations) do if not usedOperations[operationName] then moduleOperations[operationName] = nil end end TempTable.Release(usedOperations) end if not replaceOperations then -- remove existing operations and custom sources from the import context for moduleName, moduleOperations in pairs(private.importContext.operations) do for operationName in pairs(moduleOperations) do if TSM.Operations.Exists(moduleName, operationName) then moduleOperations[operationName] = nil end end if not next(moduleOperations) then private.importContext.operations[moduleName] = nil end end for name in pairs(private.importContext.customSources) do if TSM.db.global.userData.customPriceSources[name] then private.importContext.customSources[name] = nil end end end if next(private.importContext.customSources) then -- regenerate the list of custom sources in case some operations were filtered out wipe(private.importContext.customSources) for moduleName, moduleOperations in pairs(private.importContext.operations) do for _, operationSettings in pairs(moduleOperations) do for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do private.GetCustomSources(operationSettings[key], private.importContext.customSources) end end end -- create the custom sources numCustomSources = Table.Count(private.importContext.customSources) CustomPrice.BulkCreateCustomPriceSourcesFromImport(private.importContext.customSources, replaceOperations) end -- create the operations for _, moduleOperations in pairs(private.importContext.operations) do numOperations = numOperations + Table.Count(moduleOperations) end TSM.Operations.BulkCreateFromImport(private.importContext.operations, replaceOperations) end if not includeOperations then wipe(private.importContext.groupOperations) end -- filter the groups for groupPath in pairs(private.importContext.filteredGroups) do private.importContext.groups[groupPath] = nil private.importContext.groupOperations[groupPath] = nil end for itemString, groupPath in pairs(private.importContext.items) do if private.importContext.filteredGroups[groupPath] then private.importContext.items[itemString] = nil end end -- create the groups local numItems = TSM.Groups.BulkCreateFromImport(private.importContext.groupName, private.importContext.items, private.importContext.groups, private.importContext.groupOperations, moveExistingItems) -- print the message Log.PrintfUser(L["Imported group (%s) with %d items, %d operations, and %d custom sources."], private.importContext.groupName, numItems, numOperations, numCustomSources) ImportExport.ClearImportContext() end function ImportExport.ClearImportContext() private.importContext.groupName = nil private.importContext.items = nil private.importContext.groups = nil private.importContext.groupOperations = nil private.importContext.operations = nil private.importContext.customSources = nil wipe(private.importContext.filteredGroups) end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.GetCustomSources(str, result) for _, name, customSourceStr in CustomPrice.DependantCustomSourceIterator(str) do if not result[name] then result[name] = customSourceStr private.GetCustomSources(customSourceStr, result) end end end function private.DecodeNewImport(str) -- decode and decompress (if it's not a new import, the decode should fail) str = LibDeflate:DecodeForPrint(str) if not str then Log.Info("Not a valid new import string") return false end local numExtraBytes = nil str, numExtraBytes = LibDeflate:DecompressDeflate(str) if not str then Log.Err("Failed to decompress new import string") return false elseif numExtraBytes > 0 then Log.Err("Import string had extra bytes") return false end -- deserialize and validate the data local success, magicStr, version, groupName, items, groups, groupOperations, operations, customSources = LibSerialize:Deserialize(str) if not success then Log.Err("Failed to deserialize new import string") return false elseif magicStr ~= MAGIC_STR then Log.Err("Invalid magic string: "..tostring(magicStr)) return false elseif version ~= VERSION then Log.Err("Invalid version: "..tostring(version)) return false elseif type(groupName) ~= "string" or groupName == "" or strmatch(groupName, TSM.CONST.GROUP_SEP) then Log.Err("Invalid groupName: "..tostring(groupName)) return false elseif type(items) ~= "table" then Log.Err("Invalid items type: "..tostring(items)) return false elseif type(groups) ~= "table" then Log.Err("Invalid groups type: "..tostring(groups)) return false elseif type(groupOperations) ~= "table" then Log.Err("Invalid groupOperations type: "..tostring(groupOperations)) return false elseif type(operations) ~= "table" then Log.Err("Invalid operations type: "..tostring(operations)) return false elseif type(customSources) ~= "table" then Log.Err("Invalid customSources type: "..tostring(customSources)) return false end -- validate the groups table for groupPath, trueValue in pairs(groups) do if not private.IsValidGroupPath(groupPath) then Log.Err("Invalid groupPath (%s)", tostring(groupPath)) return false elseif trueValue ~= true then Log.Err("Invalid true value (%s)", tostring(trueValue)) return false end end for groupPath in pairs(groups) do local parentPath = TSM.Groups.Path.Split(groupPath) while parentPath do if not groups[parentPath] then Log.Err("Orphaned group (%s)", groupPath) return false end parentPath = TSM.Groups.Path.Split(parentPath) end end -- validate the items table local numInvalidItems = 0 for itemString, groupPath in pairs(items) do if not private.IsValidGroupPath(groupPath) then Log.Err("Invalid groupPath (%s, %s)", tostring(itemString), tostring(groupPath)) return false elseif not groups[groupPath] then Log.Err("Invalid item group (%s, %s)", itemString, groupPath) return false end local newItemString = type(itemString) == "string" and ItemString.Get(itemString) or nil if itemString ~= newItemString then -- just remove this one item and continue Log.Warn("Invalid itemString (%s, %s)", tostring(itemString), tostring(newItemString)) items[itemString] = nil numInvalidItems = numInvalidItems + 1 end end if not next(items) and numInvalidItems > 0 then Log.Err("All items were invalid") return false end -- validate the customSources table for name, customSourceStr in pairs(customSources) do if type(name) ~= "string" or name == "" or gsub(name, "([a-z]+)", "") ~= "" then Log.Err("Invalid name (%s)", tostring(name)) return false elseif type(str) ~= "string" then Log.Err("Invalid str (%s)", tostring(customSourceStr)) return false end end -- validate the operations table local numChangedOperations = private.ValidateOperationsTable(operations, true) if not numChangedOperations then return false end -- validate the groupOperations table if not private.ValidateGroupOperationsTable(groupOperations, groups, operations, true) then return false end if numInvalidItems > 0 then Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems) end if numChangedOperations > 0 then Log.PrintfUser(L["NOTE: The import contained %d operations with at least one invalid setting which was reset."], numChangedOperations) end Log.Info("Decoded new import string") private.importContext.groupName = private.DedupImportGroupName(groupName) private.importContext.items = items private.importContext.groups = groups private.importContext.groupOperations = groupOperations private.importContext.operations = operations private.importContext.customSources = customSources return true end function private.DecodeOldImport(str) if strsub(str, 1, 1) ~= "^" then Log.Info("Not an old import string") return false end local isValid, data = AceSerializer:Deserialize(str) if not isValid then Log.Err("Failed to deserialize") return false elseif type(data) ~= "table" then Log.Err("Invalid data type (%s)", tostring(data)) return false elseif data.operations ~= nil and type(data.operations) ~= "table" then Log.Err("Invalid operations type (%s)", tostring(data.operations)) return false elseif data.groupExport ~= nil and type(data.groupExport) ~= "string" then Log.Err("Invalid groupExport type (%s)", tostring(data.groupExport)) return false elseif data.groupOperations ~= nil and type(data.groupOperations) ~= "table" then Log.Err("Invalid groupOperations type (%s)", tostring(data.groupOperations)) return false elseif not data.operations and not data.groupExport then Log.Err("Doesn't contain operations or groupExport") return false end local operations, numChangedOperations = nil, 0 if data.operations then numChangedOperations = private.ValidateOperationsTable(data.operations, false) if not numChangedOperations then return false end operations = data.operations else operations = {} end local items, groups, numInvalidItems = nil, nil, nil if data.groupExport then items, groups, numInvalidItems = private.DecodeGroupExportHelper(data.groupExport) if not items then Log.Err("No items found") return false end else items = {} groups = {} numInvalidItems = 0 end local groupOperations = nil if data.groupOperations then Log.Info("Parsing group operations") local changeGroupPaths = TempTable.Acquire() for groupPath in pairs(data.groupOperations) do -- We export a "," in a group path as "``" local newGroupPath = type(groupPath) == "string" and gsub(groupPath, "``", ",") if newGroupPath and newGroupPath ~= groupPath then changeGroupPaths[groupPath] = newGroupPath if data.groupOperations[newGroupPath] then Log.Err("Duplicated group operations (%s, %s)", tostring(groupPath), tostring(newGroupPath)) return false end end end for groupPath, newGroupPath in pairs(changeGroupPaths) do data.groupOperations[newGroupPath] = data.groupOperations[groupPath] data.groupOperations[groupPath] = nil end TempTable.Release(changeGroupPaths) if not private.ValidateGroupOperationsTable(data.groupOperations, groups, operations, false) then Log.Err("Invalid group operations") return false end groupOperations = data.groupOperations else groupOperations = {} end -- check if there's a common top-level group within the import local commonTopLevelGroup = private.GetCommonTopLevelGroup(items, groups, groupOperations) if commonTopLevelGroup then private.UpdateTopLevelGroup(commonTopLevelGroup, items, groups, groupOperations) end if numInvalidItems > 0 then Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems) end if numChangedOperations > 0 then Log.PrintfUser(L["NOTE: The import contained %d operations with at least one invalid setting which was reset."], numChangedOperations) end Log.Info("Decoded old import string") private.importContext.groupName = private.DedupImportGroupName(commonTopLevelGroup or L["Imported Group"]) private.importContext.items = items private.importContext.groups = groups private.importContext.groupOperations = groupOperations private.importContext.operations = operations private.importContext.customSources = {} return true end function private.DecodeOldGroupOrItemListImport(str) local items, groups, numInvalidItems = private.DecodeGroupExportHelper(str) if not items then Log.Err("No items found") return false end local groupOperations = {} -- check if there's a common top-level group within the import local commonTopLevelGroup = private.GetCommonTopLevelGroup(items, groups, groupOperations) if commonTopLevelGroup then private.UpdateTopLevelGroup(commonTopLevelGroup, items, groups, groupOperations) end if numInvalidItems > 0 then Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems) end Log.Info("Decoded old group or item list") private.importContext.groupName = private.DedupImportGroupName(commonTopLevelGroup or L["Imported Group"]) private.importContext.items = items private.importContext.groups = groups private.importContext.groupOperations = groupOperations private.importContext.operations = {} private.importContext.customSources = {} return true end function private.DecodeGroupExportHelper(str) local items, groups, numInvalidItems = nil, nil, 0 if strmatch(str, "^[ip0-9%-:;]+$") then -- this is likely a list of itemStrings separated by semicolons instead of commas, so attempt to fix it str = gsub(str, ";", ",") end if strmatch(str, "^[0-9,]+$") then -- this is likely a list of itemIds separated by commas, so attempt to fix it str = gsub(str, "[0-9]+", "i:%1") end local relativePath = TSM.CONST.ROOT_GROUP_PATH for part in String.SplitIterator(str, ",") do part = strtrim(part) local groupPath = strmatch(part, "^group:(.+)$") local itemString = strmatch(part, "^[ip]?:?[0-9%-:]+$") local newItemString = itemString and ItemString.Get(itemString) or nil if newItemString and newItemString ~= itemString then itemString = newItemString numInvalidItems = numInvalidItems + 1 end assert(not groupPath or not itemString) if groupPath then -- We export a "," in a group path as "``" groupPath = gsub(groupPath, "``", ",") if not private.IsValidGroupPath(groupPath) then Log.Err("Invalid groupPath (%s)", tostring(groupPath)) return end relativePath = groupPath groups = groups or {} -- create the groups all the way up to the root while groupPath do groups[groupPath] = true groupPath = TSM.Groups.Path.GetParent(groupPath) end elseif itemString then items = items or {} groups = groups or {} groups[relativePath] = true items[itemString] = relativePath else Log.Err("Unknown part: %s", part) return end end return items, groups, numInvalidItems end function private.ValidateOperationsTable(operations, strict) local numChangedOperations = 0 for moduleName, moduleOperations in pairs(operations) do local isInvalidModuleName, isNotExportOperationModule = private.IsValidOperationModule(moduleName) if not isInvalidModuleName then Log.Err("Invalid module name") return nil elseif isNotExportOperationModule then if strict then Log.Err("Invalid moduleName (%s)", tostring(moduleName)) return nil else Log.Warn("Ignoring module (%s)", moduleName) operations[moduleName] = nil wipe(moduleOperations) end elseif type(moduleOperations) ~= "table" then Log.Err("Invalid moduleOperations type (%s)", tostring(moduleOperations)) return nil end for operationName, operationSettings in pairs(moduleOperations) do if type(operationName) ~= "string" or not TSM.Operations.IsValidName(operationName) then Log.Err("Invalid operationName (%s)", tostring(operationName)) return nil elseif type(operationSettings) ~= "table" then Log.Err("Invalid operationSettings type (%s)", tostring(operationSettings)) return nil end -- sanitize the operation settings if TSM.Operations.SanitizeSettings(moduleName, operationName, operationSettings, true, true) then numChangedOperations = numChangedOperations + 1 end end end return numChangedOperations end function private.ValidateGroupOperationsTable(groupOperations, groups, operations, strict) for groupPath, groupsOperationsTable in pairs(groupOperations) do if not private.IsValidGroupPath(groupPath) then Log.Err("Invalid groupPath (%s)", tostring(groupPath)) return false elseif not groups[groupPath] then if strict then Log.Err("Invalid group (%s)", groupPath) return false else Log.Info("Creating group with operations (%s)", groupPath) groups[groupPath] = true end end if not strict then groupsOperationsTable.ignoreItemVariations = nil end for moduleName, moduleOperations in pairs(groupsOperationsTable) do local isInvalidModuleName, isNotExportOperationModule = private.IsValidOperationModule(moduleName) if not isInvalidModuleName then Log.Err("Invalid module name") return false elseif isNotExportOperationModule then if strict then Log.Err("Invalid moduleName (%s)", tostring(moduleName)) return false else Log.Warn("Ignoring module (%s)", moduleName) groupsOperationsTable[moduleName] = nil wipe(moduleOperations) end elseif type(moduleOperations) ~= "table" then Log.Err("Invalid moduleOperations type (%s)", tostring(moduleOperations)) return false elseif moduleOperations.override ~= nil and moduleOperations.override ~= true then Log.Err("Invalid moduleOperations override type (%s)", tostring(moduleOperations.override)) return false elseif groupPath == TSM.CONST.ROOT_GROUP_PATH and not moduleOperations.override then if strict then Log.Err("Top-level group does not have override set") return false else Log.Info("Setting override for top-level group") moduleOperations.override = true end end local numOperations = #moduleOperations if numOperations > TSM.Operations.GetMaxNumber(moduleName) then Log.Err("Too many operations (%s, %s, %d)", groupPath, moduleName, numOperations) return false end for k, v in pairs(moduleOperations) do if k == "override" then -- pass elseif type(k) ~= "number" or k < 1 or k > numOperations then Log.Err("Unknown key (%s, %s, %s, %s)", groupPath, moduleName, tostring(k), tostring(v)) return false elseif type(v) ~= "string" then Log.Err("Invalid value (%s, %s, %s, %s)", groupPath, moduleName, k, tostring(v)) return false end end -- some old imports had "" operations attached to groups, so remove them for i = #moduleOperations, 1, -1 do if moduleOperations[i] == "" then tremove(moduleOperations, i) end end for _, operationName in ipairs(moduleOperations) do if type(operationName) ~= "string" or not TSM.Operations.IsValidName(operationName) then Log.Err("Invalid operationName (%s)", tostring(operationName)) return false elseif not operations[moduleName][operationName] then Log.Err("Unknown operation (%s)", operationName) return false end end end end return true end function private.DedupImportGroupName(groupName) if TSM.Groups.Exists(groupName) then local num = 1 while TSM.Groups.Exists(groupName.." "..num) do num = num + 1 end groupName = groupName.." "..num end return groupName end function private.IsValidGroupPath(groupPath) return type(groupPath) == "string" and not strmatch(groupPath, "^`") and not strmatch(groupPath, "`$") and not strmatch(groupPath, "``") end function private.IsValidOperationModule(moduleName) if type(moduleName) ~= "string" then Log.Err("Invalid moduleName (%s)", tostring(moduleName)) return false elseif not TSM.Operations.ModuleExists(moduleName) then Log.Err("Invalid moduleName (%s)", tostring(moduleName)) return false elseif not EXPORT_OPERATION_MODULES[moduleName] then return true, true end return true end function private.GetCommonTopLevelGroup(items, groups, groupOperations) local commonTopLevelGroup = nil -- check the items for _, groupPath in pairs(items) do if groupPath == TSM.CONST.ROOT_GROUP_PATH then return nil end local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath) if not commonTopLevelGroup then commonTopLevelGroup = topLevelGroup elseif topLevelGroup ~= commonTopLevelGroup then return nil end end -- check the groups for groupPath in pairs(groups) do if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath) if not commonTopLevelGroup then commonTopLevelGroup = topLevelGroup elseif topLevelGroup ~= commonTopLevelGroup then return nil end end end -- check the groupOperations for groupPath in pairs(groupOperations) do if groupPath == TSM.CONST.ROOT_GROUP_PATH then return nil end local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath) if not commonTopLevelGroup then commonTopLevelGroup = topLevelGroup elseif topLevelGroup ~= commonTopLevelGroup then return nil end end return commonTopLevelGroup end function private.UpdateTopLevelGroup(topLevelGroup, items, groups, groupOperations) -- update items for itemString, groupPath in pairs(items) do items[itemString] = TSM.Groups.Path.GetRelative(groupPath, topLevelGroup) end -- update groups local newGroups = TempTable.Acquire() groups[TSM.CONST.ROOT_GROUP_PATH] = nil for groupPath in pairs(groups) do newGroups[TSM.Groups.Path.GetRelative(groupPath, topLevelGroup)] = true end wipe(groups) for groupPath in pairs(newGroups) do groups[groupPath] = true end TempTable.Release(newGroups) -- update groupOperations local newGroupOperations = TempTable.Acquire() for groupPath, groupOperationsTable in pairs(groupOperations) do newGroupOperations[TSM.Groups.Path.GetRelative(groupPath, topLevelGroup)] = groupOperationsTable end wipe(groupOperations) for groupPath, groupOperationsTable in pairs(newGroupOperations) do groupOperations[groupPath] = groupOperationsTable end TempTable.Release(newGroupOperations) -- set override on new top-level group if groupOperations[TSM.CONST.ROOT_GROUP_PATH] then for _, moduleOperations in pairs(groupOperations[TSM.CONST.ROOT_GROUP_PATH]) do moduleOperations.override = true end end end