1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:33:22 +08:00

Merge branch 'master' into multi-spectator-settings-sidebar

This commit is contained in:
Dean Herbert 2024-12-01 18:33:46 +09:00
commit ddac71628d
No known key found for this signature in database
58 changed files with 828 additions and 414 deletions

View File

@ -114,7 +114,10 @@ jobs:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET workloads - name: Install .NET workloads
run: dotnet workload install android # since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
- name: Compile - name: Compile
run: dotnet build -c Debug osu.Android.slnf run: dotnet build -c Debug osu.Android.slnf

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.1128.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -175,7 +175,7 @@ namespace osu.Desktop
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{ {
presence.Buttons = new[] presence.Buttons = new[]
{ {
@ -333,20 +333,6 @@ namespace osu.Desktop
return true; return true;
} }
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapID;
case UserActivity.EditingBeatmap edit:
return edit.BeatmapID;
}
return null;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (multiplayerClient.IsNotNull()) if (multiplayerClient.IsNotNull())

View File

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

View File

@ -3,8 +3,10 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((StarFountain)Children[1]).Shoot(-1); ((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); 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] [Test]
public void TestRetryImmediatelyAfterCompletion() public void TestRetryImmediatelyAfterCompletion()
{ {

View File

@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible(); 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] [Test]
public void TestKeyboardNewChannel() public void TestKeyboardNewChannel()
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
AddStep("create", () => AddStep("create", () =>
{ {
Clear(); Clear();
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestPlayCountRankingTier() public void TestPlayCountRankingTier()
{ {
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(29) == RankingTier.Bronze);
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Silver);
} }
} }
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Containers; 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 scores_per_result = 10;
private const int real_user_position = 200; 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 lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest 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] [Test]
public void TestShowWithUserScore() public void TestShowUserScore()
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); 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] [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()); AddStep("bind user score info handler", () => bindHandler());
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); 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)); AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScoreWithDelay() public void TestShowNonUserScoresWithDelay()
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); 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)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); 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(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); 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)); AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); 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(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); 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("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); 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(); waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount); 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); 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)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); 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(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); 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] [Test]
public void TestShowWithNoScores() public void TestShowUserBestWithNoScoresPresent()
{ {
AddStep("bind user score info handler", () => bindHandler(noScores: true)); AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults(); createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1)); 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", () => 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 RulesetID = new OsuRuleset().RulesetInfo.OnlineID
})); }));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); 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() private void waitForDisplay()
{ {
AddUntilStep("wait for scores loaded", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
// request handler may need to fire more than once to get scores. // request handler may need to fire more than once to get scores.
&& totalCount > 0 && totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5); 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). // pre-check for requests we should be handling (as they are scheduled below).
switch (request) switch (request)
{ {
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest: case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest: case IndexPlaylistScoresRequest:
break; break;
@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request) switch (request)
{ {
case ShowPlaylistUserScoreRequest s: case ShowPlaylistScoreRequest s:
if (userScore == null) if (userScore == null)
triggerFail(s); triggerFail(s);
else else
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break; break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i: case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores)); triggerSuccess(i, createIndexResponse(i, noScores));
break; break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000, MaxCombo = 1000,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", 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 TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
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)
: base(score, roomId, playlistItem) : base(score, roomId, playlistItem)
{ {
AllowRetry = true; AllowRetry = true;
} }
} }
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
} }
} }

View File

@ -2,8 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
@ -14,6 +19,9 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{ {
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
[Test] [Test]
@ -37,5 +45,29 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>); AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
} }
[Test]
public void TestCloseButtonGoesAwayAfterGracePeriod()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3),
EndDate = DateTimeOffset.Now.AddMinutes(30)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
}
} }
} }

View File

@ -408,7 +408,7 @@ namespace osu.Game.Beatmaps
// user requested abort // user requested abort
return; return;
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); var video = b.Files.FirstOrDefault(f => SupportedExtensions.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null) if (video != null)
{ {
@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch. // If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0; 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; Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo; beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.Formats namespace osu.Game.Beatmaps.Formats
{ {
@ -446,7 +447,7 @@ namespace osu.Game.Beatmaps.Formats
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match. // video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{ {
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true; lineSupportedByEncoder = true;

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands; using osu.Game.Storyboards.Commands;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -112,7 +113,7 @@ namespace osu.Game.Beatmaps.Formats
// //
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break; break;
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));

View File

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

View File

@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -77,7 +78,8 @@ namespace osu.Game.Configuration
TouchInputActive, TouchInputActive,
/// <summary> /// <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> /// </summary>
LastLocalUserScore, LastLocalUserScore,

View File

@ -245,8 +245,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap);
} }
@ -269,8 +269,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
} }
@ -313,7 +313,8 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param> /// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty,
LegacyScoreAttributes attributes)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return (score.TotalScoreWithoutMods, score.TotalScore); return (score.TotalScoreWithoutMods, score.TotalScore);
@ -620,24 +621,31 @@ namespace osu.Game.Database
} }
} }
private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static double ComputeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor)
=> ComputeAccuracy(scoreInfo.Statistics, scoreInfo.MaximumStatistics, scoreProcessor);
public static double ComputeAccuracy(IReadOnlyDictionary<HitResult, int> statistics, IReadOnlyDictionary<HitResult, int> maximumStatistics, ScoreProcessor scoreProcessor)
{ {
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) int baseScore = statistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) int maxBaseScore = maximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
} }
public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); public static ScoreRank ComputeRank(ScoreInfo scoreInfo) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor());
private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static ScoreRank ComputeRank(ScoreInfo scoreInfo, ScoreProcessor processor) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, processor);
public static ScoreRank ComputeRank(double accuracy, IReadOnlyDictionary<HitResult, int> statistics, IList<Mod> mods, ScoreProcessor scoreProcessor)
{ {
var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); var rank = scoreProcessor.RankFromScore(accuracy, statistics);
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>()) foreach (var mod in mods.OfType<IApplicableToScoreProcessor>())
rank = mod.AdjustRank(rank, scoreInfo.Accuracy); rank = mod.AdjustRank(rank, accuracy);
return rank; return rank;
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -96,24 +97,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
get get
{ {
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) string extension = File.Extension.ToLowerInvariant();
if (SupportedExtensions.VIDEO_EXTENSIONS.Contains(extension))
return FontAwesome.Regular.FileVideo; return FontAwesome.Regular.FileVideo;
switch (File.Extension) if (SupportedExtensions.AUDIO_EXTENSIONS.Contains(extension))
{ return FontAwesome.Regular.FileAudio;
case @".ogg":
case @".mp3":
case @".wav":
return FontAwesome.Regular.FileAudio;
case @".jpg": if (SupportedExtensions.IMAGE_EXTENSIONS.Contains(extension))
case @".jpeg": return FontAwesome.Regular.FileImage;
case @".png":
return FontAwesome.Regular.FileImage;
default: return FontAwesome.Regular.File;
return FontAwesome.Regular.File;
}
} }
} }

View File

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

View File

@ -0,0 +1,27 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class ClosePlaylistRequest : APIRequest
{
private readonly long roomId;
public ClosePlaylistRequest(long roomId)
{
this.roomId = roomId;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.Method = HttpMethod.Delete;
return request;
}
protected override string Target => $@"rooms/{roomId}";
}
}

View File

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

View File

@ -375,6 +375,7 @@ namespace osu.Game.Online.Rooms
Type = other.Type; Type = other.Type;
MaxParticipants = other.MaxParticipants; MaxParticipants = other.MaxParticipants;
ParticipantCount = other.ParticipantCount; ParticipantCount = other.ParticipantCount;
StartDate = other.StartDate;
EndDate = other.EndDate; EndDate = other.EndDate;
UserScore = other.UserScore; UserScore = other.UserScore;
QueueMode = other.QueueMode; QueueMode = other.QueueMode;

View File

@ -195,7 +195,8 @@ namespace osu.Game
private MainMenu menuScreen; private MainMenu menuScreen;
private VersionManager versionManager; [CanBeNull]
private DevBuildBanner devBuildBanner;
[CanBeNull] [CanBeNull]
private IntroScreen introScreen; private IntroScreen introScreen;
@ -1055,10 +1056,7 @@ namespace osu.Game
}, topMostOverlayContent.Add); }, topMostOverlayContent.Add);
if (!IsDeployedBuild) if (!IsDeployedBuild)
{ loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add);
dependencies.Cache(versionManager = new VersionManager());
loadComponentSingleFile(versionManager, ScreenContainer.Add);
}
loadComponentSingleFile(osuLogo, _ => loadComponentSingleFile(osuLogo, _ =>
{ {
@ -1564,12 +1562,12 @@ namespace osu.Game
{ {
case IntroScreen intro: case IntroScreen intro:
introScreen = intro; introScreen = intro;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case MainMenu menu: case MainMenu menu:
menuScreen = menu; menuScreen = menu;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case Player player: case Player player:
@ -1577,7 +1575,7 @@ namespace osu.Game
break; break;
default: default:
versionManager?.Hide(); devBuildBanner?.Hide();
break; break;
} }

View File

@ -73,8 +73,6 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))] [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
#if DEBUG #if DEBUG
public const string GAME_NAME = "osu! (development)"; public const string GAME_NAME = "osu! (development)";
#else #else

View File

@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>(); 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 OsuScrollContainer scroll = null!;
private SearchContainer groupFlow = null!; private SearchContainer groupFlow = null!;
private ChannelGroup announceChannelGroup = null!;
private ChannelGroup publicChannelGroup = null!;
private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!; private ChannelListItem selector = null!;
private TextBox searchTextBox = null!; private TextBox searchTextBox = null!;
@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
} }
}, },
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
selector = new ChannelListItem(ChannelListingChannel), 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.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel); ChannelGroup group = getGroupFromChannel(channel);
channelMap.Add(channel, item); channelMap.Add(channel, item);
flow.Add(item); group.AddChannel(item);
updateVisibility(); updateVisibility();
} }
public void RemoveChannel(Channel channel) public void RemoveChannel(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
return; return;
ChannelListItem item = channelMap[channel]; ChannelGroup group = getGroupFromChannel(channel);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
channelMap.Remove(channel); channelMap.Remove(channel);
flow.Remove(item, true); group.RemoveChannel(item);
updateVisibility(); updateVisibility();
} }
public ChannelListItem GetItem(Channel channel) public ChannelListItem GetItem(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
return channelMap[channel]; return item;
} }
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
private FillFlowContainer<ChannelListItem> getFlowForChannel(Channel channel) private ChannelGroup getGroupFromChannel(Channel channel)
{ {
switch (channel.Type) switch (channel.Type)
{ {
case ChannelType.Public: case ChannelType.Public:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
case ChannelType.PM: case ChannelType.PM:
return privateChannelGroup.ItemFlow; return PrivateChannelGroup;
case ChannelType.Announce: case ChannelType.Announce:
return announceChannelGroup.ItemFlow; return AnnounceChannelGroup;
default: default:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
} }
} }
private void updateVisibility() private void updateVisibility()
{ {
if (announceChannelGroup.ItemFlow.Children.Count == 0) if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
announceChannelGroup.Hide(); AnnounceChannelGroup.Hide();
else 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; Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -187,7 +190,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Margin = new MarginPadding { Left = 18, Bottom = 5 }, Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
}, },
ItemFlow = new FillFlowContainer<ChannelListItem> ItemFlow = new ChannelListItemFlow(sortByRecent)
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, 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 private partial class ChannelSearchTextBox : BasicSearchTextBox

View File

@ -0,0 +1,58 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays
{
public partial class DevBuildBanner : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Alpha = 0;
AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12),
Colour = colours.YellowDark,
Text = @"DEVELOPER BUILD",
},
new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
Scale = new Vector2(0.4f, 1),
Y = 2,
},
});
}
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(500, Easing.OutQuint);
}
}
}

View File

@ -138,34 +138,31 @@ namespace osu.Game.Overlays.Profile.Header.Components
topFifty.ValueColour = colourProvider.Content2; topFifty.ValueColour = colourProvider.Content2;
} }
// reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 // reference: https://github.com/ppy/osu-web/blob/a97f156014e00ea1aa315140da60542e798a9f06/resources/js/profile-page/daily-challenge.tsx#L13-L47
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Floor(playCount / 3.0d));
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
// get truncated to 10 with an integer division and show a lower tier.
public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d));
public static RankingTier TierForDaily(int daily) public static RankingTier TierForDaily(int daily)
{ {
if (daily > 360) if (daily >= 360)
return RankingTier.Lustrous; return RankingTier.Lustrous;
if (daily > 240) if (daily >= 240)
return RankingTier.Radiant; return RankingTier.Radiant;
if (daily > 120) if (daily >= 120)
return RankingTier.Rhodium; return RankingTier.Rhodium;
if (daily > 60) if (daily >= 60)
return RankingTier.Platinum; return RankingTier.Platinum;
if (daily > 30) if (daily >= 30)
return RankingTier.Gold; return RankingTier.Gold;
if (daily > 10) if (daily >= 10)
return RankingTier.Silver; return RankingTier.Silver;
if (daily > 5) if (daily >= 5)
return RankingTier.Bronze; return RankingTier.Bronze;
return RankingTier.Iron; return RankingTier.Iron;

View File

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

View File

@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Utils;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
@ -709,7 +710,7 @@ namespace osu.Game.Overlays.SkinEditor
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; public IEnumerable<string> HandledExtensions => SupportedExtensions.IMAGE_EXTENSIONS;
#endregion #endregion

View File

@ -1,95 +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 osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public partial class VersionManager : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Alpha = 0;
FillFlowContainer mainFill;
Children = new Drawable[]
{
mainFill = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = game.Name
},
new OsuSpriteText
{
Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
Text = game.Version
},
}
},
}
}
};
if (!game.IsDeployedBuild)
{
mainFill.AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Numeric.With(size: 12),
Colour = colours.Yellow,
Text = @"Development Build"
},
new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
},
});
}
}
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(500, Easing.OutQuint);
}
}
}

View File

@ -3,13 +3,12 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Edit.Checks.Components namespace osu.Game.Rulesets.Edit.Checks.Components
{ {
public static class AudioCheckUtils public static class AudioCheckUtils
{ {
public static readonly string[] AUDIO_EXTENSIONS = { "mp3", "ogg", "wav" }; public static bool HasAudioExtension(string filename) => SupportedExtensions.AUDIO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant());
public static bool HasAudioExtension(string filename) => AUDIO_EXTENSIONS.Any(Path.GetExtension(filename).ToLowerInvariant().EndsWith);
} }
} }

View File

@ -3,19 +3,15 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -168,13 +164,7 @@ namespace osu.Game.Rulesets.UI
if (lastBackwardsSeekLogTime == null || Math.Abs(Clock.CurrentTime - lastBackwardsSeekLogTime.Value) > 1000) if (lastBackwardsSeekLogTime == null || Math.Abs(Clock.CurrentTime - lastBackwardsSeekLogTime.Value) > 1000)
{ {
lastBackwardsSeekLogTime = Clock.CurrentTime; lastBackwardsSeekLogTime = Clock.CurrentTime;
Logger.Log($"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})");
string loggableContent = $"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})";
if (parentGameplayClock is GameplayClockContainer gcc)
loggableContent += $"\n{gcc.ChildrenOfType<FramedBeatmapClock>().Single().GetSnapshot()}";
Logger.Error(new SentryOnlyDiagnosticsException("backwards seek"), loggableContent);
} }
state = PlaybackState.NotValid; state = PlaybackState.NotValid;

View File

@ -78,7 +78,7 @@ namespace osu.Game.Scoring
/// Perform a lookup query on available <see cref="ScoreInfo"/>s. /// Perform a lookup query on available <see cref="ScoreInfo"/>s.
/// </summary> /// </summary>
/// <param name="query">The query.</param> /// <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) public ScoreInfo? Query(Expression<Func<ScoreInfo, bool>> query)
{ {
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach()); return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
@ -88,8 +88,14 @@ namespace osu.Game.Scoring
{ {
ScoreInfo? databasedScoreInfo = null; ScoreInfo? databasedScoreInfo = null;
if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash)) if (originalScoreInfo is ScoreInfo scoreInfo)
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); {
if (scoreInfo.IsManaged)
return scoreInfo.Detach();
if (!string.IsNullOrEmpty(scoreInfo.Hash))
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash);
}
if (originalScoreInfo.OnlineID > 0) if (originalScoreInfo.OnlineID > 0)
databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID); databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID);

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
@ -48,12 +49,12 @@ namespace osu.Game.Screens.Edit.Setup
Children = new Drawable[] Children = new Drawable[]
{ {
backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") backgroundChooser = new FormFileSelector(SupportedExtensions.IMAGE_EXTENSIONS)
{ {
Caption = GameplaySettingsStrings.BackgroundHeader, Caption = GameplaySettingsStrings.BackgroundHeader,
PlaceholderText = EditorSetupStrings.ClickToSelectBackground, PlaceholderText = EditorSetupStrings.ClickToSelectBackground,
}, },
audioTrackChooser = new FormFileSelector(".mp3", ".ogg") audioTrackChooser = new FormFileSelector(SupportedExtensions.AUDIO_EXTENSIONS)
{ {
Caption = EditorSetupStrings.AudioTrack, Caption = EditorSetupStrings.AudioTrack,
PlaceholderText = EditorSetupStrings.ClickToSelectTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack,

View File

@ -79,9 +79,6 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(canBeNull: true)]
private VersionManager versionManager { get; set; }
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false; protected override bool PlayExitSound => false;
@ -294,16 +291,6 @@ namespace osu.Game.Screens.Menu
} }
} }
protected override void Update()
{
base.Update();
bottomElementsFlow.Margin = new MarginPadding
{
Bottom = (versionManager?.DrawHeight + 5) ?? 0
};
}
protected override void LogoSuspending(OsuLogo logo) protected override void LogoSuspending(OsuLogo logo)
{ {
var seq = logo.FadeOut(300, Easing.InSine) var seq = logo.FadeOut(300, Easing.InSine)

View File

@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void presentScore(long id) private void presentScore(long id)
{ {
if (this.IsCurrentScreen()) 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) private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -22,9 +23,14 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Container = osu.Framework.Graphics.Containers.Container; using Container = osu.Framework.Graphics.Containers.Container;
@ -48,6 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private LoungeSubScreen? lounge { get; set; } private LoungeSubScreen? lounge { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly BindableWithCurrent<Room?> selectedRoom = new BindableWithCurrent<Room?>(); private readonly BindableWithCurrent<Room?> selectedRoom = new BindableWithCurrent<Room?>();
private Sample? sampleSelect; private Sample? sampleSelect;
private Sample? sampleJoin; private Sample? sampleJoin;
@ -144,13 +156,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
public Popover GetPopover() => new PasswordEntryPopover(Room); public Popover GetPopover() => new PasswordEntryPopover(Room);
public MenuItem[] ContextMenuItems => new MenuItem[] public MenuItem[] ContextMenuItems
{ {
new OsuMenuItem("Create copy", MenuItemType.Standard, () => get
{ {
lounge?.OpenCopy(Room); var items = new List<MenuItem>
}) {
}; new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
lounge?.OpenCopy(Room);
})
};
if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded)
{
items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () =>
{
dialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
{
var request = new ClosePlaylistRequest(Room.RoomID!.Value);
request.Success += () => lounge?.RefreshRooms();
api.Queue(request);
}));
}));
}
return items.ToArray();
}
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {

View File

@ -379,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
this.Push(CreateRoomSubScreen(room)); this.Push(CreateRoomSubScreen(room));
} }
public void RefreshRooms() => ListingPollingComponent.PollImmediately();
private void updateLoadingLayer() private void updateLoadingLayer()
{ {
if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value)

View File

@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RulesetStore Rulesets { get; private set; } = null!; protected RulesetStore Rulesets { get; private set; } = null!;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; protected IAPIProvider API { get; private set; } = null!;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
protected OnlinePlayScreen? ParentScreen { get; private set; } protected OnlinePlayScreen? ParentScreen { get; private set; }
@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
private PreviewTrackManager previewTrackManager { get; set; } = null!; private PreviewTrackManager previewTrackManager { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDialogOverlay? dialogOverlay { get; set; } protected IDialogOverlay? DialogOverlay { get; private set; }
[Cached] [Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
} }
} }
protected virtual bool IsConnected => api.State.Value == APIState.Online; protected virtual bool IsConnected => API.State.Value == APIState.Online;
public override bool OnBackButton() public override bool OnBackButton()
{ {
@ -361,17 +361,17 @@ namespace osu.Game.Screens.OnlinePlay.Match
bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0; bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0;
if (dialogOverlay == null || !hasUnsavedChanges) if (DialogOverlay == null || !hasUnsavedChanges)
return true; return true;
// if the dialog is already displayed, block exiting until the user explicitly makes a decision. // if the dialog is already displayed, block exiting until the user explicitly makes a decision.
if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) if (DialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog)
{ {
discardChangesDialog.Flash(); discardChangesDialog.Flash();
return false; return false;
} }
dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => DialogOverlay.Push(new ConfirmDiscardChangesDialog(() =>
{ {
ExitConfirmed = true; ExitConfirmed = true;
settingsOverlay.Hide(); settingsOverlay.Hide();

View File

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

View File

@ -0,0 +1,19 @@
// 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.Game.Online.Rooms;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class ClosePlaylistDialog : DeletionDialog
{
public ClosePlaylistDialog(Room room, Action closeAction)
{
HeaderText = "Are you sure you want to close the following playlist:";
BodyText = room.Name;
DangerousAction = closeAction;
}
}
}

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(); var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Invoke callback to add the scores. // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
callback.Invoke(scoreInfos); callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos; return scoreInfos;
} }

View File

@ -11,13 +11,19 @@ using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
/// <summary> /// <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> /// </summary>
public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen
{ {
private readonly long scoreId; 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) : base(null, roomId, playlistItem)
{ {
this.scoreId = scoreId; 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) protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{ {
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
return scoreInfos; 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) protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
Debug.Assert(Room.RoomID != null); Debug.Assert(Room.RoomID != null);
return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{ {
AllowRetry = true, AllowRetry = true,
ShowUserStatistics = true, ShowUserStatistics = true,

View File

@ -2,9 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
@ -12,22 +17,104 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
public partial class PlaylistsRoomFooter : CompositeDrawable public partial class PlaylistsRoomFooter : CompositeDrawable
{ {
public Action? OnStart; public Action? OnStart;
public Action? OnClose;
private readonly Room room;
private DangerousRoundedButton closeButton = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public PlaylistsRoomFooter(Room room) public PlaylistsRoomFooter(Room room)
{
this.room = room;
}
[BackgroundDependencyLoader]
private void load()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new[] InternalChild = new FillFlowContainer
{ {
new PlaylistsReadyButton(room) AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
Children = new Drawable[]
{ {
Anchor = Anchor.Centre, new PlaylistsReadyButton(room)
Origin = Anchor.Centre, {
RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre,
Size = new Vector2(600, 1), Origin = Anchor.Centre,
Action = () => OnStart?.Invoke() RelativeSizeAxes = Axes.Y,
Size = new Vector2(600, 1),
Action = () => OnStart?.Invoke()
},
closeButton = new DangerousRoundedButton
{
Text = "Close",
Action = () => OnClose?.Invoke(),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(120, 1),
Alpha = 0,
RelativeSizeAxes = Axes.Y,
}
} }
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
room.PropertyChanged += onRoomChanged;
updateState();
}
private void hideCloseButton()
{
closeButton.ResizeWidthTo(0, 100, Easing.OutQuint)
.Then().FadeOut().Expire();
}
private void onRoomChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Room.Status):
case nameof(Room.Host):
case nameof(Room.StartDate):
updateState();
break;
}
}
private void updateState()
{
TimeSpan? deletionGracePeriodRemaining = room.StartDate?.AddMinutes(5) - DateTimeOffset.Now;
if (room.Host?.Id == api.LocalUser.Value.Id)
{
if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded)
{
closeButton.FadeIn();
using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds))
hideCloseButton();
}
else if (closeButton.Alpha > 0)
hideCloseButton();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
room.PropertyChanged -= onRoomChanged;
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -12,7 +13,10 @@ using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
@ -32,6 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IdleTracker? idleTracker { get; set; } private IdleTracker? idleTracker { get; set; }
@ -143,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RequestResults = item => RequestResults = item =>
{ {
Debug.Assert(Room.RoomID != null); 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));
} }
} }
}, },
@ -255,7 +262,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room)
{ {
OnStart = StartPlay OnStart = StartPlay,
OnClose = closePlaylist,
}; };
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room)
@ -273,6 +281,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})");
} }
private void closePlaylist()
{
DialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
{
var request = new ClosePlaylistRequest(Room.RoomID!.Value);
request.Success += () =>
{
Room.Status = new RoomStatusEnded();
Room.EndDate = DateTimeOffset.UtcNow;
};
API.Queue(request);
}));
}
protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) protected override Screen CreateGameplayScreen(PlaylistItem selectedItem)
{ {
return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem)

View File

@ -5,8 +5,10 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play
private StarFountain leftFountain = null!; private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!; private StarFountain rightFountain = null!;
private Bindable<bool> kiaiStarFountains = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
kiaiStarFountains = config.GetBindable<bool>(OsuSetting.StarFountains);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Children = new[] Children = new[]
@ -48,6 +54,9 @@ namespace osu.Game.Screens.Play
{ {
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!kiaiStarFountains.Value)
return;
if (effectPoint.KiaiMode && !isTriggered) if (effectPoint.KiaiMode && !isTriggered)
{ {
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; 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;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play
private FillFlowContainer disclaimers = null!; private FillFlowContainer disclaimers = null!;
private OsuScrollContainer settingsScroll = null!; private OsuScrollContainer settingsScroll = null!;
private Bindable<ScoreInfo?> lastScore = null!;
private Bindable<bool> showStoryboards = null!; private Bindable<bool> showStoryboards = null!;
private bool backgroundBrightnessReduction; private bool backgroundBrightnessReduction;
@ -179,6 +182,8 @@ namespace osu.Game.Screens.Play
{ {
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
lastScore = sessionStatics.GetBindable<ScoreInfo?>(Static.LastLocalUserScore);
showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard); showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
const float padding = 25; const float padding = 25;
@ -347,6 +352,8 @@ namespace osu.Game.Screens.Play
highPerformanceSession?.Dispose(); highPerformanceSession?.Dispose();
highPerformanceSession = null; highPerformanceSession = null;
lastScore.Value = null;
return base.OnExiting(e); return base.OnExiting(e);
} }

View File

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

View File

@ -29,7 +29,6 @@ using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
using Realms;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -207,8 +206,6 @@ namespace osu.Game.Screens.Select
private CarouselRoot root; private CarouselRoot root;
private IDisposable? subscriptionBeatmaps;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100); private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
private Sample? spinSample; 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> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = 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); 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(() => public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{ {
removeBeatmapSet(beatmapSet.ID); removeBeatmapSet(beatmapSet.ID);
@ -1292,12 +1253,5 @@ namespace osu.Game.Screens.Select
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
subscriptionBeatmaps?.Dispose();
}
} }
} }

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -14,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Localisation.SkinComponents; using osu.Game.Localisation.SkinComponents;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
@ -93,10 +94,10 @@ namespace osu.Game.Skinning
// but that requires further thought. // but that requires further thought.
var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin; var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin;
string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(
.Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) s => s.Files
|| f.Filename.EndsWith(".jpg", StringComparison.Ordinal)) .Where(f => SupportedExtensions.IMAGE_EXTENSIONS.Contains(Path.GetExtension(f.Filename).ToLowerInvariant()))
.Select(f => f.Filename).Distinct()).ToArray(); .Select(f => f.Filename).Distinct()).ToArray();
if (availableFiles?.Length > 0) if (availableFiles?.Length > 0)
Items = availableFiles; Items = availableFiles;

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osu.Game.Utils;
namespace osu.Game.Storyboards namespace osu.Game.Storyboards
{ {
@ -96,8 +97,6 @@ namespace osu.Game.Storyboards
public virtual DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null) => public virtual DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null) =>
new DrawableStoryboard(this, mods); new DrawableStoryboard(this, mods);
private static readonly string[] image_extensions = { @".png", @".jpg" };
public virtual string? GetStoragePathFromStoryboardPath(string path) public virtual string? GetStoragePathFromStoryboardPath(string path)
{ {
string? resolvedPath = null; string? resolvedPath = null;
@ -109,7 +108,7 @@ namespace osu.Game.Storyboards
else else
{ {
// Some old storyboards don't include a file extension, so let's best guess at one. // Some old storyboards don't include a file extension, so let's best guess at one.
foreach (string ext in image_extensions) foreach (string ext in SupportedExtensions.IMAGE_EXTENSIONS)
{ {
if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null) if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null)
break; break;

View File

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

View File

@ -41,6 +41,12 @@ namespace osu.Game.Users
public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker;
/// <summary>
/// Returns the ID of the beatmap involved in this activity, if applicable and/or available.
/// </summary>
/// <param name="hideIdentifiableInformation"></param>
public virtual int? GetBeatmapID(bool hideIdentifiableInformation = false) => null;
[MessagePackObject] [MessagePackObject]
public class ChoosingBeatmap : UserActivity public class ChoosingBeatmap : UserActivity
{ {
@ -76,6 +82,7 @@ namespace osu.Game.Users
public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb;
public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle;
public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => BeatmapID;
} }
[MessagePackObject] [MessagePackObject]
@ -156,6 +163,11 @@ namespace osu.Game.Users
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information. // For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
? string.Empty ? string.Empty
: BeatmapDisplayTitle; : BeatmapDisplayTitle;
public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => hideIdentifiableInformation
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
? null
: BeatmapID;
} }
[MessagePackObject] [MessagePackObject]

View File

@ -0,0 +1,19 @@
// 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.
namespace osu.Game.Utils
{
public static class SupportedExtensions
{
public static readonly string[] VIDEO_EXTENSIONS = [@".mp4", @".mov", @".avi", @".flv", @".mpg", @".wmv", @".m4v"];
public static readonly string[] AUDIO_EXTENSIONS = [@".mp3", @".ogg", @".wav"];
public static readonly string[] IMAGE_EXTENSIONS = [@".jpg", @".jpeg", @".png"];
public static readonly string[] ALL_EXTENSIONS =
[
..VIDEO_EXTENSIONS,
..AUDIO_EXTENSIONS,
..IMAGE_EXTENSIONS
];
}
}

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.1128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" />
<PackageReference Include="Sentry" Version="4.13.0" /> <PackageReference Include="Sentry" Version="4.13.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1128.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>