mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 02:22:35 +08:00
507 lines
19 KiB
C#
507 lines
19 KiB
C#
// 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;
|
|
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;
|
|
using osu.Game.Graphics.Sprites;
|
|
using osu.Game.Graphics.UserInterface;
|
|
using osu.Game.Online;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.API.Requests;
|
|
using osu.Game.Online.API.Requests.Responses;
|
|
using osu.Game.Overlays;
|
|
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;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Rulesets.UI;
|
|
using osu.Game.Screens.Ranking.Statistics.User;
|
|
using osu.Game.Tests.Resources;
|
|
using osu.Game.Users;
|
|
using osuTK;
|
|
|
|
namespace osu.Game.Tests.Visual.Ranking
|
|
{
|
|
public partial class TestSceneStatisticsPanel : OsuTestScene
|
|
{
|
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
|
|
|
private ScoreManager scoreManager = null!;
|
|
private RulesetStore rulesetStore = null!;
|
|
private BeatmapManager beatmapManager = null!;
|
|
|
|
[Cached]
|
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
|
|
|
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()
|
|
{
|
|
var score = TestResources.CreateTestScoreInfo();
|
|
score.OnlineID = 1234;
|
|
score.HitEvents = CreatePositionDistributedHitEvents();
|
|
|
|
loadPanel(score);
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreWithTimeStatistics()
|
|
{
|
|
var score = TestResources.CreateTestScoreInfo();
|
|
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
|
|
|
|
loadPanel(score);
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreWithoutStatistics()
|
|
{
|
|
loadPanel(TestResources.CreateTestScoreInfo());
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreInRulesetWhereAllStatsRequireHitEvents()
|
|
{
|
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetAllStatsRequireHitEvents().RulesetInfo));
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreInRulesetWhereNoStatsRequireHitEvents()
|
|
{
|
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetNoStatsRequireHitEvents().RulesetInfo));
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreInMixedRuleset()
|
|
{
|
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetMixed().RulesetInfo));
|
|
}
|
|
|
|
[Test]
|
|
public void TestNullScore()
|
|
{
|
|
loadPanel(null);
|
|
}
|
|
|
|
[Test]
|
|
public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad()
|
|
{
|
|
UserStatisticsWatcher userStatisticsWatcher = null!;
|
|
ScoreInfo score = null!;
|
|
|
|
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider())));
|
|
AddStep("set user statistics update", () =>
|
|
{
|
|
score = TestResources.CreateTestScoreInfo();
|
|
score.OnlineID = 1234;
|
|
((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
|
|
new UserStatistics
|
|
{
|
|
Level = new UserStatistics.LevelInfo
|
|
{
|
|
Current = 5,
|
|
Progress = 20,
|
|
},
|
|
GlobalRank = 38000,
|
|
CountryRank = 12006,
|
|
PP = 2134,
|
|
RankedScore = 21123849,
|
|
Accuracy = 0.985,
|
|
PlayCount = 13375,
|
|
PlayTime = 354490,
|
|
TotalScore = 128749597,
|
|
TotalHits = 0,
|
|
MaxCombo = 1233,
|
|
}, new UserStatistics
|
|
{
|
|
Level = new UserStatistics.LevelInfo
|
|
{
|
|
Current = 5,
|
|
Progress = 30,
|
|
},
|
|
GlobalRank = 36000,
|
|
CountryRank = 12000,
|
|
PP = (decimal)2134.5,
|
|
RankedScore = 23897015,
|
|
Accuracy = 0.984,
|
|
PlayCount = 13376,
|
|
PlayTime = 35789,
|
|
TotalScore = 132218497,
|
|
TotalHits = 0,
|
|
MaxCombo = 1233,
|
|
});
|
|
});
|
|
AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer
|
|
{
|
|
CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)],
|
|
RelativeSizeAxes = Axes.Both,
|
|
Child = new StatisticsPanel
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
State = { Value = Visibility.Visible },
|
|
Score = { Value = score, },
|
|
AchievedScore = score,
|
|
}
|
|
});
|
|
AddUntilStep("overall ranking present", () => this.ChildrenOfType<OverallRanking>().Any());
|
|
AddUntilStep("loading spinner not visible",
|
|
() => this.ChildrenOfType<OverallRanking>().Single()
|
|
.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
|
|
}
|
|
|
|
[Test]
|
|
public void TestTagging()
|
|
{
|
|
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 =>
|
|
{
|
|
switch (request)
|
|
{
|
|
case ListTagsRequest listTagsRequest:
|
|
{
|
|
Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection
|
|
{
|
|
Tags =
|
|
[
|
|
new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
|
|
new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", },
|
|
new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", },
|
|
new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
|
|
]
|
|
}), 500);
|
|
return true;
|
|
}
|
|
|
|
case GetBeatmapSetRequest getBeatmapSetRequest:
|
|
{
|
|
var beatmapSet = CreateAPIBeatmapSet(beatmap.Invoke());
|
|
beatmapSet.Beatmaps.Single().TopTags =
|
|
[
|
|
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
|
|
];
|
|
Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500);
|
|
return true;
|
|
}
|
|
|
|
case AddBeatmapTagRequest:
|
|
case RemoveBeatmapTagRequest:
|
|
{
|
|
Scheduler.AddDelayed(request.TriggerSuccess, 500);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
});
|
|
|
|
[Test]
|
|
public void TestTaggingWhenRankTooLow()
|
|
{
|
|
var score = TestResources.CreateTestScoreInfo();
|
|
score.Rank = ScoreRank.D;
|
|
|
|
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,
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TestTaggingConvert()
|
|
{
|
|
var score = TestResources.CreateTestScoreInfo();
|
|
score.Ruleset = new ManiaRuleset().RulesetInfo;
|
|
|
|
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,
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
[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 PopoverContainer
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Child = new StatisticsPanel
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
State = { Value = Visibility.Visible },
|
|
Score = { Value = score },
|
|
AchievedScore = score,
|
|
},
|
|
};
|
|
});
|
|
|
|
public static List<HitEvent> CreatePositionDistributedHitEvents()
|
|
{
|
|
var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
|
|
|
|
// Use constant seed for reproducibility
|
|
var random = new Random(0);
|
|
|
|
for (int i = 0; i < hitEvents.Count; i++)
|
|
{
|
|
double angle = random.NextDouble() * 2 * Math.PI;
|
|
double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS;
|
|
|
|
var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle)));
|
|
|
|
hitEvents[i] = hitEvents[i].With(position);
|
|
}
|
|
|
|
return hitEvents;
|
|
}
|
|
|
|
private class TestRuleset : Ruleset
|
|
{
|
|
public override IEnumerable<Mod> GetModsFor(ModType type)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap);
|
|
|
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public override string Description => string.Empty;
|
|
|
|
public override string ShortName => string.Empty;
|
|
|
|
protected static Drawable CreatePlaceholderStatistic(string message) => new Container
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
Masking = true,
|
|
CornerRadius = 20,
|
|
Height = 250,
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = OsuColour.Gray(0.5f),
|
|
Alpha = 0.5f
|
|
},
|
|
new OsuSpriteText
|
|
{
|
|
Origin = Anchor.CentreLeft,
|
|
Anchor = Anchor.CentreLeft,
|
|
Text = message,
|
|
Margin = new MarginPadding { Left = 20 }
|
|
}
|
|
}
|
|
};
|
|
|
|
private class TestBeatmapConverter : IBeatmapConverter
|
|
{
|
|
#pragma warning disable CS0067 // The event is never used
|
|
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
|
#pragma warning restore CS0067
|
|
|
|
public IBeatmap Beatmap { get; }
|
|
|
|
// ReSharper disable once NotNullOrRequiredMemberIsNotInitialized
|
|
public TestBeatmapConverter(IBeatmap beatmap)
|
|
{
|
|
Beatmap = beatmap;
|
|
}
|
|
|
|
public bool CanConvert() => true;
|
|
|
|
public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap.Clone();
|
|
}
|
|
}
|
|
|
|
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
|
|
{
|
|
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
|
{
|
|
new StatisticItem("Statistic Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
|
|
new StatisticItem("Statistic Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
|
|
};
|
|
}
|
|
|
|
private class TestRulesetNoStatsRequireHitEvents : TestRuleset
|
|
{
|
|
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
|
{
|
|
return new[]
|
|
{
|
|
new StatisticItem("Statistic Not Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")),
|
|
new StatisticItem("Statistic Not Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
|
|
};
|
|
}
|
|
}
|
|
|
|
private class TestRulesetMixed : TestRuleset
|
|
{
|
|
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
|
{
|
|
return new[]
|
|
{
|
|
new StatisticItem("Statistic Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
|
|
new StatisticItem("Statistic Not Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|