1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 09:42:54 +08:00

Merge branch 'master' into daily-challenge-tier-thresholds

This commit is contained in:
Salman Alshamrani 2024-11-28 17:46:55 -05:00 committed by GitHub
commit a8db35ac45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 422 additions and 186 deletions

View File

@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable getResult(HitResult result)
{
if (!hit_result_mapping.ContainsKey(result))
if (!hit_result_mapping.TryGetValue(result, out var value))
return null;
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value
string filename = this.GetManiaSkinConfig<string>(value)?.Value
?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);

View File

@ -3,8 +3,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((StarFountain)Children[1]).Shoot(-1);
});
}
[Test]
public void TestGameplayStarFountainsSetting()
{
Bindable<bool> starFountainsEnabled = null!;
AddStep("load configuration", () =>
{
var config = new OsuConfigManager(LocalStorage);
starFountainsEnabled = config.GetBindable<bool>(OsuSetting.StarFountains);
});
AddStep("make fountains", () =>
{
Children = new Drawable[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
};
});
AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false);
AddRepeatStep("attempt to activate fountains (disabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (re-enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
}
}
}

View File

@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("retry count is 1", () => player.RestartCount == 1);
}
[Test]
public void TestLastScoreNullAfterExitingPlayer()
{
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
}
[Test]
public void TestRetryImmediatelyAfterCompletion()
{

View File

@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible();
}
[Test]
public void TestPublicChannelsSortedByName()
{
// Intentionally join back to front.
AddStep("Show overlay with channel 2", () =>
{
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2);
chatOverlay.Show();
});
AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
AddStep("message in channel 2", () =>
{
testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
ChannelListItem getFirstVisiblePublicChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.Public);
}
[Test]
public void TestPrivateChannelsSortedByRecent()
{
Channel pmChannel1 = createPrivateChannel();
Channel pmChannel2 = createPrivateChannel();
joinChannel(pmChannel1);
joinChannel(pmChannel2);
AddStep("Show overlay", () => chatOverlay.Show());
AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
AddStep("message in channel 2", () =>
{
pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2);
AddStep("message in channel 1", () =>
{
pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
ChannelListItem getFirstVisiblePMChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.PM);
}
[Test]
public void TestKeyboardNewChannel()
{

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10;
private const int real_user_position = 200;
private TestResultsScreen resultsScreen = null!;
private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest score in the list.
@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowWithUserScore()
public void TestShowUserScore()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowNullUserScore()
public void TestShowUserBest()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createUserBestResults();
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position);
}
[Test]
public void TestShowNonUserScores()
{
AddStep("bind user score info handler", () => bindHandler());
createResults();
createUserBestResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowNullUserScoreWithDelay()
public void TestShowNonUserScoresWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
createUserBestResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
createUserBestResults();
waitForDisplay();
for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
}
}
@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults();
createUserBestResults();
waitForDisplay();
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
}
@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
AddAssert("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
}
}
/// <summary>
/// Shows the <see cref="TestUserBestResultsScreen"/> with no scores provided by the API.
/// </summary>
[Test]
public void TestShowWithNoScores()
public void TestShowUserBestWithNoScoresPresent()
{
AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any());
createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
}
private void createResults(Func<ScoreInfo>? getScore = null)
private void createResultsWithScore(Func<ScoreInfo> getScore)
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void createUserBestResults()
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}, 2));
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void waitForDisplay()
{
AddUntilStep("wait for scores loaded", () =>
requestComplete
// request handler may need to fire more than once to get scores.
&& totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
&& resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@ -231,6 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists
// pre-check for requests we should be handling (as they are scheduled below).
switch (request)
{
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest:
break;
@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request)
{
case ShowPlaylistUserScoreRequest s:
case ShowPlaylistScoreRequest s:
if (userScore == null)
triggerFail(s);
else
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores));
break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -410,18 +462,22 @@ namespace osu.Game.Tests.Visual.Playlists
};
}
private partial class TestResultsScreen : PlaylistItemUserResultsScreen
private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen
{
public new LoadingSpinner LeftSpinner => base.LeftSpinner;
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem)
public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
AllowRetry = true;
}
}
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
}
}

View File

@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0;
if (refetch || beatmapInfo.IsManaged || missingFiles)
if (beatmapInfo.IsManaged)
{
beatmapInfo = beatmapInfo.Detach();
}
else if (refetch || missingFiles)
{
Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;

View File

@ -138,6 +138,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.LightenDuringBreaks, true);
SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.StarFountains, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
@ -414,6 +415,7 @@ namespace osu.Game.Configuration
NotifyOnPrivateMessage,
UIHoldActivationDelay,
HitLighting,
StarFountains,
MenuBackgroundSource,
GameplayDisableWinKey,
SeasonalBackgroundMode,

View File

@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Configuration
{
@ -77,7 +78,8 @@ namespace osu.Game.Configuration
TouchInputActive,
/// <summary>
/// Stores the local user's last score (can be completed or aborted).
/// Contains the local user's last score (can be completed or aborted) after exiting <see cref="Player"/>.
/// Will be cleared to <c>null</c> when leaving <see cref="PlayerLoader"/>.
/// </summary>
LastLocalUserScore,

View File

@ -74,6 +74,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low");
/// <summary>
/// "Star fountains"
/// </summary>
public static LocalisableString StarFountains => new TranslatableString(getKey(@"star_fountains"), @"Star fountains");
/// <summary>
/// "Always show key overlay"
/// </summary>

View File

@ -161,7 +161,7 @@ namespace osu.Game.Online.Chat
Messages.AddRange(messages);
long? maxMessageId = messages.Max(m => m.Id);
if (maxMessageId > LastMessageId)
if (LastMessageId == null || maxMessageId > LastMessageId)
LastMessageId = maxMessageId;
purgeOldMessages();

View File

@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
public ChannelGroup AnnounceChannelGroup { get; private set; } = null!;
public ChannelGroup PublicChannelGroup { get; private set; } = null!;
public ChannelGroup PrivateChannelGroup { get; private set; } = null!;
private OsuScrollContainer scroll = null!;
private SearchContainer groupFlow = null!;
private ChannelGroup announceChannelGroup = null!;
private ChannelGroup publicChannelGroup = null!;
private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!;
private TextBox searchTextBox = null!;
@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.X,
}
},
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()),
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()),
AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
selector = new ChannelListItem(ChannelListingChannel),
privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()),
PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
},
},
},
@ -111,69 +113,70 @@ namespace osu.Game.Overlays.Chat.ChannelList
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
ChannelGroup group = getGroupFromChannel(channel);
channelMap.Add(channel, item);
flow.Add(item);
group.AddChannel(item);
updateVisibility();
}
public void RemoveChannel(Channel channel)
{
if (!channelMap.ContainsKey(channel))
if (!channelMap.TryGetValue(channel, out var item))
return;
ChannelListItem item = channelMap[channel];
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
ChannelGroup group = getGroupFromChannel(channel);
channelMap.Remove(channel);
flow.Remove(item, true);
group.RemoveChannel(item);
updateVisibility();
}
public ChannelListItem GetItem(Channel channel)
{
if (!channelMap.ContainsKey(channel))
if (!channelMap.TryGetValue(channel, out var item))
throw new ArgumentOutOfRangeException();
return channelMap[channel];
return item;
}
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
private FillFlowContainer<ChannelListItem> getFlowForChannel(Channel channel)
private ChannelGroup getGroupFromChannel(Channel channel)
{
switch (channel.Type)
{
case ChannelType.Public:
return publicChannelGroup.ItemFlow;
return PublicChannelGroup;
case ChannelType.PM:
return privateChannelGroup.ItemFlow;
return PrivateChannelGroup;
case ChannelType.Announce:
return announceChannelGroup.ItemFlow;
return AnnounceChannelGroup;
default:
return publicChannelGroup.ItemFlow;
return PublicChannelGroup;
}
}
private void updateVisibility()
{
if (announceChannelGroup.ItemFlow.Children.Count == 0)
announceChannelGroup.Hide();
if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
AnnounceChannelGroup.Hide();
else
announceChannelGroup.Show();
AnnounceChannelGroup.Show();
}
private partial class ChannelGroup : FillFlowContainer
public partial class ChannelGroup : FillFlowContainer
{
public readonly FillFlowContainer<ChannelListItem> ItemFlow;
private readonly bool sortByRecent;
public readonly ChannelListItemFlow ItemFlow;
public ChannelGroup(LocalisableString label)
public ChannelGroup(LocalisableString label, bool sortByRecent)
{
this.sortByRecent = sortByRecent;
Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -187,7 +190,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
},
ItemFlow = new FillFlowContainer<ChannelListItem>
ItemFlow = new ChannelListItemFlow(sortByRecent)
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
@ -195,6 +198,60 @@ namespace osu.Game.Overlays.Chat.ChannelList
},
};
}
public partial class ChannelListItemFlow : FillFlowContainer<ChannelListItem>
{
private readonly bool sortByRecent;
public ChannelListItemFlow(bool sortByRecent)
{
this.sortByRecent = sortByRecent;
}
public void Reflow() => InvalidateLayout();
public override IEnumerable<Drawable> FlowingChildren => sortByRecent
? base.FlowingChildren.OfType<ChannelListItem>().OrderByDescending(i => i.Channel.LastMessageId ?? long.MinValue)
: base.FlowingChildren.OfType<ChannelListItem>().OrderBy(i => i.Channel.Name);
}
public void AddChannel(ChannelListItem item)
{
ItemFlow.Add(item);
if (sortByRecent)
{
item.Channel.NewMessagesArrived += newMessagesArrived;
item.Channel.PendingMessageResolved += pendingMessageResolved;
}
ItemFlow.Reflow();
}
public void RemoveChannel(ChannelListItem item)
{
if (sortByRecent)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
ItemFlow.Remove(item, true);
}
private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow();
private void newMessagesArrived(IEnumerable<Message> _) => ItemFlow.Reflow();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var item in ItemFlow)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
}
}
private partial class ChannelSearchTextBox : BasicSearchTextBox

View File

@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = GraphicsSettingsStrings.HitLighting,
Current = config.GetBindable<bool>(OsuSetting.HitLighting)
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.StarFountains,
Current = config.GetBindable<bool>(OsuSetting.StarFountains)
},
};
}
}

View File

@ -78,7 +78,7 @@ namespace osu.Game.Scoring
/// Perform a lookup query on available <see cref="ScoreInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
/// <returns>The first result for the provided query in its detached form, or null if no results were found.</returns>
public ScoreInfo? Query(Expression<Func<ScoreInfo, bool>> query)
{
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
@ -88,8 +88,14 @@ namespace osu.Game.Scoring
{
ScoreInfo? databasedScoreInfo = null;
if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash))
if (originalScoreInfo is ScoreInfo scoreInfo)
{
if (scoreInfo.IsManaged)
return scoreInfo.Detach();
if (!string.IsNullOrEmpty(scoreInfo.Hash))
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash);
}
if (originalScoreInfo.OnlineID > 0)
databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID);

View File

@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void presentScore(long id)
{
if (this.IsCurrentScreen())
this.Push(new PlaylistItemScoreResultsScreen(room.RoomID!.Value, playlistItem, id));
this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem));
}
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)

View File

@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen
public partial class MultiplayerResultsScreen : PlaylistItemScoreResultsScreen
{
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)

View File

@ -191,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Invoke callback to add the scores.
callback.Invoke(scoreInfos);
// Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos;
}

View File

@ -11,13 +11,19 @@ using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a selected arbitrary score for a playlist item, with scores around included.
/// Shows a given score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen
{
private readonly long scoreId;
public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId)
public PlaylistItemScoreResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
scoreId = score.OnlineID;
}
public PlaylistItemScoreResultsScreen(long scoreId, long roomId, PlaylistItem playlistItem)
: base(null, roomId, playlistItem)
{
this.scoreId = scoreId;
@ -28,9 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
return scoreInfos;
}
}

View File

@ -0,0 +1,41 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a user's best score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen
{
private readonly int userId;
public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId)
: base(null, roomId, playlistItem)
{
this.userId = userId;
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault();
});
return scoreInfos;
}
}
}

View File

@ -1,46 +0,0 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows the user's best score for a given playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen
{
public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos;
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(Room.RoomID != null);
return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem)
return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{
AllowRetry = true,
ShowUserStatistics = true,

View File

@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
@ -32,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private readonly IBindable<bool> isIdle = new BindableBool();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(CanBeNull = true)]
private IdleTracker? idleTracker { get; set; }
@ -143,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RequestResults = item =>
{
Debug.Assert(Room.RoomID != null);
ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item));
ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id));
}
}
},

View File

@ -5,8 +5,10 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Menu;
@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play
private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!;
private Bindable<bool> kiaiStarFountains = null!;
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
kiaiStarFountains = config.GetBindable<bool>(OsuSetting.StarFountains);
RelativeSizeAxes = Axes.Both;
Children = new[]
@ -48,6 +54,9 @@ namespace osu.Game.Screens.Play
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!kiaiStarFountains.Value)
return;
if (effectPoint.KiaiMode && !isTriggered)
{
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500;

View File

@ -28,6 +28,7 @@ using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Performance;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning;
@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play
private FillFlowContainer disclaimers = null!;
private OsuScrollContainer settingsScroll = null!;
private Bindable<ScoreInfo?> lastScore = null!;
private Bindable<bool> showStoryboards = null!;
private bool backgroundBrightnessReduction;
@ -179,6 +182,8 @@ namespace osu.Game.Screens.Play
{
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
lastScore = sessionStatics.GetBindable<ScoreInfo?>(Static.LastLocalUserScore);
showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
const float padding = 25;
@ -347,6 +352,8 @@ namespace osu.Game.Screens.Play
highPerformanceSession?.Dispose();
highPerformanceSession = null;
lastScore.Value = null;
return base.OnExiting(e);
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play
private readonly Func<Task<ScoreInfo>>? importFailedScore;
private ScoreInfo? importedScore;
private Live<ScoreInfo>? importedScore;
private DownloadButton button = null!;
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play
switch (state.Value)
{
case DownloadState.LocallyAvailable:
game?.PresentScore(importedScore, ScorePresentType.Gameplay);
game?.PresentScore(importedScore?.Value, ScorePresentType.Gameplay);
break;
case DownloadState.NotDownloaded:
@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play
{
Task.Run(importFailedScore).ContinueWith(t =>
{
importedScore = realm.Run(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.Detach());
importedScore = realm.Run<Live<ScoreInfo>?>(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.ToLive(realm));
Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded);
}).FireAndForget();
}
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Play
if (player != null)
{
importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.Detach());
importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.ToLive(realm));
state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded;
}
@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play
{
if (state.NewValue != DownloadState.LocallyAvailable) return;
if (importedScore != null) scoreManager.Export(importedScore);
if (importedScore != null) scoreManager.Export(importedScore.Value);
this.state.ValueChanged -= exportWhenReady;
}

View File

@ -29,7 +29,6 @@ using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel;
using osuTK;
using osuTK.Input;
using Realms;
namespace osu.Game.Screens.Select
{
@ -207,8 +206,6 @@ namespace osu.Game.Screens.Select
private CarouselRoot root;
private IDisposable? subscriptionBeatmaps;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
private Sample? spinSample;
@ -258,13 +255,6 @@ namespace osu.Game.Screens.Select
}
}
protected override void LoadComplete()
{
base.LoadComplete();
subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
}
private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = new HashSet<BeatmapSetInfo>();
@ -366,35 +356,6 @@ namespace osu.Game.Screens.Select
BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find<BeatmapSetInfo>(id);
}
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
{
// we only care about actual changes in hidden status.
if (changes == null)
return;
bool changed = false;
foreach (int i in changes.InsertedIndices)
{
var beatmapInfo = sender[i];
var beatmapSet = beatmapInfo.BeatmapSet;
Debug.Assert(beatmapSet != null);
// Only require to action here if the beatmap is missing.
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
{
updateBeatmapSet(beatmapSet.Detach());
changed = true;
}
}
if (changed)
invalidateAfterChange();
}
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{
removeBeatmapSet(beatmapSet.ID);
@ -1292,12 +1253,5 @@ namespace osu.Game.Screens.Select
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
subscriptionBeatmaps?.Dispose();
}
}
}

View File

@ -78,12 +78,12 @@ namespace osu.Game.Tests.Visual.Spectator
/// <param name="state">The spectator state to end play with.</param>
public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit)
{
if (!userBeatmapDictionary.ContainsKey(userId))
if (!userBeatmapDictionary.TryGetValue(userId, out int value))
return;
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
{
BeatmapID = userBeatmapDictionary[userId],
BeatmapID = value,
RulesetID = 0,
Mods = userModsDictionary[userId],
State = state