Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Confused about mix of CamelCase and_underscores?
- -- Camel case comes from copypasta of how Blizzard calls returns/fields in their code and deriveates
- -- Underscore are my own variables
- local dump = DevTools_Dump
- local tinsert = table.insert
- local tsort = table.sort
- local wipe = wipe
- local pairs = pairs
- local GARRISON_CURRENCY = GARRISON_CURRENCY
- local GarrisonMissionFrame = GarrisonMissionFrame
- local GarrisonLandingPage = GarrisonLandingPage
- local GarrisonRecruitSelectFrame = GarrisonRecruitSelectFrame
- local MissionPage = GarrisonMissionFrame.MissionTab.MissionPage
- local AddFollowerToMission = C_Garrison.AddFollowerToMission
- local GetPartyMissionInfo = C_Garrison.GetPartyMissionInfo
- local RemoveFollowerFromMission = C_Garrison.RemoveFollowerFromMission
- local GARRISON_FOLLOWER_IN_PARTY = GARRISON_FOLLOWER_IN_PARTY
- local NUM_OPTIONS = 1
- local top_for_mission = {}
- local top_for_mission_dirty = true
- local metrics = {"successChance", "expectedResources", "expectedProgress", "expectedProgressPerFollowerHour"}
- local filtered_followers = {}
- local filtered_followers_count
- local filtered_followers_dirty = true
- local event_frame = CreateFrame("Frame")
- local events_filtered_followers_dirty = {
- GARRISON_FOLLOWER_LIST_UPDATE = true,
- GARRISON_FOLLOWER_XP_CHANGED = true,
- GARRISON_FOLLOWER_REMOVED = true,
- }
- local events_top_for_mission_dirty = {
- GARRISON_MISSION_NPC_OPENED = true,
- GARRISON_MISSION_LIST_UPDATE = true,
- }
- event_frame:SetScript("OnEvent", function(self, event)
- -- if events_top_for_mission_dirty[event] then top_for_mission_dirty = true end
- -- if events_filtered_followers_dirty[event] then filtered_followers_dirty = true end
- -- Let's clear both for now, or else we often miss one follower state update when we start mission
- if events_top_for_mission_dirty[event] or events_filtered_followers_dirty[event] then
- top_for_mission_dirty = true
- filtered_followers_dirty = true
- end
- end)
- for event in pairs(events_top_for_mission_dirty) do event_frame:RegisterEvent(event) end
- for event in pairs(events_filtered_followers_dirty) do event_frame:RegisterEvent(event) end
- local gmm_buttons = {}
- function GMM_dumpl(pattern, ...)
- local names = { strsplit(",", pattern) }
- for idx = 1, select('#', ...) do
- local name = names[idx]
- if name then name = name:gsub("^%s+", ""):gsub("%s+$", "") end
- print(GREEN_FONT_COLOR_CODE, idx, name, FONT_COLOR_CODE_CLOSE)
- dump((select(idx, ...)))
- end
- end
- -- Print contents of `tbl`, with indentation.
- -- `indent` sets the initial level of indentation.
- function tprint (tbl, indent)
- if not indent then indent = 0 end
- for k, v in pairs(tbl) do
- formatting = string.rep(" ", indent) .. k .. ": "
- if type(v) == "table" then
- print(formatting)
- tprint(v, indent+1)
- else
- print(formatting .. tostring(v))
- end
- end
- end
- local xp_to_level = {[90]=400, [91]=800, [92]=1200, [93]=1600, [94]=2000,
- [95]=3000, [96]=3500, [97]=4000,
- [98]=5400, [99]=6000}
- local xp_to_upgrade = {[2]=60000, [3]=120000}
- local function follower_progress(mission_level, follower, xp)
- if not follower then return 0 end
- if follower.level >= 100 and follower.quality >= 4 then return 0 end -- epic followers at max level cannot gain xp
- -- express follower xp as a percentage of next level/upgrade
- local progress = 0
- if follower.level < 100 then
- progress = xp / xp_to_level[follower.level] * 100
- else
- progress = xp / xp_to_upgrade[follower.quality] * 100
- end
- -- scale follower xp by level delta
- local level_delta = mission_level - follower.level
- if level_delta <= 0 then return progress end -- normal xp gain
- if level_delta <= 2 then return progress / 2 end -- slightly underlevel followers gain half xp
- return progress / 10 -- very underlevel followers gain 10% xp
- end
- -- return which of left or right is better
- -- if metric is any field present in left/right, it will be prioritized first (higher is better)
- local function BetterMissionOutcome(left, right, metric)
- if not left then return 1 end
- if not right then return -1 end
- if metric and left[metric] and right[metric] then
- if left[metric] < right[metric] then return 1 end
- if left[metric] > right[metric] then return -1 end
- end
- -- if no metric specified, or tied on that metric, break ties by the old algorithm:
- if left.expectedResources < right.expectedResources then return 1 end
- if left.expectedResources > right.expectedResources then return -1 end
- if left.successItem or right.successItem then
- if left.successChance < right.successChance then return 1 end
- if left.successChance > right.successChance then return -1 end
- end
- if left.expectedProgressPerFollowerHour < right.expectedProgressPerFollowerHour then return 1 end
- if left.expectedProgressPerFollowerHour > right.expectedProgressPerFollowerHour then return -1 end
- if left.expectedProgress < right.expectedProgress then return 1 end
- if left.expectedProgress > right.expectedProgress then return -1 end
- if left.successChance < right.successChance then return 1 end
- if left.successChance > right.successChance then return -1 end
- if left.totalTimeSeconds > right.totalTimeSeconds then return 1 end
- if left.totalTimeSeconds < right.totalTimeSeconds then return -1 end
- return 0
- end
- local _, _, garrison_currency_texture = GetCurrencyInfo(GARRISON_CURRENCY)
- garrison_currency_texture = "|T" .. garrison_currency_texture .. ":0|t"
- local time_texture = "|TInterface\\Icons\\spell_holy_borrowedtime:0|t"
- local function FindBestFollowersForMission(mission, followers)
- local top_by_metrics = {}
- for _, metric in ipairs(metrics) do
- top_by_metrics[metric] = {}
- end
- local min, max = {}, {}
- local followers_count = #followers
- local slots = mission.numFollowers
- if slots > followers_count then return top_by_metrics end
- event_frame:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonMissionFrame:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonLandingPage:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonRecruitSelectFrame:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- if FollowerLocationInfoFrame then FollowerLocationInfoFrame:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE") end
- local mission_id = mission.missionID
- if C_Garrison.GetNumFollowersOnMission(mission_id) > 0 then
- for idx = 1, #followers do
- RemoveFollowerFromMission(mission_id, followers[idx].followerID)
- end
- end
- for idx = 1, slots do
- max[idx] = followers_count - slots + idx
- min[idx] = nil
- end
- for idx = slots+1, 3 do
- max[idx] = followers_count + 1
- min[idx] = followers_count + 1
- end
- local mission_level = C_Garrison.GetBasicMissionInfo(mission_id).level
- local _, baseXP = C_Garrison.GetMissionInfo(mission_id)
- local successXP = 0
- local successResources = 0
- local successItem = nil
- for _, reward in pairs(mission.rewards) do
- if reward.currencyID == GARRISON_CURRENCY then successResources = successResources + reward.quantity end
- if reward.followerXP then successXP = successXP + reward.followerXP end
- if reward.itemID and reward.itemID ~= 120205 then successItem = reward.itemID end -- itemID 120205 is player XP reward
- end
- for i1 = 1, max[1] do
- local follower1 = followers[i1]
- local follower1_id = follower1.followerID
- for i2 = min[2] or (i1 + 1), max[2] do
- local follower2 = followers[i2]
- local follower2_id = follower2 and follower2.followerID
- for i3 = min[3] or (i2 + 1), max[3] do
- local follower3 = followers[i3]
- local follower3_id = follower3 and follower3.followerID
- -- Assign followers to mission
- if not AddFollowerToMission(mission_id, follower1_id) then --[[ error handling! ]] end
- if follower2 and not AddFollowerToMission(mission_id, follower2_id) then --[[ error handling! ]] end
- if follower3 and not AddFollowerToMission(mission_id, follower3_id) then --[[ error handling! ]] end
- -- Calculate result
- local totalTimeString, totalTimeSeconds, isMissionTimeImproved, successChance, partyBuffs, isEnvMechanicCountered, xpBonus, materialMultiplier = GetPartyMissionInfo(mission_id)
- local baseProgress = 0
- baseProgress = baseProgress + follower_progress(mission_level, follower1, baseXP + xpBonus)
- baseProgress = baseProgress + follower_progress(mission_level, follower2, baseXP + xpBonus)
- baseProgress = baseProgress + follower_progress(mission_level, follower3, baseXP + xpBonus)
- local successProgress = 0
- -- TODO: how does bonusXP apply to success rewards?
- successProgress = successProgress + follower_progress(mission_level, follower1, successXP)
- successProgress = successProgress + follower_progress(mission_level, follower2, successXP)
- successProgress = successProgress + follower_progress(mission_level, follower3, successXP)
- local expectedResources = successResources * materialMultiplier * successChance / 100
- local expectedProgress = baseProgress + (successProgress * successChance / 100)
- local expectedProgressPerFollowerHour = expectedProgress * 3600 / totalTimeSeconds / mission.numFollowers
- local outcome = {}
- outcome[1] = follower1
- outcome[2] = follower2
- outcome[3] = follower3
- outcome.successChance = successChance
- outcome.successItem = successItem
- outcome.expectedResources = expectedResources
- outcome.expectedProgress = expectedProgress
- outcome.expectedProgressPerFollowerHour = expectedProgressPerFollowerHour
- outcome.totalTimeSeconds = totalTimeSeconds
- outcome.followersUsed = mission.numFollowers
- for _, metric in ipairs(metrics) do
- local top_by_metric = top_by_metrics[metric]
- for idx = 1, NUM_OPTIONS do
- if BetterMissionOutcome(top_by_metric[idx], outcome, metric) > 0 then
- tinsert(top_by_metric, idx, outcome)
- tremove(top_by_metric, NUM_OPTIONS + 1)
- break
- end
- end
- end
- -- Unasssign
- RemoveFollowerFromMission(mission_id, follower1_id)
- if follower2 then RemoveFollowerFromMission(mission_id, follower2_id) end
- if follower3 then RemoveFollowerFromMission(mission_id, follower3_id) end
- end
- end
- end
- -- dump(top[1])
- event_frame:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonMissionFrame:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonLandingPage:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- GarrisonRecruitSelectFrame:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- if FollowerLocationInfoFrame then FollowerLocationInfoFrame:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE") end
- -- dump(top)
- -- local location, xp, environment, environmentDesc, environmentTexture, locPrefix, isExhausting, enemies = C_Garrison.GetMissionInfo(missionID);
- -- /run GMM_dumpl("location, xp, environment, environmentDesc, environmentTexture, locPrefix, isExhausting, enemies", C_Garrison.GetMissionInfo(GarrisonMissionFrame.MissionTab.MissionPage.missionInfo.missionID))
- -- /run GMM_dumpl("totalTimeString, totalTimeSeconds, isMissionTimeImproved, successChance, partyBuffs, isEnvMechanicCountered, xpBonus, materialMultiplier", C_Garrison.GetPartyMissionInfo(GarrisonMissionFrame.MissionTab.MissionPage.missionInfo.missionID))
- return top_by_metrics
- end
- local function SortFollowersByLevel(a, b)
- local a_level = a.level
- local b_level = b.level
- if a_level ~= b_level then return a_level > b_level end
- return a.iLevel > b.iLevel
- end
- local function GetFilteredFollowers()
- if not filtered_followers_dirty then
- return filtered_followers, filtered_followers_count
- end
- local followers = C_Garrison.GetFollowers()
- wipe(filtered_followers)
- filtered_followers_count = 0
- for idx = 1, #followers do
- local follower = followers[idx]
- repeat
- if not follower.isCollected then break end
- local status = follower.status
- if status and status ~= GARRISON_FOLLOWER_IN_PARTY then break end
- filtered_followers_count = filtered_followers_count + 1
- filtered_followers[filtered_followers_count] = follower
- until true
- end
- tsort(filtered_followers, SortFollowersByLevel)
- -- dump(filtered_followers)
- filtered_followers_dirty = false
- top_for_mission_dirty = true
- return filtered_followers, filtered_followers_count
- end
- -- only works for positive numbers
- function sigfigs(number, sigDigits)
- if not number then return "" end
- if number == 0 then return "0" end
- local target = math.pow(10, sigDigits)
- local shifted = 0
- while number < target do
- number = number * 10
- shifted = shifted + 1
- end
- while number >= target do
- number = number / 10
- shifted = shifted - 1
- end
- number = math.floor(number + 0.5)
- number = number / math.pow(10, shifted)
- return string.format("%." .. shifted .. "f", number)
- end
- local function FormatTextForMetric(outcome, metric)
- if not outcome then
- return ""
- elseif metric == "successChance" then
- local itemRewardString = ""
- if outcome.successItem then
- local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, itemSellPrice = GetItemInfo(outcome.successItem)
- if itemTexture then
- itemRewardString = string.format("|T%s:0|t ", itemTexture)
- end
- end
- return string.format("%s%d%%", itemRewardString, outcome.successChance)
- elseif metric == "expectedResources" then
- return string.format("%.1f %s", outcome.expectedResources, garrison_currency_texture)
- elseif metric == "expectedProgress" then
- return string.format("%s%% |TInterface\\Icons\\Spell_ChargePositive:0|t", sigfigs(outcome.expectedProgress, 2))
- elseif metric == "expectedProgressPerFollowerHour" then
- return string.format("%s%% |TInterface\\Icons\\Spell_ChargePositive:0|t/|TInterface\\Icons\\Achievement_Character_Human_Female:0|t|TInterface\\Icons\\INV_Misc_PocketWatch_01:0|t", sigfigs(outcome.expectedProgressPerFollowerHour, 2))
- else
- return string.format("%d%% ??", outcome.successChance)
- end
- end
- local function PickMetric(outcomes)
- -- Always prioritize garrison resources, if they're available.
- if outcomes["expectedResources"]["expectedResources"] > 0 then return "expectedResources" end
- -- Then prioritize winning items.
- if outcomes["successChance"]["successItem"] then return "successChance" end
- -- Finally, prioritize experience.
- return "expectedProgressPerFollowerHour"
- end
- local function SetTeamButtonText(button, top_entry)
- local metric = PickMetric(top_entry)
- button:SetText(FormatTextForMetric(top_entry[metric], metric))
- end
- local available_missions = {}
- local function BestForCurrentSelectedMission()
- local missionInfo = MissionPage.missionInfo
- local mission_id = missionInfo.missionID
- -- print("Mission ID:", mission_id)
- local filtered_followers, filtered_followers_count = GetFilteredFollowers()
- C_Garrison.GetAvailableMissions(available_missions)
- local mission
- for idx = 1, #available_missions do
- if available_missions[idx].missionID == mission_id then
- mission = available_missions[idx]
- break
- end
- end
- -- dump(mission)
- local top_by_metrics = FindBestFollowersForMission(mission, filtered_followers)
- for _, metric in ipairs(metrics) do
- for idx = 1, NUM_OPTIONS do
- local button = gmm_buttons['MissionPage_' .. metric .. idx]
- local top_entry = top_by_metrics[metric][idx]
- button[1] = top_entry[1] and top_entry[1].followerID or nil
- button[2] = top_entry[2] and top_entry[2].followerID or nil
- button[3] = top_entry[3] and top_entry[3].followerID or nil
- button:SetText(FormatTextForMetric(top_entry, metric))
- end
- end
- end
- local function MissionPage_PartyButtonOnClick(self)
- if self[1] then
- event_frame:UnregisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- local MissionPageFollowers = GarrisonMissionFrame.MissionTab.MissionPage.Followers
- for idx = 1, #MissionPageFollowers do
- GarrisonMissionPage_ClearFollower(MissionPageFollowers[idx])
- end
- for idx = 1, #MissionPageFollowers do
- local followerFrame = MissionPageFollowers[idx]
- local follower = self[idx]
- if follower then
- local followerInfo = C_Garrison.GetFollowerInfo(follower)
- GarrisonMissionPage_SetFollower(followerFrame, followerInfo)
- end
- end
- event_frame:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
- end
- GarrisonMissionPage_UpdateMissionForParty()
- end
- local function MissionList_PartyButtonOnClick(self)
- return self:GetParent():Click()
- end
- -- Add more data to mission list over Blizzard's own
- -- GarrisonMissionList_Update
- local function GarrisonMissionList_Update_More()
- local self = GarrisonMissionFrame.MissionTab.MissionList
- local scrollFrame = self.listScroll
- local buttons = scrollFrame.buttons
- local numButtons = #buttons
- if self.showInProgress then
- for i = 1, numButtons do
- gmm_buttons['MissionList' .. i]:Hide()
- end
- return
- end
- local missions = self.availableMissions
- local numMissions = #missions
- if numMissions == 0 then return end
- if top_for_mission_dirty then
- wipe(top_for_mission)
- top_for_mission_dirty = false
- end
- local missions = self.availableMissions
- local offset = HybridScrollFrame_GetOffset(scrollFrame)
- local filtered_followers, filtered_followers_count = GetFilteredFollowers()
- local more_missions_to_cache
- for i = 1, numButtons do
- local button = buttons[i]
- local alpha = 1
- local index = offset + i
- if index <= numMissions then
- local mission = missions[index]
- local gmm_button = gmm_buttons['MissionList' .. i]
- if mission.numFollowers > filtered_followers_count then
- button:SetAlpha(0.3)
- gmm_button:SetText("")
- else
- local top_for_this_mission = top_for_mission[mission.missionID]
- if not top_for_this_mission then
- if more_missions_to_cache then
- more_missions_to_cache = more_missions_to_cache + 1
- else
- more_missions_to_cache = 0
- local outcomes = FindBestFollowersForMission(mission, filtered_followers)
- top_for_this_mission = {}
- for _, metric in ipairs(metrics) do
- top_for_this_mission[metric] = outcomes[metric][1]
- end
- top_for_mission[mission.missionID] = top_for_this_mission
- end
- end
- if top_for_this_mission then
- SetTeamButtonText(gmm_button, top_for_this_mission)
- else
- gmm_button:SetText("...")
- end
- button:SetAlpha(1)
- end
- gmm_button:Show()
- end
- end
- if more_missions_to_cache and more_missions_to_cache > 0 then
- -- print(more_missions_to_cache, GetTime())
- C_Timer.After(0.001, GarrisonMissionList_Update_More)
- end
- end
- hooksecurefunc("GarrisonMissionList_Update", GarrisonMissionList_Update_More)
- hooksecurefunc(GarrisonMissionFrame.MissionTab.MissionList.listScroll, "update", GarrisonMissionList_Update_More)
- local function MissionPage_ButtonsInit()
- local prev_metric
- for _, metric in ipairs(metrics) do
- local prev_option
- for idx = 1, NUM_OPTIONS do
- local button_name = 'MissionPage_' .. metric .. idx
- if not gmm_buttons[button_name] then
- local set_followers_button = CreateFrame("Button", nil, GarrisonMissionFrame.MissionTab.MissionPage, "UIPanelButtonTemplate")
- set_followers_button:SetText(button_name)
- set_followers_button:SetWidth(120)
- set_followers_button:SetHeight(35)
- if prev_option then
- set_followers_button:SetPoint("TOPLEFT", prev_option, "TOPRIGHT", 0, 0)
- else
- if prev_metric then
- set_followers_button:SetPoint("TOPLEFT", prev_metric, "BOTTOMLEFT", 0, -20)
- else
- set_followers_button:SetPoint("TOPLEFT", GarrisonMissionFrame.MissionTab.MissionPage, "TOPRIGHT", 0, 0)
- end
- prev_metric = set_followers_button
- end
- set_followers_button:SetScript("OnClick", MissionPage_PartyButtonOnClick)
- set_followers_button:Show()
- prev_option = set_followers_button
- gmm_buttons[button_name] = set_followers_button
- end
- end
- end
- end
- local function MissionList_ButtonsInit()
- local level_anchor = GarrisonMissionFrame.MissionTab.MissionList.listScroll
- local blizzard_buttons = GarrisonMissionFrame.MissionTab.MissionList.listScroll.buttons
- for idx = 1, #blizzard_buttons do
- if not gmm_buttons['MissionList' .. idx] then
- local blizzard_button = blizzard_buttons[idx]
- -- move first reward to left a little, rest are anchored to first
- local reward = blizzard_button.Rewards[1]
- for point_idx = 1, reward:GetNumPoints() do
- local point, relative_to, relative_point, x, y = reward:GetPoint(point_idx)
- if point == "RIGHT" then
- x = x - 60
- reward:SetPoint(point, relative_to, relative_point, x, y)
- break
- end
- end
- local set_followers_button = CreateFrame("Button", nil, blizzard_button, "UIPanelButtonTemplate")
- set_followers_button:SetText(idx)
- set_followers_button:SetWidth(80)
- set_followers_button:SetHeight(35)
- set_followers_button:SetPoint("LEFT", blizzard_button, "RIGHT", -65, 0)
- set_followers_button:SetScript("OnClick", MissionList_PartyButtonOnClick)
- gmm_buttons['MissionList' .. idx] = set_followers_button
- end
- end
- -- GarrisonMissionFrame.MissionTab.MissionList.listScroll.scrollBar:SetFrameLevel(gmm_buttons['MissionList1']:GetFrameLevel() - 3)
- end
- MissionPage_ButtonsInit()
- MissionList_ButtonsInit()
- hooksecurefunc("GarrisonMissionPage_ShowMission", BestForCurrentSelectedMission)
- -- local count = 0
- -- hooksecurefunc("GarrisonFollowerList_UpdateFollowers", function(self) count = count + 1 print("GarrisonFollowerList_UpdateFollowers", count, self:GetName(), self:GetParent():GetName()) end)
- -- Globals deliberately exposed for people outside
- function GMM_Click(button_name)
- local button = gmm_buttons[button_name]
- if button then button:Click() end
- end
- -- /dump GarrisonMissionFrame.MissionTab.MissionList.listScroll.buttons
- -- /dump GarrisonMissionFrame.MissionTab.MissionList.listScroll.scrollBar
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement