From 009e1d3142fb401eebdd7f3be984da7925df5ca5 Mon Sep 17 00:00:00 2001 From: Paul Emmerich Date: Tue, 12 Dec 2023 22:35:51 +0100 Subject: [PATCH] Ashenvale: Add estimated event start timer (#123) --- DBM-PvP/Ashenvale.lua | 131 ++++++++++++++++++++++++++++++++---- DBM-PvP/localization.de.lua | 14 ++++ DBM-PvP/localization.en.lua | 14 ++++ 3 files changed, 147 insertions(+), 12 deletions(-) diff --git a/DBM-PvP/Ashenvale.lua b/DBM-PvP/Ashenvale.lua index d6ddbe7..9e02bba 100644 --- a/DBM-PvP/Ashenvale.lua +++ b/DBM-PvP/Ashenvale.lua @@ -3,6 +3,7 @@ if WOW_PROJECT_ID ~= WOW_PROJECT_CLASSIC or not C_Seasons or C_Seasons.GetActive end local MAP_ASHENVALE = 1440 local mod = DBM:NewMod("m" .. MAP_ASHENVALE, "DBM-PvP") +local L = mod:GetLocalizedStrings() mod:SetRevision("@file-date-integer@") -- TODO: we could teach this thing to handle outdoor zones instead of only instances @@ -15,6 +16,8 @@ mod:RegisterEvents( "UPDATE_UI_WIDGET" ) +local startTimer = mod:NewStageTimer(0, 20230, "EstimatedStart", nil, "EstimatedStartTimer", nil, nil, nil, true) -- last arg is "keep" + local widgetIDs = { [5360] = true, -- Alliance progress [5361] = true, -- Horde progress @@ -23,8 +26,68 @@ local widgetIDs = { [5378] = true, -- Event time remaining } -function mod:StartEvent() +mod.stateTracking = {} + +function mod:resetStateTracking() + self.stateTracking = { + alliance = {}, + horde = {}, + } +end +mod:resetStateTracking() + +---@return number|nil +---@return number|nil +local function getEstimate(data) + if #data < 3 then return end + local latest = data[#data] + for i = #data - 2, 1, -1 do -- estimate based on at least 2 ticks + local entry = data[i] + local timeDiff = latest.time - entry.time + if timeDiff > 120 then -- and at least 2 minutes + local rate = (latest.percent - entry.percent) / timeDiff + if rate == 0 then + -- shouldn't happen, but avoid stupid errors when returning infinity + return + end + local totalTime = 100 / rate + local remaining = (100 - latest.percent) / rate + return remaining, totalTime + end + end +end + +function mod:updateStartTimer() + -- Raw data dump example: https://docs.google.com/spreadsheets/d/15K8YfAKg0_cho0Ebj8iOlCCFbwoWj-QLcrDpZBpmuaA/edit#gid=0 + -- Layering can mess this up, we may want to detect large discontinuities in the data and just abort in that case + -- TODO: we may want to consider rate limiting the timer update if it jumps around a lot + -- however, these events here only gets triggered like once per minute and the rate is very stable (see data above) + -- so I haven't observed jumpiness on the timer yet + local aRemaining, aTotal = getEstimate(self.stateTracking.alliance) + local hRemaining, hTotal = getEstimate(self.stateTracking.horde) + if not aRemaining or not hRemaining or not aTotal or not hTotal then + return + end + -- TODO: we can use the estimates to estimate the start time, this should yield the same result if the estimate is good + -- TODO: some people on reddit claimed that once one faction reaches 100% their progress gets added to the other one + -- but all events that I've seen since I started on this code have been very balanced, so I couldn't observe this effect + local remaining = math.max(aRemaining, hRemaining) + local total = math.max(aTotal, hTotal) + if total > 6 * 60 * 60 then -- estimates of > 6 hours total time are probably bad and useless anyways + DBM:Debug("Got total time estimate of " .. total .. ", discarding") + return + end + startTimer:Update(total - remaining, total) + if remaining > 180 then + startTimer:UpdateName(L.TimerEstimate) + else -- last few minutes feel a bit random + startTimer:UpdateName(L.TimerSoon) + end +end + +function mod:startEvent() DBM:Debug("Detected start of Ashenvale event") + startTimer:Stop() local generalMod = DBM:GetModByName("PvPGeneral") generalMod:StopTrackHealth() generalMod:TrackHealth(212804, "RunestoneBoss", true, "YELL", BLUE_FONT_COLOR) @@ -37,22 +100,24 @@ function mod:StartEvent() generalMod:TrackHealth(212969, "BonfireBoss", true, "YELL", RED_FONT_COLOR) end -function mod:StopEvent() +function mod:stopEvent() DBM:Debug("Detected end of Ashenvale event or leaving zone") + startTimer:Stop() local generalMod = DBM:GetModByName("PvPGeneral") generalMod:StopTrackHealth() + self:resetStateTracking() end -function mod:CheckEventState() +function mod:checkEventState() local eventTime = C_UIWidgetManager.GetIconAndTextWidgetVisualizationInfo(5378) if eventTime and eventTime.state ~= Enum.IconAndTextWidgetState.Hidden then if not self.eventRunning then self.eventRunning = true - self:StartEvent() + self:startEvent() end elseif self.eventRunning then self.eventRunning = false - self:StopEvent() + self:stopEvent() end end @@ -61,29 +126,71 @@ function mod:UPDATE_UI_WIDGET(tbl) return end if tbl and widgetIDs[tbl.widgetID] then - self:CheckEventState() + self:checkEventState() + end + if tbl.widgetID == 5360 or tbl.widgetID == 5361 then + local info = C_UIWidgetManager.GetIconAndTextWidgetVisualizationInfo(tbl.widgetID) + local percent = info and info.text and info.text:match("(%d+)") + if percent then + percent = tonumber(percent) + local data = tbl.widgetID == 5360 and self.stateTracking.alliance or self.stateTracking.horde + if data[#data] and data[#data].percent >= 100 then + -- stop updating once it reaches 100. yes it can go down by a few percent(who knows why?), but we don't care + return + end + local time = GetTime() + -- Updates sometimes trigger multiple times with the new and old value mixed together + -- These duplicate triggers happen on the same frame and the latest value seems to be the current one + if data[#data] and data[#data].time == time then + data[#data] = nil + end + if not data[#data] or data[#data].percent ~= percent then + data[#data + 1] = {time = GetTime(), percent = percent} + self:updateStartTimer() + end + end end end -function mod:EnterAshenvale() +function mod:enterAshenvale() self.inZone = true - self:CheckEventState() + self:checkEventState() end -function mod:LeaveAshenvale() +function mod:leaveAshenvale() self.inZone = false - self:StopEvent() + self:stopEvent() end function mod:ZoneChanged() local map = C_Map.GetBestMapForUnit("player") if map == MAP_ASHENVALE and not self.inZone then - self:EnterAshenvale() + self:enterAshenvale() elseif map ~= MAP_ASHENVALE and self.inZone then - self:LeaveAshenvale() + self:leaveAshenvale() end end mod.LOADING_SCREEN_DISABLED = mod.ZoneChanged mod.ZONE_CHANGED_NEW_AREA = mod.ZoneChanged mod.PLAYER_ENTERING_WORLD = mod.ZoneChanged mod.OnInitialize = mod.ZoneChanged + +function mod:DebugExportState() + local export = {"Time,Alliance,Horde"} + local a, h = 1, 1 + while true do + local entryA = self.stateTracking.alliance[a] + local entryH = self.stateTracking.horde[h] + if not entryA and not entryH then + break + end + if not entryH or entryA and entryA.time < entryH.time then + export[#export + 1] = entryA.time .. "," .. entryA.percent + a = a + 1 + else + export[#export + 1] = entryH.time .. ",," .. entryH.percent + h = h + 1 + end + end + DBM:ShowUpdateReminder(nil, nil, "CSV dump of progress data for last event", table.concat(export, "\n")) +end diff --git a/DBM-PvP/localization.de.lua b/DBM-PvP/localization.de.lua index 4b80cfd..1e12b4e 100644 --- a/DBM-PvP/localization.de.lua +++ b/DBM-PvP/localization.de.lua @@ -105,3 +105,17 @@ L:SetMiscLocalization({ OrbTaken = "(%S+) hat die (%S+) Kugel genommen!", OrbReturn = "Die (%S+) Kugel wurde zurückgebracht!" }) + +---------------- +-- Ashenvale -- +---------------- +L = DBM:GetModLocalization("m1440") + +L:SetOptionLocalization({ + EstimatedStartTimer = "Zeige Timer für geschätzte Startzeit des Events" +}) + +L:SetMiscLocalization({ + TimerEstimate = "Event startet", + TimerSoon = "Event startet gleich!", +}) \ No newline at end of file diff --git a/DBM-PvP/localization.en.lua b/DBM-PvP/localization.en.lua index f99cd5e..0de441d 100644 --- a/DBM-PvP/localization.en.lua +++ b/DBM-PvP/localization.en.lua @@ -184,3 +184,17 @@ L:SetMiscLocalization({ OrbTaken = "(%S+) has taken the (%S+) orb!", OrbReturn = "The (%S+) orb has been returned!" }) + +---------------- +-- Ashenvale -- +---------------- +L = DBM:GetModLocalization("m1440") + +L:SetOptionLocalization({ + EstimatedStartTimer = "Show timer for estimated event start time" +}) + +L:SetMiscLocalization({ + TimerEstimate = "Event starts", + TimerSoon = "Event starts soon!", +})