1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 18:47:27 +08:00

Merge branch 'master' into menu-star-fountains

This commit is contained in:
Bartłomiej Dach 2023-07-19 21:26:41 +02:00
commit f3a95d4c13
No known key found for this signature in database
73 changed files with 810 additions and 522 deletions

View File

@ -8,13 +8,9 @@
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.710.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.716.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>

View File

@ -54,9 +54,6 @@ namespace osu.Desktop
client.OnReady += onReady;
// safety measure for now, until we performance test / improve backoff for failed connections.
client.OnConnectionFailed += (_, _) => client.Deinitialize();
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);

View File

@ -26,7 +26,7 @@
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@ -218,5 +220,17 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
return new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
};
}
}
}

View File

@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
{
new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT);
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT);
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
private class JudgementOrderComparer : IComparer<ManiaHitObject>
private class JudgementOrderComparer : IComparer<HitObject>
{
public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer();
public int Compare(ManiaHitObject? x, ManiaHitObject? y)
public int Compare(HitObject? x, HitObject? y)
{
if (ReferenceEquals(x, y)) return 0;
if (ReferenceEquals(x, null)) return -1;
@ -52,7 +52,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
if (x is Note && y is not Note) return -1;
if (x is not Note && y is Note) return 1;
return x.Column.CompareTo(y.Column);
return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY
? maniaX.Column.CompareTo(maniaY.Column)
: 0;
}
}
}

View File

@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.Osu
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
new OsuSpriteText
{
Text = "Overshoot",
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Padding = new MarginPadding(3),
Origin = Anchor.BottomLeft,
Padding = new MarginPadding(2),
Rotation = -rotation,
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
},
new OsuSpriteText
{
Text = "Undershoot",
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding(3),
Origin = Anchor.TopRight,
Rotation = -rotation,
Padding = new MarginPadding(2),
RelativePositionAxes = Axes.Both,
Y = (inner_portion + line_extension) / 2,
},

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -10,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
() => Is.EqualTo(expectedResult));
}
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null)
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null, Mod[]? mods = null)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
SelectedMods.Value = mods ?? Array.Empty<Mod>();
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });

View File

@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitNoneStrongDrumRoll()
{
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
}, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; ++i)
{
AssertResult<DrumRollTick>(i, HitResult.IgnoreMiss);
AssertResult<DrumRollTick.StrongNestedHit>(i, HitResult.IgnoreMiss);
}
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitAllStrongDrumRollWithOneKey()
{

View File

@ -4,10 +4,14 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Tests.Judgements
{
@ -157,5 +161,58 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertJudgementCount(1);
AssertResult<Hit>(0, HitResult.Ok);
}
[Test]
public void TestStrongHitOneKeyWithHidden()
{
const double hit_time = 1000;
var beatmap = CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = hit_time,
IsStrong = true
});
var hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
}, beatmap, new Mod[] { new TaikoModHidden() });
AssertJudgementCount(2);
AssertResult<Hit>(0, HitResult.Ok);
AssertResult<Hit.StrongNestedHit>(0, HitResult.IgnoreMiss);
}
[Test]
public void TestStrongHitTwoKeysWithHidden()
{
const double hit_time = 1000;
var beatmap = CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = hit_time,
IsStrong = true
});
var hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre),
}, beatmap, new Mod[] { new TaikoModHidden() });
AssertJudgementCount(2);
AssertResult<Hit>(0, HitResult.Ok);
AssertResult<Hit.StrongNestedHit>(0, HitResult.LargeBonus);
}
}
}

View File

@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
}
/// <summary>
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
/// </summary>
[Test]
public void TestHitSwellThenHitHit()
{
const double swell_time = 1000;
const double hit_time = 1150;
Swell swell = new Swell
{
StartTime = swell_time,
Duration = 100,
RequiredHits = 1
};
Hit hit = new Hit
{
StartTime = hit_time
};
List<ReplayFrame> frames = new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(swell_time, TaikoAction.LeftRim),
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
};
PerformTest(frames, CreateBeatmap(swell, hit));
AssertJudgementCount(3);
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
AssertResult<Swell>(0, HitResult.LargeBonus);
AssertResult<Hit>(0, HitResult.Great);
}
[Test]
public void TestMissSwellThenHitHit()
{
const double swell_time = 1000;
const double hit_time = 1150;
Swell swell = new Swell
{
StartTime = swell_time,
Duration = 100,
RequiredHits = 1
};
Hit hit = new Hit
{
StartTime = hit_time
};
List<ReplayFrame> frames = new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
};
PerformTest(frames, CreateBeatmap(swell, hit));
AssertJudgementCount(3);
AssertResult<SwellTick>(0, HitResult.IgnoreMiss);
AssertResult<Swell>(0, HitResult.IgnoreMiss);
AssertResult<Hit>(0, HitResult.Great);
}
}
}

View File

@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
: hitObject.HitStateUpdateTime;
// extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged.
hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW;
}
break;

View File

@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override void OnKilled()
{
base.OnKilled();
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
}
}

View File

@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override void OnKilled()
{
base.OnKilled();
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@ -16,5 +17,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
: base(nestedHit)
{
}
public override void OnKilled()
{
base.OnKilled();
// usually, the strong nested hit isn't judged itself, it is judged by its parent object.
// however, in rare cases (see: drum rolls, hits with hidden active),
// it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object.
// this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing.
if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime())
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
}
}

View File

@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (Time.Current < HitObject.StartTime)
return false;
if (AllJudged)
return false;
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
// Ensure alternating centre and rim hits

View File

@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)

View File

@ -478,8 +478,8 @@ namespace osu.Game.Tests.Chat
Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12"
});
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent);
Assert.AreEqual(5, result.Links.Count);
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent);
Assert.AreEqual(4, result.Links.Count);
Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links");
Assert.That(f, Is.Not.Null);
@ -500,27 +500,22 @@ namespace osu.Game.Tests.Chat
Assert.That(f, Is.Not.Null);
Assert.AreEqual(78, f.Index);
Assert.AreEqual(18, f.Length);
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
Assert.That(f, Is.Not.Null);
Assert.AreEqual(101, f.Index);
Assert.AreEqual(3, f.Length);
}
[Test]
public void TestEmoji()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent);
Assert.AreEqual(result.Links.Count, 4);
Assert.AreEqual(result.Links[0].Index, 11);
Assert.AreEqual(result.Links[1].Index, 49);
Assert.AreEqual(result.Links[2].Index, 52);
Assert.AreEqual(result.Links[3].Index, 56);
Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12");
Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10");
Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00");
Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20");
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent);
Assert.AreEqual(result.Links.Count, 0);
}
[Test]
public void TestEmojiWithSuccessiveParens()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" });
Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent);
Assert.AreEqual(result.Links.Count, 0);
}
[Test]

View File

@ -722,6 +722,30 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null);
}
[Test]
public void TestForceExitWithOperationInProgress()
{
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
AddStep("start ongoing operation", () =>
{
Game.Notifications.Post(new ProgressNotification
{
Text = "Something is still running",
Progress = 0.5f,
State = ProgressNotificationState.Active,
});
});
AddStep("attempt exit", () =>
{
for (int i = 0; i < 2; ++i)
Game.ScreenStack.CurrentScreen.Exit();
});
AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
}
[Test]
public void TestExitGameFromSongSelect()
{

View File

@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
advanceToSongSelect();
openSkinEditor();
AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() });
AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() });
switchToGameplayScene();

View File

@ -89,6 +89,7 @@ namespace osu.Game.Tests.Visual.Online
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions)
{

View File

@ -69,6 +69,35 @@ namespace osu.Game.Tests.Visual.Ranking
}));
}
private int onlineScoreID = 1;
[TestCase(1, ScoreRank.X)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{
TestResultsScreen screen = null;
loadResultsScreen(() =>
{
var score = TestResources.CreateTestScoreInfo();
score.OnlineID = onlineScoreID++;
score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents();
score.Accuracy = accuracy;
score.Rank = rank;
return screen = createResultsScreen(score);
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
[Test]
public void TestResultsWithoutPlayer()
{
@ -82,34 +111,14 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both
};
stack.Push(screen = createResultsScreen());
var score = TestResources.CreateTestScoreInfo();
stack.Push(screen = createResultsScreen(score));
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
[TestCase(0.2, ScoreRank.D)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(1, ScoreRank.X)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{
TestResultsScreen screen = null;
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = accuracy;
score.Rank = rank;
loadResultsScreen(() => screen = createResultsScreen(score));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
[Test]
public void TestResultsForUnranked()
{
@ -328,13 +337,14 @@ namespace osu.Game.Tests.Visual.Ranking
}
}
private partial class TestResultsScreen : ResultsScreen
private partial class TestResultsScreen : SoloResultsScreen
{
public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score)
: base(score, true)
{
ShowUserStatistics = true;
}
protected override void LoadComplete()

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -30,19 +32,20 @@ namespace osu.Game.Tests.Visual.Ranking
public partial class TestSceneStatisticsPanel : OsuTestScene
{
[Test]
public void TestScoreWithTimeStatistics()
public void TestScoreWithPositionStatistics()
{
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
score.OnlineID = 1234;
score.HitEvents = CreatePositionDistributedHitEvents();
loadPanel(score);
}
[Test]
public void TestScoreWithPositionStatistics()
public void TestScoreWithTimeStatistics()
{
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = createPositionDistributedHitEvents();
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
loadPanel(score);
}
@ -79,28 +82,67 @@ namespace osu.Game.Tests.Visual.Ranking
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
Child = new SoloStatisticsPanel(score)
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score }
Score = { Value = score },
StatisticsUpdate =
{
Value = new SoloStatisticsUpdate(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,
})
}
};
});
private static List<HitEvent> createPositionDistributedHitEvents()
public static List<HitEvent> CreatePositionDistributedHitEvents()
{
var hitEvents = new List<HitEvent>();
var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
// Use constant seed for reproducibility
var random = new Random(0);
for (int i = 0; i < 500; i++)
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.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position));
hitEvents[i] = hitEvents[i].With(position);
}
return hitEvents;

View File

@ -195,16 +195,16 @@ namespace osu.Game.Tests.Visual.Settings
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RevertToDefaultButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
var resetButton = settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First();
var resetButton = settingsKeyBindingRow.ChildrenOfType<RevertToDefaultButton<bool>>().First();
resetButton.TriggerClick();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RevertToDefaultButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared",
() => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
@ -225,7 +225,7 @@ namespace osu.Game.Tests.Visual.Settings
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RevertToDefaultButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Settings
resetButton.TriggerClick();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RevertToDefaultButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared",
() => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));

View File

@ -1,65 +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.
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.Settings
{
public partial class TestSceneRestoreDefaultValueButton : OsuTestScene
{
[Resolved]
private OsuColour colours { get; set; }
private float scale = 1;
private readonly Bindable<float> current = new Bindable<float>
{
Default = default,
Value = 1,
};
[Test]
public void TestBasic()
{
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
AddStep("create button", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoam
},
restoreDefaultValueButton = new RestoreDefaultValueButton<float>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
Current = current,
}
}
});
AddSliderStep("set scale", 1, 4, 1, scale =>
{
this.scale = scale;
if (restoreDefaultValueButton != null)
restoreDefaultValueButton.Scale = new Vector2(scale);
});
AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
AddToggleStep("toggle disabled state", state => current.Disabled = state);
}
}
}

View File

@ -0,0 +1,53 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.Settings
{
public partial class TestSceneRevertToDefaultButton : ThemeComparisonTestScene
{
private float scale = 1;
private readonly Bindable<float> current = new Bindable<float>
{
Default = default,
Value = 1,
};
protected override Drawable CreateContent() => new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new RevertToDefaultButton<float>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
Current = current,
}
};
[Test]
public void TestStates()
{
AddStep("create content", () => CreateThemedContent(OverlayColourScheme.Purple));
AddSliderStep("set scale", 1, 4, 1, scale =>
{
this.scale = scale;
foreach (var revertToDefaultButton in this.ChildrenOfType<RevertToDefaultButton<float>>())
revertToDefaultButton.Parent!.Scale = new Vector2(scale);
});
AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
AddToggleStep("toggle disabled state", state => current.Disabled = state);
}
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Settings
public void TestRestoreDefaultValueButtonVisibility()
{
SettingsTextBox textBox = null;
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
RevertToDefaultButton<string> revertToDefaultButton = null;
AddStep("create settings item", () =>
{
@ -33,22 +33,22 @@ namespace osu.Game.Tests.Visual.Settings
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single());
AddStep("retrieve restore default button", () => revertToDefaultButton = textBox.ChildrenOfType<RevertToDefaultButton<string>>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default");
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0);
AddStep("restore default", () => textBox.Current.SetDefault());
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0);
}
[Test]
public void TestSetAndClearLabelText()
{
SettingsTextBox textBox = null;
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
RevertToDefaultButton<string> revertToDefaultButton = null;
OsuTextBox control = null;
AddStep("create settings item", () =>
@ -61,25 +61,25 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve components", () =>
{
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
revertToDefaultButton = textBox.ChildrenOfType<RevertToDefaultButton<string>>().Single();
control = textBox.ChildrenOfType<OsuTextBox>().Single();
});
AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default");
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set non-default value", () => revertToDefaultButton.Current.Value = "non-default");
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set label", () => textBox.LabelText = "label text");
AddAssert("default value button centre aligned to label size", () =>
{
var label = textBox.ChildrenOfType<OsuSpriteText>().Single(spriteText => spriteText.Text == "label text");
return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1);
return Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, label.DrawHeight, 1);
});
AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
}
/// <summary>
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Settings
{
BindableFloat current = null;
SettingsSlider<float> sliderBar = null;
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
RevertToDefaultButton<float> revertToDefaultButton = null;
AddStep("create settings item", () =>
{
@ -107,15 +107,15 @@ namespace osu.Game.Tests.Visual.Settings
};
});
AddUntilStep("wait for loaded", () => sliderBar.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single());
AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.ChildrenOfType<RevertToDefaultButton<float>>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddAssert("restore button hidden", () => revertToDefaultButton.Alpha == 0);
AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f);
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0);
AddStep("restore default", () => sliderBar.Current.SetDefault());
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0);
}
[Test]

View File

@ -9,8 +9,8 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -304,11 +304,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName);
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
AddStep("edit preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "something new");
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("commit changes to textbox", () => InputManager.Key(Key.Enter));
AddStep("attempt preset edit via select binding", () => InputManager.Key(Key.Enter));
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName);
}

View File

@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
@ -793,7 +793,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
.ChildrenOfType<RestoreDefaultValueButton<double>>().Single().TriggerClick());
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
}

View File

@ -431,8 +431,9 @@ namespace osu.Game.Beatmaps
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
beatmapInfo.ResetOnlineInfo();
using (var stream = new MemoryStream())
Realm.Write(r =>
{
using var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
@ -458,23 +459,20 @@ namespace osu.Game.Beatmaps
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;
setInfo.CopyChangesToRealm(liveBeatmapSet);
setInfo.CopyChangesToRealm(liveBeatmapSet);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
.UpdateLocalScores(r);
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
.UpdateLocalScores(r);
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
}
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
Debug.Assert(beatmapInfo.BeatmapSet != null);

View File

@ -26,6 +26,9 @@ namespace osu.Game.Database
if (score.IsLegacyScore)
return false;
if (score.TotalScoreVersion > 30000002)
return false;
// Recalculate the old-style standardised score to see if this was an old lazer score.
bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore;
// Some older scores don't have correct statistics populated, so let's give them benefit of doubt.

View File

@ -43,6 +43,9 @@ namespace osu.Game.Graphics
[Resolved]
private GameHost host { get; set; }
[Resolved]
private Clipboard clipboard { get; set; }
private Storage storage;
[Resolved]
@ -116,7 +119,7 @@ namespace osu.Game.Graphics
using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
{
host.GetClipboard()?.SetImage(image);
clipboard.SetImage(image);
(string filename, var stream) = getWritableStream();

View File

@ -27,6 +27,9 @@ namespace osu.Game.Graphics.UserInterface
[Resolved]
private GameHost host { get; set; } = null!;
[Resolved]
private Clipboard clipboard { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@ -92,8 +95,11 @@ namespace osu.Game.Graphics.UserInterface
private void copyUrl()
{
host.GetClipboard()?.SetText(Link);
onScreenDisplay?.Display(new CopyUrlToast());
if (Link != null)
{
clipboard.SetText(Link);
onScreenDisplay?.Display(new CopyUrlToast());
}
}
}
}

View File

@ -65,7 +65,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
return base.OnKeyDown(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;

View File

@ -18,6 +18,9 @@ namespace osu.Game.Online.Chat
[Resolved]
private GameHost host { get; set; } = null!;
[Resolved]
private Clipboard clipboard { get; set; } = null!;
[Resolved(CanBeNull = true)]
private IDialogOverlay? dialogOverlay { get; set; }
@ -32,7 +35,7 @@ namespace osu.Game.Online.Chat
public void OpenUrlExternally(string url, bool bypassWarning = false)
{
if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null)
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url)));
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url)));
else
host.OpenUrlExternally(url);
}

View File

@ -279,11 +279,8 @@ namespace osu.Game.Online.Chat
// handle channels
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);
string empty = "";
while (space-- > 0)
empty += "\0";
handleMatches(emoji_regex, empty, "{0}", result, startIndex);
// see: https://github.com/ppy/osu/pull/24190
result.Text = Regex.Replace(result.Text, emoji_regex.ToString(), "[emoji]");
return result;
}

View File

@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Comments
private IAPIProvider api { get; set; } = null!;
[Resolved]
private GameHost host { get; set; } = null!;
private Clipboard clipboard { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@ -444,7 +444,7 @@ namespace osu.Game.Overlays.Comments
private void copyUrl()
{
host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopyUrlToast());
}

View File

@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Dialog
{
// Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure
// they are ready to be pressed.
Schedule(() => Buttons.OfType<T>().First().TriggerClick());
Scheduler.AddOnce(() => Buttons.OfType<T>().FirstOrDefault()?.TriggerClick());
}
protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -8,10 +8,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -95,6 +97,18 @@ namespace osu.Game.Overlays.Mods
}, true);
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Select:
createButton.TriggerClick();
return true;
}
return base.OnPressed(e);
}
private void createPreset()
{
realm.Write(r => r.Add(new ModPreset

View File

@ -8,11 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -130,6 +132,25 @@ namespace osu.Game.Overlays.Mods
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Select:
saveButton.TriggerClick();
return true;
}
return base.OnPressed(e);
}
private void useCurrentMods()
{
saveableMods = selectedMods.Value.ToHashSet();
@ -150,13 +171,6 @@ namespace osu.Game.Overlays.Mods
return !saveableMods.SetEquals(selectedMods.Value);
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
}
private void save()
{
preset.PerformWrite(s =>

View File

@ -48,7 +48,8 @@ namespace osu.Game.Overlays.Mods
/// Contrary to <see cref="OsuGameBase.AvailableMods"/> and <see cref="globalAvailableMods"/>, the <see cref="Mod"/> instances
/// inside the <see cref="ModState"/> objects are owned solely by this <see cref="ModSelectOverlay"/> instance.
/// </remarks>
public Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> AvailableMods { get; } = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(new Dictionary<ModType, IReadOnlyList<ModState>>());
public Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> AvailableMods { get; } =
new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(new Dictionary<ModType, IReadOnlyList<ModState>>());
private Func<Mod, bool> isValidMod = _ => true;
@ -636,12 +637,9 @@ namespace osu.Game.Overlays.Mods
case GlobalAction.Select:
{
// Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
// Pressing select should select first filtered mod if a search is in progress.
if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);

View File

@ -1,28 +1,25 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays
{
public partial class RestoreDefaultValueButton<T> : OsuClickableContainer, IHasCurrentValue<T>
public partial class RevertToDefaultButton<T> : OsuClickableContainer, IHasCurrentValue<T>
{
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
@ -31,11 +28,20 @@ namespace osu.Game.Overlays
// this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber.
// using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation.
private Bindable<T> current;
private Bindable<T>? current;
private SpriteIcon icon = null!;
private Circle circle = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
public Bindable<T> Current
{
get => current;
get => current.AsNonNull();
set
{
current?.UnbindAll();
@ -50,43 +56,36 @@ namespace osu.Game.Overlays
}
}
[Resolved]
private OsuColour colours { get; set; }
private const float size = 4;
private CircularContainer circle = null!;
private Box background = null!;
public RestoreDefaultValueButton()
public RevertToDefaultButton()
: base(HoverSampleSet.Button)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
private void load()
{
// size intentionally much larger than actual drawn content, so that the button is easier to click.
Size = new Vector2(3 * size);
Size = new Vector2(14);
Add(circle = new CircularContainer
AddRange(new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(size),
Masking = true,
Child = background = new Box
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = colour.Lime1
},
icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Undo,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(8),
}
});
Alpha = 0f;
Action += () =>
{
if (!current.Disabled)
if (current?.Disabled == false)
current.SetDefault();
};
}
@ -120,28 +119,25 @@ namespace osu.Game.Overlays
if (current == null)
return;
Enabled.Value = !Current.Disabled;
Enabled.Value = !current.Disabled;
if (!Current.Disabled)
this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint);
if (IsHovered && Enabled.Value)
{
this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint);
background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint);
circle.TweenEdgeEffectTo(new EdgeEffectParameters
{
Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f),
Radius = IsHovered ? 8 : 4,
Type = EdgeEffectType.Glow
}, fade_duration, Easing.OutQuint);
icon.RotateTo(-40, 500, Easing.OutQuint);
icon.FadeColour(colourProvider?.Light1 ?? colours.YellowLight, 300, Easing.OutQuint);
circle.FadeColour(colourProvider?.Background2 ?? colours.Gray6, 300, Easing.OutQuint);
this.ScaleTo(1.2f, 300, Easing.OutQuint);
}
else
{
background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint);
circle.TweenEdgeEffectTo(new EdgeEffectParameters
{
Colour = colours.Lime3.Opacity(0.1f),
Radius = 2,
Type = EdgeEffectType.Glow
}, fade_duration, Easing.OutQuint);
icon.RotateTo(0, 100, Easing.OutQuint);
icon.FadeColour(colourProvider?.Colour0 ?? colours.Yellow, 100, Easing.OutQuint);
circle.FadeColour(colourProvider?.Background3 ?? colours.Gray3, 100, Easing.OutQuint);
this.ScaleTo(1f, 100, Easing.OutQuint);
}
}
}

View File

@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
RelativeSizeAxes = Axes.Y,
Width = SettingsPanel.CONTENT_MARGINS,
Child = new RestoreDefaultValueButton<bool>
Child = new RevertToDefaultButton<bool>
{
Current = isDefault,
Action = RestoreDefaults,

View File

@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings
// intentionally done before LoadComplete to avoid overhead.
if (ShowsDefaultIndicator)
{
defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton<T>
defaultValueIndicatorContainer.Add(new RevertToDefaultButton<T>
{
Current = controlWithCurrent.Current,
Anchor = Anchor.Centre,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Difficulty
{
/// <summary>
@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty
/// Performance of a perfect play for comparison.
/// </summary>
public PerformanceAttributes PerfectPerformance { get; set; }
/// <summary>
/// Create a new performance breakdown.
/// </summary>
/// <param name="performance">Actual gameplay performance.</param>
/// <param name="perfectPerformance">Performance of a perfect play for comparison.</param>
public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance)
{
Performance = performance;
PerfectPerformance = perfectPerformance;
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] };
return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes());
}
[ItemCanBeNull]

View File

@ -11,7 +11,7 @@ using osu.Game.Replays;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData
public abstract class ModAutoplay : Mod, ICreateReplayData
{
public override string Name => "Autoplay";
public override string Acronym => "AT";
@ -20,15 +20,11 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1;
public bool PerformFail() => false;
public bool RestartOnFail => false;
public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAdaptiveSpeed) };
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods
{
public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
{
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "You can't fail, no matter what.";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) };
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition) };
}
}

View File

@ -49,6 +49,13 @@ namespace osu.Game.Scoring.Legacy
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
// TotalScoreVersion gets initialised to LATEST_VERSION.
// In the case where the incoming score has either an osu!stable or old lazer version, we need
// to mark it with the correct version increment to trigger reprocessing to new standardised scoring.
//
// See StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised().
scoreInfo.TotalScoreVersion = version < 30000002 ? 30000001 : LegacyScoreEncoder.LATEST_VERSION;
string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose
public partial class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings
{
[Resolved]
private GameHost host { get; set; }
private Clipboard hostClipboard { get; set; } = null!;
[Resolved]
private EditorClock clock { get; set; }
@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Compose
// regardless of whether anything was even selected at all.
// UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory.
// note that this means that `getTimestamp()` must handle no-selection case, too.
host.GetClipboard()?.SetText(getTimestamp());
hostClipboard.SetText(getTimestamp());
if (CanCopy.Value)
clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize();

View File

@ -23,6 +23,7 @@ using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(canBeNull: true)]
protected OnlinePlayScreen ParentScreen { get; private set; }
@ -284,6 +288,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
protected virtual bool IsConnected => api.State.Value == APIState.Online;
public override bool OnBackButton()
{
if (Room.RoomID.Value == null)
@ -356,7 +362,12 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (ExitConfirmed)
return true;
if (dialogOverlay == null || Room.RoomID.Value != null || Room.Playlist.Count == 0)
if (!IsConnected)
return true;
bool hasUnsavedChanges = Room.RoomID.Value == null && Room.Playlist.Count > 0;
if (dialogOverlay == null || !hasUnsavedChanges)
return true;
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// To work around this, temporarily remove the room and trigger an immediate listing poll.
if (e.Last is MultiplayerMatchSubScreen match)
{
RoomManager.RemoveRoom(match.Room);
RoomManager?.RemoveRoom(match.Room);
ListingPollingComponent.PollImmediately();
}
}

View File

@ -75,6 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
handleRoomLost();
}
protected override bool IsConnected => base.IsConnected && client.IsConnected.Value;
protected override Drawable CreateMainContent() => new Container
{
RelativeSizeAxes = Axes.Both,
@ -250,13 +252,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override bool OnExiting(ScreenExitEvent e)
{
// the room may not be left immediately after a disconnection due to async flow,
// so checking the IsConnected status is also required.
if (client.Room == null || !client.IsConnected.Value)
{
// room has not been created yet; exit immediately.
// room has not been created yet or we're offline; exit immediately.
if (client.Room == null || !IsConnected)
return base.OnExiting(e);
}
if (!exitConfirmed && dialogOverlay != null)
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@ -15,8 +13,8 @@ namespace osu.Game.Screens.OnlinePlay
public virtual string ShortTitle => Title;
[Resolved(CanBeNull = true)]
protected IRoomManager RoomManager { get; private set; }
[Resolved]
protected IRoomManager? RoomManager { get; private set; }
protected OnlinePlaySubScreen()
{

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
@ -30,7 +31,7 @@ namespace osu.Game.Screens.Play
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure()
{
if (!replayIsFailedScore)
if (!replayIsFailedScore && !GameplayState.Mods.OfType<ModAutoplay>().Any())
return false;
return base.CheckModsAllowFailure();

View File

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
@ -113,94 +115,92 @@ namespace osu.Game.Screens.Ranking.Statistics
}
}
if (barDrawables != null)
{
for (int i = 0; i < barDrawables.Length; i++)
{
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
}
}
if (barDrawables == null)
createBarDrawables();
else
{
int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
for (int i = 0; i < barDrawables.Length; i++)
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
}
}
Container axisFlow;
private void createBarDrawables()
{
int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = bins.Select((_, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
const float axis_font_size = 12;
Container axisFlow;
InternalChild = new GridContainer
Padding = new MarginPadding { Horizontal = 5 };
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
Content = new[]
new Drawable[]
{
new Drawable[]
new GridContainer
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] { barDrawables }
}
},
new Drawable[]
{
axisFlow = new Container
{
RelativeSizeAxes = Axes.X,
Height = axis_font_size,
}
},
RelativeSizeAxes = Axes.Both,
Content = new[] { barDrawables }
}
},
RowDimensions = new[]
new Drawable[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
};
axisFlow = new Container
{
RelativeSizeAxes = Axes.X,
Height = StatisticItem.FONT_SIZE,
}
},
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
};
// Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
double maxValue = timing_distribution_bins * binSize;
double axisValueStep = maxValue / axis_points;
// Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
double maxValue = timing_distribution_bins * binSize;
double axisValueStep = maxValue / axis_points;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "0",
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold)
});
for (int i = 1; i <= axis_points; i++)
{
double axisValue = i * axisValueStep;
float position = (float)(axisValue / maxValue);
float alpha = 1f - position * 0.8f;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "0",
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
RelativePositionAxes = Axes.X,
X = -position / 2,
Alpha = alpha,
Text = axisValue.ToString("-0"),
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold)
});
for (int i = 1; i <= axis_points; i++)
axisFlow.Add(new OsuSpriteText
{
double axisValue = i * axisValueStep;
float position = (float)(axisValue / maxValue);
float alpha = 1f - position * 0.8f;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = -position / 2,
Alpha = alpha,
Text = axisValue.ToString("-0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = position / 2,
Alpha = alpha,
Text = axisValue.ToString("+0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = position / 2,
Alpha = alpha,
Text = axisValue.ToString("+0"),
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold)
});
}
}
@ -211,13 +211,16 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly bool isCentre;
private readonly float totalValue;
private float basalHeight;
private const float minimum_height = 0.02f;
private float offsetAdjustment;
private Circle[] boxOriginals = null!;
private Circle? boxAdjustment;
private float? lastDrawHeight;
[Resolved]
private OsuColour colours { get; set; } = null!;
@ -256,15 +259,17 @@ namespace osu.Game.Screens.Ranking.Statistics
else
{
// A bin with no value draws a grey dot instead.
Circle dot = new Circle
InternalChildren = boxOriginals = new[]
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4.Gray,
Height = 0,
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4.Gray,
Height = 0,
}
};
InternalChildren = boxOriginals = new[] { dot };
}
}
@ -272,31 +277,18 @@ namespace osu.Game.Screens.Ranking.Statistics
{
base.LoadComplete();
if (!values.Any())
return;
updateBasalHeight();
foreach (var boxOriginal in boxOriginals)
{
boxOriginal.Y = 0;
boxOriginal.Height = basalHeight;
}
float offsetValue = 0;
for (int i = 0; i < values.Count; i++)
{
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
offsetValue -= values[i].Value;
}
Scheduler.AddOnce(updateMetrics, true);
}
protected override void Update()
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
base.Update();
updateBasalHeight();
if (invalidation.HasFlagFast(Invalidation.DrawSize))
{
if (lastDrawHeight != null && lastDrawHeight != DrawHeight)
Scheduler.AddOnce(updateMetrics, false);
}
return base.OnInvalidate(invalidation, source);
}
public void UpdateOffset(float adjustment)
@ -321,45 +313,32 @@ namespace osu.Game.Screens.Ranking.Statistics
}
offsetAdjustment = adjustment;
drawAdjustmentBar();
Scheduler.AddOnce(updateMetrics, true);
}
private void updateBasalHeight()
{
float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1;
if (newBasalHeight == basalHeight)
return;
basalHeight = newBasalHeight;
foreach (var dot in boxOriginals)
dot.Height = basalHeight;
draw();
}
private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue;
private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0);
private void draw()
{
resizeBars();
if (boxAdjustment != null)
drawAdjustmentBar();
}
private void resizeBars()
private void updateMetrics(bool animate = true)
{
float offsetValue = 0;
for (int i = 0; i < values.Count; i++)
for (int i = 0; i < boxOriginals.Length; i++)
{
boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight;
boxOriginals[i].Height = heightForValue(values[i].Value);
offsetValue -= values[i].Value;
int value = i < values.Count ? values[i].Value : 0;
var box = boxOriginals[i];
box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint);
offsetValue -= value;
}
if (boxAdjustment != null)
drawAdjustmentBar();
if (!animate)
FinishTransforms(true);
lastDrawHeight = DrawHeight;
}
private void drawAdjustmentBar()
@ -369,6 +348,10 @@ namespace osu.Game.Screens.Ranking.Statistics
boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint);
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
}
private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue;
private float heightForValue(float value) => minimum_height + offsetForValue(value);
}
}
}

View File

@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE),
Text = "Achieved PP",
Colour = Color4Extensions.FromHex("#66FFCC")
},
@ -105,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18),
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE),
Colour = Color4Extensions.FromHex("#66FFCC")
}
},
@ -115,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE),
Text = "Maximum",
Colour = OsuColour.Gray(0.7f)
},
@ -123,7 +123,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE),
Colour = OsuColour.Gray(0.7f)
}
}
@ -208,7 +208,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: StatisticItem.FONT_SIZE),
Text = attribute.DisplayName,
Colour = Colour4.White
},
@ -233,7 +233,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold),
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: StatisticItem.FONT_SIZE),
Text = percentage.ToLocalisableString("0%"),
Colour = Colour4.White
}

View File

@ -44,13 +44,13 @@ namespace osu.Game.Screens.Ranking.Statistics
Text = Name,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14)
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE)
},
value = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold)
}
});
}

View File

@ -98,12 +98,11 @@ namespace osu.Game.Screens.Ranking.Statistics
Direction = FillDirection.Vertical
};
private partial class Spacer : CompositeDrawable
public partial class Spacer : CompositeDrawable
{
public Spacer()
{
RelativeSizeAxes = Axes.Both;
Padding = new MarginPadding { Vertical = 4 };
InternalChild = new CircularContainer
{

View File

@ -12,6 +12,11 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
public class StatisticItem
{
/// <summary>
/// The recommended font size to use in statistic items to make sure they match others.
/// </summary>
public const float FONT_SIZE = 13;
/// <summary>
/// The name of this item.
/// </summary>

View File

@ -3,6 +3,7 @@
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
@ -15,42 +16,53 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <summary>
/// Wraps a <see cref="StatisticItem"/> to add a header and suitable layout for use in <see cref="ResultsScreen"/>.
/// </summary>
internal partial class StatisticContainer : CompositeDrawable
internal partial class StatisticItemContainer : CompositeDrawable
{
/// <summary>
/// Creates a new <see cref="StatisticContainer"/>.
/// Creates a new <see cref="StatisticItemContainer"/>.
/// </summary>
/// <param name="item">The <see cref="StatisticItem"/> to display.</param>
public StatisticContainer(StatisticItem item)
public StatisticItemContainer(StatisticItem item)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new GridContainer
Padding = new MarginPadding(5);
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
Masking = true,
CornerRadius = 6,
Children = new Drawable[]
{
new[]
new Box
{
createHeader(item)
Colour = ColourInfo.GradientVertical(
OsuColour.Gray(0.25f),
OsuColour.Gray(0.18f)
),
Alpha = 0.95f,
RelativeSizeAxes = Axes.Both,
},
new Drawable[]
new Container
{
new Container
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(5),
Children = new[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 15 },
Child = item.CreateContent()
createHeader(item),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10) { Top = 30 },
Child = item.CreateContent()
}
}
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
}
};
}
@ -63,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics
return new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Height = 20,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
@ -81,7 +93,7 @@ namespace osu.Game.Screens.Ranking.Statistics
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = item.Name,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold),
}
}
};

View File

@ -124,20 +124,23 @@ namespace osu.Game.Screens.Ranking.Statistics
}
else
{
FillFlowContainer rows;
FillFlowContainer flow;
container = new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = false,
ScrollbarOverlapsContent = false,
Alpha = 0,
Children = new[]
{
rows = new FillFlowContainer
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(30, 15)
Spacing = new Vector2(30, 15),
Direction = FillDirection.Full,
}
}
};
@ -146,35 +149,22 @@ namespace osu.Game.Screens.Ranking.Statistics
foreach (var item in statisticItems)
{
var columnContent = new List<Drawable>();
if (!hitEventsAvailable && item.RequiresHitEvents)
{
anyRequiredHitEvents = true;
continue;
}
columnContent.Add(new StatisticContainer(item)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
rows.Add(new GridContainer
flow.Add(new StatisticItemContainer(item)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { columnContent.ToArray() },
ColumnDimensions = new[] { new Dimension() },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
});
}
if (anyRequiredHitEvents)
{
rows.Add(new FillFlowContainer
flow.Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,

View File

@ -7,7 +7,6 @@ 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
{
@ -18,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
public Bindable<SoloStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
private LoadingLayer loadingLayer = null!;
private FillFlowContainer content = null!;
private GridContainer content = null!;
[BackgroundDependencyLoader]
private void load()
@ -33,21 +32,47 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{
RelativeSizeAxes = Axes.Both,
},
content = new FillFlowContainer
content = new GridContainer
{
AlwaysPresent = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
ColumnDimensions = new[]
{
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 } }
new Dimension(),
new Dimension(GridSizeMode.Absolute, 30),
new Dimension(),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new SimpleStatisticTable.Spacer(),
new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
},
new Drawable[] { },
new Drawable[]
{
new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new SimpleStatisticTable.Spacer(),
new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
},
new Drawable[] { },
new Drawable[]
{
new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new SimpleStatisticTable.Spacer(),
new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
}
}
}
};

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
@ -46,14 +47,15 @@ namespace osu.Game.Screens.Ranking.Statistics.User
new OsuSpriteText
{
Text = Label,
Font = OsuFont.Default.With(size: 18)
Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE)
},
new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
AutoSizeAxes = Axes.X,
Height = StatisticItem.FONT_SIZE * 2,
Children = new Drawable[]
{
new FillFlowContainer
@ -65,17 +67,31 @@ namespace osu.Game.Screens.Ranking.Statistics.User
Spacing = new Vector2(5),
Children = new Drawable[]
{
changeIcon = new SpriteIcon
new Container
{
Size = new Vector2(14),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(18)
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray1
},
changeIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
},
}
},
currentValueText = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.Default.With(size: 18, weight: FontWeight.Bold)
Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold)
},
}
},
@ -83,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(weight: FontWeight.Bold)
Font = OsuFont.Default.With(size: StatisticItem.FONT_SIZE, weight: FontWeight.Bold)
}
}
}
@ -123,7 +139,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
}
else
{
comparisonColour = colours.Orange1;
comparisonColour = colours.Gray4;
icon = FontAwesome.Solid.Minus;
}

View File

@ -75,8 +75,7 @@ namespace osu.Game.Screens.Select.Carousel
if (changes?.HasCollectionChanges() == false)
return;
ScoreInfo? topScore = sender.Detach().OrderByTotalScore().FirstOrDefault();
ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks));
updateable.Rank = topScore?.Rank;
updateable.Alpha = topScore != null ? 1 : 0;
}

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
@ -47,7 +47,7 @@ namespace osu.Game.Updater
{
Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n"
+ "Check with your package manager / provider to bring osu! up-to-date!",
Icon = FontAwesome.Solid.Upload,
Icon = FontAwesome.Solid.Download,
});
return true;

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
@ -54,7 +54,7 @@ namespace osu.Game.Updater
{
Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n"
+ "Click here to download the new version, which can be installed over the top of your existing installation",
Icon = FontAwesome.Solid.Upload,
Icon = FontAwesome.Solid.Download,
Activated = () =>
{
host.OpenUrlExternally(getBestUrl(latest));

View File

@ -134,7 +134,7 @@ namespace osu.Game.Updater
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Upload,
Icon = FontAwesome.Solid.Download,
Size = new Vector2(34),
Colour = OsuColour.Gray(0.2f),
Depth = float.MaxValue,

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.710.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.716.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.710.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.716.0" />
</ItemGroup>
</Project>