1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 19:54:15 +08:00

Allow tagging already played beatmaps without playing another time

Addresses
https://github.com/ppy/osu/discussions/32568#discussioncomment-12610577.

No changes in criteria (yet?), just allowing locally imported plays to
count the same way as full beatmap completion does.

The test scene is a bit rough / semi-manual but dealing with score
imports is a bit of a pain in general. The way to semi-manually test
with the test scene is to import a subset of scores, then recreate the
statistics panel, and observe behaviour. I'm not sure it's worth it to
be putting subscriptions in there, so the full recreation of the panel
is necessary.
This commit is contained in:
Bartłomiej Dach
2025-03-25 14:16:26 +01:00
Unverified
parent 31487545d0
commit 40d20f4e10
2 changed files with 141 additions and 21 deletions
@@ -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;
@@ -27,6 +31,7 @@ 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;
@@ -43,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()
{
@@ -163,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 =>
@@ -176,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.", },
]
@@ -186,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 },
@@ -206,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()
@@ -266,6 +294,79 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestTaggingInteractionWithLocalScores()
{
BeatmapInfo beatmapInfo = null!;
string originalHash = string.Empty;
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
@@ -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,17 +238,29 @@ namespace osu.Game.Screens.Ranking.Statistics
});
}
if (AchievedScore != null
&& newScore.BeatmapInfo!.OnlineID > 0
if (newScore.BeatmapInfo!.OnlineID > 0
&& api.IsLoggedIn)
{
string? preventTaggingReason = null;
// We may want to iterate on the following conditions further in the future
if (AchievedScore.Ruleset.OnlineID != AchievedScore.BeatmapInfo!.Ruleset.OnlineID)
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 (AchievedScore.Rank < ScoreRank.C)
else if (localUserScore.Rank < ScoreRank.C)
preventTaggingReason = "Set a better score to contribute to beatmap tags!";
if (preventTaggingReason == null)