namespace ytLive.Models; /// Owns the session's and the one lifecycle rule that /// matters: a new session starts from zero. /// /// Kept separate from the view-model so the rule is testable without a window, and so the /// scoreboard layer has exactly one place that decides when counting begins. public sealed class ScoreboardRecorder { public ScoreboardState State { get; private set; } = new(); /// Called when a session starts (go-live, test, or a local recording). Every start /// clears the tallies — "session" is the only scope the numbers promise. public void StartSession() { State = new ScoreboardState(); IsRunning = true; RaiseChanged(); } /// Folds a chat event in. Outside a session nothing is counted: a stale tally left over /// from the last stream would be the "today" lie this design exists to avoid. public void Record(ChatMessage message) { if (!IsRunning) return; var before = Fingerprint(State); State.Apply(message); if (Fingerprint(State) != before) RaiseChanged(); } /// True while the tallies are live. public bool IsRunning { get; private set; } /// Marks the session finished. The tallies stay visible (the End screen may show /// them) but stop moving. public void EndSession() { IsRunning = false; RaiseChanged(); } public event System.Action? Changed; private void RaiseChanged() => Changed?.Invoke(); /// Cheap change detector: only re-render the strip when a displayed value moved. private static string Fingerprint(ScoreboardState state) => $"{state.NewMembers}|{state.MilestoneCount}|{state.LastMemberName}|{state.LastGifterName}|" + $"{state.LastGiftCount}|{state.LastSuperChatAuthor}|{state.LastSuperChatDisplay}|" + $"{state.SessionTotal.Currency}|{state.SessionTotal.Total:0.######}"; }