diff --git a/Directory.Build.props b/Directory.Build.props
index 235feea8ce..3b6b985961 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -17,7 +17,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 52b728a115..a1c53ece03 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 95b96adab0..683e9fd5e8 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index d12403016d..b7a7fff18a 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 95b96adab0..683e9fd5e8 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 2c4577f239..fe3e08537e 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -98,7 +98,7 @@ namespace osu.Desktop
if (status.Value is UserStatusOnline && activity.Value != null)
{
- presence.State = truncate(activity.Value.Status);
+ presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited));
presence.Details = truncate(getDetails(activity.Value));
if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0)
@@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo;
}
@@ -183,9 +183,12 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty;
+ case UserActivity.WatchingReplay watching:
+ return watching.BeatmapInfo.ToString();
+
case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 1f4544098b..f1b9c92429 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -26,8 +26,8 @@
-
-
+
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index f47b069373..4719d54138 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 5a2e8e0bf0..01922b2a96 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index be51dc0e4c..027bf60a0c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index f5a5771386..fc0b4a9ed9 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
};
}
- private partial class ManiaScrollSlider : OsuSliderBar
+ private partial class ManiaScrollSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
index 05ba2b8f22..69eacda541 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs
@@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -34,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable? lightContainer;
private Drawable? light;
+ private LegacyNoteBodyStyle? bodyStyle;
public LegacyBodyPiece()
{
@@ -54,9 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
?? 1;
- float minimumColumnWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.MinimumColumnWidth)?.Value
- ?? 1;
-
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
// This animation is discarded and re-queried with the appropriate frame length afterwards.
var tmp = skin.GetAnimation(lightImage, true, false);
@@ -83,7 +84,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
};
}
- bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
+ bodyStyle = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.NoteBodyStyle))?.Value;
+
+ var wrapMode = bodyStyle == LegacyNoteBodyStyle.Stretch ? WrapMode.ClampToEdge : WrapMode.Repeat;
+
+ direction.BindTo(scrollingInfo.Direction);
+ isHitting.BindTo(holdNote.IsHitting);
+
+ bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
{
if (d == null)
return;
@@ -94,16 +102,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
d.Anchor = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
- d.FillMode = FillMode.Stretch;
- d.Height = minimumColumnWidth / d.DrawWidth * 1.6f; // constant matching stable.
// Todo: Wrap?
});
if (bodySprite != null)
InternalChild = bodySprite;
-
- direction.BindTo(scrollingInfo.Direction);
- isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
@@ -165,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null)
{
bodySprite.Origin = Anchor.BottomCentre;
- bodySprite.Scale = new Vector2(1, -1);
+ bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y) * -1);
}
if (light != null)
@@ -176,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null)
{
bodySprite.Origin = Anchor.TopCentre;
- bodySprite.Scale = Vector2.One;
+ bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y));
}
if (light != null)
@@ -207,6 +210,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
+
+ // here we go...
+ switch (bodyStyle)
+ {
+ case LegacyNoteBodyStyle.Stretch:
+ // this is how lazer works by default. nothing required.
+ break;
+
+ default:
+ // this is where things get fucked up.
+ // honestly there's three modes to handle here but they seem really pointless?
+ // let's wait to see if anyone actually uses them in skins.
+ if (bodySprite != null)
+ {
+ var sprite = bodySprite as Sprite ?? bodySprite.ChildrenOfType().Single();
+
+ bodySprite.FillMode = FillMode.Stretch;
+ // i dunno this looks about right??
+ bodySprite.Scale = new Vector2(1, 32800 / sprite.DrawHeight);
+ }
+
+ break;
+ }
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index c10c3ffb15..57900bffd7 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,10 +1,10 @@
-
-
+
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index 796f5721bb..781a686700 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")]
[TestCase("slider-generating-drumroll-2")]
+ [TestCase("file-hitsamples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 6af1beff69..0c39ad988b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index 2ccdfd40e5..d0361b1c8d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -2,13 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset
+ public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject
{
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
@@ -18,5 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
}
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
+ {
+ if (drawable is DrawableTaikoHitObject hit)
+ hit.SnapJudgementLocation = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index ff4edf35fa..62c8457c58 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -207,6 +207,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float gravity_time = 300;
const float gravity_travel_height = 200;
+ if (SnapJudgementLocation)
+ MainPiece.MoveToX(-X);
+
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 6172b75d2c..f695c505a4 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent;
+ ///
+ /// Whether the location of the hit should be snapped to the hit target before animating.
+ ///
+ ///
+ /// This is how osu-stable worked, but notably is not how TnT works.
+ /// Not snapping results in less visual feedback on hit accuracy.
+ ///
+ public bool SnapJudgementLocation { get; set; }
+
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject)
{
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json
new file mode 100644
index 0000000000..70348a3871
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"StartTime":500.0,"Objects":[{"StartTime":500.0,"EndTime":500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1000.0,"Objects":[{"StartTime":1000.0,"EndTime":1000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1500.0,"Objects":[{"StartTime":1500.0,"EndTime":1500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2000.0,"Objects":[{"StartTime":2000.0,"EndTime":2000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2500.0,"Objects":[{"StartTime":2500.0,"EndTime":2500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3000.0,"Objects":[{"StartTime":3000.0,"EndTime":3000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3500.0,"Objects":[{"StartTime":3500.0,"EndTime":3500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":4000.0,"Objects":[{"StartTime":4000.0,"EndTime":4000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu
new file mode 100644
index 0000000000..5d4bcb52a1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu
@@ -0,0 +1,22 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:7
+OverallDifficulty:6.5
+ApproachRate:10
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+500,500,4,2,1,50,1,0
+
+[HitObjects]
+256,192,500,1,0,0:0:0:0:sample.ogg
+256,192,1000,1,8,0:0:0:0:sample.ogg
+256,192,1500,1,2,0:0:0:0:sample.ogg
+256,192,2000,1,10,0:0:0:0:sample.ogg
+256,192,2500,1,4,0:0:0:0:sample.ogg
+256,192,3000,1,12,0:0:0:0:sample.ogg
+256,192,3500,1,6,0:0:0:0:sample.ogg
+256,192,4000,1,14,0:0:0:0:sample.ogg
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 9944c0c6b7..4f435e73b3 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -209,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great,
HitResult.Ok,
- HitResult.SmallTickHit,
-
HitResult.SmallBonus,
+ HitResult.LargeBonus,
};
}
@@ -220,6 +219,9 @@ namespace osu.Game.Rulesets.Taiko
switch (result)
{
case HitResult.SmallBonus:
+ return "drum tick";
+
+ case HitResult.LargeBonus:
return "bonus";
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index d9e80fa111..281ea4e4ff 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -214,7 +214,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
StoryboardSprite manyTimes = background.Elements.OfType().Single(s => s.Path == "many-times.png");
- Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration));
+ // It is intentional that we don't consider the loop count (40) as part of the end time calculation to match stable's handling.
+ // If we were to include the loop count, storyboards which loop for stupid long loop counts would continue playing the outro forever.
+ Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
}
}
}
diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs
similarity index 77%
rename from osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
rename to osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs
index e1accd5b5f..80237fe6c8 100644
--- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editing/BeatmapEditorChangeHandlerTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -12,7 +10,7 @@ using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
- public class EditorChangeHandlerTest
+ public class BeatmapEditorChangeHandlerTest
{
private int stateChangedFired;
@@ -23,18 +21,23 @@ namespace osu.Game.Tests.Editing
}
[Test]
- public void TestSaveRestoreState()
+ public void TestSaveRestoreStateUsingTransaction()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
- addArbitraryChange(beatmap);
- handler.SaveState();
+ handler.BeginChange();
+ // Initial state will be saved on BeginChange
Assert.That(stateChangedFired, Is.EqualTo(1));
+ addArbitraryChange(beatmap);
+ handler.EndChange();
+
+ Assert.That(stateChangedFired, Is.EqualTo(2));
+
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
@@ -43,7 +46,35 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
+ Assert.That(stateChangedFired, Is.EqualTo(3));
+ }
+
+ [Test]
+ public void TestSaveRestoreState()
+ {
+ var (handler, beatmap) = createChangeHandler();
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.False);
+
+ // Save initial state
+ handler.SaveState();
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
+ addArbitraryChange(beatmap);
+ handler.SaveState();
+
Assert.That(stateChangedFired, Is.EqualTo(2));
+
+ Assert.That(handler.CanUndo.Value, Is.True);
+ Assert.That(handler.CanRedo.Value, Is.False);
+
+ handler.RestoreState(-1);
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.True);
+
+ Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
@@ -54,6 +85,10 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
+ // Save initial state
+ handler.SaveState();
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap);
@@ -61,7 +96,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
- Assert.That(stateChangedFired, Is.EqualTo(1));
+ Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash;
@@ -69,7 +104,7 @@ namespace osu.Game.Tests.Editing
handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
- Assert.That(stateChangedFired, Is.EqualTo(2));
+ Assert.That(stateChangedFired, Is.EqualTo(3));
addArbitraryChange(beatmap);
handler.SaveState();
@@ -84,12 +119,16 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
+ // Save initial state
+ handler.SaveState();
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
- Assert.That(stateChangedFired, Is.EqualTo(1));
+ Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash;
@@ -97,7 +136,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
- Assert.That(stateChangedFired, Is.EqualTo(1));
+ Assert.That(stateChangedFired, Is.EqualTo(2));
handler.RestoreState(-1);
@@ -106,7 +145,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
- Assert.That(stateChangedFired, Is.EqualTo(2));
+ Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
@@ -114,11 +153,15 @@ namespace osu.Game.Tests.Editing
{
var (handler, beatmap) = createChangeHandler();
+ // Save initial state
+ handler.SaveState();
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
- Assert.That(stateChangedFired, Is.EqualTo(i));
+ Assert.That(stateChangedFired, Is.EqualTo(i + 1));
addArbitraryChange(beatmap);
handler.SaveState();
@@ -169,7 +212,7 @@ namespace osu.Game.Tests.Editing
},
});
- var changeHandler = new EditorChangeHandler(beatmap);
+ var changeHandler = new BeatmapEditorChangeHandler(beatmap);
changeHandler.OnStateChange += () => stateChangedFired++;
return (changeHandler, beatmap);
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index cc72731493..d96c19a13e 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Audio
private WaveformTestBeatmap beatmap;
- private OsuSliderBar lowPassSlider;
- private OsuSliderBar highPassSlider;
+ private RoundedSliderBar lowPassSlider;
+ private RoundedSliderBar highPassSlider;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- lowPassSlider = new OsuSliderBar
+ lowPassSlider = new RoundedSliderBar
{
Width = 500,
Height = 50,
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- highPassSlider = new OsuSliderBar
+ highPassSlider = new RoundedSliderBar
{
Width = 500,
Height = 50,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs
new file mode 100644
index 0000000000..ce93837925
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Screens.Play.Break;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public partial class TestSceneLetterboxOverlay : OsuTestScene
+ {
+ public TestSceneLetterboxOverlay()
+ {
+ AddRange(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new LetterboxOverlay()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
index 334d01f915..3e415af86e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
@@ -1,32 +1,64 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
{
- protected TestReplayPlayer Player;
-
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
- AddStep("Load player", () => LoadScreen(Player));
- AddUntilStep("player loaded", () => Player.IsLoaded);
- }
+ protected TestReplayPlayer Player = null!;
[Test]
public void TestPauseViaSpace()
{
+ loadPlayerWithBeatmap();
+
+ double? lastTime = null;
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+
+ AddStep("Pause playback with space", () => InputManager.Key(Key.Space));
+
+ AddAssert("player not exited", () => Player.IsCurrentScreen());
+
+ AddUntilStep("Time stopped progressing", () =>
+ {
+ double current = Player.GameplayClockContainer.CurrentTime;
+ bool changed = lastTime != current;
+ lastTime = current;
+
+ return !changed;
+ });
+
+ AddWaitStep("wait some", 10);
+
+ AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
+ }
+
+ [Test]
+ public void TestPauseViaSpaceWithSkip()
+ {
+ loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo = { AudioLeadIn = 60000 }
+ });
+
+ AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType().First().IsButtonVisible);
+
+ AddStep("Skip with space", () => InputManager.Key(Key.Space));
+
+ AddAssert("Player not paused", () => !Player.DrawableRuleset.IsPaused.Value);
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -52,6 +84,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPauseViaMiddleMouse()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -77,6 +111,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekBackwards()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -93,6 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekForwards()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -106,12 +144,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
- protected TestReplayPlayer CreatePlayer(Ruleset ruleset)
+ private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{
- Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
+ AddStep("create player", () =>
+ {
+ CreatePlayer(new OsuRuleset(), beatmap);
+ });
+
+ AddStep("Load player", () => LoadScreen(Player));
+ AddUntilStep("player loaded", () => Player.IsLoaded);
+ }
+
+ protected void CreatePlayer(Ruleset ruleset, IBeatmap? beatmap = null)
+ {
+ Beatmap.Value = beatmap != null
+ ? CreateWorkingBeatmap(beatmap)
+ : CreateWorkingBeatmap(ruleset.RulesetInfo);
+
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
- return new TestReplayPlayer(false);
+ Player = new TestReplayPlayer(false);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneGroupBadges.cs b/osu.Game.Tests/Visual/Online/TestSceneGroupBadges.cs
new file mode 100644
index 0000000000..cbf9c33b78
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneGroupBadges.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Profile.Header.Components;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public partial class TestSceneGroupBadges : OsuTestScene
+ {
+ public TestSceneGroupBadges()
+ {
+ var groups = new[]
+ {
+ new APIUser(),
+ new APIUser
+ {
+ Groups = new[]
+ {
+ new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
+ }
+ },
+ new APIUser
+ {
+ Groups = new[]
+ {
+ new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
+ }
+ },
+ new APIUser
+ {
+ Groups = new[]
+ {
+ new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
+ new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
+ }
+ },
+ new APIUser
+ {
+ Groups = new[]
+ {
+ new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
+ new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
+ new APIUserGroup { Colour = "#999999", ShortName = "ALM", Name = "osu! Alumni" },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko" }, IsProbationary = true }
+ }
+ }
+ };
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.DarkGray
+ },
+ new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(40),
+ Children = new[]
+ {
+ new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(5),
+ ChildrenEnumerable = groups.Select(g => new GroupBadgeFlow { User = { Value = g } })
+ },
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
index 4675410164..10c2b2b9e1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestEditActivity()
{
- AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
+ AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index 4c1df850b2..a047e2f0c5 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK;
@@ -107,14 +109,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set online status", () => status.Value = new UserStatusOnline());
AddStep("idle", () => activity.Value = null);
- AddStep("spectating", () => activity.Value = new UserActivity.Spectating());
+ AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
+ AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
- AddStep("editing", () => activity.Value = new UserActivity.Editing(null));
- AddStep("modding", () => activity.Value = new UserActivity.Modding());
+ AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null));
+ AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null));
+ AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null));
}
[Test]
@@ -132,6 +136,14 @@ namespace osu.Game.Tests.Visual.Online
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId));
+ private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
+ {
+ User = new APIUser
+ {
+ Username = name,
+ }
+ };
+
private partial class TestUserListPanel : UserListPanel
{
public TestUserListPanel(APIUser user)
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 9aaa616c04..a97c8aff66 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -90,7 +90,9 @@ namespace osu.Game.Tests.Visual.Online
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
- new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko", "fruits", "mania" } },
+ new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko", "fruits", "mania" }, IsProbationary = true }
},
ProfileOrder = new[]
{
@@ -119,6 +121,12 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
},
+ TournamentBanner = new TournamentBanner
+ {
+ Id = 13926,
+ TournamentId = 35,
+ ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
+ },
Badges = new[]
{
new Badge
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 0145a1dfef..bf18bd3e51 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -24,17 +24,26 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneAccuracyCircle : OsuTestScene
{
- [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 TestRank(double accuracy, ScoreRank rank)
+ [TestCase(0)]
+ [TestCase(0.2)]
+ [TestCase(0.5)]
+ [TestCase(0.6999)]
+ [TestCase(0.7)]
+ [TestCase(0.75)]
+ [TestCase(0.7999)]
+ [TestCase(0.8)]
+ [TestCase(0.85)]
+ [TestCase(0.8999)]
+ [TestCase(0.9)]
+ [TestCase(0.925)]
+ [TestCase(0.9499)]
+ [TestCase(0.95)]
+ [TestCase(0.975)]
+ [TestCase(0.9999)]
+ [TestCase(1)]
+ public void TestRank(double accuracy)
{
- var score = createScore(accuracy, rank);
+ var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy));
addCircleStep(score);
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs
new file mode 100644
index 0000000000..72adbfc104
--- /dev/null
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Screens.Select.FooterV2;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.SongSelect
+{
+ public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
+ {
+ private FooterButtonRandomV2 randomButton = null!;
+ private FooterButtonModsV2 modsButton = null!;
+
+ private bool nextRandomCalled;
+ private bool previousRandomCalled;
+
+ private DummyOverlay overlay = null!;
+
+ [Cached]
+ private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ nextRandomCalled = false;
+ previousRandomCalled = false;
+
+ FooterV2 footer;
+
+ Children = new Drawable[]
+ {
+ footer = new FooterV2
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ },
+ overlay = new DummyOverlay()
+ };
+
+ footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
+ footer.AddButton(randomButton = new FooterButtonRandomV2
+ {
+ NextRandom = () => nextRandomCalled = true,
+ PreviousRandom = () => previousRandomCalled = true
+ });
+ footer.AddButton(new FooterButtonOptionsV2());
+
+ overlay.Hide();
+ });
+
+ [Test]
+ public void TestState()
+ {
+ AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state);
+ }
+
+ [Test]
+ public void TestFooterRandom()
+ {
+ AddStep("press F2", () => InputManager.Key(Key.F2));
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRandomViaMouse()
+ {
+ AddStep("click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewind()
+ {
+ AddStep("press Shift+F2", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.PressKey(Key.F2);
+ InputManager.ReleaseKey(Key.F2);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaShiftMouseLeft()
+ {
+ AddStep("shift + click button", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaMouseRight()
+ {
+ AddStep("right click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Right);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestOverlayPresent()
+ {
+ AddStep("Press F1", () =>
+ {
+ InputManager.MoveMouseTo(modsButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
+ AddStep("Hide", () => overlay.Hide());
+ }
+
+ private partial class DummyOverlay : ShearedOverlayContainer
+ {
+ public DummyOverlay()
+ : base(OverlayColourScheme.Green)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Header.Title = "An overlay";
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
index f45f5b9f59..307f436f84 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep($"Set {name} slider to {value}", () =>
this.ChildrenOfType().First(c => c.LabelText == name)
- .ChildrenOfType>().First().Current.Value = value);
+ .ChildrenOfType>().First().Current.Value = value);
}
private void checkBindableAtValue(string name, float? expectedValue)
@@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddAssert($"Slider {name} at {expectedValue}", () =>
this.ChildrenOfType().First(c => c.LabelText == name)
- .ChildrenOfType>().First().Current.Value == expectedValue);
+ .ChildrenOfType>().First().Current.Value == expectedValue);
}
private void setBeatmapWithDifficultyParameters(float value)
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index eff320a575..5ccaebd721 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -270,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
- AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
+ AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs
new file mode 100644
index 0000000000..766f22d867
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . 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.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public partial class TestSceneShearedSliderBar : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private readonly BindableDouble current = new BindableDouble(5)
+ {
+ Precision = 0.1f,
+ MinValue = 0,
+ MaxValue = 15
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = new ShearedSliderBar
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = current,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.4f
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 24969414d0..59a786a11d 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -2,11 +2,11 @@
-
+
-
-
+
+
WinExe
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 9f2a088a4b..5847079161 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -4,9 +4,9 @@
osu.Game.Tournament.Tests.TournamentTestRunner
-
+
-
+
WinExe
diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs
index 2c63c16274..d625566ee7 100644
--- a/osu.Game/Audio/PreviewTrack.cs
+++ b/osu.Game/Audio/PreviewTrack.cs
@@ -98,6 +98,9 @@ namespace osu.Game.Audio
Track.Stop();
+ // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
+ Track.Seek(0);
+
Stopped?.Invoke();
}
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index 65b9e46764..375960305c 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -66,10 +66,16 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
- if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
- continue;
+ var bindable = ((IBindable)property.GetValue(component)!);
- skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue);
+ if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
+ {
+ // TODO: We probably want to restore default if not included in serialisation information.
+ // This is not simple to do as SetDefault() is only found in the typed Bindable interface right now.
+ continue;
+ }
+
+ skinnable.CopyAdjustedSetting(bindable, settingValue);
}
}
diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
index cbe327bac7..9f21512825 100644
--- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
+++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
@@ -100,9 +100,9 @@ namespace osu.Game.Graphics.Containers
///
/// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key).
///
- protected void AbortConfirm()
+ protected virtual void AbortConfirm()
{
- if (!AllowMultipleFires && Fired) return;
+ if (!confirming || (!AllowMultipleFires && Fired)) return;
confirming = false;
Fired = false;
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 6fe1de2216..fceee90d06 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -46,8 +46,8 @@ namespace osu.Game.Graphics.Containers
AddRangeInternal(new Drawable[]
{
+ CreateHoverSounds(sampleSet),
content,
- CreateHoverSounds(sampleSet)
});
}
diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs
index 9669fe89a5..5bc17303d8 100644
--- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs
+++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
///
public partial class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue
where T : struct, IEquatable, IComparable, IConvertible
- where TSlider : OsuSliderBar, new()
+ where TSlider : RoundedSliderBar, new()
{
private readonly OsuSpriteText label;
private readonly TSlider slider;
@@ -130,7 +130,7 @@ namespace osu.Game.Graphics.UserInterface
///
/// An implementation for the UI slider bar control.
///
- public partial class ExpandableSlider : ExpandableSlider>
+ public partial class ExpandableSlider : ExpandableSlider>
where T : struct, IEquatable, IComparable, IConvertible
{
}
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 7921dcf593..28a2eb40c3 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -1,10 +1,7 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
-using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@@ -58,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader(true)]
- private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 6d6a591673..0c36d73085 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -1,195 +1,59 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Globalization;
-using JetBrains.Annotations;
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Utils;
-using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface
{
- public partial class OsuSliderBar : SliderBar, IHasTooltip, IHasAccentColour
+ public abstract partial class OsuSliderBar : SliderBar, IHasTooltip
where T : struct, IEquatable, IComparable, IConvertible
{
- ///
- /// Maximum number of decimal digits to be displayed in the tooltip.
- ///
- private const int max_decimal_digits = 5;
-
- private Sample sample;
- private double lastSampleTime;
- private T lastSampleValue;
-
- protected readonly Nub Nub;
- protected readonly Box LeftBox;
- protected readonly Box RightBox;
- private readonly Container nubContainer;
-
- public virtual LocalisableString TooltipText { get; private set; }
-
public bool PlaySamplesOnAdjust { get; set; } = true;
- private readonly HoverClickSounds hoverClickSounds;
-
///
/// Whether to format the tooltip as a percentage or the actual value.
///
public bool DisplayAsPercentage { get; set; }
- private Color4 accentColour;
+ public virtual LocalisableString TooltipText { get; private set; }
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- accentColour = value;
- LeftBox.Colour = value;
- }
- }
+ ///
+ /// Maximum number of decimal digits to be displayed in the tooltip.
+ ///
+ private const int max_decimal_digits = 5;
- private Colour4 backgroundColour;
+ private Sample sample = null!;
- public Color4 BackgroundColour
- {
- get => backgroundColour;
- set
- {
- backgroundColour = value;
- RightBox.Colour = value;
- }
- }
+ private double lastSampleTime;
+ private T lastSampleValue;
- public OsuSliderBar()
- {
- Height = Nub.HEIGHT;
- RangePadding = Nub.EXPANDED_SIZE / 2;
- Children = new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Padding = new MarginPadding { Horizontal = 2 },
- Child = new CircularContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Masking = true,
- CornerRadius = 5f,
- Children = new Drawable[]
- {
- LeftBox = new Box
- {
- Height = 5,
- EdgeSmoothness = new Vector2(0, 0.5f),
- RelativeSizeAxes = Axes.None,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- RightBox = new Box
- {
- Height = 5,
- EdgeSmoothness = new Vector2(0, 0.5f),
- RelativeSizeAxes = Axes.None,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- },
- },
- },
- },
- nubContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Child = Nub = new Nub
- {
- Origin = Anchor.TopCentre,
- RelativePositionAxes = Axes.X,
- Current = { Value = true }
- },
- },
- hoverClickSounds = new HoverClickSounds()
- };
- }
-
- [BackgroundDependencyLoader(true)]
- private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
{
sample = audio.Samples.Get(@"UI/notch-tick");
- AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
- BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
- }
-
- protected override void Update()
- {
- base.Update();
-
- nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
-
- Current.BindDisabledChanged(disabled =>
- {
- Alpha = disabled ? 0.3f : 1;
- hoverClickSounds.Enabled.Value = !disabled;
- }, true);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- updateGlow();
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- updateGlow();
- base.OnHoverLost(e);
- }
-
- protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
- => Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- updateGlow();
- base.OnDragEnd(e);
- }
-
- private void updateGlow()
- {
- Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
}
protected override void OnUserChange(T value)
{
base.OnUserChange(value);
+
playSample(value);
+
TooltipText = getTooltipText(value);
}
@@ -236,18 +100,6 @@ namespace osu.Game.Graphics.UserInterface
return floatValue.ToString($"N{significantDigits}");
}
- protected override void UpdateAfterChildren()
- {
- base.UpdateAfterChildren();
- LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
- RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
- }
-
- protected override void UpdateValue(float value)
- {
- Nub.MoveToX(value, 250, Easing.OutQuint);
- }
-
///
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
///
diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs
index 4e23b06c2b..f83dff6295 100644
--- a/osu.Game/Graphics/UserInterface/RangeSlider.cs
+++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Graphics.UserInterface
&& screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X;
}
- protected partial class BoundSlider : OsuSliderBar
+ protected partial class BoundSlider : RoundedSliderBar
{
public string? DefaultString;
public LocalisableString? DefaultTooltip;
diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs
new file mode 100644
index 0000000000..a666b83c05
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs
@@ -0,0 +1,170 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osuTK;
+using osuTK.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Overlays;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public partial class RoundedSliderBar : OsuSliderBar
+ where T : struct, IEquatable, IComparable, IConvertible
+ {
+ protected readonly Nub Nub;
+ protected readonly Box LeftBox;
+ protected readonly Box RightBox;
+ private readonly Container nubContainer;
+
+ private readonly HoverClickSounds hoverClickSounds;
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+ LeftBox.Colour = value;
+ }
+ }
+
+ private Colour4 backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ get => backgroundColour;
+ set
+ {
+ backgroundColour = value;
+ RightBox.Colour = value;
+ }
+ }
+
+ public RoundedSliderBar()
+ {
+ Height = Nub.HEIGHT;
+ RangePadding = Nub.EXPANDED_SIZE / 2;
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Padding = new MarginPadding { Horizontal = 2 },
+ Child = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Masking = true,
+ CornerRadius = 5f,
+ Children = new Drawable[]
+ {
+ LeftBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ RightBox = new Box
+ {
+ Height = 5,
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.None,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ },
+ },
+ },
+ },
+ nubContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = Nub = new Nub
+ {
+ Origin = Anchor.TopCentre,
+ RelativePositionAxes = Axes.X,
+ Current = { Value = true }
+ },
+ },
+ hoverClickSounds = new HoverClickSounds()
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
+ {
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindDisabledChanged(disabled =>
+ {
+ Alpha = disabled ? 0.3f : 1;
+ hoverClickSounds.Enabled.Value = !disabled;
+ }, true);
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateGlow();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateGlow();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
+ => Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ updateGlow();
+ base.OnDragEnd(e);
+ }
+
+ private void updateGlow()
+ {
+ Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
+ }
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+ LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
+ RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
+ }
+
+ protected override void UpdateValue(float value)
+ {
+ Nub.MoveToX(value, 250, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs
new file mode 100644
index 0000000000..3a09fd7445
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs
@@ -0,0 +1,183 @@
+// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public partial class ShearedNub : Container, IHasCurrentValue, IHasAccentColour
+ {
+ protected const float BORDER_WIDTH = 3;
+
+ public const int HEIGHT = 30;
+ public const float EXPANDED_SIZE = 50;
+
+ public static readonly Vector2 SHEAR = new Vector2(0.15f, 0);
+
+ private readonly Box fill;
+ private readonly Container main;
+
+ ///
+ /// Implements the shape for the nub, allowing for any type of container to be used.
+ ///
+ ///
+ public ShearedNub()
+ {
+ Size = new Vector2(EXPANDED_SIZE, HEIGHT);
+ InternalChild = main = new Container
+ {
+ Shear = SHEAR,
+ BorderColour = Colour4.White,
+ BorderThickness = BORDER_WIDTH,
+ Masking = true,
+ CornerRadius = 5,
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = fill = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
+ {
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.4f) ?? colours.PinkLighter;
+ GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
+
+ main.EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = GlowColour.Opacity(0),
+ Type = EdgeEffectType.Glow,
+ Radius = 8,
+ Roundness = 4,
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(onCurrentValueChanged, true);
+ }
+
+ private bool glowing;
+
+ public bool Glowing
+ {
+ get => glowing;
+ set
+ {
+ if (glowing == value)
+ return;
+
+ glowing = value;
+
+ if (value)
+ {
+ main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
+ .Then()
+ .FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
+
+ main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
+ .Then()
+ .FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
+ }
+ else
+ {
+ main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
+ main.FadeColour(AccentColour, 800, Easing.OutQuint);
+ }
+ }
+ }
+
+ private readonly Bindable current = new Bindable();
+
+ public Bindable Current
+ {
+ get => current;
+ set
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ current.UnbindBindings();
+ current.BindTo(value);
+ }
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+ if (!Glowing)
+ main.Colour = value;
+ }
+ }
+
+ private Color4 glowingAccentColour;
+
+ public Color4 GlowingAccentColour
+ {
+ get => glowingAccentColour;
+ set
+ {
+ glowingAccentColour = value;
+ if (Glowing)
+ main.Colour = value;
+ }
+ }
+
+ private Color4 glowColour;
+
+ public Color4 GlowColour
+ {
+ get => glowColour;
+ set
+ {
+ glowColour = value;
+
+ var effect = main.EdgeEffect;
+ effect.Colour = Glowing ? value : value.Opacity(0);
+ main.EdgeEffect = effect;
+ }
+ }
+
+ private void onCurrentValueChanged(ValueChangedEvent filled)
+ {
+ const double duration = 200;
+
+ fill.FadeTo(filled.NewValue ? 1 : 0, duration, Easing.OutQuint);
+
+ if (filled.NewValue)
+ {
+ main.ResizeWidthTo(1, duration, Easing.OutElasticHalf);
+ main.TransformTo(nameof(BorderThickness), 8.5f, duration, Easing.OutElasticHalf);
+ }
+ else
+ {
+ main.ResizeWidthTo(0.75f, duration, Easing.OutQuint);
+ main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs
new file mode 100644
index 0000000000..a18a6a259c
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs
@@ -0,0 +1,173 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osuTK;
+using osuTK.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Overlays;
+using static osu.Game.Graphics.UserInterface.ShearedNub;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public partial class ShearedSliderBar : OsuSliderBar
+ where T : struct, IEquatable, IComparable, IConvertible
+ {
+ protected readonly ShearedNub Nub;
+ protected readonly Box LeftBox;
+ protected readonly Box RightBox;
+ private readonly Container nubContainer;
+
+ private readonly HoverClickSounds hoverClickSounds;
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+
+ // We want to slightly darken the colour for the box because the sheared slider has the boxes at the same height as the nub,
+ // making the nub invisible when not hovered.
+ LeftBox.Colour = value.Darken(0.1f);
+ }
+ }
+
+ private Colour4 backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ get => backgroundColour;
+ set
+ {
+ backgroundColour = value;
+ RightBox.Colour = value;
+ }
+ }
+
+ public ShearedSliderBar()
+ {
+ Shear = SHEAR;
+ Height = HEIGHT;
+ RangePadding = EXPANDED_SIZE / 2;
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Padding = new MarginPadding { Horizontal = 2 },
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Masking = true,
+ CornerRadius = 5,
+ Children = new Drawable[]
+ {
+ LeftBox = new Box
+ {
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ RightBox = new Box
+ {
+ EdgeSmoothness = new Vector2(0, 0.5f),
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ },
+ },
+ },
+ },
+ nubContainer = new Container
+ {
+ Shear = -SHEAR,
+ RelativeSizeAxes = Axes.Both,
+ Child = Nub = new ShearedNub
+ {
+ X = -SHEAR.X * HEIGHT / 2f,
+ Origin = Anchor.TopCentre,
+ RelativePositionAxes = Axes.X,
+ Current = { Value = true }
+ },
+ },
+ hoverClickSounds = new HoverClickSounds()
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider, OsuColour colours)
+ {
+ AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
+ BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindDisabledChanged(disabled =>
+ {
+ Alpha = disabled ? 0.3f : 1;
+ hoverClickSounds.Enabled.Value = !disabled;
+ }, true);
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateGlow();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateGlow();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
+ => Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ updateGlow();
+ base.OnDragEnd(e);
+ }
+
+ private void updateGlow()
+ {
+ Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
+ }
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+ LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
+ RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
+ }
+
+ protected override void UpdateValue(float value)
+ {
+ Nub.MoveToX(value, 250, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs
index 46f0821033..e4058827f3 100644
--- a/osu.Game/Graphics/UserInterface/TimeSlider.cs
+++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Graphics.UserInterface
///
/// A slider bar which displays a millisecond time value.
///
- public partial class TimeSlider : OsuSliderBar
+ public partial class TimeSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => $"{Current.Value:N0} ms";
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 9d14ce95cf..d580eea248 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -35,8 +35,8 @@ namespace osu.Game.Input.Bindings
// It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
- .Concat(ReplayKeyBindings)
.Concat(InGameKeyBindings)
+ .Concat(ReplayKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index 0e62b03f1a..e63395fe26 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -234,6 +234,10 @@ namespace osu.Game.Online.API.Requests.Responses
set => Statistics.RankHistory = value;
}
+ [JsonProperty(@"active_tournament_banner")]
+ [CanBeNull]
+ public TournamentBanner TournamentBanner;
+
[JsonProperty("badges")]
public Badge[] Badges;
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 7ab678775f..e95bc128c8 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
{
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
- connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch)));
+ connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
@@ -558,7 +558,9 @@ namespace osu.Game.Online.Chat
/// Leave the specified channel. Can be called from any thread.
///
/// The channel to leave.
- public void LeaveChannel(Channel channel) => Schedule(() =>
+ public void LeaveChannel(Channel channel) => Schedule(() => leaveChannel(channel, true));
+
+ private void leaveChannel(Channel channel, bool sendLeaveRequest)
{
if (channel == null) return;
@@ -581,10 +583,11 @@ namespace osu.Game.Online.Chat
if (channel.Joined.Value)
{
- api.Queue(new LeaveChannelRequest(channel));
+ if (sendLeaveRequest)
+ api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false;
}
- });
+ }
///
/// Opens the most recently closed channel that has not already been reopened,
diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs
index 9902704883..e7018d6993 100644
--- a/osu.Game/Online/Chat/NowPlayingCommand.cs
+++ b/osu.Game/Online/Chat/NowPlayingCommand.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
beatmapInfo = game.BeatmapInfo;
break;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
verb = "editing";
beatmapInfo = edit.BeatmapInfo;
break;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index df3d8b99f4..7c9b03bd5b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -60,6 +60,7 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
+using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
@@ -501,6 +502,23 @@ namespace osu.Game
/// The build version of the update stream
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
+ ///
+ /// Present a skin select immediately.
+ ///
+ /// The skin to select.
+ public void PresentSkin(SkinInfo skin)
+ {
+ var databasedSkin = SkinManager.Query(s => s.ID == skin.ID);
+
+ if (databasedSkin == null)
+ {
+ Logger.Log("The requested skin could not be loaded.", LoggingTarget.Information);
+ return;
+ }
+
+ SkinManager.CurrentSkinInfo.Value = databasedSkin;
+ }
+
///
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
@@ -777,6 +795,7 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => Notifications.Post(n);
+ SkinManager.PresentImport = items => PresentSkin(items.First().Value);
BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index c539cdc5ec..b44c7c48f5 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -315,10 +315,10 @@ namespace osu.Game.Overlays
channelListing.Hide();
textBar.ShowSearch.Value = false;
- if (loadedChannels.ContainsKey(newChannel))
+ if (loadedChannels.TryGetValue(newChannel, out var loadedChannel))
{
currentChannelContainer.Clear(false);
- currentChannelContainer.Add(loadedChannels[newChannel]);
+ currentChannelContainer.Add(loadedChannel);
}
else
{
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 4cc189f3a2..397dd46cdc 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -76,6 +76,7 @@ namespace osu.Game.Overlays.Comments
private GridContainer content = null!;
private VotePill votePill = null!;
private Container replyEditorContainer = null!;
+ private Container repliesButtonContainer = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@@ -239,10 +240,12 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Top = 10 },
+ Alpha = 0,
},
- new Container
+ repliesButtonContainer = new Container
{
AutoSizeAxes = Axes.Both,
+ Alpha = 0,
Children = new Drawable[]
{
showRepliesButton = new ShowRepliesButton(Comment.RepliesCount)
@@ -449,6 +452,7 @@ namespace osu.Game.Overlays.Comments
{
if (replyEditorContainer.Count == 0)
{
+ replyEditorContainer.Show();
replyEditorContainer.Add(new ReplyCommentEditor(Comment)
{
OnPost = comments =>
@@ -456,12 +460,14 @@ namespace osu.Game.Overlays.Comments
Comment.RepliesCount += comments.Length;
showRepliesButton.Count = Comment.RepliesCount;
Replies.AddRange(comments);
- }
+ },
+ OnCancel = toggleReply
});
}
else
{
- replyEditorContainer.Clear(true);
+ replyEditorContainer.ForEach(e => e.Expire());
+ replyEditorContainer.Hide();
}
}
@@ -513,9 +519,11 @@ namespace osu.Game.Overlays.Comments
int loadedRepliesCount = loadedReplies.Count;
bool hasUnloadedReplies = loadedRepliesCount != Comment.RepliesCount;
- loadRepliesButton.FadeTo(hasUnloadedReplies && loadedRepliesCount == 0 ? 1 : 0);
- showMoreButton.FadeTo(hasUnloadedReplies && loadedRepliesCount > 0 ? 1 : 0);
showRepliesButton.FadeTo(loadedRepliesCount != 0 ? 1 : 0);
+ loadRepliesButton.FadeTo(hasUnloadedReplies && loadedRepliesCount == 0 ? 1 : 0);
+ repliesButtonContainer.FadeTo(repliesButtonContainer.Any(child => child.Alpha > 0) ? 1 : 0);
+
+ showMoreButton.FadeTo(hasUnloadedReplies && loadedRepliesCount > 0 ? 1 : 0);
if (Comment.IsTopLevel)
chevronButton.FadeTo(loadedRepliesCount != 0 ? 1 : 0);
diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
index e738e6e7ec..8aca183dee 100644
--- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
+++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs
@@ -4,7 +4,6 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Online.API;
@@ -33,7 +32,6 @@ namespace osu.Game.Overlays.Comments
public ReplyCommentEditor(Comment parent)
{
parentComment = parent;
- OnCancel = () => this.FadeOut(200).Expire();
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
index 6b3716ac8d..19d7ea7a87 100644
--- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
+++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
@@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Dialog
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
+ private bool mouseDown;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@@ -73,6 +74,12 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged);
}
+ protected override void AbortConfirm()
+ {
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
+ base.AbortConfirm();
+ }
+
protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
@@ -83,6 +90,7 @@ namespace osu.Game.Overlays.Dialog
protected override bool OnMouseDown(MouseDownEvent e)
{
BeginConfirm();
+ mouseDown = true;
return true;
}
@@ -90,11 +98,28 @@ namespace osu.Game.Overlays.Dialog
{
if (!e.HasAnyButtonPressed)
{
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
AbortConfirm();
+ mouseDown = false;
}
}
+ protected override bool OnHover(HoverEvent e)
+ {
+ if (mouseDown)
+ BeginConfirm();
+
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+
+ if (!mouseDown) return;
+
+ AbortConfirm();
+ }
+
private void progressChanged(ValueChangedEvent progress)
{
if (progress.NewValue < progress.OldValue) return;
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
index 1bcb1bcdf4..e3cd2ae36c 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Overlays.FirstRunSetup
public override bool? AllowTrackAdjustments => false;
}
- private partial class UIScaleSlider : OsuSliderBar
+ private partial class UIScaleSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => base.TooltipText + "x";
}
diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs
new file mode 100644
index 0000000000..8e6648dc4b
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Profile.Header.Components;
+
+namespace osu.Game.Overlays.Profile.Header
+{
+ public partial class BannerHeaderContainer : CompositeDrawable
+ {
+ public readonly Bindable User = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Alpha = 0;
+ RelativeSizeAxes = Axes.Both;
+ FillMode = FillMode.Fit;
+ FillAspectRatio = 1000 / 60f;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ User.BindValueChanged(u => updateDisplay(u.NewValue?.User), true);
+ }
+
+ private CancellationTokenSource? cancellationTokenSource;
+
+ private void updateDisplay(APIUser? user)
+ {
+ cancellationTokenSource?.Cancel();
+ cancellationTokenSource = new CancellationTokenSource();
+
+ ClearInternal();
+
+ var banner = user?.TournamentBanner;
+
+ if (banner != null)
+ {
+ Show();
+
+ LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token);
+ }
+ else
+ {
+ Hide();
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationTokenSource?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs
new file mode 100644
index 0000000000..26d333ff95
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API;
+using osu.Game.Users;
+
+namespace osu.Game.Overlays.Profile.Header.Components
+{
+ [LongRunningLoad]
+ public partial class DrawableTournamentBanner : OsuClickableContainer
+ {
+ private readonly TournamentBanner banner;
+
+ public DrawableTournamentBanner(TournamentBanner banner)
+ {
+ this.banner = banner;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LargeTextureStore textures, OsuGame? game, IAPIProvider api)
+ {
+ Child = new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get(banner.Image),
+ };
+
+ Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}");
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ this.FadeInFromZero(200);
+ }
+
+ public override LocalisableString TooltipText => "view in browser";
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs b/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs
index 4d6ee36254..4e17627e04 100644
--- a/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs
@@ -35,6 +35,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
CornerRadius = 8;
TooltipText = group.Name;
+
+ if (group.IsProbationary)
+ {
+ Alpha = 0.6f;
+ }
}
[BackgroundDependencyLoader]
@@ -47,7 +52,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = colourProvider?.Background6 ?? Colour4.Black
+ Colour = colourProvider?.Background6 ?? Colour4.Black,
+ // Normal badges background opacity is 75%, probationary is full opacity as the whole badge gets a bit transparent
+ // Goal is to match osu-web so this is the most accurate it can be, its a bit scuffed but it is what it is
+ // Source: https://github.com/ppy/osu-web/blob/master/resources/css/bem/user-group-badge.less#L50
+ Alpha = group.IsProbationary ? 1 : 0.75f,
},
innerContainer = new FillFlowContainer
{
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index fe1069eea1..4f3f1ac2c3 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -66,10 +66,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
int days = ranked_days - index + 1;
- return new UserGraphTooltipContent(
- UsersStrings.ShowRankGlobalSimple,
- rank.ToLocalisableString("\\##,##0"),
- days == 0 ? "now" : $"{"day".ToQuantity(days)} ago");
+ return new UserGraphTooltipContent
+ {
+ Name = UsersStrings.ShowRankGlobalSimple,
+ Count = rank.ToLocalisableString("\\##,##0"),
+ Time = days == 0 ? "now" : $"{"day".ToQuantity(days)} ago",
+ };
}
}
}
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 652ebcec26..2f4f49788f 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Profile.Header.Components;
@@ -104,76 +103,69 @@ namespace osu.Game.Overlays.Profile.Header
Colour = Colour4.Black.Opacity(0.25f),
}
},
- new OsuContextMenuContainer
+ new FillFlowContainer
{
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
- Child = new FillFlowContainer
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Children = new Drawable[]
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new FillFlowContainer
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5, 0),
+ Children = new Drawable[]
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5, 0),
- Children = new Drawable[]
+ usernameText = new OsuSpriteText
{
- usernameText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
- },
- supporterTag = new SupporterIcon
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Height = 15,
- },
- openUserExternally = new ExternalLinkButton
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- groupBadgeFlow = new GroupBadgeFlow
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- }
- },
- titleText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
- Margin = new MarginPadding { Bottom = 5 }
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
+ Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
+ },
+ supporterTag = new SupporterIcon
{
- userFlag = new UpdateableFlag
- {
- Size = new Vector2(28, 20),
- ShowPlaceholderOnUnknown = false,
- },
- userCountryText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
- Margin = new MarginPadding { Left = 5 },
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- }
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Height = 15,
+ },
+ openUserExternally = new ExternalLinkButton
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ groupBadgeFlow = new GroupBadgeFlow
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ }
+ },
+ titleText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
+ Margin = new MarginPadding { Bottom = 5 }
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ userFlag = new UpdateableFlag
+ {
+ Size = new Vector2(28, 20),
+ ShowPlaceholderOnUnknown = false,
+ },
+ userCountryText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
+ Margin = new MarginPadding { Left = 5 },
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
}
- },
- }
- },
+ }
+ },
+ }
},
}
},
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 5d753dea7d..363eb5d58e 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -47,6 +47,10 @@ namespace osu.Game.Overlays.Profile
RelativeSizeAxes = Axes.X,
User = { BindTarget = User },
},
+ new BannerHeaderContainer
+ {
+ User = { BindTarget = User },
+ },
new BadgeHeaderContainer
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
index 310607e757..0259231001 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
@@ -27,9 +27,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
protected override float GetDataPointHeight(long playCount) => playCount;
protected override UserGraphTooltipContent GetTooltipContent(DateTime date, long playCount) =>
- new UserGraphTooltipContent(
- tooltipCounterName,
- playCount.ToLocalisableString("N0"),
- date.ToLocalisableString("MMMM yyyy"));
+ new UserGraphTooltipContent
+ {
+ Name = tooltipCounterName,
+ Count = playCount.ToLocalisableString("N0"),
+ Time = date.ToLocalisableString("MMMM yyyy")
+ };
}
}
diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs
index 63cdbea5a4..0a5e6ca710 100644
--- a/osu.Game/Overlays/Profile/UserGraph.cs
+++ b/osu.Game/Overlays/Profile/UserGraph.cs
@@ -298,16 +298,8 @@ namespace osu.Game.Overlays.Profile
public class UserGraphTooltipContent
{
- // todo: could use init-only properties on C# 9 which read better than a constructor.
- public LocalisableString Name { get; }
- public LocalisableString Count { get; }
- public LocalisableString Time { get; }
-
- public UserGraphTooltipContent(LocalisableString name, LocalisableString count, LocalisableString time)
- {
- Name = name;
- Count = count;
- Time = time;
- }
+ public LocalisableString Name { get; init; }
+ public LocalisableString Count { get; init; }
+ public LocalisableString Time { get; init; }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
index a83707b5f6..7066be4f92 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
protected override Drawable CreateControl()
{
- var sliderBar = (OsuSliderBar)base.CreateControl();
+ var sliderBar = (RoundedSliderBar)base.CreateControl();
sliderBar.PlaySamplesOnAdjust = false;
return sliderBar;
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index f140c14a0b..6465d62ef0 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -329,7 +329,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
- private partial class UIScaleSlider : OsuSliderBar
+ private partial class UIScaleSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => base.TooltipText + "x";
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 5a6338dc09..dfaeafbf5d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
- public partial class SensitivitySlider : OsuSliderBar
+ public partial class SensitivitySlider : RoundedSliderBar
{
public override LocalisableString TooltipText => Current.Disabled ? MouseSettingsStrings.EnableHighPrecisionForSensitivityAdjust : $"{base.TooltipText}x";
}
diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs
index 26d6147bb7..20d77bef0d 100644
--- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs
+++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections
///
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
///
- public partial class SizeSlider : OsuSliderBar
+ public partial class SizeSlider : RoundedSliderBar
where T : struct, IEquatable, IComparable, IConvertible, IFormattable
{
public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo);
diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs
index babac8ec69..e1483d4202 100644
--- a/osu.Game/Overlays/Settings/SettingsSlider.cs
+++ b/osu.Game/Overlays/Settings/SettingsSlider.cs
@@ -10,14 +10,14 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
- public partial class SettingsSlider : SettingsSlider>
+ public partial class SettingsSlider : SettingsSlider>
where T : struct, IEquatable, IComparable, IConvertible
{
}
public partial class SettingsSlider : SettingsItem
where TValue : struct, IEquatable, IComparable, IConvertible
- where TSlider : OsuSliderBar, new()
+ where TSlider : RoundedSliderBar, new()
{
protected override Drawable CreateControl() => new TSlider
{
diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
index a8d64c1de8..28ceaf09fc 100644
--- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
@@ -148,9 +148,9 @@ namespace osu.Game.Overlays.SkinEditor
component.Origin = Anchor.Centre;
}
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
if (component.DrawSize != Vector2.Zero)
{
diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs
index 83cea65542..866de7e621 100644
--- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs
@@ -24,6 +24,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.OSD;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning;
@@ -31,7 +32,7 @@ using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor
{
[Cached(typeof(SkinEditor))]
- public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler
+ public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler, IEditorChangeHandler
{
public const double TRANSITION_DURATION = 300;
@@ -72,6 +73,11 @@ namespace osu.Game.Overlays.SkinEditor
private EditorSidebar componentsSidebar = null!;
private EditorSidebar settingsSidebar = null!;
+ private SkinEditorChangeHandler? changeHandler;
+
+ private EditorMenuItem undoMenuItem = null!;
+ private EditorMenuItem redoMenuItem = null!;
+
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@@ -131,6 +137,14 @@ namespace osu.Game.Overlays.SkinEditor
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
+ new MenuItem(CommonStrings.MenuBarEdit)
+ {
+ Items = new[]
+ {
+ undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
+ redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
+ }
+ },
}
},
headerText = new OsuTextFlowContainer
@@ -210,6 +224,14 @@ namespace osu.Game.Overlays.SkinEditor
{
switch (e.Action)
{
+ case PlatformAction.Undo:
+ Undo();
+ return true;
+
+ case PlatformAction.Redo:
+ Redo();
+ return true;
+
case PlatformAction.Save:
if (e.Repeat)
return false;
@@ -229,6 +251,8 @@ namespace osu.Game.Overlays.SkinEditor
{
this.targetScreen = targetScreen;
+ changeHandler?.Dispose();
+
SelectedComponents.Clear();
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
@@ -241,6 +265,10 @@ namespace osu.Game.Overlays.SkinEditor
{
Debug.Assert(content != null);
+ changeHandler = new SkinEditorChangeHandler(targetScreen);
+ changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
+ changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
+
content.Child = new SkinBlueprintContainer(targetScreen);
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
@@ -333,6 +361,10 @@ namespace osu.Game.Overlays.SkinEditor
}
}
+ protected void Undo() => changeHandler?.RestoreState(-1);
+
+ protected void Redo() => changeHandler?.RestoreState(1);
+
public void Save(bool userTriggered = true)
{
if (!hasBegunMutating)
@@ -436,5 +468,27 @@ namespace osu.Game.Overlays.SkinEditor
{
}
}
+
+ #region Delegation of IEditorChangeHandler
+
+ public event Action? OnStateChange
+ {
+ add => throw new NotImplementedException();
+ remove => throw new NotImplementedException();
+ }
+
+ private IEditorChangeHandler? beginChangeHandler;
+
+ public void BeginChange()
+ {
+ // Change handler may change between begin and end, which can cause unbalanced operations.
+ // Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
+ (beginChangeHandler = changeHandler)?.BeginChange();
+ }
+
+ public void EndChange() => beginChangeHandler?.EndChange();
+ public void SaveState() => changeHandler?.SaveState();
+
+ #endregion
}
}
diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs
new file mode 100644
index 0000000000..c8f66f3e56
--- /dev/null
+++ b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Extensions;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
+
+namespace osu.Game.Overlays.SkinEditor
+{
+ public partial class SkinEditorChangeHandler : EditorChangeHandler
+ {
+ private readonly ISkinnableTarget? firstTarget;
+
+ // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
+ private readonly BindableList? components;
+
+ public SkinEditorChangeHandler(Drawable targetScreen)
+ {
+ // To keep things simple, we are currently only handling the current target screen for undo / redo.
+ // In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
+ // We'll also need to consider cases where multiple targets are on screen at the same time.
+
+ firstTarget = targetScreen.ChildrenOfType().FirstOrDefault();
+
+ if (firstTarget == null)
+ return;
+
+ components = new BindableList { BindTarget = firstTarget.Components };
+ components.BindCollectionChanged((_, _) => SaveState());
+ }
+
+ protected override void WriteCurrentStateToStream(MemoryStream stream)
+ {
+ if (firstTarget == null)
+ return;
+
+ var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray();
+ string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
+ stream.Write(Encoding.UTF8.GetBytes(json));
+ }
+
+ protected override void ApplyStateChange(byte[] previousState, byte[] newState)
+ {
+ if (firstTarget == null)
+ return;
+
+ var deserializedContent = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(newState));
+
+ if (deserializedContent == null)
+ return;
+
+ SkinnableInfo[] skinnableInfo = deserializedContent.ToArray();
+ Drawable[] targetComponents = firstTarget.Components.OfType().ToArray();
+
+ if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
+ {
+ // Perform a naive full reload for now.
+ firstTarget.Reload(skinnableInfo);
+ }
+ else
+ {
+ int i = 0;
+
+ foreach (var drawable in targetComponents)
+ drawable.ApplySkinnableInfo(skinnableInfo[i++]);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs
index 129b9c1b44..86fcd35e03 100644
--- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs
@@ -241,6 +241,8 @@ namespace osu.Game.Overlays.SkinEditor
private void applyOrigins(Anchor origin)
{
+ OnOperationBegan();
+
foreach (var item in SelectedItems)
{
var drawable = (Drawable)item;
@@ -255,6 +257,8 @@ namespace osu.Game.Overlays.SkinEditor
ApplyClosestAnchor(drawable);
}
+
+ OnOperationEnded();
}
///
@@ -266,6 +270,8 @@ namespace osu.Game.Overlays.SkinEditor
private void applyFixedAnchors(Anchor anchor)
{
+ OnOperationBegan();
+
foreach (var item in SelectedItems)
{
var drawable = (Drawable)item;
@@ -273,15 +279,21 @@ namespace osu.Game.Overlays.SkinEditor
item.UsesFixedAnchor = true;
applyAnchor(drawable, anchor);
}
+
+ OnOperationEnded();
}
private void applyClosestAnchors()
{
+ OnOperationBegan();
+
foreach (var item in SelectedItems)
{
item.UsesFixedAnchor = false;
ApplyClosestAnchor((Drawable)item);
}
+
+ OnOperationEnded();
}
private static Anchor getClosestAnchor(Drawable drawable)
diff --git a/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs
index 5a48dee973..a2b9db2665 100644
--- a/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinSettingsToolbox.cs
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Localisation;
+using osu.Game.Overlays.Settings;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
@@ -13,19 +16,41 @@ namespace osu.Game.Overlays.SkinEditor
{
internal partial class SkinSettingsToolbox : EditorSidebarSection
{
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
protected override Container Content { get; }
+ private readonly Drawable component;
+
public SkinSettingsToolbox(Drawable component)
: base(SkinEditorStrings.Settings(component.GetType().Name))
{
+ this.component = component;
+
base.Content.Add(Content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
- Children = component.CreateSettingsControls().ToArray()
});
}
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var controls = component.CreateSettingsControls().ToArray();
+
+ Content.AddRange(controls);
+
+ // track any changes to update undo states.
+ foreach (var c in controls.OfType())
+ {
+ // TODO: SettingChanged is called too often for cases like SettingsTextBox and SettingsSlider.
+ // We will want to expose a SettingCommitted or similar to make this work better.
+ c.SettingChanged += () => changeHandler?.SaveState();
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 8849c674dc..c5f8a820ea 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@@ -100,17 +101,22 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre,
};
- Add(sectionsContainer = new ProfileSectionsContainer
+ Add(new OsuContextMenuContainer
{
- ExpandableHeader = Header,
- FixedHeader = tabs,
- HeaderBackground = new Box
+ RelativeSizeAxes = Axes.Both,
+ Child = sectionsContainer = new ProfileSectionsContainer
{
- // this is only visible as the ProfileTabControl background
- Colour = ColourProvider.Background5,
- RelativeSizeAxes = Axes.Both
- },
+ ExpandableHeader = Header,
+ FixedHeader = tabs,
+ HeaderBackground = new Box
+ {
+ // this is only visible as the ProfileTabControl background
+ Colour = ColourProvider.Background5,
+ RelativeSizeAxes = Axes.Both
+ },
+ }
});
+
sectionsContainer.SelectedSection.ValueChanged += section =>
{
if (lastSection != section.NewValue)
diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
index 38ced4c9e7..a941c0a1db 100644
--- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
@@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
- new OsuSliderBar
+ new RoundedSliderBar
{
RelativeSizeAxes = Axes.X,
Current = currentNumber,
diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
index 59fbfe49b6..dc7594f469 100644
--- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
+++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
}
}
- public partial class PercentSlider : OsuSliderBar
+ public partial class PercentSlider : RoundedSliderBar
{
public PercentSlider()
{
diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs
index 59c4cfa85b..367ceeb446 100644
--- a/osu.Game/Rulesets/Mods/ModMuted.cs
+++ b/osu.Game/Rulesets/Mods/ModMuted.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mods
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
}
- public partial class MuteComboSlider : OsuSliderBar
+ public partial class MuteComboSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always muted" : base.TooltipText;
}
diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs
index cfac44066e..5b9dfc0430 100644
--- a/osu.Game/Rulesets/Mods/ModNoScope.cs
+++ b/osu.Game/Rulesets/Mods/ModNoScope.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mods
}
}
- public partial class HiddenComboSlider : OsuSliderBar
+ public partial class HiddenComboSlider : RoundedSliderBar
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 930ee0448f..68ca6bc506 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -439,19 +439,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
- // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
- if (!string.IsNullOrEmpty(bankInfo.Filename))
- {
- return new List { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) };
- }
+ var soundTypes = new List();
- var soundTypes = new List
+ if (string.IsNullOrEmpty(bankInfo.Filename))
{
- new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
- // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
- // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
- type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
- };
+ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
+ // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
+ // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
+ type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)));
+ }
+ else
+ {
+ // Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
+ soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
+ }
if (type.HasFlagFast(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index b5f42ec2cc..96f6922224 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -7,21 +7,28 @@ using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Localisation;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
-using osu.Framework.Localisation;
-using osu.Game.Localisation;
namespace osu.Game.Rulesets.Scoring
{
public partial class ScoreProcessor : JudgementProcessor
{
+ private const double accuracy_cutoff_x = 1;
+ private const double accuracy_cutoff_s = 0.95;
+ private const double accuracy_cutoff_a = 0.9;
+ private const double accuracy_cutoff_b = 0.8;
+ private const double accuracy_cutoff_c = 0.7;
+ private const double accuracy_cutoff_d = 0;
+
private const double max_score = 1000000;
///
@@ -160,7 +167,7 @@ namespace osu.Game.Rulesets.Scoring
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
- Rank.Value = rankFrom(accuracy.NewValue);
+ Rank.Value = RankFromAccuracy(accuracy.NewValue);
foreach (var mod in Mods.Value.OfType())
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
@@ -369,22 +376,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- private ScoreRank rankFrom(double acc)
- {
- if (acc == 1)
- return ScoreRank.X;
- if (acc >= 0.95)
- return ScoreRank.S;
- if (acc >= 0.9)
- return ScoreRank.A;
- if (acc >= 0.8)
- return ScoreRank.B;
- if (acc >= 0.7)
- return ScoreRank.C;
-
- return ScoreRank.D;
- }
-
///
/// Resets this ScoreProcessor to a default state.
///
@@ -583,6 +574,62 @@ namespace osu.Game.Rulesets.Scoring
hitEvents.Clear();
}
+ #region Static helper methods
+
+ ///
+ /// Given an accuracy (0..1), return the correct .
+ ///
+ public static ScoreRank RankFromAccuracy(double accuracy)
+ {
+ if (accuracy == accuracy_cutoff_x)
+ return ScoreRank.X;
+ if (accuracy >= accuracy_cutoff_s)
+ return ScoreRank.S;
+ if (accuracy >= accuracy_cutoff_a)
+ return ScoreRank.A;
+ if (accuracy >= accuracy_cutoff_b)
+ return ScoreRank.B;
+ if (accuracy >= accuracy_cutoff_c)
+ return ScoreRank.C;
+
+ return ScoreRank.D;
+ }
+
+ ///
+ /// Given a , return the cutoff accuracy (0..1).
+ /// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank.
+ ///
+ public static double AccuracyCutoffFromRank(ScoreRank rank)
+ {
+ switch (rank)
+ {
+ case ScoreRank.X:
+ case ScoreRank.XH:
+ return accuracy_cutoff_x;
+
+ case ScoreRank.S:
+ case ScoreRank.SH:
+ return accuracy_cutoff_s;
+
+ case ScoreRank.A:
+ return accuracy_cutoff_a;
+
+ case ScoreRank.B:
+ return accuracy_cutoff_b;
+
+ case ScoreRank.C:
+ return accuracy_cutoff_c;
+
+ case ScoreRank.D:
+ return accuracy_cutoff_d;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rank), rank, null);
+ }
+ }
+
+ #endregion
+
///
/// Stores the required scoring data that fulfils the minimum requirements for a to calculate score.
///
diff --git a/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs b/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs
new file mode 100644
index 0000000000..3c19994a8a
--- /dev/null
+++ b/osu.Game/Screens/Edit/BeatmapEditorChangeHandler.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Text;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Edit
+{
+ public partial class BeatmapEditorChangeHandler : EditorChangeHandler
+ {
+ private readonly LegacyEditorBeatmapPatcher patcher;
+ private readonly EditorBeatmap editorBeatmap;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The to track the s of.
+ public BeatmapEditorChangeHandler(EditorBeatmap editorBeatmap)
+ {
+ this.editorBeatmap = editorBeatmap;
+
+ editorBeatmap.TransactionBegan += BeginChange;
+ editorBeatmap.TransactionEnded += EndChange;
+ editorBeatmap.SaveStateTriggered += SaveState;
+
+ patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
+ }
+
+ protected override void WriteCurrentStateToStream(MemoryStream stream)
+ {
+ using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
+ new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
+ }
+
+ protected override void ApplyStateChange(byte[] previousState, byte[] newState) =>
+ patcher.Patch(previousState, newState);
+ }
+}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 74ea933255..bd133383d1 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -157,7 +157,16 @@ namespace osu.Game.Screens.Edit
private bool isNewBeatmap;
- protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
+ protected override UserActivity InitialActivity
+ {
+ get
+ {
+ if (Beatmap.Value.Metadata.Author.OnlineID == api.LocalUser.Value.OnlineID)
+ return new UserActivity.EditingBeatmap(Beatmap.Value.BeatmapInfo);
+
+ return new UserActivity.ModdingBeatmap(Beatmap.Value.BeatmapInfo);
+ }
+ }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -240,7 +249,7 @@ namespace osu.Game.Screens.Edit
if (canSave)
{
- changeHandler = new EditorChangeHandler(editorBeatmap);
+ changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
dependencies.CacheAs(changeHandler);
}
diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs
index 964b86cad3..0bb17e4c5d 100644
--- a/osu.Game/Screens/Edit/EditorChangeHandler.cs
+++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs
@@ -1,31 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps.Formats;
-using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
{
///
/// Tracks changes to the .
///
- public partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
+ public abstract partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
{
public readonly Bindable CanUndo = new Bindable();
public readonly Bindable CanRedo = new Bindable();
- public event Action OnStateChange;
+ public event Action? OnStateChange;
- private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List savedStates = new List();
private int currentState = -1;
@@ -37,32 +31,28 @@ namespace osu.Game.Screens.Edit
{
get
{
+ ensureStateSaved();
+
using (var stream = new MemoryStream(savedStates[currentState]))
return stream.ComputeSHA2Hash();
}
}
- private readonly EditorBeatmap editorBeatmap;
private bool isRestoring;
public const int MAX_SAVED_STATES = 50;
- ///
- /// Creates a new .
- ///
- /// The to track the s of.
- public EditorChangeHandler(EditorBeatmap editorBeatmap)
+ public override void BeginChange()
{
- this.editorBeatmap = editorBeatmap;
+ ensureStateSaved();
- editorBeatmap.TransactionBegan += BeginChange;
- editorBeatmap.TransactionEnded += EndChange;
- editorBeatmap.SaveStateTriggered += SaveState;
+ base.BeginChange();
+ }
- patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
-
- // Initial state.
- SaveState();
+ private void ensureStateSaved()
+ {
+ if (savedStates.Count == 0)
+ SaveState();
}
protected override void UpdateState()
@@ -72,9 +62,7 @@ namespace osu.Game.Screens.Edit
using (var stream = new MemoryStream())
{
- using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw);
-
+ WriteCurrentStateToStream(stream);
byte[] newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
@@ -113,7 +101,8 @@ namespace osu.Game.Screens.Edit
isRestoring = true;
- patcher.Patch(savedStates[currentState], savedStates[newState]);
+ ApplyStateChange(savedStates[currentState], savedStates[newState]);
+
currentState = newState;
isRestoring = false;
@@ -122,6 +111,20 @@ namespace osu.Game.Screens.Edit
updateBindables();
}
+ ///
+ /// Write a serialised copy of the currently tracked state to the provided stream.
+ /// This will be stored as a state which can be restored in the future.
+ ///
+ /// The stream which the state should be written to.
+ protected abstract void WriteCurrentStateToStream(MemoryStream stream);
+
+ ///
+ /// Given a previous and new state, apply any changes required to bring the current state in line with the new state.
+ ///
+ /// The previous (current before this call) serialised state.
+ /// The new state to be applied.
+ protected abstract void ApplyStateChange(byte[] previousState, byte[] newState);
+
private void updateBindables()
{
CanUndo.Value = savedStates.Count > 0 && currentState > 0;
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index e7db1c105b..7dff05667d 100644
--- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.Play;
+using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
{
@@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor;
private readonly EditorState editorState;
+ protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
+
[Resolved]
private MusicController musicController { get; set; } = null!;
diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs
index e7abc1c43d..9fe40ba1b1 100644
--- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs
+++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
+using osu.Framework.Allocation;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
@@ -11,12 +10,13 @@ namespace osu.Game.Screens.Edit
///
/// Interface for a component that manages changes in the .
///
+ [Cached]
public interface IEditorChangeHandler
{
///
/// Fired whenever a state change occurs.
///
- event Action OnStateChange;
+ event Action? OnStateChange;
///
/// Begins a bulk state change event. should be invoked soon after.
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
new file mode 100644
index 0000000000..555c36aac0
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
@@ -0,0 +1,209 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+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.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public partial class ControlPointList : CompositeDrawable
+ {
+ private OsuButton deleteButton = null!;
+ private ControlPointTable table = null!;
+ private OsuScrollContainer scroll = null!;
+ private RoundedButton addButton = null!;
+
+ private readonly IBindableList controlPointGroups = new BindableList();
+
+ [Resolved]
+ private EditorClock clock { get; set; } = null!;
+
+ [Resolved]
+ protected EditorBeatmap Beatmap { get; private set; } = null!;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; } = null!;
+
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ const float margins = 10;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Background4,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Box
+ {
+ Colour = colours.Background3,
+ RelativeSizeAxes = Axes.Y,
+ Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
+ },
+ scroll = new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = table = new ControlPointTable(),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding(margins),
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ deleteButton = new RoundedButton
+ {
+ Text = "-",
+ Size = new Vector2(30, 30),
+ Action = delete,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ addButton = new RoundedButton
+ {
+ Action = addNew,
+ Size = new Vector2(160, 30),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected =>
+ {
+ deleteButton.Enabled.Value = selected.NewValue != null;
+
+ addButton.Text = selected.NewValue != null
+ ? "+ Clone to current time"
+ : "+ Add at current time";
+ }, true);
+
+ controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
+ controlPointGroups.BindCollectionChanged((_, _) =>
+ {
+ table.ControlGroups = controlPointGroups;
+ changeHandler?.SaveState();
+ }, true);
+
+ table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ selectedGroup.Value = null;
+ return true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ trackActivePoint();
+
+ addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
+ }
+
+ private Type? trackedType;
+
+ ///
+ /// Given the user has selected a control point group, we want to track any group which is
+ /// active at the current point in time which matches the type the user has selected.
+ ///
+ /// So if the user is currently looking at a timing point and seeks into the future, a
+ /// future timing point would be automatically selected if it is now the new "current" point.
+ ///
+ private void trackActivePoint()
+ {
+ // For simplicity only match on the first type of the active control point.
+ if (selectedGroup.Value == null)
+ trackedType = null;
+ else
+ {
+ // If the selected group only has one control point, update the tracking type.
+ if (selectedGroup.Value.ControlPoints.Count == 1)
+ trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
+ // If the selected group has more than one control point, choose the first as the tracking type
+ // if we don't already have a singular tracked type.
+ else if (trackedType == null)
+ trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
+ }
+
+ if (trackedType != null)
+ {
+ // We don't have an efficient way of looking up groups currently, only individual point types.
+ // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
+
+ // Find the next group which has the same type as the selected one.
+ var found = Beatmap.ControlPointInfo.Groups
+ .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
+ .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
+
+ if (found != null)
+ selectedGroup.Value = found;
+ }
+ }
+
+ private void delete()
+ {
+ if (selectedGroup.Value == null)
+ return;
+
+ Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
+
+ selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
+ }
+
+ private void addNew()
+ {
+ bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
+
+ var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+
+ if (isFirstControlPoint)
+ group.Add(new TimingControlPoint());
+ else
+ {
+ // Try and create matching types from the currently selected control point.
+ var selected = selectedGroup.Value;
+
+ if (selected != null && !ReferenceEquals(selected, group))
+ {
+ foreach (var controlPoint in selected.ControlPoints)
+ {
+ group.Add(controlPoint.DeepClone());
+ }
+ }
+ }
+
+ selectedGroup.Value = group;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index 2450909929..3f911f5067 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -1,20 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Linq;
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.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Graphics.UserInterfaceV2;
-using osu.Game.Overlays;
-using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
@@ -23,6 +14,9 @@ namespace osu.Game.Screens.Edit.Timing
[Cached]
public readonly Bindable SelectedGroup = new Bindable();
+ [Resolved]
+ private EditorClock? editorClock { get; set; }
+
public TimingScreen()
: base(EditorScreenMode.Timing)
{
@@ -46,192 +40,17 @@ namespace osu.Game.Screens.Edit.Timing
}
};
- public partial class ControlPointList : CompositeDrawable
+ protected override void LoadComplete()
{
- private OsuButton deleteButton = null!;
- private ControlPointTable table = null!;
- private OsuScrollContainer scroll = null!;
- private RoundedButton addButton = null!;
+ base.LoadComplete();
- private readonly IBindableList controlPointGroups = new BindableList();
-
- [Resolved]
- private EditorClock clock { get; set; } = null!;
-
- [Resolved]
- protected EditorBeatmap Beatmap { get; private set; } = null!;
-
- [Resolved]
- private Bindable selectedGroup { get; set; } = null!;
-
- [Resolved]
- private IEditorChangeHandler? changeHandler { get; set; }
-
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colours)
+ if (editorClock != null)
{
- RelativeSizeAxes = Axes.Both;
-
- const float margins = 10;
- InternalChildren = new Drawable[]
- {
- new Box
- {
- Colour = colours.Background4,
- RelativeSizeAxes = Axes.Both,
- },
- new Box
- {
- Colour = colours.Background3,
- RelativeSizeAxes = Axes.Y,
- Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
- },
- scroll = new OsuScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- Child = table = new ControlPointTable(),
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Direction = FillDirection.Horizontal,
- Margin = new MarginPadding(margins),
- Spacing = new Vector2(5),
- Children = new Drawable[]
- {
- deleteButton = new RoundedButton
- {
- Text = "-",
- Size = new Vector2(30, 30),
- Action = delete,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- addButton = new RoundedButton
- {
- Action = addNew,
- Size = new Vector2(160, 30),
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- }
- },
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- selectedGroup.BindValueChanged(selected =>
- {
- deleteButton.Enabled.Value = selected.NewValue != null;
-
- addButton.Text = selected.NewValue != null
- ? "+ Clone to current time"
- : "+ Add at current time";
- }, true);
-
- controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
- controlPointGroups.BindCollectionChanged((_, _) =>
- {
- table.ControlGroups = controlPointGroups;
- changeHandler?.SaveState();
- }, true);
-
- table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- selectedGroup.Value = null;
- return true;
- }
-
- protected override void Update()
- {
- base.Update();
-
- trackActivePoint();
-
- addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
- }
-
- private Type? trackedType;
-
- ///
- /// Given the user has selected a control point group, we want to track any group which is
- /// active at the current point in time which matches the type the user has selected.
- ///
- /// So if the user is currently looking at a timing point and seeks into the future, a
- /// future timing point would be automatically selected if it is now the new "current" point.
- ///
- private void trackActivePoint()
- {
- // For simplicity only match on the first type of the active control point.
- if (selectedGroup.Value == null)
- trackedType = null;
- else
- {
- // If the selected group only has one control point, update the tracking type.
- if (selectedGroup.Value.ControlPoints.Count == 1)
- trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
- // If the selected group has more than one control point, choose the first as the tracking type
- // if we don't already have a singular tracked type.
- else if (trackedType == null)
- trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
- }
-
- if (trackedType != null)
- {
- // We don't have an efficient way of looking up groups currently, only individual point types.
- // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
-
- // Find the next group which has the same type as the selected one.
- var found = Beatmap.ControlPointInfo.Groups
- .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
- .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
-
- if (found != null)
- selectedGroup.Value = found;
- }
- }
-
- private void delete()
- {
- if (selectedGroup.Value == null)
- return;
-
- Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
-
- selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
- }
-
- private void addNew()
- {
- bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
-
- var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
-
- if (isFirstControlPoint)
- group.Add(new TimingControlPoint());
- else
- {
- // Try and create matching types from the currently selected control point.
- var selected = selectedGroup.Value;
-
- if (selected != null && !ReferenceEquals(selected, group))
- {
- foreach (var controlPoint in selected.ControlPoints)
- {
- group.Add(controlPoint.DeepClone());
- }
- }
- }
-
- selectedGroup.Value = group;
+ // When entering the timing screen, let's choose the closest valid timing point.
+ // This will emulate the osu-stable behaviour where a metronome and timing information
+ // are presented on entering the screen.
+ var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime);
+ SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time);
}
}
}
diff --git a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs
index 55c9cf86c3..92f1e19e6f 100644
--- a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs
+++ b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Graphics;
@@ -16,17 +14,17 @@ namespace osu.Game.Screens.Edit
///
/// Fires whenever a transaction begins. Will not fire on nested transactions.
///
- public event Action TransactionBegan;
+ public event Action? TransactionBegan;
///
/// Fires when the last transaction completes.
///
- public event Action TransactionEnded;
+ public event Action? TransactionEnded;
///
/// Fires when is called and results in a non-transactional state save.
///
- public event Action SaveStateTriggered;
+ public event Action? SaveStateTriggered;
public bool TransactionActive => bulkChangesStarted > 0;
@@ -35,7 +33,7 @@ namespace osu.Game.Screens.Edit
///
/// Signal the beginning of a change.
///
- public void BeginChange()
+ public virtual void BeginChange()
{
if (bulkChangesStarted++ == 0)
TransactionBegan?.Invoke();
diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
index 92b432831d..c4e2dbf403 100644
--- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
+++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -22,29 +20,21 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
- new Container
+ new Box
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
Height = height,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
- }
+ Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
},
- new Container
+ new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = height,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
- }
+ Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
};
}
diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs
index da759b4329..9fdae50615 100644
--- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs
+++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs
@@ -69,8 +69,7 @@ namespace osu.Game.Screens.Play.HUD
{
var bindable = (IBindable)property.GetValue(component)!;
- if (!bindable.IsDefault)
- Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
+ Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
}
if (component is Container container)
diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
index 7f9f353ded..c04ecd671f 100644
--- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
@@ -121,8 +121,8 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
- Origin = Anchor.CentreRight,
- Anchor = Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs
index d9c60519ad..db42998c45 100644
--- a/osu.Game/Screens/Play/PauseOverlay.cs
+++ b/osu.Game/Screens/Play/PauseOverlay.cs
@@ -44,6 +44,14 @@ namespace osu.Game.Screens.Play
});
}
+ public void StopAllSamples()
+ {
+ if (!IsLoaded)
+ return;
+
+ pauseLoop.Stop();
+ }
+
protected override void PopIn()
{
base.PopIn();
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index bf7f38cdd3..6dc4854e80 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -1073,7 +1073,10 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(ScreenExitEvent e)
{
screenSuspension?.RemoveAndDisposeImmediately();
+
+ // Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations.
failAnimationLayer?.Stop();
+ PauseOverlay?.StopAllSamples();
if (LoadedBeatmapSuccessfully)
{
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
index 45d4995753..45009684a6 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs
@@ -15,11 +15,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
public partial class PlayerSliderBar : SettingsSlider
where T : struct, IEquatable, IComparable, IConvertible
{
- public OsuSliderBar Bar => (OsuSliderBar)Control;
+ public RoundedSliderBar Bar => (RoundedSliderBar)Control;
protected override Drawable CreateControl() => new SliderBar();
- protected partial class SliderBar : OsuSliderBar
+ protected partial class SliderBar : RoundedSliderBar
{
public SliderBar()
{
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index c5ef6b1585..8a4e63d21c 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
+using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@@ -24,6 +25,8 @@ namespace osu.Game.Screens.Play
private readonly bool replayIsFailedScore;
+ protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
+
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure()
{
diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
index 240fbcf662..c9d1f4acaa 100644
--- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
+++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
+using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@@ -14,6 +15,8 @@ namespace osu.Game.Screens.Play
{
private readonly Score score;
+ protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo);
+
public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null)
: base(score, configuration)
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 8e04bb68fb..2ec4270c3c 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -17,6 +17,7 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
@@ -28,6 +29,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
public partial class AccuracyCircle : CompositeDrawable
{
+ private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
+ private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
+ private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
+ private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
+ private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
+ private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
+
///
/// Duration for the transforms causing this component to appear.
///
@@ -73,6 +81,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
private const double virtual_ss_percentage = 0.01;
+ ///
+ /// The width of a in terms of accuracy.
+ ///
+ public const double NOTCH_WIDTH_PERCENTAGE = 1.0 / 360;
+
///
/// The easing for the circle filling transforms.
///
@@ -145,49 +158,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.X),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 1 }
+ Current = { Value = accuracy_x }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.S),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 1 - virtual_ss_percentage }
+ Current = { Value = accuracy_x - virtual_ss_percentage }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.A),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.95f }
+ Current = { Value = accuracy_s }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.B),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.9f }
+ Current = { Value = accuracy_a }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.C),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.8f }
+ Current = { Value = accuracy_b }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.D),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.7f }
+ Current = { Value = accuracy_c }
},
- new RankNotch(0),
- new RankNotch((float)(1 - virtual_ss_percentage)),
- new RankNotch(0.95f),
- new RankNotch(0.9f),
- new RankNotch(0.8f),
- new RankNotch(0.7f),
+ new RankNotch((float)accuracy_x),
+ new RankNotch((float)(accuracy_x - virtual_ss_percentage)),
+ new RankNotch((float)accuracy_s),
+ new RankNotch((float)accuracy_a),
+ new RankNotch((float)accuracy_b),
+ new RankNotch((float)accuracy_c),
new BufferedContainer
{
Name = "Graded circle mask",
@@ -215,12 +228,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Padding = new MarginPadding { Vertical = -15, Horizontal = -20 },
Children = new[]
{
- new RankBadge(1, getRank(ScoreRank.X)),
- new RankBadge(0.95, getRank(ScoreRank.S)),
- new RankBadge(0.9, getRank(ScoreRank.A)),
- new RankBadge(0.8, getRank(ScoreRank.B)),
- new RankBadge(0.7, getRank(ScoreRank.C)),
- new RankBadge(0.35, getRank(ScoreRank.D)),
+ // The S and A badges are moved down slightly to prevent collision with the SS badge.
+ new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)),
+ new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)),
+ new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)),
+ new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)),
+ new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)),
+ new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)),
}
},
rankText = new RankText(score.Rank)
@@ -263,7 +277,39 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY))
{
- double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy);
+ double targetAccuracy = score.Accuracy;
+ double[] notchPercentages =
+ {
+ accuracy_s,
+ accuracy_a,
+ accuracy_b,
+ accuracy_c,
+ };
+
+ // Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es),
+ // to prevent ambiguity on what grade it's pointing at.
+ foreach (double p in notchPercentages)
+ {
+ if (Precision.AlmostEquals(p, targetAccuracy, NOTCH_WIDTH_PERCENTAGE / 2))
+ {
+ int tippingDirection = targetAccuracy - p >= 0 ? 1 : -1; // We "round up" here to match rank criteria
+ targetAccuracy = p + tippingDirection * (NOTCH_WIDTH_PERCENTAGE / 2);
+ break;
+ }
+ }
+
+ // The final gap between 99.999...% (S) and 100% (SS) is exaggerated by `virtual_ss_percentage`. We don't want to land there either.
+ if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH)
+ targetAccuracy = 1;
+ else
+ targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy);
+
+ // The accuracy circle gauge visually fills up a bit too much.
+ // This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases.
+ const double visual_alignment_offset = 0.001;
+
+ if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
+ targetAccuracy -= visual_alignment_offset;
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
@@ -293,7 +339,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (badge.Accuracy > score.Accuracy)
continue;
- using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
+ using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
{
badge.Appear();
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
index 5432b4cbeb..7af327828e 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
@@ -27,6 +27,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
public readonly double Accuracy;
+ ///
+ /// The position around the to display this badge.
+ ///
+ private readonly double displayPosition;
+
private readonly ScoreRank rank;
private Drawable rankContainer;
@@ -36,10 +41,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// Creates a new .
///
/// The accuracy value corresponding to .
+ /// The position around the to display this badge.
/// The to be displayed in this .
- public RankBadge(double accuracy, ScoreRank rank)
+ public RankBadge(double accuracy, double position, ScoreRank rank)
{
Accuracy = accuracy;
+ displayPosition = position;
this.rank = rank;
RelativeSizeAxes = Axes.Both;
@@ -92,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
base.Update();
// Starts at -90deg (top) and moves counter-clockwise by the accuracy
- rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)Accuracy) * MathF.PI * 2);
+ rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)displayPosition) * MathF.PI * 2);
}
private Vector2 circlePosition(float t)
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
index 7e73767318..32f2eb2fa5 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Height = AccuracyCircle.RANK_CIRCLE_RADIUS,
- Width = 1f,
+ Width = (float)AccuracyCircle.NOTCH_WIDTH_PERCENTAGE * 360f,
Colour = OsuColour.Gray(0.3f),
EdgeSmoothness = new Vector2(1f)
}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs
new file mode 100644
index 0000000000..b8c9f0b34b
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonModsV2 : FooterButtonV2
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ Text = "Mods";
+ Icon = FontAwesome.Solid.ExchangeAlt;
+ AccentColour = colour.Lime1;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs
new file mode 100644
index 0000000000..87cca0042a
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Input.Bindings;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonOptionsV2 : FooterButtonV2
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ Text = "Options";
+ Icon = FontAwesome.Solid.Cog;
+ AccentColour = colour.Purple1;
+ Hotkey = GlobalAction.ToggleBeatmapOptions;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs
new file mode 100644
index 0000000000..70d1c0c19e
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs
@@ -0,0 +1,161 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonRandomV2 : FooterButtonV2
+ {
+ public Action? NextRandom { get; set; }
+ public Action? PreviousRandom { get; set; }
+
+ private Container persistentText = null!;
+ private OsuSpriteText randomSpriteText = null!;
+ private OsuSpriteText rewindSpriteText = null!;
+ private bool rewindSearch;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ //TODO: use https://fontawesome.com/icons/shuffle?s=solid&f=classic when local Fontawesome is updated
+ Icon = FontAwesome.Solid.Random;
+ AccentColour = colour.Blue1;
+ TextContainer.Add(persistentText = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AlwaysPresent = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ randomSpriteText = new OsuSpriteText
+ {
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = "Random",
+ },
+ rewindSpriteText = new OsuSpriteText
+ {
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = "Rewind",
+ Alpha = 0f,
+ }
+ }
+ });
+
+ Action = () =>
+ {
+ if (rewindSearch)
+ {
+ const double fade_time = 500;
+
+ OsuSpriteText fallingRewind;
+
+ TextContainer.Add(fallingRewind = new OsuSpriteText
+ {
+ Alpha = 0,
+ Text = rewindSpriteText.Text,
+ AlwaysPresent = true, // make sure the button is sized large enough to always show this
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ });
+
+ fallingRewind.FadeOutFromOne(fade_time, Easing.In);
+ fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
+ fallingRewind.Expire();
+
+ persistentText.FadeInFromZero(fade_time, Easing.In);
+
+ PreviousRandom?.Invoke();
+ }
+ else
+ {
+ NextRandom?.Invoke();
+ }
+ };
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ updateText(e.ShiftPressed);
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ updateText(e.ShiftPressed);
+ base.OnKeyUp(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ try
+ {
+ // this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
+ rewindSearch |= e.ShiftPressed;
+ return base.OnClick(e);
+ }
+ finally
+ {
+ rewindSearch = false;
+ }
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ if (e.Button == MouseButton.Right)
+ {
+ rewindSearch = true;
+ TriggerClick();
+ return;
+ }
+
+ base.OnMouseUp(e);
+ }
+
+ public override bool OnPressed(KeyBindingPressEvent e)
+ {
+ rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
+
+ if (e.Action != GlobalAction.SelectNextRandom && e.Action != GlobalAction.SelectPreviousRandom)
+ {
+ return false;
+ }
+
+ if (!e.Repeat)
+ TriggerClick();
+ return true;
+ }
+
+ public override void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action == GlobalAction.SelectPreviousRandom)
+ {
+ rewindSearch = false;
+ }
+ }
+
+ private void updateText(bool rewind = false)
+ {
+ randomSpriteText.Alpha = rewind ? 0 : 1;
+ rewindSpriteText.Alpha = rewind ? 1 : 0;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs
new file mode 100644
index 0000000000..2f5046d2bb
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs
@@ -0,0 +1,211 @@
+// Copyright (c) ppy Pty Ltd . 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.Extensions.Color4Extensions;
+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.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler
+ {
+ private const int button_height = 90;
+ private const int button_width = 140;
+ private const int corner_radius = 10;
+ private const int transition_length = 500;
+
+ // This should be 12 by design, but an extra allowance is added due to the corner radius specification.
+ public const float SHEAR_WIDTH = 13.5f;
+
+ public Bindable OverlayState = new Bindable();
+
+ protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / button_height, 0);
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private Colour4 buttonAccentColour;
+
+ protected Colour4 AccentColour
+ {
+ set
+ {
+ buttonAccentColour = value;
+ bar.Colour = buttonAccentColour;
+ icon.Colour = buttonAccentColour;
+ }
+ }
+
+ protected IconUsage Icon
+ {
+ set => icon.Icon = value;
+ }
+
+ protected LocalisableString Text
+ {
+ set => text.Text = value;
+ }
+
+ private readonly SpriteText text;
+ private readonly SpriteIcon icon;
+
+ protected Container TextContainer;
+ private readonly Box bar;
+ private readonly Box backgroundBox;
+
+ public FooterButtonV2()
+ {
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 4,
+ // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
+ Colour = Colour4.Black.Opacity(0.25f),
+ Offset = new Vector2(0, 2),
+ };
+ Shear = SHEAR;
+ Size = new Vector2(button_width, button_height);
+ Masking = true;
+ CornerRadius = corner_radius;
+ Children = new Drawable[]
+ {
+ backgroundBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+
+ // For elements that should not be sheared.
+ new Container
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Shear = -SHEAR,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ TextContainer = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Y = 42,
+ AutoSizeAxes = Axes.Both,
+ Child = text = new OsuSpriteText
+ {
+ // figma design says the size is 16, but due to the issues with font sizes 19 matches better
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true
+ }
+ },
+ icon = new SpriteIcon
+ {
+ Y = 12,
+ Size = new Vector2(20),
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ }
+ },
+ new Container
+ {
+ Shear = -SHEAR,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Y = -corner_radius,
+ Size = new Vector2(120, 6),
+ Masking = true,
+ CornerRadius = 3,
+ Child = bar = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ OverlayState.BindValueChanged(_ => updateDisplay());
+ Enabled.BindValueChanged(_ => updateDisplay(), true);
+
+ FinishTransforms(true);
+ }
+
+ public GlobalAction? Hotkey;
+
+ private bool handlingMouse;
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateDisplay();
+ return true;
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ handlingMouse = true;
+ updateDisplay();
+ return base.OnMouseDown(e);
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ handlingMouse = false;
+ updateDisplay();
+ base.OnMouseUp(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
+
+ public virtual bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action != Hotkey || e.Repeat) return false;
+
+ TriggerClick();
+ return true;
+ }
+
+ public virtual void OnReleased(KeyBindingReleaseEvent e) { }
+
+ private void updateDisplay()
+ {
+ Color4 backgroundColour = colourProvider.Background3;
+
+ if (!Enabled.Value)
+ {
+ backgroundColour = colourProvider.Background3.Darken(0.4f);
+ }
+ else
+ {
+ if (OverlayState.Value == Visibility.Visible)
+ backgroundColour = buttonAccentColour.Darken(0.5f);
+
+ if (IsHovered)
+ {
+ backgroundColour = backgroundColour.Lighten(0.3f);
+
+ if (handlingMouse)
+ backgroundColour = backgroundColour.Lighten(0.3f);
+ }
+ }
+
+ backgroundBox.FadeColour(backgroundColour, transition_length, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Select/FooterV2/FooterV2.cs
new file mode 100644
index 0000000000..cd95f3eb6c
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterV2.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterV2 : InputBlockingContainer
+ {
+ //Should be 60, setting to 50 for now for the sake of matching the current BackButton height.
+ private const int height = 50;
+ private const int padding = 80;
+
+ private readonly List overlays = new List();
+
+ /// The button to be added.
+ /// The to be toggled by this button.
+ public void AddButton(FooterButtonV2 button, OverlayContainer? overlay = null)
+ {
+ if (overlay != null)
+ {
+ overlays.Add(overlay);
+ button.Action = () => showOverlay(overlay);
+ button.OverlayState.BindTo(overlay.State);
+ }
+
+ buttons.Add(button);
+ }
+
+ private void showOverlay(OverlayContainer overlay)
+ {
+ foreach (var o in overlays)
+ {
+ if (o == overlay)
+ o.ToggleVisibility();
+ else
+ o.Hide();
+ }
+ }
+
+ private FillFlowContainer buttons = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background5
+ },
+ buttons = new FillFlowContainer
+ {
+ Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(-FooterButtonV2.SHEAR_WIDTH + 7, 0),
+ AutoSizeAxes = Axes.Both
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs
index 57c78bfe1c..3f116f8f76 100644
--- a/osu.Game/Skinning/ISkinnableTarget.cs
+++ b/osu.Game/Skinning/ISkinnableTarget.cs
@@ -36,6 +36,11 @@ namespace osu.Game.Skinning
///
void Reload();
+ ///
+ /// Reload this target from the provided skinnable information.
+ ///
+ void Reload(SkinnableInfo[] skinnableInfo);
+
///
/// Add a new skinnable component to this target.
///
@@ -46,6 +51,6 @@ namespace osu.Game.Skinning
/// Remove an existing skinnable component from this target.
///
/// The component to remove.
- public void Remove(ISkinnableDrawable component);
+ void Remove(ISkinnableDrawable component);
}
}
diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
index 5ed2ddc73c..f460a3d31a 100644
--- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Skinning
public Dictionary ImageLookups = new Dictionary();
+ public float WidthForNoteHeightScale;
+
public readonly float[] ColumnLineWidth;
public readonly float[] ColumnSpacing;
public readonly float[] ColumnWidth;
@@ -41,6 +43,8 @@ namespace osu.Game.Skinning
public bool ShowJudgementLine = true;
public bool KeysUnderNotes;
+ public LegacyNoteBodyStyle? NoteBodyStyle;
+
public LegacyManiaSkinConfiguration(int keys)
{
Keys = keys;
@@ -55,12 +59,6 @@ namespace osu.Game.Skinning
ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE);
}
- private float? minimumColumnWidth;
-
- public float MinimumColumnWidth
- {
- get => minimumColumnWidth ?? ColumnWidth.Min();
- set => minimumColumnWidth = value;
- }
+ public float MinimumColumnWidth => ColumnWidth.Min();
}
}
diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
index 3ec0ee6006..a2408a92bb 100644
--- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
@@ -54,6 +54,7 @@ namespace osu.Game.Skinning
HoldNoteBodyImage,
HoldNoteLightImage,
HoldNoteLightScale,
+ WidthForNoteHeightScale,
ExplosionImage,
ExplosionScale,
ColumnLineColour,
@@ -71,5 +72,6 @@ namespace osu.Game.Skinning
Hit50,
Hit0,
KeysUnderNotes,
+ NoteBodyStyle
}
}
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index 0aafdd4db0..e880e3c1ed 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
@@ -114,10 +114,13 @@ namespace osu.Game.Skinning
parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth);
break;
+ case "NoteBodyStyle":
+ if (Enum.TryParse(pair.Value, out var style))
+ currentConfig.NoteBodyStyle = style;
+ break;
+
case "WidthForNoteHeightScale":
- float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
- if (minWidth > 0)
- currentConfig.MinimumColumnWidth = minWidth;
+ currentConfig.WidthForNoteHeightScale = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break;
case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal):
diff --git a/osu.Game/Skinning/LegacyNoteBodyStyle.cs b/osu.Game/Skinning/LegacyNoteBodyStyle.cs
new file mode 100644
index 0000000000..3c1108dcef
--- /dev/null
+++ b/osu.Game/Skinning/LegacyNoteBodyStyle.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Skinning
+{
+ public enum LegacyNoteBodyStyle
+ {
+ Stretch = 0,
+
+ // listed as the default on https://osu.ppy.sh/wiki/en/Skinning/skin.ini, but is seemingly not according to the source.
+ // Repeat = 1,
+
+ RepeatTop = 2,
+ RepeatBottom = 3,
+ RepeatTopAndBottom = 4,
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 5f12d2ce23..b2619fa55b 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -138,6 +138,10 @@ namespace osu.Game.Skinning
Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value]));
+ case LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale:
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(new Bindable(existing.WidthForNoteHeightScale));
+
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value]));
@@ -185,6 +189,16 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth:
return SkinUtils.As(new Bindable(existing.MinimumColumnWidth));
+ case LegacyManiaSkinConfigurationLookups.NoteBodyStyle:
+
+ if (existing.NoteBodyStyle != null)
+ return SkinUtils.As(new Bindable(existing.NoteBodyStyle.Value));
+
+ if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
+ return SkinUtils.As(new Bindable(LegacyNoteBodyStyle.Stretch));
+
+ return SkinUtils.As(new Bindable(LegacyNoteBodyStyle.RepeatBottom));
+
case LegacyManiaSkinConfigurationLookups.NoteImage:
Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}"));
diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs
index 794a12da82..df5299d427 100644
--- a/osu.Game/Skinning/SkinnableTargetContainer.cs
+++ b/osu.Game/Skinning/SkinnableTargetContainer.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Screens.Play.HUD;
namespace osu.Game.Skinning
{
@@ -30,16 +32,31 @@ namespace osu.Game.Skinning
Target = target;
}
- ///
- /// Reload all components in this container from the current skin.
- ///
- public void Reload()
+ public void Reload(SkinnableInfo[] skinnableInfo)
+ {
+ var drawables = new List();
+
+ foreach (var i in skinnableInfo)
+ drawables.Add(i.CreateInstance());
+
+ Reload(new SkinnableTargetComponentsContainer
+ {
+ Children = drawables,
+ });
+ }
+
+ public void Reload() => Reload(CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer);
+
+ public void Reload(SkinnableTargetComponentsContainer? componentsContainer)
{
ClearInternal();
components.Clear();
ComponentsLoaded = false;
- content = CurrentSkin.GetDrawableComponent(new GlobalSkinComponentLookup(Target)) as SkinnableTargetComponentsContainer;
+ if (componentsContainer == null)
+ return;
+
+ content = componentsContainer;
cancellationSource?.Cancel();
cancellationSource = null;
diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs
index 0f26ed2c66..29e034d86c 100644
--- a/osu.Game/Storyboards/CommandLoop.cs
+++ b/osu.Game/Storyboards/CommandLoop.cs
@@ -18,7 +18,12 @@ namespace osu.Game.Storyboards
public readonly int TotalIterations;
public override double StartTime => LoopStartTime + CommandsStartTime;
- public override double EndTime => StartTime + CommandsDuration * TotalIterations;
+
+ public override double EndTime =>
+ // In an ideal world, we would multiply the command duration by TotalIterations here.
+ // Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro
+ // sequences for minutes or hours.
+ StartTime + CommandsDuration;
///
/// Construct a new command loop.
diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs
index de5da3118a..d198ed68bd 100644
--- a/osu.Game/Storyboards/CommandTimelineGroup.cs
+++ b/osu.Game/Storyboards/CommandTimelineGroup.cs
@@ -84,9 +84,6 @@ namespace osu.Game.Storyboards
[JsonIgnore]
public virtual double EndTime => CommandsEndTime;
- [JsonIgnore]
- public double Duration => EndTime - StartTime;
-
[JsonIgnore]
public bool HasCommands
{
diff --git a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs
index ae0225d8df..acfff4cefe 100644
--- a/osu.Game/Tests/Visual/DependencyProvidingContainer.cs
+++ b/osu.Game/Tests/Visual/DependencyProvidingContainer.cs
@@ -20,8 +20,7 @@ namespace osu.Game.Tests.Visual
///
/// The dependencies provided to the children.
///
- // TODO: should be an init-only property when C# 9
- public (Type, object)[] CachedDependencies { get; set; } = Array.Empty<(Type, object)>();
+ public (Type, object)[] CachedDependencies { get; init; } = Array.Empty<(Type, object)>();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
index 0f3f9f2199..a77ea80958 100644
--- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
+++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Tests.Visual
{
///
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index 5a3009dfcd..e74ffc9d54 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
@@ -13,56 +10,49 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
{
- public partial class ClickableAvatar : Container
+ public partial class ClickableAvatar : OsuClickableContainer
{
private const string default_tooltip_text = "view profile";
- ///
- /// Whether to open the user's profile when clicked.
- ///
- public bool OpenOnClick
+ public override LocalisableString TooltipText
{
- set => clickableArea.Enabled.Value = clickableArea.Action != null && value;
+ get
+ {
+ if (!Enabled.Value)
+ return string.Empty;
+
+ return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text;
+ }
+ set => throw new NotSupportedException();
}
///
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
/// Setting this to true exposes the username via tooltip for special cases where this is not true.
///
- public bool ShowUsernameTooltip
- {
- set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
- }
+ public bool ShowUsernameTooltip { get; set; }
- private readonly APIUser user;
+ private readonly APIUser? user;
- [Resolved(CanBeNull = true)]
- private OsuGame game { get; set; }
-
- private readonly ClickableArea clickableArea;
+ [Resolved]
+ private OsuGame? game { get; set; }
///
/// A clickable avatar for the specified user, with UI sounds included.
- /// If is true, clicking will open the user's profile.
///
/// The user. A null value will get a placeholder avatar.
- public ClickableAvatar(APIUser user = null)
+ public ClickableAvatar(APIUser? user = null)
{
this.user = user;
- Add(clickableArea = new ClickableArea
- {
- RelativeSizeAxes = Axes.Both,
- });
-
if (user?.Id != APIUser.SYSTEM_USER_ID)
- clickableArea.Action = openProfile;
+ Action = openProfile;
}
[BackgroundDependencyLoader]
private void load()
{
- LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
+ LoadComponentAsync(new DrawableAvatar(user), Add);
}
private void openProfile()
@@ -71,23 +61,12 @@ namespace osu.Game.Users.Drawables
game?.ShowUser(user);
}
- private partial class ClickableArea : OsuClickableContainer
+ protected override bool OnClick(ClickEvent e)
{
- private LocalisableString tooltip = default_tooltip_text;
+ if (!Enabled.Value)
+ return false;
- public override LocalisableString TooltipText
- {
- get => Enabled.Value ? tooltip : default;
- set => tooltip = value;
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- if (!Enabled.Value)
- return false;
-
- return base.OnClick(e);
- }
+ return base.OnClick(e);
}
}
}
diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs
index 9c04eb5706..c659685807 100644
--- a/osu.Game/Users/Drawables/UpdateableAvatar.cs
+++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -13,9 +11,9 @@ namespace osu.Game.Users.Drawables
///
/// An avatar which can update to a new user when needed.
///
- public partial class UpdateableAvatar : ModelBackedDrawable
+ public partial class UpdateableAvatar : ModelBackedDrawable
{
- public APIUser User
+ public APIUser? User
{
get => Model;
set => Model = value;
@@ -58,7 +56,7 @@ namespace osu.Game.Users.Drawables
/// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.
/// Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if is also true)
/// Whether to show a default guest representation on null user (as opposed to nothing).
- public UpdateableAvatar(APIUser user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
+ public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.isInteractive = isInteractive;
this.showUsernameTooltip = showUsernameTooltip;
@@ -67,7 +65,7 @@ namespace osu.Game.Users.Drawables
User = user;
}
- protected override Drawable CreateDrawable(APIUser user)
+ protected override Drawable? CreateDrawable(APIUser? user)
{
if (user == null && !showGuestOnNull)
return null;
@@ -76,7 +74,6 @@ namespace osu.Game.Users.Drawables
{
return new ClickableAvatar(user)
{
- OpenOnClick = true,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs
index 4ea3c036c1..3c1b68f9ef 100644
--- a/osu.Game/Users/ExtendedUserPanel.cs
+++ b/osu.Game/Users/ExtendedUserPanel.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Users
// Set status message based on activity (if we have one) and status is not offline
if (activity != null && !(status is UserStatusOffline))
{
- statusMessage.Text = activity.Status;
+ statusMessage.Text = activity.GetStatus();
statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint);
return;
}
diff --git a/osu.Game/Users/TournamentBanner.cs b/osu.Game/Users/TournamentBanner.cs
new file mode 100644
index 0000000000..62e1913412
--- /dev/null
+++ b/osu.Game/Users/TournamentBanner.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using Newtonsoft.Json;
+
+namespace osu.Game.Users
+{
+ public class TournamentBanner
+ {
+ [JsonProperty("id")]
+ public int Id;
+
+ [JsonProperty("tournament_id")]
+ public int TournamentId;
+
+ [JsonProperty("image")]
+ public string ImageLowRes = null!;
+
+ // TODO: remove when api returns @2x image link: https://github.com/ppy/osu-web/issues/9816
+ public string Image => $@"{Path.ChangeExtension(ImageLowRes, null)}@2x{Path.GetExtension(ImageLowRes)}";
+ }
+}
diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs
index 6de797ca3a..0b11d12c46 100644
--- a/osu.Game/Users/UserActivity.cs
+++ b/osu.Game/Users/UserActivity.cs
@@ -7,24 +7,31 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
using osuTK.Graphics;
namespace osu.Game.Users
{
public abstract class UserActivity
{
- public abstract string Status { get; }
+ public abstract string GetStatus(bool hideIdentifiableInformation = false);
+
public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker;
- public class Modding : UserActivity
+ public class ModdingBeatmap : EditingBeatmap
{
- public override string Status => "Modding a map";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark;
+
+ public ModdingBeatmap(IBeatmapInfo info)
+ : base(info)
+ {
+ }
}
public class ChoosingBeatmap : UserActivity
{
- public override string Status => "Choosing a beatmap";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Choosing a beatmap";
}
public abstract class InGame : UserActivity
@@ -39,7 +46,7 @@ namespace osu.Game.Users
Ruleset = ruleset;
}
- public override string Status => Ruleset.CreateInstance().PlayingVerb;
+ public override string GetStatus(bool hideIdentifiableInformation = false) => Ruleset.CreateInstance().PlayingVerb;
}
public class InMultiplayerGame : InGame
@@ -49,7 +56,7 @@ namespace osu.Game.Users
{
}
- public override string Status => $@"{base.Status} with others";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => $@"{base.GetStatus(hideIdentifiableInformation)} with others";
}
public class SpectatingMultiplayerGame : InGame
@@ -59,7 +66,7 @@ namespace osu.Game.Users
{
}
- public override string Status => $"Watching others {base.Status.ToLowerInvariant()}";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}";
}
public class InPlaylistGame : InGame
@@ -78,31 +85,62 @@ namespace osu.Game.Users
}
}
- public class Editing : UserActivity
+ public class TestingBeatmap : InGame
+ {
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap";
+
+ public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
+ : base(beatmapInfo, ruleset)
+ {
+ }
+ }
+
+ public class EditingBeatmap : UserActivity
{
public IBeatmapInfo BeatmapInfo { get; }
- public Editing(IBeatmapInfo info)
+ public EditingBeatmap(IBeatmapInfo info)
{
BeatmapInfo = info;
}
- public override string Status => @"Editing a beatmap";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap";
}
- public class Spectating : UserActivity
+ public class WatchingReplay : UserActivity
{
- public override string Status => @"Spectating a game";
+ private readonly ScoreInfo score;
+
+ protected string Username => score.User.Username;
+
+ public BeatmapInfo BeatmapInfo => score.BeatmapInfo;
+
+ public WatchingReplay(ScoreInfo score)
+ {
+ this.score = score;
+ }
+
+ public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {Username}'s replay";
+ }
+
+ public class SpectatingUser : WatchingReplay
+ {
+ public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {Username}";
+
+ public SpectatingUser(ScoreInfo score)
+ : base(score)
+ {
+ }
}
public class SearchingForLobby : UserActivity
{
- public override string Status => @"Looking for a lobby";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"Looking for a lobby";
}
public class InLobby : UserActivity
{
- public override string Status => @"In a lobby";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby";
public readonly Room Room;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index cdb3d9b66b..65ea301cbd 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,29 +18,29 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
-
+