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:
+1
-1
@@ -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.
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user