diff --git a/esoui/ingame/lfg/zo_activityfinderroot_manager.lua b/esoui/ingame/lfg/zo_activityfinderroot_manager.lua
new file mode 100644
index 0000000..57178a6
--- /dev/null
+++ b/esoui/ingame/lfg/zo_activityfinderroot_manager.lua
@@ -0,0 +1,435 @@
+local GROUP_TYPE_TO_MAX_SIZE =
+{
+ [LFG_GROUP_TYPE_REGULAR] = SMALL_GROUP_SIZE_THRESHOLD,
+ [LFG_GROUP_TYPE_MEDIUM] = RAID_GROUP_SIZE_THRESHOLD,
+ [LFG_GROUP_TYPE_LARGE] = GROUP_SIZE_MAX,
+}
+
+local function LFGLevelSort(entry1, entry2)
+ if entry1.veteranRankMin ~= entry2.veteranRankMin then
+ return entry1.veteranRankMin < entry2.veteranRankMin
+ elseif entry1.levelMin ~= entry2.levelMin then
+ return entry1.levelMin < entry2.levelMin
+ elseif entry1.veteranRankMax ~= entry2.veteranRankMax then
+ return entry1.veteranRankMax < entry2.veteranRankMax
+ elseif entry1.levelMax ~= entry2.levelMax then
+ return entry1.levelMax < entry2.levelMax
+ else
+ return entry1.rawName < entry2.rawName
+ end
+end
+
+local function GetLFGEntryStringKeyboard(rawName, activityType)
+ if activityType == LFG_ACTIVITY_MASTER_DUNGEON then
+ return zo_iconTextFormat(GetVeteranRankIcon(), "100%", "100%", rawName)
+ else
+ return zo_strformat(SI_LFG_ACTIVITY_NAME, rawName)
+ end
+end
+
+local function GetLFGEntryStringGamepad(rawName, activityType)
+ if activityType == LFG_ACTIVITY_MASTER_DUNGEON then
+ return zo_strformat(GetString(SI_GAMEPAD_ACTIVITY_FINDER_VETERAN_LOCATION_FORMAT), rawName)
+ else
+ return zo_strformat(SI_LFG_ACTIVITY_NAME, rawName)
+ end
+end
+
+local function GetLevelRankRequirementText(levelMin, levelMax, rankMin, rankMax)
+ local playerRank = GetUnitVeteranRank("player")
+
+ if playerRank > 0 or levelMin == GetMaxLevel() then
+ if playerRank < rankMin then
+ return zo_strformat(SI_LFG_LOCK_REASON_PLAYER_MIN_RANK_REQUIREMENT, rankMin)
+ elseif playerRank > rankMax then
+ return zo_strformat(SI_LFG_LOCK_REASON_PLAYER_MAX_RANK_REQUIREMENT, rankMax)
+ end
+ else
+ local playerLevel = GetUnitLevel("player")
+
+ if playerLevel < levelMin then
+ return zo_strformat(SI_LFG_LOCK_REASON_PLAYER_MIN_LEVEL_REQUIREMENT, levelMin)
+ elseif playerLevel > levelMax then
+ return zo_strformat(SI_LFG_LOCK_REASON_PLAYER_MAX_LEVEL_REQUIREMENT, levelMax)
+ end
+ end
+end
+
+local function IsPreferredRoleSelected()
+ local isDPS, isHeal, isTank = GetPlayerRoles()
+ return isDPS or isHeal or isTank
+end
+
+local function CreateLocationData(activityType, lfgIndex)
+ local name, levelMin, levelMax, veteranRankMin, veteranRankMax, groupType, minNumMembers, description = GetLFGOption(activityType, lfgIndex)
+ local descriptionTextureSmallKeyboard, descriptionTextureLargeKeyboard = GetLFGOptionKeyboardDescriptionTextures(activityType, lfgIndex)
+ local descriptionTextureGamepad = GetLFGOptionGamepadDescriptionTexture(activityType, lfgIndex)
+ local requiredCollectible = GetRequiredLFGCollectibleId(activityType, lfgIndex)
+
+ return
+ {
+ activityType = activityType,
+ lfgIndex = lfgIndex,
+ nameKeyboard = GetLFGEntryStringKeyboard(name, activityType),
+ nameGamepad = GetLFGEntryStringGamepad(name, activityType),
+ rawName = name,
+ description = description,
+ descriptionTextureSmallKeyboard = descriptionTextureSmallKeyboard,
+ descriptionTextureLargeKeyboard = descriptionTextureLargeKeyboard,
+ descriptionTextureGamepad = descriptionTextureGamepad,
+ levelMin = levelMin,
+ levelMax = levelMax,
+ veteranRankMin = veteranRankMin,
+ veteranRankMax = veteranRankMax,
+ groupType = groupType,
+ minGroupSize = minNumMembers,
+ maxGroupSize = GetGroupSizeFromLFGGroupType(groupType),
+ requiredCollectible = requiredCollectible,
+ }
+end
+
+------------------
+--Initialization--
+------------------
+
+ActivityFinderRoot_Manager = ZO_CallbackObject:Subclass()
+
+function ActivityFinderRoot_Manager:New(...)
+ local singleton = ZO_CallbackObject.New(self)
+ singleton:Initialize(...)
+ return singleton
+end
+
+function ActivityFinderRoot_Manager:Initialize()
+ self.groupSize = 0
+ self:InitializeLocationData()
+ self:RegisterForEvents()
+end
+
+function ActivityFinderRoot_Manager:RegisterForEvents()
+ local function ClearSelections()
+ self:ClearSelections()
+ end
+
+ local function ClearAndUpdate()
+ self:ClearAndUpdate()
+ end
+
+ function UpdateGroupStatus()
+ self:UpdateGroupStatus()
+ end
+
+ function OnLevelUpdate(eventCode, unitTag)
+ if unitTag == "player" or ZO_Group_IsGroupUnitTag(unitTag) then
+ self:ClearAndUpdate()
+ self:FireCallbacks("OnLevelUpdate")
+ end
+ end
+
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_GROUPING_TOOLS_STATUS_UPDATE, function(eventCode, ...) self:OnGroupingToolsStatusUpdate(...) end)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_ACTIVITY_FINDER_COOLDOWNS_UPDATE, function() self:FireCallbacks("OnCooldownsUpdate") end)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_CURRENT_CAMPAIGN_CHANGED, ClearAndUpdate)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_COLLECTION_UPDATED, ClearAndUpdate)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_COLLECTIBLE_UPDATED, ClearAndUpdate)
+
+ --We should clear selections when switching filters, but we won't necessarily clear them when closing scenes
+ --However, we can't ensure that gamepad and keyboard will stay on the same filter, so we'll clear selections when switching between modes
+ --This won't require rechecking lock statuses
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_GAMEPAD_PREFERRED_MODE_CHANGED, ClearSelections)
+
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_LEVEL_UPDATE, OnLevelUpdate)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_VETERAN_RANK_UPDATE, OnLevelUpdate)
+
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_PLAYER_ACTIVATED, UpdateGroupStatus)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_GROUP_MEMBER_LEFT, UpdateGroupStatus)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_UNIT_CREATED, function(eventCode, unitTag)
+ if ZO_Group_IsGroupUnitTag(unitTag) then
+ self:UpdateGroupStatus()
+ end
+ end)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_GROUP_UPDATE, UpdateGroupStatus)
+ EVENT_MANAGER:RegisterForEvent("ActivityFinderRoot_Manager", EVENT_LEADER_UPDATE, UpdateGroupStatus)
+end
+
+function ActivityFinderRoot_Manager:InitializeLocationData()
+ local locationsLookupData = {}
+ local sortedLocationsData = {}
+ local randomActivityTypeGroupSizeRanges = {}
+
+ for activityType = LFG_ACTIVITY_MIN_VALUE, LFG_ACTIVITY_MAX_VALUE do
+ local numOptions = GetNumLFGOptions(activityType)
+ if numOptions > 0 then
+ local lookupActivityData = {}
+ local sortedActivityData = {}
+ local minGroupSize, maxGroupSize
+
+ for lfgIndex = 1, numOptions do
+ local data = CreateLocationData(activityType, lfgIndex)
+ table.insert(lookupActivityData, data)
+ table.insert(sortedActivityData, data)
+ if not minGroupSize or minGroupSize > data.minGroupSize then
+ minGroupSize = data.minGroupSize
+ end
+
+ if not maxGroupSize or maxGroupSize < data.maxGroupSize then
+ maxGroupSize = data.maxGroupSize
+ end
+ end
+
+ table.sort(sortedActivityData, LFGLevelSort)
+ locationsLookupData[activityType] = lookupActivityData
+ sortedLocationsData[activityType] = sortedActivityData
+ randomActivityTypeGroupSizeRanges[activityType] = { min = minGroupSize, max = maxGroupSize }
+ end
+ end
+
+ self.sortedLocationsData = sortedLocationsData
+ self.locationsLookupData = locationsLookupData
+ self.randomActivityTypeGroupSizeRanges = randomActivityTypeGroupSizeRanges
+ self.numSelected = 0
+ self.randomActivityTypeLockReasons = {}
+ self.randomActivitySelections = {}
+end
+
+-----------
+--Updates--
+-----------
+
+function ActivityFinderRoot_Manager:UpdateGroupStatus()
+ local wasGrouped = self.playerIsGrouped
+ local wasLeader = self.playerIsLeader
+ self.playerIsGrouped = IsUnitGrouped("player")
+ self.playerIsLeader = IsUnitGroupLeader("player")
+ self.groupSize = GetGroupSize()
+ local groupStateChanged = wasGrouped ~= self.playerIsGrouped or wasLeader ~= self.playerIsLeader
+ if groupStateChanged then
+ self:FireCallbacks("OnUpdateGroupStatus", wasGrouped, self.playerIsGrouped, wasLeader, self.playerIsLeader)
+ end
+ self:ClearAndUpdate()
+end
+
+function ActivityFinderRoot_Manager:GetGroupStatus()
+ return self.playerIsGrouped, self.playerIsLeader, self.groupSize
+end
+
+function ActivityFinderRoot_Manager:UpdateLocationData()
+ --Determine lock status for each location
+ local inAGroup = IsUnitGrouped("player")
+ local isLeader = IsUnitGroupLeader("player")
+ local isRoleSelected = IsPreferredRoleSelected()
+ ZO_ClearTable(self.randomActivityTypeLockReasons)
+
+ for activityType, locationsByActivity in pairs(self.locationsLookupData) do
+ if locationsByActivity then
+ local activityIsAvA = activityType == LFG_ACTIVITY_AVA
+ local anyEligible = false
+ local anyLockReason = nil
+
+ for index, location in ipairs(locationsByActivity) do
+ location.isLocked = true
+
+ if inAGroup and not isLeader then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_NOT_LEADER)
+ elseif not isRoleSelected then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_NO_ROLES_SELECTED)
+ elseif activityIsAvA and not IsInAvAZone() then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_NOT_IN_AVA)
+ elseif not activityIsAvA and IsInAvAZone() then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_IN_AVA)
+ elseif IsInAvAZone() and IsInLFGGroup() then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_AVA_CROSS_ALLIANCE)
+ else
+ location.playerMeetsLevelRequirements = DoesPlayerMeetLFGLevelRequirements(activityType, index)
+ location.groupMeetsLevelRequirements = DoesGroupMeetLFGLevelRequirements(activityType, index)
+
+ local groupTooLarge = self.groupSize > GROUP_TYPE_TO_MAX_SIZE[location.groupType]
+
+ if groupTooLarge then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_GROUP_TOO_LARGE)
+ elseif not location.playerMeetsLevelRequirements then
+ location.lockReasonText = GetLevelRankRequirementText(location.levelMin, location.levelMax, location.veteranRankMin, location.veteranRankMax)
+ elseif not location.groupMeetsLevelRequirements and inAGroup then
+ location.lockReasonText = GetString(SI_LFG_LOCK_REASON_GROUP_LOCATION_LEVEL_REQUIREMENTS)
+ elseif location.requiredCollectible ~= 0 and not IsCollectibleUnlocked(location.requiredCollectible) then
+ location.lockReasonText = zo_strformat(SI_LFG_LOCK_REASON_DLC_NOT_UNLOCKED, GetCollectibleName(location.requiredCollectible))
+ else
+ location.isLocked = false
+ location.lockReasonText = ""
+ anyEligible = true
+ end
+ end
+
+ if location.lockReasonText ~= "" then
+ anyLockReason = location.lockReasonText
+ end
+ end
+
+ if anyEligible then
+ self.randomActivityTypeLockReasons[activityType] = nil
+ else
+ self.randomActivityTypeLockReasons[activityType] = anyLockReason
+ end
+ end
+ end
+
+ self:FireCallbacks("OnUpdateLocationData")
+end
+
+function ActivityFinderRoot_Manager:ClearSelections()
+ for activityType, locationsByActivity in pairs(self.locationsLookupData) do
+ self.randomActivitySelections[activityType] = false
+
+ for index, location in ipairs(locationsByActivity) do
+ location.isSelected = false
+ end
+ end
+
+ self.numSelected = GetNumLFGRequests()
+
+ for i = 1, self.numSelected do
+ local activityType, index = GetLFGRequestInfo(i)
+ local location = self.locationsLookupData[activityType][index]
+ location.isSelected = true
+ end
+end
+
+function ActivityFinderRoot_Manager:ClearAndUpdate()
+ --A clear and update is required when the selections may change due to requirement changes. (Ex: a group member
+ --joins that doesn't meet the level requirement of a location already selected. The location needs to be unselected
+ --and locked, and then all other locations need to be refreshed again in case they are now unlocked. Instead of
+ --nesting refreshes, just clear and update when an event occurs that can lead to this.)
+
+ self:ClearSelections()
+ self:UpdateLocationData()
+end
+
+function ActivityFinderRoot_Manager:OnGroupingToolsStatusUpdate(isInQueue)
+ self.isInQueue = isInQueue
+ self:FireCallbacks("OnGroupingToolsStatusUpdate", isInQueue)
+end
+
+-------------
+--Accessors--
+-------------
+
+function ActivityFinderRoot_Manager:GetLocationsData(activityType)
+ if activityType then
+ return self.sortedLocationsData[activityType]
+ else
+ return self.sortedLocationsData
+ end
+end
+
+function ActivityFinderRoot_Manager:GetLocation(activityType, lfgIndex)
+ local locationsByActivity = self.locationsLookupData[activityType]
+ if locationsByActivity then
+ local location = locationsByActivity[lfgIndex]
+ if location then
+ return location
+ end
+ end
+ assert(false) --We should never be asking for a location using a bad activity or lfgIndex, fix the code that called this
+end
+
+function ActivityFinderRoot_Manager:GetIsCurrentlyInQueue()
+ return self.isInQueue
+end
+
+function ActivityFinderRoot_Manager:ToggleLocationSelected(location)
+ self:SetLocationSelected(location, not location.isSelected)
+end
+
+function ActivityFinderRoot_Manager:SetLocationSelected(location, selected)
+ if location.isLocked or IsCurrentlySearchingForGroup() or location.isSelected == selected then
+ return
+ end
+
+ location.isSelected = selected
+ local delta = location.isSelected and 1 or -1
+ self.numSelected = self.numSelected + delta
+ self:FireCallbacks("OnSelectionsChanged")
+end
+
+function ActivityFinderRoot_Manager:ToggleActivityTypeSelected(activityType)
+ self:SetActivityTypeSelected(activityType, not self.randomActivitySelections[activityType])
+end
+
+function ActivityFinderRoot_Manager:SetActivityTypeSelected(activityType, selected)
+ if not self:CanChooseRandomForActivityType(activityType) or IsCurrentlySearchingForGroup() or self.randomActivitySelections[activityType] == selected then
+ return
+ end
+
+ self.randomActivitySelections[activityType] = selected
+ local delta = selected and 1 or -1
+ self.numSelected = self.numSelected + delta
+ self:FireCallbacks("OnSelectionsChanged")
+end
+
+function ActivityFinderRoot_Manager:IsActivityTypeSelected(activityType)
+ return self.randomActivitySelections[activityType]
+end
+
+function ActivityFinderRoot_Manager:IsAnyLocationSelected()
+ return self.numSelected > 0
+end
+
+function ActivityFinderRoot_Manager:CanChooseRandomForActivityType(activityType)
+ return self.randomActivityTypeLockReasons[activityType] == nil
+end
+
+function ActivityFinderRoot_Manager:GetLockReasonForActivityType(activityType)
+ return self.randomActivityTypeLockReasons[activityType]
+end
+
+function ActivityFinderRoot_Manager:GetGroupSizeRangeForActivityType(activityType)
+ local groupSizeRangeTable = self.randomActivityTypeGroupSizeRanges[activityType]
+ return groupSizeRangeTable.min, groupSizeRangeTable.max
+end
+
+do
+ local function IsRoleSelected(roles)
+ return roles[LFG_ROLE_DPS] or roles[LFG_ROLE_HEAL] or roles[LFG_ROLE_TANK]
+ end
+
+ function ActivityFinderRoot_Manager:StartSearch()
+ if IsCurrentlySearchingForGroup() then
+ return
+ end
+
+ ClearGroupFinderSearch()
+
+ local roles = PREFERRED_ROLES:GetRoles()
+ if not IsRoleSelected(roles) then
+ ZO_AlertEvent(EVENT_ACTIVITY_QUEUE_RESULT, ACTIVITY_QUEUE_RESULT_NO_ROLES_SELECTED)
+ return
+ end
+
+ --Add locations
+ for activityType, locationsByActivity in pairs(self.locationsLookupData) do
+ if self.randomActivitySelections[activityType] then
+ AddGroupFinderSearchEntry(activityType)
+ end
+
+ for index, location in ipairs(locationsByActivity) do
+ if location.isSelected then
+ AddGroupFinderSearchEntry(activityType, index)
+ end
+ end
+ end
+
+ local result = StartGroupFinderSearch()
+ if result ~= ACTIVITY_QUEUE_RESULT_SUCCESS then
+ ZO_AlertEvent(EVENT_ACTIVITY_QUEUE_RESULT, result)
+ end
+ end
+end
+
+function ActivityFinderRoot_Manager:HandleLFMPromptResponse(accept)
+ --Any response should clear the prompt, but only acceptance sends the request
+ if accept then
+ SendLFMRequest()
+ end
+ self:FireCallbacks("OnHandleLFMPromptResponse")
+end
+
+ ZO_ACTIVITY_FINDER_ROOT_MANAGER = ActivityFinderRoot_Manager:New()
\ No newline at end of file