1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 03:20:16 +08:00

Merge branch 'master' into matchmaking-jumpy-jump

This commit is contained in:
Jamie Taylor
2025-10-29 15:34:50 +09:00
Unverified
47 changed files with 792 additions and 254 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1021.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1 -1
View File
@@ -189,7 +189,7 @@ namespace osu.Desktop
}
// user party
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
{
MultiplayerRoom room = multiplayerClient.Room;
@@ -7,5 +7,15 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
{
public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
// Use larger extended limits for mania to include OD values that occur with EZ or HR enabled
ExtendedMaxValue = 15,
ExtendedMinValue = -15,
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
};
}
}
@@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay();
protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay();
}
@@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene
{
private MatchmakingChatDisplay? chat;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add chat", () =>
{
chat?.Expire();
ScreenFooter.Add(chat = new MatchmakingChatDisplay(new Room())
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Size = new Vector2(700, 130),
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Alpha = 0
});
});
AddStep("show footer", () => ScreenFooter.Show());
}
[Test]
public void TestAppearDisappear()
{
AddStep("appear", () => chat!.Appear());
AddWaitStep("wait for animation", 3);
AddStep("disappear", () => chat!.Disappear());
AddWaitStep("wait for animation", 3);
}
}
}
@@ -102,5 +102,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
}
[Test]
public void TestQuit()
{
AddToggleStep("toggle quit", quit => panel.HasQuit = quit);
}
}
}
@@ -118,9 +118,12 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
AddUntilStep("two panels displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(2));
AddAssert("no panels quit", () => this.ChildrenOfType<PlayerPanel>().Count(p => p.HasQuit), () => Is.EqualTo(0));
AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(1));
AddUntilStep("one panel quit", () => this.ChildrenOfType<PlayerPanel>().Count(p => p.HasQuit), () => Is.EqualTo(1));
AddAssert("two panels still displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(2));
}
[Test]
@@ -471,7 +471,7 @@ namespace osu.Game.Tests.Visual.Online
public DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child;
public ChannelScrollContainer ScrollContainer => DrawableChannel.ChildrenOfType<ChannelScrollContainer>().Single();
public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
@@ -373,12 +373,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
#endregion
private static async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
#region Favourites grouping
[Test]
public async Task TestFavouritesGrouping()
{
var groupingFilter = new BeatmapCarouselFilterGrouping(
() => new FilterCriteria { Group = group },
() => new List<BeatmapCollection>(),
_ => new Dictionary<Guid, ScoreRank>());
int total = 0;
var beatmapSets = new List<BeatmapSetInfo>();
addBeatmapSet(s => s.OnlineID = 1, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 21, beatmapSets, out var firstFavourite);
addBeatmapSet(s => s.OnlineID = 321, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 4321, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 54321, beatmapSets, out var secondFavourite);
favouriteBeatmapSets = [21, 54321];
var results = await runGrouping(GroupMode.Favourites, beatmapSets);
assertGroup(results, 0, "Favourites", firstFavourite.Beatmaps.Concat(secondFavourite.Beatmaps), ref total);
assertTotal(results, total);
}
#endregion
private HashSet<int> favouriteBeatmapSets = [];
private async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
{
var groupingFilter = new BeatmapCarouselFilterGrouping
{
GetCriteria = () => new FilterCriteria { Group = group },
GetCollections = () => new List<BeatmapCollection>(),
GetLocalUserTopRanks = _ => new Dictionary<Guid, ScoreRank>(),
GetFavouriteBeatmapSets = () => favouriteBeatmapSets,
};
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
}
@@ -88,6 +88,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last()));
}
[Test]
public void TestFilterSingleResult_ReselectedAfterRulesetSwitches()
{
LoadSongSelect();
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
AddStep("set filter text", () => filterTextBox.Current.Value = $"\"{Beatmaps.GetAllUsableBeatmapSets().Last().Metadata.Title}\"");
AddWaitStep("wait for debounce", 5);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.First()));
AddStep("select last difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapSetInfo.Beatmaps.Last()));
AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last()));
ChangeRuleset(1);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is default", () => Beatmap.IsDefault);
ChangeRuleset(0);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last()));
}
[Test]
public void TestFilterOnResumeAfterChange()
{
@@ -62,6 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
SetLoading(false);
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
favouriteRequest.Failure += e =>
{
+6
View File
@@ -238,10 +238,12 @@ namespace osu.Game.Online.API
public BindableList<APIRelation> Friends { get; } = new BindableList<APIRelation>();
public BindableList<APIRelation> Blocks { get; } = new BindableList<APIRelation>();
public BindableList<int> FavouriteBeatmapSets { get; } = new BindableList<int>();
IBindable<APIUser> ILocalUserState.User => User;
IBindableList<APIRelation> ILocalUserState.Friends => Friends;
IBindableList<APIRelation> ILocalUserState.Blocks => Blocks;
IBindableList<int> ILocalUserState.FavouriteBeatmapSets => FavouriteBeatmapSets;
public void UpdateFriends()
{
@@ -250,6 +252,10 @@ namespace osu.Game.Online.API
public void UpdateBlocks()
{
}
public void UpdateFavouriteBeatmapSets()
{
}
}
}
}
+2
View File
@@ -11,8 +11,10 @@ namespace osu.Game.Online.API
IBindable<APIUser> User { get; }
IBindableList<APIRelation> Friends { get; }
IBindableList<APIRelation> Blocks { get; }
IBindableList<int> FavouriteBeatmapSets { get; }
void UpdateFriends();
void UpdateBlocks();
void UpdateFavouriteBeatmapSets();
}
}
+22
View File
@@ -16,12 +16,14 @@ namespace osu.Game.Online.API
public IBindable<APIUser> User => localUser;
public IBindableList<APIRelation> Friends => friends;
public IBindableList<APIRelation> Blocks => blocks;
public IBindableList<int> FavouriteBeatmapSets => favouriteBeatmapSets;
private readonly IAPIProvider api;
private readonly Bindable<APIUser> localUser = new Bindable<APIUser>(createGuestUser());
private readonly BindableList<APIRelation> friends = new BindableList<APIRelation>();
private readonly BindableList<APIRelation> blocks = new BindableList<APIRelation>();
private readonly BindableList<int> favouriteBeatmapSets = new BindableList<int>();
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
@@ -62,6 +64,7 @@ namespace osu.Game.Online.API
UpdateFriends();
UpdateBlocks();
UpdateFavouriteBeatmapSets();
}
public void ClearLocalUser()
@@ -76,6 +79,7 @@ namespace osu.Game.Online.API
configSupporter.Value = false;
friends.Clear();
blocks.Clear();
favouriteBeatmapSets.Clear();
});
}
@@ -125,5 +129,23 @@ namespace osu.Game.Online.API
api.Queue(blocksReq);
}
public void UpdateFavouriteBeatmapSets()
{
if (!api.IsLoggedIn)
return;
var favouritesReq = new GetMyFavouriteBeatmapSetsRequest();
favouritesReq.Success += res =>
{
var existingBeatmapSets = favouriteBeatmapSets.ToHashSet();
var updatedBeatmapSets = res.BeatmapSetIds.ToHashSet();
favouriteBeatmapSets.AddRange(updatedBeatmapSets.Except(existingBeatmapSets));
favouriteBeatmapSets.RemoveAll(b => !updatedBeatmapSets.Contains(b));
};
api.Queue(favouritesReq);
}
}
}
@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetMyFavouriteBeatmapSetsRequest : APIRequest<GetMyFavouriteBeatmapSetsResponse>
{
protected override string Target => @"me/beatmapset-favourites";
}
}
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class GetMyFavouriteBeatmapSetsResponse
{
[JsonProperty("beatmapset_ids")]
public int[] BeatmapSetIds { get; set; } = [];
}
}
@@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata
userStatus.BindValueChanged(status =>
{
if (localUser.Value is not GuestUser)
UpdateStatus(status.NewValue);
UpdateStatus(status.NewValue).FireAndForget();
}, true);
userActivity.BindValueChanged(activity =>
{
if (localUser.Value is not GuestUser)
UpdateActivity(activity.NewValue);
UpdateActivity(activity.NewValue).FireAndForget();
}, true);
}
@@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata
if (localUser.Value is not GuestUser)
{
UpdateActivity(userActivity.Value);
UpdateStatus(userStatus.Value);
UpdateActivity(userActivity.Value).FireAndForget();
UpdateStatus(userStatus.Value).FireAndForget();
}
if (lastQueueId.Value >= 0)
@@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue)
{
if (Room != null)
LeaveRoom();
LeaveRoom().FireAndForget();
MatchmakingQueueLeft?.Invoke();
}
@@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer
return;
if (user.Equals(LocalUser))
LeaveRoom();
LeaveRoom().FireAndForget();
handleUserLeft(user, UserKicked);
});
+5 -4
View File
@@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() => DisconnectInternal());
Schedule(() => DisconnectInternal().FireAndForget());
return Task.CompletedTask;
}
@@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator
else
currentState.State = SpectatedUserState.Quit;
EndPlayingInternal(currentState);
EndPlayingInternal(currentState).FireAndForget();
});
}
@@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator
return;
}
WatchUserInternal(userId);
WatchUserInternal(userId).FireAndForget();
}
public void StopWatchingUser(int userId)
@@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator
watchedUsersRefCounts.Remove(userId);
watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
StopWatchingUserInternal(userId).FireAndForget();
});
}
@@ -74,6 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
{
favourited.Toggle();
loading.Hide();
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
request.Failure += e =>
+11 -17
View File
@@ -12,7 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.Chat;
using osuTK.Graphics;
@@ -49,25 +48,20 @@ namespace osu.Game.Overlays.Chat
[BackgroundDependencyLoader]
private void load()
{
Child = new OsuContextMenuContainer
Child = scroll = new ChannelScrollContainer
{
ScrollbarVisible = scrollbarVisible,
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = scroll = new ChannelScrollContainer
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
ScrollbarVisible = scrollbarVisible,
RelativeSizeAxes = Axes.Both,
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
},
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
};
newMessagesArrived(Channel.Messages);
+6 -1
View File
@@ -19,6 +19,7 @@ using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online;
@@ -142,9 +143,13 @@ namespace osu.Game.Overlays
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = currentChannelContainer = new Container<DrawableChannel>
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
}
}
},
loading = new LoadingLayer(true),
@@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
using osu.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
@@ -19,9 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader;
[Resolved]
private AudioManager audio { get; set; }
private AudioManager audio { get; set; } = null!;
private SettingsDropdown<string> dropdown;
private SettingsDropdown<string> dropdown = null!;
private SettingsCheckbox? wasapiExperimental;
[BackgroundDependencyLoader]
private void load()
@@ -32,17 +34,44 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
LabelText = AudioSettingsStrings.OutputDevice,
Keywords = new[] { "speaker", "headphone", "output" }
}
},
};
updateItems();
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(wasapiExperimental = new SettingsCheckbox
{
LabelText = "Use experimental audio mode",
TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.",
Current = audio.UseExperimentalWasapi,
Keywords = new[] { "wasapi", "latency", "exclusive" }
});
wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
}
audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged;
dropdown.Current = audio.AudioDevice;
onDeviceChanged(string.Empty);
}
private void onDeviceChanged(string name) => updateItems();
private void onDeviceChanged(string _)
{
updateItems();
if (wasapiExperimental != null)
{
if (wasapiExperimental.Current.Value)
{
wasapiExperimental.SetNoticeText(
"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true);
}
else
wasapiExperimental.ClearNoticeText();
}
}
private void updateItems()
{
@@ -61,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
// functionality would require involved OS-specific code.
dropdown.Items = deviceItems
// Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271)
.Where(i => i != null)
.Where(i => i.IsNotNull())
.Distinct()
.ToList();
}
@@ -70,7 +99,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
base.Dispose(isDisposing);
if (audio != null)
if (audio.IsNotNull())
{
audio.OnNewDevice -= onDeviceChanged;
audio.OnLostDevice -= onDeviceChanged;
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
public virtual DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
+25 -13
View File
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer
private Box background = null!;
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
private Container footerContentContainer = null!;
private Container overlayContentContainer = null!;
private Container<ScreenFooterButton> hiddenButtonsContainer = null!;
private LogoTrackingContainer logoTrackingContainer = null!;
@@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer
{
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
{
Name = "Visible footer buttons",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = ScreenFooterButton.CORNER_RADIUS,
@@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer
Spacing = new Vector2(7, 0),
AutoSizeAxes = Axes.Both,
},
footerContentContainer = new Container
overlayContentContainer = new Container
{
Name = "Overlay-provided extra content",
RelativeSizeAxes = Axes.Both,
Y = -OsuGame.SCREEN_EDGE_MARGIN,
},
@@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer
},
hiddenButtonsContainer = new Container<ScreenFooterButton>
{
Name = "Hidden footer buttons",
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
Y = ScreenFooterButton.CORNER_RADIUS,
Anchor = Anchor.BottomLeft,
@@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer
public ShearedOverlayContainer? ActiveOverlay { get; private set; }
private VisibilityContainer? activeFooterContent;
private VisibilityContainer? activeOverlayContent;
private readonly List<ScreenFooterButton> temporarilyHiddenButtons = new List<ScreenFooterButton>();
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent)
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent)
{
if (ActiveOverlay != null)
{
@@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer
updateColourScheme(overlay.ColourProvider.Hue);
footerContent = overlay.CreateFooterContent();
activeFooterContent = footerContent;
var content = footerContent;
overlayContent = overlay.CreateFooterContent();
activeOverlayContent = overlayContent;
var content = overlayContent;
if (content != null)
footerContentContainer.Child = content;
overlayContentContainer.Child = content;
if (temporarilyHiddenButtons.Count > 0)
this.Delay(60).Schedule(() => content?.Show());
@@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer
if (ActiveOverlay == null)
return;
Debug.Assert(activeFooterContent != null);
activeFooterContent.Hide();
Debug.Assert(activeOverlayContent != null);
activeOverlayContent.Hide();
double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current;
double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current;
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
{
var button = temporarilyHiddenButtons[i];
hiddenButtonsContainer.Remove(button, false);
// temporarily bypass autosize on the X axis to prevent the buttons taking space
// immediately upon being moved back to the flow.
// this prevents the overlay content jumping to the right during its fade-out.
button.BypassAutoSizeAxes = Axes.X;
buttonsFlow.Add(button);
makeButtonAppearFromBottom(button, 0);
@@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer
updateColourScheme(OverlayColourScheme.Aquamarine.GetHue());
activeFooterContent.Delay(timeUntilRun).Expire();
activeFooterContent = null;
activeOverlayContent.Delay(timeUntilRun).Schedule(() =>
{
// overlay content is done displaying, re-enable autosize on all active buttons
foreach (var button in buttonsFlow)
button.BypassAutoSizeAxes = Axes.None;
}).Expire();
activeOverlayContent = null;
ActiveOverlay = null;
}
@@ -41,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
Padding = new MarginPadding(-2),
Child = new FastCircle
{
RelativeSizeAxes = Axes.Both,
@@ -50,20 +49,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
});
}
AddInternal(new CircularContainer
AddInternal(new Container
{
Padding = new MarginPadding(2),
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
Child = new CircularContainer
{
new Box
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.LightSlateGray,
},
new ClickableAvatar(user, true)
{
RelativeSizeAxes = Axes.Both,
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.LightSlateGray,
},
new ClickableAvatar(user, true)
{
RelativeSizeAxes = Axes.Both,
}
}
}
});
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input;
@@ -66,5 +68,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
public void Appear()
{
FinishTransforms();
this.MoveToY(150f)
.FadeOut()
.MoveToY(0f, 240, Easing.OutCubic)
.FadeIn(240, Easing.OutCubic);
}
public TransformSequence<MatchmakingChatDisplay> Disappear()
{
FinishTransforms();
return this.FadeOut(240, Easing.InOutCubic)
.MoveToY(150f, 240, Easing.InOutCubic);
}
}
}
@@ -51,6 +51,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public readonly MultiplayerRoomUser RoomUser;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action? Action;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
@@ -84,47 +90,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
[Resolved]
private MetadataClient? metadataClient { get; set; }
public readonly APIUser User;
private readonly Action viewProfile;
private OsuSpriteText rankText = null!;
private OsuSpriteText scoreText = null!;
private Drawable avatarPositionTarget = null!;
private Drawable avatarJumpTarget = null!;
private MatchmakingAvatar avatar = null!;
private Drawable avatar = null!;
private OsuSpriteText username = null!;
private Container mainContent = null!;
private Box solidBackgroundLayer = null!;
private Drawable background = null!;
private OsuSpriteText quitText = null!;
private BufferedContainer backgroundQuitTarget = null!;
private BufferedContainer avatarQuitTarget = null!;
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
private bool hasQuit;
private Sample? jumpSample;
private SampleChannel? jumpSampleChannel;
private double samplePitch;
public PlayerPanelDisplayMode DisplayMode
{
get => displayMode;
set
{
displayMode = value;
if (IsLoaded)
updateLayout(false);
}
}
public readonly APIUser User;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action? Action;
protected Action ViewProfile { get; private set; } = null!;
public Box SolidBackgroundLayer { get; private set; } = null!;
protected Drawable? Background { get; private set; }
public PlayerPanel(MultiplayerRoomUser user)
: base(HoverSampleSet.Button)
{
@@ -132,100 +124,126 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
User = user.User;
RoomUser = user;
base.Action = viewProfile = () =>
{
Action?.Invoke();
profileOverlay?.ShowUser(User);
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Add(SolidBackgroundLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background5 ?? colours.Gray1
});
Background = new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.Gray7,
User = User
};
if (Background != null)
Add(Background);
base.Action = ViewProfile = () =>
{
Action?.Invoke();
profileOverlay?.ShowUser(User);
};
Content.Masking = true;
Content.CornerRadius = 10;
Content.CornerExponent = 10;
Content.Anchor = Anchor.Centre;
Content.Origin = Anchor.Centre;
Add(new Container
Child = backgroundQuitTarget = new BufferedContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FrameBufferScale = new Vector2(1.5f),
RelativeSizeAxes = Axes.Both,
Child = mainContent = new Container
Children = new[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
solidBackgroundLayer = new Box
{
avatarPositionTarget = new Container
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background5 ?? colours.Gray1
},
background = new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.Gray7,
User = User
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = mainContent = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = avatar_size,
Child = avatarJumpTarget = new Container
RelativeSizeAxes = Axes.Both,
Children = new[]
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
quitText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = Vector2.One
Text = "QUIT",
Font = OsuFont.Default.With(weight: "Bold", size: 70),
Rotation = -22.5f,
Colour = OsuColour.Gray(0.3f),
Blending = BlendingParameters.Additive
},
avatarPositionTarget = new Container
{
Origin = Anchor.Centre,
Size = avatar_size,
Child = avatarJumpTarget = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Child = avatar = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
// Needs to be re-buffered as the avatar is proxied outside of the parent buffered container.
Child = avatarQuitTarget = new BufferedContainer
{
FrameBufferScale = new Vector2(1.5f),
RelativeSizeAxes = Axes.Both,
Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = Vector2.One
}
}
},
}
},
rankText = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomCentre,
Blending = BlendingParameters.Additive,
Margin = new MarginPadding(4),
Text = "-",
Font = OsuFont.Style.Title.With(size: 55),
},
username = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Text = User.Username,
Font = OsuFont.Style.Heading1,
},
scoreText = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(10),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Style.Heading2,
Text = "0 pts"
}
}
},
rankText = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomCentre,
Blending = BlendingParameters.Additive,
Margin = new MarginPadding(4),
Text = "-",
Font = OsuFont.Style.Title.With(size: 55),
},
username = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Text = User.Username,
Font = OsuFont.Style.Heading1,
},
scoreText = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(10),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Style.Heading2,
Text = "0 pts"
}
}
}
});
};
// Allow avatar to exist outside of masking for when it jumps around and stuff.
AddInternal(avatar.CreateProxy());
@@ -252,6 +270,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
samplePitch = 0.75f + RNG.NextDouble(0f, 0.5f);
}
public PlayerPanelDisplayMode DisplayMode
{
get => displayMode;
set
{
displayMode = value;
if (IsLoaded)
updateLayout(false);
}
}
public bool HasQuit
{
get => hasQuit;
set
{
hasQuit = value;
if (IsLoaded)
updateLayout(false);
}
}
private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal;
private Vector2 avatarPosition
@@ -288,16 +328,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
scoreText.Hide();
username.Hide();
Background.FadeOut(200, Easing.OutQuint);
SolidBackgroundLayer.FadeOut(200, Easing.OutQuint);
background.FadeOut(200, Easing.OutQuint);
solidBackgroundLayer.FadeOut(200, Easing.OutQuint);
this.ResizeTo(avatar_size, duration, Easing.OutPow10);
break;
case PlayerPanelDisplayMode.Horizontal:
case PlayerPanelDisplayMode.Vertical:
Background.FadeIn(200);
SolidBackgroundLayer.FadeIn(200);
background.FadeIn(200);
solidBackgroundLayer.FadeIn(200);
using (BeginDelayedSequence(100))
{
@@ -319,11 +359,37 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
rankText.MoveTo(horizontal ? new Vector2(-40, -20) : new Vector2(-70, 0), duration, Easing.OutPow10);
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
quitText.MoveTo(horizontal ? new Vector2(40, 0) : new Vector2(0, 40), duration, Easing.OutPow10);
break;
default:
throw new ArgumentOutOfRangeException();
}
// quit text doesn't fit on avataronly mode.
if (HasQuit && displayMode != PlayerPanelDisplayMode.AvatarOnly)
quitText.FadeIn(duration, Easing.OutPow10);
else
quitText.FadeOut(duration, Easing.OutPow10);
if (HasQuit)
{
backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
}
else
{
backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
}
}
protected override void Update()
{
base.Update();
// Not sure why this is required but it is.
avatarQuitTarget.Alpha = Alpha;
}
protected override bool OnHover(HoverEvent e)
@@ -454,7 +520,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile)
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, viewProfile)
};
if (User.Equals(api.LocalUser.Value))
@@ -122,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() =>
{
panels.Single(p => p.RoomUser.Equals(user)).Expire();
panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true;
updateDisplay();
});
@@ -29,10 +29,10 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Users;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
@@ -87,19 +87,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private MusicController music { get; set; } = null!;
private readonly MultiplayerRoom room;
private readonly MatchmakingChatDisplay chat;
private Sample? sampleStart;
private CancellationTokenSource? downloadCheckCancellation;
private int? lastDownloadCheckedBeatmapId;
private MatchChatDisplay chat = null!;
public ScreenMatchmaking(MultiplayerRoom room)
{
this.room = room;
Activity.Value = new UserActivity.InLobby(room);
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
chat = new MatchmakingChatDisplay(new Room(room))
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Size = new Vector2(700, 130),
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING },
Alpha = 0
};
}
[BackgroundDependencyLoader]
@@ -156,13 +164,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 700,
Height = 130,
Padding = new MarginPadding { Bottom = row_padding },
Child = chat = new MatchmakingChatDisplay(new Room(room))
{
RelativeSizeAxes = Axes.Both,
}
Size = new Vector2(700, 130),
Margin = new MarginPadding { Bottom = row_padding }
}
]
}
@@ -183,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
Footer!.Add(chat.CreateProxy());
Footer?.Add(new ChatContainer(chat));
}
private void onRoomUpdated()
@@ -326,12 +329,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
chat.Appear();
beginHandlingTrack();
}
public override void OnSuspending(ScreenTransitionEvent e)
{
onLeaving();
chat.Disappear();
endHandlingTrack();
base.OnSuspending(e);
}
@@ -347,7 +354,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
return true;
}
onLeaving();
chat.Disappear().Expire();
endHandlingTrack();
client.LeaveRoom().FireAndForget();
return false;
}
@@ -370,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
chat.Appear();
beginHandlingTrack();
if (e.Last is not MultiplayerPlayerLoader playerLoader)
@@ -381,12 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
return;
}
client.ChangeState(MultiplayerUserState.Idle);
}
private void onLeaving()
{
endHandlingTrack();
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
}
/// <summary>
@@ -439,5 +445,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
client.LoadRequested -= onLoadRequested;
}
}
// Contains the chat display and a context menu container for it. Shared lifetime with the chat display (expires along with it).
private partial class ChatContainer : CompositeDrawable
{
public override double LifetimeStart => chat.LifetimeStart;
public override double LifetimeEnd => chat.LifetimeEnd;
private readonly MatchmakingChatDisplay chat;
public ChatContainer(MatchmakingChatDisplay chat)
{
this.chat = chat;
Anchor = Anchor.BottomRight;
Origin = Anchor.BottomRight;
// This component is added to the screen footer which is only about 50px high.
// Therefore, it's given a large absolute size to give the context menu enough space to display correctly.
Size = new Vector2(700);
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = chat
};
}
}
}
}
@@ -0,0 +1,106 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
public partial class StageDisplay
{
public partial class TimerText : CompositeDrawable
{
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private OsuSpriteText text = null!;
private DateTimeOffset countdownEndTime;
public TimerText()
{
AutoSizeAxes = Axes.X;
Height = 18;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = text = new OsuSpriteText
{
Height = 18,
Spacing = new Vector2(-1, 0),
Font = OsuFont.Style.Heading2.With(fixedWidth: true),
AlwaysPresent = true,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
client.CountdownStarted += onCountdownStarted;
client.CountdownStopped += onCountdownStopped;
if (client.Room != null)
{
foreach (var countdown in client.Room.ActiveCountdowns)
onCountdownStarted(countdown);
}
}
protected override void Update()
{
base.Update();
TimeSpan remaining = countdownEndTime - DateTimeOffset.Now;
text.Alpha = remaining.TotalSeconds > 0 ? 1f : 0.2f;
if (remaining.TotalSeconds > 10)
text.Font = text.Font.With(weight: FontWeight.SemiBold);
else
text.Font = text.Font.With(weight: FontWeight.Bold);
int minutes = (int)Math.Max(0, remaining.TotalMinutes);
int seconds = Math.Max(0, remaining.Seconds);
int ms = Math.Max(0, remaining.Milliseconds);
text.Text = $"{minutes:00}:{seconds:00}.{ms:000}";
}
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
{
if (countdown is MatchmakingStageCountdown)
countdownEndTime = DateTimeOffset.Now + countdown.TimeRemaining;
});
private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() =>
{
if (countdown is not MatchmakingStageCountdown)
return;
countdownEndTime = DateTimeOffset.Now;
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
client.CountdownStarted -= onCountdownStarted;
client.CountdownStopped -= onCountdownStopped;
}
}
}
}
}
@@ -72,6 +72,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
Direction = FillDirection.Horizontal,
},
},
new TimerText
{
Y = -38,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
new StatusText
{
Y = 32,
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@@ -32,11 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
[Resolved]
private INotificationOverlay? notifications { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
private ProgressNotification? backgroundNotification;
private Notification? readyNotification;
private BackgroundQueueNotification? backgroundNotification;
private bool isBackgrounded;
protected override void LoadComplete()
@@ -118,27 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
return;
notifications?.Post(backgroundNotification = new ProgressNotification
{
Text = "Searching for opponents...",
CompletionTarget = n => notifications.Post(readyNotification = n),
CompletionText = "Your match is ready! Click to join.",
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
performer?.PerformFromScreen(s => s.Push(new IntroScreen()));
closeNotifications();
return true;
},
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
closeNotifications();
return true;
}
});
notifications?.Post(backgroundNotification = new BackgroundQueueNotification());
}
private void closeNotifications()
@@ -146,13 +124,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
if (backgroundNotification != null)
{
backgroundNotification.State = ProgressNotificationState.Cancelled;
backgroundNotification.Close(false);
backgroundNotification.CloseAll();
backgroundNotification = null;
}
readyNotification?.Close(false);
backgroundNotification = null;
readyNotification = null;
}
protected override void Dispose(bool isDisposing)
@@ -168,5 +142,66 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
client.MatchmakingRoomReady -= onMatchmakingRoomReady;
}
}
private partial class BackgroundQueueNotification : ProgressNotification
{
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Notification? foundNotification;
private Sample? matchFoundSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Text = "Searching for opponents...";
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
performer?.PerformFromScreen(s => s.Push(new IntroScreen()));
Close(false);
return true;
};
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
return true;
};
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
}
protected override Notification CreateCompletionNotification()
{
// Playing here means it will play even if notification overlay is hidden.
//
// If we add support for the completion notification to be processed during gameplay,
// this can be moved inside the `MatchFoundNotification` implementation.
matchFoundSample?.Play();
return foundNotification = new MatchFoundNotification
{
Activated = CompletionClickAction,
Text = "Your match is ready! Click to join.",
};
}
public void CloseAll()
{
foundNotification?.Close(false);
Close(false);
}
public partial class MatchFoundNotification : ProgressCompletionNotification
{
// for future use.
}
}
}
}
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.LocalUser != null);
if (client.LocalUser.State == MultiplayerUserState.Results)
client.ChangeState(MultiplayerUserState.Idle);
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
}
protected override string ScreenTitle => "Multiplayer";
@@ -618,7 +618,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
updateGameplayState();
if (client.LocalUser.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
break;
}
}
@@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.LocalUser?.State == MultiplayerUserState.Loaded)
{
loadingDisplay.Show();
client.ChangeState(MultiplayerUserState.ReadyForGameplay);
client.ChangeState(MultiplayerUserState.ReadyForGameplay).FireAndForget();
}
// This will pause the clock, pending the gameplay started callback from the server.
@@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// On a manual exit, set the player back to idle unless gameplay has finished.
// Of note, this doesn't cover exiting using alt-f4 or menu home option.
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
multiplayerClient.ChangeState(MultiplayerUserState.Idle).FireAndForget();
return base.OnBackButton();
}
@@ -109,6 +109,7 @@ namespace osu.Game.Screens.Ranking
Enabled.Value = true;
loading.Hide();
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
favouriteRequest.Failure += e =>
{
+2 -2
View File
@@ -32,8 +32,8 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Difficulty))]
Difficulty,
// [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))]
// Favourites,
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.Favourites))]
Favourites,
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LastPlayed))]
LastPlayed,
+31 -3
View File
@@ -28,6 +28,7 @@ using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Select;
@@ -105,7 +106,13 @@ namespace osu.Game.Screens.SelectV2
{
new BeatmapCarouselFilterMatching(() => Criteria!),
new BeatmapCarouselFilterSorting(() => Criteria!),
grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, GetAllCollections, GetBeatmapInfoGuidToTopRankMapping)
grouping = new BeatmapCarouselFilterGrouping
{
GetCriteria = () => Criteria!,
GetCollections = GetAllCollections,
GetLocalUserTopRanks = GetBeatmapInfoGuidToTopRankMapping,
GetFavouriteBeatmapSets = GetFavouriteBeatmapSets,
}
};
AddInternal(loading = new LoadingLayer());
@@ -554,8 +561,19 @@ namespace osu.Game.Screens.SelectV2
var beatmaps = items.Select(i => i.Model).OfType<GroupedBeatmap>();
if (beatmaps.Any(b => b.Equals(CurrentSelection as GroupedBeatmap)))
// do not request recommended selection if the user already had selected a difficulty within the single filtered beatmap set,
// as it could change the difficulty that will be selected
var preexistingSelection = beatmaps.FirstOrDefault(b => b.Equals(CurrentSelection as GroupedBeatmap));
if (preexistingSelection != null)
{
// the selection might not have an item associated with it, if it was fully filtered away previously
// in this case, request to reselect it
if (CurrentSelectionItem == null)
RequestSelection(preexistingSelection);
return;
}
RequestRecommendedSelection(beatmaps);
}
@@ -804,11 +822,14 @@ namespace osu.Game.Screens.SelectV2
#endregion
#region Database fetches for grouping support
#region Fetches for grouping support
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
/// <remarks>
/// FOOTGUN WARNING: this being sorted on the realm side before detaching is IMPORTANT.
/// realm supports sorting as an internal operation, and realm's implementation of string sorting does NOT match dotnet's
@@ -841,6 +862,13 @@ namespace osu.Game.Screens.SelectV2
return topRankMapping;
});
/// <remarks>
/// Note that calling <c>.ToHashSet()</c> below has two purposes:
/// one being performance of contain checks in filtering code,
/// another being slightly better thread safety (as <see cref="ILocalUserState.FavouriteBeatmapSets"/> could be mutated during async filtering).
/// </remarks>
protected HashSet<int> GetFavouriteBeatmapSets() => api.LocalUserState.FavouriteBeatmapSets.ToHashSet();
#endregion
#region Drawable pooling
@@ -39,17 +39,10 @@ namespace osu.Game.Screens.SelectV2
private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>();
private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
private readonly Func<FilterCriteria> getCriteria;
private readonly Func<List<BeatmapCollection>> getCollections;
private readonly Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks;
public BeatmapCarouselFilterGrouping(Func<FilterCriteria> getCriteria, Func<List<BeatmapCollection>> getCollections,
Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> getLocalUserTopRanks)
{
this.getCriteria = getCriteria;
this.getCollections = getCollections;
this.getLocalUserTopRanks = getLocalUserTopRanks;
}
public required Func<FilterCriteria> GetCriteria { get; init; }
public required Func<List<BeatmapCollection>> GetCollections { get; init; }
public required Func<FilterCriteria, IReadOnlyDictionary<Guid, ScoreRank>> GetLocalUserTopRanks { get; init; }
public required Func<HashSet<int>> GetFavouriteBeatmapSets { get; init; }
public async Task<List<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken)
{
@@ -59,7 +52,7 @@ namespace osu.Game.Screens.SelectV2
var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count);
var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count);
var criteria = getCriteria();
var criteria = GetCriteria();
var newItems = new List<CarouselItem>();
BeatmapSetsGroupedTogether = ShouldGroupBeatmapsTogether(criteria);
@@ -215,7 +208,7 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.Collections:
{
var collections = getCollections();
var collections = GetCollections();
return getGroupsBy(b => defineGroupByCollection(b, collections), items);
}
@@ -224,13 +217,15 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.RankAchieved:
{
var topRankMapping = getLocalUserTopRanks(criteria);
var topRankMapping = GetLocalUserTopRanks(criteria);
return getGroupsBy(b => defineGroupByRankAchieved(b, topRankMapping), items);
}
// TODO: need implementation
// case GroupMode.Favourites:
// goto case GroupMode.None;
case GroupMode.Favourites:
{
var favouriteBeatmapSets = GetFavouriteBeatmapSets();
return getGroupsBy(b => defineGroupByFavourites(b, favouriteBeatmapSets), items);
}
default:
throw new ArgumentOutOfRangeException();
@@ -445,6 +440,14 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(int.MaxValue, "Unplayed").Yield();
}
private IEnumerable<GroupDefinition> defineGroupByFavourites(BeatmapInfo beatmap, HashSet<int> favouriteBeatmapSets)
{
if (beatmap.BeatmapSet?.OnlineID > 0 && favouriteBeatmapSets.Contains(beatmap.BeatmapSet.OnlineID))
return new GroupDefinition(0, "Favourites").Yield();
return [];
}
private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup);
}
}
@@ -233,6 +233,8 @@ namespace osu.Game.Screens.SelectV2
// if the beatmap set reference changed under the callback, abort visual updates to avoid showing stale data
if (onlineBeatmapSet == null || ReferenceEquals(beatmapSet, onlineBeatmapSet))
setBeatmapSet(beatmapSet, withHeartAnimation: hasFavourited);
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
favouriteRequest.Failure += e =>
{
@@ -57,6 +57,7 @@ namespace osu.Game.Screens.SelectV2
private RealmAccess realm { get; set; } = null!;
private IBindable<APIUser> localUser = null!;
private readonly IBindableList<int> localUserFavouriteBeatmapSets = new BindableList<int>();
public LocalisableString StatusText
{
@@ -186,6 +187,7 @@ namespace osu.Game.Screens.SelectV2
};
localUser = api.LocalUser.GetBoundCopy();
localUserFavouriteBeatmapSets.BindTo(api.LocalUserState.FavouriteBeatmapSets);
}
protected override void LoadComplete()
@@ -237,6 +239,7 @@ namespace osu.Game.Screens.SelectV2
});
localUser.BindValueChanged(_ => updateCriteria());
localUserFavouriteBeatmapSets.BindCollectionChanged((_, _) => updateCriteria());
updateCriteria();
}
@@ -851,7 +851,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
await StartCountdown(new MatchmakingStageCountdown
{
Stage = stage,
TimeRemaining = TimeSpan.FromSeconds(10)
TimeRemaining = TimeSpan.FromSeconds(stage == MatchmakingStage.UserBeatmapSelect ? 30 : 10)
}).ConfigureAwait(false);
}
+10 -2
View File
@@ -274,8 +274,16 @@ namespace osu.Game.Users
public InLobby(MultiplayerRoom room)
{
RoomID = room.RoomID;
RoomName = room.Settings.Name;
if (room.Settings.MatchType == MatchType.Matchmaking)
{
RoomID = -1;
RoomName = "Quick Play";
}
else
{
RoomID = room.RoomID;
RoomName = room.Settings.Name;
}
}
[SerializationConstructor]
+1 -1
View File
@@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.1021.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.1028.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.1006.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1021.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1028.0" />
</ItemGroup>
</Project>