1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 01:37:31 +08:00

Merge branch 'master' into remove_obsolete_star_colours

This commit is contained in:
Bartłomiej Dach 2022-12-26 16:35:47 +01:00 committed by GitHub
commit e416bdd06b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1473 additions and 447 deletions

View File

@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2019
image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
@ -83,4 +83,4 @@ artifacts:
deploy:
- provider: Environment
name: nuget
name: nuget

View File

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

View File

@ -1,193 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
Y = 160;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
{
Colour = colours.ForHitResult(Result),
});
}
}
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
/// <remarks>
/// The base implementation only handles fade (for all result types) and misses.
/// Individual rulesets are recommended to implement their appropriate hit animations.
/// </remarks>
public virtual void PlayAnimation()
{
switch (Result)
{
default:
JudgementText
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
}
this.FadeOutFromOne(800);
ringExplosion?.PlayAnimation();
}
public Drawable? GetAboveHitObjectsProxiedContent() => null;
private partial class RingExplosion : CompositeDrawable
{
private readonly float travel = 52;
public RingExplosion(HitResult result)
{
const float thickness = 4;
const float small_size = 9;
const float large_size = 14;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Blending = BlendingParameters.Additive;
int countSmall = 0;
int countLarge = 0;
switch (result)
{
case HitResult.Meh:
countSmall = 3;
travel *= 0.3f;
break;
case HitResult.Ok:
case HitResult.Good:
countSmall = 4;
travel *= 0.6f;
break;
case HitResult.Great:
case HitResult.Perfect:
countSmall = 4;
countLarge = 4;
break;
}
for (int i = 0; i < countSmall; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
for (int i = 0; i < countLarge; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
}
public void PlayAnimation()
{
foreach (var c in InternalChildren)
{
const float start_position_ratio = 0.3f;
float direction = RNG.NextSingle(0, 360);
float distance = RNG.NextSingle(travel / 2, travel);
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance * start_position_ratio,
MathF.Sin(direction) * distance * start_position_ratio
));
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance,
MathF.Sin(direction) * distance
), 600, Easing.OutQuint);
}
this.FadeOutFromOne(1000, Easing.OutQuint);
}
public partial class RingPiece : CircularContainer
{
public RingPiece(float thickness = 9)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Masking = true;
BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
};
}
}
}
}
}

View File

@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
if (random == null) throw new ArgumentNullException(nameof(random));
if (originalBeatmap == null) throw new ArgumentNullException(nameof(originalBeatmap));
ArgumentNullException.ThrowIfNull(random);
ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
OriginalBeatmap = originalBeatmap;

View File

@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
{
if (hitObject == null) throw new ArgumentNullException(nameof(hitObject));
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern));
ArgumentNullException.ThrowIfNull(hitObject);
ArgumentNullException.ThrowIfNull(beatmap);
ArgumentNullException.ThrowIfNull(previousPattern);
HitObject = hitObject;
Beatmap = beatmap;

View File

@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
public static void Sort(T[] keys, IComparer<T> comparer)
{
if (keys == null)
throw new ArgumentNullException(nameof(keys));
ArgumentNullException.ThrowIfNull(keys);
if (keys.Length == 0)
return;

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -18,20 +17,18 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Y = 160;
}
@ -39,22 +36,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -64,6 +45,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -27,6 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Mania.UI
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
{
if (stageDefinitions == null)
throw new ArgumentNullException(nameof(stageDefinitions));
ArgumentNullException.ThrowIfNull(stageDefinitions);
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");

View File

@ -160,9 +160,9 @@ namespace osu.Game.Rulesets.Osu.Tests
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private Drawable testSimpleBig(int repeats = 0) => createSlider(repeats: repeats);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(repeats: repeats, stackHeight: 10);
private Drawable testDistanceOverflow(int repeats = 0)
{

View File

@ -84,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Replays
{
public int Compare(ReplayFrame? f1, ReplayFrame? f2)
{
if (f1 == null) throw new ArgumentNullException(nameof(f1));
if (f2 == null) throw new ArgumentNullException(nameof(f2));
ArgumentNullException.ThrowIfNull(f1);
ArgumentNullException.ThrowIfNull(f2);
return f1.Time.CompareTo(f2.Time);
}

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -17,42 +16,24 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -62,6 +43,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponentLookup osuComponent:

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -18,20 +17,16 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
}
@ -39,21 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
RelativePositionAxes = Axes.Both,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -64,6 +44,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
RelativePositionAxes = Axes.Both,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
switch (component)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case TaikoSkinComponentLookup taikoComponent:

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
private ZoomableScrollContainer scrollContainer;
private TestZoomableScrollContainer scrollContainer;
private Drawable innerBox;
[SetUpSteps]
@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
scrollContainer = new ZoomableScrollContainer(1, 60, 1)
scrollContainer = new TestZoomableScrollContainer(1, 60, 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -93,6 +93,14 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]
public void TestWidthUpdatesOnSecondZoomSetup()
{
AddAssert("Inner container width = 1x", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
AddStep("reload zoom", () => scrollContainer.SetupZoom(10, 10, 60));
AddAssert("Inner container width = 10x", () => innerBox.DrawWidth == scrollContainer.DrawWidth * 10);
}
[Test]
public void TestZoom0()
{
@ -190,5 +198,15 @@ namespace osu.Game.Tests.Visual.Editing
private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad;
private Quad boxQuad => innerBox.ScreenSpaceDrawQuad;
private partial class TestZoomableScrollContainer : ZoomableScrollContainer
{
public TestZoomableScrollContainer(int minimum, float maximum, float initial)
: base(minimum, maximum, initial)
{
}
public new void SetupZoom(float initial, float minimum, float maximum) => base.SetupZoom(initial, minimum, maximum);
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTestAPI(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset
createPlayerTest(createRuleset: () => new OsuRuleset
{
RulesetInfo =
{

View File

@ -0,0 +1,280 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
[HeadlessTest]
public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
{
protected override bool UseOnlineAPI => false;
private SoloStatisticsWatcher watcher = null!;
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private Action<GetUsersRequest>? handleGetUsersRequest;
private Action<GetUserRequest>? handleGetUserRequest;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear server-side stats", () => serverSideStatistics.Clear());
AddStep("set up request handling", () =>
{
handleGetUserRequest = null;
handleGetUsersRequest = null;
dummyAPI.HandleRequest = request =>
{
switch (request)
{
case GetUsersRequest getUsersRequest:
if (handleGetUsersRequest != null)
{
handleGetUsersRequest?.Invoke(getUsersRequest);
}
else
{
int userId = getUsersRequest.UserIds.Single();
var response = new GetUsersResponse
{
Users = new List<APIUser>
{
new APIUser
{
Id = userId,
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
["osu"] = tryGetStatistics(userId, "osu"),
["taiko"] = tryGetStatistics(userId, "taiko"),
["fruits"] = tryGetStatistics(userId, "fruits"),
["mania"] = tryGetStatistics(userId, "mania"),
}
}
}
};
getUsersRequest.TriggerSuccess(response);
}
return true;
case GetUserRequest getUserRequest:
if (handleGetUserRequest != null)
{
handleGetUserRequest.Invoke(getUserRequest);
}
else
{
int userId = int.Parse(getUserRequest.Lookup);
string rulesetName = getUserRequest.Ruleset.ShortName;
var response = new APIUser
{
Id = userId,
Statistics = tryGetStatistics(userId, rulesetName)
};
getUserRequest.TriggerSuccess(response);
}
return true;
default:
return false;
}
};
});
AddStep("create watcher", () =>
{
Child = watcher = new SoloStatisticsWatcher();
});
}
private UserStatistics tryGetStatistics(int userId, string rulesetName)
=> serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
[Test]
public void TestStatisticsUpdateFiredAfterRegistrationAddedAndScoreProcessed()
{
int userId = getUserId();
long scoreId = getScoreId();
setUpUser(userId);
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
}
[Test]
public void TestStatisticsUpdateFiredAfterScoreProcessedAndRegistrationAdded()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
// note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000);
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
}
[Test]
public void TestStatisticsUpdateNotFiredIfUserLoggedOut()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("log out user", () => dummyAPI.Logout());
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
AddStep("log in user", () => dummyAPI.Login("user", "password"));
}
[Test]
public void TestStatisticsUpdateNotFiredIfAnotherUserLoggedIn()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("change user", () => dummyAPI.LocalUser.Value = new APIUser { Id = getUserId() });
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
[Test]
public void TestStatisticsUpdateNotFiredIfScoreIdDoesNotMatch()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal another score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, getScoreId()));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
// the behaviour exercised in this test may not be final, it is mostly assumed for simplicity.
// in the long run we may want each score's update to be entirely isolated from others, rather than have prior unobserved updates merge into the latest.
[Test]
public void TestIgnoredScoreUpdateIsMergedIntoNextOne()
{
int userId = getUserId();
setUpUser(userId);
long firstScoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, firstScoreId));
long secondScoreId = getScoreId();
feignScoreProcessing(userId, ruleset, 6_000_000);
SoloStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
}
private int nextUserId = 2000;
private long nextScoreId = 50000;
private int getUserId() => ++nextUserId;
private long getScoreId() => ++nextScoreId;
private void setUpUser(int userId)
{
AddStep("fetch initial stats", () =>
{
serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
dummyAPI.LocalUser.Value = new APIUser { Id = userId };
});
}
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => watcher.RegisterForStatisticsUpdateAfter(
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
{
Ruleset = rulesetInfo,
OnlineID = scoreId
},
onUpdateReady));
private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
=> AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
}
}

View File

@ -4,8 +4,11 @@
#nullable disable
using System;
using System.Linq;
using System.Net;
using NUnit.Framework;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -29,6 +32,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show main page", () => wiki.Show());
}
[Test]
public void TestCancellationDoesntShowError()
{
AddStep("Show main page", () => wiki.Show());
AddStep("Show another page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
AddUntilStep("Current path is not error", () => wiki.CurrentPath != "error");
}
[Test]
public void TestArticlePage()
{
@ -56,7 +68,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestErrorPage()
{
setUpWikiResponse(responseArticlePage);
AddStep("Show Error Page", () => wiki.ShowPage("Error"));
AddStep("Show nonexistent page", () => wiki.ShowPage("This_page_will_error_out"));
AddUntilStep("Wait for error page", () => wiki.CurrentPath == "error");
AddUntilStep("Error message correct", () => wiki.ChildrenOfType<SpriteText>().Any(text => text.Text == "\"This_page_will_error_out\"."));
}
private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)

View File

@ -0,0 +1,117 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneOverallRanking : OsuTestScene
{
private OverallRanking overallRanking = null!;
[Test]
public void TestUpdatePending()
{
createDisplay();
}
[Test]
public void TestAllIncreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 0.9899,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
},
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 0.9907,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
});
}
[Test]
public void TestAllDecreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 0.9907,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
},
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 0.9899,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
});
}
[Test]
public void TestNoChanges()
{
var statistics = new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 0.9899,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
};
createDisplay();
displayUpdate(statistics, statistics);
}
[Test]
public void TestNotRanked()
{
var statistics = new UserStatistics
{
GlobalRank = null,
Accuracy = 0.9899,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = null
};
createDisplay();
displayUpdate(statistics, statistics);
}
private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
{
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components
public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null)
{
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
ArgumentNullException.ThrowIfNull(beatmap);
Beatmap = beatmap;
this.mod = mod;

View File

@ -211,8 +211,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : class, IControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
ArgumentNullException.ThrowIfNull(list);
if (list.Count == 0)
return null;

View File

@ -15,8 +15,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapBackgroundSprite(IWorkingBeatmap working)
{
if (working == null)
throw new ArgumentNullException(nameof(working));
ArgumentNullException.ThrowIfNull(working);
this.working = working;
}

View File

@ -18,8 +18,7 @@ namespace osu.Game.Beatmaps.Drawables
public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
{
if (set == null)
throw new ArgumentNullException(nameof(set));
ArgumentNullException.ThrowIfNull(set);
this.set = set;
this.type = type;

View File

@ -57,8 +57,7 @@ namespace osu.Game.Beatmaps.Formats
public static Decoder<T> GetDecoder<T>(LineBufferedReader stream)
where T : new()
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
ArgumentNullException.ThrowIfNull(stream);
if (!decoders.TryGetValue(typeof(T), out var typedDecoders))
throw new IOException(@"Unknown decoder type");

View File

@ -36,8 +36,7 @@ namespace osu.Game.Graphics.Containers
/// <param name="easing">The easing type of the initial transform.</param>
public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None)
{
if (logo == null)
throw new ArgumentNullException(nameof(logo));
ArgumentNullException.ThrowIfNull(logo);
if (logo.IsTracking && Logo == null)
throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s");

View File

@ -52,8 +52,8 @@ namespace osu.Game.Graphics.UserInterface
public readonly SpriteIcon Chevron;
//don't allow clicking between transitions and don't make the chevron clickable
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceivePositionalInputAt(screenSpacePos);
//don't allow clicking between transitions
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && base.ReceivePositionalInputAt(screenSpacePos);
public override bool HandleNonPositionalInput => State == Visibility.Visible;
public override bool HandlePositionalInput => State == Visibility.Visible;
@ -95,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface
{
Text.Font = Text.Font.With(size: 18);
Text.Margin = new MarginPadding { Vertical = 8 };
Padding = new MarginPadding { Right = padding + ChevronSize };
Margin = new MarginPadding { Right = padding + ChevronSize };
Add(Chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,

View File

@ -114,8 +114,7 @@ namespace osu.Game.Graphics.UserInterface
get => current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);

View File

@ -23,8 +23,7 @@ namespace osu.Game.IO.FileAbstraction
public void CloseStream(Stream stream)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
ArgumentNullException.ThrowIfNull(stream);
stream.Close();
}

View File

@ -64,6 +64,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
/// <summary>
/// "Learn more about lazer"
/// </summary>
public static LocalisableString LearnMoreAboutLazer => new TranslatableString(getKey(@"learn_more_about_lazer"), @"Learn more about lazer");
/// <summary>
/// "Check out the feature comparison and FAQ"
/// </summary>
public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
/// <summary>
/// "You are running the latest release ({0})"
/// </summary>

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest<GetUsersResponse>
{
private readonly int[] userIds;
public readonly int[] UserIds;
private const int max_ids_per_request = 50;
@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
this.userIds = userIds;
UserIds = userIds;
}
protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", userIds);
protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", UserIds);
}
}

View File

@ -118,8 +118,7 @@ namespace osu.Game.Online.Chat
/// <param name="name"></param>
public void OpenChannel(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ArgumentNullException.ThrowIfNull(name);
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
}
@ -130,8 +129,7 @@ namespace osu.Game.Online.Chat
/// <param name="user">The user the private channel is opened with.</param>
public void OpenPrivateChannel(APIUser user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
ArgumentNullException.ThrowIfNull(user);
if (user.Id == api.LocalUser.Value.Id)
return;

View File

@ -0,0 +1,42 @@
// 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.Scoring;
using osu.Game.Users;
namespace osu.Game.Online.Solo
{
/// <summary>
/// Contains data about the change in a user's profile statistics after completing a score.
/// </summary>
public class SoloStatisticsUpdate
{
/// <summary>
/// The score set by the user that triggered the update.
/// </summary>
public ScoreInfo Score { get; }
/// <summary>
/// The user's profile statistics prior to the score being set.
/// </summary>
public UserStatistics Before { get; }
/// <summary>
/// The user's profile statistics after the score was set.
/// </summary>
public UserStatistics After { get; }
/// <summary>
/// Creates a new <see cref="SoloStatisticsUpdate"/>.
/// </summary>
/// <param name="score">The score set by the user that triggered the update.</param>
/// <param name="before">The user's profile statistics prior to the score being set.</param>
/// <param name="after">The user's profile statistics after the score was set.</param>
public SoloStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
{
Score = score;
Before = before;
After = after;
}
}
}

View File

@ -0,0 +1,144 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online.Solo
{
/// <summary>
/// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
/// </summary>
public partial class SoloStatisticsWatcher : Component
{
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<long, StatisticsUpdateCallback> callbacks = new Dictionary<long, StatisticsUpdateCallback>();
private long? lastProcessedScoreId;
private Dictionary<string, UserStatistics>? latestStatistics;
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
spectatorClient.OnUserScoreProcessed += userScoreProcessed;
}
/// <summary>
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
/// </summary>
/// <param name="score">The score to listen for the statistics update for.</param>
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
public void RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady) => Schedule(() =>
{
if (!api.IsLoggedIn)
return;
if (!score.Ruleset.IsLegacyRuleset())
return;
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
if (lastProcessedScoreId == score.OnlineID)
{
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
return;
}
callbacks.Add(score.OnlineID, callback);
});
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
callbacks.Clear();
lastProcessedScoreId = null;
latestStatistics = null;
if (localUser == null || localUser.OnlineID <= 1)
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += response => Schedule(() =>
{
latestStatistics = new Dictionary<string, UserStatistics>();
foreach (var rulesetStats in response.Users.Single().RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
});
api.Queue(userRequest);
});
private void userScoreProcessed(int userId, long scoreId)
{
if (userId != api.LocalUser.Value?.OnlineID)
return;
lastProcessedScoreId = scoreId;
if (!callbacks.TryGetValue(scoreId, out var callback))
return;
requestStatisticsUpdate(userId, callback);
callbacks.Remove(scoreId);
}
private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback)
{
var request = new GetUserRequest(userId, callback.Score.Ruleset);
request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics));
api.Queue(request);
}
private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserStatistics updatedStatistics)
{
string rulesetName = callback.Score.Ruleset.ShortName;
if (latestStatistics == null)
return;
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
latestRulesetStatistics ??= new UserStatistics();
var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics);
callback.OnUpdateReady.Invoke(update);
latestStatistics[rulesetName] = updatedStatistics;
}
protected override void Dispose(bool isDisposing)
{
if (spectatorClient.IsNotNull())
spectatorClient.OnUserScoreProcessed -= userScoreProcessed;
base.Dispose(isDisposing);
}
private class StatisticsUpdateCallback
{
public ScoreInfo Score { get; }
public Action<SoloStatisticsUpdate> OnUpdateReady { get; }
public StatisticsUpdateCallback(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
{
Score = score;
OnUpdateReady = onUpdateReady;
}
}
}
}

View File

@ -353,7 +353,10 @@ namespace osu.Game
break;
case LinkAction.SearchBeatmapSet:
SearchBeatmapSet(argString);
if (link.Argument is RomanisableString romanisable)
SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
else
SearchBeatmapSet(argString);
break;
case LinkAction.OpenEditorTimestamp:
@ -1040,9 +1043,7 @@ namespace osu.Game
Logger.NewEntry += entry =>
{
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database) return;
Debug.Assert(entry.Target != null);
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const int short_term_display_limit = 3;

View File

@ -46,6 +46,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
@ -193,6 +194,7 @@ namespace osu.Game
protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
private SoloStatisticsWatcher soloStatisticsWatcher;
private RealmAccess realm;
@ -301,6 +303,7 @@ namespace osu.Game
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
@ -346,6 +349,7 @@ namespace osu.Game
AddInternal(spectatorClient);
AddInternal(MultiplayerClient);
AddInternal(metadataClient);
AddInternal(soloStatisticsWatcher);
AddInternal(rulesetConfigCache);

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -12,17 +13,20 @@ using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
@ -41,12 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box coverGradient;
private readonly OsuSpriteText title, artist;
private readonly LinkFlowContainer title, artist;
private readonly AuthorInfo author;
private readonly ExplicitContentBeatmapBadge explicitContent;
private readonly SpotlightBeatmapBadge spotlight;
private readonly FeaturedArtistBeatmapBadge featuredArtist;
private ExternalLinkButton externalLink;
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
@ -65,8 +67,6 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapSetHeaderContent()
{
ExternalLinkButton externalLink;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
@ -120,58 +120,19 @@ namespace osu.Game.Overlays.BeatmapSet
AutoSizeAxes = Axes.Y,
Child = Picker = new BeatmapPicker(),
},
new FillFlowContainer
title = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
})
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 15 },
Children = new Drawable[]
{
title = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
},
externalLink = new ExternalLinkButton
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
},
explicitContent = new ExplicitContentBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10, Bottom = 4 },
},
spotlight = new SpotlightBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10, Bottom = 4 },
}
}
},
new FillFlowContainer
artist = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
})
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = 20 },
Children = new Drawable[]
{
artist = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
},
featuredArtist = new FeaturedArtistBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10 }
}
}
},
new Container
{
@ -237,12 +198,17 @@ namespace osu.Game.Overlays.BeatmapSet
Picker.Beatmap.ValueChanged += b =>
{
Details.BeatmapInfo = b.NewValue;
externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}";
updateExternalLink();
onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None;
};
}
private void updateExternalLink()
{
if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}";
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@ -275,12 +241,38 @@ namespace osu.Game.Overlays.BeatmapSet
loading.Hide();
title.Text = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
artist.Text = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
explicitContent.Alpha = setInfo.NewValue.HasExplicitContent ? 1 : 0;
spotlight.Alpha = setInfo.NewValue.FeaturedInSpotlight ? 1 : 0;
featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0;
title.Clear();
artist.Clear();
title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText);
title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
if (setInfo.NewValue.HasExplicitContent)
{
title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
title.AddArbitraryDrawable(new ExplicitContentBeatmapBadge());
}
if (setInfo.NewValue.FeaturedInSpotlight)
{
title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
}
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText);
if (setInfo.NewValue.TrackId != null)
{
artist.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
artist.AddArbitraryDrawable(new FeaturedArtistBeatmapBadge());
}
updateExternalLink();
onlineStatusPill.FadeIn(500, Easing.OutQuint);
@ -327,5 +319,32 @@ namespace osu.Game.Overlays.BeatmapSet
break;
}
}
public partial class MetadataFlowContainer : LinkFlowContainer
{
public MetadataFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
TextAnchor = Anchor.CentreLeft;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
protected override DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new MetadataLinkCompiler(textPart);
public partial class MetadataLinkCompiler : DrawableLinkCompiler
{
public MetadataLinkCompiler(ITextPart part)
: base(part)
{
}
[BackgroundDependencyLoader]
private void load()
{
IdleColour = Color4.White;
}
}
}
}
}

View File

@ -95,8 +95,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersScore, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, minSize: 60, maxSize: 70)),
new TableColumn("", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 25)), // flag
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 125)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(minSize: 125)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(minSize: 70, maxSize: 120))
};
// All statistics across all scores, unordered.
@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var displayName = ruleset.GetDisplayNameForHitResult(result);
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
}

View File

@ -70,7 +70,7 @@ namespace osu.Game.Overlays
/// <see cref="APIChangelogBuild.DisplayVersion"/> are specified, the header will instantly display them.</param>
public void ShowBuild([NotNull] APIChangelogBuild build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
ArgumentNullException.ThrowIfNull(build);
Current.Value = build;
Show();
@ -78,8 +78,8 @@ namespace osu.Game.Overlays
public void ShowBuild([NotNull] string updateStream, [NotNull] string version)
{
if (updateStream == null) throw new ArgumentNullException(nameof(updateStream));
if (version == null) throw new ArgumentNullException(nameof(version));
ArgumentNullException.ThrowIfNull(updateStream);
ArgumentNullException.ThrowIfNull(version);
performAfterFetch(() =>
{

View File

@ -58,7 +58,7 @@ namespace osu.Game.Overlays
/// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is already being tracked from the same <paramref name="source"/>.</exception>
public void BeginTracking(object source, ITrackableConfigManager configManager)
{
if (configManager == null) throw new ArgumentNullException(nameof(configManager));
ArgumentNullException.ThrowIfNull(configManager);
if (trackedConfigManagers.ContainsKey((source, configManager)))
throw new InvalidOperationException($"{nameof(configManager)} is already registered.");
@ -82,7 +82,7 @@ namespace osu.Game.Overlays
/// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is not being tracked from the same <paramref name="source"/>.</exception>
public void StopTracking(object source, ITrackableConfigManager configManager)
{
if (configManager == null) throw new ArgumentNullException(nameof(configManager));
ArgumentNullException.ThrowIfNull(configManager);
if (!trackedConfigManagers.TryGetValue((source, configManager), out var existing))
return;

View File

@ -1,12 +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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.General;
@ -15,7 +14,10 @@ namespace osu.Game.Overlays.Settings.Sections
public partial class GeneralSection : SettingsSection
{
[Resolved(CanBeNull = true)]
private FirstRunSetupOverlay firstRunSetupOverlay { get; set; }
private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
[Resolved(CanBeNull = true)]
private OsuGame? game { get; set; }
public override LocalisableString Header => GeneralSettingsStrings.GeneralSectionHeader;
@ -24,15 +26,24 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.Cog
};
public GeneralSection()
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Children = new Drawable[]
{
new SettingsButton
{
Text = GeneralSettingsStrings.RunSetupWizard,
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(),
},
new SettingsButton
{
Text = GeneralSettingsStrings.LearnMoreAboutLazer,
TooltipText = GeneralSettingsStrings.LearnMoreAboutLazerTooltip,
BackgroundColour = colours.YellowDark,
Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer")
},
new LanguageSettings(),
new UpdateSettings(),
};

View File

@ -28,8 +28,7 @@ namespace osu.Game.Overlays.Volume
get => current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);

View File

@ -21,6 +21,8 @@ namespace osu.Game.Overlays
{
private const string index_path = @"main_page";
public string CurrentPath => path.Value;
private readonly Bindable<string> path = new Bindable<string>(index_path);
private readonly Bindable<APIWikiPage> wikiData = new Bindable<APIWikiPage>();
@ -105,6 +107,9 @@ namespace osu.Game.Overlays
if (e.NewValue == wikiData.Value?.Path)
return;
if (e.NewValue == "error")
return;
cancellationToken?.Cancel();
request?.Cancel();
@ -118,7 +123,11 @@ namespace osu.Game.Overlays
Loading.Show();
request.Success += response => Schedule(() => onSuccess(response));
request.Failure += _ => Schedule(onFail);
request.Failure += ex =>
{
if (ex is not OperationCanceledException)
Schedule(onFail, request.Path);
};
api.PerformAsync(request);
}
@ -146,10 +155,11 @@ namespace osu.Game.Overlays
}
}
private void onFail()
private void onFail(string originalPath)
{
path.Value = "error";
LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/",
$"Something went wrong when trying to fetch page \"{path.Value}\".\n\n[Return to the main page](Main_Page)."));
$"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page](Main_Page)."));
}
private void showParentPage()

View File

@ -1,12 +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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -15,40 +10,25 @@ using osuTK;
namespace osu.Game.Rulesets.Judgements
{
public partial class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class DefaultJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; }
[Resolved]
private OsuColour colours { get; set; }
public DefaultJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
: base(result)
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Font = OsuFont.Numeric.With(size: 20),
Scale = new Vector2(0.85f, 1),
}
};
Origin = Anchor.Centre;
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 20),
Scale = new Vector2(0.85f, 1),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
@ -75,6 +55,6 @@ namespace osu.Game.Rulesets.Judgements
this.FadeOutFromOne(800);
}
public Drawable GetAboveHitObjectsProxiedContent() => null;
public Drawable? GetAboveHitObjectsProxiedContent() => null;
}
}

View File

@ -0,0 +1,38 @@
// 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.Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Judgements
{
public abstract partial class JudgementPiece : CompositeDrawable
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
protected JudgementPiece(HitResult result)
{
Result = result;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(JudgementText = CreateJudgementText());
JudgementText.Colour = colours.ForHitResult(Result);
JudgementText.Text = Result.GetDescription().ToUpperInvariant();
}
protected abstract SpriteText CreateJudgementText();
}
}

View File

@ -126,8 +126,7 @@ namespace osu.Game.Rulesets.Mods
get => this;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
if (currentBound != null) UnbindFrom(currentBound);
BindTo(currentBound = value);

View File

@ -208,8 +208,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
public void Apply([NotNull] HitObject hitObject)
{
if (hitObject == null)
throw new ArgumentNullException($"Cannot apply a null {nameof(HitObject)}.");
ArgumentNullException.ThrowIfNull(hitObject);
Apply(new SyntheticHitObjectEntry(hitObject));
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.UI
{
public override void Add(T judgement)
{
if (judgement == null) throw new ArgumentNullException(nameof(judgement));
ArgumentNullException.ThrowIfNull(judgement);
// remove any existing judgements for the judged object.
// this can be the case when rewinding.

View File

@ -71,8 +71,8 @@ namespace osu.Game.Scoring
// These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed).
// Under no circumstance do we want these to be written to realm as null.
if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
ArgumentNullException.ThrowIfNull(model.BeatmapInfo);
ArgumentNullException.ThrowIfNull(model.Ruleset);
PopulateMaximumStatistics(model);

View File

@ -99,9 +99,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
isZoomSetUp = true;
CurrentZoom = zoomTarget = initial;
zoomedContentWidthCache.Invalidate();
isZoomSetUp = true;
zoomedContent.Show();
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Screens.Menu
private void addAmplitudesFromSource(IHasAmplitudes source)
{
if (source == null) throw new ArgumentNullException(nameof(source));
ArgumentNullException.ThrowIfNull(source);
var amplitudes = source.CurrentAmplitudes.FrequencyAmplitudes.Span;

View File

@ -33,8 +33,7 @@ namespace osu.Game.Screens.Play.HUD
get => current.Current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
current.Current = value;
}

View File

@ -30,8 +30,7 @@ namespace osu.Game.Screens.Play.HUD
get => current.Current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
current.Current = value;
}

View File

@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play
public override void Add(KeyCounter key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
ArgumentNullException.ThrowIfNull(key);
base.Add(key);
key.IsCounting = IsCounting;

View File

@ -96,11 +96,11 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
statisticsPanel = new StatisticsPanel
statisticsPanel = CreateStatisticsPanel().With(panel =>
{
RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore }
},
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
}),
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
@ -231,6 +231,11 @@ namespace osu.Game.Screens.Ranking
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
/// <summary>
/// Creates the <see cref="StatisticsPanel"/> to be used to display extended information about scores.
/// </summary>
protected virtual StatisticsPanel CreateStatisticsPanel() => new StatisticsPanel();
private void fetchScoresCallback(IEnumerable<ScoreInfo> scores) => Schedule(() =>
{
foreach (var s in scores)

View File

@ -7,11 +7,14 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Screens.Ranking
{
@ -22,11 +25,28 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private SoloStatisticsWatcher soloStatisticsWatcher { get; set; }
private readonly Bindable<SoloStatisticsUpdate> statisticsUpdate = new Bindable<SoloStatisticsUpdate>();
public SoloResultsScreen(ScoreInfo score, bool allowRetry)
: base(score, allowRetry)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
soloStatisticsWatcher.RegisterForStatisticsUpdateAfter(Score, update => statisticsUpdate.Value = update);
}
protected override StatisticsPanel CreateStatisticsPanel() => new SoloStatisticsPanel(Score)
{
StatisticsUpdate = { BindTarget = statisticsUpdate }
};
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)

View File

@ -0,0 +1,54 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
namespace osu.Game.Screens.Ranking.Statistics
{
public partial class SoloStatisticsPanel : StatisticsPanel
{
private readonly ScoreInfo achievedScore;
public SoloStatisticsPanel(ScoreInfo achievedScore)
{
this.achievedScore = achievedScore;
}
public Bindable<SoloStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
protected override ICollection<StatisticRow> CreateStatisticRows(ScoreInfo newScore, IBeatmap playableBeatmap)
{
var rows = base.CreateStatisticRows(newScore, playableBeatmap);
if (newScore.UserID > 1
&& newScore.UserID == achievedScore.UserID
&& newScore.OnlineID > 0
&& newScore.OnlineID == achievedScore.OnlineID)
{
rows = rows.Append(new StatisticRow
{
Columns = new[]
{
new StatisticItem("Overall Ranking", () => new OverallRanking
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
StatisticsUpdate = { BindTarget = StatisticsUpdate }
})
}
}).ToArray();
}
return rows;
}
}
}

View File

@ -100,7 +100,7 @@ namespace osu.Game.Screens.Ranking.Statistics
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
Container<Drawable> container;
var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, task.GetResultSafely());
var statisticRows = CreateStatisticRows(newScore, task.GetResultSafely());
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
{
@ -218,6 +218,14 @@ namespace osu.Game.Screens.Ranking.Statistics
}), localCancellationSource.Token);
}
/// <summary>
/// Creates the <see cref="StatisticRow"/>s to be displayed in this panel for a given <paramref name="newScore"/>.
/// </summary>
/// <param name="newScore">The score to create the rows for.</param>
/// <param name="playableBeatmap">The beatmap on which the score was set.</param>
protected virtual ICollection<StatisticRow> CreateStatisticRows(ScoreInfo newScore, IBeatmap playableBeatmap)
=> newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap);
protected override bool OnClick(ClickEvent e)
{
ToggleVisibility();

View File

@ -0,0 +1,35 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class AccuracyChangeRow : RankingChangeRow<double>
{
public AccuracyChangeRow()
: base(stats => stats.Accuracy)
{
}
protected override LocalisableString Label => UsersStrings.ShowStatsHitAccuracy;
protected override LocalisableString FormatCurrentValue(double current) => current.FormatAccuracy();
protected override int CalculateDifference(double previous, double current, out LocalisableString formattedDifference)
{
double difference = current - previous;
if (difference < 0)
formattedDifference = difference.FormatAccuracy();
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($@"+{difference.FormatAccuracy()}");
else
formattedDifference = string.Empty;
return current.CompareTo(previous);
}
}
}

View File

@ -0,0 +1,58 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class GlobalRankChangeRow : RankingChangeRow<int?>
{
public GlobalRankChangeRow()
: base(stats => stats.GlobalRank)
{
}
protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple;
protected override LocalisableString FormatCurrentValue(int? current)
=> current == null ? string.Empty : current.Value.FormatRank();
protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference)
{
if (previous == null && current == null)
{
formattedDifference = string.Empty;
return 0;
}
if (previous == null && current != null)
{
formattedDifference = LocalisableString.Interpolate($"+{current.Value.FormatRank()}");
return 1;
}
if (previous != null && current == null)
{
formattedDifference = LocalisableString.Interpolate($"-{previous.Value.FormatRank()}");
return -1;
}
Debug.Assert(previous != null && current != null);
// note that ranks work backwards, i.e. lower rank is _better_.
int difference = previous.Value - current.Value;
if (difference < 0)
formattedDifference = difference.FormatRank();
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($"+{difference.FormatRank()}");
else
formattedDifference = string.Empty;
return difference;
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class MaximumComboChangeRow : RankingChangeRow<int>
{
public MaximumComboChangeRow()
: base(stats => stats.MaxCombo)
{
}
protected override LocalisableString Label => UsersStrings.ShowStatsMaximumCombo;
protected override LocalisableString FormatCurrentValue(int current) => LocalisableString.Interpolate($@"{current:N0}x");
protected override int CalculateDifference(int previous, int current, out LocalisableString formattedDifference)
{
int difference = current - previous;
if (difference < 0)
formattedDifference = LocalisableString.Interpolate($@"{difference:N0}x");
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($@"+{difference:N0}x");
else
formattedDifference = string.Empty;
return current.CompareTo(previous);
}
}
}

View File

@ -0,0 +1,78 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Solo;
using osuTK;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class OverallRanking : CompositeDrawable
{
private const float transition_duration = 300;
public Bindable<SoloStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
private LoadingLayer loadingLayer = null!;
private FillFlowContainer content = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Y;
AutoSizeEasing = Easing.OutQuint;
AutoSizeDuration = transition_duration;
InternalChildren = new Drawable[]
{
loadingLayer = new LoadingLayer(withBox: false)
{
RelativeSizeAxes = Axes.Both,
},
content = new FillFlowContainer
{
AlwaysPresent = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
StatisticsUpdate.BindValueChanged(onUpdateReceived, true);
FinishTransforms(true);
}
private void onUpdateReceived(ValueChangedEvent<SoloStatisticsUpdate?> update)
{
if (update.NewValue == null)
{
loadingLayer.Show();
content.FadeOut(transition_duration, Easing.OutQuint);
}
else
{
loadingLayer.Hide();
content.FadeIn(transition_duration, Easing.OutQuint);
}
}
}
}

View File

@ -0,0 +1,56 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class PerformancePointsChangeRow : RankingChangeRow<decimal?>
{
public PerformancePointsChangeRow()
: base(stats => stats.PP)
{
}
protected override LocalisableString Label => RankingsStrings.StatPerformance;
protected override LocalisableString FormatCurrentValue(decimal? current)
=> current == null ? string.Empty : LocalisableString.Interpolate($@"{current:N0}pp");
protected override int CalculateDifference(decimal? previous, decimal? current, out LocalisableString formattedDifference)
{
if (previous == null && current == null)
{
formattedDifference = string.Empty;
return 0;
}
if (previous == null && current != null)
{
formattedDifference = LocalisableString.Interpolate($"+{current.Value:N0}pp");
return 1;
}
if (previous != null && current == null)
{
formattedDifference = LocalisableString.Interpolate($"-{previous.Value:N0}pp");
return -1;
}
Debug.Assert(previous != null && current != null);
decimal difference = current.Value - previous.Value;
if (difference < 0)
formattedDifference = LocalisableString.Interpolate($@"{difference:N0}pp");
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($@"+{difference:N0}pp");
else
formattedDifference = string.Empty;
return current.Value.CompareTo(previous.Value);
}
}
}

View File

@ -0,0 +1,35 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class RankedScoreChangeRow : RankingChangeRow<long>
{
public RankedScoreChangeRow()
: base(stats => stats.RankedScore)
{
}
protected override LocalisableString Label => UsersStrings.ShowStatsRankedScore;
protected override LocalisableString FormatCurrentValue(long current) => current.ToLocalisableString(@"N0");
protected override int CalculateDifference(long previous, long current, out LocalisableString formattedDifference)
{
long difference = current - previous;
if (difference < 0)
formattedDifference = difference.ToLocalisableString(@"N0");
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($@"+{difference:N0}");
else
formattedDifference = string.Empty;
return current.CompareTo(previous);
}
}
}

View File

@ -0,0 +1,144 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Solo;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public abstract partial class RankingChangeRow<T> : CompositeDrawable
{
public Bindable<SoloStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
private readonly Func<UserStatistics, T> accessor;
private OsuSpriteText currentValueText = null!;
private SpriteIcon changeIcon = null!;
private OsuSpriteText changeText = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
protected RankingChangeRow(
Func<UserStatistics, T> accessor)
{
this.accessor = accessor;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Text = Label,
Font = OsuFont.Default.With(size: 18)
},
new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Children = new Drawable[]
{
changeIcon = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(18)
},
currentValueText = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.Default.With(size: 18, weight: FontWeight.Bold)
},
}
},
changeText = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(weight: FontWeight.Bold)
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true);
}
private void onStatisticsUpdate(ValueChangedEvent<SoloStatisticsUpdate?> statisticsUpdate)
{
var update = statisticsUpdate.NewValue;
if (update == null)
return;
T previousValue = accessor.Invoke(update.Before);
T currentValue = accessor.Invoke(update.After);
int comparisonResult = CalculateDifference(previousValue, currentValue, out var formattedDifference);
Colour4 comparisonColour;
IconUsage icon;
if (comparisonResult < 0)
{
comparisonColour = colours.Red1;
icon = FontAwesome.Solid.ArrowDown;
}
else if (comparisonResult > 0)
{
comparisonColour = colours.Lime1;
icon = FontAwesome.Solid.ArrowUp;
}
else
{
comparisonColour = colours.Orange1;
icon = FontAwesome.Solid.Minus;
}
currentValueText.Text = FormatCurrentValue(currentValue);
changeIcon.Icon = icon;
changeIcon.Colour = comparisonColour;
changeText.Text = formattedDifference;
changeText.Colour = comparisonColour;
}
protected abstract LocalisableString Label { get; }
protected abstract LocalisableString FormatCurrentValue(T current);
protected abstract int CalculateDifference(T previous, T current, out LocalisableString formattedDifference);
}
}

View File

@ -0,0 +1,35 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Ranking.Statistics.User
{
public partial class TotalScoreChangeRow : RankingChangeRow<long>
{
public TotalScoreChangeRow()
: base(stats => stats.TotalScore)
{
}
protected override LocalisableString Label => UsersStrings.ShowStatsTotalScore;
protected override LocalisableString FormatCurrentValue(long current) => current.ToLocalisableString(@"N0");
protected override int CalculateDifference(long previous, long current, out LocalisableString formattedDifference)
{
long difference = current - previous;
if (difference < 0)
formattedDifference = difference.ToLocalisableString(@"N0");
else if (difference > 0)
formattedDifference = LocalisableString.Interpolate($@"+{difference:N0}");
else
formattedDifference = string.Empty;
return current.CompareTo(previous);
}
}
}

View File

@ -27,8 +27,7 @@ namespace osu.Game.Users.Drawables
[BackgroundDependencyLoader]
private void load(TextureStore ts)
{
if (ts == null)
throw new ArgumentNullException(nameof(ts));
ArgumentNullException.ThrowIfNull(ts);
string textureName = countryCode == CountryCode.Unknown ? "__" : countryCode.ToString();
Texture = ts.Get($@"Flags/{textureName}") ?? ts.Get(@"Flags/__");

View File

@ -36,8 +36,7 @@ namespace osu.Game.Users
protected UserPanel(APIUser user)
: base(HoverSampleSet.Button)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
ArgumentNullException.ThrowIfNull(user);
User = user;
}

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.18.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1219.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1226.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1221.0" />
<PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,7 +16,7 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1219.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1226.0" />
<!-- Required since Veldrid references a library that depends on Microsoft.DotNet.PlatformAbstractions (2.0.3), which doesn't play nice with Realm. -->
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />

View File

@ -146,7 +146,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyCanBeMadeInitOnly_002EGlobal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyCanBeMadeInitOnly_002ELocal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PublicConstructorInAbstractClass/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArgumentDefaultValue/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArgumentDefaultValue/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArrayCreationExpression/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeParentheses/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeUsageProperty/@EntryIndexedValue">WARNING</s:String>