1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 06:21:22 +08:00

Merge branch 'master' into refactor-room-panels

This commit is contained in:
Bartłomiej Dach
2025-03-26 11:58:20 +01:00
committed by GitHub
Unverified
91 changed files with 1648 additions and 518 deletions
+13 -3
View File
@@ -82,8 +82,18 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
shell: pwsh
run: >
dotnet test
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
--
NUnit.ConsoleOut=0
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
@@ -136,4 +146,4 @@ jobs:
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
- name: Build
run: dotnet build -c Debug osu.iOS
run: dotnet build -c Debug osu.iOS.slnf
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.318.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.321.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -1,8 +1,9 @@
// 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.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps;
@@ -35,21 +36,21 @@ namespace osu.Game.Rulesets.Catch.Mods
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
if (!CircleSize.IsDefault)
yield return ("Circle size", $"{CircleSize.Value:N1}");
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate,
spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
foreach (var setting in base.SettingDescription)
yield return setting;
if (!ApproachRate.IsDefault)
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
if (!HardRockOffsets.IsDefault)
yield return ("Spicy patterns", "On");
}
}
@@ -1,7 +1,7 @@
// 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.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@@ -36,19 +36,18 @@ namespace osu.Game.Rulesets.Osu.Mods
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
if (!CircleSize.IsDefault)
yield return ("Circle size", $"{CircleSize.Value:N1}");
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
foreach (var setting in base.SettingDescription)
yield return setting;
if (!ApproachRate.IsDefault)
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
}
}
@@ -1,7 +1,8 @@
// 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.Linq;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -19,17 +20,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
ReadCurrentFromDifficulty = _ => 1,
};
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
foreach (var setting in base.SettingDescription)
yield return setting;
return string.Join(", ", new[]
{
base.SettingDescription,
scrollSpeed
}.Where(s => !string.IsNullOrEmpty(s)));
if (!ScrollSpeed.IsDefault)
yield return ("Scroll speed", $"x{ScrollSpeed.Value:N2}");
}
}
@@ -60,11 +60,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
const double animation_time = 120;
(sprite as IFramedAnimation)?.GotoFrame(0);
var animation = sprite as IFramedAnimation;
animation?.GotoFrame(0);
(strongSprite as IFramedAnimation)?.GotoFrame(0);
this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
// legacy judgements don't play any transforms if they are an animation.
if (animation?.FrameCount > 1)
return;
this.ScaleTo(0.6f)
.Then().ScaleTo(1.1f, animation_time * 0.8)
.Then().ScaleTo(0.9f, animation_time * 0.4)
+10 -1
View File
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-ios</TargetFramework>
@@ -6,11 +7,19 @@
<RootNamespace>osu.Game.Tests</RootNamespace>
<AssemblyName>osu.Game.Tests.iOS</AssemblyName>
</PropertyGroup>
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\osu.Game.Tests\**\*.cs" Exclude="**\obj\**">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<!-- TargetPath is relative to RootNamespace,
and DllResourceStore is relative to AssemblyName. -->
<EmbeddedResource Include="..\osu.Game.Tests\**\Resources\**\*">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
<TargetPath>iOS\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
</ItemGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
@@ -62,12 +62,11 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundDataStoreProcessor());
});
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("wait for difficulties repopulated", () =>
AddAssert("Difficulties repopulated", () =>
{
return Realm.Run(r =>
{
@@ -101,13 +100,10 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundDataStoreProcessor());
});
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddWaitStep("wait some", 500);
AddAssert("Difficulty still not populated", () =>
{
return Realm.Run(r =>
@@ -118,8 +114,9 @@ namespace osu.Game.Tests.Database
});
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("wait for difficulties repopulated", () =>
AddAssert("Difficulties repopulated", () =>
{
return Realm.Run(r =>
{
@@ -151,9 +148,11 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
@@ -183,7 +182,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
@@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
@@ -15,16 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene
{
private DifficultySpectrumDisplay display;
private DifficultySpectrumDisplay display = null!;
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
[SetUpSteps]
public void SetUpSteps()
{
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay
{
RulesetID = difficulty.rulesetId,
StarRating = difficulty.stars
}).ToArray()
};
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3)
});
}
[Test]
public void TestSingleRuleset()
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 0, stars: 3.2),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 1, stars: 4.3),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
@@ -61,52 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 0, stars: 5.6),
(rulesetId: 15, stars: 7.8));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestMaximumUncollapsed()
{
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestMinimumCollapsed()
{
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestAdjustableDotSize()
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
{
var beatmapSet = createBeatmapSetWith(
(rulesetId: 0, stars: 2.0),
(rulesetId: 3, stars: 2.3),
(rulesetId: 0, stars: 3.2),
(rulesetId: 1, stars: 4.3),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("change dot dimensions", () =>
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
{
display.DotSize = new Vector2(8, 12);
display.DotSpacing = 2;
});
AddStep("change dot dimensions back", () =>
{
display.DotSize = new Vector2(4, 8);
display.DotSpacing = 1;
});
}
private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3)
});
RulesetID = difficulty.rulesetId,
StarRating = difficulty.stars
}).ToArray()
};
}
}
@@ -101,15 +101,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public void SetUpSteps()
{
PlaylistItem item = null!;
AddStep("reset state", () =>
{
multiplayerClient.Invocations.Clear();
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
@@ -127,7 +125,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist = { TestMultiplayerClient.CreateMultiplayerPlaylistItem(item) },
Playlist = { new MultiplayerPlaylistItem(item) },
Users = { localUser },
Host = localUser,
};
@@ -139,8 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(250, 50),
SelectedItem = new Bindable<PlaylistItem?>(item)
Size = new Vector2(250, 50)
};
});
}
@@ -924,7 +924,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -956,7 +956,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -1,11 +1,9 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -29,10 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter
{
SelectedItem = new Bindable<PlaylistItem?>()
}
Child = new MultiplayerMatchFooter()
}
};
});
@@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () =>
{
MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();
@@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -71,15 +70,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
Size = new Vector2(200, 50)
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
Size = new Vector2(200, 50)
}
}
}
@@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "flyte",
Id = 3103765,
IsOnline = true,
WasRecentlyOnline = true,
Statistics = new UserStatistics { GlobalRank = 1111 },
CountryCode = CountryCode.JP,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
@@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "peppy",
Id = 2,
IsOnline = false,
WasRecentlyOnline = false,
Statistics = new UserStatistics { GlobalRank = 2222 },
CountryCode = CountryCode.AU,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
@@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 8195163,
CountryCode = CountryCode.BY,
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
WasRecentlyOnline = false,
LastVisit = DateTimeOffset.Now
}
};
@@ -0,0 +1,52 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays.Comments;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneImageProxying : OsuTestScene
{
[Test]
public void TestExternalImageLink()
{
MarkdownContainer markdown = null!;
// use base MarkdownContainer as a method of directly attempting to load an image without proxying logic.
AddStep("load external without proxying", () => Child = markdown = new MarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddWaitStep("wait", 5);
AddAssert("image not loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture == null);
AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddUntilStep("image loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture != null);
}
[Test]
public void TestExternalImageLinkInComments()
{
MarkdownContainer markdown = null!;
AddStep("load external with proxying", () => Child = markdown = new CommentMarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddUntilStep("image loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture != null);
}
}
}
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = countryCode,
CoverUrl = cover,
Colour = color ?? "000000",
IsOnline = true
WasRecentlyOnline = true
};
return new ClickableAvatar(user, showPanel)
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
IsOnline = true
WasRecentlyOnline = true
}) { Width = 300 },
new UserGridPanel(new APIUser
{
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
WasRecentlyOnline = true,
}, new OsuRuleset().RulesetInfo));
AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1002,
Username = "IAmOffline",
LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
WasRecentlyOnline = false,
}, new OsuRuleset().RulesetInfo));
}
@@ -105,6 +105,40 @@ namespace osu.Game.Tests.Visual.Ranking
displayUpdate(statistics, statistics);
}
[Test]
public void TestFromNothing()
{
createDisplay();
displayUpdate(
new UserStatistics(),
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
});
}
[Test]
public void TestToNothing()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
},
new UserStatistics());
}
private void createDisplay() => AddStep("create display", () =>
{
statisticsUpdate.Value = null;
@@ -8,11 +8,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -24,8 +28,10 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
@@ -42,6 +48,22 @@ namespace osu.Game.Tests.Visual.Ranking
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private ScoreManager scoreManager = null!;
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
return dependencies;
}
[Test]
public void TestScoreWithPositionStatistics()
{
@@ -162,6 +184,24 @@ namespace osu.Game.Tests.Visual.Ranking
{
var score = TestResources.CreateTestScoreInfo();
setUpTaggingRequests(() => score.BeatmapInfo);
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
private void setUpTaggingRequests(Func<BeatmapInfo> beatmap) =>
AddStep("set up network requests", () =>
{
dummyAPI.HandleRequest = request =>
@@ -175,7 +215,11 @@ namespace osu.Game.Tests.Visual.Ranking
Tags =
[
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
new APITag
{
Id = 2, Name = "alt",
Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.",
},
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
]
@@ -185,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking
case GetBeatmapSetRequest getBeatmapSetRequest:
{
var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo);
var beatmapSet = CreateAPIBeatmapSet(beatmap.Invoke());
beatmapSet.Beatmaps.Single().TopTags =
[
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
@@ -205,21 +249,6 @@ namespace osu.Game.Tests.Visual.Ranking
return false;
};
});
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
[Test]
public void TestTaggingWhenRankTooLow()
@@ -243,6 +272,100 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestTaggingConvert()
{
var score = TestResources.CreateTestScoreInfo();
score.Ruleset = new ManiaRuleset().RulesetInfo;
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
[Test]
public void TestTaggingInteractionWithLocalScores()
{
BeatmapInfo beatmapInfo = null!;
AddStep(@"Import beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
});
AddStep("import bad score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.Rank = ScoreRank.D;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
AddStep("import score by another user", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.Rank = ScoreRank.D;
score.User = new APIUser { Username = "notme", Id = 5678 };
scoreManager.Import(score);
});
AddStep("import convert score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = new OsuRuleset().RulesetInfo;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
AddStep("import correct score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
setUpTaggingRequests(() => beatmapInfo);
AddStep("load panel", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
}
};
});
}
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
@@ -993,7 +993,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("customisation panel closed",
AddUntilStep("customisation panel closed",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
@@ -1018,7 +1018,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active)
{
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active",
AddUntilStep($"customisation panel is {(active ? "" : "not ")}active",
() => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
}
@@ -196,6 +196,37 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("external overlay content still not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
}
[Test]
public void TestButtonResizedAfterFooterIsDisplayed()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("set buttons", () => screenFooter.SetButtons(new[]
{
new ScreenFooterButton(externalOverlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("resize active button", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(240, 300, Easing.OutQuint));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(116, 300, Easing.OutQuint));
AddStep("hide overlay", () => externalOverlay.Hide());
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
@@ -0,0 +1,43 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearedDropdown : ThemeComparisonTestScene
{
public TestSceneShearedDropdown()
: base(false)
{
}
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black.Opacity(0.75f),
RelativeSizeAxes = Axes.Both,
},
new ShearedDropdown<string>("Test")
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 300f,
Width = 140,
Current = new Bindable<string>(),
Items = new[] { "Global", "Friends", "Local", "Really lonnnnnnng option" },
}
}
};
}
}
+2 -2
View File
@@ -6,9 +6,9 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
namespace osu.Game.Audio
{
@@ -30,7 +30,7 @@ namespace osu.Game.Audio
[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
trackStore = audioManager.GetTrackStore(new OnlineStore());
trackStore = audioManager.GetTrackStore(new TrustedDomainOnlineStore());
}
/// <summary>
+3 -2
View File
@@ -125,9 +125,10 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo()
public void ResetOnlineInfo(bool resetOnlineId = true)
{
OnlineID = -1;
if (resetOnlineId)
OnlineID = -1;
LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty;
if (Status != BeatmapOnlineStatus.LocallyModified)
@@ -36,11 +36,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Origin = Anchor.CentreLeft,
TextSize = 13f
},
new DifficultySpectrumDisplay(beatmapSet)
new DifficultySpectrumDisplay
{
BeatmapSet = beatmapSet,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DotSize = new Vector2(5, 10)
}
}
};
@@ -1,7 +1,6 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
@@ -18,34 +17,6 @@ namespace osu.Game.Beatmaps.Drawables
{
public partial class DifficultySpectrumDisplay : CompositeDrawable
{
private Vector2 dotSize = new Vector2(4, 8);
public Vector2 DotSize
{
get => dotSize;
set
{
dotSize = value;
if (IsLoaded)
updateDisplay();
}
}
private float dotSpacing = 1;
public float DotSpacing
{
get => dotSpacing;
set
{
dotSpacing = value;
if (IsLoaded)
updateDisplay();
}
}
private IBeatmapSetInfo? beatmapSet;
public IBeatmapSetInfo? BeatmapSet
@@ -60,9 +31,12 @@ namespace osu.Game.Beatmaps.Drawables
}
}
private readonly FillFlowContainer<RulesetDifficultyGroup> flow;
private FillFlowContainer<RulesetDifficultyGroup> flow = null!;
public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null)
private const int max_difficulties_before_collapsing = 12;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
@@ -72,8 +46,6 @@ namespace osu.Game.Beatmaps.Drawables
Spacing = new Vector2(10, 0),
Direction = FillDirection.Horizontal,
};
BeatmapSet = beatmapSet;
}
protected override void LoadComplete()
@@ -84,36 +56,70 @@ namespace osu.Game.Beatmaps.Drawables
private void updateDisplay()
{
flow.Clear();
foreach (var group in flow)
group.Alpha = 0;
if (beatmapSet == null)
{
foreach (var group in flow)
group.Beatmaps = [];
return;
}
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
bool collapsed = beatmapSet.Beatmaps.Count() > max_difficulties_before_collapsing;
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
{
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize)
int rulesetId = rulesetGrouping.Key.OnlineID;
var group = flow.SingleOrDefault(rg => rg.RulesetId == rulesetId);
if (group == null)
{
Spacing = new Vector2(DotSpacing, 0f),
});
group = new RulesetDifficultyGroup(rulesetId);
flow.Add(group);
flow.SetLayoutPosition(group, rulesetId);
}
group.Alpha = 1;
group.Beatmaps = rulesetGrouping.ToArray();
group.Collapsed = collapsed;
}
}
private partial class RulesetDifficultyGroup : FillFlowContainer
{
private readonly int rulesetId;
private readonly IEnumerable<IBeatmapInfo> beatmapInfos;
private readonly bool collapsed;
private readonly Vector2 dotSize;
public readonly int RulesetId;
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed, Vector2 dotSize)
private IBeatmapInfo[] beatmaps = [];
public IBeatmapInfo[] Beatmaps
{
this.rulesetId = rulesetId;
this.beatmapInfos = beatmapInfos;
this.collapsed = collapsed;
this.dotSize = dotSize;
set
{
beatmaps = value.OrderBy(bi => bi.StarRating).ToArray();
updateDisplay();
}
}
private bool collapsed;
public bool Collapsed
{
get => collapsed;
set
{
collapsed = value;
updateDisplay();
}
}
private OsuSpriteText countText = null!;
public RulesetDifficultyGroup(int rulesetId)
{
RulesetId = rulesetId;
}
[BackgroundDependencyLoader]
@@ -123,53 +129,83 @@ namespace osu.Game.Beatmaps.Drawables
Spacing = new Vector2(1, 0);
Direction = FillDirection.Horizontal;
var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
var icon = rulesets.GetRuleset(RulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
Add(icon.With(i =>
{
i.Size = new Vector2(14);
i.Anchor = i.Origin = Anchor.Centre;
}));
if (!collapsed)
for (int i = 0; i < max_difficulties_before_collapsing; i++)
Add(new DifficultyDot());
Add(countText = new OsuSpriteText
{
foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating))
Add(new DifficultyDot(beatmapInfo.StarRating, dotSize));
}
else
Font = OsuFont.Default.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 1 }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDisplay();
}
private void updateDisplay()
{
countText.Alpha = collapsed ? 1 : 0;
countText.Text = beatmaps.Length.ToLocalisableString(@"N0");
var dots = this.OfType<DifficultyDot>().ToArray();
for (int i = 0; i < max_difficulties_before_collapsing; i++)
{
Add(new OsuSpriteText
var dot = dots[i];
if (collapsed || i >= beatmaps.Length)
{
Text = beatmapInfos.Count().ToLocalisableString(@"N0"),
Font = OsuFont.Default.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 1 }
});
dot.Alpha = 0;
continue;
}
dot.Alpha = 1;
dot.StarDifficulty = beatmaps[i].StarRating;
}
}
}
private partial class DifficultyDot : CircularContainer
private partial class DifficultyDot : Circle
{
private readonly double starDifficulty;
private double starDifficulty;
public DifficultyDot(double starDifficulty, Vector2 dotSize)
public double StarDifficulty
{
this.starDifficulty = starDifficulty;
Size = dotSize;
get => starDifficulty;
set
{
starDifficulty = value;
updateColour();
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Anchor = Origin = Anchor.Centre;
Masking = true;
[Resolved]
private OsuColour colours { get; set; } = null!;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.ForStarDifficulty(starDifficulty)
};
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(5, 10);
Anchor = Origin = Anchor.Centre;
updateColour();
}
private void updateColour()
{
Colour = colours.ForStarDifficulty(starDifficulty);
}
}
}
+11 -1
View File
@@ -97,8 +97,9 @@ namespace osu.Game.Database
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues).
/// </summary>
private const int schema_version = 47;
private const int schema_version = 48;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -1245,6 +1246,15 @@ namespace osu.Game.Database
break;
}
case 48:
const int qualified = (int)BeatmapOnlineStatus.Qualified;
var beatmaps = migration.NewRealm.All<BeatmapInfo>().Where(b => b.StatusInt == qualified);
foreach (var beatmap in beatmaps)
beatmap.ResetOnlineInfo(resetOnlineId: false);
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
+11 -2
View File
@@ -127,8 +127,6 @@ namespace osu.Game.Graphics.Backgrounds
{
base.Update();
Invalidate(Invalidation.DrawNode);
if (CreateNewTriangles)
addTriangles(false);
@@ -138,6 +136,10 @@ namespace osu.Game.Graphics.Backgrounds
: 1;
float elapsedSeconds = (float)Time.Elapsed / 1000;
if (elapsedSeconds == 0)
return;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
// Since we will later multiply by the scale of individual triangles we normalize by
// dividing by triangleScale.
@@ -157,6 +159,8 @@ namespace osu.Game.Graphics.Backgrounds
if (bottomPos < 0)
parts.RemoveAt(i);
}
Invalidate(Invalidation.DrawNode);
}
/// <summary>
@@ -183,8 +187,13 @@ namespace osu.Game.Graphics.Backgrounds
int currentCount = parts.Count;
if (AimCount - currentCount == 0)
return;
for (int i = 0; i < AimCount - currentCount; i++)
parts.Add(createTriangle(randomY));
Invalidate(Invalidation.DrawNode);
}
private TriangleParticle createTriangle(bool randomY)
+11 -2
View File
@@ -91,12 +91,14 @@ namespace osu.Game.Graphics.Backgrounds
{
base.Update();
Invalidate(Invalidation.DrawNode);
if (CreateNewTriangles)
addTriangles(false);
float elapsedSeconds = (float)Time.Elapsed / 1000;
if (elapsedSeconds == 0)
return;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
float movedDistance = -elapsedSeconds * Velocity * base_velocity / DrawHeight;
@@ -112,6 +114,8 @@ namespace osu.Game.Graphics.Backgrounds
if (bottomPos < 0)
parts.RemoveAt(i);
}
Invalidate(Invalidation.DrawNode);
}
/// <summary>
@@ -138,8 +142,13 @@ namespace osu.Game.Graphics.Backgrounds
int currentCount = parts.Count;
if (AimCount - currentCount == 0)
return;
for (int i = 0; i < AimCount - currentCount; i++)
parts.Add(createTriangle(randomY));
Invalidate(Invalidation.DrawNode);
}
private TriangleParticle createTriangle(bool randomY)
@@ -13,7 +13,7 @@ namespace osu.Game.Graphics.Containers.Markdown
public LocalisableString TooltipText { get; }
public OsuMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)
: base($"https://osu.ppy.sh/media-url?url={linkInline.Url}")
{
TooltipText = linkInline.Title;
}
@@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface
#region OsuDropdownMenu
protected partial class OsuDropdownMenu : DropdownMenu
public partial class OsuDropdownMenu : DropdownMenu
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
@@ -0,0 +1,308 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class ShearedDropdown<T> : Dropdown<T>, IKeyBindingHandler<GlobalAction>
{
protected override DropdownHeader CreateHeader() => new ShearedDropdownHeader();
protected override DropdownMenu CreateMenu() => new ShearedDropdownMenu();
public ShearedDropdown(LocalisableString label)
{
if (Header is ShearedDropdownHeader osuHeader)
{
osuHeader.Dropdown = this;
osuHeader.LeftSideLabel = label;
}
}
protected override void Update()
{
base.Update();
var header = (ShearedDropdownHeader)Header;
var menu = (ShearedDropdownMenu)Menu;
menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f };
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
if (e.Action == GlobalAction.Back)
return Back();
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
protected partial class ShearedDropdownMenu : OsuDropdown<T>.OsuDropdownMenu
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
public ShearedDropdownMenu()
{
Shear = shear;
Margin = new MarginPadding { Top = 5f };
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public ShearedMenuItem(MenuItem item)
: base(item)
{
Foreground.Shear = -shear;
}
}
}
public partial class ShearedDropdownHeader : DropdownHeader
{
private const float corner_radius = 5f;
private LocalisableString label;
protected override LocalisableString Label
{
get => label;
set
{
label = value;
valueText.Text = value;
}
}
public LocalisableString LeftSideLabel
{
set => labelText.Text = value;
}
private readonly OsuSpriteText labelText;
private readonly OsuSpriteText valueText;
private readonly Box labelBox;
private readonly SpriteIcon chevron;
public Container LabelContainer { get; }
public ShearedDropdown<T> Dropdown = null!;
private ShearedDropdownSearchBar searchBar = null!;
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ShearedDropdownHeader()
{
Shear = shear;
CornerRadius = corner_radius;
Masking = true;
Foreground.Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new[]
{
LabelContainer = new Container
{
CornerRadius = corner_radius,
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
labelBox = new Box
{
RelativeSizeAxes = Axes.Both
},
labelText = new OsuSpriteText
{
Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
Shear = -shear,
},
},
},
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10f },
Shear = -shear,
Children = new Drawable[]
{
valueText = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 15f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Y = 1f,
Icon = FontAwesome.Solid.ChevronDown,
Size = new Vector2(10f),
}
},
},
}
}
},
};
AddInternal(LabelContainer.CreateProxy());
}
[BackgroundDependencyLoader]
private void load()
{
labelBox.Colour = colourProvider.Background3;
}
protected override void LoadComplete()
{
base.LoadComplete();
Dropdown.Menu.StateChanged += _ => updateChevron();
SearchBar.State.ValueChanged += _ => updateColour();
Enabled.BindValueChanged(_ => updateColour());
updateColour();
}
protected override void Update()
{
base.Update();
searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth };
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius };
}
protected override bool OnHover(HoverEvent e)
{
updateColour();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateColour();
}
private void updateColour()
{
bool hovered = Enabled.Value && IsHovered;
var hoveredColour = colourProvider.Light4;
var unhoveredColour = colourProvider.Background5;
Colour = Color4.White;
Alpha = Enabled.Value ? 1 : 0.3f;
if (SearchBar.State.Value == Visibility.Visible)
{
chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
Background.Colour = unhoveredColour;
}
else
{
chevron.Colour = Color4.White;
Background.Colour = hovered ? hoveredColour : unhoveredColour;
}
}
private void updateChevron()
{
Debug.Assert(Dropdown != null);
bool open = Dropdown.Menu.State == MenuState.Open;
chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
protected override DropdownSearchBar CreateSearchBar() => searchBar = new ShearedDropdownSearchBar();
private partial class ShearedDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => new DropdownSearchTextBox
{
FontSize = OsuFont.Default.Size,
};
private partial class DropdownSearchTextBox : OsuTextBox
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
TextContainer.Shear = -shear;
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
BorderThickness = 0;
}
}
}
}
}
}
@@ -17,7 +17,13 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Whether to show extended information for each mod."
/// </summary>
public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
public static LocalisableString ShowExtendedInformationDescription =>
new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
/// <summary>
/// "Display direction"
/// </summary>
public static LocalisableString DisplayDirection => new TranslatableString(getKey(@"display_direction"), "Display direction");
/// <summary>
/// "Expansion mode"
@@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Game.Extensions;
using osu.Game.Online.Metadata;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
@@ -111,8 +112,13 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"is_active")]
public bool Active;
/// <summary>
/// From osu-web's perspective, whether a user was recently online.
/// This doesn't imply the user is online in a lazer client (may be updated from stable or web browser).
/// Use <see cref="MetadataClient.GetPresence"/> for real-time lazer online status checks.
/// </summary>
[JsonProperty(@"is_online")]
public bool IsOnline;
public bool WasRecentlyOnline;
[JsonProperty(@"pm_friends_only")]
public bool PMFriendsOnly;
@@ -1,6 +1,7 @@
// 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.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
@@ -219,7 +220,7 @@ namespace osu.Game.Online.Leaderboards
}
};
string description = mod.SettingDescription;
string description = string.Join(", ", mod.SettingDescription.Select(svp => $"{svp.setting}: {svp.value}"));
if (!string.IsNullOrEmpty(description))
{
@@ -227,7 +228,7 @@ namespace osu.Game.Online.Leaderboards
{
RelativeSizeAxes = Axes.Y,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = mod.SettingDescription,
Text = description,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Top = 1 },
@@ -57,6 +57,9 @@ namespace osu.Game.Online.Metadata
/// <summary>
/// Attempts to retrieve the presence of a user.
/// </summary>
/// <remarks>
/// This will return data if the client is currently receiving presence data. See <see cref="BeginWatchingUserPresence"/>.
/// </remarks>
/// <param name="userId">The user ID.</param>
/// <returns>The user presence, or null if not available or the user's offline.</returns>
public UserPresence? GetPresence(int userId)
@@ -44,6 +44,8 @@ namespace osu.Game.Online
(typeof(UserActivity.EditingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.TestingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.InDailyChallengeLobby), typeof(UserActivity)),
(typeof(UserActivity.PlayingDailyChallenge), typeof(UserActivity)),
};
}
}
@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
namespace osu.Game.Online
{
public sealed class TrustedDomainOnlineStore : OnlineStore
{
protected override string GetLookupUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase))
{
Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important);
return string.Empty;
}
return url;
}
}
}
+3 -1
View File
@@ -108,6 +108,8 @@ namespace osu.Game
public virtual EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary>
@@ -278,7 +280,7 @@ namespace osu.Game
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore()));
dependencies.Cache(largeStore);
dependencies.CacheAs(LocalConfig);
@@ -249,6 +249,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
getScoresRequest = null;
noScoresPlaceholder.Hide();
noTeamPlaceholder.Hide();
notSupporterPlaceholder.Hide();
if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending))
{
@@ -271,9 +273,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
}
noTeamPlaceholder.Hide();
notSupporterPlaceholder.Hide();
Show();
loading.Show();
+37 -8
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -22,7 +24,10 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
using ChatStrings = osu.Game.Localisation.ChatStrings;
@@ -69,6 +74,12 @@ namespace osu.Game.Overlays.Chat
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient? multiplayerClient { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved(canBeNull: true)]
private ChannelManager? chatManager { get; set; }
@@ -161,13 +172,10 @@ namespace osu.Game.Overlays.Chat
if (user.Equals(APIUser.SYSTEM_USER))
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)
};
if (user.Equals(api.LocalUser.Value))
return Array.Empty<MenuItem>();
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
List<MenuItem> items = new List<MenuItem>();
if (currentChannel?.Value != null)
{
@@ -177,8 +185,29 @@ namespace osu.Game.Overlays.Chat
}));
}
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
items.Add(new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile));
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
// We should probably be checking against an online state here.
// But we can't use MetadataClient.GetPresence because we may not be requesting/receiving presences.
// This isn't really too bad worst case scenario the client will open spectator view and show the user as "offline".
{
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
{
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user)));
}));
if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true)
{
items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id)));
}
}
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
return items.ToArray();
}
+22 -7
View File
@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
@@ -75,17 +76,31 @@ namespace osu.Game.Overlays.Mods
TabbableContentContainer = this,
Current = { Value = preset.PerformRead(p => p.Description) },
},
new OsuScrollContainer
new Container
{
RelativeSizeAxes = Axes.X,
Height = 100,
Padding = new MarginPadding(7),
Child = scrollContent = new FillFlowContainer
CornerRadius = 10,
Masking = true,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7),
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(7),
Child = scrollContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7),
}
},
}
},
new FillFlowContainer
+47 -17
View File
@@ -1,10 +1,11 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -14,12 +15,20 @@ namespace osu.Game.Overlays.Mods
{
public partial class ModPresetRow : FillFlowContainer
{
private readonly Mod mod;
public ModPresetRow(Mod mod)
{
this.mod = mod;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(4);
Spacing = new Vector2(5);
InternalChildren = new Drawable[]
{
new FillFlowContainer
@@ -39,26 +48,47 @@ namespace osu.Game.Overlays.Mods
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
Origin = Anchor.CentreLeft,
Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold),
Colour = colourProvider.Content1,
UseFullGlyphHeight = false,
Text = mod.Name,
},
}
}
};
if (!string.IsNullOrEmpty(mod.SettingDescription))
{
AddInternal(new OsuTextFlowContainer
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 14 },
Text = mod.SettingDescription
});
}
Padding = new MarginPadding { Horizontal = 10f },
Alpha = mod.SettingDescription.Any() ? 1 : 0,
Children = new Drawable[]
{
new TextFlowContainer(t =>
{
t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold);
})
{
AutoSizeAxes = Axes.Both,
Colour = colourProvider.Content2,
Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.setting)),
},
new TextFlowContainer(t =>
{
t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold);
})
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Colour = colourProvider.Content1,
TextAnchor = Anchor.TopRight,
Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.value)),
},
}
}
};
}
}
}
+15 -2
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,6 +15,9 @@ namespace osu.Game.Overlays.Mods
{
public partial class ModPresetTooltip : VisibilityContainer, ITooltip<ModPreset>
{
[Cached]
private readonly OverlayColourProvider colourProvider;
protected override Container<Drawable> Content { get; }
private const double transition_duration = 200;
@@ -22,6 +26,8 @@ namespace osu.Game.Overlays.Mods
public ModPresetTooltip(OverlayColourProvider colourProvider)
{
this.colourProvider = colourProvider;
Width = 250;
AutoSizeAxes = Axes.Y;
@@ -39,7 +45,7 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 },
Padding = new MarginPadding(10f),
Spacing = new Vector2(7),
Children = new[]
{
@@ -51,6 +57,7 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 5f },
}
}
}
@@ -64,7 +71,13 @@ namespace osu.Game.Overlays.Mods
if (ReferenceEquals(preset, lastPreset))
return;
descriptionText.Text = preset.Description;
if (!string.IsNullOrEmpty(preset.Description))
{
descriptionText.Show();
descriptionText.Text = preset.Description;
}
else
descriptionText.Hide();
lastPreset = preset;
+16 -13
View File
@@ -20,6 +20,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
using osu.Game.Seasonal;
namespace osu.Game.Overlays
{
@@ -256,8 +257,8 @@ namespace osu.Game.Overlays
playableSet = getNextRandom(-1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks)
?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks);
playableSet = getBeatmapSets(allowProtectedTracks).TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault()
?? getBeatmapSets(allowProtectedTracks).LastOrDefault();
}
if (playableSet != null)
@@ -352,10 +353,8 @@ namespace osu.Game.Overlays
playableSet = getNextRandom(1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo))
.Where(i => !i.Value.Protected || allowProtectedTracks)
.ElementAtOrDefault(1)
?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks);
playableSet = getBeatmapSets(allowProtectedTracks).SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1)
?? getBeatmapSets(allowProtectedTracks).FirstOrDefault();
}
var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault();
@@ -376,12 +375,13 @@ namespace osu.Game.Overlays
{
Live<BeatmapSetInfo> result;
var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList();
var possibleSets = getBeatmapSets(allowProtectedTracks).ToList();
if (possibleSets.Count == 0)
return null;
// if there is only one possible set left, play it, even if it is the same as the current track.
// if there is only
// one possible set left, play it, even if it is the same as the current track.
// looping is preferable over playing nothing.
if (possibleSets.Count == 1)
return possibleSets.Single();
@@ -459,9 +459,12 @@ namespace osu.Game.Overlays
private TrackChangeDirection? queuedDirection;
private IEnumerable<Live<BeatmapSetInfo>> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending)
.AsEnumerable()
.Select(s => new RealmLive<BeatmapSetInfo>(s, realm));
private IEnumerable<Live<BeatmapSetInfo>> getBeatmapSets(bool allowProtectedTracks) =>
realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending)
.AsEnumerable()
.Select(s => new RealmLive<BeatmapSetInfo>(s, realm))
.Where(i => (allowProtectedTracks || !i.Value.Protected)
&& (SeasonalUIConfig.ENABLED || i.Value.Hash != IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH));
private void changeBeatmap(WorkingBeatmap newWorking)
{
@@ -488,8 +491,8 @@ namespace osu.Game.Overlays
else
{
// figure out the best direction based on order in playlist.
int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count();
int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count();
int last = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count();
int next = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count();
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
}
@@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Profile.Header
addSpacer(topLinkContainer);
if (user.IsOnline)
if (user.WasRecentlyOnline)
{
topLinkContainer.AddText(UsersStrings.ShowLastvisitOnline);
addSpacer(topLinkContainer);
+4 -19
View File
@@ -3,14 +3,13 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -29,8 +28,6 @@ namespace osu.Game.Overlays
private const int header_height = 30;
private const int corner_radius = 5;
private readonly Cached headerTextVisibilityCache = new Cached();
protected override Container<Drawable> Content => content;
private readonly FillFlowContainer content = new FillFlowContainer
@@ -156,13 +153,9 @@ namespace osu.Game.Overlays
{
base.Update();
if (!headerTextVisibilityCache.IsValid)
{
// These toolbox grouped may be contracted to only show icons.
// For now, let's hide the header to avoid text truncation weirdness in such cases.
headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint);
headerTextVisibilityCache.Validate();
}
// These toolbox grouped may be contracted to only show icons.
// For now, let's hide the header to avoid text truncation weirdness in such cases.
headerText.Alpha = (float)Interpolation.DampContinuously(headerText.Alpha, headerText.DrawWidth < DrawWidth ? 1 : 0, 40, Time.Elapsed);
// Dragged child finished its drag operation.
if (draggedChild != null && inputManager.DraggedDrawable != draggedChild)
@@ -172,14 +165,6 @@ namespace osu.Game.Overlays
}
}
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
if (invalidation.HasFlag(Invalidation.DrawSize))
headerTextVisibilityCache.Invalidate();
return base.OnInvalidate(invalidation, source);
}
private void updateExpandedState(bool animate)
{
// before we collapse down, let's double check the user is not dragging a UI control contained within us.
+4 -27
View File
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mods
@@ -43,36 +42,16 @@ namespace osu.Game.Rulesets.Mods
public abstract LocalisableString Description { get; }
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
/// </summary>
/// <remarks>
/// Differs from <see cref="Name"/>, as the value of attributes (AR, CS, etc) changeable via the mod
/// are displayed in the tooltip.
/// </remarks>
[JsonIgnore]
public string IconTooltip
{
get
{
string description = SettingDescription;
return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})";
}
}
/// <summary>
/// The description of editable settings of a mod to use in the <see cref="IconTooltip"/>.
/// The description of editable settings of a mod.
/// </summary>
/// <remarks>
/// Parentheses are added to the tooltip, surrounding the value of this property. If this property is <c>string.Empty</c>,
/// the tooltip will not have parentheses.
/// </remarks>
public virtual string SettingDescription
public virtual IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
var tooltipTexts = new List<string>();
foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties())
{
var bindable = (IBindable)property.GetValue(this)!;
@@ -82,7 +61,7 @@ namespace osu.Game.Rulesets.Mods
switch (bindable)
{
case Bindable<bool> b:
valueText = b.Value ? "on" : "off";
valueText = b.Value ? "On" : "Off";
break;
default:
@@ -91,10 +70,8 @@ namespace osu.Game.Rulesets.Mods
}
if (!bindable.IsDefault)
tooltipTexts.Add($"{attr.Label}: {valueText}");
yield return (attr.Label, valueText);
}
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
}
}
+16 -2
View File
@@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation.HUD;
@@ -33,7 +34,20 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true;
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
if (!MinimumAccuracy.IsDefault)
yield return ("Minimum accuracy", $"{MinimumAccuracy.Value:##%}");
if (!AccuracyJudgeMode.IsDefault)
yield return ("Accuracy mode", AccuracyJudgeMode.Value.ToLocalisableString());
if (!Restart.IsDefault)
yield return ("Restart on fail", "On");
}
}
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))]
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
+9 -1
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
@@ -38,7 +39,14 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm");
yield return ("Direction", Direction.Value.GetDescription());
}
}
private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!;
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@@ -65,18 +65,15 @@ namespace osu.Game.Rulesets.Mods
}
}
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}";
string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}";
if (!DrainRate.IsDefault)
yield return ("HP drain", $"{DrainRate.Value:N1}");
return string.Join(", ", new[]
{
drainRate,
overallDifficulty
}.Where(s => !string.IsNullOrEmpty(s)));
if (!OverallDifficulty.IsDefault)
yield return ("Accuracy", $"{OverallDifficulty.Value:N1}");
}
}
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
@@ -20,7 +22,15 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 10
};
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
if (!Retries.IsDefault)
yield return ("Extra lives", "lives".ToQuantity(Retries.Value));
}
}
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries;
+10 -3
View File
@@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -24,8 +26,13 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) };
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
public override string ExtendedIconInformation => SettingDescription;
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
if (!SpeedChange.IsDefault)
yield return ("Speed change", $"{SpeedChange.Value:N2}x");
}
}
}
}
+9 -1
View File
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
@@ -34,7 +36,13 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x");
}
}
private double finalRateTime;
private double beginRampTime;
+9 -3
View File
@@ -10,11 +10,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// Display the specified mod at a fixed size.
/// </summary>
public partial class ModIcon : Container, IHasTooltip
public partial class ModIcon : Container, IHasCustomTooltip<Mod>
{
public readonly BindableBool Selected = new BindableBool();
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.UI
public static readonly Vector2 MOD_ICON_SIZE = new Vector2(80);
public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : string.Empty;
public Mod? TooltipContent { get; private set; }
private IMod mod;
@@ -70,6 +70,9 @@ namespace osu.Game.Rulesets.UI
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
private Color4 backgroundColour;
private Sprite extendedBackground = null!;
@@ -188,6 +191,7 @@ namespace osu.Game.Rulesets.UI
modAcronym.Text = value.Acronym;
modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question;
TooltipContent = showTooltip ? value as Mod : null;
if (value.Icon == null)
{
@@ -227,5 +231,7 @@ namespace osu.Game.Rulesets.UI
base.Dispose(isDisposing);
modSettingsChangeTracker?.Dispose();
}
public ITooltip<Mod> GetCustomTooltip() => new ModTooltip(colourProvider);
}
}
+141
View File
@@ -0,0 +1,141 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.UI
{
public partial class ModTooltip : VisibilityContainer, ITooltip<Mod>
{
private readonly OverlayColourProvider colourProvider;
private OsuSpriteText nameText = null!;
private TextFlowContainer settingsLabelsFlow = null!;
private TextFlowContainer settingsValuesFlow = null!;
public ModTooltip(OverlayColourProvider? colourProvider = null)
{
this.colourProvider = colourProvider ?? new OverlayColourProvider(OverlayColourScheme.Aquamarine);
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 7;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 10f,
};
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding(10f),
Spacing = new Vector2(20f, 0f),
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
nameText = new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold),
Colour = colourProvider.Content1,
UseFullGlyphHeight = false,
},
settingsLabelsFlow = new TextFlowContainer(t =>
{
t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold);
})
{
AutoSizeAxes = Axes.Both,
Colour = colourProvider.Content2,
},
},
},
settingsValuesFlow = new TextFlowContainer(t =>
{
t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold);
})
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Colour = colourProvider.Content1,
TextAnchor = Anchor.TopRight,
},
},
}
};
}
private Mod? displayedContent;
public void SetContent(Mod content)
{
if (content == displayedContent)
return;
displayedContent = content;
nameText.Text = content.Name;
settingsLabelsFlow.Clear();
settingsValuesFlow.Clear();
if (content.SettingDescription.Any())
{
settingsLabelsFlow.Show();
settingsValuesFlow.Show();
foreach (var part in content.SettingDescription)
{
settingsLabelsFlow.AddText(part.setting);
settingsLabelsFlow.NewLine();
settingsValuesFlow.AddText(part.value);
settingsValuesFlow.NewLine();
}
}
else
{
settingsLabelsFlow.Hide();
settingsValuesFlow.Hide();
}
}
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
}
}
@@ -146,22 +146,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
},
new Drawable[]
{
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
{
Padding = new MarginPadding { Horizontal = 15, Vertical = 2 },
Text = "beat snap",
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre,
},
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 40),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 15)
}
}
};
+58 -36
View File
@@ -33,7 +33,8 @@ namespace osu.Game.Screens.Footer
private Box background = null!;
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
private Container<ScreenFooterButton> removedButtonsContainer = null!;
private Container footerContentContainer = null!;
private Container<ScreenFooterButton> hiddenButtonsContainer = null!;
private LogoTrackingContainer logoTrackingContainer = null!;
[Cached]
@@ -71,15 +72,35 @@ namespace osu.Game.Screens.Footer
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
new GridContainer
{
Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
Y = 10f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7, 0),
AutoSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Y = 10f,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7, 0),
AutoSizeAxes = Axes.Both,
},
footerContentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Y = -15f,
},
},
}
},
BackButton = new ScreenBackButton
{
@@ -88,7 +109,7 @@ namespace osu.Game.Screens.Footer
Origin = Anchor.BottomLeft,
Action = onBackPressed,
},
removedButtonsContainer = new Container<ScreenFooterButton>
hiddenButtonsContainer = new Container<ScreenFooterButton>
{
Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding },
Y = 10f,
@@ -153,7 +174,7 @@ namespace osu.Game.Screens.Footer
var oldButton = oldButtons[i];
buttonsFlow.Remove(oldButton, false);
removedButtonsContainer.Add(oldButton);
hiddenButtonsContainer.Add(oldButton);
if (buttons.Count > 0)
makeButtonDisappearToRight(oldButton, i, oldButtons.Length, true);
@@ -188,7 +209,7 @@ namespace osu.Game.Screens.Footer
}
private ShearedOverlayContainer? activeOverlay;
private Container? contentContainer;
private VisibilityContainer? activeFooterContent;
private readonly List<ScreenFooterButton> temporarilyHiddenButtons = new List<ScreenFooterButton>();
@@ -210,33 +231,28 @@ namespace osu.Game.Screens.Footer
? buttonsFlow.SkipWhile(b => b != targetButton).Skip(1)
: buttonsFlow);
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
makeButtonDisappearToBottom(temporarilyHiddenButtons[i], 0, 0, false);
for (int i = temporarilyHiddenButtons.Count - 1; i >= 0; i--)
{
var button = temporarilyHiddenButtons[i];
buttonsFlow.Remove(button, false);
hiddenButtonsContainer.Add(button);
var fallbackPosition = buttonsFlow.Any()
? buttonsFlow.ToSpaceOfOtherDrawable(Vector2.Zero, this)
: BackButton.ToSpaceOfOtherDrawable(BackButton.LayoutRectangle.TopRight + new Vector2(5f, 0f), this);
var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition;
makeButtonDisappearToBottom(button, 0, 0, false);
}
updateColourScheme(overlay.ColourProvider.Hue);
footerContent = overlay.CreateFooterContent();
activeFooterContent = footerContent;
var content = footerContent;
var content = footerContent ?? Empty();
Add(contentContainer = new Container
{
Y = -15f,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = targetPosition.X },
Child = content,
});
if (content != null)
footerContentContainer.Child = content;
if (temporarilyHiddenButtons.Count > 0)
this.Delay(60).Schedule(() => content.Show());
this.Delay(60).Schedule(() => content?.Show());
else
content.Show();
content?.Show();
return new InvokeOnDisposal(clearActiveOverlayContainer);
}
@@ -246,20 +262,26 @@ namespace osu.Game.Screens.Footer
if (activeOverlay == null)
return;
Debug.Assert(contentContainer != null);
contentContainer.Child.Hide();
Debug.Assert(activeFooterContent != null);
activeFooterContent.Hide();
double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current;
double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current;
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0);
{
var button = temporarilyHiddenButtons[i];
hiddenButtonsContainer.Remove(button, false);
buttonsFlow.Add(button);
makeButtonAppearFromBottom(button, 0);
}
temporarilyHiddenButtons.Clear();
updateColourScheme(OverlayColourScheme.Aquamarine.GetHue());
contentContainer.Delay(timeUntilRun).Expire();
contentContainer = null;
activeFooterContent.Delay(timeUntilRun).Expire();
activeFooterContent = null;
activeOverlay = null;
}
+10 -1
View File
@@ -20,6 +20,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -45,7 +46,7 @@ using osu.Game.Localisation;
namespace osu.Game.Screens.Menu
{
public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler<GlobalAction>
public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler<GlobalAction>, ISamplePlaybackDisabler
{
public const float FADE_IN_DURATION = 300;
@@ -84,6 +85,10 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
// used to stop kiai fountain samples when navigating to other screens
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false;
@@ -369,6 +374,8 @@ namespace osu.Game.Screens.Menu
supporterDisplay
.FadeOut(500, Easing.OutQuint);
samplePlaybackDisabled.Value = true;
}
public override void OnResuming(ScreenTransitionEvent e)
@@ -389,6 +396,8 @@ namespace osu.Game.Screens.Menu
bottomElementsFlow
.ScaleTo(1, 1000, Easing.OutQuint)
.FadeIn(1000, Easing.OutQuint);
samplePlaybackDisabled.Value = false;
}
public override bool OnExiting(ScreenExitEvent e)
@@ -39,6 +39,7 @@ using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
@@ -107,6 +108,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
public override bool? ApplyModTrackAdjustments => true;
protected override UserActivity InitialActivity => new UserActivity.InDailyChallengeLobby();
public DailyChallenge(Room room)
{
this.room = room;
@@ -526,7 +529,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void startPlay()
{
sampleStart?.Play();
this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem)
this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem)
{
Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores())
}));
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengePlayer : PlaylistsPlayer
{
protected override UserActivity InitialActivity => new UserActivity.PlayingDailyChallenge(Beatmap.Value.BeatmapInfo, Ruleset.Value);
public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null)
: base(room, playlistItem, configuration)
{
}
}
}
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osuTK;
@@ -23,22 +22,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public partial class MatchStartControl : CompositeDrawable
{
public required Bindable<PlaylistItem?> SelectedItem
{
get => selectedItem;
set => selectedItem.Current = value;
}
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
[Resolved(canBeNull: true)]
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private readonly BindableWithCurrent<PlaylistItem?> selectedItem = new BindableWithCurrent<PlaylistItem?>();
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
@@ -98,9 +90,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => updateState());
client.RoomUpdated += onRoomUpdated;
client.LoadRequested += onLoadRequested;
updateState();
}
@@ -214,8 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
readyButton.Enabled.Value = countdownButton.Enabled.Value =
client.Room.State != MultiplayerRoomState.Closed
&& SelectedItem.Value?.ID == client.Room.Settings.PlaylistItemId
&& !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired
&& !client.Room.CurrentPlaylistItem.Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
@@ -1,10 +1,8 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
@@ -13,14 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private const float ready_button_width = 600;
private const float spectate_button_width = 200;
public required Bindable<PlaylistItem?> SelectedItem
{
get => selectedItem;
set => selectedItem.Current = value;
}
private readonly BindableWithCurrent<PlaylistItem?> selectedItem = new BindableWithCurrent<PlaylistItem?>();
public MultiplayerMatchFooter()
{
RelativeSizeAxes = Axes.Both;
@@ -36,13 +26,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
new MultiplayerSpectateButton
{
RelativeSizeAxes = Axes.Both,
SelectedItem = selectedItem
},
null,
new MatchStartControl
{
RelativeSizeAxes = Axes.Both,
SelectedItem = selectedItem
},
null
}
@@ -21,12 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public partial class MultiplayerSpectateButton : CompositeDrawable
{
public required Bindable<PlaylistItem?> SelectedItem
{
get => selectedItem;
set => selectedItem.Current = value;
}
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
@@ -36,7 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private readonly BindableWithCurrent<PlaylistItem?> selectedItem = new BindableWithCurrent<PlaylistItem?>();
private readonly RoundedButton button;
private IBindable<bool> operationInProgress = null!;
@@ -75,7 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
client.RoomUpdated += onRoomUpdated;
updateState();
}
@@ -121,11 +113,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void checkForAutomaticDownload()
{
PlaylistItem? item = SelectedItem.Value;
downloadCheckCancellation?.Cancel();
if (item == null)
if (client.Room == null)
return;
if (!automaticallyDownload.Value)
@@ -140,10 +130,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (client.LocalUser?.State != MultiplayerUserState.Spectating)
return;
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
beatmapLookupCache
.GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.ContinueWith(resolved => Schedule(() =>
{
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
@@ -60,6 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return false;
}
if (!base.OnStart())
return false;
selectionOperation = operationTracker.BeginOperation();
client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID)
@@ -254,10 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
this.Push(new MultiplayerMatchFreestyleSelect(Room, item));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
{
SelectedItem = SelectedItem
};
protected override Drawable CreateFooter() => new MultiplayerMatchFooter();
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
@@ -7,6 +7,7 @@ using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms;
@@ -43,6 +44,40 @@ namespace osu.Game.Screens.OnlinePlay
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
}
protected override bool OnStart()
{
FilterCriteria criteria = FilterControl.CreateCriteria();
// Beatmaps with too different of a duration are filtered away; this is just a final safety.
if (!criteria.Length.IsInRange(Beatmap.Value.BeatmapInfo.Length))
{
Logger.Log("The selected beatmap's duration differs too much from the host's selection.", level: LogLevel.Error);
return false;
}
// Beatmaps without a valid online ID are filtered away; this is just a final safety.
if (Beatmap.Value.BeatmapInfo.OnlineID < 0)
{
Logger.Log("The selected beatmap is not available online.", level: LogLevel.Error);
return false;
}
// Beatmaps from different sets are filtered away; this is just a final safety.
if (Beatmap.Value.BeatmapSetInfo.OnlineID != criteria.BeatmapSetId)
{
Logger.Log("The selected beatmap is from a different beatmap set.", level: LogLevel.Error);
return false;
}
if (Ruleset.Value.OnlineID < 0)
{
Logger.Log("The selected ruleset is not available online.", level: LogLevel.Error);
return false;
}
return true;
}
protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item);
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
@@ -21,15 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override bool OnStart()
{
// Beatmaps without a valid online ID are filtered away; this is just a final safety.
if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0)
return false;
if (base.Ruleset.Value.OnlineID < 0)
if (!base.OnStart())
return false;
Beatmap.Value = base.Beatmap.Value.BeatmapInfo;
Ruleset.Value = base.Ruleset.Value;
this.Exit();
return true;
}
+8 -2
View File
@@ -67,6 +67,12 @@ namespace osu.Game.Screens.Play.HUD
}
}
public FillDirection FillDirection
{
get => iconsContainer.Direction;
set => iconsContainer.Direction = value;
}
private readonly FillFlowContainer<ModIcon> iconsContainer;
public ModDisplay(bool showExtendedInformation = true)
@@ -122,13 +128,13 @@ namespace osu.Game.Screens.Play.HUD
private void expand(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysContracted)
iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(5, -10), duration, Easing.OutQuint);
}
private void contract(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysExpanded)
iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(-25), duration, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
@@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))]
public Bindable<ExpansionMode> ExpansionModeSetting { get; } = new Bindable<ExpansionMode>();
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.DisplayDirection))]
public Bindable<Direction> Direction { get; } = new Bindable<Direction>();
[BackgroundDependencyLoader]
private void load()
{
@@ -50,6 +53,7 @@ namespace osu.Game.Screens.Play.HUD
ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true);
ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true);
Direction.BindValueChanged(_ => modDisplay.FillDirection = Direction.Value == Framework.Graphics.Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical, true);
FinishTransforms(true);
}
+3
View File
@@ -147,6 +147,9 @@ namespace osu.Game.Screens.Play
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// This display is potentially a duplicate of users with a local ModDisplay in their skins.
// It would be very nice to remove this, but the version here has special logic with regards to replays
// and initial states, so needs a bit of thought before doing so.
ModDisplay = CreateModsContainer(),
}
},
+21 -3
View File
@@ -5,9 +5,12 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Audio;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
@@ -31,14 +34,29 @@ namespace osu.Game.Screens.Play
OnResume?.Invoke();
};
private readonly IBindable<bool> windowActive = new Bindable<bool>(true);
private float targetVolume => windowActive.Value && State.Value == Visibility.Visible ? 1.0f : 0;
[BackgroundDependencyLoader]
private void load()
private void load(GameHost? host)
{
AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop"))
{
Looping = true,
Volume = { Value = 0 }
});
if (host != null)
windowActive.BindTo(host.IsActive);
}
protected override void LoadComplete()
{
base.LoadComplete();
// Schedule required because host.IsActive doesn't seem to always run on the update thread.
windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out)));
}
public void StopAllSamples()
@@ -53,7 +71,7 @@ namespace osu.Game.Screens.Play
{
base.PopIn();
pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint);
pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.InQuint);
pauseLoop.Play();
}
@@ -61,7 +79,7 @@ namespace osu.Game.Screens.Play
{
base.PopOut();
pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop());
pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop());
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@@ -108,12 +108,10 @@ namespace osu.Game.Screens.Ranking.Contracted
Offset = new Vector2(0, 1),
}
},
new OsuSpriteText
new ClickableUsername(score.User)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = score.RealmUser.Username,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold)
},
new FillFlowContainer
{
@@ -106,22 +106,7 @@ namespace osu.Game.Screens.Ranking.Expanded
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
},
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
},
new ClickableMetadata(beatmap.OnlineID, metadata),
new Container
{
Anchor = Anchor.TopCentre,
@@ -316,5 +301,47 @@ namespace osu.Game.Screens.Ranking.Expanded
time.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
}
}
internal partial class ClickableMetadata : OsuHoverContainer
{
[Resolved]
private OsuGame? game { get; set; }
public ClickableMetadata(int beatmapId, IBeatmapMetadataInfo metadata)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
},
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
}
}
};
if (beatmapId > 0)
Action = () => game?.ShowBeatmap(beatmapId);
}
}
}
}
@@ -8,8 +8,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users.Drawables;
using osuTK;
@@ -62,12 +60,10 @@ namespace osu.Game.Screens.Ranking.Expanded
CornerExponent = 2.5f,
Masking = true,
},
new OsuSpriteText
new ClickableUsername(user)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = user.Username,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold)
}
}
};
@@ -14,14 +14,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osuTK;
using Realms;
namespace osu.Game.Screens.Ranking.Statistics
{
@@ -43,6 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -231,14 +238,32 @@ namespace osu.Game.Screens.Ranking.Statistics
});
}
if (AchievedScore != null
&& newScore.BeatmapInfo!.OnlineID > 0
if (newScore.BeatmapInfo!.OnlineID > 0
&& api.IsLoggedIn)
{
if (
// We may want to iterate on this condition
AchievedScore.Rank >= ScoreRank.C
)
string? preventTaggingReason = null;
// We may want to iterate on the following conditions further in the future
var localUserScore = AchievedScore ?? realm.Run(r =>
r.All<ScoreInfo>()
.Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
+ $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
+ $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
+ $@" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, newScore.BeatmapInfo.ID, newScore.BeatmapInfo.Ruleset.ShortName)
.AsEnumerable()
.OrderByDescending(score => score.Ruleset.MatchesOnlineID(newScore.BeatmapInfo.Ruleset))
.ThenByDescending(score => score.Rank)
.FirstOrDefault());
if (localUserScore == null)
preventTaggingReason = "Play the beatmap to contribute to beatmap tags!";
else if (localUserScore.Ruleset.OnlineID != newScore.BeatmapInfo!.Ruleset.OnlineID)
preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!";
else if (localUserScore.Rank < ScoreRank.C)
preventTaggingReason = "Set a better score to contribute to beatmap tags!";
if (preventTaggingReason == null)
{
yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo)
{
@@ -254,7 +279,7 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.Centre,
Text = "Set a better score to contribute to beatmap tags!",
Text = preventTaggingReason,
});
}
}
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils;
namespace osu.Game.Screens.Ranking.Statistics.User
{
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple;
protected override LocalisableString FormatCurrentValue(int? current)
=> current == null ? string.Empty : current.Value.FormatRank();
=> current?.ToLocalisableString(@"N0") ?? string.Empty;
protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference)
{
@@ -30,13 +30,13 @@ namespace osu.Game.Screens.Ranking.Statistics.User
if (previous == null && current != null)
{
formattedDifference = LocalisableString.Interpolate($"+{current.Value.FormatRank()}");
formattedDifference = LocalisableString.Interpolate($"+{current.Value:N0}");
return 1;
}
if (previous != null && current == null)
{
formattedDifference = LocalisableString.Interpolate($"-{previous.Value.FormatRank()}");
formattedDifference = LocalisableString.Interpolate($"-{previous.Value:N0}");
return -1;
}
@@ -46,9 +46,9 @@ namespace osu.Game.Screens.Ranking.Statistics.User
int difference = previous.Value - current.Value;
if (difference < 0)
formattedDifference = difference.FormatRank();
formattedDifference = difference.ToLocalisableString(@"N0");
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($"+{difference.FormatRank()}");
formattedDifference = LocalisableString.Interpolate($"+{difference:N0}");
else
formattedDifference = string.Empty;
+1 -1
View File
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2
{
public Action<BeatmapInfo>? RequestPresentBeatmap { private get; init; }
public const float SPACING = 5f;
public const float SPACING = 3f;
private IBindableList<BeatmapSetInfo> detachedBeatmaps = null!;
@@ -244,18 +244,18 @@ namespace osu.Game.Screens.SelectV2.Footer
Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true);
}
public ITooltip<IReadOnlyList<Mod>> GetCustomTooltip() => new ModTooltip(colourProvider);
public ITooltip<IReadOnlyList<Mod>> GetCustomTooltip() => new ModOverflowTooltip(colourProvider);
public IReadOnlyList<Mod>? TooltipContent => Mods.Value;
public partial class ModTooltip : VisibilityContainer, ITooltip<IReadOnlyList<Mod>>
public partial class ModOverflowTooltip : VisibilityContainer, ITooltip<IReadOnlyList<Mod>>
{
private ModDisplay extendedModDisplay = null!;
[Cached]
private OverlayColourProvider colourProvider;
public ModTooltip(OverlayColourProvider colourProvider)
public ModOverflowTooltip(OverlayColourProvider colourProvider)
{
this.colourProvider = colourProvider;
}
@@ -715,18 +715,21 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
public LocalisableString TooltipText { get; }
}
private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip
private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip<Mod>
{
private readonly IMod mod;
public Mod? TooltipContent { get; }
public ColouredModSwitchTiny(IMod mod)
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ColouredModSwitchTiny(Mod mod)
: base(mod)
{
this.mod = mod;
TooltipContent = mod;
Active.Value = true;
}
public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name;
public ITooltip<Mod> GetCustomTooltip() => new ModTooltip(colourProvider);
}
private sealed partial class MoreModSwitchTiny : CompositeDrawable
@@ -106,8 +106,6 @@ namespace osu.Game.Screens.SelectV2
},
difficultiesDisplay = new DifficultySpectrumDisplay
{
DotSize = new Vector2(5, 10),
DotSpacing = 2,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
+1 -1
View File
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2
{
private const float logo_scale = 0.4f;
private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay
private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine)
{
ShowPresets = true,
};
@@ -238,7 +238,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
QueueMode = ServerAPIRoom.QueueMode,
AutoStartDuration = ServerAPIRoom.AutoStartDuration
},
Playlist = ServerAPIRoom.Playlist.Select(CreateMultiplayerPlaylistItem).ToList(),
Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(),
Users = { localUser },
Host = localUser
};
@@ -687,21 +687,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
}
public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem(PlaylistItem item) => new MultiplayerPlaylistItem
{
ID = item.ID,
OwnerID = item.OwnerID,
BeatmapID = item.Beatmap.OnlineID,
BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.ToArray(),
AllowedMods = item.AllowedMods.ToArray(),
Expired = item.Expired,
PlaylistOrder = item.PlaylistOrder ?? 0,
PlayedAt = item.PlayedAt,
StarRating = item.Beatmap.StarRating,
};
public override Task DisconnectInternal()
{
isConnected.Value = false;
@@ -0,0 +1,48 @@
// 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.Cursor;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
{
internal partial class ClickableUsername : OsuHoverContainer, IHasCustomTooltip<APIUser>
{
public ITooltip<APIUser?> GetCustomTooltip() => new ClickableAvatar.NoCardTooltip();
public APIUser? TooltipContent { get; }
private readonly APIUser user;
[Resolved]
private OsuGame? game { get; set; }
public ClickableUsername(APIUser? user)
{
TooltipContent = this.user = user ?? new GuestUser();
AutoSizeAxes = Axes.Both;
Child = new OsuSpriteText
{
Text = user!.Username,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
};
if (user.Id != APIUser.SYSTEM_USER_ID)
Action = openProfile;
}
private void openProfile()
{
if (user.Id > 1 || !string.IsNullOrEmpty(user.Username))
game?.ShowUser(user);
}
}
}
@@ -8,8 +8,10 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
@@ -64,6 +66,12 @@ namespace osu.Game.Users.Drawables
public LocalisableString TooltipText { get; }
[Resolved]
private OsuGame? game { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
public TeamFlag(APITeam team)
{
this.team = team;
@@ -91,6 +99,12 @@ namespace osu.Game.Users.Drawables
}
};
}
protected override bool OnClick(ClickEvent e)
{
game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/teams/{team.Id}");
return true;
}
}
}
}
+29 -1
View File
@@ -34,6 +34,8 @@ namespace osu.Game.Users
[Union(41, typeof(EditingBeatmap))]
[Union(42, typeof(ModdingBeatmap))]
[Union(43, typeof(TestingBeatmap))]
[Union(51, typeof(InDailyChallengeLobby))]
[Union(52, typeof(PlayingDailyChallenge))]
public abstract class UserActivity
{
public abstract string GetStatus(bool hideIdentifiableInformation = false);
@@ -58,6 +60,7 @@ namespace osu.Game.Users
[Union(23, typeof(InMultiplayerGame))]
[Union(24, typeof(SpectatingMultiplayerGame))]
[Union(31, typeof(InPlaylistGame))]
[Union(52, typeof(PlayingDailyChallenge))]
public abstract class InGame : UserActivity
{
[Key(0)]
@@ -244,7 +247,7 @@ namespace osu.Game.Users
[SerializationConstructor]
public SpectatingMultiplayerGame() { }
public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}";
public override string GetStatus(bool hideIdentifiableInformation = false) => @"Spectating a multiplayer game";
}
[MessagePackObject]
@@ -277,5 +280,30 @@ namespace osu.Game.Users
? null
: RoomName;
}
[MessagePackObject]
public class InDailyChallengeLobby : UserActivity
{
[SerializationConstructor]
public InDailyChallengeLobby() { }
public override string GetStatus(bool hideIdentifiableInformation = false) => @"In daily challenge lobby";
}
[MessagePackObject]
public class PlayingDailyChallenge : InGame
{
public PlayingDailyChallenge(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
: base(beatmapInfo, ruleset)
{
}
[SerializationConstructor]
public PlayingDailyChallenge()
{
}
public override string GetStatus(bool hideIdentifiableInformation = false) => @$"{RulesetPlayingVerb} in daily challenge";
}
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.318.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.318.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.321.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.321.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" />
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.318.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.321.0" />
</ItemGroup>
</Project>