local _,rematch = ... local L = rematch.localization local C = rematch.constants local settings = rematch.settings rematch.queue = {} --[[ The queue can process (update queue and change pets in leveling slots) in many scenarios: - changing queue sort - toggling preferences - changing preferences from CurrentPreferences dialog - saving a team with SavedTeam dialog - slotting a pet (could be leveling pet or moving leveling slot) - loading team - adding a pet to the queue - removing a pet from the queue - leaving pet battle - releasing a pet - caging a pet - logging in - using a rarity stone - using a leveling stone In any of these scenarios it should call: rematch.queue:Process() This will do a 0-frame wait (in case multiple of the above happen in the same frame) and then call startProcess() which will update the queue (apply preferences, kick out level 25 pets, sort if active sort enabled), and then call runProcess() to slot any pets in the queue into the respecting leveling slots if they're not already slotted. If a pet from the queue is slotted, it will call itself after a short (0.25 second) interval to confirm pets loaded, repeating until all expected pets are loaded. ]] local queueLookup = {} -- indexed by petID, the index into settings.LevelingQueue for the petID local timeout = 0 -- after C.QUEUE_PROCESS_TIMEOUT tries, queue gives up slotting leveling pets local blingPetID -- at the end of a process, scroll to and bling the petID in the queue local topPicks = {} -- top three pets in the queue to be chosen for a leveling slot local excludePetIDs = {} -- in the event the queue needs rebuilt, petIDs already in the queue are added here rematch.events:Register(rematch.queue,"PLAYER_LOGIN",function(self) -- if pets changed level or rarity then queue may be changing (can't limit to ActiveSort due to a pet reaching 25/out of preferences) rematch.events:Register(self,"REMATCH_PETS_CHANGED",self.Process) -- after pets loaded on login, remove any invalid pets (caged or released while addon disabled or potentially a petID reassignment) rematch.events:Register(self,"REMATCH_PETS_LOADED",self.REMATCH_PETS_LOADED) rematch.events:Register(self,"PET_JOURNAL_PET_DELETED",self.PET_JOURNAL_PET_DELETED) rematch.events:Register(self,"NEW_PET_ADDED",self.NEW_PET_ADDED) end) -- updates queue including lookup for indexes, removing pets that are 25/no longer exist, applying preferences and sorting if active sort enabled function rematch.queue:Update(teamID) if not rematch.main:IsPlayerInWorld() then return -- player is in a loading screen, leave end local preferences = rematch.preferences:GetCurrentPreferences(teamID) -- if teamID not provided, it will get current teamID -- rebuild lookup indexes wipe(queueLookup) for index,info in ipairs(settings.LevelingQueue) do if not info.petID then info.petID = "delete" -- flag this for delete, petID doesn't exist elseif queueLookup[info.petID] then info.petID = "delete" -- flag this for delete, petID already encountered else queueLookup[info.petID] = index end end local invalidPetFound = false -- becomes true when an invalid pet is found (to initialize excludePetIDs) -- remove any flagged for deletion (or that have hit level 25) and set preferred flag while here for index=#settings.LevelingQueue,1,-1 do local info = settings.LevelingQueue[index] if info.petID=="delete" then tremove(settings.LevelingQueue,index) -- petID doesn't exist, remove from queue else local petInfo = rematch.petInfo:Fetch(info.petID) if petInfo.level==25 then tremove(settings.LevelingQueue,index) -- pet is level 25, remove from queue else if not petInfo.isValid then -- if pet is not valid, then look for a replacement if not invalidPetFound then -- first finding an invalid pet, fill excludePetIDs from queue invalidPetFound = true wipe(excludePetIDs) for _,queueInfo in ipairs(settings.LevelingQueue) do excludePetIDs[queueInfo.petID] = true end end local newPetID = rematch.petTags:FindPetID(info.petTag,excludePetIDs) if newPetID then info.petID = newPetID else -- there was no replacement petID, should delete but letting it be a blank pet for now --print(info.petID,petInfo.name,"is still not valid, tag:",info.petTag) end end info.preferred = rematch.preferences:IsPetPreferred(info.petID,preferences) end end end -- after preferences updated, update top picks rematch.queue:UpdateTopPicks() if settings.QueueActiveSort then rematch.queue:SortQueue(C.QUEUE_SORT_ALL) end end -- after preferences updated, this picks the top 3 pets from the queue function rematch.queue:UpdateTopPicks() wipe(topPicks) local pick = 1 -- look for preferred pets first for _,info in ipairs(settings.LevelingQueue) do if info.preferred then topPicks[pick] = info.petID pick = pick + 1 if pick > 3 then return -- no need to stick around after 3 pets picked end end end -- if still here, then not enough preferred pets to fill 3 top picks; try unpreferred for _,info in ipairs(settings.LevelingQueue) do if not info.preferred then topPicks[pick] = info.petID pick = pick + 1 if pick > 3 then return end end end end -- called immediately from startProcess and C.QUEUE_PROCESS_WAIT (0.25 seconds) after any leveling pets slotted to confirm they loaded local function runProcess() local pickIndex = 1 local petSlotted = false -- true if any pets are slotted local firstSlotted -- petID of first pet (pickIndex 1) slotted for toasting purposes for slot=1,3 do if rematch.loadouts:GetSpecialSlotType(slot)=="leveling" then local petID = rematch.queue:GetTopPick(pickIndex) local petInfo = rematch.petInfo:Fetch(petID) if petInfo.isOwned and petInfo.isSummonable then if rematch.loadouts:GetLoadoutInfo(slot)~=petID then rematch.loadouts:SlotPet(slot,petID,0,true) petSlotted = true if pickIndex==1 then firstSlotted = petID end end pickIndex = pickIndex + 1 end end end -- update UI if it's visible (to hide.show leveling badges everywhere) if rematch.frame:IsVisible() then rematch.frame:Update() end -- if a petID is waiting to be blinged if blingPetID then -- if queue panel is up, scroll to pet added to queue and bling it if rematch.queuePanel:IsVisible() and rematch.queue:IsPetLeveling(blingPetID) then rematch.queuePanel.List:BlingData(rematch.queue:GetPetIndex(blingPetID)) end -- whether queue panel was up or not, clear petID so it's not blinged when going to queue panel later blingPetID = nil end -- if any pets were attempted to be slotted, come back in a bit to confirm they're all slotted if petSlotted and timeout>0 then timeout = timeout - 1 rematch.timer:Start(C.QUEUE_PROCESS_WAIT,runProcess) end if firstSlotted and firstSlotted~=settings.LastToastedPetID then settings.LastToastedPetID = firstSlotted rematch.toast:ToastLevelingPet(firstSlotted) end end -- called from rematch.queue:Process() after a 0-frame update; updates the queue and kicks off runProcess local function startProcess() rematch.queue:Update() timeout = C.QUEUE_PROCESS_TIMEOUT runProcess() end -- procedure to call when the queue may change or pet levels/rarity may change; can potentially slot leveling pets in their place function rematch.queue:Process() if rematch.main:IsPlayerInWorld() then rematch.timer:Start(0,startProcess) else -- if in a loading screen, stop any pending queue process rematch.queue:CancelProcess() end end -- when queue should be updated/processed without a wait function rematch.queue:ProcessNow() rematch.queue:CancelProcess() if rematch.main:IsPlayerInWorld() then startProcess() end end -- call this to cancel any pending swaps from a queue being processed (eg team loading) function rematch.queue:CancelProcess() rematch.timer:Stop(startProcess) rematch.timer:Stop(runProcess) end -- bling the given petID at the end of the next process function rematch.queue:BlingPetID(petID) blingPetID = petID end -- returns true if the petID is in the leveling queue; using lookup for performance function rematch.queue:IsPetLeveling(petID) return (petID and queueLookup[petID]) and true or false end -- returns the numeric index of a petID in the queue from its petID function rematch.queue:GetPetIndex(petID) return petID and queueLookup[petID] end -- return the topmost preferred petID from the queue, by the given index. (so index 3 will get the 3rd topmost pet from the queue) function rematch.queue:GetTopPick(index) return topPicks[index] -- if there's a pet in the queue available, return it end -- returns true if the petID can level (is owned, can battle, has a level and is under 25) function rematch.queue:PetIDCanLevel(petID) local petInfo = rematch.petInfo:Fetch(petID) return petInfo.isOwned and petInfo.canBattle and petInfo.level and petInfo.level<25 end -- adds a petID to the end of the queue function rematch.queue:AddPetID(petID) rematch.queue:InsertPetID(petID,#settings.LevelingQueue+1) -- adding to end of queue end -- inserts a petID into the queue at the given index function rematch.queue:InsertPetID(petID,index) assert(type(index)=="number" and index>=1 and index<=(#settings.LevelingQueue+1),"Invalid index ("..(index or "nil")..") in queue:InsertPetID(index)") if rematch.queue:PetIDCanLevel(petID) then if rematch.queue:IsPetLeveling(petID) then rematch.queue:MoveIndex(rematch.queue:GetPetIndex(petID),index) -- pet was already in queue, move to this new position else tinsert(settings.LevelingQueue,index,{petID=petID,petTag=rematch.petTags:Create(petID,"Q"),added=rematch.utils:GetDateTime()}) end rematch.queue:Process() end end -- removes a petID from the queue function rematch.queue:RemovePetID(petID) local index = rematch.queue:GetPetIndex(petID) if index then tremove(settings.LevelingQueue,index) end rematch.queue:Process() end -- moves a pet in the queue from oldIndex to newIndex (newIndex can be #queue+1 to add to end of queue) function rematch.queue:MoveIndex(oldIndex,newIndex) local queueSize = #settings.LevelingQueue assert(type(oldIndex)=="number" and oldIndex>=1 and oldIndex<=queueSize,"Invalid oldIndex ("..(oldIndex or "nil")..") in queue:MoveIndex(oldIndex,newIndex)") assert(type(newIndex)=="number" and newIndex>=1 and newIndex<=(queueSize+1),"Invalid newIndex ("..(newIndex or "nil")..") in queue:MoveIndex(oldIndex,newIndex)") local deleteIndex = newIndex petNames[e2] else -- if reached here, no need to swap anything return false end end -- does a stable sort (insertion sort) of the queue for the given sort (C.QUEUE_SORT_ASC, C.QUEUE_SORT_MID, etc.; C.QUEUE_SORT_ALL for active sort) function rematch.queue:SortQueue(sort) -- update weights before going into sort fillSortWeights(sort) -- perform insertion sort on the given sort (can't use table.sort since it's unstable) -- most of the time the queue should be mostly sorted so this shouldn't be too much work local queue = settings.LevelingQueue for i=2,#queue do local petID = queue[i].petID local petTag = queue[i].petTag local preferred = queue[i].preferred local added = queue[i].added local j = i-1 while j>0 and shouldSwap(queue[j].petID,petID) do queue[j+1].petID = queue[j].petID queue[j+1].petTag = queue[j].petTag queue[j+1].preferred = queue[j].preferred queue[j+1].added = queue[j].added j = j-1 end queue[j+1].petID = petID queue[j+1].petTag = petTag queue[j+1].preferred = preferred queue[j+1].added = added end end --[[ events ]] -- this runs once after login when pets are loaded, to remove any pets that no longer exist, possibly from pets released/caged -- while addon disabled or server-side petID reassignment. if all pets are invalid a petID reassignment happened, rebuild the queue function rematch.queue:REMATCH_PETS_LOADED() local queue = settings.LevelingQueue local excludePetIDs = {} -- lookup table of petIDs added to queue (so they don't get added for repeats) -- first see if ALL pets are invalid; if so then likely a server-side petID reassignment happened local validFound = false local invalidFound = false for index,info in ipairs(queue) do local petInfo = rematch.petInfo:Fetch(queue[index].petID) if petInfo.idType=="species" then -- if a species, look for a pet to take its place; possibly remainingg a species local petID = rematch.petTags:FindPetID(info.petTag,excludePetIDs) if petID then excludePetIDs[petID] = true info.petID = petID end elseif petInfo.isValid then validFound = true else info.invalid = true -- flag pet for removal invalidFound = true end end if validFound and invalidFound then -- if only some pets are invalid, then remove them from the queue for index=#queue,1,-1 do if queue[index].invalid then tremove(queue,index) end end elseif invalidFound then -- if ALL pets are invalid, rebuild entire queue from tags for index,info in ipairs(queue) do local speciesID = rematch.petTags:GetSpecies(info.petTag) if speciesID then local petID = rematch.petTags:FindPetID(info.petTag,excludePetIDs) if petID then excludePetIDs[petID] = true info.petID = petID end end end end -- clear isValid flag from all pets in the queue, whole queue is now validated for _,info in ipairs(settings.LevelingQueue) do info.isValid = nil end rematch.queue:Process() rematch.events:Register(rematch.queue,"PET_JOURNAL_LIST_UPDATE",rematch.queue.Process) end -- when a pet is caged or released, remove from queue directly (this event doesn't fire naturally on release; roster is firing it on a hook) function rematch.queue:PET_JOURNAL_PET_DELETED(petID) for index=#settings.LevelingQueue,1,-1 do local info = settings.LevelingQueue[index] if info.petID==petID then tremove(settings.LevelingQueue,index) end end end -- fills the queue with pets from the petsPanel (filtered and in the current order in the petsPanel) -- when fillMore is false, only levelable pets with a species not already in the queue and not already at 25 will be added -- when fillMore is true, one levelable copy of each species will be added to the queue -- when countOnly is true, the pets won't actually be added, just a count of what would be added is returned local speciesInQueue = {} function rematch.queue:FillQueue(fillMore,countOnly) local count = 0 wipe(speciesInQueue) if not fillMore then for _,info in ipairs(settings.LevelingQueue) do local petID = info.petID local petInfo = rematch.petInfo:Fetch(petID) speciesInQueue[petInfo.speciesID] = true end end -- for each petID in the filtered list of pets for _,petID in ipairs(rematch.filters:RunFilters()) do local petInfo = rematch.petInfo:Fetch(petID) local speciesID = petInfo.speciesID -- if speciesID is not in the queue and there's no level 25 version of the pet if speciesID and not speciesInQueue[speciesID] and not rematch.queue:IsPetLeveling(petID) and rematch.queue:PetIDCanLevel(petID) and (fillMore or not rematch.collectionInfo:IsSpeciesAt25(speciesID)) then if not countOnly then rematch.queue:AddPetID(petID) -- add it (this triggers a 0-frame process queue) end speciesInQueue[speciesID] = true count = count + 1 end end return count end -- returns all petTags in the queue in a comma-separated list function rematch.queue:ExportQueue() local result = "" for index,info in ipairs(settings.LevelingQueue) do result = result..info.petTag..(index==#settings.LevelingQueue and "" or ",") end return result end -- takes an export/import string and returns: numNew,numOld,numCant,numBad function rematch.queue:AnalyzeImport(import) local numNew = 0 -- the number of pets importing this would add to the queue (will be added) local numOld = 0 -- the number of pets already in the queue (won't be added) local numCant = 0 -- the number of pets can't add to queue due to level or missing (can't be added) local numBad = 0 -- the number of "pets" in the import that could not be interpreted local excludePetIDs = {} -- lookup table of petIDs added to queue (so they don't get added for repeats) -- if nothing to import, return nothing if type(import)~="string" or import:trim()=="" then return end for petTag in import:gmatch("[^,]+") do local speciesID = rematch.petTags:GetSpecies(petTag) if speciesID then local petID = rematch.petTags:FindPetID(petTag,excludePetIDs) local petInfo = rematch.petInfo:Fetch(petID) if not petInfo.isValid or not petInfo.level then -- this pet is bad numBad = numBad + 1 --print(petID,petInfo.name,"is bad") else if petInfo.level==25 then -- this pet is level 25, can't add numCant = numCant + 1 elseif not petInfo.isOwned then -- this pet is not owned, can't add numCant = numCant + 1 elseif rematch.queue:IsPetLeveling(petID) then -- this pet is already leveling, won't add numOld = numOld + 1 else -- this pet is ok to add numNew = numNew + 1 excludePetIDs[petID] = true end end else -- this whole species is bad numBad = numBad + 1 end end return numNew,numOld,numCant,numBad end -- imports the pets in the given string into the queue where it can function rematch.queue:ImportQueue(import) local excludePetIDs = {} if not import or import=="" then return end for petTag in import:gmatch("[^,]+") do local speciesID = rematch.petTags:GetSpecies(petTag) if speciesID then local petID = rematch.petTags:FindPetID(petTag,excludePetIDs) excludePetIDs[petID] = true local petInfo = rematch.petInfo:Fetch(petID) if petInfo.isValid and petInfo.level and petInfo.level<25 and petInfo.isOwned and not rematch.queue:IsPetLeveling(petID) then rematch.queue:AddPetID(petID) end end end end -- fires when a pet is captured in a pet battle, learned from a caged pet, and from an item that learns a pet -- if settings enabled, add new pets to the queue function rematch.queue:NEW_PET_ADDED(petID) if settings.QueueAutoLearn then local petInfo = rematch.petInfo:Fetch(petID) if petInfo.canBattle and petInfo.level and petInfo.level<25 then -- if QueueAutoLearnOnly, only add pets whose species does not have a verion at 25 or in queue -- if QueueAutoLearnRare, only add pets that are rare local isRare = petInfo.rarity==4 local isAnyAt25 = rematch.collectionInfo:IsSpeciesAt25(petInfo.speciesID) -- QueueAutoLearnOnly also checks if any same species in queue; only bother checking if a 25 not already found if not isAnyAt25 and settings.QueueAutoLearnOnly then for _,info in ipairs(settings.LevelingQueue) do local speciesID = rematch.petTags:GetSpecies(info.petTag) if speciesID==petInfo.speciesID then isAnyAt25 = true -- found species in queue, flag it as if there's a 25 end end end if (not settings.QueueAutoLearnOnly or not isAnyAt25) and (not settings.QueueAutoLearnRare or isRare) then -- add pet to queue and print a "system" message to match the that the pet was just added to journal rematch.queue:AddPetID(petID) rematch.utils:WriteSystem(format(L["%s has also been added to your leveling queue!"],petInfo.formattedName)) end end end end