// 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 System.Diagnostics; using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Users; using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay { public abstract partial class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen { public string ShortTitle => "song selection"; public override string Title => ShortTitle.Humanize(); public override bool AllowEditing => false; [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } = null!; [Resolved] private RulesetStore rulesets { get; set; } = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); private readonly Room room; private readonly PlaylistItem? initialItem; private readonly FreeModSelectOverlay freeModSelectOverlay; private IDisposable? freeModSelectOverlayRegistration; /// /// Creates a new . /// /// The room. /// An optional initial to use for the initial beatmap/ruleset/mods. /// If null, the last in the room will be used. protected OnlinePlaySongSelect(Room room, PlaylistItem? initialItem = null) { this.room = room; this.initialItem = initialItem ?? room.Playlist.LastOrDefault(); Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; freeModSelectOverlay = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, }; } [BackgroundDependencyLoader] private void load() { LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; LoadComponent(freeModSelectOverlay); } protected override void LoadComplete() { base.LoadComplete(); if (initialItem != null) { // Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection. BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo; // And in the case that this isn't a local databased beatmap, query by online ID. if (beatmapInfo == null) { int onlineId = initialItem.Beatmap.OnlineID; beatmapInfo = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId); } if (beatmapInfo != null) Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); RulesetInfo? ruleset = rulesets.GetRuleset(initialItem.RulesetID); if (ruleset != null) { Ruleset.Value = ruleset; var rulesetInstance = ruleset.CreateInstance(); Debug.Assert(rulesetInstance != null); // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); } private void onModsChanged(ValueChangedEvent> mods) { FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. freeModSelectOverlay.IsValidMod = IsValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) { FreeMods.Value = Array.Empty(); } protected sealed override bool OnStart() { var item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; return SelectItem(item); } /// /// Invoked when the user has requested a selection of a beatmap. /// /// The resultant . This item has not yet been added to the 's. /// true if a selection occurred. protected abstract bool SelectItem(PlaylistItem item); public override bool OnBackButton() { if (freeModSelectOverlay.State.Value == Visibility.Visible) { freeModSelectOverlay.Hide(); return true; } return base.OnBackButton(); } public override bool OnExiting(ScreenExitEvent e) { freeModSelectOverlay.Hide(); return base.OnExiting(e); } protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum) { IsValidMod = IsValidMod }; protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() { var buttons = base.CreateFooterButtons().ToList(); buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); return buttons; } /// /// Checks whether a given is valid for global selection. /// /// The to check. /// Whether is a valid mod for online play. protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); /// /// Checks whether a given is valid for per-player free-mod selection. /// /// The to check. /// Whether is a selectable free-mod. protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); private bool checkCompatibleFreeMod(Mod mod) => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); freeModSelectOverlayRegistration?.Dispose(); } } }