-- Freewood-OctoWoW-Codex: Zeitmessung -- Kampfdauer (Time to kill): wie lange dauert ein Solo-Kampf gegen einen einzelnen Gegner, -- je Klasse, Spielerstufe und Stufenabstand (Gegner minus Spieler, -5 bis +5). -- Quest-Zeit: aktive Spielzeit vom ersten Fortschritt bis zur Abgabebereitschaft, bei mehreren -- gleichzeitig laufenden Quests gleichmaessig aufgeteilt. -- Zeiten werden als ganze Zehntelsekunden gespeichert. -- -- Gespeichert: -- db.ttk[Klasse][Spielerstufe][Abstand] = { n, sum, min, max } (Klasse zum Beispiel "WARRIOR") -- units[npcID].tt[Klasse][Spielerstufe] = { n, sum, min, max } (nur lokal) -- quests[id].tm aufgeteilte aktive Zeit bis fertig, .tr aktive Zeit bis fertig ohne Aufteilen, -- quests[id].ta aktive Zeit bis zur Abgabe -- db.qtimer[questID] = laufende Zeitnehmer (ueberleben Neustart des Spiels) local ADDON, NS = ... local FW = _G.Freewood local Codex = NS and NS.Codex if not FW or not Codex then return end local Util = FW.Util local Time = {} NS.Time = Time Codex.Time = Time local MAX_DELTA = 5 local MIN_KILL, MAX_KILL = 0.5, 120 -- Sekunden local KILL_END_WINDOW = 8 -- Kampf muss kurz nach dem Kill enden local MAX_QUEST_SECONDS = 4 * 3600 local TICK = 1 Time.stats = { fights = 0, ok = 0, skip = {}, quests = 0 } local function Now() return GetTime and GetTime() or time() end local function Safe(fn, ...) local ok, err = pcall(fn, ...) if not ok and FW.Debug then FW:Debug("Codex.Time: " .. tostring(err)) end return ok end local function Skip(reason) Time.stats.skip[reason] = (Time.stats.skip[reason] or 0) + 1 end local function Sample(tbl, key, value) local s = tbl[key] if not s then s = { n = 0, sum = 0 } tbl[key] = s end s.n = s.n + 1 s.sum = s.sum + value s.min = s.min and math.min(s.min, value) or value s.max = s.max and math.max(s.max, value) or value return s end function Time.PlayerToken() local token if UnitClass then local _, t = UnitClass("player") token = t end if type(token) == "string" and token:match("^%u+$") then return token end return nil end -- Klassenname des Spielers in der Spielsprache (fuer Anzeigen) function Time.ClassName() if UnitClass then local name = UnitClass("player") if type(name) == "string" and name ~= "" then return name end end return Time.PlayerToken() or "?" end ------------------------------------------------------------------------ -- Kampfdauer ------------------------------------------------------------------------ local fight = nil local function InGroup() return (IsInGroup and IsInGroup()) or (GetNumGroupMembers and GetNumGroupMembers() > 1) or false end local function InInstance() if IsInInstance then local inside = IsInInstance() return inside and true or false end return false end function Time:NoteTarget() if not fight or not fight.ok then return end if not (UnitExists and UnitExists("target")) then return end if UnitCanAttack and not UnitCanAttack("player", "target") then return end local guid = UnitGUID and UnitGUID("target") if guid == nil or Util.IsSecret(guid) then fight.ok, fight.why = false, "secret" return end if not fight.targets[guid] then fight.targets[guid] = true fight.nTargets = fight.nTargets + 1 if fight.nTargets > 1 then fight.ok, fight.why = false, "multi" return end local classification = UnitClassification and UnitClassification("target") if classification and classification ~= "normal" then fight.ok, fight.why = false, "elite" end local level = UnitLevel and tonumber(UnitLevel("target")) if level and level > 0 then fight.tl = level -- Stufe des Gegners, falls der Erfahrungs-Hook sie nicht kennt end end end function Time:OnRegenDisabled() Safe(function() fight = { start = Now(), targets = {}, nTargets = 0, kills = 0, ok = true } if InGroup() then fight.ok, fight.why = false, "group" elseif InInstance() then fight.ok, fight.why = false, "instance" end Time:NoteTarget() end) end function Time:OnTargetChanged() Safe(Time.NoteTarget, Time) end -- vom Erfahrungs-Hook aufgerufen: ein Gegner ist gestorben (pl, ml: Stufen, id: NPC-ID oder nil) function Time:OnKill(pl, ml, id, classification) if not fight then return end fight.kills = fight.kills + 1 fight.killAt = Now() fight.pl, fight.ml, fight.id = pl, ml or fight.ml, id or fight.id if classification and classification ~= "normal" and fight.ok then fight.ok, fight.why = false, "elite" end end function Time:Record(token, pl, delta, seconds, id) local ds = math.floor(seconds * 10 + 0.5) local db = Codex.db db.ttk = db.ttk or {} db.ttk[token] = db.ttk[token] or {} db.ttk[token][pl] = db.ttk[token][pl] or {} Sample(db.ttk[token][pl], delta, ds) if id then local u = Codex:Unit(id, true) u.tt = u.tt or {} u.tt[token] = u.tt[token] or {} Sample(u.tt[token], pl, ds) end FW:Fire("CODEX_CHANGED", "ttk", pl) end function Time:OnRegenEnabled() Safe(function() local f = fight fight = nil if not f then return end local st = Time.stats st.fights = st.fights + 1 if not f.ok then return Skip(f.why or "other") end if f.kills ~= 1 then return Skip(f.kills == 0 and "nokill" or "kills") end local seconds = f.killAt - f.start if seconds < MIN_KILL then return Skip("short") end if seconds > MAX_KILL then return Skip("long") end if Now() - f.killAt > KILL_END_WINDOW then return Skip("late") end local pl = f.pl or (UnitLevel and tonumber(UnitLevel("player"))) local ml = f.ml or f.tl local delta = ml and pl and (ml - pl) if not delta or math.abs(delta) > MAX_DELTA then return Skip("range") end local token = Time.PlayerToken() if not token then return Skip("class") end st.ok = st.ok + 1 Time:Record(token, pl, delta, seconds, f.id) end) end -- Klassenname zu einem Klassenschluessel ("WARRIOR" -> "Krieger"), sonst der Schluessel selbst function Time.ClassLabel(token) local names = _G.LOCALIZED_CLASS_NAMES_MALE if type(names) == "table" and type(names[token]) == "string" then return names[token] end return tostring(token) end -- Kampfdauer einer Kreatur je Klasse (eigene und geteilte Messungen): -- Liste { cls, secs, n, own } nach Dauer sortiert, Durchschnitt ueber alle Spielerstufen function Time.UnitClasses(u) local list = {} local mine = Time.PlayerToken() for cls, byPl in pairs(u and u.tt or {}) do local n, sum = 0, 0 for _, s in pairs(byPl) do n = n + (s.n or 0) sum = sum + (s.sum or 0) end if n > 0 then list[#list + 1] = { cls = cls, secs = sum / n / 10, n = n, own = cls == mine } end end table.sort(list, function(a, b) if a.secs ~= b.secs then return a.secs < b.secs end return a.cls < b.cls end) return list end -- Durchschnittliche Kampfdauer (Sekunden, Anzahl) einer Kreatur fuer die eigene Klasse function Time.UnitAverage(u) local token = Time.PlayerToken() local byPl = u and u.tt and token and u.tt[token] if not byPl then return nil end local n, sum = 0, 0 for _, s in pairs(byPl) do n = n + (s.n or 0) sum = sum + (s.sum or 0) end if n < 1 then return nil end return sum / n / 10, n end ------------------------------------------------------------------------ -- Quest-Zeit ------------------------------------------------------------------------ local lastTick = nil local ticker = nil local function QTimers() Codex.db.qtimer = Codex.db.qtimer or {} return Codex.db.qtimer end local function LogEntries() local list = {} local count if C_QuestLog and C_QuestLog.GetNumQuestLogEntries then count = C_QuestLog.GetNumQuestLogEntries() elseif GetNumQuestLogEntries then count = GetNumQuestLogEntries() end for i = 1, tonumber(count) or 0 do local questID, isHeader if C_QuestLog and C_QuestLog.GetInfo then local info = C_QuestLog.GetInfo(i) if type(info) == "table" then questID, isHeader = info.questID, info.isHeader end elseif GetQuestLogTitle then local _, _, _, h, _, _, _, id = GetQuestLogTitle(i) questID, isHeader = id, h end if type(questID) == "number" and questID > 0 and not isHeader then list[#list + 1] = questID end end return list end -- Fortschritt (Summe der erfuellten Ziele) und ob alles erledigt ist local function Progress(questID) local have, finished, count = 0, true, 0 if C_QuestLog and C_QuestLog.GetQuestObjectives then local objs = C_QuestLog.GetQuestObjectives(questID) if type(objs) == "table" then for _, o in ipairs(objs) do if type(o) == "table" then count = count + 1 have = have + (tonumber(o.numFulfilled) or 0) if not o.finished then finished = false end end end end end if count == 0 then finished = false end if not finished and C_QuestLog and C_QuestLog.IsComplete then local ok, done = pcall(C_QuestLog.IsComplete, questID) if ok and done and count > 0 then finished = true end end return have, finished end function Time:ScanQuests() Safe(function() local timers = QTimers() local seen = {} for _, questID in ipairs(LogEntries()) do seen[questID] = true local have, finished = Progress(questID) local t = timers[questID] if not t then -- neu im Log: nur ohne vorhandenen Fortschritt kennen wir den Beginn t = { eff = 0, raw = 0, all = 0, have = have, started = false, skip = have > 0 or finished } timers[questID] = t end if not t.skip then if have > (t.have or 0) and not t.started then t.started = true end if finished and t.started and not t.done then t.done = true end end t.have = have end for questID, t in pairs(timers) do if not seen[questID] then if t.done then -- fertige Quests kurz behalten: die Abgabe kann nach dem Verlassen des Logs eintreffen t.goneAt = t.goneAt or Now() if Now() - t.goneAt > 60 then timers[questID] = nil end else timers[questID] = nil end end end end) end function Time:Tick() Safe(function() local now = Now() local dt = lastTick and (now - lastTick) or 0 lastTick = now if dt <= 0 or dt > 3 then return end if UnitIsAFK and UnitIsAFK("player") then return end local timers = QTimers() local active = 0 for _, t in pairs(timers) do if t.started and not t.done and not t.skip then active = active + 1 end end for _, t in pairs(timers) do if t.started and not t.skip then t.all = (t.all or 0) + dt if not t.done then t.eff = t.eff + dt / math.max(1, active) t.raw = t.raw + dt end end end end) end -- Quest abgegeben: Zeiten in die Quest schreiben function Time:OnTurnedIn(questID) Safe(function() questID = tonumber(questID) local t = questID and QTimers()[questID] if not t or t.skip or not t.started then if questID then QTimers()[questID] = nil end return end QTimers()[questID] = nil if not t.done or t.raw < 1 or t.all > MAX_QUEST_SECONDS then return end local q = Codex:Quest(questID, true) local function Add(field, seconds) local ds = math.floor(seconds * 10 + 0.5) q[field] = q[field] or { n = 0, sum = 0 } local s = q[field] s.n = s.n + 1 s.sum = s.sum + ds s.min = s.min and math.min(s.min, ds) or ds s.max = s.max and math.max(s.max, ds) or ds end Add("tm", t.eff) Add("tr", t.raw) Add("ta", t.all) Time.stats.quests = Time.stats.quests + 1 FW:Fire("CODEX_CHANGED", "quest", questID) end) end function Time:OnQuestLog() if self.scanPending then return end self.scanPending = true C_Timer.After(0.5, function() Time.scanPending = nil Time:ScanQuests() end) end ------------------------------------------------------------------------ -- Start / Stop ------------------------------------------------------------------------ function Time:Start() FW:RegisterEvent("PLAYER_REGEN_DISABLED", self.OnRegenDisabled, self) FW:RegisterEvent("PLAYER_REGEN_ENABLED", self.OnRegenEnabled, self) FW:RegisterEvent("PLAYER_TARGET_CHANGED", self.OnTargetChanged, self) FW:RegisterEvent("QUEST_LOG_UPDATE", self.OnQuestLog, self) FW:RegisterEvent("UNIT_QUEST_LOG_CHANGED", self.OnQuestLog, self) FW:RegisterEvent("QUEST_ACCEPTED", self.OnQuestLog, self) FW:RegisterEvent("QUEST_TURNED_IN", self.OnTurnedIn, self) if not ticker and C_Timer and C_Timer.NewTicker then lastTick = Now() ticker = C_Timer.NewTicker(TICK, function() Time:Tick() end) end C_Timer.After(4, function() Time:ScanQuests() end) end function Time:Stop() for _, event in ipairs({ "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED", "PLAYER_TARGET_CHANGED", "QUEST_LOG_UPDATE", "UNIT_QUEST_LOG_CHANGED", "QUEST_ACCEPTED", "QUEST_TURNED_IN" }) do FW:UnregisterEvent(event, self) end if ticker and ticker.Cancel then ticker:Cancel() end ticker = nil fight = nil end function Time:Debug() local st = self.stats local n = 0 for _, byPl in pairs(Codex.db.ttk or {}) do for _, byDelta in pairs(byPl) do for _ in pairs(byDelta) do n = n + 1 end end end local reasons = {} for k, v in pairs(st.skip) do reasons[#reasons + 1] = k .. "=" .. v end table.sort(reasons) local active, running = 0, 0 for _, t in pairs(QTimers()) do running = running + 1 if t.started and not t.done and not t.skip then active = active + 1 end end FW:Print(string.format("Zeit: Kaempfe %d, gewertet %d, Messreihen %d, uebersprungen: %s", st.fights, st.ok, n, #reasons > 0 and table.concat(reasons, " ") or "-")) FW:Print(string.format("Quest-Zeit: Zeitnehmer %d, davon aktiv %d, gespeicherte Abgaben %d", running, active, st.quests)) end