// 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.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// /// Provides an area for and manages the hierarchy of a spectated player within a . /// public class PlayerArea : CompositeDrawable { /// /// Raised after is called on . /// public event Action? OnGameplayStarted; /// /// Whether a is loaded in the area. /// public bool PlayerLoaded => (stack?.CurrentScreen as Player)?.IsLoaded == true; /// /// The user id this corresponds to. /// public readonly int UserId; /// /// The used to control the gameplay running state of a loaded . /// public readonly ISpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. /// public Score? Score { get; private set; } [Resolved] private IBindable beatmap { get; set; } = null!; private readonly BindableDouble volumeAdjustment = new BindableDouble(); private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; public PlayerArea(int userId, ISpectatorPlayerClock clock) { UserId = userId; GameplayClock = clock; RelativeSizeAxes = Axes.Both; Masking = true; AudioContainer audioContainer; InternalChildren = new Drawable[] { audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both, Child = gameplayContent = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both }, }, loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } } }; audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); } public void LoadScore(Score score) { if (Score != null) throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score."); Score = score; gameplayContent.Child = new PlayerIsolationContainer(beatmap.Value, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack { Name = nameof(PlayerArea), } }; stack.Push(new MultiSpectatorPlayerLoader(Score, () => { var player = new MultiSpectatorPlayer(Score, GameplayClock); player.OnGameplayStarted += () => OnGameplayStarted?.Invoke(); return player; })); loadingLayer.Hide(); } private bool mute = true; public bool Mute { get => mute; set { mute = value; volumeAdjustment.Value = value ? 0 : 1; } } // Player interferes with global input, so disable input for now. public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; /// /// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings). /// private class PlayerIsolationContainer : Container { [Cached] private readonly Bindable ruleset = new Bindable(); [Cached] private readonly Bindable beatmap = new Bindable(); [Cached] private readonly Bindable> mods = new Bindable>(); public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) { this.beatmap.Value = beatmap; this.ruleset.Value = ruleset; this.mods.Value = mods; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(ruleset.BeginLease(false)); dependencies.CacheAs(beatmap.BeginLease(false)); dependencies.CacheAs(mods.BeginLease(false)); return dependencies; } } } }