// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerReadyButton : ReadyButton { public new Triangles Triangles => base.Triangles; [Resolved] private MultiplayerClient multiplayerClient { get; set; } [Resolved] private OsuColour colours { get; set; } [CanBeNull] private MultiplayerRoom room => multiplayerClient.Room; private Sample countdownTickSample; private Sample countdownWarnSample; private Sample countdownWarnFinalSample; [BackgroundDependencyLoader] private void load(AudioManager audio) { countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); countdownWarnSample = audio.Samples.Get(@"Multiplayer/countdown-warn"); countdownWarnFinalSample = audio.Samples.Get(@"Multiplayer/countdown-warn-final"); } protected override void LoadComplete() { base.LoadComplete(); multiplayerClient.RoomUpdated += onRoomUpdated; onRoomUpdated(); } private MultiplayerCountdown countdown; private double countdownChangeTime; private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() => Scheduler.AddOnce(() => { MultiplayerCountdown newCountdown; switch (room?.Countdown) { case MatchStartCountdown _: newCountdown = room.Countdown; break; // Clear the countdown with any other (including non-null) countdown values. default: newCountdown = null; break; } if (newCountdown != countdown) { countdown = room?.Countdown; countdownChangeTime = Time.Current; } scheduleNextCountdownUpdate(); updateButtonText(); updateButtonColour(); }); private void scheduleNextCountdownUpdate() { countdownUpdateDelegate?.Cancel(); if (countdown != null) { // The remaining time on a countdown may be at a fractional portion between two seconds. // We want to align certain audio/visual cues to the point at which integer seconds change. // To do so, we schedule to the next whole second. Note that scheduler invocation isn't // guaranteed to be accurate, so this may still occur slightly late, but even in such a case // the next invocation will be roughly correct. double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000; countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond); } else { countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate = null; } void onCountdownTick() { updateButtonText(); int secondsRemaining = countdownTimeRemaining.Seconds; playTickSound(secondsRemaining); if (secondsRemaining > 0) scheduleNextCountdownUpdate(); } } private void playTickSound(int secondsRemaining) { if (secondsRemaining < 10) countdownTickSample?.Play(); if (secondsRemaining <= 3) { if (secondsRemaining > 0) countdownWarnSample?.Play(); else countdownWarnFinalSample?.Play(); } } private void updateButtonText() { if (room == null) { Text = "Ready"; return; } var localUser = multiplayerClient.LocalUser; int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); string countText = $"({countReady} / {countTotal} ready)"; if (countdown != null) { string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}"; switch (localUser?.State) { default: Text = $"Ready ({countdownText.ToLowerInvariant()})"; break; case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: Text = $"{countdownText} {countText}"; break; } } else { switch (localUser?.State) { default: Text = "Ready"; break; case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: Text = room.Host?.Equals(localUser) == true ? $"Start match {countText}" : $"Waiting for host... {countText}"; break; } } } private TimeSpan countdownTimeRemaining { get { double timeElapsed = Time.Current - countdownChangeTime; TimeSpan remaining; if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds) remaining = TimeSpan.Zero; else remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed); return remaining; } } private void updateButtonColour() { if (room == null) { setGreen(); return; } var localUser = multiplayerClient.LocalUser; switch (localUser?.State) { default: setGreen(); break; case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: if (room?.Host?.Equals(localUser) == true && room.Countdown == null) setGreen(); else setYellow(); break; } void setYellow() { BackgroundColour = colours.YellowDark; Triangles.ColourDark = colours.YellowDark; Triangles.ColourLight = colours.Yellow; } void setGreen() { BackgroundColour = colours.Green; Triangles.ColourDark = colours.Green; Triangles.ColourLight = colours.GreenLight; } } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (multiplayerClient != null) multiplayerClient.RoomUpdated -= onRoomUpdated; } public override LocalisableString TooltipText { get { if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled) return "Cancel countdown"; return base.TooltipText; } } } }